@esso0428/pi-subagents 0.17.5 → 0.17.7
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/CHANGELOG.md +14 -0
- package/CONTRIBUTING.md +4 -0
- package/README.md +1 -1
- package/dist/abortable.d.ts +13 -0
- package/dist/abortable.d.ts.map +1 -0
- package/dist/abortable.js +43 -0
- package/dist/abortable.js.map +1 -0
- package/dist/agent-color.d.ts +36 -0
- package/dist/agent-color.d.ts.map +1 -0
- package/dist/agent-color.js +124 -0
- package/dist/agent-color.js.map +1 -0
- package/dist/agent-file-toggle.d.ts +126 -0
- package/dist/agent-file-toggle.d.ts.map +1 -0
- package/dist/agent-file-toggle.js +259 -0
- package/dist/agent-file-toggle.js.map +1 -0
- package/dist/agent-history.d.ts +4 -0
- package/dist/agent-history.d.ts.map +1 -1
- package/dist/agent-history.js +47 -1
- package/dist/agent-history.js.map +1 -1
- package/dist/agent-manager.d.ts +370 -56
- package/dist/agent-manager.d.ts.map +1 -1
- package/dist/agent-manager.js +1123 -409
- package/dist/agent-manager.js.map +1 -1
- package/dist/agent-runner.d.ts +100 -10
- package/dist/agent-runner.d.ts.map +1 -1
- package/dist/agent-runner.js +166 -21
- package/dist/agent-runner.js.map +1 -1
- package/dist/agent-types.d.ts +57 -5
- package/dist/agent-types.d.ts.map +1 -1
- package/dist/agent-types.js +164 -32
- package/dist/agent-types.js.map +1 -1
- package/dist/child-context.d.ts +3 -0
- package/dist/child-context.d.ts.map +1 -0
- package/dist/child-context.js +13 -0
- package/dist/child-context.js.map +1 -0
- package/dist/cross-extension-rpc.d.ts +23 -3
- package/dist/cross-extension-rpc.d.ts.map +1 -1
- package/dist/cross-extension-rpc.js +79 -17
- package/dist/cross-extension-rpc.js.map +1 -1
- package/dist/custom-agents.d.ts +38 -1
- package/dist/custom-agents.d.ts.map +1 -1
- package/dist/custom-agents.js +164 -12
- package/dist/custom-agents.js.map +1 -1
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1908 -495
- package/dist/index.js.map +1 -1
- package/dist/invocation-config.d.ts +87 -2
- package/dist/invocation-config.d.ts.map +1 -1
- package/dist/invocation-config.js +71 -3
- package/dist/invocation-config.js.map +1 -1
- package/dist/mention-clone.d.ts +88 -0
- package/dist/mention-clone.d.ts.map +1 -0
- package/dist/mention-clone.js +154 -0
- package/dist/mention-clone.js.map +1 -0
- package/dist/mention.d.ts +82 -0
- package/dist/mention.d.ts.map +1 -0
- package/dist/mention.js +132 -0
- package/dist/mention.js.map +1 -0
- package/dist/model-resolver.d.ts +17 -0
- package/dist/model-resolver.d.ts.map +1 -1
- package/dist/model-resolver.js +15 -0
- package/dist/model-resolver.js.map +1 -1
- package/dist/model-scope.d.ts +50 -0
- package/dist/model-scope.d.ts.map +1 -0
- package/dist/model-scope.js +49 -0
- package/dist/model-scope.js.map +1 -0
- package/dist/nested-tools.d.ts +57 -0
- package/dist/nested-tools.d.ts.map +1 -0
- package/dist/nested-tools.js +301 -0
- package/dist/nested-tools.js.map +1 -0
- package/dist/output-file.d.ts +22 -3
- package/dist/output-file.d.ts.map +1 -1
- package/dist/output-file.js +58 -7
- package/dist/output-file.js.map +1 -1
- package/dist/prompts.d.ts +23 -0
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +20 -2
- package/dist/prompts.js.map +1 -1
- package/dist/schedule.d.ts.map +1 -1
- package/dist/schedule.js +36 -15
- package/dist/schedule.js.map +1 -1
- package/dist/settings.d.ts +228 -2
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +94 -0
- package/dist/settings.js.map +1 -1
- package/dist/status-note.d.ts +49 -1
- package/dist/status-note.d.ts.map +1 -1
- package/dist/status-note.js +62 -1
- package/dist/status-note.js.map +1 -1
- package/dist/structured-output.d.ts +62 -0
- package/dist/structured-output.d.ts.map +1 -0
- package/dist/structured-output.js +113 -0
- package/dist/structured-output.js.map +1 -0
- package/dist/types.d.ts +176 -10
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/agent-mention.d.ts +83 -0
- package/dist/ui/agent-mention.d.ts.map +1 -0
- package/dist/ui/agent-mention.js +188 -0
- package/dist/ui/agent-mention.js.map +1 -0
- package/dist/ui/agent-widget.d.ts +96 -75
- package/dist/ui/agent-widget.d.ts.map +1 -1
- package/dist/ui/agent-widget.js +397 -420
- package/dist/ui/agent-widget.js.map +1 -1
- package/dist/ui/conversation-blocks.d.ts.map +1 -1
- package/dist/ui/conversation-blocks.js +6 -0
- package/dist/ui/conversation-blocks.js.map +1 -1
- package/dist/ui/conversation-timeline.d.ts +10 -2
- package/dist/ui/conversation-timeline.d.ts.map +1 -1
- package/dist/ui/conversation-timeline.js +130 -23
- package/dist/ui/conversation-timeline.js.map +1 -1
- package/dist/ui/conversation-viewer.d.ts +20 -5
- package/dist/ui/conversation-viewer.d.ts.map +1 -1
- package/dist/ui/conversation-viewer.js +274 -73
- package/dist/ui/conversation-viewer.js.map +1 -1
- package/dist/ui/fleet-list.d.ts +198 -0
- package/dist/ui/fleet-list.d.ts.map +1 -0
- package/dist/ui/fleet-list.js +487 -0
- package/dist/ui/fleet-list.js.map +1 -0
- package/dist/ui/schedule-menu.d.ts.map +1 -1
- package/dist/ui/schedule-menu.js +6 -7
- package/dist/ui/schedule-menu.js.map +1 -1
- package/dist/ui/select-item.d.ts +28 -0
- package/dist/ui/select-item.d.ts.map +1 -0
- package/dist/ui/select-item.js +35 -0
- package/dist/ui/select-item.js.map +1 -0
- package/dist/ui/workflow-card.d.ts +176 -0
- package/dist/ui/workflow-card.d.ts.map +1 -0
- package/dist/ui/workflow-card.js +333 -0
- package/dist/ui/workflow-card.js.map +1 -0
- package/dist/ui/workflow-dialog.d.ts +306 -0
- package/dist/ui/workflow-dialog.d.ts.map +1 -0
- package/dist/ui/workflow-dialog.js +844 -0
- package/dist/ui/workflow-dialog.js.map +1 -0
- package/dist/ui/workflow-menu.d.ts +61 -0
- package/dist/ui/workflow-menu.d.ts.map +1 -0
- package/dist/ui/workflow-menu.js +148 -0
- package/dist/ui/workflow-menu.js.map +1 -0
- package/dist/usage.d.ts +86 -1
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js +72 -1
- package/dist/usage.js.map +1 -1
- package/dist/workflow/collisions.d.ts +96 -0
- package/dist/workflow/collisions.d.ts.map +1 -0
- package/dist/workflow/collisions.js +89 -0
- package/dist/workflow/collisions.js.map +1 -0
- package/dist/workflow/entry.d.ts +33 -0
- package/dist/workflow/entry.d.ts.map +1 -0
- package/dist/workflow/entry.js +30 -0
- package/dist/workflow/entry.js.map +1 -0
- package/dist/workflow/host.d.ts +63 -0
- package/dist/workflow/host.d.ts.map +1 -0
- package/dist/workflow/host.js +363 -0
- package/dist/workflow/host.js.map +1 -0
- package/dist/workflow/journal.d.ts +98 -0
- package/dist/workflow/journal.d.ts.map +1 -0
- package/dist/workflow/journal.js +121 -0
- package/dist/workflow/journal.js.map +1 -0
- package/dist/workflow/json-schema.d.ts +52 -0
- package/dist/workflow/json-schema.d.ts.map +1 -0
- package/dist/workflow/json-schema.js +112 -0
- package/dist/workflow/json-schema.js.map +1 -0
- package/dist/workflow/meta.d.ts +68 -0
- package/dist/workflow/meta.d.ts.map +1 -0
- package/dist/workflow/meta.js +318 -0
- package/dist/workflow/meta.js.map +1 -0
- package/dist/workflow/progress.d.ts +225 -0
- package/dist/workflow/progress.d.ts.map +1 -0
- package/dist/workflow/progress.js +362 -0
- package/dist/workflow/progress.js.map +1 -0
- package/dist/workflow/runtime.d.ts +335 -0
- package/dist/workflow/runtime.d.ts.map +1 -0
- package/dist/workflow/runtime.js +831 -0
- package/dist/workflow/runtime.js.map +1 -0
- package/dist/workflow/saved.d.ts +91 -0
- package/dist/workflow/saved.d.ts.map +1 -0
- package/dist/workflow/saved.js +204 -0
- package/dist/workflow/saved.js.map +1 -0
- package/dist/workflow/task.d.ts +137 -0
- package/dist/workflow/task.d.ts.map +1 -0
- package/dist/workflow/task.js +208 -0
- package/dist/workflow/task.js.map +1 -0
- package/dist/workflow/tool-description.d.ts +39 -0
- package/dist/workflow/tool-description.d.ts.map +1 -0
- package/dist/workflow/tool-description.js +200 -0
- package/dist/workflow/tool-description.js.map +1 -0
- package/dist/workflow/worker-source.d.ts +48 -0
- package/dist/workflow/worker-source.d.ts.map +1 -0
- package/dist/workflow/worker-source.js +779 -0
- package/dist/workflow/worker-source.js.map +1 -0
- package/dist/worktree.d.ts +10 -3
- package/dist/worktree.d.ts.map +1 -1
- package/dist/worktree.js +58 -54
- package/dist/worktree.js.map +1 -1
- package/dist/xml.d.ts +11 -0
- package/dist/xml.d.ts.map +1 -0
- package/dist/xml.js +13 -0
- package/dist/xml.js.map +1 -0
- package/docs/rpc.md +183 -0
- package/docs/superpowers/plans/2026-09-30-conversation-viewer-scrollbar.md +216 -0
- package/docs/superpowers/plans/2026-09-30-upstream-event-workflow-partial-history.md +195 -0
- package/docs/superpowers/specs/2026-09-30-conversation-viewer-scrollbar-design.md +82 -0
- package/docs/superpowers/specs/2026-09-30-upstream-event-workflow-partial-history-design.md +49 -0
- package/docs/workflows.md +437 -0
- package/examples/agent-tool-description.md +7 -7
- package/examples/workflows/compose.js +51 -0
- package/examples/workflows/fan-out-audit.js +47 -0
- package/examples/workflows/gated-fix.js +60 -0
- package/examples/workflows/lib/count-child.js +27 -0
- package/examples/workflows/review-panel.js +63 -0
- package/examples/workflows/structured-findings.js +78 -0
- package/package.json +1 -1
- package/src/abortable.ts +43 -0
- package/src/agent-color.ts +161 -0
- package/src/agent-file-toggle.ts +269 -0
- package/src/agent-history.ts +54 -2
- package/src/agent-manager.ts +1263 -402
- package/src/agent-runner.ts +251 -27
- package/src/agent-types.ts +188 -32
- package/src/child-context.ts +15 -0
- package/src/cross-extension-rpc.ts +96 -20
- package/src/custom-agents.ts +170 -13
- package/src/index.ts +2024 -537
- package/src/invocation-config.ts +118 -3
- package/src/mention-clone.ts +196 -0
- package/src/mention.ts +141 -0
- package/src/model-resolver.ts +18 -0
- package/src/model-scope.ts +70 -0
- package/src/nested-tools.ts +424 -0
- package/src/output-file.ts +61 -6
- package/src/prompts.ts +45 -2
- package/src/schedule.ts +35 -14
- package/src/settings.ts +312 -2
- package/src/status-note.ts +66 -1
- package/src/structured-output.ts +130 -0
- package/src/types.ts +177 -10
- package/src/ui/agent-mention.ts +216 -0
- package/src/ui/agent-widget.ts +389 -441
- package/src/ui/conversation-blocks.ts +6 -0
- package/src/ui/conversation-timeline.ts +139 -25
- package/src/ui/conversation-viewer.ts +284 -69
- package/src/ui/fleet-list.ts +558 -0
- package/src/ui/schedule-menu.ts +9 -8
- package/src/ui/select-item.ts +45 -0
- package/src/ui/workflow-card.ts +470 -0
- package/src/ui/workflow-dialog.ts +1115 -0
- package/src/ui/workflow-menu.ts +193 -0
- package/src/usage.ts +109 -2
- package/src/workflow/collisions.ts +123 -0
- package/src/workflow/entry.ts +47 -0
- package/src/workflow/host.ts +403 -0
- package/src/workflow/journal.ts +164 -0
- package/src/workflow/json-schema.ts +128 -0
- package/src/workflow/meta.ts +325 -0
- package/src/workflow/progress.ts +550 -0
- package/src/workflow/runtime.ts +1219 -0
- package/src/workflow/saved.ts +217 -0
- package/src/workflow/task.ts +302 -0
- package/src/workflow/tool-description.ts +200 -0
- package/src/workflow/worker-source.ts +781 -0
- package/src/worktree.ts +69 -55
- package/src/xml.ts +13 -0
- package/vitest.config.ts +0 -18
package/dist/ui/agent-widget.js
CHANGED
|
@@ -4,23 +4,17 @@
|
|
|
4
4
|
* Displays a tree of agents with animated spinners, live stats, and activity descriptions.
|
|
5
5
|
* Uses the callback form of setWidget for themed rendering.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
8
|
+
import { renderAgentName } from "../agent-color.js";
|
|
9
|
+
import { isTopLevelAgent } from "../agent-manager.js";
|
|
8
10
|
import { getConfig } from "../agent-types.js";
|
|
9
|
-
import { getLifetimeTotal, getSessionContextPercent } from "../usage.js";
|
|
11
|
+
import { getLifetimeCost, getLifetimeTotal, getSessionContextPercent } from "../usage.js";
|
|
10
12
|
// ---- Constants ----
|
|
11
13
|
/** Maximum number of rendered lines before overflow collapse kicks in. */
|
|
12
14
|
export const MAX_WIDGET_LINES = 12;
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
* Derive the widget ceiling from the terminal height while retaining the
|
|
17
|
-
* historical MAX_WIDGET_LINES ceiling. Very short terminals may have no room
|
|
18
|
-
* for the widget; that is preferable to consuming the editor/input area.
|
|
19
|
-
*/
|
|
20
|
-
export function getWidgetLineBudget(rows) {
|
|
21
|
-
if (!Number.isFinite(rows))
|
|
22
|
-
return MAX_WIDGET_LINES;
|
|
23
|
-
return Math.min(MAX_WIDGET_LINES, Math.max(0, Math.floor(rows) - MIN_EDITOR_LINES));
|
|
15
|
+
/** Number of lines available above an editor, retaining room for the prompt. */
|
|
16
|
+
export function getWidgetLineBudget(terminalRows) {
|
|
17
|
+
return Math.max(1, Math.min(MAX_WIDGET_LINES, Math.floor(terminalRows) - 4));
|
|
24
18
|
}
|
|
25
19
|
/** Braille spinner frames for animated running indicator. */
|
|
26
20
|
export const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
@@ -51,6 +45,33 @@ export function formatTokens(count) {
|
|
|
51
45
|
return `${(count / 1_000).toFixed(1)}k token`;
|
|
52
46
|
return `${count} token`;
|
|
53
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Format a cost as `~$0.0042`, or "" when there is nothing to show.
|
|
50
|
+
*
|
|
51
|
+
* The tilde is load-bearing: this is pi's own estimate from the model's listed
|
|
52
|
+
* rates, not a billed figure, and the surfaces that print it sit next to token
|
|
53
|
+
* counts that ARE exact.
|
|
54
|
+
*
|
|
55
|
+
* Nothing is printed for zero, which is also what a model with no pricing data
|
|
56
|
+
* reports: `$0.00` beside a local model's tokens would claim its cost was
|
|
57
|
+
* measured and found to be nothing, rather than never measured at all. For the
|
|
58
|
+
* same reason a real cost too small for four decimals reads `<$0.0001` — it was
|
|
59
|
+
* measured, and rounding it to `~$0.0000` would say the opposite.
|
|
60
|
+
*/
|
|
61
|
+
export function formatCost(cost) {
|
|
62
|
+
if (!(cost > 0))
|
|
63
|
+
return ""; // also catches NaN
|
|
64
|
+
if (cost < 0.0001)
|
|
65
|
+
return "<$0.0001";
|
|
66
|
+
if (cost >= 1)
|
|
67
|
+
return `~$${cost.toFixed(2)}`;
|
|
68
|
+
// Under a dollar: cents at minimum, four decimals at most, nothing trailing.
|
|
69
|
+
// Most single runs land between a tenth of a cent and a dime, where rounding
|
|
70
|
+
// to cents would collapse a 4x difference in spend into the same figure.
|
|
71
|
+
const rounded = Number(cost.toFixed(4));
|
|
72
|
+
const decimals = (String(rounded).split(".")[1] ?? "").length;
|
|
73
|
+
return `~$${rounded.toFixed(Math.max(2, decimals))}`;
|
|
74
|
+
}
|
|
54
75
|
/**
|
|
55
76
|
* Token count with optional context-fill % and compaction-count annotations.
|
|
56
77
|
* Thresholds for percent: <70% dim, 70–85% warning, ≥85% error.
|
|
@@ -98,12 +119,19 @@ export function getPromptModeLabel(type) {
|
|
|
98
119
|
const config = getConfig(type);
|
|
99
120
|
return config.promptMode === "append" ? "twin" : undefined;
|
|
100
121
|
}
|
|
101
|
-
/**
|
|
122
|
+
/**
|
|
123
|
+
* Mode label is not included — callers add it where they want it.
|
|
124
|
+
*
|
|
125
|
+
* Both model forms come back so each surface can pick by width; the
|
|
126
|
+
* "(asked X)" annotation is applied here rather than by callers, so a value the
|
|
127
|
+
* spawn did not honor cannot be rendered as though it had been (#182).
|
|
128
|
+
*/
|
|
102
129
|
export function buildInvocationTags(invocation) {
|
|
103
130
|
const tags = [];
|
|
104
131
|
if (!invocation)
|
|
105
132
|
return { tags };
|
|
106
|
-
const
|
|
133
|
+
const asked = (value, requested) => value && requested && requested !== value ? `${value} (asked ${requested})` : value;
|
|
134
|
+
const thinking = asked(invocation.thinking, invocation.requestedThinking);
|
|
107
135
|
if (thinking)
|
|
108
136
|
tags.push(`thinking: ${thinking}`);
|
|
109
137
|
if (invocation.isolated)
|
|
@@ -116,11 +144,15 @@ export function buildInvocationTags(invocation) {
|
|
|
116
144
|
tags.push("background");
|
|
117
145
|
if (invocation.maxTurns != null)
|
|
118
146
|
tags.push(`max turns: ${invocation.maxTurns}`);
|
|
119
|
-
return {
|
|
147
|
+
return {
|
|
148
|
+
modelName: asked(invocation.modelName, invocation.requestedModel),
|
|
149
|
+
modelId: asked(invocation.modelId, invocation.requestedModel),
|
|
150
|
+
tags,
|
|
151
|
+
};
|
|
120
152
|
}
|
|
121
|
-
/**
|
|
153
|
+
/** Truncate text to a single line, max `len` chars. */
|
|
122
154
|
function truncateLine(text, len = 60) {
|
|
123
|
-
const line = text.
|
|
155
|
+
const line = text.split("\n").find(l => l.trim())?.trim() ?? "";
|
|
124
156
|
if (line.length <= len)
|
|
125
157
|
return line;
|
|
126
158
|
return line.slice(0, len) + "…";
|
|
@@ -155,37 +187,60 @@ export class AgentWidget {
|
|
|
155
187
|
manager;
|
|
156
188
|
agentActivity;
|
|
157
189
|
mode;
|
|
158
|
-
options;
|
|
159
190
|
uiCtx;
|
|
160
191
|
widgetFrame = 0;
|
|
161
192
|
widgetInterval;
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
selectedAgentId;
|
|
167
|
-
/** Last logical roster index of the selected row, used when it disappears. */
|
|
168
|
-
selectedRosterIndex = 0;
|
|
169
|
-
/** First logical row currently represented by the bounded viewport. */
|
|
170
|
-
viewportStart = 0;
|
|
193
|
+
/** Tracks how many turns each finished agent has survived. Key: agent ID, Value: turns since finished. */
|
|
194
|
+
finishedTurnAge = new Map();
|
|
195
|
+
/** How many extra turns errors/aborted agents linger (completed agents clear after 1 turn). */
|
|
196
|
+
static ERROR_LINGER_TURNS = 2;
|
|
171
197
|
/** Whether the widget callback is currently registered with the TUI. */
|
|
172
198
|
widgetRegistered = false;
|
|
173
199
|
/** Cached TUI reference from widget factory callback, used for requestRender(). */
|
|
174
200
|
tui;
|
|
175
201
|
/** Last status bar text, used to avoid redundant setStatus calls. */
|
|
176
202
|
lastStatusText;
|
|
177
|
-
|
|
178
|
-
|
|
203
|
+
lastSnapshot = "";
|
|
204
|
+
inputCleanup;
|
|
205
|
+
navigationActive = false;
|
|
206
|
+
selectedAgentId;
|
|
207
|
+
viewportStart = 0;
|
|
208
|
+
selectedRosterIndex = 0;
|
|
209
|
+
openOptions;
|
|
210
|
+
showCost;
|
|
211
|
+
showModel;
|
|
179
212
|
constructor(manager, agentActivity,
|
|
180
|
-
/**
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
213
|
+
/**
|
|
214
|
+
* Read live at render time. Selects which agents the widget shows — see
|
|
215
|
+
* `WidgetMode`. Defaults to `"all"` when a caller supplies no policy; the
|
|
216
|
+
* extension supplies one defaulting to `"background"`.
|
|
217
|
+
*/
|
|
218
|
+
mode = () => "all",
|
|
219
|
+
/**
|
|
220
|
+
* Read live at render time, like `mode`. Whether running agents show an
|
|
221
|
+
* estimated cost beside their token count. Defaults to off — the extension
|
|
222
|
+
* supplies the user's `showCost` setting.
|
|
223
|
+
*/
|
|
224
|
+
/**
|
|
225
|
+
* Read live at render time, like `mode`. Whether running agents name the
|
|
226
|
+
* model driving them and the thinking level it is running at. Defaults to
|
|
227
|
+
* off — the extension supplies the user's `showModel` setting — because the
|
|
228
|
+
* row is already dense and the same pair is on the tool result and in the
|
|
229
|
+
* conversation viewer unconditionally.
|
|
230
|
+
*/
|
|
231
|
+
showCostOrOptions = () => false, showModel = () => false) {
|
|
185
232
|
this.manager = manager;
|
|
186
233
|
this.agentActivity = agentActivity;
|
|
187
234
|
this.mode = mode;
|
|
188
|
-
|
|
235
|
+
if (typeof showCostOrOptions === "function") {
|
|
236
|
+
this.showCost = showCostOrOptions;
|
|
237
|
+
this.openOptions = {};
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
this.showCost = () => false;
|
|
241
|
+
this.openOptions = showCostOrOptions;
|
|
242
|
+
}
|
|
243
|
+
this.showModel = showModel;
|
|
189
244
|
}
|
|
190
245
|
/**
|
|
191
246
|
* Agents eligible for the widget, per the current `WidgetMode`:
|
|
@@ -199,7 +254,7 @@ export class AgentWidget {
|
|
|
199
254
|
* - `all`: every agent.
|
|
200
255
|
*/
|
|
201
256
|
widgetAgents() {
|
|
202
|
-
const all = this.manager.listAgents();
|
|
257
|
+
const all = this.manager.listAgents().filter(isTopLevelAgent);
|
|
203
258
|
switch (this.mode()) {
|
|
204
259
|
case "off": return [];
|
|
205
260
|
case "background": return all.filter(a => a.isBackground !== false);
|
|
@@ -208,181 +263,135 @@ export class AgentWidget {
|
|
|
208
263
|
}
|
|
209
264
|
/** Set the UI context (grabbed from first tool execution). */
|
|
210
265
|
setUICtx(ctx) {
|
|
211
|
-
if (ctx
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
this.selectedAgentId = undefined;
|
|
224
|
-
this.selectedRosterIndex = 0;
|
|
225
|
-
this.viewportStart = 0;
|
|
226
|
-
// Print/RPC tests and lightweight embedders may provide only the widget
|
|
227
|
-
// surface; real interactive contexts always implement this hook.
|
|
228
|
-
if (typeof ctx.onTerminalInput === "function") {
|
|
229
|
-
this.inputUnsub = ctx.onTerminalInput(data => this.handleKey(data));
|
|
266
|
+
if (ctx !== this.uiCtx) {
|
|
267
|
+
this.inputCleanup?.();
|
|
268
|
+
this.inputCleanup = undefined;
|
|
269
|
+
this.uiCtx = ctx;
|
|
270
|
+
this.widgetRegistered = false;
|
|
271
|
+
this.tui = undefined;
|
|
272
|
+
this.lastStatusText = undefined;
|
|
273
|
+
this.navigationActive = false;
|
|
274
|
+
this.selectedAgentId = undefined;
|
|
275
|
+
this.viewportStart = 0;
|
|
276
|
+
if (ctx.onTerminalInput)
|
|
277
|
+
this.inputCleanup = ctx.onTerminalInput(data => this.handleInput(data));
|
|
230
278
|
}
|
|
231
|
-
return true;
|
|
232
|
-
}
|
|
233
|
-
/** Request a render on the currently registered TUI without touching input. */
|
|
234
|
-
requestUiRefresh(force = true) {
|
|
235
|
-
if (!this.tui)
|
|
236
|
-
return false;
|
|
237
|
-
this.tui.requestRender(force);
|
|
238
|
-
return true;
|
|
239
279
|
}
|
|
240
|
-
/**
|
|
280
|
+
/**
|
|
281
|
+
* Called on each new turn (tool_execution_start).
|
|
282
|
+
* Ages finished agents and clears those that have lingered long enough.
|
|
283
|
+
*/
|
|
241
284
|
onTurnStart() {
|
|
285
|
+
// Age all finished agents
|
|
286
|
+
for (const [id, age] of this.finishedTurnAge) {
|
|
287
|
+
this.finishedTurnAge.set(id, age + 1);
|
|
288
|
+
}
|
|
289
|
+
// Trigger a widget refresh (will filter out expired agents)
|
|
242
290
|
this.update();
|
|
243
291
|
}
|
|
244
|
-
/**
|
|
292
|
+
/** Ensure the widget update timer is running. */
|
|
245
293
|
ensureTimer() {
|
|
246
|
-
if (!this.uiCtx || !this.widgetAgents().some(a => a.status === "running"))
|
|
247
|
-
return;
|
|
248
294
|
if (!this.widgetInterval) {
|
|
249
|
-
this.widgetInterval = setInterval(() => this.update(
|
|
295
|
+
this.widgetInterval = setInterval(() => this.update(), 80);
|
|
250
296
|
}
|
|
251
297
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
298
|
+
/** Check if a finished agent should still be shown in the widget. */
|
|
299
|
+
shouldShowFinished(agentId, status) {
|
|
300
|
+
const age = this.finishedTurnAge.get(agentId) ?? 0;
|
|
301
|
+
const maxAge = ERROR_STATUSES.has(status) ? AgentWidget.ERROR_LINGER_TURNS : 1;
|
|
302
|
+
return age < maxAge;
|
|
303
|
+
}
|
|
304
|
+
/** Record an agent as finished (call when agent completes). */
|
|
305
|
+
markFinished(agentId) {
|
|
306
|
+
if (!this.finishedTurnAge.has(agentId)) {
|
|
307
|
+
this.finishedTurnAge.set(agentId, 0);
|
|
259
308
|
}
|
|
260
309
|
}
|
|
261
310
|
/**
|
|
262
|
-
*
|
|
263
|
-
*
|
|
311
|
+
* Drop an agent's finished-age (call when a settled agent starts running
|
|
312
|
+
* again, i.e. a background resume). markFinished only seeds an age it has not
|
|
313
|
+
* seen before, so a resumed agent would otherwise keep the age from its
|
|
314
|
+
* previous run — already past the linger limit, hiding the new run's
|
|
315
|
+
* completion line entirely.
|
|
264
316
|
*/
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
* Records represented by selectable rows in the above-editor widget.
|
|
268
|
-
*
|
|
269
|
-
* Keep this order as the single source of truth for both rendering and key
|
|
270
|
-
* navigation. `listAgents()` is newest-first; active rows come first so the
|
|
271
|
-
* panel exposes currently useful work before terminal history.
|
|
272
|
-
*/
|
|
273
|
-
roster() {
|
|
274
|
-
const agents = this.widgetAgents();
|
|
275
|
-
const finished = agents.filter(record => record.status !== "running" && record.status !== "queued"
|
|
276
|
-
&& record.completedAt !== undefined
|
|
277
|
-
&& this.options.canOpenHistory(record));
|
|
278
|
-
const running = agents.filter(record => record.status === "running");
|
|
279
|
-
const queued = agents.filter(record => record.status === "queued");
|
|
280
|
-
return [...running, ...queued, ...finished];
|
|
281
|
-
}
|
|
282
|
-
/** True when pi's prompt editor owns the keyboard. */
|
|
283
|
-
editorHasFocus() {
|
|
284
|
-
const focused = this.tui?.focusedComponent;
|
|
285
|
-
return focused == null || focused instanceof Editor;
|
|
317
|
+
markRunning(agentId) {
|
|
318
|
+
this.finishedTurnAge.delete(agentId);
|
|
286
319
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
// where the user left off. Lifecycle resets (context, dispose, empty
|
|
295
|
-
// roster) clear this state explicitly instead of treating every exit as a
|
|
296
|
-
// reset.
|
|
297
|
-
this.navigationActive = false;
|
|
298
|
-
this.update();
|
|
299
|
-
}
|
|
300
|
-
/** Resume navigation from the retained row, or select the first row. */
|
|
301
|
-
activate(records) {
|
|
302
|
-
this.navigationActive = true;
|
|
303
|
-
const selectedIndex = this.selectedIndexOf(records);
|
|
304
|
-
if (selectedIndex >= 0) {
|
|
305
|
-
this.selectedRosterIndex = selectedIndex;
|
|
306
|
-
}
|
|
307
|
-
else {
|
|
308
|
-
// The previously selected row may have been cleaned up while navigation
|
|
309
|
-
// was inactive. Resume at its old logical position, clamped to the new
|
|
310
|
-
// roster, rather than jumping back to the first row.
|
|
311
|
-
const fallbackIndex = Math.max(0, Math.min(this.selectedRosterIndex, records.length - 1));
|
|
312
|
-
this.selectedAgentId = records[fallbackIndex].id;
|
|
313
|
-
this.selectedRosterIndex = fallbackIndex;
|
|
314
|
-
this.viewportStart = Math.min(this.viewportStart, Math.max(0, records.length - 1));
|
|
315
|
-
}
|
|
316
|
-
this.update();
|
|
317
|
-
}
|
|
318
|
-
/** Move the selected row, activating only from an empty focused editor. */
|
|
319
|
-
moveSelection(direction) {
|
|
320
|
-
const records = this.roster();
|
|
321
|
-
const ui = this.uiCtx;
|
|
322
|
-
if (records.length === 0 || !ui)
|
|
323
|
-
return false;
|
|
324
|
-
if (!this.navigationActive) {
|
|
325
|
-
if (direction !== 1 || !this.editorHasFocus() || (ui.getEditorText?.() ?? "") !== "")
|
|
326
|
-
return false;
|
|
327
|
-
this.activate(records);
|
|
328
|
-
return true;
|
|
329
|
-
}
|
|
330
|
-
const currentIndex = Math.max(0, this.selectedIndexOf(records));
|
|
331
|
-
if (direction === -1 && currentIndex === 0) {
|
|
332
|
-
this.deactivate();
|
|
333
|
-
return true;
|
|
334
|
-
}
|
|
335
|
-
const nextIndex = Math.max(0, Math.min(records.length - 1, currentIndex + direction));
|
|
336
|
-
this.selectedAgentId = records[nextIndex].id;
|
|
337
|
-
this.selectedRosterIndex = nextIndex;
|
|
338
|
-
this.update();
|
|
339
|
-
return true;
|
|
320
|
+
navigationAgents() {
|
|
321
|
+
const agents = this.widgetAgents().filter(agent => agent.status === "running" || agent.status === "queued" || (agent.completedAt !== undefined && this.shouldShowFinished(agent.id, agent.status)));
|
|
322
|
+
return agents.sort((a, b) => {
|
|
323
|
+
const activeA = a.status === "running" || a.status === "queued" ? 0 : 1;
|
|
324
|
+
const activeB = b.status === "running" || b.status === "queued" ? 0 : 1;
|
|
325
|
+
return activeA - activeB;
|
|
326
|
+
});
|
|
340
327
|
}
|
|
341
|
-
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
if (
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
328
|
+
updateViewport(roster) {
|
|
329
|
+
const budget = getWidgetLineBudget(this.tui?.terminal?.rows ?? 24);
|
|
330
|
+
const visibleRows = Math.max(1, budget - 1);
|
|
331
|
+
if (this.selectedRosterIndex < this.viewportStart)
|
|
332
|
+
this.viewportStart = this.selectedRosterIndex;
|
|
333
|
+
if (this.selectedRosterIndex >= this.viewportStart + visibleRows)
|
|
334
|
+
this.viewportStart = this.selectedRosterIndex - visibleRows + 1;
|
|
335
|
+
this.viewportStart = Math.max(0, Math.min(this.viewportStart, Math.max(0, roster.length - visibleRows)));
|
|
348
336
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
return undefined;
|
|
353
|
-
if (!this.editorHasFocus()) {
|
|
354
|
-
if (this.navigationActive)
|
|
355
|
-
this.deactivate();
|
|
356
|
-
return undefined;
|
|
357
|
-
}
|
|
337
|
+
handleInput(data) {
|
|
338
|
+
const down = data === "\u001b[B";
|
|
339
|
+
const up = data === "\u001b[A";
|
|
358
340
|
if (!this.navigationActive) {
|
|
359
|
-
|
|
360
|
-
if (!matchesKey(data, "down") || (this.uiCtx.getEditorText?.() ?? "") !== "" || records.length === 0) {
|
|
341
|
+
if (!down || !this.tui || this.uiCtx?.getEditorText?.() !== "")
|
|
361
342
|
return undefined;
|
|
362
|
-
|
|
363
|
-
|
|
343
|
+
const roster = this.navigationAgents();
|
|
344
|
+
if (roster.length === 0)
|
|
345
|
+
return undefined;
|
|
346
|
+
this.navigationActive = true;
|
|
347
|
+
const index = this.selectedAgentId ? roster.findIndex(agent => agent.id === this.selectedAgentId) : -1;
|
|
348
|
+
this.selectedRosterIndex = index >= 0 ? index : 0;
|
|
349
|
+
this.selectedAgentId = roster[this.selectedRosterIndex]?.id;
|
|
350
|
+
this.updateViewport(roster);
|
|
351
|
+
this.requestUiRefresh();
|
|
364
352
|
return { consume: true };
|
|
365
353
|
}
|
|
366
|
-
if (
|
|
367
|
-
this.
|
|
354
|
+
if (data === "\u001b") {
|
|
355
|
+
this.navigationActive = false;
|
|
356
|
+
this.requestUiRefresh();
|
|
368
357
|
return { consume: true };
|
|
369
358
|
}
|
|
370
|
-
if (
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
359
|
+
if (down || up) {
|
|
360
|
+
const roster = this.navigationAgents();
|
|
361
|
+
if (roster.length === 0)
|
|
362
|
+
return { consume: true };
|
|
363
|
+
if (up && this.selectedRosterIndex === 0) {
|
|
364
|
+
this.navigationActive = false;
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
this.selectedRosterIndex = Math.max(0, Math.min(roster.length - 1, this.selectedRosterIndex + (down ? 1 : -1)));
|
|
368
|
+
this.selectedAgentId = roster[this.selectedRosterIndex]?.id;
|
|
369
|
+
this.updateViewport(roster);
|
|
370
|
+
}
|
|
371
|
+
this.requestUiRefresh();
|
|
376
372
|
return { consume: true };
|
|
377
373
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
374
|
+
if (data === "\r" || data === "\n") {
|
|
375
|
+
const record = this.navigationAgents()[this.selectedRosterIndex];
|
|
376
|
+
if (record) {
|
|
377
|
+
const terminal = record.status !== "running" && record.status !== "queued";
|
|
378
|
+
if (!terminal || (this.openOptions.canOpenHistory?.(record) ?? true)) {
|
|
379
|
+
this.openOptions.onOpen?.(record, terminal ? "history" : "live");
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return { consume: true };
|
|
383
|
+
}
|
|
384
|
+
this.navigationActive = false;
|
|
381
385
|
return undefined;
|
|
382
386
|
}
|
|
387
|
+
requestUiRefresh(force = false) {
|
|
388
|
+
if (!this.tui || typeof this.tui.requestRender !== "function")
|
|
389
|
+
return false;
|
|
390
|
+
this.tui.requestRender(force);
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
383
393
|
/** Render a finished agent line. */
|
|
384
394
|
renderFinishedLine(a, theme) {
|
|
385
|
-
const name = getDisplayName(a.type);
|
|
386
395
|
const modeLabel = getPromptModeLabel(a.type);
|
|
387
396
|
const duration = formatMs((a.completedAt ?? Date.now()) - a.startedAt);
|
|
388
397
|
let icon;
|
|
@@ -401,7 +410,7 @@ export class AgentWidget {
|
|
|
401
410
|
}
|
|
402
411
|
else if (a.status === "error") {
|
|
403
412
|
icon = theme.fg("error", "✗");
|
|
404
|
-
const errMsg = a.error ? `: ${
|
|
413
|
+
const errMsg = a.error ? `: ${a.error.slice(0, 60)}` : "";
|
|
405
414
|
statusText = theme.fg("error", ` error${errMsg}`);
|
|
406
415
|
}
|
|
407
416
|
else {
|
|
@@ -415,9 +424,16 @@ export class AgentWidget {
|
|
|
415
424
|
parts.push(formatTurns(activity.turnCount, activity.maxTurns));
|
|
416
425
|
if (a.toolUses > 0)
|
|
417
426
|
parts.push(`${a.toolUses} tool use${a.toolUses === 1 ? "" : "s"}`);
|
|
427
|
+
// From the record, not the activity tracker: that entry is deleted the
|
|
428
|
+
// moment an agent finishes, and "what did it cost" is a question asked
|
|
429
|
+
// about finished agents.
|
|
430
|
+
const costText = this.showCost() ? formatCost(getLifetimeCost(a.lifetimeUsage)) : "";
|
|
431
|
+
if (costText)
|
|
432
|
+
parts.push(costText);
|
|
418
433
|
parts.push(duration);
|
|
419
434
|
const modeTag = modeLabel ? ` ${theme.fg("dim", `(${modeLabel})`)}` : "";
|
|
420
|
-
|
|
435
|
+
const description = a.description.replace(/[\r\n]+/g, " ");
|
|
436
|
+
return `${icon} ${renderAgentName(a.type, theme, { fallbackColor: "dim" })}${modeTag} ${theme.fg("dim", description)} ${theme.fg("dim", "·")} ${theme.fg("dim", parts.join(" · "))}${statusText}`;
|
|
421
437
|
}
|
|
422
438
|
/**
|
|
423
439
|
* Render the widget content. Called from the registered widget's render() callback,
|
|
@@ -427,19 +443,16 @@ export class AgentWidget {
|
|
|
427
443
|
const allAgents = this.widgetAgents();
|
|
428
444
|
const running = allAgents.filter(a => a.status === "running");
|
|
429
445
|
const queued = allAgents.filter(a => a.status === "queued");
|
|
430
|
-
const finished = allAgents.filter(a => a.status !== "running" && a.status !== "queued" && a.completedAt
|
|
431
|
-
&& this.
|
|
432
|
-
const selectedId = this.navigationActive ? this.selectedAgentId : undefined;
|
|
446
|
+
const finished = allAgents.filter(a => a.status !== "running" && a.status !== "queued" && a.completedAt
|
|
447
|
+
&& this.shouldShowFinished(a.id, a.status));
|
|
433
448
|
const hasActive = running.length > 0 || queued.length > 0;
|
|
434
449
|
const hasFinished = finished.length > 0;
|
|
435
450
|
// Nothing to show — return empty (widget will be unregistered by update())
|
|
436
451
|
if (!hasActive && !hasFinished)
|
|
437
452
|
return [];
|
|
438
453
|
const w = tui.terminal.columns;
|
|
439
|
-
const
|
|
440
|
-
|
|
441
|
-
return [];
|
|
442
|
-
const truncate = (line, width = w) => truncateToWidth(line, Math.max(0, width));
|
|
454
|
+
const budget = getWidgetLineBudget(tui.terminal.rows ?? 24);
|
|
455
|
+
const truncate = (line) => truncateToWidth(line.replace(/[\r\n]+/g, " "), w);
|
|
443
456
|
const headingColor = hasActive ? "accent" : "dim";
|
|
444
457
|
const headingIcon = hasActive ? "●" : "○";
|
|
445
458
|
const frame = SPINNER[this.widgetFrame % SPINNER.length];
|
|
@@ -447,230 +460,203 @@ export class AgentWidget {
|
|
|
447
460
|
// Each running agent = 2 lines (header + activity), finished = 1 line, queued = 1 line.
|
|
448
461
|
const finishedLines = [];
|
|
449
462
|
for (const a of finished) {
|
|
450
|
-
|
|
451
|
-
finishedLines.push({
|
|
452
|
-
record: a,
|
|
453
|
-
lines: [truncate(theme.fg("dim", "├─") + ` ${marker} ` + this.renderFinishedLine(a, theme))],
|
|
454
|
-
});
|
|
463
|
+
finishedLines.push(truncate(theme.fg("dim", "├─") + " " + this.renderFinishedLine(a, theme)));
|
|
455
464
|
}
|
|
456
465
|
const runningLines = []; // each entry is [header, activity]
|
|
457
466
|
for (const a of running) {
|
|
458
|
-
const name = getDisplayName(a.type);
|
|
459
467
|
const modeLabel = getPromptModeLabel(a.type);
|
|
460
468
|
const modeTag = modeLabel ? ` ${theme.fg("dim", `(${modeLabel})`)}` : "";
|
|
461
469
|
const elapsed = formatMs(Date.now() - a.startedAt);
|
|
462
470
|
const bg = this.agentActivity.get(a.id);
|
|
463
471
|
const toolUses = bg?.toolUses ?? a.toolUses;
|
|
464
|
-
|
|
472
|
+
// Spend comes from the record, never from the activity tracker: the record
|
|
473
|
+
// is the one that survives the agent finishing, and the one nested-tools
|
|
474
|
+
// folds a hidden child's spend into. Reading the tracker while an agent
|
|
475
|
+
// runs and the record once it stops made the figure jump at completion.
|
|
476
|
+
const tokens = getLifetimeTotal(a.lifetimeUsage);
|
|
465
477
|
const contextPercent = getSessionContextPercent(bg?.session);
|
|
466
478
|
const tokenText = tokens > 0 ? formatSessionTokens(tokens, contextPercent, theme, a.compactionCount) : "";
|
|
479
|
+
const costText = this.showCost() ? formatCost(getLifetimeCost(a.lifetimeUsage)) : "";
|
|
467
480
|
const parts = [];
|
|
481
|
+
if (this.showModel()) {
|
|
482
|
+
// Leading, and paired: a thinking level means nothing without the model
|
|
483
|
+
// it applies to. The tag is taken from buildInvocationTags rather than
|
|
484
|
+
// rebuilt so the "(asked X)" annotation survives.
|
|
485
|
+
const { modelName, tags } = buildInvocationTags(a.invocation);
|
|
486
|
+
if (modelName)
|
|
487
|
+
parts.push(modelName);
|
|
488
|
+
const thinkingTag = tags.find(tag => tag.startsWith("thinking: "));
|
|
489
|
+
if (thinkingTag)
|
|
490
|
+
parts.push(thinkingTag);
|
|
491
|
+
}
|
|
468
492
|
if (bg)
|
|
469
493
|
parts.push(formatTurns(bg.turnCount, bg.maxTurns));
|
|
470
494
|
if (toolUses > 0)
|
|
471
495
|
parts.push(`${toolUses} tool use${toolUses === 1 ? "" : "s"}`);
|
|
472
496
|
if (tokenText)
|
|
473
497
|
parts.push(tokenText);
|
|
498
|
+
if (costText)
|
|
499
|
+
parts.push(costText);
|
|
474
500
|
parts.push(elapsed);
|
|
475
501
|
const statsText = parts.join(" · ");
|
|
476
|
-
const activity = bg ? describeActivity(bg.activeTools, bg.responseText) : "thinking…";
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
const
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
record: a,
|
|
490
|
-
lines: [truncate(theme.fg("dim", "├─") + ` ${marker} ${theme.fg("muted", "◦")} ${theme.fg("dim", `${getDisplayName(a.type)} ${truncateLine(a.description)} · queued`)}`)],
|
|
491
|
-
};
|
|
492
|
-
});
|
|
493
|
-
// Assemble with a responsive cap (heading + overflow indicator = 2
|
|
494
|
-
// reserved lines when content exceeds the available body budget).
|
|
495
|
-
const maxBody = maxLines - 1; // heading takes 1 line
|
|
496
|
-
const rows = [...runningLines, ...queuedLines, ...finishedLines];
|
|
497
|
-
const totalBody = rows.reduce((total, row) => total + row.lines.length, 0);
|
|
498
|
-
const heading = "Agents ↑↓ select · enter view · esc back";
|
|
499
|
-
const lines = [truncate(theme.fg(headingColor, headingIcon) + " " + theme.fg(headingColor, heading))];
|
|
500
|
-
if (maxLines === 1) {
|
|
501
|
-
// There is room only for the heading; do not consume the editor/input row.
|
|
502
|
-
return lines;
|
|
503
|
-
}
|
|
502
|
+
const activity = (bg ? describeActivity(bg.activeTools, bg.responseText) : "thinking…").replace(/[\r\n]+/g, " ");
|
|
503
|
+
runningLines.push([
|
|
504
|
+
truncate(theme.fg("dim", "├─") + ` ${theme.fg("accent", frame)} ${renderAgentName(a.type, theme, { bold: true })}${modeTag} ${theme.fg("muted", a.description)} ${theme.fg("dim", "·")} ${fgPreservingNestedStyles(theme, "dim", statsText)}`),
|
|
505
|
+
truncate(theme.fg("dim", "│ ") + theme.fg("dim", ` ⎿ ${activity}`)),
|
|
506
|
+
]);
|
|
507
|
+
}
|
|
508
|
+
const queuedLine = queued.length > 0
|
|
509
|
+
? truncate(theme.fg("dim", "├─") + ` ${theme.fg("muted", "◦")} ${theme.fg("dim", `${queued.length} queued`)}`)
|
|
510
|
+
: undefined;
|
|
511
|
+
// Assemble with overflow cap (heading + overflow indicator = 2 reserved lines).
|
|
512
|
+
const maxBody = Math.max(1, budget - 1); // heading takes 1 line
|
|
513
|
+
const totalBody = finishedLines.length + runningLines.length * 2 + (queuedLine ? 1 : 0);
|
|
514
|
+
const lines = [truncate(theme.fg(headingColor, headingIcon) + " " + theme.fg(headingColor, "Agents"))];
|
|
504
515
|
if (totalBody <= maxBody) {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
516
|
+
// Everything fits — add all lines and fix up connectors for the last item.
|
|
517
|
+
lines.push(...finishedLines);
|
|
518
|
+
for (const pair of runningLines)
|
|
519
|
+
lines.push(...pair);
|
|
520
|
+
if (queuedLine)
|
|
521
|
+
lines.push(queuedLine);
|
|
522
|
+
// Fix last connector: swap ├─ → └─ and │ → space for activity lines.
|
|
523
|
+
if (lines.length > 1) {
|
|
524
|
+
const last = lines.length - 1;
|
|
525
|
+
lines[last] = lines[last].replace("├─", "└─");
|
|
526
|
+
// If last item is a running agent activity line, fix indent of that line
|
|
527
|
+
// and fix the header line above it.
|
|
528
|
+
if (runningLines.length > 0 && !queuedLine) {
|
|
529
|
+
// The last two lines are the last running agent's header + activity.
|
|
530
|
+
if (last >= 2) {
|
|
531
|
+
lines[last - 1] = lines[last - 1].replace("├─", "└─");
|
|
532
|
+
lines[last] = lines[last].replace("│ ", " ");
|
|
533
|
+
}
|
|
514
534
|
}
|
|
515
535
|
}
|
|
516
536
|
}
|
|
517
537
|
else {
|
|
518
|
-
//
|
|
519
|
-
//
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
538
|
+
// Overflow — prioritize: running > queued > finished.
|
|
539
|
+
// Reserve 1 line for overflow indicator.
|
|
540
|
+
let budget = maxBody - 1;
|
|
541
|
+
let hiddenRunning = 0;
|
|
542
|
+
let hiddenFinished = 0;
|
|
543
|
+
// Reserve the queued line's row up front. It is a single summary of N
|
|
544
|
+
// waiting agents, so it cannot be folded into the "+N more" count (which
|
|
545
|
+
// is denominated in agents) without either under-reporting it as 1 or
|
|
546
|
+
// inflating the total with agents that were never getting their own rows.
|
|
547
|
+
// Reserving costs at most one running agent — which IS counted below —
|
|
548
|
+
// and makes the drop unreachable. It matters most exactly when it used to
|
|
549
|
+
// vanish: the pool is saturated and the queue is what the user needs to see.
|
|
550
|
+
const queuedReserve = queuedLine ? 1 : 0;
|
|
551
|
+
budget -= queuedReserve;
|
|
552
|
+
// 1. Running agents (2 lines each)
|
|
553
|
+
for (const pair of runningLines) {
|
|
554
|
+
if (budget >= 2) {
|
|
555
|
+
lines.push(...pair);
|
|
556
|
+
budget -= 2;
|
|
557
|
+
}
|
|
558
|
+
else {
|
|
559
|
+
hiddenRunning++;
|
|
528
560
|
}
|
|
529
|
-
return end;
|
|
530
|
-
};
|
|
531
|
-
let start = Math.max(0, Math.min(this.viewportStart, Math.max(0, rows.length - 1)));
|
|
532
|
-
const selectedIndex = this.navigationActive && selectedId
|
|
533
|
-
? rows.findIndex(row => row.record.id === selectedId)
|
|
534
|
-
: -1;
|
|
535
|
-
if (selectedIndex >= 0) {
|
|
536
|
-
if (selectedIndex < start)
|
|
537
|
-
start = selectedIndex;
|
|
538
|
-
if (selectedIndex >= endFor(start, baseContentBudget))
|
|
539
|
-
start = selectedIndex;
|
|
540
|
-
}
|
|
541
|
-
// Keep both directional affordances in the viewport chrome: the top
|
|
542
|
-
// count sits below the heading and the bottom count sits below the
|
|
543
|
-
// bounded roster. Each consumes one row when that direction is hidden.
|
|
544
|
-
let showTopMore = start > 0;
|
|
545
|
-
let contentBudget = Math.max(0, maxBody - (showTopMore ? 2 : 1));
|
|
546
|
-
if (selectedIndex >= 0 && selectedIndex >= endFor(start, contentBudget)) {
|
|
547
|
-
start = selectedIndex;
|
|
548
|
-
showTopMore = start > 0;
|
|
549
|
-
contentBudget = Math.max(0, maxBody - (showTopMore ? 2 : 1));
|
|
550
|
-
}
|
|
551
|
-
this.viewportStart = start;
|
|
552
|
-
let used = 0;
|
|
553
|
-
let end = start;
|
|
554
|
-
const visibleBody = [];
|
|
555
|
-
while (end < rows.length && used + heightAt(end) <= contentBudget) {
|
|
556
|
-
visibleBody.push(...rows[end].lines);
|
|
557
|
-
used += heightAt(end);
|
|
558
|
-
end++;
|
|
559
561
|
}
|
|
560
|
-
//
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
562
|
+
// 2. Queued line (always fits — its row was reserved above)
|
|
563
|
+
if (queuedLine) {
|
|
564
|
+
budget += queuedReserve;
|
|
565
|
+
lines.push(queuedLine);
|
|
566
|
+
budget--;
|
|
565
567
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
const direction = [
|
|
576
|
-
...(hiddenBefore > 0 ? [`↑ ${hiddenBefore} more`] : []),
|
|
577
|
-
...(hiddenAfter > 0 ? [`↓ ${hiddenAfter} more`] : []),
|
|
578
|
-
].join(" · ");
|
|
579
|
-
const summary = `+${hidden} more (${direction}${categoryCounts.length > 0 ? `; ${categoryCounts.join(", ")}` : ""})`;
|
|
580
|
-
if (showTopMore)
|
|
581
|
-
lines.push(truncate(theme.fg("dim", `↑ ${hiddenBefore} more`)));
|
|
582
|
-
// Fill the bounded viewport so the scrollbar track has a stable height.
|
|
583
|
-
while (visibleBody.length < contentBudget)
|
|
584
|
-
visibleBody.push("");
|
|
585
|
-
const totalBody = rows.reduce((total, row) => total + row.lines.length, 0);
|
|
586
|
-
const startOffset = rows.slice(0, start).reduce((total, row) => total + row.lines.length, 0);
|
|
587
|
-
const trackHeight = Math.max(0, contentBudget);
|
|
588
|
-
const minThumbHeight = Math.min(2, trackHeight);
|
|
589
|
-
const thumbHeight = Math.max(minThumbHeight, Math.min(trackHeight, Math.round((trackHeight * trackHeight) / totalBody)));
|
|
590
|
-
const maxScrollTop = Math.max(0, totalBody - trackHeight);
|
|
591
|
-
const maxThumbTop = Math.max(0, trackHeight - thumbHeight);
|
|
592
|
-
const thumbOffset = maxScrollTop === 0
|
|
593
|
-
? 0
|
|
594
|
-
: Math.round((startOffset / maxScrollTop) * maxThumbTop);
|
|
595
|
-
const contentWidth = Math.max(0, w - 1);
|
|
596
|
-
for (let index = 0; index < visibleBody.length; index++) {
|
|
597
|
-
const isThumb = index >= thumbOffset && index < thumbOffset + thumbHeight;
|
|
598
|
-
const scrollbar = isThumb
|
|
599
|
-
? theme.fg("scrollbarThumb", this.navigationActive ? "█" : "┃")
|
|
600
|
-
: theme.fg("scrollbarTrack", "│");
|
|
601
|
-
const bodyLine = truncate(visibleBody[index] ?? "", contentWidth);
|
|
602
|
-
lines.push(bodyLine + " ".repeat(Math.max(0, contentWidth - visibleWidth(bodyLine))) + scrollbar);
|
|
568
|
+
// 3. Finished agents
|
|
569
|
+
for (const fl of finishedLines) {
|
|
570
|
+
if (budget >= 1) {
|
|
571
|
+
lines.push(fl);
|
|
572
|
+
budget--;
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
hiddenFinished++;
|
|
576
|
+
}
|
|
603
577
|
}
|
|
604
|
-
//
|
|
605
|
-
//
|
|
606
|
-
|
|
607
|
-
|
|
578
|
+
// Overflow summary
|
|
579
|
+
// On extremely short terminals the queued summary itself consumes the
|
|
580
|
+
// last useful row; keep its count in the compact footer. At the normal
|
|
581
|
+
// 12-line ceiling the summary is a single visible row and must not be
|
|
582
|
+
// counted as hidden agents.
|
|
583
|
+
const hiddenQueued = queuedLine && (tui.terminal.rows ?? 24) < 10 ? queued.length : 0;
|
|
584
|
+
const overflowParts = [];
|
|
585
|
+
if (hiddenRunning > 0)
|
|
586
|
+
overflowParts.push(`${hiddenRunning} running`);
|
|
587
|
+
if (hiddenFinished > 0)
|
|
588
|
+
overflowParts.push(`${hiddenFinished} finished`);
|
|
589
|
+
if (hiddenQueued > 0)
|
|
590
|
+
overflowParts.push(`${hiddenQueued} queued`);
|
|
591
|
+
const hiddenTotal = hiddenRunning + hiddenFinished + hiddenQueued;
|
|
592
|
+
const overflowText = overflowParts.join(", ");
|
|
593
|
+
lines.push(truncate(theme.fg("dim", "└─") + ` ${theme.fg("dim", `+${hiddenTotal} more (${overflowText})`)}`));
|
|
594
|
+
}
|
|
595
|
+
if (this.navigationActive) {
|
|
596
|
+
const roster = this.navigationAgents();
|
|
597
|
+
const selected = roster[this.selectedRosterIndex];
|
|
598
|
+
const hasSelected = selected ? lines.some(line => line.includes(selected.description.replace(/[\r\n]+/g, " "))) : true;
|
|
599
|
+
if (selected && !hasSelected) {
|
|
600
|
+
const selectedLine = truncate(theme.fg("dim", "└─") + ` ${selected.status === "queued" ? "◦" : "⠋"} ${theme.fg("muted", selected.description.replace(/[\r\n]+/g, " "))}`);
|
|
601
|
+
if (lines.length >= budget)
|
|
602
|
+
lines[lines.length - 1] = selectedLine;
|
|
603
|
+
else
|
|
604
|
+
lines.push(selectedLine);
|
|
608
605
|
}
|
|
609
|
-
|
|
610
|
-
lines.
|
|
606
|
+
if (this.viewportStart > 0) {
|
|
607
|
+
if (lines.length >= budget)
|
|
608
|
+
lines.splice(1, 1, truncate(theme.fg("dim", "↑ more")));
|
|
609
|
+
else
|
|
610
|
+
lines.splice(1, 0, truncate(theme.fg("dim", "↑ more")));
|
|
611
611
|
}
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
lines
|
|
612
|
+
const railStart = this.viewportStart > 0 ? 2 : 1;
|
|
613
|
+
for (let index = railStart; index < lines.length; index++) {
|
|
614
|
+
const plain = lines[index] ?? "";
|
|
615
|
+
lines[index] = truncate(plain + (selected && plain.includes(selected.description.replace(/[\r\n]+/g, " ")) ? "┃" : "│"));
|
|
615
616
|
}
|
|
617
|
+
if (lines.length > budget)
|
|
618
|
+
lines.length = budget;
|
|
616
619
|
}
|
|
617
620
|
return lines;
|
|
618
621
|
}
|
|
619
|
-
/**
|
|
620
|
-
|
|
621
|
-
const activities = allAgents.map(a => {
|
|
622
|
-
const activity = this.agentActivity.get(a.id);
|
|
623
|
-
return activity ? {
|
|
624
|
-
id: a.id,
|
|
625
|
-
activeTools: [...activity.activeTools.entries()],
|
|
626
|
-
toolUses: activity.toolUses,
|
|
627
|
-
responseText: activity.responseText,
|
|
628
|
-
turnCount: activity.turnCount,
|
|
629
|
-
maxTurns: activity.maxTurns,
|
|
630
|
-
lifetimeUsage: activity.lifetimeUsage,
|
|
631
|
-
contextPercent: getSessionContextPercent(activity.session),
|
|
632
|
-
} : undefined;
|
|
633
|
-
});
|
|
634
|
-
return JSON.stringify({
|
|
635
|
-
frame: this.widgetFrame,
|
|
636
|
-
agents: allAgents.map(a => ({
|
|
637
|
-
id: a.id,
|
|
638
|
-
type: a.type,
|
|
639
|
-
description: a.description,
|
|
640
|
-
status: a.status,
|
|
641
|
-
toolUses: a.toolUses,
|
|
642
|
-
startedAt: a.startedAt,
|
|
643
|
-
completedAt: a.completedAt,
|
|
644
|
-
error: a.error,
|
|
645
|
-
lifetimeUsage: a.lifetimeUsage,
|
|
646
|
-
compactionCount: a.compactionCount,
|
|
647
|
-
isBackground: a.isBackground,
|
|
648
|
-
hasSession: a.session !== undefined,
|
|
649
|
-
transcriptPath: a.transcriptPath,
|
|
650
|
-
openableHistory: this.options.canOpenHistory(a),
|
|
651
|
-
})),
|
|
652
|
-
activities,
|
|
653
|
-
navigationActive: this.navigationActive,
|
|
654
|
-
selectedAgentId: this.selectedAgentId,
|
|
655
|
-
viewportStart: this.viewportStart,
|
|
656
|
-
});
|
|
657
|
-
}
|
|
658
|
-
/** Force an immediate widget update. `advanceSpinner` is reserved for the timer. */
|
|
659
|
-
update(advanceSpinner = false) {
|
|
622
|
+
/** Force an immediate widget update. */
|
|
623
|
+
update() {
|
|
660
624
|
if (!this.uiCtx)
|
|
661
625
|
return;
|
|
662
626
|
const allAgents = this.widgetAgents();
|
|
663
|
-
|
|
627
|
+
if (this.navigationActive || this.selectedAgentId) {
|
|
628
|
+
const roster = this.navigationAgents();
|
|
629
|
+
const selectedIndex = this.selectedAgentId ? roster.findIndex(agent => agent.id === this.selectedAgentId) : -1;
|
|
630
|
+
if (selectedIndex >= 0) {
|
|
631
|
+
this.selectedRosterIndex = selectedIndex;
|
|
632
|
+
}
|
|
633
|
+
else if (roster.length > 0 && this.selectedAgentId) {
|
|
634
|
+
this.selectedRosterIndex = Math.min(this.selectedRosterIndex, roster.length - 1);
|
|
635
|
+
this.selectedAgentId = roster[this.selectedRosterIndex]?.id;
|
|
636
|
+
}
|
|
637
|
+
else if (roster.length === 0) {
|
|
638
|
+
this.navigationActive = false;
|
|
639
|
+
this.selectedAgentId = undefined;
|
|
640
|
+
this.selectedRosterIndex = 0;
|
|
641
|
+
this.viewportStart = 0;
|
|
642
|
+
}
|
|
643
|
+
this.updateViewport(roster);
|
|
644
|
+
}
|
|
664
645
|
// Lightweight existence checks — full categorization happens in renderWidget()
|
|
665
646
|
let runningCount = 0;
|
|
666
647
|
let queuedCount = 0;
|
|
667
|
-
|
|
668
|
-
|
|
648
|
+
let hasFinished = false;
|
|
649
|
+
for (const a of allAgents) {
|
|
650
|
+
if (a.status === "running") {
|
|
669
651
|
runningCount++;
|
|
670
|
-
|
|
652
|
+
}
|
|
653
|
+
else if (a.status === "queued") {
|
|
671
654
|
queuedCount++;
|
|
655
|
+
}
|
|
656
|
+
else if (a.completedAt && this.shouldShowFinished(a.id, a.status)) {
|
|
657
|
+
hasFinished = true;
|
|
658
|
+
}
|
|
672
659
|
}
|
|
673
|
-
const hasFinished = roster.some(a => a.status !== "running" && a.status !== "queued");
|
|
674
660
|
const hasActive = runningCount > 0 || queuedCount > 0;
|
|
675
661
|
// Nothing to show — clear widget
|
|
676
662
|
if (!hasActive && !hasFinished) {
|
|
@@ -683,12 +669,15 @@ export class AgentWidget {
|
|
|
683
669
|
this.uiCtx.setStatus("subagents", undefined);
|
|
684
670
|
this.lastStatusText = undefined;
|
|
685
671
|
}
|
|
686
|
-
this.
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
this.
|
|
672
|
+
if (this.widgetInterval) {
|
|
673
|
+
clearInterval(this.widgetInterval);
|
|
674
|
+
this.widgetInterval = undefined;
|
|
675
|
+
}
|
|
676
|
+
// Clean up stale entries
|
|
677
|
+
for (const [id] of this.finishedTurnAge) {
|
|
678
|
+
if (!allAgents.some(a => a.id === id))
|
|
679
|
+
this.finishedTurnAge.delete(id);
|
|
680
|
+
}
|
|
692
681
|
return;
|
|
693
682
|
}
|
|
694
683
|
// Status bar — only call setStatus when the text actually changes
|
|
@@ -706,33 +695,25 @@ export class AgentWidget {
|
|
|
706
695
|
this.uiCtx.setStatus("subagents", newStatusText);
|
|
707
696
|
this.lastStatusText = newStatusText;
|
|
708
697
|
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
// logical index and viewport are clamped as the roster shrinks.
|
|
729
|
-
const fallbackIndex = Math.max(0, Math.min(this.selectedRosterIndex, roster.length - 1));
|
|
730
|
-
this.selectedAgentId = roster[fallbackIndex].id;
|
|
731
|
-
this.selectedRosterIndex = fallbackIndex;
|
|
732
|
-
this.viewportStart = Math.min(this.viewportStart, roster.length - 1);
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
const renderKey = this.renderKey(allAgents);
|
|
698
|
+
const snapshot = allAgents.map(agent => {
|
|
699
|
+
const activity = this.agentActivity.get(agent.id);
|
|
700
|
+
return [
|
|
701
|
+
agent.id,
|
|
702
|
+
agent.status,
|
|
703
|
+
agent.description,
|
|
704
|
+
agent.toolUses,
|
|
705
|
+
agent.completedAt ?? "",
|
|
706
|
+
this.finishedTurnAge.get(agent.id) ?? "",
|
|
707
|
+
getLifetimeTotal(agent.lifetimeUsage),
|
|
708
|
+
activity?.toolUses ?? "",
|
|
709
|
+
activity?.responseText ?? "",
|
|
710
|
+
Math.floor((Date.now() - agent.startedAt) / 1000),
|
|
711
|
+
].join("\u0000");
|
|
712
|
+
}).join("\u0001");
|
|
713
|
+
if (this.widgetRegistered && snapshot === this.lastSnapshot)
|
|
714
|
+
return;
|
|
715
|
+
this.lastSnapshot = snapshot;
|
|
716
|
+
this.widgetFrame++;
|
|
736
717
|
// Register widget callback once; subsequent updates use requestRender()
|
|
737
718
|
// which re-invokes render() without replacing the component (avoids layout thrashing).
|
|
738
719
|
if (!this.widgetRegistered) {
|
|
@@ -744,20 +725,23 @@ export class AgentWidget {
|
|
|
744
725
|
// Theme changed — force re-registration so factory captures fresh theme.
|
|
745
726
|
this.widgetRegistered = false;
|
|
746
727
|
this.tui = undefined;
|
|
747
|
-
this.lastRenderKey = undefined;
|
|
748
728
|
},
|
|
749
729
|
};
|
|
750
730
|
}, { placement: "aboveEditor" });
|
|
751
731
|
this.widgetRegistered = true;
|
|
752
732
|
}
|
|
753
|
-
else
|
|
754
|
-
// Widget already registered — request a re-render
|
|
755
|
-
// changed (or the spinner timer advanced).
|
|
733
|
+
else {
|
|
734
|
+
// Widget already registered — just request a re-render of existing components.
|
|
756
735
|
this.tui?.requestRender();
|
|
757
736
|
}
|
|
758
|
-
this.lastRenderKey = renderKey;
|
|
759
737
|
}
|
|
760
738
|
dispose() {
|
|
739
|
+
this.inputCleanup?.();
|
|
740
|
+
this.inputCleanup = undefined;
|
|
741
|
+
this.navigationActive = false;
|
|
742
|
+
this.selectedAgentId = undefined;
|
|
743
|
+
this.viewportStart = 0;
|
|
744
|
+
this.selectedRosterIndex = 0;
|
|
761
745
|
if (this.widgetInterval) {
|
|
762
746
|
clearInterval(this.widgetInterval);
|
|
763
747
|
this.widgetInterval = undefined;
|
|
@@ -766,17 +750,10 @@ export class AgentWidget {
|
|
|
766
750
|
this.uiCtx.setWidget("agents", undefined);
|
|
767
751
|
this.uiCtx.setStatus("subagents", undefined);
|
|
768
752
|
}
|
|
769
|
-
this.inputUnsub?.();
|
|
770
|
-
this.inputUnsub = undefined;
|
|
771
753
|
this.widgetRegistered = false;
|
|
772
754
|
this.tui = undefined;
|
|
773
755
|
this.lastStatusText = undefined;
|
|
774
|
-
this.
|
|
775
|
-
this.navigationActive = false;
|
|
776
|
-
this.selectedAgentId = undefined;
|
|
777
|
-
this.selectedRosterIndex = 0;
|
|
778
|
-
this.viewportStart = 0;
|
|
779
|
-
this.uiCtx = undefined;
|
|
756
|
+
this.lastSnapshot = "";
|
|
780
757
|
}
|
|
781
758
|
}
|
|
782
759
|
//# sourceMappingURL=agent-widget.js.map
|