@ferris1225/pi-subagents 4.1.21 → 4.1.24
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 +43 -18
- package/package.json +55 -55
- package/src/announcements.ts +26 -0
- package/src/config.ts +50 -2
- package/src/dispatch.ts +26 -8
- package/src/monitor.ts +114 -4
- package/src/recovery.ts +145 -145
- package/src/rpc-run.ts +993 -993
- package/src/setup.ts +12 -15
- package/src/widget.ts +247 -134
package/src/setup.ts
CHANGED
|
@@ -248,6 +248,9 @@ async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, ba
|
|
|
248
248
|
|
|
249
249
|
const next: SubagentsConfig = {
|
|
250
250
|
enabledAgents: enabled,
|
|
251
|
+
// The wizard surfaces every built-in, so an untoggled one was seen and
|
|
252
|
+
// deliberately left off — record them all as known.
|
|
253
|
+
knownAgents: [...BUILTIN_AGENT_NAMES],
|
|
251
254
|
agentModels,
|
|
252
255
|
// Full setup returns every agent to capability-aware Auto thinking.
|
|
253
256
|
agentThinkingLevels: {},
|
|
@@ -283,23 +286,17 @@ async function runMenu(ctx: ExtensionCommandContext, configPath: string, config:
|
|
|
283
286
|
const enabled = await pickEnabledAgents(ctx, config.enabledAgents);
|
|
284
287
|
if (enabled === undefined) continue;
|
|
285
288
|
next.enabledAgents = enabled;
|
|
286
|
-
// A newly enabled role inherits
|
|
289
|
+
// A newly enabled role inherits explorer's configured model and
|
|
287
290
|
// thinking level, so the file reflects what it will actually run
|
|
288
|
-
// instead of silently falling back to the current main model:
|
|
289
|
-
//
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
[
|
|
293
|
-
|
|
294
|
-
["synthesizer", "explorer"],
|
|
295
|
-
];
|
|
296
|
-
for (const [agent, from] of modelInheritance) {
|
|
297
|
-
if (config.enabledAgents.includes(agent) || !enabled.includes(agent)) continue;
|
|
298
|
-
if (!next.agentModels[agent] && config.agentModels[from]) {
|
|
299
|
-
next.agentModels[agent] = config.agentModels[from];
|
|
291
|
+
// instead of silently falling back to the current main model: these
|
|
292
|
+
// roles do light migration-grade work on the fast explorer lane.
|
|
293
|
+
for (const agent of enabled) {
|
|
294
|
+
if (agent === "explorer" || config.enabledAgents.includes(agent)) continue;
|
|
295
|
+
if (!next.agentModels[agent] && config.agentModels.explorer) {
|
|
296
|
+
next.agentModels[agent] = config.agentModels.explorer;
|
|
300
297
|
}
|
|
301
|
-
if (!next.agentThinkingLevels[agent] && config.agentThinkingLevels
|
|
302
|
-
next.agentThinkingLevels[agent] = config.agentThinkingLevels
|
|
298
|
+
if (!next.agentThinkingLevels[agent] && config.agentThinkingLevels.explorer) {
|
|
299
|
+
next.agentThinkingLevels[agent] = config.agentThinkingLevels.explorer;
|
|
303
300
|
}
|
|
304
301
|
}
|
|
305
302
|
next.agentModels = keepAgentEntries(next.agentModels, enabled);
|
package/src/widget.ts
CHANGED
|
@@ -2,16 +2,24 @@
|
|
|
2
2
|
* Compact, glanceable active-run widget for interactive Pi sessions.
|
|
3
3
|
*
|
|
4
4
|
* Layout contract (redesign):
|
|
5
|
-
* - Aligned columns: `icon #id agent` pad to the widest displayed
|
|
6
|
-
* agent so every label starts at the same column; a resumed thread
|
|
7
|
-
* a dim `↻` inside the agent column.
|
|
8
|
-
* -
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
5
|
+
* - Aligned identity columns: `icon #id agent` pad to the widest displayed
|
|
6
|
+
* id and agent so every label starts at the same column; a resumed thread
|
|
7
|
+
* carries a dim `↻` inside the agent column.
|
|
8
|
+
* - A live run owns two lines. Line 1 is what it is: identity, task label,
|
|
9
|
+
* then the telemetry flow (`provider/model`, token flow in the pi-footer
|
|
10
|
+
* vocabulary `↑in ↓out R/W cache`, cost, wait state, seconds-precision
|
|
11
|
+
* elapsed). Line 2 is what it is doing right now: the live activity, dim,
|
|
12
|
+
* indented under the label column behind a `↳` marker.
|
|
13
|
+
* - Telemetry drops leftmost-first under width pressure (badge, wait, usage,
|
|
14
|
+
* model); the elapsed survives every width.
|
|
15
|
+
* - First line of the widget is the parent pi session itself: what the current
|
|
16
|
+
* model is doing right now while its agent loop runs (model/thinking, live
|
|
17
|
+
* activity, loop elapsed), fed by the session's own extension events.
|
|
18
|
+
* - A managed workflow renders as a tree chain under its parent line: one
|
|
19
|
+
* `├`/`└`-connected row per stage, each carrying its own model, token flow,
|
|
20
|
+
* and elapsed — settled stages from the snapshot frozen at settlement, the
|
|
21
|
+
* live stage from its child row. An oversized chain keeps a window anchored
|
|
22
|
+
* on the live stage.
|
|
15
23
|
* - Queued rows say what they actually wait for ("queued" for a process slot,
|
|
16
24
|
* "repo lane" for shared-writer serialization, "starting" while the child
|
|
17
25
|
* process launches) instead of one catch-all "queued".
|
|
@@ -20,14 +28,22 @@
|
|
|
20
28
|
import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
21
29
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
22
30
|
import {
|
|
31
|
+
formatDuration,
|
|
23
32
|
formatElapsed,
|
|
24
33
|
formatTaskSummary,
|
|
34
|
+
formatUsageTokens,
|
|
25
35
|
isRunActiveStatus,
|
|
26
36
|
monitor,
|
|
37
|
+
shrinkRunLabel,
|
|
27
38
|
statusIcon,
|
|
39
|
+
sumUsage,
|
|
40
|
+
usageCostPart,
|
|
41
|
+
type MainActivity,
|
|
28
42
|
type RunView,
|
|
29
43
|
type WorkflowStage,
|
|
44
|
+
type WorkflowStageStatus,
|
|
30
45
|
} from "./monitor.ts";
|
|
46
|
+
import type { UsageStats } from "./rpc-run.ts";
|
|
31
47
|
|
|
32
48
|
export const SUBAGENTS_WIDGET_ID = "pi-subagents";
|
|
33
49
|
|
|
@@ -38,8 +54,10 @@ const MAX_WIDGET_LINES = 10;
|
|
|
38
54
|
const SEPARATOR = " · ";
|
|
39
55
|
/** Column gap between the identity block and the run's label. */
|
|
40
56
|
const IDENTITY_GAP = " ";
|
|
41
|
-
/** Splits "what this run is" from "what it is doing right now". */
|
|
57
|
+
/** Splits "what this run is" from "what it is doing right now" on a stage row. */
|
|
42
58
|
const ACTIVITY_SEPARATOR = " — ";
|
|
59
|
+
/** Marker introducing the live-activity second line of a running row. */
|
|
60
|
+
const ACTIVITY_MARKER = "↳ ";
|
|
43
61
|
/** Columns kept for left content before right-tail parts are dropped. */
|
|
44
62
|
const LEFT_MIN_CONTENT = 8;
|
|
45
63
|
/** Minimum useful width for a live-activity fragment. */
|
|
@@ -53,17 +71,13 @@ interface ColumnLayout {
|
|
|
53
71
|
agentWidth: number;
|
|
54
72
|
}
|
|
55
73
|
|
|
56
|
-
/**
|
|
57
|
-
*
|
|
58
|
-
* line
|
|
59
|
-
function
|
|
60
|
-
if (
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (rightWidth >= width) return truncateToWidth(right, width, "");
|
|
64
|
-
const leftBudget = width - rightWidth - 1;
|
|
65
|
-
const leftText = visibleWidth(left) > leftBudget ? truncateToWidth(left, leftBudget, "…") : left;
|
|
66
|
-
return `${leftText}${" ".repeat(Math.max(1, width - visibleWidth(leftText) - rightWidth))}${right}`;
|
|
74
|
+
/** Join left content and telemetry inline — `left · telemetry` — so a
|
|
75
|
+
* multi-line chain reads as one flowing sentence instead of leaving blank
|
|
76
|
+
* padding across the width; the whole line truncates as a last resort. */
|
|
77
|
+
function composeLine(left: string, tail: string, theme: Theme, width: number): string {
|
|
78
|
+
if (!tail) return truncateToWidth(left, width, "…");
|
|
79
|
+
const line = `${left}${theme.fg("dim", SEPARATOR)}${theme.fg("dim", tail)}`;
|
|
80
|
+
return visibleWidth(line) <= width ? line : truncateToWidth(line, width, "…");
|
|
67
81
|
}
|
|
68
82
|
|
|
69
83
|
/** Short truthful wait word for a queued row, shown in the telemetry column. */
|
|
@@ -124,145 +138,229 @@ function identitySegment(run: RunView, theme: Theme, layout: ColumnLayout): stri
|
|
|
124
138
|
return `${icon} ${theme.fg("dim", id)} ${name}${resumed}${pad}`;
|
|
125
139
|
}
|
|
126
140
|
|
|
127
|
-
/** One
|
|
128
|
-
*
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
141
|
+
/** One footer-style usage part: token flow plus accrued cost, dropped as a
|
|
142
|
+
* unit before the model under width pressure. */
|
|
143
|
+
function usagePart(usage: UsageStats | undefined): string | undefined {
|
|
144
|
+
return [formatUsageTokens(usage), usageCostPart(usage)].filter(Boolean).join(" ") || undefined;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Telemetry tail parts shared by primary and stage rows: badge and wait word
|
|
148
|
+
* first (dropped first under pressure), then the usage part, the model, and
|
|
149
|
+
* the always-surviving elapsed. `usage` defaults to the run's own; the managed
|
|
150
|
+
* workflow parent overrides it with the workflow-wide aggregate. */
|
|
151
|
+
function telemetryTailParts(run: RunView, now: number, usage: UsageStats = run.usage): Array<string | undefined> {
|
|
137
152
|
// Queued rows omit the model (the route is re-resolved at actual start);
|
|
138
|
-
// workflow parents omit it too (each stage owns its own model).
|
|
139
|
-
|
|
153
|
+
// workflow parents omit it too (each stage row owns its own model). The
|
|
154
|
+
// full provider/model ref is kept — "which provider served this run" is
|
|
155
|
+
// exactly what a multi-provider session needs to see.
|
|
156
|
+
const modelPart = run.status === "queued" || run.managedWorkflow || !run.model
|
|
140
157
|
? undefined
|
|
141
|
-
:
|
|
158
|
+
: run.model;
|
|
142
159
|
const badge = run.isolation === "worktree" ? worktreeBadge(run) : undefined;
|
|
143
160
|
const wait = run.status === "queued" ? waitWord(run) : undefined;
|
|
144
|
-
|
|
145
|
-
// Drop order under pressure: badge, then model, then wait word; elapsed
|
|
161
|
+
// Drop order under pressure: badge, wait word, usage, model; elapsed
|
|
146
162
|
// survives every width the identity leaves room for.
|
|
147
|
-
|
|
163
|
+
return [badge, wait, usagePart(usage), modelPart, formatElapsed(run, now) || undefined];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Two lines for a live run. Line 1 is what the run is: identity, task label,
|
|
167
|
+
* then the telemetry flow (worktree badge, token flow, cost, provider/model,
|
|
168
|
+
* wait state, elapsed). Line 2 is what it is doing right now: the live
|
|
169
|
+
* activity, dim, indented under the label column behind a `↳` marker. The
|
|
170
|
+
* label takes the full content budget on line 1; the identity and the elapsed
|
|
171
|
+
* survive every width. */
|
|
172
|
+
function primaryLine(
|
|
173
|
+
run: RunView,
|
|
174
|
+
theme: Theme,
|
|
175
|
+
width: number,
|
|
176
|
+
now: number,
|
|
177
|
+
layout: ColumnLayout,
|
|
178
|
+
usage: UsageStats = run.usage,
|
|
179
|
+
): string[] {
|
|
180
|
+
const identity = identitySegment(run, theme, layout);
|
|
181
|
+
const tailBudget = Math.max(0, width - visibleWidth(identity) - LEFT_MIN_CONTENT);
|
|
182
|
+
const tail = composeTail(telemetryTailParts(run, now, usage), tailBudget);
|
|
148
183
|
|
|
149
184
|
// A chain child rendered at root level (its parent row is gone) keeps its
|
|
150
185
|
// workflow relation; the templated brief itself would only repeat content.
|
|
151
186
|
const label = run.parentRunId !== undefined
|
|
152
187
|
? [run.relationLabel, run.label].filter((part): part is string => Boolean(part)).join(SEPARATOR)
|
|
153
188
|
: run.label ?? formatTaskSummary(run.task, 48);
|
|
189
|
+
const contentBudget = width
|
|
190
|
+
- visibleWidth(identity)
|
|
191
|
+
- (tail ? visibleWidth(tail) + visibleWidth(SEPARATOR) : 0)
|
|
192
|
+
- visibleWidth(IDENTITY_GAP);
|
|
193
|
+
// The label is already fragment-extracted (runLabel); narrowing it keeps
|
|
194
|
+
// its tail so a second squeeze never trades away the recognisable
|
|
195
|
+
// filename, and no second head…tail ellipsis stacks on top of it.
|
|
196
|
+
const content = label && contentBudget > 0 ? shrinkRunLabel(label, contentBudget) : "";
|
|
197
|
+
const left = content ? `${identity}${IDENTITY_GAP}${content}` : identity;
|
|
198
|
+
const lines = [composeLine(left, tail, theme, width)];
|
|
199
|
+
|
|
154
200
|
// The parent's own activity is a placeholder while a managed workflow runs;
|
|
155
|
-
// the timeline
|
|
201
|
+
// the timeline chain below carries the live stage instead.
|
|
156
202
|
const activity = !run.managedWorkflow && (run.status === "running" || run.status === "interrupting")
|
|
157
203
|
? run.activity?.trim()
|
|
158
204
|
: undefined;
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
- visibleWidth(
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const labelBudget = activity
|
|
167
|
-
? Math.min(visibleWidth(label), Math.max(12, Math.floor(contentBudget * 0.4)))
|
|
168
|
-
: contentBudget;
|
|
169
|
-
// The label is already fragment-extracted (runLabel); plain right
|
|
170
|
-
// truncation avoids stacking a second head…tail ellipsis on top of it.
|
|
171
|
-
const labelText = label
|
|
172
|
-
? visibleWidth(label) > labelBudget ? truncateToWidth(label, labelBudget, "…") : label
|
|
173
|
-
: "";
|
|
174
|
-
if (activity) {
|
|
175
|
-
const activityBudget = contentBudget
|
|
176
|
-
- visibleWidth(labelText)
|
|
177
|
-
- (labelText ? visibleWidth(ACTIVITY_SEPARATOR) : 0);
|
|
178
|
-
content = activityBudget >= ACTIVITY_MIN_WIDTH
|
|
179
|
-
? `${labelText}${labelText ? dim(ACTIVITY_SEPARATOR) : ""}${dim(formatTaskSummary(activity, activityBudget))}`
|
|
180
|
-
// Too narrow for both: the live activity is the stronger signal.
|
|
181
|
-
: dim(formatTaskSummary(activity, contentBudget));
|
|
182
|
-
} else {
|
|
183
|
-
content = labelText;
|
|
205
|
+
if (activity) {
|
|
206
|
+
const indent = visibleWidth(identity) + visibleWidth(IDENTITY_GAP);
|
|
207
|
+
const activityBudget = width - indent - visibleWidth(ACTIVITY_MARKER);
|
|
208
|
+
if (activityBudget >= ACTIVITY_MIN_WIDTH) {
|
|
209
|
+
lines.push(
|
|
210
|
+
`${" ".repeat(indent)}${theme.fg("dim", `${ACTIVITY_MARKER}${formatTaskSummary(activity, activityBudget)}`)}`,
|
|
211
|
+
);
|
|
184
212
|
}
|
|
185
213
|
}
|
|
186
|
-
|
|
187
|
-
const left = content ? `${identity}${IDENTITY_GAP}${content}` : identity;
|
|
188
|
-
return layoutLine(left, tail ? dim(tail) : "", width);
|
|
214
|
+
return lines;
|
|
189
215
|
}
|
|
190
216
|
|
|
191
|
-
function
|
|
192
|
-
|
|
193
|
-
switch (stage.status) {
|
|
217
|
+
function stageIcon(status: WorkflowStageStatus, theme: Theme): string {
|
|
218
|
+
switch (status) {
|
|
194
219
|
case "done":
|
|
195
|
-
return theme.fg("success",
|
|
220
|
+
return theme.fg("success", "✓");
|
|
196
221
|
case "active":
|
|
197
|
-
return theme.fg("accent", theme.bold(
|
|
222
|
+
return theme.fg("accent", theme.bold("●"));
|
|
198
223
|
case "changes":
|
|
199
|
-
return theme.fg("warning",
|
|
224
|
+
return theme.fg("warning", "!");
|
|
200
225
|
case "failed":
|
|
201
|
-
return theme.fg("error",
|
|
226
|
+
return theme.fg("error", "✗");
|
|
202
227
|
default:
|
|
203
|
-
return theme.fg("dim",
|
|
228
|
+
return theme.fg("dim", "○");
|
|
204
229
|
}
|
|
205
230
|
}
|
|
206
231
|
|
|
207
|
-
/** Stage
|
|
208
|
-
*
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
232
|
+
/** Stage telemetry source: the live child while the stage runs, the frozen
|
|
233
|
+
* snapshot once it has settled. */
|
|
234
|
+
interface StageTelemetry {
|
|
235
|
+
model?: string;
|
|
236
|
+
thinking?: string;
|
|
237
|
+
usage?: UsageStats;
|
|
238
|
+
elapsed: string;
|
|
239
|
+
activity?: string;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function stageTelemetry(
|
|
243
|
+
stage: WorkflowStage,
|
|
244
|
+
live: RunView | undefined,
|
|
245
|
+
now: number,
|
|
246
|
+
): StageTelemetry {
|
|
247
|
+
if (live) {
|
|
248
|
+
return {
|
|
249
|
+
model: live.model,
|
|
250
|
+
thinking: live.thinking,
|
|
251
|
+
usage: live.usage,
|
|
252
|
+
elapsed: formatElapsed(live, now),
|
|
253
|
+
activity: live.activity?.trim() || undefined,
|
|
254
|
+
};
|
|
218
255
|
}
|
|
219
|
-
|
|
220
|
-
if (focusIndex === -1) focusIndex = stages.length - 1;
|
|
221
|
-
const omittedPrefix = focusIndex > 0 ? theme.fg("dim", "… ─ ") : "";
|
|
222
|
-
return truncateToWidth(`${omittedPrefix}${render(stages.slice(focusIndex))}`, width, "…");
|
|
256
|
+
return { model: stage.model, usage: stage.usage, elapsed: stage.elapsedMs !== undefined ? formatDuration(stage.elapsedMs) : "" };
|
|
223
257
|
}
|
|
224
258
|
|
|
225
|
-
/**
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
function
|
|
230
|
-
|
|
259
|
+
/** The parent pi session's own line — what the current model is doing right
|
|
260
|
+
* now while its agent loop runs: full model/thinking ref, live activity, loop
|
|
261
|
+
* elapsed. Not a run, so it owns no `#id`; it joins the identity layout so its
|
|
262
|
+
* label column lines up with the run rows. */
|
|
263
|
+
function mainLine(main: MainActivity, theme: Theme, width: number, now: number, layout: ColumnLayout): string {
|
|
264
|
+
const dim = (text: string): string => theme.fg("dim", text);
|
|
265
|
+
const icon = theme.fg("accent", "●");
|
|
266
|
+
const name = theme.fg("accent", theme.bold("pi"));
|
|
267
|
+
const pad = " ".repeat(Math.max(0, layout.agentWidth - visibleWidth("pi")));
|
|
268
|
+
const identity = `${icon} ${" ".repeat(layout.idWidth + 1)}${name}${pad}`;
|
|
269
|
+
const modelPart = main.model ? `${main.model}${main.thinking ? `/${main.thinking}` : ""}` : undefined;
|
|
270
|
+
const elapsed = formatDuration(Math.max(0, now - main.activeSince));
|
|
271
|
+
const tail = composeTail([modelPart, elapsed], Math.max(0, width - visibleWidth(identity) - LEFT_MIN_CONTENT));
|
|
272
|
+
const contentBudget = width
|
|
273
|
+
- visibleWidth(identity)
|
|
274
|
+
- (tail ? visibleWidth(tail) + visibleWidth(SEPARATOR) : 0)
|
|
275
|
+
- visibleWidth(IDENTITY_GAP);
|
|
276
|
+
let left = identity;
|
|
277
|
+
if (main.activity?.trim() && contentBudget >= ACTIVITY_MIN_WIDTH) {
|
|
278
|
+
left = `${identity}${IDENTITY_GAP}${dim(formatTaskSummary(main.activity.trim(), contentBudget))}`;
|
|
279
|
+
}
|
|
280
|
+
return composeLine(left, tail, theme, width);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** One `├`/`└`-connected row per managed-workflow stage: status icon,
|
|
284
|
+
* relation, live activity for the running stage, and its own model/token/
|
|
285
|
+
* cost/elapsed telemetry flowing inline. The chain hangs off the parent line,
|
|
286
|
+
* so who dispatched what stays visible while the auto-fix workflow progresses. */
|
|
287
|
+
function workflowStageLines(
|
|
288
|
+
stages: readonly WorkflowStage[],
|
|
231
289
|
children: readonly RunView[],
|
|
232
290
|
theme: Theme,
|
|
233
291
|
width: number,
|
|
234
292
|
now: number,
|
|
235
|
-
): string
|
|
236
|
-
const
|
|
237
|
-
const indent = ` ${theme.fg("dim", "└")} `;
|
|
238
|
-
const budget = width - visibleWidth(indent);
|
|
239
|
-
if (!stages || stages.length === 0 || budget <= 0) return undefined;
|
|
240
|
-
const child = children.find((candidate) => candidate.status === "running" || candidate.status === "interrupting")
|
|
293
|
+
): { lines: string[]; activeIndex: number } {
|
|
294
|
+
const live = children.find((candidate) => candidate.status === "running" || candidate.status === "interrupting")
|
|
241
295
|
?? children.at(-1);
|
|
242
|
-
const
|
|
243
|
-
const
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
-
|
|
296
|
+
const activeIndex = stages.findIndex((stage) => stage.status === "active");
|
|
297
|
+
const lines: string[] = [];
|
|
298
|
+
for (const [index, stage] of stages.entries()) {
|
|
299
|
+
const telemetry = stageTelemetry(stage, index === activeIndex ? live : undefined, now);
|
|
300
|
+
const connector = theme.fg("dim", index === stages.length - 1 ? "└" : "├");
|
|
301
|
+
const indent = ` ${connector} `;
|
|
302
|
+
const budget = width - visibleWidth(indent);
|
|
303
|
+
if (budget <= 0) break;
|
|
304
|
+
const icon = stageIcon(stage.status, theme);
|
|
305
|
+
const label = `${icon} ${stage.relation}`;
|
|
306
|
+
const modelPart = telemetry.model
|
|
307
|
+
? `${telemetry.model}${telemetry.thinking ? `/${telemetry.thinking}` : ""}`
|
|
308
|
+
: undefined;
|
|
309
|
+
const parts = [
|
|
310
|
+
usagePart(telemetry.usage),
|
|
311
|
+
modelPart,
|
|
312
|
+
telemetry.elapsed || undefined,
|
|
313
|
+
];
|
|
314
|
+
const tailBudget = Math.max(0, budget - visibleWidth(label) - ACTIVITY_MIN_WIDTH);
|
|
315
|
+
const tail = composeTail(parts, tailBudget);
|
|
316
|
+
const contentBudget = budget - (tail ? visibleWidth(tail) + visibleWidth(SEPARATOR) : 0);
|
|
317
|
+
let left = label;
|
|
318
|
+
if (telemetry.activity && contentBudget - visibleWidth(label) - visibleWidth(ACTIVITY_SEPARATOR) >= ACTIVITY_MIN_WIDTH) {
|
|
319
|
+
const activityBudget = contentBudget - visibleWidth(label) - visibleWidth(ACTIVITY_SEPARATOR);
|
|
320
|
+
left = `${label}${theme.fg("dim", ACTIVITY_SEPARATOR)}${theme.fg("dim", formatTaskSummary(telemetry.activity, activityBudget))}`;
|
|
321
|
+
}
|
|
322
|
+
lines.push(`${indent}${composeLine(left, tail, theme, budget)}`);
|
|
254
323
|
}
|
|
255
|
-
return
|
|
324
|
+
return { lines, activeIndex };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** Fit one workflow group into the remaining line budget: the primary lines
|
|
328
|
+
* (identity line plus the live-activity line) always survive, and the stage
|
|
329
|
+
* window anchors on the live stage — settled stages above the window collapse
|
|
330
|
+
* into one `… +N` marker, never the live stage itself. */
|
|
331
|
+
function fitGroupLines(
|
|
332
|
+
primary: readonly string[],
|
|
333
|
+
stages: readonly string[],
|
|
334
|
+
activeIndex: number,
|
|
335
|
+
remaining: number,
|
|
336
|
+
theme: Theme,
|
|
337
|
+
): string[] {
|
|
338
|
+
if (primary.length > remaining) return primary.slice(0, Math.max(1, remaining));
|
|
339
|
+
if (stages.length === 0 || stages.length + primary.length <= remaining) return [...primary, ...stages];
|
|
340
|
+
const slots = remaining - primary.length;
|
|
341
|
+
// Reserve one line for an overflow marker so a cut is always announced.
|
|
342
|
+
const room = Math.max(1, slots - 1);
|
|
343
|
+
const anchor = activeIndex >= 0 ? activeIndex : 0;
|
|
344
|
+
const start = Math.max(0, Math.min(anchor, stages.length - room));
|
|
345
|
+
const window = stages.slice(start, start + room);
|
|
346
|
+
const parts = [
|
|
347
|
+
...(start > 0 ? [theme.fg("dim", `… +${start}`)] : []),
|
|
348
|
+
...window,
|
|
349
|
+
...(start + room < stages.length ? [theme.fg("dim", `… +${stages.length - start - room}`)] : []),
|
|
350
|
+
];
|
|
351
|
+
while (parts.length > slots) parts.pop();
|
|
352
|
+
return [...primary, ...parts];
|
|
256
353
|
}
|
|
257
354
|
|
|
258
|
-
/** Render active runs as compact per-run line groups:
|
|
259
|
-
* two per
|
|
260
|
-
*
|
|
355
|
+
/** Render active runs as compact per-run line groups: the parent model's own
|
|
356
|
+
* line first, then one two-line group per simple run, a tree chain per managed
|
|
357
|
+
* workflow (parent line + one row per stage). All rows share one column layout. */
|
|
261
358
|
export function formatActiveRunLines(
|
|
262
359
|
runs: readonly RunView[],
|
|
263
360
|
theme: Theme,
|
|
264
361
|
width: number,
|
|
265
362
|
now: number = Date.now(),
|
|
363
|
+
main?: MainActivity,
|
|
266
364
|
): string[] {
|
|
267
365
|
const active = runs.filter((run) => isRunActiveStatus(run.status));
|
|
268
366
|
const activeIds = new Set(active.map((run) => run.id));
|
|
@@ -279,24 +377,39 @@ export function formatActiveRunLines(
|
|
|
279
377
|
}
|
|
280
378
|
const layout: ColumnLayout = {
|
|
281
379
|
idWidth: Math.max(...roots.map((root) => visibleWidth(`#${root.id}`)), 0),
|
|
282
|
-
agentWidth: Math.max(...roots.map((root) => visibleWidth(agentColumnText(root))), 0),
|
|
380
|
+
agentWidth: Math.max(...roots.map((root) => visibleWidth(agentColumnText(root))), main ? visibleWidth("pi") : 0),
|
|
283
381
|
};
|
|
284
|
-
const groups: string[][] = roots.map((root) => {
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
382
|
+
const groups: Array<{ primary: readonly string[]; stages: readonly string[]; activeIndex: number }> = roots.map((root) => {
|
|
383
|
+
const children = childrenOf.get(root.id) ?? [];
|
|
384
|
+
// Workflow-wide tokens/cost on the parent line: every stage snapshot plus
|
|
385
|
+
// the live child (whose snapshot is frozen only at settlement). Without
|
|
386
|
+
// a stage projection the parent's own usage stands in.
|
|
387
|
+
let usage = root.usage;
|
|
388
|
+
if (root.managedWorkflow && root.workflowStages) {
|
|
389
|
+
const settled = root.workflowStages.map((stage) => stage.usage).filter((u): u is UsageStats => Boolean(u));
|
|
390
|
+
const live = children.find((candidate) => candidate.usage.input || candidate.usage.output || candidate.usage.cost);
|
|
391
|
+
usage = sumUsage([...(settled.length > 0 ? settled : [root.usage]), ...(live ? [live.usage] : [])]);
|
|
392
|
+
}
|
|
393
|
+
const primary = primaryLine(root, theme, width, now, layout, usage);
|
|
394
|
+
let activeIndex = -1;
|
|
395
|
+
let stages: readonly string[] = [];
|
|
396
|
+
if (root.managedWorkflow && root.workflowStages && root.workflowStages.length > 0) {
|
|
397
|
+
const rendered = workflowStageLines(root.workflowStages, children, theme, width, now);
|
|
398
|
+
stages = rendered.lines;
|
|
399
|
+
activeIndex = rendered.activeIndex;
|
|
400
|
+
}
|
|
401
|
+
return { primary, stages, activeIndex };
|
|
291
402
|
});
|
|
292
403
|
|
|
293
404
|
const lines: string[] = [];
|
|
405
|
+
if (main && MAX_WIDGET_LINES > 1) {
|
|
406
|
+
lines.push(mainLine(main, theme, width, now, layout));
|
|
407
|
+
}
|
|
294
408
|
let shownRoots = 0;
|
|
295
409
|
for (const group of groups) {
|
|
296
410
|
const remaining = MAX_WIDGET_LINES - 1 - lines.length;
|
|
297
411
|
if (remaining <= 0) break;
|
|
298
|
-
|
|
299
|
-
lines.push(...(group.length <= remaining ? group : group.slice(0, remaining)));
|
|
412
|
+
lines.push(...fitGroupLines(group.primary, group.stages, group.activeIndex, remaining, theme));
|
|
300
413
|
shownRoots++;
|
|
301
414
|
}
|
|
302
415
|
const hiddenRoots = roots.length - shownRoots;
|
|
@@ -307,13 +420,13 @@ export function formatActiveRunLines(
|
|
|
307
420
|
}
|
|
308
421
|
|
|
309
422
|
function hasTickingRun(): boolean {
|
|
310
|
-
return monitor.
|
|
311
|
-
(run) => isRunActiveStatus(run.status) && run.activeSince !== undefined
|
|
312
|
-
);
|
|
423
|
+
return monitor.isMainAgentActive()
|
|
424
|
+
|| monitor.getRuns().some((run) => isRunActiveStatus(run.status) && run.activeSince !== undefined);
|
|
313
425
|
}
|
|
314
426
|
|
|
315
|
-
/** Install the widget for one TUI session. Its timer exists only while at
|
|
316
|
-
* one active run has started
|
|
427
|
+
/** Install the widget for one TUI session. Its timer exists only while at
|
|
428
|
+
* least one active run has started or the parent agent loop is running, and
|
|
429
|
+
* is disposed with the widget. */
|
|
317
430
|
export function installActiveRunsWidget(ctx: Pick<ExtensionContext, "mode" | "ui">): void {
|
|
318
431
|
if (ctx.mode !== "tui") return;
|
|
319
432
|
ctx.ui.setWidget(
|
|
@@ -341,7 +454,7 @@ export function installActiveRunsWidget(ctx: Pick<ExtensionContext, "mode" | "ui
|
|
|
341
454
|
syncTimer();
|
|
342
455
|
|
|
343
456
|
return {
|
|
344
|
-
render: (width: number) => formatActiveRunLines(monitor.getRuns(), theme, width),
|
|
457
|
+
render: (width: number) => formatActiveRunLines(monitor.getRuns(), theme, width, Date.now(), monitor.getMainActivity()),
|
|
345
458
|
invalidate() {},
|
|
346
459
|
dispose() {
|
|
347
460
|
disposed = true;
|