@ferris1225/pi-subagents 2.0.2 → 2.1.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/widget.ts CHANGED
@@ -1,144 +1,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
- 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
+ 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
+ }