@ferris1225/pi-subagents 0.32.2 → 1.0.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 +35 -16
- package/package.json +2 -2
- package/src/announcements.ts +59 -0
- package/src/dispatch.ts +1833 -1878
- package/src/fixloop.ts +1 -1
- package/src/format.ts +2 -1
- package/src/index.ts +4 -8
- package/src/models.ts +23 -35
- package/src/monitor.ts +29 -88
- package/src/rpc-run.ts +1 -26
- package/src/runtime.ts +284 -285
- package/src/setup.ts +4 -4
- package/src/spawn.ts +1 -6
- package/src/tools.ts +730 -748
- package/src/trajectory.ts +16 -207
- package/src/inspector-panel.ts +0 -363
- package/src/inspector.ts +0 -369
- package/src/widget.ts +0 -195
package/src/inspector.ts
DELETED
|
@@ -1,369 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure view-model builder for the /subagents-inspect overlay.
|
|
3
|
-
*
|
|
4
|
-
* Joins three read-only sources into one snapshot:
|
|
5
|
-
* - monitor (live widget rows: status/usage/activity),
|
|
6
|
-
* - runtime.threads (thread state + control phase), and
|
|
7
|
-
* - inspectorStore (append-only trajectory + bounded transcript + retained
|
|
8
|
-
* snapshots for finished/parked threads whose monitor rows are gone).
|
|
9
|
-
*
|
|
10
|
-
* The overlay component renders this snapshot directly and never mutates
|
|
11
|
-
* runtime state; building it must therefore be side-effect free.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { stripVTControlCharacters } from "node:util";
|
|
15
|
-
import { formatUsageCompact, monitor, type RunStatus } from "./monitor.ts";
|
|
16
|
-
import { emptyUsage, type UsageStats } from "./rpc-run.ts";
|
|
17
|
-
import type { SubagentRuntime, ThreadState } from "./runtime.ts";
|
|
18
|
-
import { inspectorStore, type TrajectoryEvent } from "./trajectory.ts";
|
|
19
|
-
|
|
20
|
-
export interface InspectorRunItem {
|
|
21
|
-
id: number;
|
|
22
|
-
agent: string;
|
|
23
|
-
label: string;
|
|
24
|
-
status: RunStatus;
|
|
25
|
-
/** Thread state text ("running", "parked", "completed", ...) or the monitor
|
|
26
|
-
* status label for threads without a control record (chain internals). */
|
|
27
|
-
stateText: string;
|
|
28
|
-
generation?: number;
|
|
29
|
-
elapsedMs?: number;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface InspectorToolEntry {
|
|
33
|
-
tool: string;
|
|
34
|
-
summary?: string;
|
|
35
|
-
isError?: boolean;
|
|
36
|
-
running?: boolean;
|
|
37
|
-
at: number;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface InspectorThreadView {
|
|
41
|
-
id: number;
|
|
42
|
-
agent: string;
|
|
43
|
-
label: string;
|
|
44
|
-
task: string;
|
|
45
|
-
status: RunStatus;
|
|
46
|
-
stateText: string;
|
|
47
|
-
/** Control phase of the current attempt (queued/starting/running/...). */
|
|
48
|
-
phase?: string;
|
|
49
|
-
generation?: number;
|
|
50
|
-
elapsedMs?: number;
|
|
51
|
-
startedAt?: number;
|
|
52
|
-
endedAt?: number;
|
|
53
|
-
model?: string;
|
|
54
|
-
/** Ordered candidate refs from the dispatch record (primary first). */
|
|
55
|
-
modelChain: string[];
|
|
56
|
-
modelFallbackFrom?: string;
|
|
57
|
-
thinking?: string;
|
|
58
|
-
isolation?: "shared" | "worktree";
|
|
59
|
-
integrationStatus?: string;
|
|
60
|
-
integrationApplied?: boolean;
|
|
61
|
-
originalCwd?: string;
|
|
62
|
-
isolationCwd?: string;
|
|
63
|
-
integrationWorktreePath?: string;
|
|
64
|
-
integrationPatchPath?: string;
|
|
65
|
-
integrationError?: string;
|
|
66
|
-
forkedFromRunId?: number;
|
|
67
|
-
forkChildRunIds: number[];
|
|
68
|
-
usage: UsageStats;
|
|
69
|
-
toolCount: number;
|
|
70
|
-
activity?: string;
|
|
71
|
-
currentTool?: string;
|
|
72
|
-
tools: InspectorToolEntry[];
|
|
73
|
-
trajectory: readonly TrajectoryEvent[];
|
|
74
|
-
trajectoryTotal: number;
|
|
75
|
-
transcript: { text: string; textTruncated: boolean; thinking: string; thinkingTruncated: boolean };
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export interface InspectorSnapshot {
|
|
79
|
-
now: number;
|
|
80
|
-
items: InspectorRunItem[];
|
|
81
|
-
detail?: InspectorThreadView;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const TOOL_LIMIT = 6;
|
|
85
|
-
const TRAJECTORY_LIMIT = 10;
|
|
86
|
-
|
|
87
|
-
function monitorRunsSafe(): ReturnType<typeof monitor.getRuns> {
|
|
88
|
-
try {
|
|
89
|
-
return monitor.getRuns();
|
|
90
|
-
} catch {
|
|
91
|
-
return [];
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function threadStatusToRunStatus(state: ThreadState): RunStatus {
|
|
96
|
-
switch (state) {
|
|
97
|
-
case "queued":
|
|
98
|
-
case "resuming":
|
|
99
|
-
return "queued";
|
|
100
|
-
case "running":
|
|
101
|
-
return "running";
|
|
102
|
-
case "steering":
|
|
103
|
-
return "steering";
|
|
104
|
-
case "interrupting":
|
|
105
|
-
return "interrupting";
|
|
106
|
-
case "parked":
|
|
107
|
-
return "parked";
|
|
108
|
-
case "completed":
|
|
109
|
-
return "done";
|
|
110
|
-
case "failed":
|
|
111
|
-
case "stopped":
|
|
112
|
-
return "failed";
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export function buildInspectorSnapshot(options: {
|
|
117
|
-
runtime: Pick<SubagentRuntime, "threads">;
|
|
118
|
-
selectedId?: number;
|
|
119
|
-
now?: number;
|
|
120
|
-
toolLimit?: number;
|
|
121
|
-
trajectoryLimit?: number;
|
|
122
|
-
}): InspectorSnapshot {
|
|
123
|
-
const now = options.now ?? Date.now();
|
|
124
|
-
const toolLimit = options.toolLimit ?? TOOL_LIMIT;
|
|
125
|
-
const trajectoryLimit = options.trajectoryLimit ?? TRAJECTORY_LIMIT;
|
|
126
|
-
const monitorRuns = monitorRunsSafe();
|
|
127
|
-
|
|
128
|
-
// Union of the three sources keyed by id.
|
|
129
|
-
const ids = new Set<number>();
|
|
130
|
-
for (const id of options.runtime.threads.keys()) ids.add(id);
|
|
131
|
-
for (const state of inspectorStore.all()) ids.add(state.runId);
|
|
132
|
-
for (const run of monitorRuns) ids.add(run.id);
|
|
133
|
-
|
|
134
|
-
const items: InspectorRunItem[] = [];
|
|
135
|
-
let detail: InspectorThreadView | undefined;
|
|
136
|
-
|
|
137
|
-
for (const id of [...ids].sort((a, b) => a - b)) {
|
|
138
|
-
const monitorRun = monitorRuns.find((r) => r.id === id);
|
|
139
|
-
const thread = options.runtime.threads.get(id);
|
|
140
|
-
const inspect = inspectorStore.find(id);
|
|
141
|
-
|
|
142
|
-
const latestSettled = [...(inspect?.trajectory.getGenerationEvents() ?? [])]
|
|
143
|
-
.reverse()
|
|
144
|
-
.find((event): event is Extract<TrajectoryEvent, { kind: "settled" }> => event.kind === "settled");
|
|
145
|
-
const trajectoryStatus = latestSettled
|
|
146
|
-
? latestSettled.status === "done" ? "done" : "failed"
|
|
147
|
-
: undefined;
|
|
148
|
-
const agent = monitorRun?.agent ?? thread?.agentName ?? inspect?.agent ?? "?";
|
|
149
|
-
const label = monitorRun?.label ?? inspect?.label ?? "";
|
|
150
|
-
const task = monitorRun?.task ?? thread?.task ?? inspect?.task ?? "";
|
|
151
|
-
const status: RunStatus = monitorRun?.status ?? (thread
|
|
152
|
-
? threadStatusToRunStatus(thread.state)
|
|
153
|
-
: trajectoryStatus ?? statusFromText(inspect?.status));
|
|
154
|
-
const stateText = thread?.state ?? monitorRun?.status ?? latestSettled?.status ?? inspect?.status ?? "queued";
|
|
155
|
-
const startedAt = monitorRun?.startedAt ?? inspect?.startedAt;
|
|
156
|
-
const endedAt = monitorRun?.endedAt ?? inspect?.endedAt ?? inspect?.trajectory.summary().endedAt;
|
|
157
|
-
const elapsedMs = startedAt === undefined ? undefined : Math.max(0, (endedAt ?? now) - startedAt);
|
|
158
|
-
|
|
159
|
-
items.push({ id, agent, label, status, stateText, generation: thread?.generation ?? inspect?.generation, elapsedMs });
|
|
160
|
-
|
|
161
|
-
if (options.selectedId === id) {
|
|
162
|
-
detail = buildDetail({
|
|
163
|
-
id,
|
|
164
|
-
agent,
|
|
165
|
-
label,
|
|
166
|
-
task,
|
|
167
|
-
status,
|
|
168
|
-
stateText,
|
|
169
|
-
startedAt,
|
|
170
|
-
endedAt,
|
|
171
|
-
elapsedMs,
|
|
172
|
-
monitorRun,
|
|
173
|
-
thread,
|
|
174
|
-
inspect,
|
|
175
|
-
toolLimit,
|
|
176
|
-
trajectoryLimit,
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
return { now, items, detail };
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function statusFromText(text: string | undefined): RunStatus {
|
|
185
|
-
switch (text) {
|
|
186
|
-
case "done":
|
|
187
|
-
case "completed":
|
|
188
|
-
return "done";
|
|
189
|
-
case "failed":
|
|
190
|
-
case "stopped":
|
|
191
|
-
return "failed";
|
|
192
|
-
case "running":
|
|
193
|
-
return "running";
|
|
194
|
-
case "steering":
|
|
195
|
-
return "steering";
|
|
196
|
-
case "interrupting":
|
|
197
|
-
return "interrupting";
|
|
198
|
-
case "parked":
|
|
199
|
-
return "parked";
|
|
200
|
-
default:
|
|
201
|
-
return "queued";
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function buildDetail(input: {
|
|
206
|
-
id: number;
|
|
207
|
-
agent: string;
|
|
208
|
-
label: string;
|
|
209
|
-
task: string;
|
|
210
|
-
status: RunStatus;
|
|
211
|
-
stateText: string;
|
|
212
|
-
startedAt?: number;
|
|
213
|
-
endedAt?: number;
|
|
214
|
-
elapsedMs?: number;
|
|
215
|
-
monitorRun?: ReturnType<typeof monitor.getRuns>[number];
|
|
216
|
-
thread?: {
|
|
217
|
-
state: ThreadState;
|
|
218
|
-
generation: number;
|
|
219
|
-
control: { getPhase(): string };
|
|
220
|
-
isolation: "shared" | "worktree";
|
|
221
|
-
cwd: string;
|
|
222
|
-
executionCwd: string;
|
|
223
|
-
worktree?: { worktreePath: string; patchPath: string; state: string };
|
|
224
|
-
forkedFromRunId?: number;
|
|
225
|
-
forkChildRunIds: number[];
|
|
226
|
-
};
|
|
227
|
-
inspect?: ReturnType<typeof inspectorStore.find>;
|
|
228
|
-
toolLimit: number;
|
|
229
|
-
trajectoryLimit: number;
|
|
230
|
-
}): InspectorThreadView {
|
|
231
|
-
const { monitorRun, thread, inspect } = input;
|
|
232
|
-
const genEvents = inspect?.trajectory.getGenerationEvents() ?? [];
|
|
233
|
-
const allEvents = inspect?.trajectory.getEvents() ?? [];
|
|
234
|
-
|
|
235
|
-
// Model chain: primary first, then the recorded pool fallbacks. The actual
|
|
236
|
-
// model currently running is the trajectory summary's latest, or the live
|
|
237
|
-
// monitor model. modelFallbackFrom marks a pool advancement.
|
|
238
|
-
const dispatchEvent = [...genEvents].reverse().find((e): e is Extract<TrajectoryEvent, { kind: "dispatch" }> => e.kind === "dispatch");
|
|
239
|
-
const chain: string[] = [];
|
|
240
|
-
if (dispatchEvent?.model) chain.push(dispatchEvent.model);
|
|
241
|
-
for (const ref of dispatchEvent?.pool ?? []) if (ref && !chain.includes(ref)) chain.push(ref);
|
|
242
|
-
const model = monitorRun?.model ?? inspect?.trajectory.summary().model ?? inspect?.model;
|
|
243
|
-
const fallbackFrom = monitorRun?.modelFallbackFrom ?? inspect?.trajectory.summary().modelFallbackFrom;
|
|
244
|
-
if (model && !chain.includes(model)) chain.push(model);
|
|
245
|
-
|
|
246
|
-
// Recent tool entries from the current-generation trajectory.
|
|
247
|
-
const tools: InspectorToolEntry[] = [];
|
|
248
|
-
for (let i = genEvents.length - 1; i >= 0 && tools.length < input.toolLimit; i--) {
|
|
249
|
-
const e = genEvents[i];
|
|
250
|
-
if (e.kind === "tool_start") {
|
|
251
|
-
// Correlate parallel calls by Pi's stable toolCallId. Name matching is
|
|
252
|
-
// retained only for legacy events that predate id capture.
|
|
253
|
-
const ended = genEvents.slice(i + 1).find((later) =>
|
|
254
|
-
later.kind === "tool_end" &&
|
|
255
|
-
(e.toolCallId
|
|
256
|
-
? later.toolCallId === e.toolCallId
|
|
257
|
-
: later.toolCallId === undefined && later.tool === e.tool),
|
|
258
|
-
);
|
|
259
|
-
tools.unshift({
|
|
260
|
-
tool: e.tool,
|
|
261
|
-
summary: e.summary || undefined,
|
|
262
|
-
isError: ended && ended.kind === "tool_end" ? ended.isError : undefined,
|
|
263
|
-
running: ended === undefined,
|
|
264
|
-
at: e.at,
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const trajectory = genEvents.slice(-input.trajectoryLimit);
|
|
270
|
-
const usage = monitorRun?.usage ?? inspect?.runInfo?.usage ?? emptyUsage();
|
|
271
|
-
const toolCount = monitorRun?.toolCount ?? inspect?.runInfo?.toolCount ?? inspect?.trajectory.summary().toolCount ?? 0;
|
|
272
|
-
const trajectorySummary = inspect?.trajectory.summary();
|
|
273
|
-
const lastWorktreeEvent = [...allEvents]
|
|
274
|
-
.reverse()
|
|
275
|
-
.find((event): event is Extract<TrajectoryEvent, { kind: "worktree" }> => event.kind === "worktree");
|
|
276
|
-
const isolation = thread?.isolation ?? monitorRun?.isolation ?? trajectorySummary?.isolation;
|
|
277
|
-
|
|
278
|
-
return {
|
|
279
|
-
id: input.id,
|
|
280
|
-
agent: input.agent,
|
|
281
|
-
label: input.label,
|
|
282
|
-
task: input.task,
|
|
283
|
-
status: input.status,
|
|
284
|
-
stateText: input.stateText,
|
|
285
|
-
phase: thread?.control.getPhase(),
|
|
286
|
-
generation: thread?.generation ?? (genEvents.length > 0 ? inspect?.generation : undefined),
|
|
287
|
-
elapsedMs: input.elapsedMs,
|
|
288
|
-
startedAt: input.startedAt,
|
|
289
|
-
endedAt: input.endedAt,
|
|
290
|
-
model: model ?? undefined,
|
|
291
|
-
modelChain: chain,
|
|
292
|
-
modelFallbackFrom: fallbackFrom,
|
|
293
|
-
thinking: monitorRun?.thinking ?? trajectorySummary?.thinking ?? inspect?.thinking,
|
|
294
|
-
isolation,
|
|
295
|
-
integrationStatus: monitorRun?.integrationStatus ?? trajectorySummary?.integrationStatus ?? thread?.worktree?.state,
|
|
296
|
-
integrationApplied: lastWorktreeEvent?.integrated,
|
|
297
|
-
originalCwd: thread?.cwd ?? trajectorySummary?.originalCwd,
|
|
298
|
-
isolationCwd: thread?.executionCwd ?? trajectorySummary?.isolationCwd,
|
|
299
|
-
integrationWorktreePath: lastWorktreeEvent?.worktreePath ?? (thread?.worktree?.state === "retained" ? thread.worktree.worktreePath : undefined),
|
|
300
|
-
integrationPatchPath: lastWorktreeEvent?.patchPath ?? (thread?.worktree?.state === "retained" ? thread.worktree.patchPath : undefined),
|
|
301
|
-
integrationError: lastWorktreeEvent?.error,
|
|
302
|
-
forkedFromRunId: thread?.forkedFromRunId ?? trajectorySummary?.forkedFromRunId,
|
|
303
|
-
forkChildRunIds: [...(thread?.forkChildRunIds ?? trajectorySummary?.forkChildRunIds ?? [])],
|
|
304
|
-
usage,
|
|
305
|
-
toolCount,
|
|
306
|
-
activity: monitorRun?.activity ?? inspect?.runInfo?.activity ?? inspect?.trajectory.summary().activity,
|
|
307
|
-
currentTool: monitorRun?.currentTool ?? inspect?.runInfo?.currentTool ?? inspect?.trajectory.summary().currentTool,
|
|
308
|
-
tools,
|
|
309
|
-
trajectory,
|
|
310
|
-
trajectoryTotal: allEvents.length,
|
|
311
|
-
transcript: inspect?.transcript.snapshot() ?? { text: "", textTruncated: false, thinking: "", thinkingTruncated: false },
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
/** One-line event text for the trajectory pane (plain, pre-theme). */
|
|
316
|
-
export function formatTrajectoryEvent(event: TrajectoryEvent): string {
|
|
317
|
-
return stripVTControlCharacters(formatTrajectoryEventUnsafe(event));
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
function formatTrajectoryEventUnsafe(event: TrajectoryEvent): string {
|
|
321
|
-
switch (event.kind) {
|
|
322
|
-
case "dispatch":
|
|
323
|
-
return `dispatch ${event.agent}${event.resumed ? " (resumed)" : ""}${event.model ? ` · ${event.model}` : ""}`;
|
|
324
|
-
case "status":
|
|
325
|
-
return `status: ${event.status}`;
|
|
326
|
-
case "candidate":
|
|
327
|
-
return event.fallbackFrom ? `model: ${event.model ?? "?"} (pool fallback from ${event.fallbackFrom})` : `model: ${event.model ?? "?"}`;
|
|
328
|
-
case "retry":
|
|
329
|
-
return `retry${event.reason ? `: ${event.reason}` : ""}`;
|
|
330
|
-
case "steer":
|
|
331
|
-
return `steer: ${compactOne(event.instruction, 60)}`;
|
|
332
|
-
case "retarget":
|
|
333
|
-
return `retarget: ${compactOne(event.objective, 60)}`;
|
|
334
|
-
case "park":
|
|
335
|
-
return "parked at checkpoint";
|
|
336
|
-
case "resume":
|
|
337
|
-
return event.objective ? `resume: ${compactOne(event.objective, 60)}` : "resume from retained context";
|
|
338
|
-
case "fork":
|
|
339
|
-
return event.runId === event.sourceRunId
|
|
340
|
-
? `forked child #${event.childRunId}${event.objective ? `: ${compactOne(event.objective, 48)}` : ""}`
|
|
341
|
-
: `forked from #${event.sourceRunId}${event.objective ? `: ${compactOne(event.objective, 48)}` : ""}`;
|
|
342
|
-
case "stop":
|
|
343
|
-
return event.reason ? `stop: ${compactOne(event.reason, 60)}` : "stopped";
|
|
344
|
-
case "worktree":
|
|
345
|
-
return event.status === "created"
|
|
346
|
-
? `worktree created${event.isolationCwd ? ` · ${compactOne(event.isolationCwd, 48)}` : ""}`
|
|
347
|
-
: event.status === "retained" && event.integrated
|
|
348
|
-
? `worktree cleanup failed after apply${event.error ? `: ${compactOne(event.error, 48)}` : ""}`
|
|
349
|
-
: `worktree ${event.status}${event.error ? `: ${compactOne(event.error, 48)}` : ""}`;
|
|
350
|
-
case "settled":
|
|
351
|
-
return `settled: ${event.status}${event.model ? ` · ${event.model}` : ""}${event.integrationStatus ? ` · worktree ${event.integrationStatus}` : ""}`;
|
|
352
|
-
case "tool_start":
|
|
353
|
-
return `→ ${event.tool}${event.summary ? ` ${compactOne(event.summary, 48)}` : ""}`;
|
|
354
|
-
case "tool_end":
|
|
355
|
-
return `${e2(event)}${event.tool}`;
|
|
356
|
-
case "usage":
|
|
357
|
-
return `usage: ${formatUsageCompact(event.usage) || "—"}`;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
function e2(event: Extract<TrajectoryEvent, { kind: "tool_end" }>): string {
|
|
362
|
-
return event.isError ? "✗ " : "✓ ";
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
function compactOne(text: string, max: number): string {
|
|
366
|
-
const oneLine = stripVTControlCharacters(text).replace(/\s+/g, " ").trim();
|
|
367
|
-
const chars = [...oneLine];
|
|
368
|
-
return chars.length > max ? `${chars.slice(0, max - 1).join("")}…` : oneLine;
|
|
369
|
-
}
|
package/src/widget.ts
DELETED
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* session_start wiring: the persistent status widget above the editor, plus
|
|
3
|
-
* one-time feature announcements (a new configurable option is surfaced to the
|
|
4
|
-
* user once after an update; the marker persists in `announcedFeatures`).
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { stat } from "node:fs/promises";
|
|
8
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
9
|
-
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
10
|
-
import { loadConfig, saveConfig } from "./config.ts";
|
|
11
|
-
import {
|
|
12
|
-
activityStateLabel,
|
|
13
|
-
compactLine,
|
|
14
|
-
deriveActivityState,
|
|
15
|
-
formatElapsed,
|
|
16
|
-
formatUsageCompact,
|
|
17
|
-
isRunActiveStatus,
|
|
18
|
-
monitor,
|
|
19
|
-
statusIcon,
|
|
20
|
-
statusLabel,
|
|
21
|
-
} from "./monitor.ts";
|
|
22
|
-
import { announceRecoveryRecords } from "./recovery.ts";
|
|
23
|
-
import type { SubagentRuntime } from "./runtime.ts";
|
|
24
|
-
|
|
25
|
-
/** Features whose one-time announcement is still pending (keyed by config
|
|
26
|
-
* `announcedFeatures` entry). When the feature's precondition is unmet and the
|
|
27
|
-
* marker is absent, the user is told about it exactly once. */
|
|
28
|
-
const ANNOUNCEMENTS: Array<{
|
|
29
|
-
key: string;
|
|
30
|
-
condition: (config: Awaited<ReturnType<typeof loadConfig>>) => boolean;
|
|
31
|
-
message: string;
|
|
32
|
-
}> = [
|
|
33
|
-
{
|
|
34
|
-
key: "visionModel",
|
|
35
|
-
condition: (config) => config.visionModel === undefined,
|
|
36
|
-
message:
|
|
37
|
-
"pi-subagents: new — a vision-capable model can now handle image tasks (screenshots, mockups, designs). Run /subagents-setup to configure it; until set, vision tasks use the main session's current model.",
|
|
38
|
-
},
|
|
39
|
-
];
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* One-time feature announcements: when an update introduces a new configurable
|
|
43
|
-
* feature, tell the user once (the marker persists in announcedFeatures) so they
|
|
44
|
-
* know it exists — e.g. the vision model, which is unset by default. Only runs
|
|
45
|
-
* when a config file already exists: on a fresh install there is nothing to
|
|
46
|
-
* announce (and writing the file here would make /subagents-setup skip its
|
|
47
|
-
* first-time wizard). A failed announcement must never break session startup.
|
|
48
|
-
*/
|
|
49
|
-
async function announceNewFeatures(
|
|
50
|
-
ctx: { ui: { notify: (message: string, kind: "info" | "warning" | "error") => void } },
|
|
51
|
-
runtime: SubagentRuntime,
|
|
52
|
-
): Promise<void> {
|
|
53
|
-
try {
|
|
54
|
-
let configExists = true;
|
|
55
|
-
try {
|
|
56
|
-
await stat(runtime.configPath);
|
|
57
|
-
} catch {
|
|
58
|
-
configExists = false;
|
|
59
|
-
}
|
|
60
|
-
if (!configExists) return;
|
|
61
|
-
|
|
62
|
-
const config = await loadConfig(runtime.configPath);
|
|
63
|
-
const pending = ANNOUNCEMENTS.filter(
|
|
64
|
-
(announcement) =>
|
|
65
|
-
announcement.condition(config) && !config.announcedFeatures.includes(announcement.key),
|
|
66
|
-
);
|
|
67
|
-
if (pending.length === 0) return;
|
|
68
|
-
await saveConfig(
|
|
69
|
-
{
|
|
70
|
-
...config,
|
|
71
|
-
announcedFeatures: [...config.announcedFeatures, ...pending.map((a) => a.key)],
|
|
72
|
-
},
|
|
73
|
-
runtime.configPath,
|
|
74
|
-
);
|
|
75
|
-
for (const announcement of pending) {
|
|
76
|
-
ctx.ui.notify(announcement.message, "info");
|
|
77
|
-
}
|
|
78
|
-
} catch {
|
|
79
|
-
/* announcement failures are non-fatal */
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function registerWidget(pi: ExtensionAPI, runtime: SubagentRuntime): void {
|
|
84
|
-
pi.on("session_start", async (_e, ctx) => {
|
|
85
|
-
// Recovery paths survive the old runtime and are shown again in the next
|
|
86
|
-
// UI-capable session before any transient widget state is rebuilt.
|
|
87
|
-
await announceRecoveryRecords(runtime.configPath, ctx);
|
|
88
|
-
if (ctx.mode !== "tui") return;
|
|
89
|
-
await announceNewFeatures(ctx, runtime);
|
|
90
|
-
|
|
91
|
-
ctx.ui.setWidget(
|
|
92
|
-
"pi-subagents",
|
|
93
|
-
(tui, theme) => {
|
|
94
|
-
const unsub = monitor.subscribe(() => tui.requestRender());
|
|
95
|
-
// Tick once a second so elapsed time stays live while runs are active.
|
|
96
|
-
const timer = setInterval(() => {
|
|
97
|
-
if (monitor.getRuns().some((r) => isRunActiveStatus(r.status))) {
|
|
98
|
-
tui.requestRender();
|
|
99
|
-
}
|
|
100
|
-
}, 1000);
|
|
101
|
-
return {
|
|
102
|
-
render(width: number): string[] {
|
|
103
|
-
const runs = monitor.getRuns();
|
|
104
|
-
if (runs.length === 0) return [];
|
|
105
|
-
const now = Date.now();
|
|
106
|
-
const lines: string[] = [];
|
|
107
|
-
// Tree layout: each top-level agent is a root whose title/activity hang
|
|
108
|
-
// off it as branches; auto-fix chain runs (groupId) become child nodes
|
|
109
|
-
// under their parent root, with a "│" continuation while more siblings
|
|
110
|
-
// follow. Blank lines separate agent blocks so parallel runs don't blur
|
|
111
|
-
// into one wall of text.
|
|
112
|
-
const dim = (t: string): string => theme.fg("dim", t);
|
|
113
|
-
for (let idx = 0; idx < runs.length; idx++) {
|
|
114
|
-
const r = runs[idx];
|
|
115
|
-
const isChain = Boolean(r.groupId);
|
|
116
|
-
const chainContinues = isChain && runs[idx + 1]?.groupId === r.groupId;
|
|
117
|
-
const activity =
|
|
118
|
-
r.activity && isRunActiveStatus(r.status) ? r.activity : undefined;
|
|
119
|
-
const hasActivity = activity !== undefined;
|
|
120
|
-
const icon = statusIcon(r.status, theme);
|
|
121
|
-
// Chain-internal runs (auto-fix worker/reviewer) are child nodes under
|
|
122
|
-
// their parent reviewer. Their relationLabel ("fix round 1") is more
|
|
123
|
-
// distinguishing than the repeated worker/reviewer name.
|
|
124
|
-
const name = isChain ? (r.relationLabel ?? r.agent) : r.agent;
|
|
125
|
-
// Two lines per run: the header row (icon, run id, agent name) and the
|
|
126
|
-
// live activity branch below. The task summary is deliberately not
|
|
127
|
-
// shown — the task lives in the tool result, and the agent name plus
|
|
128
|
-
// what it is doing right now is enough to tell runs apart. The header
|
|
129
|
-
// stays exactly as it was (accent name, dim stats), matching the
|
|
130
|
-
// referenced sub-agent widgets (tintinweb): the running indicator
|
|
131
|
-
// uses the accent color, everything else is quiet.
|
|
132
|
-
if (!isChain && lines.length > 0) lines.push("");
|
|
133
|
-
const nodeBranch = isChain ? (chainContinues ? "├─ " : "└─ ") : "";
|
|
134
|
-
// Content label (task-derived) trails the agent name so concurrent
|
|
135
|
-
// same-agent runs read as what they do, not just their run id. Chain
|
|
136
|
-
// nodes already carry a distinguishing relationLabel.
|
|
137
|
-
const labelPart = !isChain && r.label ? ` ${dim(`· ${r.label}`)}` : "";
|
|
138
|
-
const left = `${dim(nodeBranch)}${icon} ${dim(`#${r.id}`)} ${isChain ? name : theme.fg("accent", theme.bold(name))}${labelPart}`;
|
|
139
|
-
|
|
140
|
-
// Right side: full model ref (provider/model), token usage (in/out +
|
|
141
|
-
// cache read/write), tool count, elapsed, and the soft activity-state
|
|
142
|
-
// annotation (idle / long-running). Trailing the header with a single
|
|
143
|
-
// " · " chain keeps the row compact (no center gap); compactLine
|
|
144
|
-
// clips on overflow, never the right side on its own.
|
|
145
|
-
const model = r.modelFallbackFrom
|
|
146
|
-
? `${r.model ?? "?"} (pool fallback from ${r.modelFallbackFrom})`
|
|
147
|
-
: (r.model ?? "?");
|
|
148
|
-
const usage = formatUsageCompact(r.usage);
|
|
149
|
-
const tools = r.toolCount ? `${r.toolCount} tool${r.toolCount === 1 ? "" : "s"}` : "";
|
|
150
|
-
const elapsed = formatElapsed(r, now);
|
|
151
|
-
// The round outcome summary leads the metadata so a finished chain
|
|
152
|
-
// row reads as what it did ("fail · src/index.ts · render()",
|
|
153
|
-
// "pass", "src/index.ts · tests/monitor.test.ts").
|
|
154
|
-
const isolation = r.isolation === "worktree" ? `worktree ${r.integrationStatus ?? "active"}` : undefined;
|
|
155
|
-
const relation = r.forkedFromRunId !== undefined
|
|
156
|
-
? `fork of #${r.forkedFromRunId}`
|
|
157
|
-
: (r.forkChildRunIds?.length ?? 0) > 0
|
|
158
|
-
? `forks ${r.forkChildRunIds!.map((id) => `#${id}`).join(",")}`
|
|
159
|
-
: undefined;
|
|
160
|
-
const metaParts = [r.summary, relation, isolation, model, usage, tools, elapsed].filter(Boolean);
|
|
161
|
-
// Running is conveyed by the icon + elapsed; spell out the label only for
|
|
162
|
-
// the other states (ready / done / stopped) so they are unambiguous.
|
|
163
|
-
if (r.status !== "running") metaParts.push(statusLabel(r.status));
|
|
164
|
-
const state = deriveActivityState(r, now);
|
|
165
|
-
if (state) metaParts.push(activityStateLabel(state));
|
|
166
|
-
if (r.annotation) metaParts.push(r.annotation);
|
|
167
|
-
// Metadata trails the header in dim — quiet, never competing with the
|
|
168
|
-
// accent agent name (the same restraint the referenced widgets use).
|
|
169
|
-
// Trailing with a single " · " chain keeps the row compact (no center
|
|
170
|
-
// gap); compactLine clips on overflow, never the right side on its own.
|
|
171
|
-
const right = metaParts.length ? dim(` · ${metaParts.join(" · ")}`) : "";
|
|
172
|
-
lines.push(compactLine(left, right, width));
|
|
173
|
-
|
|
174
|
-
// Current activity ("read src/index.ts", "bash npm test") is the only
|
|
175
|
-
// branch: gray, so it never competes with the agent name or pi's own
|
|
176
|
-
// UI. Chain nodes that still have siblings carry a "│" continuation
|
|
177
|
-
// down to the last one.
|
|
178
|
-
if (hasActivity) {
|
|
179
|
-
const continuation = isChain ? (chainContinues ? "│ " : " ") : "";
|
|
180
|
-
lines.push(truncateToWidth(`${continuation}${dim("└─ ")}${dim(activity)}`, width));
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
return lines;
|
|
184
|
-
},
|
|
185
|
-
invalidate() {},
|
|
186
|
-
dispose() {
|
|
187
|
-
unsub();
|
|
188
|
-
clearInterval(timer);
|
|
189
|
-
},
|
|
190
|
-
};
|
|
191
|
-
},
|
|
192
|
-
{ placement: "aboveEditor" },
|
|
193
|
-
);
|
|
194
|
-
});
|
|
195
|
-
}
|