@ferris1225/pi-subagents 4.1.23 → 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/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 a kindred role's configured model and
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: cleaner
289
- // follows the reviewer; documenter and synthesizer intentionally
290
- // follow the faster explorer route.
291
- const modelInheritance: ReadonlyArray<[agent: string, from: string]> = [
292
- ["cleaner", "reviewer"],
293
- ["documenter", "explorer"],
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[from]) {
302
- next.agentThinkingLevels[agent] = config.agentThinkingLevels[from];
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
@@ -5,15 +5,16 @@
5
5
  * - Aligned identity columns: `icon #id agent` pad to the widest displayed
6
6
  * id and agent so every label starts at the same column; a resumed thread
7
7
  * carries a dim `↻` inside the agent column.
8
- * - Multi-line rows flow inline: the telemetry (`provider/model/thinking`,
9
- * token flow in the pi-footer vocabulary `↑in ↓out R/W cache`, cost, wait
10
- * state, seconds-precision elapsed) follows the content after ` · ` instead
11
- * of a right-aligned column no blank padding across a multi-line chain.
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.
12
13
  * - Telemetry drops leftmost-first under width pressure (badge, wait, usage,
13
14
  * model); the elapsed survives every width.
14
- * - First line is the parent pi session itself: what the current model is
15
- * doing right now while its agent loop runs (model/thinking, live activity,
16
- * loop elapsed), fed by the session's own extension events.
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.
17
18
  * - A managed workflow renders as a tree chain under its parent line: one
18
19
  * `├`/`└`-connected row per stage, each carrying its own model, token flow,
19
20
  * and elapsed — settled stages from the snapshot frozen at settlement, the
@@ -33,6 +34,7 @@ import {
33
34
  formatUsageTokens,
34
35
  isRunActiveStatus,
35
36
  monitor,
37
+ shrinkRunLabel,
36
38
  statusIcon,
37
39
  sumUsage,
38
40
  usageCostPart,
@@ -52,8 +54,10 @@ const MAX_WIDGET_LINES = 10;
52
54
  const SEPARATOR = " · ";
53
55
  /** Column gap between the identity block and the run's label. */
54
56
  const IDENTITY_GAP = " ";
55
- /** 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. */
56
58
  const ACTIVITY_SEPARATOR = " — ";
59
+ /** Marker introducing the live-activity second line of a running row. */
60
+ const ACTIVITY_MARKER = "↳ ";
57
61
  /** Columns kept for left content before right-tail parts are dropped. */
58
62
  const LEFT_MIN_CONTENT = 8;
59
63
  /** Minimum useful width for a live-activity fragment. */
@@ -151,7 +155,7 @@ function telemetryTailParts(run: RunView, now: number, usage: UsageStats = run.u
151
155
  // exactly what a multi-provider session needs to see.
152
156
  const modelPart = run.status === "queued" || run.managedWorkflow || !run.model
153
157
  ? undefined
154
- : `${run.model}${run.thinking ? `/${run.thinking}` : ""}`;
158
+ : run.model;
155
159
  const badge = run.isolation === "worktree" ? worktreeBadge(run) : undefined;
156
160
  const wait = run.status === "queued" ? waitWord(run) : undefined;
157
161
  // Drop order under pressure: badge, wait word, usage, model; elapsed
@@ -159,11 +163,12 @@ function telemetryTailParts(run: RunView, now: number, usage: UsageStats = run.u
159
163
  return [badge, wait, usagePart(usage), modelPart, formatElapsed(run, now) || undefined];
160
164
  }
161
165
 
162
- /** One primary line per run. Left: identity label activity, where the
163
- * label stays plain and the live activity is dim. Telemetry flows inline
164
- * after ` · ` (worktree badge, token flow, cost, model/thinking, wait state,
165
- * elapsed). The activity outranks the label when space runs out; the identity
166
- * and elapsed survive every width. */
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. */
167
172
  function primaryLine(
168
173
  run: RunView,
169
174
  theme: Theme,
@@ -171,8 +176,7 @@ function primaryLine(
171
176
  now: number,
172
177
  layout: ColumnLayout,
173
178
  usage: UsageStats = run.usage,
174
- ): string {
175
- const dim = (text: string): string => theme.fg("dim", text);
179
+ ): string[] {
176
180
  const identity = identitySegment(run, theme, layout);
177
181
  const tailBudget = Math.max(0, width - visibleWidth(identity) - LEFT_MIN_CONTENT);
178
182
  const tail = composeTail(telemetryTailParts(run, now, usage), tailBudget);
@@ -182,41 +186,32 @@ function primaryLine(
182
186
  const label = run.parentRunId !== undefined
183
187
  ? [run.relationLabel, run.label].filter((part): part is string => Boolean(part)).join(SEPARATOR)
184
188
  : run.label ?? formatTaskSummary(run.task, 48);
185
- // The parent's own activity is a placeholder while a managed workflow runs;
186
- // the timeline line below carries the live stage instead.
187
- const activity = !run.managedWorkflow && (run.status === "running" || run.status === "interrupting")
188
- ? run.activity?.trim()
189
- : undefined;
190
-
191
189
  const contentBudget = width
192
190
  - visibleWidth(identity)
193
191
  - (tail ? visibleWidth(tail) + visibleWidth(SEPARATOR) : 0)
194
192
  - visibleWidth(IDENTITY_GAP);
195
- let content = "";
196
- if (contentBudget > 0) {
197
- const labelBudget = activity
198
- ? Math.min(visibleWidth(label), Math.max(12, Math.floor(contentBudget * 0.4)))
199
- : contentBudget;
200
- // The label is already fragment-extracted (runLabel); plain right
201
- // truncation avoids stacking a second head…tail ellipsis on top of it.
202
- const labelText = label
203
- ? visibleWidth(label) > labelBudget ? truncateToWidth(label, labelBudget, "…") : label
204
- : "";
205
- if (activity) {
206
- const activityBudget = contentBudget
207
- - visibleWidth(labelText)
208
- - (labelText ? visibleWidth(ACTIVITY_SEPARATOR) : 0);
209
- content = activityBudget >= ACTIVITY_MIN_WIDTH
210
- ? `${labelText}${labelText ? dim(ACTIVITY_SEPARATOR) : ""}${dim(formatTaskSummary(activity, activityBudget))}`
211
- // Too narrow for both: the live activity is the stronger signal.
212
- : dim(formatTaskSummary(activity, contentBudget));
213
- } else {
214
- content = labelText;
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
+
200
+ // The parent's own activity is a placeholder while a managed workflow runs;
201
+ // the timeline chain below carries the live stage instead.
202
+ const activity = !run.managedWorkflow && (run.status === "running" || run.status === "interrupting")
203
+ ? run.activity?.trim()
204
+ : undefined;
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
+ );
215
212
  }
216
213
  }
217
-
218
- const left = content ? `${identity}${IDENTITY_GAP}${content}` : identity;
219
- return composeLine(left, tail, theme, width);
214
+ return lines;
220
215
  }
221
216
 
222
217
  function stageIcon(status: WorkflowStageStatus, theme: Theme): string {
@@ -329,20 +324,20 @@ function workflowStageLines(
329
324
  return { lines, activeIndex };
330
325
  }
331
326
 
332
- /** Fit one workflow group into the remaining line budget: the primary line
333
- * always survives, and the stage window anchors on the live stage — settled
334
- * stages above the window collapse into one `… +N` marker, never the live
335
- * stage itself. */
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. */
336
331
  function fitGroupLines(
337
- primary: string,
332
+ primary: readonly string[],
338
333
  stages: readonly string[],
339
334
  activeIndex: number,
340
335
  remaining: number,
341
336
  theme: Theme,
342
337
  ): string[] {
343
- if (stages.length === 0 || stages.length + 1 <= remaining) return [primary, ...stages];
344
- const slots = remaining - 1;
345
- if (slots <= 0) return [primary];
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;
346
341
  // Reserve one line for an overflow marker so a cut is always announced.
347
342
  const room = Math.max(1, slots - 1);
348
343
  const anchor = activeIndex >= 0 ? activeIndex : 0;
@@ -354,12 +349,12 @@ function fitGroupLines(
354
349
  ...(start + room < stages.length ? [theme.fg("dim", `… +${stages.length - start - room}`)] : []),
355
350
  ];
356
351
  while (parts.length > slots) parts.pop();
357
- return [primary, ...parts];
352
+ return [...primary, ...parts];
358
353
  }
359
354
 
360
355
  /** Render active runs as compact per-run line groups: the parent model's own
361
- * line first, then one line per simple run, a tree chain per managed workflow
362
- * (parent line + one row per stage). All rows share one column layout. */
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. */
363
358
  export function formatActiveRunLines(
364
359
  runs: readonly RunView[],
365
360
  theme: Theme,
@@ -384,7 +379,7 @@ export function formatActiveRunLines(
384
379
  idWidth: Math.max(...roots.map((root) => visibleWidth(`#${root.id}`)), 0),
385
380
  agentWidth: Math.max(...roots.map((root) => visibleWidth(agentColumnText(root))), main ? visibleWidth("pi") : 0),
386
381
  };
387
- const groups: Array<{ lines: string[]; activeIndex: number }> = roots.map((root) => {
382
+ const groups: Array<{ primary: readonly string[]; stages: readonly string[]; activeIndex: number }> = roots.map((root) => {
388
383
  const children = childrenOf.get(root.id) ?? [];
389
384
  // Workflow-wide tokens/cost on the parent line: every stage snapshot plus
390
385
  // the live child (whose snapshot is frozen only at settlement). Without
@@ -395,14 +390,15 @@ export function formatActiveRunLines(
395
390
  const live = children.find((candidate) => candidate.usage.input || candidate.usage.output || candidate.usage.cost);
396
391
  usage = sumUsage([...(settled.length > 0 ? settled : [root.usage]), ...(live ? [live.usage] : [])]);
397
392
  }
398
- const lines = [primaryLine(root, theme, width, now, layout, usage)];
393
+ const primary = primaryLine(root, theme, width, now, layout, usage);
399
394
  let activeIndex = -1;
395
+ let stages: readonly string[] = [];
400
396
  if (root.managedWorkflow && root.workflowStages && root.workflowStages.length > 0) {
401
397
  const rendered = workflowStageLines(root.workflowStages, children, theme, width, now);
402
- lines.push(...rendered.lines);
398
+ stages = rendered.lines;
403
399
  activeIndex = rendered.activeIndex;
404
400
  }
405
- return { lines, activeIndex };
401
+ return { primary, stages, activeIndex };
406
402
  });
407
403
 
408
404
  const lines: string[] = [];
@@ -413,7 +409,7 @@ export function formatActiveRunLines(
413
409
  for (const group of groups) {
414
410
  const remaining = MAX_WIDGET_LINES - 1 - lines.length;
415
411
  if (remaining <= 0) break;
416
- lines.push(...fitGroupLines(group.lines[0]!, group.lines.slice(1), group.activeIndex, remaining, theme));
412
+ lines.push(...fitGroupLines(group.primary, group.stages, group.activeIndex, remaining, theme));
417
413
  shownRoots++;
418
414
  }
419
415
  const hiddenRoots = roots.length - shownRoots;