@gotgenes/pi-subagents 16.5.0 → 17.0.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.
Files changed (32) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/public.d.ts +4 -1
  3. package/docs/architecture/architecture.md +39 -31
  4. package/docs/plans/0421-migrate-activity-readers-to-record-getters.md +309 -0
  5. package/docs/plans/0422-delete-activity-tracker-ui-observer.md +204 -0
  6. package/docs/plans/0423-widget-self-drive-from-lifecycle.md +264 -0
  7. package/docs/plans/0424-drop-widget-dep-from-subagent-tool.md +166 -0
  8. package/docs/plans/0425-reconcile-subagent-events-contract.md +144 -0
  9. package/docs/retro/0420-fold-run-metrics-live-activity-onto-record.md +45 -0
  10. package/docs/retro/0421-migrate-activity-readers-to-record-getters.md +105 -0
  11. package/docs/retro/0422-delete-activity-tracker-ui-observer.md +97 -0
  12. package/docs/retro/0423-widget-self-drive-from-lifecycle.md +112 -0
  13. package/docs/retro/0424-drop-widget-dep-from-subagent-tool.md +89 -0
  14. package/docs/retro/0425-reconcile-subagent-events-contract.md +46 -0
  15. package/package.json +1 -1
  16. package/src/index.ts +12 -6
  17. package/src/observation/composite-subagent-observer.ts +49 -0
  18. package/src/observation/notification.ts +3 -12
  19. package/src/observation/subagent-events-observer.ts +1 -4
  20. package/src/runtime.ts +0 -6
  21. package/src/service/service.ts +4 -1
  22. package/src/tools/agent-tool.ts +1 -36
  23. package/src/tools/background-spawner.ts +1 -25
  24. package/src/tools/foreground-runner.ts +9 -34
  25. package/src/tools/helpers.ts +5 -4
  26. package/src/types.ts +1 -1
  27. package/src/ui/agent-menu.ts +0 -12
  28. package/src/ui/agent-widget.ts +56 -17
  29. package/src/ui/conversation-viewer.ts +4 -9
  30. package/src/ui/widget-renderer.ts +14 -22
  31. package/src/ui/agent-activity-tracker.ts +0 -84
  32. package/src/ui/ui-observer.ts +0 -61
@@ -1,7 +1,6 @@
1
1
  import type { AgentToolResult } from "@earendil-works/pi-coding-agent";
2
2
  import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
3
3
  import type { AgentSpawnConfig } from "#src/lifecycle/subagent-manager";
