@gotgenes/pi-subagents 7.2.8 → 7.3.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 CHANGED
@@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [7.3.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v7.2.8...pi-subagents-v7.3.0) (2026-05-25)
9
+
10
+
11
+ ### Features
12
+
13
+ * extract assembleWidgetState from agent-widget update ([c63fdac](https://github.com/gotgenes/pi-packages/commit/c63fdacdb184f989598ebafbf595cc9c01c9d3a0))
14
+
15
+
16
+ ### Documentation
17
+
18
+ * plan decompose update in agent-widget.ts ([#207](https://github.com/gotgenes/pi-packages/issues/207)) ([5ae2e74](https://github.com/gotgenes/pi-packages/commit/5ae2e74385753cd8e68504f767539aff019e5d08))
19
+ * plan decompose update in agent-widget.ts ([#207](https://github.com/gotgenes/pi-packages/issues/207)) ([fb30d98](https://github.com/gotgenes/pi-packages/commit/fb30d98fc0fcc2c3cd6f016b78251cdb4caf6d96))
20
+ * **retro:** add planning stage notes for issue [#207](https://github.com/gotgenes/pi-packages/issues/207) ([1624d2d](https://github.com/gotgenes/pi-packages/commit/1624d2d4aef5fee77db319940de7e2a3e93a8a27))
21
+ * **retro:** add planning stage notes for issue [#207](https://github.com/gotgenes/pi-packages/issues/207) ([931ff0e](https://github.com/gotgenes/pi-packages/commit/931ff0ea3810d87a35f49b3b671d1ef4d88d55f7))
22
+ * **retro:** add TDD stage notes for issue [#207](https://github.com/gotgenes/pi-packages/issues/207) ([770940a](https://github.com/gotgenes/pi-packages/commit/770940a3ff5c6b7ddd84ac35a4352ffc6c7c189e))
23
+ * update complexity hotspots after widget decomposition ([5848a17](https://github.com/gotgenes/pi-packages/commit/5848a173158f13fa58ccd357edf9a310559f92e1))
24
+
8
25
  ## [7.2.8](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v7.2.7...pi-subagents-v7.2.8) (2026-05-25)
9
26
 
10
27
 
@@ -466,10 +466,7 @@ Bags with 10+ fields are the highest priority for decomposition.
466
466
 
467
467
  Functions with cyclomatic complexity ≥ 21 (critical threshold):
468
468
 
469
- | Function | Cyclomatic | Cognitive | File | Concern |
470
- | ------------------- | ---------- | --------- | --------------------------- | ------------------------------- |
471
- | `renderWidgetLines` | 25 | 44 | `ui/widget-renderer.ts` | Renders widget status lines |
472
- | `update` | 21 | 31 | `ui/agent-widget.ts` | Widget lifecycle + polling |
469
+ No functions remain above the critical threshold — all hotspots resolved in Phase 12.
473
470
 
474
471
  ### Churn hotspots
475
472
 
@@ -0,0 +1,242 @@
1
+ ---
2
+ issue: 207
3
+ issue_title: "Decompose update in agent-widget.ts (cognitive 31)"
4
+ ---
5
+
6
+ # Decompose `update` in `agent-widget.ts`
7
+
8
+ ## Problem Statement
9
+
10
+ `update` in `ui/agent-widget.ts` has cognitive complexity 31 (CRITICAL per fallow health).
11
+ It mixes agent state categorization, widget teardown, status bar management, and widget registration dispatch in a single 70-line method.
12
+ Phase 12, Step 3 targets cognitive complexity < 10 per function.
13
+
14
+ ## Goals
15
+
16
+ - Extract `assembleWidgetState` as an exported pure function (agent list → lightweight counts/flags) that is directly unit-testable.
17
+ - Extract `clearWidget` as a private method encapsulating the "nothing to show" teardown path.
18
+ - Extract `updateStatusBar` as a private method encapsulating status text computation and conditional update.
19
+ - Simplify `update` to a thin orchestrator: guard → assemble → if idle clear → else update status + register.
20
+ - Target: cognitive complexity < 10 for every function in the file.
21
+
22
+ ## Non-Goals
23
+
24
+ - Decomposing `renderWidgetLines` (#205, done), `showAgentDetail` (#206, done), or shared test fixtures (#208) — sibling Phase 12 steps.
25
+ - Extracting a separate timer-manager class — the timer lifecycle (`ensureTimer` + `clearWidget`) is two lines of `setInterval`/`clearInterval`.
26
+ - Changing the widget's visual output, status bar format, registration timing, or timer interval.
27
+ - Narrowing the `AgentManager` dependency to an interface — tracked separately.
28
+ - Adding end-to-end tests for `AgentWidget` — the widget depends on the Pi TUI context.
29
+ - Changing `dispose` behavior — it remains a shutdown-only teardown path.
30
+
31
+ ## Background
32
+
33
+ `agent-widget.ts` was substantially decomposed in #148 (Phase 9, Step P), which extracted pure rendering into `widget-renderer.ts`.
34
+ The widget shrank from 374 to ~198 lines.
35
+ `update` remained as a 70-line orchestrator with five interwoven concerns:
36
+
37
+ 1. **Guard** — early return when `uiCtx` is not yet set.
38
+ 2. **Agent state categorization** — counting running/queued/finished agents from `listAgents()`.
39
+ 3. **Clear path** — unregistering widget, clearing status, stopping timer, cleaning stale `finishedTurnAge` entries.
40
+ 4. **Status bar update** — computing status text from running/queued counts, conditionally calling `setStatus`.
41
+ 5. **Widget lifecycle** — incrementing `widgetFrame`, registering the widget factory on first use, calling `requestRender()` on subsequent ticks.
42
+
43
+ Concerns 3–4 consume counts from concern 2 but each has a distinct responsibility.
44
+
45
+ `categorizeAgents` in `widget-renderer.ts` performs a similar filter but returns full `WidgetAgent[]` arrays for rendering.
46
+ `assembleWidgetState` returns lightweight counts for lifecycle decisions — different outputs for different consumers, not duplication.
47
+
48
+ No existing tests cover `AgentWidget` methods — `test/widget-renderer.test.ts` covers the rendering layer.
49
+
50
+ ### Complexity sources in `update` (cognitive 31)
51
+
52
+ 1. Agent counting loop with 3-branch if/else (`status` checks + `shouldShowFinished`) — contributes ~6.
53
+ 2. Conditional widget clear path: nested checks for `widgetRegistered`, `lastStatusText`, `widgetInterval`, plus a `for`-loop with nested `if` for stale-entry cleanup — contributes ~12.
54
+ 3. Status text computation with conditional `if (hasActive)` and nested `if`/push branches — contributes ~5.
55
+ 4. Conditional status update (`newStatusText !== this.lastStatusText`) — contributes ~2.
56
+ 5. Widget registration dispatch (`if (!widgetRegistered) ... else ...`) — contributes ~4.
57
+
58
+ ### `dispose` is not a duplication target
59
+
60
+ `dispose` (10 lines, cognitive ~2) and `update`'s idle path share *some* statements — `clearInterval`, `setWidget(undefined)`, `setStatus(undefined)`, flag resets — but differ in two ways:
61
+
62
+ - **Guards:** `update`'s idle path guards each call (`if (widgetRegistered)`, `if (lastStatusText !== undefined)`) to avoid redundant SDK calls during repeated timer ticks.
63
+ `dispose` unconditionally calls `setWidget`/`setStatus` (when `uiCtx` exists) as a correctness guarantee during shutdown.
64
+ - **Stale-entry cleanup:** `update`'s idle path cleans `finishedTurnAge` entries for agents no longer in `listAgents()`.
65
+ `dispose` skips this — the Map is about to be garbage collected.
66
+
67
+ Per the code-design skill ("duplication is far cheaper than the wrong abstraction" — verify structural context before extracting), these are different lifecycle semantics.
68
+ `dispose` stays as-is; `clearWidget` is extracted only from `update`'s idle path.
69
+
70
+ ## Design Overview
71
+
72
+ ### `assembleWidgetState` (exported, pure)
73
+
74
+ ```typescript
75
+ /** Minimal agent shape needed for widget lifecycle decisions. */
76
+ interface AgentSummary {
77
+ readonly id: string;
78
+ readonly status: string;
79
+ readonly completedAt?: number;
80
+ }
81
+
82
+ export interface WidgetState {
83
+ readonly runningCount: number;
84
+ readonly queuedCount: number;
85
+ readonly hasFinished: boolean;
86
+ readonly hasActive: boolean;
87
+ }
88
+
89
+ export function assembleWidgetState(
90
+ agents: readonly AgentSummary[],
91
+ shouldShowFinished: (agentId: string, status: string) => boolean,
92
+ ): WidgetState
93
+ ```
94
+
95
+ The input uses a local `AgentSummary` interface (3 fields) rather than `WidgetAgent` (10+ fields).
96
+ This follows ISP — `assembleWidgetState` only reads `id`, `status`, and `completedAt`.
97
+ Tests can pass plain objects without constructing full agent fixtures.
98
+ `AgentRecord` satisfies `AgentSummary` structurally, so no adapter is needed at the call site.
99
+
100
+ `hasActive` is derived from the counts (`runningCount > 0 || queuedCount > 0`) but included for call-site readability.
101
+ Only `assembleWidgetState` constructs `WidgetState`, so consistency is guaranteed.
102
+
103
+ ### `clearWidget` (private method)
104
+
105
+ Encapsulates `update`'s "nothing to show" teardown path:
106
+
107
+ - Unregister widget via `setWidget("agents", undefined)` if `widgetRegistered`.
108
+ - Clear status via `setStatus("subagents", undefined)` if `lastStatusText` is set.
109
+ - Stop timer via `clearInterval` if `widgetInterval` is running.
110
+ - Reset lifecycle flags (`widgetRegistered`, `tui`, `lastStatusText`, `widgetInterval`).
111
+ - Clean stale `finishedTurnAge` entries (agents no longer in `allAgents`).
112
+
113
+ Accepts `allAgents` as a parameter for the stale-entry cleanup.
114
+
115
+ ```typescript
116
+ private clearWidget(allAgents: readonly AgentSummary[]): void
117
+ ```
118
+
119
+ `dispose` does **not** delegate to `clearWidget` — see "dispose is not a duplication target" above.
120
+
121
+ ### `updateStatusBar` (private method)
122
+
123
+ Encapsulates the status text concern:
124
+
125
+ - Compute status text from `runningCount` / `queuedCount` (undefined when `!hasActive`).
126
+ - Call `setStatus("subagents", text)` only when text differs from `lastStatusText`.
127
+ - Cache the new value in `lastStatusText`.
128
+
129
+ ```typescript
130
+ private updateStatusBar(state: WidgetState): void
131
+ ```
132
+
133
+ ### After refactoring
134
+
135
+ ```typescript
136
+ update() {
137
+ if (!this.uiCtx) return;
138
+
139
+ const allAgents = this.manager.listAgents();
140
+ const state = assembleWidgetState(allAgents, (id, status) => this.shouldShowFinished(id, status));
141
+
142
+ if (!state.hasActive && !state.hasFinished) {
143
+ this.clearWidget(allAgents);
144
+ return;
145
+ }
146
+
147
+ this.updateStatusBar(state);
148
+ this.widgetFrame++;
149
+
150
+ if (!this.widgetRegistered) {
151
+ this.uiCtx.setWidget("agents", (tui, theme) => {
152
+ this.tui = tui;
153
+ return {
154
+ render: () => this.renderWidget(tui, theme),
155
+ invalidate: () => {
156
+ this.widgetRegistered = false;
157
+ this.tui = undefined;
158
+ },
159
+ };
160
+ }, { placement: "aboveEditor" });
161
+ this.widgetRegistered = true;
162
+ } else {
163
+ this.tui?.requestRender();
164
+ }
165
+ }
166
+ ```
167
+
168
+ Cognitive complexity: ~4 (one guard early return + one if/else branch + flat registration dispatch).
169
+
170
+ ### Complexity budget
171
+
172
+ | Function | Estimated cognitive complexity |
173
+ | --------------------- | ----------------------------------------------- |
174
+ | `assembleWidgetState` | ~3 (flat loop with 3 branches) |
175
+ | `clearWidget` | ~6 (4 guards + loop with if) |
176
+ | `updateStatusBar` | ~4 (hasActive check + diff check) |
177
+ | `update` (after) | ~4 (guard + idle check + registration dispatch) |
178
+ | `dispose` (unchanged) | ~2 |
179
+
180
+ All under 10.
181
+
182
+ ## Module-Level Changes
183
+
184
+ ### Changed: `src/ui/agent-widget.ts`
185
+
186
+ - Add local `AgentSummary` interface (3 fields: `id`, `status`, `completedAt?`).
187
+ - Add exported `WidgetState` interface.
188
+ - Add exported `assembleWidgetState(agents, shouldShowFinished)` pure function.
189
+ - Add private `clearWidget(allAgents)` method — extracted from `update`'s idle path.
190
+ - Add private `updateStatusBar(state)` method — extracted from `update`'s status bar logic.
191
+ - Simplify `update` to orchestrate: guard → assemble → if idle clear → else update status + register.
192
+ - `dispose` is unchanged.
193
+
194
+ No exports are removed or renamed.
195
+ The public API (`AgentWidget` class with `setUICtx`, `onTurnStart`, `ensureTimer`, `markFinished`, `update`, `dispose`) is unchanged.
196
+ `UICtx` type stays exported.
197
+
198
+ ### Unchanged: `test/widget-renderer.test.ts`
199
+
200
+ No changes — this file covers `widget-renderer.ts` functions, not `agent-widget.ts`.
201
+
202
+ ### Changed: `docs/architecture/architecture.md`
203
+
204
+ - Update the complexity hotspots table: remove `update` row (no longer ≥ 21 cyclomatic).
205
+ - Also remove the `renderWidgetLines` row if #205 is already implemented (it is — closed).
206
+
207
+ ## Test Impact Analysis
208
+
209
+ 1. **New tests enabled:** Direct unit tests for `assembleWidgetState` — a pure function accepting plain objects.
210
+ Tests cover: empty list, running-only, queued-only, finished-only, mixed states, `shouldShowFinished` filtering, agents without `completedAt` excluded from finished.
211
+ The narrow `AgentSummary` input type means test fixtures are 3-field objects — no need for full agent records.
212
+ 2. **No existing tests become redundant** — there are currently no tests for `AgentWidget`.
213
+ 3. **No existing tests must change** — `test/widget-renderer.test.ts` (23 tests) exercises the renderer layer and is unaffected.
214
+
215
+ ## TDD Order
216
+
217
+ 1. **Red → Green:** Write unit tests for `assembleWidgetState` covering all agent status combinations.
218
+ Add the `AgentSummary` interface, `WidgetState` interface, and `assembleWidgetState` function as exported module-level items.
219
+ Implement to make tests pass.
220
+ Commit: `feat: extract assembleWidgetState from agent-widget update`
221
+
222
+ 2. **Refactor:** Wire `update` to use `assembleWidgetState`.
223
+ Extract `clearWidget(allAgents)` method from the idle path.
224
+ Extract `updateStatusBar(state)` method from the status bar logic.
225
+ All existing tests pass (no behavior change, no export changes, `dispose` unchanged).
226
+ Commit: `refactor: decompose update into clearWidget and updateStatusBar`
227
+
228
+ 3. **Docs:** Update the complexity hotspots table in `docs/architecture/architecture.md`.
229
+ Remove both `renderWidgetLines` (done in #205) and `update` rows.
230
+ Commit: `docs: update complexity hotspots after widget decomposition`
231
+
232
+ ## Risks and Mitigations
233
+
234
+ | Risk | Mitigation |
235
+ | --------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
236
+ | `assembleWidgetState` has overlapping categorization with `categorizeAgents` in `widget-renderer.ts` | Different outputs for different consumers: counts+flags for lifecycle vs. full arrays for rendering. Not duplication. |
237
+ | No existing tests for `AgentWidget` — refactoring risks are higher for `clearWidget`/`updateStatusBar` | The pure function `assembleWidgetState` is tested directly. The method extractions are mechanical code moves with no semantic change — the type checker verifies structural integrity. |
238
+ | `clearWidget` guards redundant SDK calls (`if (widgetRegistered)`) — caller might expect unconditional teardown | Only `update`'s idle path calls `clearWidget`. `dispose` stays as-is with its own unconditional teardown semantics. |
239
+
240
+ ## Open Questions
241
+
242
+ None — the decomposition is a mechanical extraction of existing code into named functions and methods, following the pattern established by Phase 12 Steps 1 and 2.
@@ -0,0 +1,56 @@
1
+ ---
2
+ issue: 207
3
+ issue_title: "Decompose update in agent-widget.ts (cognitive 31)"
4
+ ---
5
+
6
+ # Retro: #207 — Decompose `update` in `agent-widget.ts`
7
+
8
+ ## Stage: Planning (2026-05-25T04:12:00Z)
9
+
10
+ ### Session summary
11
+
12
+ Planned the decomposition of `update` (cognitive complexity 31) into an exported pure `assembleWidgetState` function, a `clearWidget` method, and an `updateStatusBar` method.
13
+ The plan follows the Phase 12 pattern established by Steps 1 and 2 (#205, #206) — extract pure functions where possible, otherwise extract methods, and simplify the original function to a thin orchestrator.
14
+
15
+ ### Observations
16
+
17
+ - The sibling plans (#205, #206) provided a clear template for this plan — structure, section ordering, and test impact analysis all followed the established pattern.
18
+ - There are **no existing tests** for `AgentWidget` — the only testable concern is the newly extracted `assembleWidgetState` pure function.
19
+ The rest of the refactoring is a mechanical extraction verified by the type checker.
20
+ - `categorizeAgents` in `widget-renderer.ts` does a similar filter but returns full arrays (for rendering), while `assembleWidgetState` returns lightweight counts (for lifecycle decisions).
21
+ Different outputs for different consumers — no duplication concern.
22
+ - No `ask_user` was needed — the issue's "Proposed change" section was unambiguous and the design pattern was well-established by the two preceding Phase 12 steps.
23
+
24
+ ## Stage: Planning — revision (2026-05-25T16:00:00Z)
25
+
26
+ ### Session summary
27
+
28
+ Reviewed and revised the prior plan after a thorough code audit of `agent-widget.ts`, `widget-renderer.ts`, `agent-record.ts`, and `runtime.ts`.
29
+ Three design changes were made to the original plan.
30
+
31
+ ### Observations
32
+
33
+ - **Narrowed the input type:** Changed `assembleWidgetState` from accepting `WidgetAgent[]` (10+ fields) to a local `AgentSummary` interface (3 fields: `id`, `status`, `completedAt?`).
34
+ The original plan violated ISP — the function only reads 3 fields, so requiring full `WidgetAgent` fixtures in tests would be needless friction.
35
+ `AgentRecord` satisfies `AgentSummary` structurally, so no adapter is needed at the call site.
36
+ - **Kept `dispose` independent:** The original plan made `dispose` delegate to `clearWidget`, but `dispose` and `update`'s idle path have different lifecycle semantics — `dispose` uses unconditional teardown (correctness guarantee), while `update`'s idle path uses guarded calls (avoiding redundant SDK calls during repeated ticks).
37
+ `dispose` also skips stale-entry cleanup (the Map is about to be GC'd).
38
+ Per the code-design skill's Sandi Metz principle, this is structural duplication that should not be extracted.
39
+ - **Added complexity budget table:** Explicitly estimated cognitive complexity for each extracted function to verify the < 10 target is achievable across the board.
40
+
41
+ ## Stage: Implementation — TDD (2026-05-25T13:10:00Z)
42
+
43
+ ### Session summary
44
+
45
+ Completed all three TDD steps from the plan.
46
+ `assembleWidgetState` was extracted and tested with 16 unit tests covering all status combinations; `clearWidget` and `updateStatusBar` were extracted as private methods simplifying `update` to a thin orchestrator.
47
+ Test count went from 868 to 884 (+16 tests across 55 files, up from 54).
48
+
49
+ ### Observations
50
+
51
+ - No deviations from the plan.
52
+ The non-null assertion (`this.uiCtx!`) in `clearWidget` and `updateStatusBar` is safe because both methods are only called from `update` after the `if (!this.uiCtx) return` guard.
53
+ - The `AgentSummary` interface and narrow test fixtures worked exactly as planned — test objects are plain 3-field literals, no `createTestRecord` needed.
54
+ - The complexity hotspots table in `architecture.md` now has no rows (both `renderWidgetLines` from #205 and `update` from this issue are resolved).
55
+ The section note was updated to reflect that Phase 12 cleared all critical hotspots.
56
+ - `pnpm fallow dead-code` (from repo root) passed with no issues.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-subagents",
3
- "version": "7.2.8",
3
+ "version": "7.3.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/service.ts"
@@ -14,6 +14,42 @@ import { renderWidgetLines } from "#src/ui/widget-renderer";
14
14
 
15
15
  // ---- Types ----
16
16
 
17
+ /** Minimal agent shape needed for widget lifecycle decisions. */
18
+ interface AgentSummary {
19
+ readonly id: string;
20
+ readonly status: string;
21
+ readonly completedAt?: number;
22
+ }
23
+
24
+ /** Lightweight state snapshot used by AgentWidget.update() to decide what to show. */
25
+ export interface WidgetState {
26
+ readonly runningCount: number;
27
+ readonly queuedCount: number;
28
+ readonly hasFinished: boolean;
29
+ /** True when runningCount > 0 || queuedCount > 0. Included for call-site readability. */
30
+ readonly hasActive: boolean;
31
+ }
32
+
33
+ /**
34
+ * Count agents by status and return a lightweight state snapshot.
35
+ * Pure function — no IO, no side effects. Exported for direct unit testing.
36
+ */
37
+ export function assembleWidgetState(
38
+ agents: readonly AgentSummary[],
39
+ shouldShowFinished: (agentId: string, status: string) => boolean,
40
+ ): WidgetState {
41
+ let runningCount = 0;
42
+ let queuedCount = 0;
43
+ let hasFinished = false;
44
+ for (const a of agents) {
45
+ if (a.status === "running") { runningCount++; }
46
+ else if (a.status === "queued") { queuedCount++; }
47
+ else if (a.completedAt && shouldShowFinished(a.id, a.status)) { hasFinished = true; }
48
+ }
49
+ const hasActive = runningCount > 0 || queuedCount > 0;
50
+ return { runningCount, queuedCount, hasFinished, hasActive };
51
+ }
52
+
17
53
  export type UICtx = {
18
54
  setStatus(key: string, text: string | undefined): void;
19
55
  setWidget(
@@ -108,55 +144,59 @@ export class AgentWidget {
108
144
  });
109
145
  }
110
146
 
111
- /** Force an immediate widget update. */
112
- update() {
113
- if (!this.uiCtx) return;
114
- const allAgents = this.manager.listAgents();
115
-
116
- // Lightweight existence checks full categorization happens in renderWidget()
117
- let runningCount = 0;
118
- let queuedCount = 0;
119
- let hasFinished = false;
120
- for (const a of allAgents) {
121
- if (a.status === "running") { runningCount++; }
122
- else if (a.status === "queued") { queuedCount++; }
123
- else if (a.completedAt && this.shouldShowFinished(a.id, a.status)) { hasFinished = true; }
147
+ /**
148
+ * Unregister the widget, clear the status bar, stop the interval timer, and
149
+ * purge stale `finishedTurnAge` entries for agents no longer in `allAgents`.
150
+ * Called only from `update`'s idle path — not from `dispose`.
151
+ */
152
+ private clearWidget(allAgents: readonly AgentSummary[]): void {
153
+ if (this.widgetRegistered) {
154
+ this.uiCtx!.setWidget("agents", undefined);
155
+ this.widgetRegistered = false;
156
+ this.tui = undefined;
124
157
  }
125
- const hasActive = runningCount > 0 || queuedCount > 0;
126
-
127
- // Nothing to show — clear widget
128
- if (!hasActive && !hasFinished) {
129
- if (this.widgetRegistered) {
130
- this.uiCtx.setWidget("agents", undefined);
131
- this.widgetRegistered = false;
132
- this.tui = undefined;
133
- }
134
- if (this.lastStatusText !== undefined) {
135
- this.uiCtx.setStatus("subagents", undefined);
136
- this.lastStatusText = undefined;
137
- }
138
- if (this.widgetInterval) { clearInterval(this.widgetInterval); this.widgetInterval = undefined; }
139
- // Clean up stale entries
140
- for (const [id] of this.finishedTurnAge) {
141
- if (!allAgents.some(a => a.id === id)) this.finishedTurnAge.delete(id);
142
- }
143
- return;
158
+ if (this.lastStatusText !== undefined) {
159
+ this.uiCtx!.setStatus("subagents", undefined);
160
+ this.lastStatusText = undefined;
144
161
  }
162
+ if (this.widgetInterval) { clearInterval(this.widgetInterval); this.widgetInterval = undefined; }
163
+ for (const [id] of this.finishedTurnAge) {
164
+ if (!allAgents.some(a => a.id === id)) this.finishedTurnAge.delete(id);
165
+ }
166
+ }
145
167
 
146
- // Status bar — only call setStatus when the text actually changes
168
+ /**
169
+ * Compute the status bar text from the current widget state and call
170
+ * `setStatus` only when it differs from the last cached value.
171
+ */
172
+ private updateStatusBar(state: WidgetState): void {
147
173
  let newStatusText: string | undefined;
148
- if (hasActive) {
174
+ if (state.hasActive) {
149
175
  const statusParts: string[] = [];
150
- if (runningCount > 0) statusParts.push(`${runningCount} running`);
151
- if (queuedCount > 0) statusParts.push(`${queuedCount} queued`);
152
- const total = runningCount + queuedCount;
176
+ if (state.runningCount > 0) statusParts.push(`${state.runningCount} running`);
177
+ if (state.queuedCount > 0) statusParts.push(`${state.queuedCount} queued`);
178
+ const total = state.runningCount + state.queuedCount;
153
179
  newStatusText = `${statusParts.join(", ")} agent${total === 1 ? "" : "s"}`;
154
180
  }
155
181
  if (newStatusText !== this.lastStatusText) {
156
- this.uiCtx.setStatus("subagents", newStatusText);
182
+ this.uiCtx!.setStatus("subagents", newStatusText);
157
183
  this.lastStatusText = newStatusText;
158
184
  }
185
+ }
186
+
187
+ /** Force an immediate widget update. */
188
+ update() {
189
+ if (!this.uiCtx) return;
190
+
191
+ const allAgents = this.manager.listAgents();
192
+ const state = assembleWidgetState(allAgents, (id, status) => this.shouldShowFinished(id, status));
193
+
194
+ if (!state.hasActive && !state.hasFinished) {
195
+ this.clearWidget(allAgents);
196
+ return;
197
+ }
159
198
 
199
+ this.updateStatusBar(state);
160
200
  this.widgetFrame++;
161
201
 
162
202
  // Register widget callback once; subsequent updates use requestRender()