@gotgenes/pi-subagents 16.4.0 → 16.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/dist/public.d.ts +7 -4
- package/docs/architecture/architecture.md +184 -209
- package/docs/architecture/history/phase-17-core-consolidation.md +214 -0
- package/docs/plans/0412-unify-session-mock-builders.md +146 -0
- package/docs/plans/0420-fold-run-metrics-live-activity-onto-record.md +228 -0
- package/docs/plans/0421-migrate-activity-readers-to-record-getters.md +309 -0
- package/docs/retro/0380-extract-layered-settings-loader.md +49 -0
- package/docs/retro/0412-unify-session-mock-builders.md +80 -0
- package/docs/retro/0420-fold-run-metrics-live-activity-onto-record.md +90 -0
- package/docs/retro/0421-migrate-activity-readers-to-record-getters.md +56 -0
- package/package.json +1 -1
- package/src/index.ts +1 -3
- package/src/lifecycle/subagent-state.ts +49 -4
- package/src/lifecycle/subagent.ts +4 -0
- package/src/observation/notification.ts +3 -12
- package/src/observation/record-observer.ts +25 -1
- package/src/observation/subagent-events-observer.ts +1 -4
- package/src/tools/foreground-runner.ts +8 -4
- package/src/tools/helpers.ts +5 -4
- package/src/ui/agent-menu.ts +0 -12
- package/src/ui/agent-widget.ts +24 -5
- package/src/ui/conversation-viewer.ts +4 -9
- package/src/ui/widget-renderer.ts +14 -22
package/src/index.ts
CHANGED
|
@@ -62,7 +62,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
62
62
|
// widget dependency — keeping the construction graph a cycle-free DAG.
|
|
63
63
|
const notifications = new NotificationManager(
|
|
64
64
|
(msg, opts) => pi.sendMessage(msg, opts),
|
|
65
|
-
runtime.agentActivity,
|
|
66
65
|
);
|
|
67
66
|
|
|
68
67
|
// Settings: owns all three in-memory values and handles load/save/emit.
|
|
@@ -130,7 +129,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
130
129
|
|
|
131
130
|
// Live widget: constructed after the manager (it polls listAgents()) and
|
|
132
131
|
// injected directly into its consumers — no post-construction field write.
|
|
133
|
-
const widget = new AgentWidget(manager,
|
|
132
|
+
const widget = new AgentWidget(manager, registry);
|
|
134
133
|
|
|
135
134
|
// Grab UI context from first tool execution + clear lingering widget on new turn
|
|
136
135
|
const toolStart = new ToolStartHandler(widget);
|
|
@@ -157,7 +156,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
157
156
|
const agentsMenu = new AgentsMenuHandler(
|
|
158
157
|
manager,
|
|
159
158
|
registry,
|
|
160
|
-
runtime.agentActivity,
|
|
161
159
|
settings,
|
|
162
160
|
new FsAgentFileOps(),
|
|
163
161
|
join(getAgentDir(), "agents"),
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* subagent-state.ts — SubagentState value object: lifecycle status and
|
|
2
|
+
* subagent-state.ts — SubagentState value object: lifecycle status, metrics, and live activity.
|
|
3
3
|
*
|
|
4
4
|
* Owns the passive, readable state of a subagent — status, result, error,
|
|
5
|
-
* timestamps,
|
|
6
|
-
*
|
|
7
|
-
* methods (
|
|
5
|
+
* timestamps, stats (toolUses, lifetimeUsage, compactionCount), and live-activity
|
|
6
|
+
* fields (turnCount, activeTools, responseText) — together with the transition
|
|
7
|
+
* methods (markRunning, markCompleted, …), accumulation methods
|
|
8
|
+
* (incrementToolUses, addUsage, incrementCompactions), and live-activity
|
|
9
|
+
* transition methods (incrementTurnCount, addActiveTool, removeActiveTool,
|
|
10
|
+
* resetResponseText, appendResponseText) that mutate them.
|
|
8
11
|
*
|
|
9
12
|
* State is encapsulated behind getters; external code reads through them but
|
|
10
13
|
* mutates only via the transition/accumulation methods. The value object owns
|
|
@@ -62,6 +65,18 @@ export class SubagentState {
|
|
|
62
65
|
private _compactionCount = 0;
|
|
63
66
|
get compactionCount(): number { return this._compactionCount; }
|
|
64
67
|
|
|
68
|
+
// Live activity — accumulated via transition methods, readable via getters
|
|
69
|
+
private _turnCount = 1;
|
|
70
|
+
get turnCount(): number { return this._turnCount; }
|
|
71
|
+
|
|
72
|
+
private _activeTools = new Map<string, string>();
|
|
73
|
+
get activeTools(): ReadonlyMap<string, string> { return this._activeTools; }
|
|
74
|
+
|
|
75
|
+
private _toolKeySeq = 0;
|
|
76
|
+
|
|
77
|
+
private _responseText = "";
|
|
78
|
+
get responseText(): string { return this._responseText; }
|
|
79
|
+
|
|
65
80
|
constructor(init: SubagentStateInit = {}) {
|
|
66
81
|
this._status = init.status ?? "queued";
|
|
67
82
|
this._result = init.result;
|
|
@@ -85,6 +100,36 @@ export class SubagentState {
|
|
|
85
100
|
this._compactionCount++;
|
|
86
101
|
}
|
|
87
102
|
|
|
103
|
+
/** Record a turn boundary. Called by record-observer on turn_end. */
|
|
104
|
+
incrementTurnCount(): void {
|
|
105
|
+
this._turnCount++;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Record a tool starting. Called by record-observer on tool_execution_start. */
|
|
109
|
+
addActiveTool(toolName: string): void {
|
|
110
|
+
this._activeTools.set(toolName + "_" + (++this._toolKeySeq), toolName);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Remove one active tool by name (first match). Called by record-observer on tool_execution_end. */
|
|
114
|
+
removeActiveTool(toolName: string): void {
|
|
115
|
+
for (const [key, name] of this._activeTools) {
|
|
116
|
+
if (name === toolName) {
|
|
117
|
+
this._activeTools.delete(key);
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Reset the current response text. Called by record-observer on message_start. */
|
|
124
|
+
resetResponseText(): void {
|
|
125
|
+
this._responseText = "";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Append a text delta to the current response text. Called by record-observer on message_update. */
|
|
129
|
+
appendResponseText(delta: string): void {
|
|
130
|
+
this._responseText += delta;
|
|
131
|
+
}
|
|
132
|
+
|
|
88
133
|
/** Transition to running state. Sets status and startedAt. */
|
|
89
134
|
markRunning(startedAt: number): void {
|
|
90
135
|
this._status = "running";
|
|
@@ -94,6 +94,10 @@ export class Subagent {
|
|
|
94
94
|
get toolUses(): number { return this.state.toolUses; }
|
|
95
95
|
get lifetimeUsage(): Readonly<LifetimeUsage> { return this.state.lifetimeUsage; }
|
|
96
96
|
get compactionCount(): number { return this.state.compactionCount; }
|
|
97
|
+
get turnCount(): number { return this.state.turnCount; }
|
|
98
|
+
get activeTools(): ReadonlyMap<string, string> { return this.state.activeTools; }
|
|
99
|
+
get responseText(): string { return this.state.responseText; }
|
|
100
|
+
get maxTurns(): number | undefined { return this.execution.maxTurns; }
|
|
97
101
|
|
|
98
102
|
readonly abortController: AbortController;
|
|
99
103
|
private _promise?: Promise<void>;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { debugLog } from "#src/debug";
|
|
2
2
|
import { getLifetimeTotal } from "#src/lifecycle/usage";
|
|
3
3
|
import type { Subagent } from "#src/types";
|
|
4
|
-
import type { AgentActivityTracker } from "#src/ui/agent-activity-tracker";
|
|
5
4
|
|
|
6
5
|
/** Details attached to custom notification messages for visual rendering. */
|
|
7
6
|
export interface NotificationDetails {
|
|
@@ -77,7 +76,6 @@ export function formatTaskNotification(record: Subagent, resultMaxLen: number):
|
|
|
77
76
|
export function buildNotificationDetails(
|
|
78
77
|
record: Subagent,
|
|
79
78
|
resultMaxLen: number,
|
|
80
|
-
activity?: AgentActivityTracker,
|
|
81
79
|
): NotificationDetails {
|
|
82
80
|
const totalTokens = getLifetimeTotal(record.lifetimeUsage);
|
|
83
81
|
|
|
@@ -86,8 +84,8 @@ export function buildNotificationDetails(
|
|
|
86
84
|
description: record.description,
|
|
87
85
|
status: record.status,
|
|
88
86
|
toolUses: record.toolUses,
|
|
89
|
-
turnCount:
|
|
90
|
-
maxTurns:
|
|
87
|
+
turnCount: record.turnCount,
|
|
88
|
+
maxTurns: record.maxTurns,
|
|
91
89
|
totalTokens,
|
|
92
90
|
durationMs: record.completedAt ? record.completedAt - record.startedAt : 0,
|
|
93
91
|
outputFile: record.outputFile,
|
|
@@ -127,7 +125,6 @@ export function buildEventData(record: Subagent) {
|
|
|
127
125
|
export interface NotificationSystem {
|
|
128
126
|
cancelNudge: (key: string) => void;
|
|
129
127
|
sendCompletion: (record: Subagent) => void;
|
|
130
|
-
cleanupCompleted: (id: string) => void;
|
|
131
128
|
dispose: () => void;
|
|
132
129
|
}
|
|
133
130
|
|
|
@@ -141,7 +138,6 @@ export class NotificationManager implements NotificationSystem {
|
|
|
141
138
|
msg: { customType: string; content: string; display: boolean; details?: unknown },
|
|
142
139
|
opts?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
|
|
143
140
|
) => void,
|
|
144
|
-
private agentActivity: Map<string, AgentActivityTracker>,
|
|
145
141
|
) {}
|
|
146
142
|
|
|
147
143
|
cancelNudge(key: string): void {
|
|
@@ -153,14 +149,9 @@ export class NotificationManager implements NotificationSystem {
|
|
|
153
149
|
}
|
|
154
150
|
|
|
155
151
|
sendCompletion(record: Subagent): void {
|
|
156
|
-
this.agentActivity.delete(record.id);
|
|
157
152
|
this.scheduleNudge(record.id, () => this.emitIndividualNudge(record));
|
|
158
153
|
}
|
|
159
154
|
|
|
160
|
-
cleanupCompleted(id: string): void {
|
|
161
|
-
this.agentActivity.delete(id);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
155
|
dispose(): void {
|
|
165
156
|
for (const timer of this.pendingNudges.values()) clearTimeout(timer);
|
|
166
157
|
this.pendingNudges.clear();
|
|
@@ -193,7 +184,7 @@ export class NotificationManager implements NotificationSystem {
|
|
|
193
184
|
customType: "subagent-notification",
|
|
194
185
|
content: notification + footer,
|
|
195
186
|
display: true,
|
|
196
|
-
details: buildNotificationDetails(record, 500
|
|
187
|
+
details: buildNotificationDetails(record, 500),
|
|
197
188
|
},
|
|
198
189
|
{ deliverAs: "followUp", triggerTurn: true },
|
|
199
190
|
);
|
|
@@ -18,8 +18,12 @@ export interface SubagentObserverOptions {
|
|
|
18
18
|
* Subscribe to session events and accumulate stats on the subagent state.
|
|
19
19
|
*
|
|
20
20
|
* Handles:
|
|
21
|
-
* - `
|
|
21
|
+
* - `tool_execution_start` → `state.addActiveTool(name)`
|
|
22
|
+
* - `tool_execution_end` → `state.removeActiveTool(name)`, `state.incrementToolUses()`
|
|
23
|
+
* - `message_start` → `state.resetResponseText()`
|
|
24
|
+
* - `message_update` (text_delta) → `state.appendResponseText(delta)`
|
|
22
25
|
* - `message_end` (assistant, with usage) → `state.addUsage(…)`
|
|
26
|
+
* - `turn_end` → `state.incrementTurnCount()`
|
|
23
27
|
* - `compaction_end` (not aborted) → `state.incrementCompactions()`, call `onCompact`
|
|
24
28
|
*
|
|
25
29
|
* @returns An unsubscribe function.
|
|
@@ -30,10 +34,30 @@ export function subscribeSubagentObserver(
|
|
|
30
34
|
options?: SubagentObserverOptions,
|
|
31
35
|
): () => void {
|
|
32
36
|
return session.subscribe((event) => {
|
|
37
|
+
if (event.type === "tool_execution_start") {
|
|
38
|
+
state.addActiveTool(event.toolName);
|
|
39
|
+
}
|
|
40
|
+
|
|
33
41
|
if (event.type === "tool_execution_end") {
|
|
42
|
+
state.removeActiveTool(event.toolName);
|
|
34
43
|
state.incrementToolUses();
|
|
35
44
|
}
|
|
36
45
|
|
|
46
|
+
if (event.type === "message_start") {
|
|
47
|
+
state.resetResponseText();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (
|
|
51
|
+
event.type === "message_update" &&
|
|
52
|
+
event.assistantMessageEvent.type === "text_delta"
|
|
53
|
+
) {
|
|
54
|
+
state.appendResponseText(event.assistantMessageEvent.delta);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (event.type === "turn_end") {
|
|
58
|
+
state.incrementTurnCount();
|
|
59
|
+
}
|
|
60
|
+
|
|
37
61
|
if (event.type === "message_end" && event.message.role === "assistant") {
|
|
38
62
|
const u = event.message.usage;
|
|
39
63
|
state.addUsage({
|
|
@@ -65,10 +65,7 @@ export class SubagentEventsObserver implements SubagentManagerObserver {
|
|
|
65
65
|
});
|
|
66
66
|
|
|
67
67
|
// Skip notification if result was already consumed via get_subagent_result.
|
|
68
|
-
if (record.notification?.resultConsumed)
|
|
69
|
-
this.notifications.cleanupCompleted(record.id);
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
68
|
+
if (record.notification?.resultConsumed) return;
|
|
72
69
|
|
|
73
70
|
this.notifications.sendCompletion(record);
|
|
74
71
|
}
|
|
@@ -70,11 +70,15 @@ export async function runForeground(
|
|
|
70
70
|
...presentation.detailBase,
|
|
71
71
|
toolUses,
|
|
72
72
|
tokens: recordRef ? formatLifetimeTokens(recordRef) : "",
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
// Read activity off the record; fall back to safe defaults before onSessionCreated fires
|
|
74
|
+
turnCount: recordRef?.turnCount ?? 1,
|
|
75
|
+
maxTurns: recordRef?.maxTurns ?? execution.effectiveMaxTurns,
|
|
75
76
|
durationMs: Date.now() - startedAt,
|
|
76
77
|
status: "running",
|
|
77
|
-
activity: describeActivity(
|
|
78
|
+
activity: describeActivity(
|
|
79
|
+
recordRef?.activeTools ?? new Map(),
|
|
80
|
+
recordRef?.responseText ?? "",
|
|
81
|
+
),
|
|
78
82
|
spinnerFrame: spinnerFrame % SPINNER.length,
|
|
79
83
|
};
|
|
80
84
|
onUpdate?.({
|
|
@@ -136,7 +140,7 @@ export async function runForeground(
|
|
|
136
140
|
}
|
|
137
141
|
|
|
138
142
|
const tokenText = formatLifetimeTokens(record);
|
|
139
|
-
const details = buildDetails(presentation.detailBase, record,
|
|
143
|
+
const details = buildDetails(presentation.detailBase, record, { tokens: tokenText });
|
|
140
144
|
|
|
141
145
|
const fallbackNote = identity.fellBack
|
|
142
146
|
? `Note: Unknown agent type "${identity.rawType}" — using general-purpose.\n\n`
|
package/src/tools/helpers.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { AgentConfigLookup } from "#src/config/agent-types";
|
|
2
2
|
import { getLifetimeTotal, type LifetimeUsage } from "#src/lifecycle/usage";
|
|
3
|
-
import { AgentActivityTracker } from "#src/ui/agent-activity-tracker";
|
|
4
3
|
import { type AgentDetails, formatTokens } from "#src/ui/display";
|
|
5
4
|
|
|
6
5
|
/** Parenthetical status note for completed agent result text. */
|
|
@@ -28,16 +27,18 @@ export function buildDetails(
|
|
|
28
27
|
error?: string;
|
|
29
28
|
id?: string;
|
|
30
29
|
lifetimeUsage: LifetimeUsage;
|
|
30
|
+
/** Live-activity counters — exposed as getters on Subagent (Phase 18 Step 2). */
|
|
31
|
+
turnCount?: number;
|
|
32
|
+
maxTurns?: number;
|
|
31
33
|
},
|
|
32
|
-
activity?: AgentActivityTracker,
|
|
33
34
|
overrides?: Partial<AgentDetails>,
|
|
34
35
|
): AgentDetails {
|
|
35
36
|
return {
|
|
36
37
|
...base,
|
|
37
38
|
toolUses: record.toolUses,
|
|
38
39
|
tokens: formatLifetimeTokens(record),
|
|
39
|
-
turnCount:
|
|
40
|
-
maxTurns:
|
|
40
|
+
turnCount: record.turnCount,
|
|
41
|
+
maxTurns: record.maxTurns,
|
|
41
42
|
durationMs: (record.completedAt ?? Date.now()) - record.startedAt,
|
|
42
43
|
status: record.status as AgentDetails["status"],
|
|
43
44
|
agentId: record.id,
|
package/src/ui/agent-menu.ts
CHANGED
|
@@ -5,7 +5,6 @@ import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
|
|
|
5
5
|
import { type ModelRegistry, resolveModel } from "#src/session/model-resolver";
|
|
6
6
|
import { getModelLabelFromConfig } from "#src/tools/helpers";
|
|
7
7
|
import type { AgentConfig, Subagent } from "#src/types";
|
|
8
|
-
import type { AgentActivityTracker } from "#src/ui/agent-activity-tracker";
|
|
9
8
|
import { AgentConfigEditor } from "#src/ui/agent-config-editor";
|
|
10
9
|
import { AgentCreationWizard } from "#src/ui/agent-creation-wizard";
|
|
11
10
|
import type { AgentFileOps } from "#src/ui/agent-file-ops";
|
|
@@ -36,14 +35,6 @@ export interface AgentMenuSettings {
|
|
|
36
35
|
applyGraceTurns(n: number): { message: string; level: "info" | "warning" };
|
|
37
36
|
}
|
|
38
37
|
|
|
39
|
-
/**
|
|
40
|
-
* Read-only interface for the agent-menu's agentActivity access.
|
|
41
|
-
* Only the conversation viewer needs to read a tracker by agent ID.
|
|
42
|
-
*/
|
|
43
|
-
export interface AgentActivityReader {
|
|
44
|
-
get(id: string): AgentActivityTracker | undefined;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
38
|
// ---- Narrow UI context types ----
|
|
48
39
|
|
|
49
40
|
/** Narrow UI interface — only the ctx.ui methods menu handlers actually call. */
|
|
@@ -70,7 +61,6 @@ export class AgentsMenuHandler {
|
|
|
70
61
|
constructor(
|
|
71
62
|
private readonly manager: AgentMenuManager,
|
|
72
63
|
private readonly registry: AgentTypeRegistry,
|
|
73
|
-
private readonly agentActivity: AgentActivityReader,
|
|
74
64
|
private readonly settings: AgentMenuSettings,
|
|
75
65
|
fileOps: AgentFileOps,
|
|
76
66
|
personalAgentsDir: string,
|
|
@@ -263,14 +253,12 @@ export class AgentsMenuHandler {
|
|
|
263
253
|
const { ConversationViewer, VIEWPORT_HEIGHT_PCT } = await import(
|
|
264
254
|
"./conversation-viewer"
|
|
265
255
|
);
|
|
266
|
-
const activity = this.agentActivity.get(record.id);
|
|
267
256
|
|
|
268
257
|
await ui.custom<undefined>(
|
|
269
258
|
(tui: any, theme: any, _keybindings: any, done: any) => {
|
|
270
259
|
return new ConversationViewer({
|
|
271
260
|
tui,
|
|
272
261
|
record,
|
|
273
|
-
activity,
|
|
274
262
|
theme,
|
|
275
263
|
done,
|
|
276
264
|
registry: this.registry,
|
package/src/ui/agent-widget.ts
CHANGED
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { AgentTypeRegistry } from "#src/config/agent-types";
|
|
10
|
+
import type { Subagent } from "#src/lifecycle/subagent";
|
|
10
11
|
import type { SubagentManager } from "#src/lifecycle/subagent-manager";
|
|
11
|
-
import type { AgentActivityTracker } from "#src/ui/agent-activity-tracker";
|
|
12
12
|
import { ERROR_STATUSES, type Theme } from "#src/ui/display";
|
|
13
|
-
import { renderWidgetLines } from "#src/ui/widget-renderer";
|
|
13
|
+
import { renderWidgetLines, type WidgetAgent } from "#src/ui/widget-renderer";
|
|
14
14
|
|
|
15
15
|
// ---- Types ----
|
|
16
16
|
|
|
@@ -79,7 +79,6 @@ export class AgentWidget {
|
|
|
79
79
|
|
|
80
80
|
constructor(
|
|
81
81
|
private manager: SubagentManager,
|
|
82
|
-
private agentActivity: Map<string, AgentActivityTracker>,
|
|
83
82
|
private registry: AgentTypeRegistry,
|
|
84
83
|
) {}
|
|
85
84
|
|
|
@@ -129,11 +128,31 @@ export class AgentWidget {
|
|
|
129
128
|
}
|
|
130
129
|
}
|
|
131
130
|
|
|
131
|
+
/** Project a live Subagent record onto a pure-data WidgetAgent snapshot. */
|
|
132
|
+
private toWidgetAgent(record: Subagent): WidgetAgent {
|
|
133
|
+
return {
|
|
134
|
+
id: record.id,
|
|
135
|
+
type: record.type,
|
|
136
|
+
status: record.status,
|
|
137
|
+
description: record.description,
|
|
138
|
+
toolUses: record.toolUses,
|
|
139
|
+
startedAt: record.startedAt,
|
|
140
|
+
completedAt: record.completedAt,
|
|
141
|
+
error: record.error,
|
|
142
|
+
lifetimeUsage: record.lifetimeUsage,
|
|
143
|
+
compactionCount: record.compactionCount,
|
|
144
|
+
turnCount: record.turnCount,
|
|
145
|
+
maxTurns: record.maxTurns,
|
|
146
|
+
activeTools: record.activeTools,
|
|
147
|
+
responseText: record.responseText,
|
|
148
|
+
contextPercent: record.getContextPercent(),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
132
152
|
/** Delegate rendering to the pure widget-renderer module. */
|
|
133
153
|
private renderWidget(tui: any, theme: Theme): string[] {
|
|
134
154
|
return renderWidgetLines({
|
|
135
|
-
agents: this.manager.listAgents(),
|
|
136
|
-
activityMap: this.agentActivity,
|
|
155
|
+
agents: this.manager.listAgents().map(r => this.toWidgetAgent(r)),
|
|
137
156
|
registry: this.registry,
|
|
138
157
|
spinnerFrame: this.widgetFrame,
|
|
139
158
|
terminalWidth: tui.terminal.columns,
|
|
@@ -9,7 +9,6 @@ import { type Component, matchesKey, type TUI, truncateToWidth, visibleWidth } f
|
|
|
9
9
|
import type { AgentConfigLookup } from "#src/config/agent-types";
|
|
10
10
|
import { getLifetimeTotal } from "#src/lifecycle/usage";
|
|
11
11
|
import type { Subagent } from "#src/types";
|
|
12
|
-
import type { AgentActivityTracker } from "#src/ui/agent-activity-tracker";
|
|
13
12
|
import { buildInvocationTags, formatDuration, formatSessionTokens, getDisplayName, getPromptModeLabel, type Theme } from "#src/ui/display";
|
|
14
13
|
import { formatMessage, formatStreamingIndicator } from "#src/ui/message-formatters";
|
|
15
14
|
|
|
@@ -24,7 +23,6 @@ export const VIEWPORT_HEIGHT_PCT = 70;
|
|
|
24
23
|
export interface ConversationViewerOptions {
|
|
25
24
|
tui: TUI;
|
|
26
25
|
record: Subagent;
|
|
27
|
-
activity: AgentActivityTracker | undefined;
|
|
28
26
|
theme: Theme;
|
|
29
27
|
done: (result: undefined) => void;
|
|
30
28
|
registry: AgentConfigLookup;
|
|
@@ -40,7 +38,6 @@ export class ConversationViewer implements Component {
|
|
|
40
38
|
|
|
41
39
|
private tui: TUI;
|
|
42
40
|
private record: Subagent;
|
|
43
|
-
private activity: AgentActivityTracker | undefined;
|
|
44
41
|
private theme: Theme;
|
|
45
42
|
private done: (result: undefined) => void;
|
|
46
43
|
private registry: AgentConfigLookup;
|
|
@@ -49,7 +46,6 @@ export class ConversationViewer implements Component {
|
|
|
49
46
|
constructor({
|
|
50
47
|
tui,
|
|
51
48
|
record,
|
|
52
|
-
activity,
|
|
53
49
|
theme,
|
|
54
50
|
done,
|
|
55
51
|
registry,
|
|
@@ -57,7 +53,6 @@ export class ConversationViewer implements Component {
|
|
|
57
53
|
}: ConversationViewerOptions) {
|
|
58
54
|
this.tui = tui;
|
|
59
55
|
this.record = record;
|
|
60
|
-
this.activity = activity;
|
|
61
56
|
this.theme = theme;
|
|
62
57
|
this.done = done;
|
|
63
58
|
this.registry = registry;
|
|
@@ -231,11 +226,11 @@ export class ConversationViewer implements Component {
|
|
|
231
226
|
needsSeparator = true;
|
|
232
227
|
}
|
|
233
228
|
|
|
234
|
-
// Streaming indicator for running agents
|
|
235
|
-
if (this.record.status === "running"
|
|
229
|
+
// Streaming indicator for running agents — read activity off the record
|
|
230
|
+
if (this.record.status === "running") {
|
|
236
231
|
lines.push(...formatStreamingIndicator(
|
|
237
|
-
this.
|
|
238
|
-
this.
|
|
232
|
+
this.record.activeTools,
|
|
233
|
+
this.record.responseText,
|
|
239
234
|
width,
|
|
240
235
|
th,
|
|
241
236
|
));
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
9
9
|
import type { AgentConfigLookup } from "#src/config/agent-types";
|
|
10
|
-
import type { LifetimeUsage
|
|
11
|
-
import { getLifetimeTotal
|
|
10
|
+
import type { LifetimeUsage } from "#src/lifecycle/usage";
|
|
11
|
+
import { getLifetimeTotal } from "#src/lifecycle/usage";
|
|
12
12
|
import type { SubagentType } from "#src/types";
|
|
13
13
|
import {
|
|
14
14
|
describeActivity,
|
|
@@ -35,15 +35,13 @@ export interface WidgetAgent {
|
|
|
35
35
|
readonly error?: string;
|
|
36
36
|
readonly lifetimeUsage?: Readonly<LifetimeUsage>;
|
|
37
37
|
readonly compactionCount: number;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
/** Read-only activity snapshot for widget rendering. */
|
|
41
|
-
export interface WidgetActivity {
|
|
42
|
-
readonly activeTools: ReadonlyMap<string, string>;
|
|
43
|
-
readonly responseText: string;
|
|
38
|
+
// Live activity (folded from the former WidgetActivity — precomputed by AgentWidget)
|
|
44
39
|
readonly turnCount: number;
|
|
45
40
|
readonly maxTurns?: number;
|
|
46
|
-
readonly
|
|
41
|
+
readonly activeTools: ReadonlyMap<string, string>;
|
|
42
|
+
readonly responseText: string;
|
|
43
|
+
/** Context-window utilisation (0–100), or null when unavailable. */
|
|
44
|
+
readonly contextPercent: number | null;
|
|
47
45
|
}
|
|
48
46
|
|
|
49
47
|
// ── Per-agent rendering ──────────────────────────────────────────────────────
|
|
@@ -51,7 +49,6 @@ export interface WidgetActivity {
|
|
|
51
49
|
/** Render a single finished agent line (no tree connector prefix). */
|
|
52
50
|
export function renderFinishedLine(
|
|
53
51
|
agent: WidgetAgent,
|
|
54
|
-
activity: WidgetActivity | undefined,
|
|
55
52
|
registry: AgentConfigLookup,
|
|
56
53
|
theme: Theme,
|
|
57
54
|
): string {
|
|
@@ -81,7 +78,7 @@ export function renderFinishedLine(
|
|
|
81
78
|
}
|
|
82
79
|
|
|
83
80
|
const parts: string[] = [];
|
|
84
|
-
|
|
81
|
+
parts.push(formatTurns(agent.turnCount, agent.maxTurns));
|
|
85
82
|
if (agent.toolUses > 0) parts.push(`${agent.toolUses} tool use${agent.toolUses === 1 ? "" : "s"}`);
|
|
86
83
|
parts.push(duration);
|
|
87
84
|
|
|
@@ -92,7 +89,6 @@ export function renderFinishedLine(
|
|
|
92
89
|
/** Render a single running agent as header + activity line pair (no tree connector prefix). */
|
|
93
90
|
export function renderRunningLines(
|
|
94
91
|
agent: WidgetAgent,
|
|
95
|
-
activity: WidgetActivity | undefined,
|
|
96
92
|
registry: AgentConfigLookup,
|
|
97
93
|
spinnerFrame: number,
|
|
98
94
|
theme: Theme,
|
|
@@ -103,18 +99,17 @@ export function renderRunningLines(
|
|
|
103
99
|
const elapsed = formatMs(Date.now() - agent.startedAt);
|
|
104
100
|
|
|
105
101
|
const tokens = getLifetimeTotal(agent.lifetimeUsage);
|
|
106
|
-
const
|
|
107
|
-
const tokenText = tokens > 0 ? formatSessionTokens(tokens, contextPercent, theme, agent.compactionCount) : "";
|
|
102
|
+
const tokenText = tokens > 0 ? formatSessionTokens(tokens, agent.contextPercent, theme, agent.compactionCount) : "";
|
|
108
103
|
|
|
109
104
|
const parts: string[] = [];
|
|
110
|
-
|
|
105
|
+
parts.push(formatTurns(agent.turnCount, agent.maxTurns));
|
|
111
106
|
if (agent.toolUses > 0) parts.push(`${agent.toolUses} tool use${agent.toolUses === 1 ? "" : "s"}`);
|
|
112
107
|
if (tokenText) parts.push(tokenText);
|
|
113
108
|
parts.push(elapsed);
|
|
114
109
|
const statsText = parts.join(" · ");
|
|
115
110
|
|
|
116
111
|
const frame = SPINNER[spinnerFrame % SPINNER.length];
|
|
117
|
-
const activityText =
|
|
112
|
+
const activityText = describeActivity(agent.activeTools, agent.responseText);
|
|
118
113
|
|
|
119
114
|
const header = `${theme.fg("accent", frame)} ${theme.bold(name)}${modeTag} ${theme.fg("muted", agent.description)} ${theme.fg("dim", "·")} ${theme.fg("dim", statsText)}`;
|
|
120
115
|
const activityLine = theme.fg("dim", ` \u23BF ${activityText}`);
|
|
@@ -157,7 +152,6 @@ interface WidgetSections {
|
|
|
157
152
|
/** Render each agent bucket into pre-formatted lines with ├─ tree connectors. */
|
|
158
153
|
function buildSections(
|
|
159
154
|
categories: AgentCategories,
|
|
160
|
-
activityMap: ReadonlyMap<string, WidgetActivity>,
|
|
161
155
|
registry: AgentConfigLookup,
|
|
162
156
|
spinnerFrame: number,
|
|
163
157
|
theme: Theme,
|
|
@@ -165,12 +159,12 @@ function buildSections(
|
|
|
165
159
|
): WidgetSections {
|
|
166
160
|
const finishedLines: string[] = [];
|
|
167
161
|
for (const a of categories.finished) {
|
|
168
|
-
finishedLines.push(truncate(theme.fg("dim", "\u251C\u2500") + " " + renderFinishedLine(a,
|
|
162
|
+
finishedLines.push(truncate(theme.fg("dim", "\u251C\u2500") + " " + renderFinishedLine(a, registry, theme)));
|
|
169
163
|
}
|
|
170
164
|
|
|
171
165
|
const runningLines: [string, string][] = [];
|
|
172
166
|
for (const a of categories.running) {
|
|
173
|
-
const [header, act] = renderRunningLines(a,
|
|
167
|
+
const [header, act] = renderRunningLines(a, registry, spinnerFrame, theme);
|
|
174
168
|
runningLines.push([
|
|
175
169
|
truncate(theme.fg("dim", "\u251C\u2500") + ` ${header}`),
|
|
176
170
|
truncate(theme.fg("dim", "\u2502 ") + act),
|
|
@@ -259,14 +253,13 @@ function assembleOverflow(
|
|
|
259
253
|
/** Pure rendering of the widget body. Returns lines to display. */
|
|
260
254
|
export function renderWidgetLines(params: {
|
|
261
255
|
agents: readonly WidgetAgent[];
|
|
262
|
-
activityMap: ReadonlyMap<string, WidgetActivity>;
|
|
263
256
|
registry: AgentConfigLookup;
|
|
264
257
|
spinnerFrame: number;
|
|
265
258
|
terminalWidth: number;
|
|
266
259
|
theme: Theme;
|
|
267
260
|
shouldShowFinished: (agentId: string, status: string) => boolean;
|
|
268
261
|
}): string[] {
|
|
269
|
-
const { agents,
|
|
262
|
+
const { agents, registry, spinnerFrame, terminalWidth, theme, shouldShowFinished } = params;
|
|
270
263
|
|
|
271
264
|
const { running, queued, finished } = categorizeAgents(agents, shouldShowFinished);
|
|
272
265
|
|
|
@@ -281,7 +274,6 @@ export function renderWidgetLines(params: {
|
|
|
281
274
|
|
|
282
275
|
const { finishedLines, runningLines, queuedLine } = buildSections(
|
|
283
276
|
{ running, queued, finished },
|
|
284
|
-
activityMap,
|
|
285
277
|
registry,
|
|
286
278
|
spinnerFrame,
|
|
287
279
|
theme,
|