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