@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/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 = statusIcon(run.status, theme);
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
- const identity = `${prefix}${icon} ${name}`;
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
- : formatTaskSummary(run.task, 64);
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 a tree: main-agent dispatches are roots and managed
144
- * documenter/reviewer/fix steps nest under the stable parent row. Fork labels
145
- * include their source id; other control ids remain available through status. */
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
- lines.push(runPrimaryLine(root, theme, width, now, ""));
169
- // The parent's managed-workflow placeholder is redundant while a child
170
- // row shows live progress; keep it only between stages.
171
- if (children.length === 0) lines.push(...runActivityLine(root, theme, width, " "));
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;