@gotgenes/pi-subagents 6.9.1 → 6.9.2

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,15 @@ 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
+ ## [6.9.2](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.9.1...pi-subagents-v6.9.2) (2026-05-22)
9
+
10
+
11
+ ### Documentation
12
+
13
+ * mark E2 type housekeeping done in architecture ([#116](https://github.com/gotgenes/pi-packages/issues/116)) ([89f18a8](https://github.com/gotgenes/pi-packages/commit/89f18a82fac39b4a62be8e440355b859afaa6d2f))
14
+ * plan type housekeeping and small structural cleanups ([#116](https://github.com/gotgenes/pi-packages/issues/116)) ([e1cbd26](https://github.com/gotgenes/pi-packages/commit/e1cbd269961a1bff3b11e2e916733a79c39a087d))
15
+ * **retro:** add retro notes for issue [#115](https://github.com/gotgenes/pi-packages/issues/115) ([05b8809](https://github.com/gotgenes/pi-packages/commit/05b88093f7b70ea886b6c7cb5f0cc96161a95df6))
16
+
8
17
  ## [6.9.1](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.9.0...pi-subagents-v6.9.1) (2026-05-22)
9
18
 
10
19
 
@@ -384,13 +384,13 @@ Each step is sequenced so it makes the next step easier.
384
384
  | Smell | Location | Evidence |
385
385
  | ------------------------------ | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
386
386
  | ~~Global mutable state~~ | ~~`agent-types.ts`~~ | **Fixed #108**: `AgentTypeRegistry` class; `reloadCustomAgents` callback removed from `AgentToolDeps` and `AgentMenuDeps` |
387
- | Closure bag as class | `createNotificationSystem()` | Returns 4 functions sharing closure state (`pendingNudges`, timers) |
387
+ | ~~Closure bag as class~~ | ~~`createNotificationSystem()`~~ | **Fixed #116**: `NotificationManager` class; `pendingNudges` and timer state are private fields |
388
388
  | ~~Mutable state bag~~ | ~~`AgentActivity` (7 fields)~~ | **Fixed #110**: `AgentActivityTracker` class; `ui-observer.ts` calls transition methods; widget, notification, agent-tool use read-only accessors |
389
389
  | ~~Settings relay~~ | ~~`AgentMenuDeps` (13 fields)~~ | **Fixed #109**: `SettingsManager` class; 6 callback fields collapsed to `settings: SettingsManager`; `AgentMenuDeps` now 8 fields |
390
390
  | ~~Post-construction mutation~~ | ~~`AgentRecord` non-transition state~~ | **Fixed #111**: `ExecutionState`, `WorktreeState`, `NotificationState` collaborators; `pendingSteers` moved to `AgentManager`; stats encapsulated behind mutation methods |
391
391
  | ~~Fire-and-forget callbacks~~ | ~~`AgentManagerOptions`~~ | **Fixed #112**: `AgentManagerObserver` interface; `observer` replaces 3 callbacks; `index.ts` constructs one observer object instead of 3 closure lambdas |
392
392
  | ~~Duplicate `SpawnOptions`~~ | ~~`service.ts` + `agent-manager.ts`~~ | **Fixed #113**: internal type renamed to `AgentSpawnConfig`; public `SpawnOptions` in `service.ts` unchanged |
393
- | Type dumping ground | `types.ts` | `NotificationDetails` used only by notification/renderer; ~~`DEFAULT_AGENT_NAMES` moved to `AgentTypeRegistry` (#108)~~; `AgentConfig` (21 fields) consumers use 2–4 each |
393
+ | ~~Type dumping ground~~ | ~~`types.ts`~~ | **Fixed #116**: `NotificationDetails` `notification.ts`; `ParentSnapshot` `parent-snapshot.ts`; `EnvInfo` `env.ts`; `AgentIdentity` and `AgentPromptConfig` narrow subsets |
394
394
  | ~~Wide dependency bags~~ | ~~`AgentToolDeps` (9), `AgentMenuDeps` (8)~~ | **Fixed #114**: `AgentToolDeps` 9 → 6; `AgentMenuDeps` 8 → 7; `emitEvent` removed from both; description text derived from registry; `agentActivity` narrowed to typed interfaces |
395
395
 
396
396
  ### Step A: Extract state into classes (foundation, parallel)
@@ -487,29 +487,28 @@ Fixed two upstream API gaps before extracting: `onSessionCreated` now receives `
487
487
  Extracted `foreground-runner.ts` (~175 lines) and `background-spawner.ts` (~116 lines).
488
488
  `agent-tool.ts` reduced from 579 → 411 lines (orchestrator + rendering).
489
489
 
490
- #### E2. Type housekeeping (#116)
490
+ #### ~~E2. Type housekeeping (#116)~~ — **Done**
491
491
 
492
- - Move `NotificationDetails` from `types.ts` to `notification.ts`.
493
- - Move `DEFAULT_AGENT_NAMES` from `types.ts` to the registry.
494
- - Move `ParentSnapshot` from `types.ts` to `parent-snapshot.ts`.
495
- - Move `EnvInfo` from `types.ts` to `env.ts`.
496
- - Convert `createNotificationSystem` closure to `NotificationManager` class.
497
- - Convert `ConversationViewer` constructor from 6 positional parameters to an options bag.
498
- - Define narrow `AgentConfig` subset interfaces for consumers that use 2–4 fields of the 21-field type.
492
+ - Moved `NotificationDetails` from `types.ts` to `notification.ts`.
493
+ - Moved `ParentSnapshot` from `types.ts` to `parent-snapshot.ts` (`DEFAULT_AGENT_NAMES` was already moved in #108).
494
+ - Moved `EnvInfo` from `types.ts` to `env.ts`.
495
+ - Converted `createNotificationSystem` closure to `NotificationManager` class.
496
+ - Converted `ConversationViewer` constructor from 7 positional parameters to `ConversationViewerOptions` bag.
497
+ - Defined `AgentIdentity` and `AgentPromptConfig` narrow subsets; `AgentConfig extends` both; `buildAgentPrompt` narrowed to `AgentPromptConfig`.
499
498
 
500
499
  ### Expected impact
501
500
 
502
- | Metric | Before | After |
503
- | ------------------------------------------ | -------------------------------------------------------------------------------- | ------- |
504
- | Module-scoped mutable state | ~~1 (`agent-types.ts` Map)~~ | **0** ✓ |
505
- | Closure-bag "classes" | ~~2~~ 1 (`createNotificationSystem`; settings free functions **fixed #109**) | 0 |
506
- | Externally-mutated state bags | ~~2~~ ~~1~~ **0** (`AgentRecord` **fixed #111**; `AgentActivity` **fixed #110**) | 0 ✓ |
507
- | `AgentManagerOptions` fields | 8 | 5 |
508
- | `AgentToolDeps` fields | ~~9~~ **6** (−3 text fields derived; −1 emitEvent → observer; activity narrowed) | 6 ✓ |
509
- | `AgentMenuDeps` fields | ~~13~~ **7** (−6 settings #109; −1 registry #108; −1 dead emitEvent #114) | 7 ✓ |
510
- | `SpawnOptions` callback fields | 1 (`onSessionCreated`) | 0 |
511
- | Callbacks threaded through deps | ~~8~~ 0 remaining settings callbacks (**fixed #109**); `emitEvent` ×3 remain | 0 |
512
- | Types in `types.ts` without a natural home | 4 | 0 |
501
+ | Metric | Before | After |
502
+ | ------------------------------------------ | -------------------------------------------------------------------------------------- | ------- |
503
+ | Module-scoped mutable state | ~~1 (`agent-types.ts` Map)~~ | **0** ✓ |
504
+ | Closure-bag "classes" | ~~2~~ ~~1~~ **0** (`createNotificationSystem` **fixed #116**; settings **fixed #109**) | 0|
505
+ | Externally-mutated state bags | ~~2~~ ~~1~~ **0** (`AgentRecord` **fixed #111**; `AgentActivity` **fixed #110**) | 0 ✓ |
506
+ | `AgentManagerOptions` fields | 8 | 5 |
507
+ | `AgentToolDeps` fields | ~~9~~ **6** (−3 text fields derived; −1 emitEvent → observer; activity narrowed) | 6 ✓ |
508
+ | `AgentMenuDeps` fields | ~~13~~ **7** (−6 settings #109; −1 registry #108; −1 dead emitEvent #114) | 7 ✓ |
509
+ | `SpawnOptions` callback fields | 1 (`onSessionCreated`) | 0 |
510
+ | Callbacks threaded through deps | ~~8~~ 0 remaining settings callbacks (**fixed #109**); `emitEvent` ×3 remain | 0 |
511
+ | Types in `types.ts` without a natural home | ~~4~~ **0** ✓ (all moved #116) | 0|
513
512
 
514
513
  ### Dependency graph
515
514
 
@@ -0,0 +1,351 @@
1
+ ---
2
+ issue: 116
3
+ issue_title: "refactor(pi-subagents): type housekeeping and small structural cleanups"
4
+ ---
5
+
6
+ # Type housekeeping and small structural cleanups
7
+
8
+ ## Problem Statement
9
+
10
+ `types.ts` is a type dumping ground — it collects types that don't have a natural home because the module that should own them didn't exist yet.
11
+ With foundation extractions (#108 registry, `parent-snapshot.ts`, `env.ts`, `notification.ts`) now in place, most of these types have a natural home.
12
+ Several other small housekeeping items share the same "polish" character: a closure-bag factory that is a class in disguise, a positional constructor that creates test friction, and a 22-field `AgentConfig` interface consumed by modules that touch 2–4 of its fields.
13
+
14
+ ## Goals
15
+
16
+ - Move `NotificationDetails` to `notification.ts`, `ParentSnapshot` to `parent-snapshot.ts`, and `EnvInfo` to `env.ts`.
17
+ - Convert `createNotificationSystem` closure to a `NotificationManager` class.
18
+ - Convert `ConversationViewer` constructor from 7 positional parameters to an options bag.
19
+ - Define narrow `AgentConfig` subset interfaces for consumers that use only a few fields.
20
+ - Leave `types.ts` containing only genuinely cross-cutting types: `SubagentType`, `MemoryScope`, `IsolationMode`, `AgentConfig`, `AgentInvocation`, `ShellExec`, and re-exports (`AgentRecord`, `ThinkingLevel`).
21
+
22
+ ## Non-Goals
23
+
24
+ - Refactoring `AgentConfig` itself (field additions, removals, renames).
25
+ - Changing the `NotificationDeps` interface shape.
26
+ - Modifying `agent-menu.ts` to use narrow subsets — it legitimately reads most `AgentConfig` fields as a config editor/viewer.
27
+ - Touching `invocation-config.ts` — it already receives `AgentConfig | undefined` and only reads invocation-default fields; narrowing its parameter is a separate concern.
28
+
29
+ ## Background
30
+
31
+ ### Prerequisite status
32
+
33
+ | Issue | Title | Status |
34
+ | ----- | --------------------------------- | ------- |
35
+ | #108 | Extract `AgentTypeRegistry` class | ✅ Done |
36
+
37
+ `DEFAULT_AGENT_NAMES` was already moved to `AgentTypeRegistry.DEFAULT_AGENT_NAMES` as part of #108.
38
+ The remaining work from the issue's checklist is type relocations, two structural conversions, and narrow subset interfaces.
39
+
40
+ ### Current `types.ts` exports
41
+
42
+ | Export | Kind | Should stay? |
43
+ | -------------------------------------------------- | --------- | ------------------------------- |
44
+ | `AgentRecord` (re-export) | class | ✅ Cross-cutting |
45
+ | `AgentRecordInit`, `AgentRecordStatus` (re-export) | type | ✅ Cross-cutting |
46
+ | `ThinkingLevel` (re-export) | type | ✅ Cross-cutting |
47
+ | `SubagentType` | type | ✅ Cross-cutting |
48
+ | `MemoryScope` | type | ✅ Cross-cutting |
49
+ | `IsolationMode` | type | ✅ Cross-cutting |
50
+ | `AgentConfig` | interface | ✅ Cross-cutting |
51
+ | `AgentInvocation` | interface | ✅ Cross-cutting |
52
+ | `ShellExec` | type | ✅ Cross-cutting |
53
+ | `NotificationDetails` | interface | ❌ Move to `notification.ts` |
54
+ | `ParentSnapshot` | interface | ❌ Move to `parent-snapshot.ts` |
55
+ | `EnvInfo` | interface | ❌ Move to `env.ts` |
56
+
57
+ ### `createNotificationSystem` closure analysis
58
+
59
+ The factory in `notification.ts` shares `pendingNudges` (a `Map`) and timer state across 4 inner functions (`cancelNudge`, `scheduleNudge`, `sendCompletion`, `cleanupCompleted`, `dispose`).
60
+ This is a class in disguise — mutable state + methods that read/write it.
61
+ Converting to a `NotificationManager` class makes the state explicit and lets tests use instance methods directly.
62
+
63
+ ### `ConversationViewer` constructor
64
+
65
+ The constructor takes 7 positional parameters:
66
+
67
+ ```typescript
68
+ constructor(tui, session, record, activity, theme, done, registry)
69
+ ```
70
+
71
+ Every test must reconstruct all 7 in order.
72
+ An options bag reduces friction and is resilient to new parameters.
73
+
74
+ ### `AgentConfig` field usage by consumer
75
+
76
+ | Consumer | Fields used |
77
+ | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
78
+ | `agent-widget.ts` (`getDisplayName`, `getPromptModeLabel`) | `name`, `displayName`, `promptMode` |
79
+ | `tools/helpers.ts` (`formatAgentList`) | `name`, `description`, `model` |
80
+ | `prompts.ts` (`buildAgentPrompt`) | `name`, `promptMode`, `systemPrompt` |
81
+ | `session-config.ts` (`assembleSessionConfig`) | `name`, `model`, `thinking`, `maxTurns`, `extensions`, `skills`, `memory`, `disallowedTools` |
82
+ | `agent-menu.ts` | Nearly all fields (config viewer/editor) |
83
+
84
+ Natural clusters for narrow interfaces:
85
+
86
+ - **`AgentIdentity`**: `name`, `displayName`, `description`, `promptMode` — UI display and agent listing.
87
+ - **`AgentPromptConfig`**: `name`, `promptMode`, `systemPrompt` — prompt assembly.
88
+
89
+ `session-config.ts` uses 8 fields; narrowing it yields limited value vs. complexity.
90
+ `invocation-config.ts` receives `AgentConfig | undefined` and reads invocation-default fields; this is a separate concern.
91
+
92
+ ## Design Overview
93
+
94
+ ### Type relocations
95
+
96
+ Move each type to its natural home module and re-export from `types.ts` during an interim step.
97
+ After updating all importers to point at the new home, remove the re-export.
98
+ This two-phase approach (move + re-export, then update importers + remove re-export) avoids a big-bang commit.
99
+
100
+ However, since importers are few (2–4 each) and the types are only used internally, a single step per type is simpler: move the type, update all importers in the same commit.
101
+
102
+ ### `NotificationManager` class
103
+
104
+ Replace the `createNotificationSystem` factory with a class that owns its state:
105
+
106
+ ```typescript
107
+ export class NotificationManager {
108
+ private pendingNudges = new Map<string, ReturnType<typeof setTimeout>>();
109
+
110
+ constructor(private deps: NotificationDeps) {}
111
+
112
+ cancelNudge(key: string): void { /* ... */ }
113
+ sendCompletion(record: AgentRecord): void { /* ... */ }
114
+ cleanupCompleted(id: string): void { /* ... */ }
115
+ dispose(): void { /* ... */ }
116
+
117
+ // private helpers
118
+ private scheduleNudge(key: string, send: () => void, delay?: number): void { /* ... */ }
119
+ private emitIndividualNudge(record: AgentRecord): void { /* ... */ }
120
+ }
121
+ ```
122
+
123
+ The `NotificationSystem` interface stays as-is — `NotificationManager implements NotificationSystem`.
124
+ The `NotificationDeps` interface is unchanged.
125
+ Callers in `index.ts` switch from `createNotificationSystem(deps)` to `new NotificationManager(deps)`.
126
+
127
+ Consumer call site (index.ts):
128
+
129
+ ```typescript
130
+ const notifications = new NotificationManager({
131
+ sendMessage: (msg, opts) => pi.sendMessage(msg, opts),
132
+ agentActivity: runtime.agentActivity,
133
+ markFinished: (id) => runtime.markFinished(id),
134
+ updateWidget: () => runtime.updateWidget(),
135
+ });
136
+ ```
137
+
138
+ ### `ConversationViewer` options bag
139
+
140
+ Replace positional parameters with a named options interface:
141
+
142
+ ```typescript
143
+ export interface ConversationViewerOptions {
144
+ tui: TUI;
145
+ session: AgentSession;
146
+ record: AgentRecord;
147
+ activity: AgentActivityTracker | undefined;
148
+ theme: Theme;
149
+ done: (result: undefined) => void;
150
+ registry: AgentConfigLookup;
151
+ }
152
+
153
+ export class ConversationViewer {
154
+ constructor(private options: ConversationViewerOptions) {
155
+ // destructure into private fields or use options.field access
156
+ }
157
+ }
158
+ ```
159
+
160
+ The existing private fields (`tui`, `session`, `record`, etc.) become reads from `this.options.*` or are destructured in the constructor to named private fields.
161
+ The constructor body (session subscription) is unchanged.
162
+
163
+ ### Narrow `AgentConfig` subset interfaces
164
+
165
+ Define two narrow interfaces in `types.ts` and make `AgentConfig` extend them:
166
+
167
+ ```typescript
168
+ /** UI display and agent listing — name, display name, description, prompt mode. */
169
+ export interface AgentIdentity {
170
+ name: string;
171
+ displayName?: string;
172
+ description: string;
173
+ promptMode: "replace" | "append";
174
+ }
175
+
176
+ /** Prompt assembly — name, prompt mode, system prompt. */
177
+ export interface AgentPromptConfig {
178
+ name: string;
179
+ promptMode: "replace" | "append";
180
+ systemPrompt: string;
181
+ }
182
+
183
+ export interface AgentConfig extends AgentIdentity, AgentPromptConfig {
184
+ // remaining fields unchanged
185
+ }
186
+ ```
187
+
188
+ Then update consumers to accept the narrow interface:
189
+
190
+ - `agent-widget.ts`: `getDisplayName(type, registry)` and `getPromptModeLabel(type, registry)` — these go through `AgentConfigLookup.resolveAgentConfig()`, which returns `AgentConfig`.
191
+ The narrowing applies to the *return type usage*, not the function signature.
192
+ No change needed here — the function already destructures only `displayName`, `name`, `promptMode`.
193
+ - `tools/helpers.ts`: `formatAgentList` reads `description`, `model` — these are not in `AgentIdentity`.
194
+ `model` is session-config territory.
195
+ Leave as-is.
196
+ - `prompts.ts`: `buildAgentPrompt(config: AgentPromptConfig, ...)` — narrow the parameter type.
197
+ - `AgentConfigLookup.resolveAgentConfig()` return type stays `AgentConfig` — the registry always has the full config.
198
+
199
+ Since `AgentConfig extends AgentIdentity, AgentPromptConfig`, any code passing an `AgentConfig` to a function expecting the narrow type works without casts.
200
+
201
+ ## Module-Level Changes
202
+
203
+ ### Modified files
204
+
205
+ 1. **`src/types.ts`**
206
+ - Remove `NotificationDetails`, `ParentSnapshot`, `EnvInfo` interface definitions.
207
+ - Add `AgentIdentity` and `AgentPromptConfig` interfaces.
208
+ - Make `AgentConfig` extend `AgentIdentity` and `AgentPromptConfig`.
209
+ - Remove fields from `AgentConfig` body that are now inherited (`name`, `displayName`, `description`, `promptMode`, `systemPrompt`).
210
+
211
+ 2. **`src/notification.ts`**
212
+ - Add `NotificationDetails` interface (moved from `types.ts`).
213
+ - Replace `createNotificationSystem` factory with `NotificationManager` class implementing `NotificationSystem`.
214
+ - Export `NotificationManager` (named export) and keep `NotificationSystem` interface export.
215
+ - Remove `createNotificationSystem` export.
216
+
217
+ 3. **`src/parent-snapshot.ts`**
218
+ - Add `ParentSnapshot` interface (moved from `types.ts`).
219
+
220
+ 4. **`src/env.ts`**
221
+ - Add `EnvInfo` interface (moved from `types.ts`).
222
+
223
+ 5. **`src/prompts.ts`**
224
+ - Change `config` parameter type from `AgentConfig` to `AgentPromptConfig`.
225
+ - Update import to use `AgentPromptConfig` from `./types.js`.
226
+
227
+ 6. **`src/index.ts`**
228
+ - Update `NotificationDetails` import from `./types.js` to `./notification.js`.
229
+ - Replace `createNotificationSystem(deps)` with `new NotificationManager(deps)`.
230
+ - Update import: `NotificationManager` from `./notification.js`.
231
+
232
+ 7. **`src/renderer.ts`**
233
+ - Update `NotificationDetails` import from `./types.js` to `./notification.js`.
234
+
235
+ 8. **`src/agent-runner.ts`**
236
+ - Update `ParentSnapshot` import from `./types.js` to `./parent-snapshot.js`.
237
+
238
+ 9. **`src/agent-manager.ts`**
239
+ - Update `ParentSnapshot` import from `./types.js` to `./parent-snapshot.js`.
240
+
241
+ 10. **`src/session-config.ts`**
242
+ - Update `EnvInfo` import from `./types.js` to `./env.js`.
243
+
244
+ 11. **`src/ui/conversation-viewer.ts`**
245
+ - Define `ConversationViewerOptions` interface.
246
+ - Replace 7 positional constructor parameters with single `options: ConversationViewerOptions` parameter.
247
+ - Assign private fields from `options` in constructor body.
248
+
249
+ ### Unchanged files
250
+
251
+ - `src/agent-types.ts` — `DEFAULT_AGENT_NAMES` already moved in #108.
252
+ - `src/invocation-config.ts` — narrowing is a separate concern.
253
+ - `src/ui/agent-menu.ts` — legitimately uses most `AgentConfig` fields.
254
+
255
+ ### Test files
256
+
257
+ 12. **`test/notification.test.ts`**
258
+ - Replace `createNotificationSystem(deps)` calls with `new NotificationManager(deps)`.
259
+ - Update import.
260
+ - All existing assertions stay — the public API is identical.
261
+
262
+ 13. **`test/conversation-viewer.test.ts`**
263
+ - Replace positional `new ConversationViewer(tui, session, record, activity, theme, done, registry)` calls with `new ConversationViewer({ tui, session, record, activity, theme, done, registry })`.
264
+ - All existing assertions stay.
265
+
266
+ 14. **`test/prompts.test.ts`**
267
+ - Verify existing test fixtures satisfy `AgentPromptConfig` (they should — tests already pass `name`, `promptMode`, `systemPrompt`).
268
+ - May need to narrow mock type annotations.
269
+
270
+ 15. **Other test files importing relocated types** (`test/parent-snapshot.test.ts`, `test/renderer.test.ts`)
271
+ - Update import paths if they import directly from `types.ts` (most import from the source module already).
272
+
273
+ ## Test Impact Analysis
274
+
275
+ 1. **New unit tests enabled:**
276
+ - `NotificationManager` as a class can be tested with standard `new` + method calls — no factory indirection.
277
+ However, the existing factory tests are already clean and simply switch to `new NotificationManager(deps)`.
278
+ No fundamentally new test capabilities.
279
+
280
+ 2. **Existing tests that simplify:**
281
+ - `conversation-viewer.test.ts` — the options bag makes test construction more readable and resilient to parameter additions.
282
+ Each test only needs to specify the fields it cares about (with a helper providing defaults).
283
+ - `prompts.test.ts` — narrower `AgentPromptConfig` parameter means test fixtures can omit 19 irrelevant `AgentConfig` fields.
284
+
285
+ 3. **Existing tests that must stay as-is:**
286
+ - `notification.test.ts` — all factory tests stay, just switch constructor syntax.
287
+ - `conversation-viewer.test.ts` — all render and input tests stay, just switch constructor syntax.
288
+ - `parent-snapshot.test.ts`, `env.test.ts`, `renderer.test.ts` — behavior unchanged.
289
+
290
+ ## TDD Order
291
+
292
+ 1. **Move `NotificationDetails` from `types.ts` to `notification.ts`.**
293
+ Cut the interface from `types.ts`, paste into `notification.ts`.
294
+ Update importers: `index.ts`, `renderer.ts` (change import from `./types.js` to `./notification.js`).
295
+ `notification.ts` already imports from `./types.js` for `AgentRecord` — no circular dependency.
296
+ Run `pnpm run check`.
297
+ Commit: `refactor: move NotificationDetails to notification.ts`
298
+
299
+ 2. **Move `ParentSnapshot` from `types.ts` to `parent-snapshot.ts`.**
300
+ Cut the interface from `types.ts`, paste into `parent-snapshot.ts`.
301
+ Update importers: `agent-manager.ts`, `agent-runner.ts` (change import from `./types.js` to `./parent-snapshot.js`).
302
+ Run `pnpm run check`.
303
+ Commit: `refactor: move ParentSnapshot to parent-snapshot.ts`
304
+
305
+ 3. **Move `EnvInfo` from `types.ts` to `env.ts`.**
306
+ Cut the interface from `types.ts`, paste into `env.ts`.
307
+ Update importers: `session-config.ts`, `prompts.ts` (change import from `./types.js` to `./env.js`).
308
+ `env.ts` already imports `ShellExec` from `./types.js` — no circular dependency.
309
+ Run `pnpm run check`.
310
+ Commit: `refactor: move EnvInfo to env.ts`
311
+
312
+ 4. **Convert `createNotificationSystem` to `NotificationManager` class.**
313
+ Replace the factory with a class implementing `NotificationSystem`.
314
+ Move `pendingNudges` and timer logic to private instance state.
315
+ Convert inner functions to methods.
316
+ Keep `NotificationSystem` interface, `NotificationDeps` interface, and `NUDGE_HOLD_MS` constant unchanged.
317
+ Remove `createNotificationSystem` export, add `NotificationManager` export.
318
+ Update `index.ts`: `new NotificationManager(deps)` instead of `createNotificationSystem(deps)`.
319
+ Update `test/notification.test.ts`: `new NotificationManager(deps)` instead of `createNotificationSystem(deps)`, update import.
320
+ Run `pnpm vitest run test/notification.test.ts`.
321
+ Commit: `refactor: convert createNotificationSystem to NotificationManager class`
322
+
323
+ 5. **Convert `ConversationViewer` constructor to options bag.**
324
+ Define `ConversationViewerOptions` interface.
325
+ Replace 7 positional parameters with `options: ConversationViewerOptions`.
326
+ Assign private fields from options in constructor body.
327
+ Update all call sites in `test/conversation-viewer.test.ts` and `src/index.ts` (or wherever `new ConversationViewer(...)` is called).
328
+ Run `pnpm vitest run test/conversation-viewer.test.ts`.
329
+ Commit: `refactor: convert ConversationViewer to options bag constructor`
330
+
331
+ 6. **Define narrow `AgentIdentity` and `AgentPromptConfig` interfaces.**
332
+ Add interfaces to `types.ts`.
333
+ Make `AgentConfig` extend both; remove inherited fields from `AgentConfig` body.
334
+ Narrow `prompts.ts` `buildAgentPrompt` parameter from `AgentConfig` to `AgentPromptConfig`.
335
+ Update `test/prompts.test.ts` type annotations if needed.
336
+ Run `pnpm run check` and `pnpm vitest run`.
337
+ Commit: `refactor: define AgentIdentity and AgentPromptConfig subset interfaces`
338
+
339
+ ## Risks and Mitigations
340
+
341
+ | Risk | Mitigation |
342
+ | --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
343
+ | Moving types breaks import paths in files not caught by grep | Run `pnpm run check` (full `tsc --noEmit`) after each type relocation step. |
344
+ | `NotificationManager` class changes test mock patterns (e.g., spreading instances) | Tests already use the factory return as an opaque object — switching to `new` is mechanical. No spread patterns in notification tests. |
345
+ | `ConversationViewer` options bag breaks all test call sites at once | All 314 lines of tests use the same 7-arg pattern. A search-and-replace handles it. The options bag is a single-step change. |
346
+ | `AgentConfig extends AgentIdentity, AgentPromptConfig` changes structural compatibility | `extends` is purely additive — existing code passing `AgentConfig` anywhere still works. Narrowed consumers gain type safety, nothing breaks. |
347
+ | `prompts.ts` parameter narrowing breaks callers passing `AgentConfig` | `AgentConfig extends AgentPromptConfig`, so all existing call sites are compatible without changes. |
348
+
349
+ ## Open Questions
350
+
351
+ - Whether to define an `AgentSessionConfig` subset for `session-config.ts` (8 fields) — deferred because the narrowing ratio (8 of 22) yields less clarity benefit than `AgentIdentity` (4 of 22) or `AgentPromptConfig` (3 of 22).
@@ -0,0 +1,51 @@
1
+ ---
2
+ issue: 115
3
+ issue_title: "refactor(pi-subagents): decompose agent-tool.ts into foreground/background modules"
4
+ ---
5
+
6
+ # Retro: #115 — decompose agent-tool.ts into foreground/background modules
7
+
8
+ ## Final Retrospective (2026-05-21T22:30:00-04:00)
9
+
10
+ ### Session summary
11
+
12
+ Decomposed the 579-line `tools/agent-tool.ts` into focused modules: `foreground-runner.ts` (spinner, streaming, cleanup) and `background-spawner.ts` (activity setup, notification wiring), with `agent-tool.ts` remaining as the orchestrator (411 lines).
13
+ Before extracting, fixed two upstream API gaps: widened `onSessionCreated` callback to `(session, record)` to eliminate a `listAgents()` reverse-search, and added `toolCallId` to `AgentSpawnConfig` so the manager wires `NotificationState` at spawn time.
14
+ Test count increased from 641 to 690.
15
+ Released as `pi-subagents-v6.9.1`.
16
+
17
+ ### Observations
18
+
19
+ #### What went well
20
+
21
+ - The revised plan (after user feedback) was structurally clean — fixing API gaps first made the extraction trivial; each extracted module received only what it needed without workarounds.
22
+ - TDD execution was smooth: 6 steps, all green after each commit, only minor deviations (type annotation issue, `index.ts` call-site fix).
23
+
24
+ #### What caused friction (agent side)
25
+
26
+ - `wrong-abstraction` (user-caught) — The initial plan was a mechanical code-move that defined 4 new interfaces (`ForegroundRunDeps`, `BackgroundSpawnDeps`, `ForegroundRunParams`, `BackgroundSpawnParams`) to paper over two upstream API gaps: a `listAgents()` reverse-search in the foreground `onSessionCreated` callback, and a post-spawn `record.notification` mutation in the background path.
27
+ The user asked *"What dependencies are still missing for these split tools, that they want, rather than some low level state or collaborators that they have?"*
28
+ — redirecting me to fix the API surface before extracting.
29
+ Impact: entire plan rewritten (~15 minutes), but the revised plan was significantly cleaner and the implementation went smoothly.
30
+
31
+ - `missing-context` (self-identified) — During step 5 (foreground extraction), two tests in `foreground-runner.test.ts` failed because mock sessions were `{}` objects lacking a `subscribe` method required by `subscribeUIObserver`.
32
+ The function's dependency on `SubscribableSession` (requiring `.subscribe()`) wasn't accounted for in the test mock.
33
+ Impact: one test fix cycle (~2 minutes), no rework.
34
+
35
+ - `missing-context` (self-identified) — Annotating `runForeground` with an explicit `Promise<AgentToolResult<any>>` return type widened the content array type from `{ type: "text", text: string }[]` to `(TextContent | ImageContent)[]`, breaking existing `content[0].text` patterns in tests.
36
+ Fixed by removing the explicit annotation and letting TypeScript infer the narrow type.
37
+ Impact: three edit cycles to diagnose and fix (~5 minutes).
38
+
39
+ - `missing-context` (self-identified) — Step 1 removed `listAgents` from `AgentToolManager` but didn't update the construction site in `index.ts`.
40
+ `pnpm run check` caught it in step 4.
41
+ Impact: one-line fix in the same commit, no rework.
42
+
43
+ #### What caused friction (user side)
44
+
45
+ - The initial plan's size and mechanical nature required a user redirect.
46
+ The `/plan-issue` prompt's Design Overview section asks to sketch consumer call sites for *new collaborators*, but the same Tell-Don't-Ask check should apply to *extracted* modules' interactions with their upstream dependencies.
47
+ A prompt tweak could help the agent catch this pattern earlier.
48
+
49
+ ### Changes made
50
+
51
+ 1. `.pi/prompts/plan-issue.md` — added extraction-specific Tell-Don't-Ask verification step to the Design Overview section: sketch the extracted module's upstream interactions before planning the extraction, fix API gaps first.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-subagents",
3
- "version": "6.9.1",
3
+ "version": "6.9.2",
4
4
  "exports": {
5
5
  ".": "./src/service.ts"
6
6
  },
@@ -14,10 +14,11 @@ import type { AgentRunner } from "./agent-runner.js";
14
14
  import { AgentTypeRegistry } from "./agent-types.js";
15
15
  import { debugLog } from "./debug.js";
16
16
  import { NotificationState } from "./notification-state.js";
17
+ import type { ParentSnapshot } from "./parent-snapshot.js";
17
18
  import { buildParentSnapshot } from "./parent-snapshot.js";
18
19
  import { subscribeRecordObserver } from "./record-observer.js";
19
20
  import type { RunConfig } from "./runtime.js";
20
- import type { AgentInvocation, IsolationMode, ParentSnapshot, ShellExec, SubagentType, ThinkingLevel } from "./types.js";
21
+ import type { AgentInvocation, IsolationMode, ShellExec, SubagentType, ThinkingLevel } from "./types.js";
21
22
  import type { WorktreeManager } from "./worktree.js";
22
23
  import { WorktreeState } from "./worktree-state.js";
23
24
 
@@ -15,9 +15,10 @@ import {
15
15
  import type { AgentConfigLookup } from "./agent-types.js";
16
16
  import { extractText } from "./context.js";
17
17
  import { detectEnv } from "./env.js";
18
+ import type { ParentSnapshot } from "./parent-snapshot.js";
18
19
  import { assembleSessionConfig } from "./session-config.js";
19
20
  import { deriveSubagentSessionDir } from "./session-dir.js";
20
- import type { ParentSnapshot, ShellExec, SubagentType, ThinkingLevel } from "./types.js";
21
+ import type { ShellExec, SubagentType, ThinkingLevel } from "./types.js";
21
22
 
22
23
  /** Names of tools registered by this extension that subagents must NOT inherit. */
23
24
  const EXCLUDED_TOOL_NAMES = ["Agent", "get_subagent_result", "steer_subagent"];
package/src/env.ts CHANGED
@@ -3,7 +3,13 @@
3
3
  */
4
4
 
5
5
  import { debugLog } from "./debug.js";
6
- import type { EnvInfo, ShellExec } from "./types.js";
6
+ import type { ShellExec } from "./types.js";
7
+
8
+ export interface EnvInfo {
9
+ isGitRepo: boolean;
10
+ branch: string;
11
+ platform: string;
12
+ }
7
13
 
8
14
  export async function detectEnv(exec: ShellExec, cwd: string): Promise<EnvInfo> {
9
15
  let isGitRepo = false;
package/src/index.ts CHANGED
@@ -18,7 +18,7 @@ import { AgentTypeRegistry } from "./agent-types.js";
18
18
  import { loadCustomAgents } from "./custom-agents.js";
19
19
  import { SessionLifecycleHandler, ToolStartHandler } from "./handlers/index.js";
20
20
  import { type ModelRegistry, resolveModel } from "./model-resolver.js";
21
- import { buildEventData, createNotificationSystem } from "./notification.js";
21
+ import { buildEventData, type NotificationDetails, NotificationManager } from "./notification.js";
22
22
  import { createNotificationRenderer } from "./renderer.js";
23
23
  import { createSubagentRuntime } from "./runtime.js";
24
24
  import { publishSubagentsService, unpublishSubagentsService } from "./service.js";
@@ -28,7 +28,6 @@ import { createAgentTool } from "./tools/agent-tool.js";
28
28
  import { createGetResultTool } from "./tools/get-result-tool.js";
29
29
  import { getModelLabelFromConfig } from "./tools/helpers.js";
30
30
  import { createSteerTool } from "./tools/steer-tool.js";
31
- import { type NotificationDetails } from "./types.js";
32
31
  import { createAgentsMenuHandler } from "./ui/agent-menu.js";
33
32
  import {
34
33
  AgentWidget,
@@ -48,7 +47,7 @@ export default function (pi: ExtensionAPI) {
48
47
  // ---- Notification system ----
49
48
  // runtime.widget is assigned after AgentManager construction; arrow closures
50
49
  // capture `runtime` by reference so they always read the current value.
51
- const notifications = createNotificationSystem({
50
+ const notifications = new NotificationManager({
52
51
  sendMessage: (msg, opts) => pi.sendMessage(msg, opts),
53
52
  agentActivity: runtime.agentActivity,
54
53
  markFinished: (id) => runtime.markFinished(id),
@@ -1,8 +1,23 @@
1
1
  import { debugLog } from "./debug.js";
2
- import type { AgentRecord, NotificationDetails } from "./types.js";
2
+ import type { AgentRecord } from "./types.js";
3
3
  import type { AgentActivityTracker } from "./ui/agent-activity-tracker.js";
4
4
  import { getLifetimeTotal, getSessionContextPercent } from "./usage.js";
5
5
 
6
+ /** Details attached to custom notification messages for visual rendering. */
7
+ export interface NotificationDetails {
8
+ id: string;
9
+ description: string;
10
+ status: string;
11
+ toolUses: number;
12
+ turnCount: number;
13
+ maxTurns?: number;
14
+ totalTokens: number;
15
+ durationMs: number;
16
+ outputFile?: string;
17
+ error?: string;
18
+ resultPreview: string;
19
+ }
20
+
6
21
  // ---- Pure helpers (exported for unit testing) ----
7
22
 
8
23
  /** Escape XML special characters to prevent injection in structured notifications. */
@@ -129,23 +144,43 @@ export interface NotificationSystem {
129
144
 
130
145
  const NUDGE_HOLD_MS = 200;
131
146
 
132
- export function createNotificationSystem(deps: NotificationDeps): NotificationSystem {
133
- const pendingNudges = new Map<string, ReturnType<typeof setTimeout>>();
147
+ export class NotificationManager implements NotificationSystem {
148
+ private pendingNudges = new Map<string, ReturnType<typeof setTimeout>>();
134
149
 
135
- function cancelNudge(key: string) {
136
- const timer = pendingNudges.get(key);
150
+ constructor(private deps: NotificationDeps) {}
151
+
152
+ cancelNudge(key: string): void {
153
+ const timer = this.pendingNudges.get(key);
137
154
  if (timer != null) {
138
155
  clearTimeout(timer);
139
- pendingNudges.delete(key);
156
+ this.pendingNudges.delete(key);
140
157
  }
141
158
  }
142
159
 
143
- function scheduleNudge(key: string, send: () => void, delay = NUDGE_HOLD_MS) {
144
- cancelNudge(key);
145
- pendingNudges.set(
160
+ sendCompletion(record: AgentRecord): void {
161
+ this.deps.agentActivity.delete(record.id);
162
+ this.deps.markFinished(record.id);
163
+ this.scheduleNudge(record.id, () => this.emitIndividualNudge(record));
164
+ this.deps.updateWidget();
165
+ }
166
+
167
+ cleanupCompleted(id: string): void {
168
+ this.deps.agentActivity.delete(id);
169
+ this.deps.markFinished(id);
170
+ this.deps.updateWidget();
171
+ }
172
+
173
+ dispose(): void {
174
+ for (const timer of this.pendingNudges.values()) clearTimeout(timer);
175
+ this.pendingNudges.clear();
176
+ }
177
+
178
+ private scheduleNudge(key: string, send: () => void, delay = NUDGE_HOLD_MS): void {
179
+ this.cancelNudge(key);
180
+ this.pendingNudges.set(
146
181
  key,
147
182
  setTimeout(() => {
148
- pendingNudges.delete(key);
183
+ this.pendingNudges.delete(key);
149
184
  try {
150
185
  send();
151
186
  } catch (err) {
@@ -155,41 +190,21 @@ export function createNotificationSystem(deps: NotificationDeps): NotificationSy
155
190
  );
156
191
  }
157
192
 
158
- function emitIndividualNudge(record: AgentRecord) {
193
+ private emitIndividualNudge(record: AgentRecord): void {
159
194
  if (record.notification?.resultConsumed) return;
160
195
 
161
196
  const notification = formatTaskNotification(record, 500);
162
197
  const outputFile = record.execution?.outputFile;
163
198
  const footer = outputFile ? `\nFull transcript available at: ${outputFile}` : "";
164
199
 
165
- deps.sendMessage(
200
+ this.deps.sendMessage(
166
201
  {
167
202
  customType: "subagent-notification",
168
203
  content: notification + footer,
169
204
  display: true,
170
- details: buildNotificationDetails(record, 500, deps.agentActivity.get(record.id)),
205
+ details: buildNotificationDetails(record, 500, this.deps.agentActivity.get(record.id)),
171
206
  },
172
207
  { deliverAs: "followUp", triggerTurn: true },
173
208
  );
174
209
  }
175
-
176
- function sendCompletion(record: AgentRecord) {
177
- deps.agentActivity.delete(record.id);
178
- deps.markFinished(record.id);
179
- scheduleNudge(record.id, () => emitIndividualNudge(record));
180
- deps.updateWidget();
181
- }
182
-
183
- function cleanupCompleted(id: string) {
184
- deps.agentActivity.delete(id);
185
- deps.markFinished(id);
186
- deps.updateWidget();
187
- }
188
-
189
- function dispose() {
190
- for (const timer of pendingNudges.values()) clearTimeout(timer);
191
- pendingNudges.clear();
192
- }
193
-
194
- return { cancelNudge, sendCompletion, cleanupCompleted, dispose };
195
210
  }
@@ -4,7 +4,26 @@
4
4
 
5
5
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
6
6
  import { buildParentContext } from "./context.js";
7
- import type { ParentSnapshot } from "./types.js";
7
+
8
+ /**
9
+ * Plain data snapshot of the parent session state captured at spawn time.
10
+ * Replaces live `ExtensionContext` references so queued agents don't read stale state.
11
+ */
12
+ export interface ParentSnapshot {
13
+ /** Parent working directory. */
14
+ cwd: string;
15
+ /** Parent's effective system prompt (for append-mode agents). */
16
+ systemPrompt: string;
17
+ /** Parent's current model instance (fallback when agent config has no model). */
18
+ model: unknown;
19
+ /** Model registry for resolving config.model strings and creating sessions. */
20
+ modelRegistry: {
21
+ find(provider: string, modelId: string): unknown;
22
+ getAvailable?(): Array<{ provider: string; id: string }>;
23
+ };
24
+ /** Pre-built parent conversation text (when inheritContext was requested). */
25
+ parentContext?: string;
26
+ }
8
27
 
9
28
  /**
10
29
  * Build an immutable snapshot of the parent session state.
package/src/prompts.ts CHANGED
@@ -2,7 +2,8 @@
2
2
  * prompts.ts — System prompt builder for agents.
3
3
  */
4
4
 
5
- import type { AgentConfig, EnvInfo } from "./types.js";
5
+ import type { EnvInfo } from "./env.js";
6
+ import type { AgentPromptConfig } from "./types.js";
6
7
 
7
8
  /** Extra sections to inject into the system prompt (memory, skills, etc.). */
8
9
  export interface PromptExtras {
@@ -27,7 +28,7 @@ export interface PromptExtras {
27
28
  * @param extras Optional extra sections to inject (memory, preloaded skills).
28
29
  */
29
30
  export function buildAgentPrompt(
30
- config: AgentConfig,
31
+ config: AgentPromptConfig,
31
32
  cwd: string,
32
33
  env: EnvInfo,
33
34
  parentSystemPrompt?: string,
package/src/renderer.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Text } from "@earendil-works/pi-tui";
2
- import type { NotificationDetails } from "./types.js";
2
+ import type { NotificationDetails } from "./notification.js";
3
3
  import { formatMs, formatTokens, formatTurns } from "./ui/agent-widget.js";
4
4
 
5
5
  /** Narrow theme interface — only the methods the renderer actually calls. */
@@ -15,10 +15,11 @@ import {
15
15
  getMemoryToolNames,
16
16
  getReadOnlyMemoryToolNames,
17
17
  } from "./agent-types.js";
18
+ import type { EnvInfo } from "./env.js";
18
19
  import { buildMemoryBlock, buildReadOnlyMemoryBlock } from "./memory.js";
19
20
  import { buildAgentPrompt, type PromptExtras } from "./prompts.js";
20
21
  import { preloadSkills } from "./skill-loader.js";
21
- import type { EnvInfo, SubagentType, ThinkingLevel } from "./types.js";
22
+ import type { SubagentType, ThinkingLevel } from "./types.js";
22
23
 
23
24
  // ── Public interfaces ────────────────────────────────────────────────────────
24
25
 
package/src/types.ts CHANGED
@@ -18,11 +18,23 @@ export type MemoryScope = "user" | "project" | "local";
18
18
  /** Isolation mode for agent execution. */
19
19
  export type IsolationMode = "worktree";
20
20
 
21
- /** Unified agent configurationused for both default and user-defined agents. */
22
- export interface AgentConfig {
21
+ /** UI display and agent listingname, display name, description, prompt mode. */
22
+ export interface AgentIdentity {
23
23
  name: string;
24
24
  displayName?: string;
25
25
  description: string;
26
+ promptMode: "replace" | "append";
27
+ }
28
+
29
+ /** Prompt assembly — name, prompt mode, system prompt. */
30
+ export interface AgentPromptConfig {
31
+ name: string;
32
+ promptMode: "replace" | "append";
33
+ systemPrompt: string;
34
+ }
35
+
36
+ /** Unified agent configuration — used for both default and user-defined agents. */
37
+ export interface AgentConfig extends AgentIdentity, AgentPromptConfig {
26
38
  builtinToolNames?: string[];
27
39
  /** Tool denylist — these tools are removed even if `builtinToolNames` or extensions include them. */
28
40
  disallowedTools?: string[];
@@ -33,8 +45,6 @@ export interface AgentConfig {
33
45
  model?: string;
34
46
  thinking?: ThinkingLevel;
35
47
  maxTurns?: number;
36
- systemPrompt: string;
37
- promptMode: "replace" | "append";
38
48
  /** Default for spawn: fork parent conversation. undefined = caller decides. */
39
49
  inheritContext?: boolean;
40
50
  /** Default for spawn: run in background. undefined = caller decides. */
@@ -64,48 +74,6 @@ export interface AgentInvocation {
64
74
  isolation?: IsolationMode;
65
75
  }
66
76
 
67
- /** Details attached to custom notification messages for visual rendering. */
68
- export interface NotificationDetails {
69
- id: string;
70
- description: string;
71
- status: string;
72
- toolUses: number;
73
- turnCount: number;
74
- maxTurns?: number;
75
- totalTokens: number;
76
- durationMs: number;
77
- outputFile?: string;
78
- error?: string;
79
- resultPreview: string;
80
-
81
- }
82
-
83
- /**
84
- * Plain data snapshot of the parent session state captured at spawn time.
85
- * Replaces live `ExtensionContext` references so queued agents don't read stale state.
86
- */
87
- export interface ParentSnapshot {
88
- /** Parent working directory. */
89
- cwd: string;
90
- /** Parent's effective system prompt (for append-mode agents). */
91
- systemPrompt: string;
92
- /** Parent's current model instance (fallback when agent config has no model). */
93
- model: unknown;
94
- /** Model registry for resolving config.model strings and creating sessions. */
95
- modelRegistry: {
96
- find(provider: string, modelId: string): unknown;
97
- getAvailable?(): Array<{ provider: string; id: string }>;
98
- };
99
- /** Pre-built parent conversation text (when inheritContext was requested). */
100
- parentContext?: string;
101
- }
102
-
103
- export interface EnvInfo {
104
- isGitRepo: boolean;
105
- branch: string;
106
- platform: string;
107
- }
108
-
109
77
  /**
110
78
  * Narrow shell-exec callback replacing `ExtensionAPI` in `detectEnv()`.
111
79
  * Matches the shape of `pi.exec()` without carrying an SDK dependency.
@@ -220,7 +220,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
220
220
 
221
221
  await ctx.ui.custom<undefined>(
222
222
  (tui: any, theme: any, _keybindings: any, done: any) => {
223
- return new ConversationViewer(tui, session, record, activity, theme, done, deps.registry);
223
+ return new ConversationViewer({ tui, session, record, activity, theme, done, registry: deps.registry });
224
224
  },
225
225
  {
226
226
  overlay: true,
@@ -20,6 +20,16 @@ const MIN_VIEWPORT = 3;
20
20
  /** Height ceiling shared by the overlay's `maxHeight` and the viewer's internal viewport cap. */
21
21
  export const VIEWPORT_HEIGHT_PCT = 70;
22
22
 
23
+ export interface ConversationViewerOptions {
24
+ tui: TUI;
25
+ session: AgentSession;
26
+ record: AgentRecord;
27
+ activity: AgentActivityTracker | undefined;
28
+ theme: Theme;
29
+ done: (result: undefined) => void;
30
+ registry: AgentConfigLookup;
31
+ }
32
+
23
33
  export class ConversationViewer implements Component {
24
34
  private scrollOffset = 0;
25
35
  private autoScroll = true;
@@ -27,16 +37,23 @@ export class ConversationViewer implements Component {
27
37
  private lastInnerW = 0;
28
38
  private closed = false;
29
39
 
30
- constructor(
31
- private tui: TUI,
32
- private session: AgentSession,
33
- private record: AgentRecord,
34
- private activity: AgentActivityTracker | undefined,
35
- private theme: Theme,
36
- private done: (result: undefined) => void,
37
- private registry: AgentConfigLookup,
38
- ) {
39
- this.unsubscribe = session.subscribe(() => {
40
+ private tui: TUI;
41
+ private session: AgentSession;
42
+ private record: AgentRecord;
43
+ private activity: AgentActivityTracker | undefined;
44
+ private theme: Theme;
45
+ private done: (result: undefined) => void;
46
+ private registry: AgentConfigLookup;
47
+
48
+ constructor(options: ConversationViewerOptions) {
49
+ this.tui = options.tui;
50
+ this.session = options.session;
51
+ this.record = options.record;
52
+ this.activity = options.activity;
53
+ this.theme = options.theme;
54
+ this.done = options.done;
55
+ this.registry = options.registry;
56
+ this.unsubscribe = options.session.subscribe(() => {
40
57
  if (this.closed) return;
41
58
  this.tui.requestRender();
42
59
  });