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