@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/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, runtime.agentActivity, registry);
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 metrics.
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, and stats (toolUses, lifetimeUsage, compactionCount) together
6
- * with the transition methods (markRunning, markCompleted, ) and accumulation
7
- * methods (incrementToolUses, addUsage, incrementCompactions) that mutate it.
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: activity?.turnCount ?? 0,
90
- maxTurns: activity?.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, this.agentActivity.get(record.id)),
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
- * - `tool_execution_end` → `state.incrementToolUses()`
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
- turnCount: fgState.turnCount,
74
- maxTurns: fgState.maxTurns,
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(fgState.activeTools, fgState.responseText),
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, fgState, { tokens: tokenText });
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`
@@ -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: activity?.turnCount,
40
- maxTurns: activity?.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,
@@ -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,
@@ -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" && this.activity) {
229
+ // Streaming indicator for running agents — read activity off the record
230
+ if (this.record.status === "running") {
236
231
  lines.push(...formatStreamingIndicator(
237
- this.activity.activeTools,
238
- this.activity.responseText,
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, SessionLike } from "#src/lifecycle/usage";
11
- import { getLifetimeTotal, getSessionContextPercent } from "#src/lifecycle/usage";
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 session?: SessionLike;
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
- if (activity) parts.push(formatTurns(activity.turnCount, activity.maxTurns));
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 contextPercent = activity?.session ? getSessionContextPercent(activity.session) : null;
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
- if (activity) parts.push(formatTurns(activity.turnCount, activity.maxTurns));
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 = activity ? describeActivity(activity.activeTools, activity.responseText) : "thinking\u2026";
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, activityMap.get(a.id), registry, theme)));
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, activityMap.get(a.id), registry, spinnerFrame, theme);
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, activityMap, registry, spinnerFrame, terminalWidth, theme, shouldShowFinished } = params;
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,