4
- import type { AgentActivityAccess } from "#src/tools/agent-tool";
5
4
  import {
6
5
  buildDetails,
7
6
  formatLifetimeTokens,
@@ -10,14 +9,12 @@ import {
10
9
  } from "#src/tools/helpers";
11
10
  import type { ResolvedSpawnConfig } from "#src/tools/spawn-config";
12
11
  import type { ParentSessionInfo, Subagent } from "#src/types";
13
- import { AgentActivityTracker } from "#src/ui/agent-activity-tracker";
14
12
  import {
15
13
  type AgentDetails,
16
14
  describeActivity,
17
15
  formatMs,
18
16
  SPINNER,
19
17
  } from "#src/ui/display";
20
- import { subscribeUIObserver } from "#src/ui/ui-observer";
21
18
 
22
19
  /** Narrow manager interface for the foreground runner. */
23
20
  export interface ForegroundManagerDeps {
@@ -29,12 +26,6 @@ export interface ForegroundManagerDeps {
29
26
  ): Promise<Subagent>;
30
27
  }
31
28
 
32
- /** Narrow widget interface for the foreground runner. */
33
- export interface ForegroundWidgetDeps {
34
- ensureTimer(): void;
35
- markFinished(id: string): void;
36
- }
37
-
38
29
  /** All values the foreground runner needs beyond the resolved config. */
39
30
  export interface ForegroundParams {
40
31
  config: ResolvedSpawnConfig;
@@ -44,13 +35,10 @@ export interface ForegroundParams {
44
35
 
45
36
  /**
46
37
  * Run an agent synchronously in the foreground, streaming spinner updates.
47
- * Owns: spinner interval, AgentActivityTracker creation, UI observer subscription,
48
- * streaming onUpdate callbacks, cleanup, and result formatting.
38
+ * Owns: spinner interval, streaming onUpdate callbacks, cleanup, and result formatting.
49
39
  */
50
40
  export async function runForeground(
51
41
  manager: ForegroundManagerDeps,
52
- widget: ForegroundWidgetDeps,
53
- agentActivity: AgentActivityAccess,
54
42
  params: ForegroundParams,
55
43
  signal: AbortSignal | undefined,
56
44
  onUpdate: ((update: AgentToolResult<any>) => void) | undefined,
@@ -58,10 +46,7 @@ export async function runForeground(
58
46
  const { identity, execution, presentation } = params.config;
59
47
  let spinnerFrame = 0;
60
48
  const startedAt = Date.now();
61
- let fgId: string | undefined;
62
49
 
63
- const fgState = new AgentActivityTracker(execution.effectiveMaxTurns);
64
- let unsubUI: (() => void) | undefined;
65
50
  let recordRef: Subagent | undefined;
66
51
 
67
52
  const streamUpdate = () => {
@@ -70,11 +55,15 @@ export async function runForeground(
70
55
  ...presentation.detailBase,
71
56
  toolUses,
72
57
  tokens: recordRef ? formatLifetimeTokens(recordRef) : "",
73
- turnCount: fgState.turnCount,
74
- maxTurns: fgState.maxTurns,
58
+ // Read activity off the record; fall back to safe defaults before onSessionCreated fires
59
+ turnCount: recordRef?.turnCount ?? 1,
60
+ maxTurns: recordRef?.maxTurns ?? execution.effectiveMaxTurns,
75
61
  durationMs: Date.now() - startedAt,
76
62
  status: "running",
77
- activity: describeActivity(fgState.activeTools, fgState.responseText),
63
+ activity: describeActivity(
64
+ recordRef?.activeTools ?? new Map(),
65
+ recordRef?.responseText ?? "",
66
+ ),
78
67
  spinnerFrame: spinnerFrame % SPINNER.length,
79
68
  };
80
69
  onUpdate?.({
@@ -109,34 +98,20 @@ export async function runForeground(
109
98
  parentSession: params.parentSession,
110
99
  observer: {
111
100
  onSessionCreated: (agent) => {
112
- const sub = agent.subagentSession!;
113
- fgState.setSession(sub);
114
101
  recordRef = agent;
115
- unsubUI = subscribeUIObserver(sub, fgState, streamUpdate);
116
- fgId = agent.id;
117
- agentActivity.set(agent.id, fgState);
118
- widget.ensureTimer();
119
102
  },
120
103
  },
121
104
  },
122
105
  );
123
106
  } catch (err) {
124
107
  clearInterval(spinnerInterval);
125
- unsubUI?.();
126
108
  return textResult(err instanceof Error ? err.message : String(err));
127
109
  }
128
110
 
129
111
  clearInterval(spinnerInterval);
130
- unsubUI?.();
131
-
132
- // Clean up foreground agent from widget
133
- if (fgId) {
134
- agentActivity.delete(fgId);
135
- widget.markFinished(fgId);
136
- }
137
112
 
138
113
  const tokenText = formatLifetimeTokens(record);
139
- const details = buildDetails(presentation.detailBase, record, fgState, { tokens: tokenText });
114
+ const details = buildDetails(presentation.detailBase, record, { tokens: tokenText });
140
115
 
141
116
  const fallbackNote = identity.fellBack
142
117
  ? `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,
package/src/types.ts CHANGED
@@ -12,7 +12,7 @@ export type { AgentSessionEvent, ThinkingLevel };
12
12
 
13
13
  /**
14
14
  * Narrow session interface for event subscription.
15
- * Used by record-observer and ui-observer — only the subscribe method is needed.
15
+ * Used by record-observer — only the subscribe method is needed.
16
16
  */
17
17
  export interface SubscribableSession {
18
18
  subscribe(fn: (event: AgentSessionEvent) => void): () => void;
@@ -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,11 @@
7
7
  */
8
8
 
9
9
  import { AgentTypeRegistry } from "#src/config/agent-types";
10
- import type { SubagentManager } from "#src/lifecycle/subagent-manager";
11
- import type { AgentActivityTracker } from "#src/ui/agent-activity-tracker";
10
+ import type { Subagent } from "#src/lifecycle/subagent";
11
+ import type { SubagentManager, SubagentManagerObserver } from "#src/lifecycle/subagent-manager";
12
+ import type { CompactionInfo } from "#src/types";
12
13
  import { ERROR_STATUSES, type Theme } from "#src/ui/display";
13
- import { renderWidgetLines } from "#src/ui/widget-renderer";
14
+ import { renderWidgetLines, type WidgetAgent } from "#src/ui/widget-renderer";
14
15
 
15
16
  // ---- Types ----
16
17
 
@@ -61,7 +62,7 @@ export type UICtx = {
61
62
 
62
63
  // ---- Widget manager ----
63
64
 
64
- export class AgentWidget {
65
+ export class AgentWidget implements SubagentManagerObserver {
65
66
  private uiCtx: UICtx | undefined;
66
67
  private widgetFrame = 0;
67
68
  private widgetInterval: ReturnType<typeof setInterval> | undefined;
@@ -79,7 +80,6 @@ export class AgentWidget {
79
80
 
80
81
  constructor(
81
82
  private manager: SubagentManager,
82
- private agentActivity: Map<string, AgentActivityTracker>,
83
83
  private registry: AgentTypeRegistry,
84
84
  ) {}
85
85
 
@@ -108,9 +108,36 @@ export class AgentWidget {
108
108
  this.update();
109
109
  }
110
110
 
111
+ // ---- SubagentManagerObserver: react to lifecycle, self-drive the timer ----
112
+
113
+ /** A subagent started running — ensure the update loop is live and render. */
114
+ onSubagentStarted(_record: Subagent) {
115
+ this.startLoop();
116
+ }
117
+
118
+ /** A background subagent was created (queued) — ensure the loop is live and render. */
119
+ onSubagentCreated(_record: Subagent) {
120
+ this.startLoop();
121
+ }
122
+
123
+ /** A subagent completed — render so the finished state is seeded and shown. */
124
+ onSubagentCompleted(_record: Subagent) {
125
+ this.update();
126
+ }
127
+
128
+ /** A subagent's session compacted — render to refresh the compaction count. */
129
+ onSubagentCompacted(_record: Subagent, _info: CompactionInfo) {
130
+ this.update();
131
+ }
132
+
133
+ /** Start the update timer (if not already running) and render immediately. */
134
+ private startLoop() {
135
+ this.ensureTimer();
136
+ this.update();
137
+ }
138
+
111
139
  /** Ensure the widget update timer is running. */
112
- // fallow-ignore-next-line unused-class-member
113
- ensureTimer() {
140
+ private ensureTimer() {
114
141
  this.widgetInterval ??= setInterval(() => this.update(), 80);
115
142
  }
116
143
 
@@ -121,19 +148,31 @@ export class AgentWidget {
121
148
  return age < maxAge;
122
149
  }
123
150
 
124
- /** Record an agent as finished (call when agent completes). */
125
- // fallow-ignore-next-line unused-class-member
126
- markFinished(agentId: string) {
127
- if (!this.finishedTurnAge.has(agentId)) {
128
- this.finishedTurnAge.set(agentId, 0);
129
- }
151
+ /** Project a live Subagent record onto a pure-data WidgetAgent snapshot. */
152
+ private toWidgetAgent(record: Subagent): WidgetAgent {
153
+ return {
154
+ id: record.id,
155
+ type: record.type,
156
+ status: record.status,
157
+ description: record.description,
158
+ toolUses: record.toolUses,
159
+ startedAt: record.startedAt,
160
+ completedAt: record.completedAt,
161
+ error: record.error,
162
+ lifetimeUsage: record.lifetimeUsage,
163
+ compactionCount: record.compactionCount,
164
+ turnCount: record.turnCount,
165
+ maxTurns: record.maxTurns,
166
+ activeTools: record.activeTools,
167
+ responseText: record.responseText,
168
+ contextPercent: record.getContextPercent(),
169
+ };
130
170
  }
131
171
 
132
172
  /** Delegate rendering to the pure widget-renderer module. */
133
173
  private renderWidget(tui: any, theme: Theme): string[] {
134
174
  return renderWidgetLines({
135
- agents: this.manager.listAgents(),
136
- activityMap: this.agentActivity,
175
+ agents: this.manager.listAgents().map(r => this.toWidgetAgent(r)),
137
176
  registry: this.registry,
138
177
  spinnerFrame: this.widgetFrame,
139
178
  terminalWidth: tui.terminal.columns,
@@ -184,8 +223,8 @@ export class AgentWidget {
184
223
 
185
224
  /**
186
225
  * Seed linger tracking for any newly-observed finished agent.
187
- * Replaces the external `markFinished` call NotificationManager used to make:
188
- * the widget owns detection of completions it sees via `listAgents()`.
226
+ * The widget owns detection of completions it observes via `listAgents()`,
227
+ * so no external bookkeeping call is needed.
189
228
  * Idempotent — only seeds when an entry is absent, so repeated updates within
190
229
  * a turn neither reset nor advance the age.
191
230
  */
@@ -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,
@@ -1,84 +0,0 @@
1
- /**
2
- * agent-activity-tracker.ts — Per-agent live activity state with explicit transition methods.
3
- *
4
- * Replaces the mutable `AgentActivity` interface that was written via output arguments
5
- * in `ui-observer.ts`. Callers use named transition methods; readers use read-only accessors.
6
- */
7
-
8
- import type { SessionLike } from "#src/lifecycle/usage";
9
-
10
- /** Per-agent live activity state with explicit transition methods and read-only accessors. */
11
- export class AgentActivityTracker {
12
- private _activeTools = new Map<string, string>();
13
- private _toolKeySeq = 0;
14
- private _responseText = "";
15
- private _session: SessionLike | undefined = undefined;
16
- private _turnCount = 1;
17
-
18
- constructor(private readonly _maxTurns?: number) {}
19
-
20
- // ── Transition methods (write surface) ──────────────────────────────────
21
-
22
- /** Record that a tool has started executing. */
23
- onToolStart(toolName: string): void {
24
- this._activeTools.set(toolName + "_" + (++this._toolKeySeq), toolName);
25
- }
26
-
27
- /** Remove a tool from active tools (called when tool execution ends). No-op when no matching tool is active. */
28
- onToolDone(toolName: string): void {
29
- for (const [key, name] of this._activeTools) {
30
- if (name === toolName) {
31
- this._activeTools.delete(key);
32
- break;
33
- }
34
- }
35
- }
36
-
37
- /** Reset the current response text (called at the start of each assistant message). */
38
- onMessageStart(): void {
39
- this._responseText = "";
40
- }
41
-
42
- /** Append a text delta to the current response text. */
43
- onMessageUpdate(delta: string): void {
44
- this._responseText += delta;
45
- }
46
-
47
- /** Record that a turn has ended; increments turnCount. */
48
- onTurnEnd(): void {
49
- this._turnCount++;
50
- }
51
-
52
- /** Bind the session reference (called once when the agent session is created). */
53
- setSession(session: SessionLike): void {
54
- this._session = session;
55
- }
56
-
57
- // ── Read-only accessors ──────────────────────────────────────────────────
58
-
59
- /** Currently-active tools: key → tool name. Multiple entries for concurrent same-name tools. */
60
- get activeTools(): ReadonlyMap<string, string> {
61
- return this._activeTools;
62
- }
63
-
64
- /** The agent's latest partial response text (reset at each message start). */
65
- get responseText(): string {
66
- return this._responseText;
67
- }
68
-
69
- /** The active SDK session, or undefined before the first session is created. */
70
- get session(): SessionLike | undefined {
71
- return this._session;
72
- }
73
-
74
- /** Current turn count (starts at 1). */
75
- get turnCount(): number {
76
- return this._turnCount;
77
- }
78
-
79
- /** Effective max turns for this agent, or undefined for unlimited. */
80
- get maxTurns(): number | undefined {
81
- return this._maxTurns;
82
- }
83
-
84
- }
@@ -1,61 +0,0 @@
1
- /**
2
- * ui-observer.ts — Subscribes to session events and updates AgentActivityTracker state.
3
- *
4
- * Replaces the callback-based createActivityTracker pattern with a direct
5
- * session subscription for streaming UI state (active tools, response text,
6
- * turn count, lifetime usage).
7
- */
8
-
9
- import type { SubscribableSession } from "#src/types";
10
- import type { AgentActivityTracker } from "#src/ui/agent-activity-tracker";
11
-
12
- /**
13
- * Subscribe to session events and stream UI state into an AgentActivityTracker.
14
- *
15
- * Handles:
16
- * - `tool_execution_start` → `tracker.onToolStart(name)`
17
- * - `tool_execution_end` → `tracker.onToolEnd(name)`
18
- * - `message_start` → `tracker.onMessageStart()`
19
- * - `message_update` (text_delta) → `tracker.onMessageUpdate(delta)`
20
- * - `turn_end` → `tracker.onTurnEnd()`
21
- * - `message_end` (assistant, with usage) → `tracker.onUsageUpdate(usage)`
22
- *
23
- * Calls `onUpdate?.()` after each state mutation to trigger re-renders.
24
- *
25
- * @returns An unsubscribe function.
26
- */
27
- export function subscribeUIObserver(
28
- session: SubscribableSession,
29
- tracker: AgentActivityTracker,
30
- onUpdate?: () => void,
31
- ): () => void {
32
- return session.subscribe((event) => {
33
- if (event.type === "tool_execution_start") {
34
- tracker.onToolStart(event.toolName);
35
- onUpdate?.();
36
- }
37
-
38
- if (event.type === "tool_execution_end") {
39
- tracker.onToolDone(event.toolName);
40
- onUpdate?.();
41
- }
42
-
43
- if (event.type === "message_start") {
44
- tracker.onMessageStart();
45
- }
46
-
47
- if (
48
- event.type === "message_update" &&
49
- event.assistantMessageEvent.type === "text_delta"
50
- ) {
51
- tracker.onMessageUpdate(event.assistantMessageEvent.delta);
52
- onUpdate?.();
53
- }
54
-
55
- if (event.type === "turn_end") {
56
- tracker.onTurnEnd();
57
- onUpdate?.();
58
- }
59
-
60
- });
61
- }