@ferris1225/pi-subagents 2.0.3 → 2.2.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/README.md +37 -19
- package/agents/reviewer.md +7 -9
- package/package.json +1 -1
- package/src/announcements.ts +70 -70
- package/src/completion.ts +10 -2
- package/src/dispatch.ts +30 -997
- package/src/fixloop.ts +24 -21
- package/src/format.ts +174 -177
- package/src/index.ts +3 -2
- package/src/monitor.ts +19 -0
- package/src/prompt.ts +2 -2
- package/src/rpc-run.ts +1125 -1125
- package/src/thread-lifecycle.ts +1063 -0
- package/src/tools.ts +1 -0
- package/src/widget.ts +186 -144
- package/src/worktree.ts +687 -687
package/src/tools.ts
CHANGED
|
@@ -695,6 +695,7 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
695
695
|
agent: result.agent,
|
|
696
696
|
block: formatCompletionBlock(result, maxResultLines, result.projectCwd ?? ctx.cwd),
|
|
697
697
|
triggerTurn: true,
|
|
698
|
+
usage: result.usage,
|
|
698
699
|
})));
|
|
699
700
|
runtime.completionBatcher.flush();
|
|
700
701
|
}
|
package/src/widget.ts
CHANGED
|
@@ -1,144 +1,186 @@
|
|
|
1
|
-
/** Lightweight active-run widget for interactive Pi sessions. */
|
|
2
|
-
|
|
3
|
-
import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
5
|
-
import {
|
|
6
|
-
formatElapsed,
|
|
7
|
-
formatTaskSummary,
|
|
8
|
-
isRunActiveStatus,
|
|
9
|
-
monitor,
|
|
10
|
-
statusIcon,
|
|
11
|
-
type RunView,
|
|
12
|
-
} from "./monitor.ts";
|
|
13
|
-
|
|
14
|
-
export const SUBAGENTS_WIDGET_ID = "pi-subagents";
|
|
15
|
-
|
|
16
|
-
/** Keep the elapsed tail visible while clipping the descriptive left side. */
|
|
17
|
-
function compactLine(left: string, right: string, width: number): string {
|
|
18
|
-
if (width <= 0) return "";
|
|
19
|
-
if (!right) return truncateToWidth(left, width, "");
|
|
20
|
-
const separator = " ";
|
|
21
|
-
const rightWidth = visibleWidth(right);
|
|
22
|
-
if (rightWidth >= width) return truncateToWidth(right, width, "");
|
|
23
|
-
const leftWidth = width - rightWidth - visibleWidth(separator);
|
|
24
|
-
if (leftWidth <= 0) return truncateToWidth(right, width, "");
|
|
25
|
-
return `${truncateToWidth(left, leftWidth, "…")}${separator}${right}`;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** One compact primary line per genuinely active run, plus an optional indented
|
|
29
|
-
* activity line. The primary line reserves effective model/thinking and elapsed
|
|
30
|
-
* width before truncating the task. Settled and parked threads never appear, so
|
|
31
|
-
* elapsed time cannot keep ticking beside a terminal status. */
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
theme: Theme,
|
|
35
|
-
width: number,
|
|
36
|
-
now: number
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
1
|
+
/** Lightweight active-run widget for interactive Pi sessions. */
|
|
2
|
+
|
|
3
|
+
import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
5
|
+
import {
|
|
6
|
+
formatElapsed,
|
|
7
|
+
formatTaskSummary,
|
|
8
|
+
isRunActiveStatus,
|
|
9
|
+
monitor,
|
|
10
|
+
statusIcon,
|
|
11
|
+
type RunView,
|
|
12
|
+
} from "./monitor.ts";
|
|
13
|
+
|
|
14
|
+
export const SUBAGENTS_WIDGET_ID = "pi-subagents";
|
|
15
|
+
|
|
16
|
+
/** Keep the elapsed tail visible while clipping the descriptive left side. */
|
|
17
|
+
function compactLine(left: string, right: string, width: number): string {
|
|
18
|
+
if (width <= 0) return "";
|
|
19
|
+
if (!right) return truncateToWidth(left, width, "");
|
|
20
|
+
const separator = " ";
|
|
21
|
+
const rightWidth = visibleWidth(right);
|
|
22
|
+
if (rightWidth >= width) return truncateToWidth(right, width, "");
|
|
23
|
+
const leftWidth = width - rightWidth - visibleWidth(separator);
|
|
24
|
+
if (leftWidth <= 0) return truncateToWidth(right, width, "");
|
|
25
|
+
return `${truncateToWidth(left, leftWidth, "…")}${separator}${right}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** One compact primary line per genuinely active run, plus an optional indented
|
|
29
|
+
* activity line. The primary line reserves effective model/thinking and elapsed
|
|
30
|
+
* width before truncating the task. Settled and parked threads never appear, so
|
|
31
|
+
* elapsed time cannot keep ticking beside a terminal status. */
|
|
32
|
+
function runPrimaryLine(
|
|
33
|
+
run: RunView,
|
|
34
|
+
theme: Theme,
|
|
35
|
+
width: number,
|
|
36
|
+
now: number,
|
|
37
|
+
prefix: string,
|
|
38
|
+
): string {
|
|
39
|
+
const dim = (text: string): string => theme.fg("dim", text);
|
|
40
|
+
const icon = statusIcon(run.status, theme);
|
|
41
|
+
const name = theme.fg("accent", theme.bold(run.agent));
|
|
42
|
+
const identity = `${prefix}${icon} ${name}`;
|
|
43
|
+
const elapsed = formatElapsed(run, now);
|
|
44
|
+
// Render only the resolved model id plus thinking level. Provider auth and
|
|
45
|
+
// other configuration never enter monitor state or this line.
|
|
46
|
+
const modelId = run.model?.split("/").at(-1);
|
|
47
|
+
const modelSource = formatTaskSummary(
|
|
48
|
+
modelId ? `${modelId}${run.thinking ? `/${run.thinking}` : ""}` : run.thinking ? `thinking:${run.thinking}` : "",
|
|
49
|
+
64,
|
|
50
|
+
false,
|
|
51
|
+
);
|
|
52
|
+
// A chain child shows its role in the chain plus a task-derived label; the
|
|
53
|
+
// templated fix brief itself would only repeat the parent review's content.
|
|
54
|
+
const taskSource = run.parentRunId !== undefined
|
|
55
|
+
? [run.relationLabel, run.label].filter((part): part is string => Boolean(part)).join(" · ")
|
|
56
|
+
: formatTaskSummary(run.task, 64);
|
|
57
|
+
const primaryPartCount = 2 + (modelSource ? 1 : 0) + (elapsed ? 1 : 0);
|
|
58
|
+
const contentWidth = Math.max(
|
|
59
|
+
0,
|
|
60
|
+
width -
|
|
61
|
+
visibleWidth(identity) -
|
|
62
|
+
visibleWidth(elapsed) -
|
|
63
|
+
(primaryPartCount - 1) * visibleWidth(" · "),
|
|
64
|
+
);
|
|
65
|
+
const modelDesired = visibleWidth(modelSource);
|
|
66
|
+
const modelFloor = Math.min(modelDesired, Math.min(12, contentWidth));
|
|
67
|
+
let modelWidth = modelSource
|
|
68
|
+
? Math.min(modelDesired, Math.max(modelFloor, contentWidth - 8))
|
|
69
|
+
: 0;
|
|
70
|
+
let taskWidth = contentWidth - modelWidth;
|
|
71
|
+
if (visibleWidth(taskSource) < taskWidth) {
|
|
72
|
+
modelWidth = Math.min(modelDesired, modelWidth + taskWidth - visibleWidth(taskSource));
|
|
73
|
+
taskWidth = contentWidth - modelWidth;
|
|
74
|
+
}
|
|
75
|
+
const task = taskWidth > 0 ? formatTaskSummary(taskSource, taskWidth, run.parentRunId === undefined) : "";
|
|
76
|
+
const modelThinking = modelWidth > 0 ? formatTaskSummary(modelSource, modelWidth, false) : "";
|
|
77
|
+
const primaryLeft = [
|
|
78
|
+
identity,
|
|
79
|
+
task ? dim(task) : undefined,
|
|
80
|
+
modelThinking ? dim(modelThinking) : undefined,
|
|
81
|
+
].filter((part): part is string => Boolean(part)).join(" · ");
|
|
82
|
+
return compactLine(primaryLeft, elapsed ? dim(`· ${elapsed}`) : "", width);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function runActivityLine(run: RunView, theme: Theme, width: number, indent: string): string[] {
|
|
86
|
+
const dim = (text: string): string => theme.fg("dim", text);
|
|
87
|
+
const activity = run.activity?.trim();
|
|
88
|
+
if (!activity) return [];
|
|
89
|
+
const activityWidth = width - visibleWidth(indent);
|
|
90
|
+
if (activityWidth <= 0) return [];
|
|
91
|
+
const activitySummary = formatTaskSummary(activity, activityWidth);
|
|
92
|
+
if (!activitySummary) return [];
|
|
93
|
+
return [truncateToWidth(`${indent}${dim(activitySummary)}`, width, "")];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Render active runs as a tree: main-agent dispatches are roots, auto-fix chain
|
|
97
|
+
* rounds nest under the triggering reviewer row that owns the chain. No run ids
|
|
98
|
+
* appear here — the tree and the task label say what each row is, and ids stay
|
|
99
|
+
* available through subagent_status when a thread must be controlled. */
|
|
100
|
+
export function formatActiveRunLines(
|
|
101
|
+
runs: readonly RunView[],
|
|
102
|
+
theme: Theme,
|
|
103
|
+
width: number,
|
|
104
|
+
now: number = Date.now(),
|
|
105
|
+
): string[] {
|
|
106
|
+
const active = runs.filter((run) => isRunActiveStatus(run.status));
|
|
107
|
+
const activeIds = new Set(active.map((run) => run.id));
|
|
108
|
+
const childrenOf = new Map<number, RunView[]>();
|
|
109
|
+
const roots: RunView[] = [];
|
|
110
|
+
for (const run of active) {
|
|
111
|
+
if (run.parentRunId !== undefined && activeIds.has(run.parentRunId)) {
|
|
112
|
+
const siblings = childrenOf.get(run.parentRunId);
|
|
113
|
+
if (siblings) siblings.push(run);
|
|
114
|
+
else childrenOf.set(run.parentRunId, [run]);
|
|
115
|
+
} else {
|
|
116
|
+
roots.push(run);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const lines: string[] = [];
|
|
120
|
+
for (const root of roots) {
|
|
121
|
+
const children = childrenOf.get(root.id) ?? [];
|
|
122
|
+
lines.push(runPrimaryLine(root, theme, width, now, ""));
|
|
123
|
+
// The parent's "auto-fix chain running" placeholder is redundant while its
|
|
124
|
+
// child rows show live progress; keep it only between rounds.
|
|
125
|
+
if (children.length === 0) lines.push(...runActivityLine(root, theme, width, " "));
|
|
126
|
+
children.forEach((child, index) => {
|
|
127
|
+
const connector = index === children.length - 1 ? "└ " : "├ ";
|
|
128
|
+
lines.push(runPrimaryLine(child, theme, width, now, theme.fg("dim", ` ${connector}`)));
|
|
129
|
+
lines.push(...runActivityLine(child, theme, width, " "));
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return lines;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function hasTickingRun(): boolean {
|
|
136
|
+
return monitor.getRuns().some(
|
|
137
|
+
(run) => isRunActiveStatus(run.status) && run.startedAt !== undefined,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Install the widget for one TUI session. Its timer exists only while at least
|
|
142
|
+
* one active run has started and is disposed with the widget. */
|
|
143
|
+
export function installActiveRunsWidget(ctx: Pick<ExtensionContext, "mode" | "ui">): void {
|
|
144
|
+
if (ctx.mode !== "tui") return;
|
|
145
|
+
ctx.ui.setWidget(
|
|
146
|
+
SUBAGENTS_WIDGET_ID,
|
|
147
|
+
(tui, theme) => {
|
|
148
|
+
let timer: ReturnType<typeof setInterval> | undefined;
|
|
149
|
+
let disposed = false;
|
|
150
|
+
|
|
151
|
+
const syncTimer = (): void => {
|
|
152
|
+
if (disposed) return;
|
|
153
|
+
if (hasTickingRun()) {
|
|
154
|
+
if (timer) return;
|
|
155
|
+
timer = setInterval(() => tui.requestRender(), 1_000);
|
|
156
|
+
timer.unref?.();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (timer) clearInterval(timer);
|
|
160
|
+
timer = undefined;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const unsubscribe = monitor.subscribe(() => {
|
|
164
|
+
syncTimer();
|
|
165
|
+
tui.requestRender();
|
|
166
|
+
});
|
|
167
|
+
syncTimer();
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
render: (width: number) => formatActiveRunLines(monitor.getRuns(), theme, width),
|
|
171
|
+
invalidate() {},
|
|
172
|
+
dispose() {
|
|
173
|
+
disposed = true;
|
|
174
|
+
unsubscribe();
|
|
175
|
+
if (timer) clearInterval(timer);
|
|
176
|
+
timer = undefined;
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
},
|
|
180
|
+
{ placement: "aboveEditor" },
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function clearActiveRunsWidget(ctx: Pick<ExtensionContext, "mode" | "ui">): void {
|
|
185
|
+
if (ctx.mode === "tui") ctx.ui.setWidget(SUBAGENTS_WIDGET_ID, undefined);
|
|
186
|
+
}
|