@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/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
- export function formatActiveRunLines(
33
- runs: readonly RunView[],
34
- theme: Theme,
35
- width: number,
36
- now: number = Date.now(),
37
- ): string[] {
38
- const dim = (text: string): string => theme.fg("dim", text);
39
- return runs
40
- .filter((run) => isRunActiveStatus(run.status))
41
- .flatMap((run) => {
42
- const icon = statusIcon(run.status, theme);
43
- const name = theme.fg("accent", theme.bold(run.agent));
44
- const identity = `${icon} ${dim(`#${run.id}`)} ${name}`;
45
- const elapsed = formatElapsed(run, now);
46
- // Render only the resolved model id plus thinking level. Provider auth and
47
- // other configuration never enter monitor state or this line.
48
- const modelId = run.model?.split("/").at(-1);
49
- const modelSource = formatTaskSummary(
50
- modelId ? `${modelId}${run.thinking ? `/${run.thinking}` : ""}` : run.thinking ? `thinking:${run.thinking}` : "",
51
- 64,
52
- false,
53
- );
54
- const taskSource = formatTaskSummary(run.task, 64);
55
- const primaryPartCount = 2 + (modelSource ? 1 : 0) + (elapsed ? 1 : 0);
56
- const contentWidth = Math.max(
57
- 0,
58
- width -
59
- visibleWidth(identity) -
60
- visibleWidth(elapsed) -
61
- (primaryPartCount - 1) * visibleWidth(" · "),
62
- );
63
- const modelDesired = visibleWidth(modelSource);
64
- const modelFloor = Math.min(modelDesired, Math.min(12, contentWidth));
65
- let modelWidth = modelSource
66
- ? Math.min(modelDesired, Math.max(modelFloor, contentWidth - 8))
67
- : 0;
68
- let taskWidth = contentWidth - modelWidth;
69
- if (visibleWidth(taskSource) < taskWidth) {
70
- modelWidth = Math.min(modelDesired, modelWidth + taskWidth - visibleWidth(taskSource));
71
- taskWidth = contentWidth - modelWidth;
72
- }
73
- const task = taskWidth > 0 ? formatTaskSummary(taskSource, taskWidth) : "";
74
- const modelThinking = modelWidth > 0 ? formatTaskSummary(modelSource, modelWidth, false) : "";
75
- const primaryLeft = [
76
- identity,
77
- task ? dim(task) : undefined,
78
- modelThinking ? dim(modelThinking) : undefined,
79
- ].filter((part): part is string => Boolean(part)).join(" · ");
80
- const primary = compactLine(primaryLeft, elapsed ? dim(`· ${elapsed}`) : "", width);
81
-
82
- const activity = run.activity?.trim();
83
- if (!activity) return [primary];
84
- const activityIndent = " ";
85
- const activityWidth = width - visibleWidth(activityIndent);
86
- if (activityWidth <= 0) return [primary];
87
- const activitySummary = formatTaskSummary(activity, activityWidth);
88
- if (!activitySummary) return [primary];
89
- return [primary, truncateToWidth(`${activityIndent}${dim(activitySummary)}`, width, "")];
90
- });
91
- }
92
-
93
- function hasTickingRun(): boolean {
94
- return monitor.getRuns().some(
95
- (run) => isRunActiveStatus(run.status) && run.startedAt !== undefined,
96
- );
97
- }
98
-
99
- /** Install the widget for one TUI session. Its timer exists only while at least
100
- * one active run has started and is disposed with the widget. */
101
- export function installActiveRunsWidget(ctx: Pick<ExtensionContext, "mode" | "ui">): void {
102
- if (ctx.mode !== "tui") return;
103
- ctx.ui.setWidget(
104
- SUBAGENTS_WIDGET_ID,
105
- (tui, theme) => {
106
- let timer: ReturnType<typeof setInterval> | undefined;
107
- let disposed = false;
108
-
109
- const syncTimer = (): void => {
110
- if (disposed) return;
111
- if (hasTickingRun()) {
112
- if (timer) return;
113
- timer = setInterval(() => tui.requestRender(), 1_000);
114
- timer.unref?.();
115
- return;
116
- }
117
- if (timer) clearInterval(timer);
118
- timer = undefined;
119
- };
120
-
121
- const unsubscribe = monitor.subscribe(() => {
122
- syncTimer();
123
- tui.requestRender();
124
- });
125
- syncTimer();
126
-
127
- return {
128
- render: (width: number) => formatActiveRunLines(monitor.getRuns(), theme, width),
129
- invalidate() {},
130
- dispose() {
131
- disposed = true;
132
- unsubscribe();
133
- if (timer) clearInterval(timer);
134
- timer = undefined;
135
- },
136
- };
137
- },
138
- { placement: "aboveEditor" },
139
- );
140
- }
141
-
142
- export function clearActiveRunsWidget(ctx: Pick<ExtensionContext, "mode" | "ui">): void {
143
- if (ctx.mode === "tui") ctx.ui.setWidget(SUBAGENTS_WIDGET_ID, undefined);
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
+ }