@gotgenes/pi-subagents 6.16.1 → 6.16.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,16 @@ 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.16.2](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.16.1...pi-subagents-v6.16.2) (2026-05-23)
9
+
10
+
11
+ ### Documentation
12
+
13
+ * fix plan for narrow UI context ([#146](https://github.com/gotgenes/pi-packages/issues/146)) ([3d5b591](https://github.com/gotgenes/pi-packages/commit/3d5b591c0668ffcd9f66a6fc9a2c5d57236865af))
14
+ * mark Step N complete in architecture doc ([#146](https://github.com/gotgenes/pi-packages/issues/146)) ([9948869](https://github.com/gotgenes/pi-packages/commit/9948869b849817dac6f221f1e90db5e47fd12d31))
15
+ * plan narrow UI context for menu handlers ([#146](https://github.com/gotgenes/pi-packages/issues/146)) ([88318b4](https://github.com/gotgenes/pi-packages/commit/88318b4550fe95fc70b1a9ee1e904c608a2189a3))
16
+ * **retro:** add retro notes for issue [#148](https://github.com/gotgenes/pi-packages/issues/148) ([982fe51](https://github.com/gotgenes/pi-packages/commit/982fe51d7e27b7a127605710ced709bbd6291a0f))
17
+
8
18
  ## [6.16.1](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.16.0...pi-subagents-v6.16.1) (2026-05-23)
9
19
 
10
20
 
@@ -616,20 +616,20 @@ Phase 9 targets the next layer: observation model consolidation, `ExtensionConte
616
616
 
617
617
  ### Current smells
618
618
 
619
- | Smell | Location | Evidence | Severity |
620
- | ------------------------------------------------ | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
621
- | `execute` does config resolution for its callees | `agent-tool.ts` (145-line `execute`) | ~60 lines unpack config, resolve model, compute metadata, repack into 16-field bags for spawners; `ctx` threaded 4 layers deep | Medium |
622
- | Wide `ctx` in menu handlers | `agent-menu.ts`, `agent-config-editor.ts`, `agent-creation-wizard.ts` | Functions declare `ctx: ExtensionContext` but only call `ctx.ui.select/confirm/input/notify/editor`; 43 `ctx as any` casts across 3 test files | Medium |
623
- | Direct SDK import in `conversation-viewer.ts` | `conversation-viewer.test.ts` | Hoisted `vi.mock("@earendil-works/pi-tui")` to intercept `wrapTextWithAnsi` | Low |
624
- | ~~Widget mixes rendering, lifecycle, and state~~ | ~~`agent-widget.ts` (370 lines)~~ | Resolved by #148: rendering extracted to `widget-renderer.ts`; widget is now 198 lines | Done |
625
- | `deps.` prefix noise in function bodies | remaining modules across tools, UI, service-adapter | Functions accept a `deps` bag and access every field as `deps.foo`; hides real dependencies and lengthens every call line | Low |
619
+ | Smell | Location | Evidence | Severity |
620
+ | ------------------------------------------------ | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | -------- |
621
+ | `execute` does config resolution for its callees | `agent-tool.ts` (145-line `execute`) | ~60 lines unpack config, resolve model, compute metadata, repack into 16-field bags for spawners; `ctx` threaded 4 layers deep | Medium |
622
+ | ~~Wide `ctx` in menu handlers~~ | ~~`agent-menu.ts`, `agent-config-editor.ts`, `agent-creation-wizard.ts`~~ | Resolved by #146: `MenuUI` interface introduced; 42 `ctx as any` casts eliminated across 5 test files | Done |
623
+ | Direct SDK import in `conversation-viewer.ts` | `conversation-viewer.test.ts` | Hoisted `vi.mock("@earendil-works/pi-tui")` to intercept `wrapTextWithAnsi` | Low |
624
+ | ~~Widget mixes rendering, lifecycle, and state~~ | ~~`agent-widget.ts` (370 lines)~~ | Resolved by #148: rendering extracted to `widget-renderer.ts`; widget is now 198 lines | Done |
625
+ | `deps.` prefix noise in function bodies | remaining modules across tools, UI, service-adapter | Functions accept a `deps` bag and access every field as `deps.foo`; hides real dependencies and lengthens every call line | Low |
626
626
 
627
627
  ### Dependency bag convention
628
628
 
629
629
  Applied incrementally as each step touches a module:
630
630
 
631
- - **≤4 fields** accept as plain parameters; drop the interface.
632
- - **≥5 fields** keep a named interface but destructure in the function signature (`{ manager, widget }: ForegroundDeps`) so the function body uses bare names, not `deps.foo`.
631
+ - **≤4 fields** - accept as plain parameters; drop the interface.
632
+ - **≥5 fields** - keep a named interface but destructure in the function signature (`{ manager, widget }: ForegroundDeps`) so the function body uses bare names, not `deps.foo`.
633
633
 
634
634
  This eliminates the `deps.` prefix noise across ~124 callsites in 12 modules.
635
635
 
@@ -665,19 +665,23 @@ After this step, `ExtensionContext` appears only in:
665
665
 
666
666
  Impact: `execute` dropped from ~145 to ~25 lines; eliminated 16-field parameter bags; eliminated `vi.mock("../src/parent-snapshot.js")` in `agent-manager.test.ts`; foreground/background runner tests no longer need `ctx` mocks; `AgentManager` operates entirely on domain types.
667
667
 
668
- ### Step N: Narrow UI context for menu handlers (#146)
668
+ ### Step N: Narrow UI context for menu handlers (#146)
669
669
 
670
- Define a `MenuUI` interface with `select`, `confirm`, `input`, `notify`, and `editor` methods.
671
- Menu handler functions (`showAgentsMenu`, `showAgentDetail`, `showCreateWizard`, etc.) accept `MenuUI` instead of `ExtensionContext`.
672
- `index.ts` passes `ctx.ui` at the call site.
670
+ Defined `MenuUI` interface (exported from `agent-menu.ts`) with `select`, `confirm`, `input`, `notify`, `editor`, and `custom` methods — the exact subset `ctx.ui` methods used by menu handlers.
671
+ All inner functions in `agent-menu.ts`, `agent-config-editor.ts`, and `agent-creation-wizard.ts` now accept `(ui: MenuUI)` instead of `(ctx: ExtensionContext)`.
672
+ `index.ts` passes `ctx.ui`, `ctx.modelRegistry`, and `buildParentSnapshot(ctx)` to the handler.
673
673
 
674
- Creation wizard’s `spawnAndWait` call changes: the narrow `AgentMenuManager.spawnAndWait` accepts `ParentSnapshot` (enabled by Step M) instead of `ExtensionContext`.
674
+ `AgentMenuManager.spawnAndWait` and `WizardManager.spawnAndWait` both accept `ParentSnapshot` (enabled by Step M).
675
+ Creation wizard threads `parentSnapshot` from `showCreateWizard(ui, parentSnapshot)` → `showGenerateWizard(ui, parentSnapshot, targetDir)` → `manager.spawnAndWait(parentSnapshot, ...)`.
675
676
 
676
- Apply the dependency bag convention to touched modules: `AgentConfigEditorDeps` (4 fields), `SteerToolDeps` (4 fields), and `GetResultDeps` (4 fields) become plain parameters; `AgentMenuDeps` (8 fields) and `AgentCreationWizardDeps` (5 fields) are destructured in the signature.
677
+ Applied the dependency bag convention:
677
678
 
678
- After Steps M and N, `ExtensionContext` appears only at true boundaries: `index.ts` closures, `service-adapter.ts` (cross-extension bridge), and `index.ts` (extension entry point).
679
+ - `AgentConfigEditorDeps` (4 fields), `GetResultDeps` (4 fields), `SteerToolDeps` (4 fields) dissolved into plain parameters.
680
+ - `AgentMenuDeps` (8 fields) and `AgentCreationWizardDeps` (5 fields) kept as interfaces, destructured in the function signature.
679
681
 
680
- Impact: eliminates ~43 `ctx as any` casts across menu, editor, and wizard test files; tests construct a plain object satisfying `MenuUI` with no cast.
682
+ After Steps M and N, `ExtensionContext` appears only at true boundaries: `index.ts` closures and `service-adapter.ts` (cross-extension bridge).
683
+
684
+ Impact: eliminated 42 `ctx as any` casts across 5 test files (`agent-menu.test.ts`: 8, `agent-config-editor.test.ts`: 20, `agent-creation-wizard.test.ts`: 14); tests construct plain `MenuUI`-shaped objects with no cast.
681
685
 
682
686
  ### Step O: Inject text wrapping into ConversationViewer (#147)
683
687
 
@@ -693,7 +697,7 @@ Impact: eliminates the hoisted `vi.mock("@earendil-works/pi-tui")` in `conversat
693
697
 
694
698
  Extracted pure rendering functions (`renderWidgetLines`, `renderFinishedLine`, `renderRunningLines`) from `AgentWidget` into `ui/widget-renderer.ts`.
695
699
  The widget is now a thin lifecycle/polling wrapper (198 lines, down from 374) that delegates to pure render functions.
696
- Rendering functions receive data (agent list, activity map, registry) and return formatted strings testable without widget lifecycle. 23 new unit tests cover all status variants, overflow, tree connectors, and empty states.
700
+ Rendering functions receive data (agent list, activity map, registry) and return formatted strings - testable without widget lifecycle. 23 new unit tests cover all status variants, overflow, tree connectors, and empty states.
697
701
 
698
702
  ### Step dependencies
699
703
 
@@ -0,0 +1,319 @@
1
+ ---
2
+ issue: 146
3
+ issue_title: "Narrow UI context for menu handlers (Phase 9, Step N)"
4
+ ---
5
+
6
+ # Narrow UI context for menu handlers
7
+
8
+ ## Problem Statement
9
+
10
+ Menu handler functions (`showAgentsMenu`, `showAgentDetail`, `showCreateWizard`, etc.) declare `ctx: ExtensionContext` but only call `ctx.ui.select/confirm/input/notify/editor/custom` and `ctx.modelRegistry`.
11
+ This forces 42 `ctx as any` casts across 3 test files (`agent-menu.test.ts`: 8, `agent-config-editor.test.ts`: 20, `agent-creation-wizard.test.ts`: 14) because tests cannot construct a full `ExtensionContext`.
12
+
13
+ ## Goals
14
+
15
+ - Define a `MenuUI` interface with the subset of `ctx.ui` methods that menu handlers actually use (`select`, `confirm`, `input`, `notify`, `editor`, `custom`).
16
+ - Menu handler functions accept `MenuUI` (plus `modelRegistry` passed separately) instead of `ExtensionContext`.
17
+ - `index.ts` handler registration extracts `ctx.ui` and `ctx.modelRegistry` from the SDK `ExtensionContext`.
18
+ - Change `WizardManager.spawnAndWait` to accept `ParentSnapshot` (introduced by #145) instead of `ExtensionContext`.
19
+ - Apply dependency bag convention: dissolve ≤4-field deps into plain parameters; keep ≥5-field interfaces but destructure in signature.
20
+ - Eliminate all 42 `ctx as any` casts from menu, editor, and wizard test files.
21
+
22
+ ## Non-Goals
23
+
24
+ - Changing the behavior of `ctx.ui.custom` — pass-through only.
25
+ - Narrowing `ExtensionContext` usage in `index.ts` closures (the `as any` casts for `runtime.currentCtx?.ctx` are addressed separately).
26
+ - Injecting `modelRegistry` further (already a narrow interface from `model-resolver.ts`).
27
+
28
+ ## Background
29
+
30
+ ### Dependency: #145 (Step M) — Decompose execute
31
+
32
+ Issue #145 is **closed/implemented**.
33
+ `buildParentSnapshot(ctx)` converts `ExtensionContext` → `ParentSnapshot` at the call site.
34
+ This enables `WizardManager.spawnAndWait` to accept `ParentSnapshot` instead of `ExtensionContext`.
35
+
36
+ ### Existing modules
37
+
38
+ - `agent-menu.ts` (296 lines) — menu handler factory, 8-field `AgentMenuDeps`, all inner functions take `ctx: ExtensionContext`
39
+ - `agent-config-editor.ts` (202 lines) — `AgentConfigEditorDeps` (4 fields), `showAgentDetail` takes `ctx: ExtensionContext`
40
+ - `agent-creation-wizard.ts` (246 lines) — `AgentCreationWizardDeps` (5 fields), `WizardManager.spawnAndWait` takes `ctx: ExtensionContext`
41
+ - `tools/get-result-tool.ts` — `GetResultDeps` (4 fields)
42
+ - `tools/steer-tool.ts` — `SteerToolDeps` (4 fields)
43
+ - `index.ts` — wires everything, handler registration extracts `ctx.ui` and passes `ExtensionContext`
44
+
45
+ ### ExtensionContext usage in menu handlers
46
+
47
+ Every `ctx` reference in the three menu UI modules maps to exactly one of:
48
+
49
+ - `ctx.ui.select(...)` — 9 call sites
50
+ - `ctx.ui.confirm(...)` — 5 call sites
51
+ - `ctx.ui.input(...)` — 7 call sites
52
+ - `ctx.ui.notify(...)` — 15 call sites
53
+ - `ctx.ui.editor(...)` — 2 call sites
54
+ - `ctx.ui.custom(...)` — 1 call site (conversation viewer overlay)
55
+ - `ctx.modelRegistry` — 1 call site (model label resolution in `showAllAgentsList`)
56
+
57
+ No other `ExtensionContext` properties (session, tools, hooks, etc.) are accessed.
58
+
59
+ ## Design Overview
60
+
61
+ ### MenuUI interface
62
+
63
+ A narrow interface capturing only the `ctx.ui` methods used by menu handlers:
64
+
65
+ ```typescript
66
+ export interface MenuUI {
67
+ select(title: string, options: string[]): Promise<string | undefined>;
68
+ confirm(title: string, message: string): Promise<boolean>;
69
+ input(title: string, defaultValue?: string): Promise<string | undefined>;
70
+ notify(message: string, level: "info" | "warning" | "error"): void;
71
+ editor(title: string, content: string): Promise<string | undefined>;
72
+ custom<R>(component: any, options?: any): Promise<R>;
73
+ }
74
+ ```
75
+
76
+ `select` uses a plain `string` return (not a generic `<T extends string>`) to match the SDK's structural signature.
77
+
78
+ `modelRegistry` is not included in `MenuUI` — it is not a UI concern.
79
+ Instead, the handler registration in `index.ts` passes it separately.
80
+
81
+ ### Handler signature change
82
+
83
+ The menu handler currently receives `ExtensionContext` directly:
84
+
85
+ ```typescript
86
+ // index.ts — before
87
+ handler: async (_args, ctx) => { await agentsMenuHandler(ctx); },
88
+ ```
89
+
90
+ After this change, `index.ts` destructures what each handler needs:
91
+
92
+ ```typescript
93
+ // index.ts — after
94
+ handler: async (_args, ctx) => {
95
+ await agentsMenuHandler({
96
+ ui: ctx.ui,
97
+ modelRegistry: ctx.modelRegistry,
98
+ parentSnapshot: buildParentSnapshot(ctx),
99
+ });
100
+ },
101
+ ```
102
+
103
+ In `agent-menu.ts`, the return type changes from `(ctx: ExtensionContext) => Promise<void>` to a function that accepts `{ ui: MenuUI; modelRegistry: ModelRegistry; parentSnapshot: ParentSnapshot }`.
104
+ The `ExtensionContext` import is removed from `agent-menu.ts`, `agent-config-editor.ts`, and `agent-creation-wizard.ts`.
105
+
106
+ `modelRegistry` is threaded from the handler through `showAgentsMenu` → `showAllAgentsList` (the only consumer).
107
+ `parentSnapshot` is threaded from the handler through `showAgentsMenu` → `wizard.showCreateWizard` → `showGenerateWizard` (the only consumer).
108
+
109
+ ### Wizard spawnAndWait — drop ctx parameter
110
+
111
+ `WizardManager.spawnAndWait` currently takes `ctx: ExtensionContext` as its first parameter and passes it to `deps.manager.spawnAndWait(ctx, ...)`.
112
+ Once the menu handler no longer receives `ExtensionContext`, the wizard has no `ctx` to pass.
113
+
114
+ Thread `parentSnapshot` as a parameter from the handler through the wizard, keeping `AgentMenuManager.spawnAndWait` accepting `ParentSnapshot` as its first parameter (consistent with `AgentManager.spawnAndWait`).
115
+ The wizard's `showGenerateWizard` receives `parentSnapshot` and passes it to `deps.manager.spawnAndWait(parentSnapshot, ...)`.
116
+
117
+ ```typescript
118
+ // agent-creation-wizard.ts — after
119
+ async function showGenerateWizard(
120
+ ui: MenuUI,
121
+ parentSnapshot: ParentSnapshot,
122
+ targetDir: string,
123
+ ) {
124
+ // ...
125
+ const record = await deps.manager.spawnAndWait(
126
+ parentSnapshot, "general-purpose", generatePrompt, { ... },
127
+ );
128
+ }
129
+ ```
130
+
131
+ The creation wizard no longer imports `ExtensionContext`.
132
+
133
+ ### Dependency bag convention
134
+
135
+ Per `docs/architecture/architecture.md` § Dependency bag convention:
136
+
137
+ - **≤4 fields** → dissolve the interface, accept as plain parameters.
138
+ - **≥5 fields** → keep the interface but destructure in the function signature.
139
+
140
+ #### Dissolve (≤4 fields)
141
+
142
+ `AgentConfigEditorDeps` (4 fields: `fileOps`, `registry`, `personalAgentsDir`, `projectAgentsDir`) → plain parameters on `createAgentConfigEditor`.
143
+
144
+ `GetResultDeps` (4 fields: `getRecord`, `cancelNudge`, `getConversation`, `registry`) → plain parameters on `createGetResultTool`.
145
+
146
+ `SteerToolDeps` (4 fields: `getRecord`, `emitEvent`, `steerAgent`, `queueSteer`) → plain parameters on `createSteerTool`.
147
+
148
+ #### Keep + destructure (≥5 fields)
149
+
150
+ `AgentMenuDeps` (8 fields) — keep the interface, destructure in `createAgentsMenuHandler({ manager, registry, ... })`.
151
+
152
+ `AgentCreationWizardDeps` (5 fields) — keep the interface, destructure in `createAgentCreationWizard({ fileOps, manager, ... })`.
153
+
154
+ ### Consumer call-site sketch (menu handler registration)
155
+
156
+ ```typescript
157
+ // index.ts
158
+ pi.registerCommand('agents', {
159
+ description: 'Manage agents',
160
+ handler: async (_args, ctx) => {
161
+ await agentsMenuHandler({
162
+ ui: ctx.ui,
163
+ modelRegistry: ctx.modelRegistry,
164
+ parentSnapshot: buildParentSnapshot(ctx),
165
+ });
166
+ },
167
+ });
168
+ ```
169
+
170
+ ### Extracted module interaction sketch (agent-config-editor)
171
+
172
+ ```typescript
173
+ // agent-config-editor.ts — after dissolving deps
174
+ export function createAgentConfigEditor(
175
+ fileOps: AgentFileOps,
176
+ registry: AgentTypeRegistry,
177
+ personalAgentsDir: string,
178
+ projectAgentsDir: string,
179
+ ) {
180
+ // ... closures capture these directly; no deps.foo indirection
181
+ }
182
+ ```
183
+
184
+ No Tell-Don't-Ask violations — each parameter is a primitive or injectable collaborator.
185
+ No output-argument mutations — pure closure capture.
186
+
187
+ ## Module-Level Changes
188
+
189
+ ### New file: none
190
+
191
+ All changes are modifications to existing files.
192
+
193
+ ### Modified: `src/ui/agent-menu.ts`
194
+
195
+ - Add `MenuUI` interface export (the new narrow type).
196
+ - Import `ModelRegistry` from `model-resolver.js`.
197
+ - Remove `ExtensionContext` import.
198
+ - Change all inner function signatures from `(ctx: ExtensionContext)` to `(ui: MenuUI)`.
199
+ - Replace `ctx.ui.xxx(...)` → `ui.xxx(...)`.
200
+ - Replace `ctx.modelRegistry` → parameter `modelRegistry` threaded to `showAllAgentsList`.
201
+ - Change `AgentMenuDeps` usage: destructure in `createAgentsMenuHandler` signature.
202
+ - Change return type from `(ctx: ExtensionContext) => Promise<void>` to `(params: { ui: MenuUI; modelRegistry: ModelRegistry; parentSnapshot: ParentSnapshot }) => Promise<void>`.
203
+ - Thread `modelRegistry` from handler through `showAgentsMenu` → `showAllAgentsList`.
204
+ - Thread `parentSnapshot` from handler through `showAgentsMenu` → `wizard.showCreateWizard` → `showGenerateWizard`.
205
+ - Update `AgentMenuManager.spawnAndWait` to accept `ParentSnapshot` instead of `ExtensionContext`.
206
+ - Remove `Omit<AgentSpawnConfig, "isBackground">` in favor of plain inline type.
207
+
208
+ ### Modified: `src/ui/agent-config-editor.ts`
209
+
210
+ - Remove `ExtensionContext` import.
211
+ - Add `MenuUI` import from `agent-menu.js`.
212
+ - Change all inner function signatures from `(ctx: ExtensionContext)` to `(ui: MenuUI)`.
213
+ - Replace `ctx.ui.xxx(...)` → `ui.xxx(...)`.
214
+ - Dissolve `AgentConfigEditorDeps`: replace single deps parameter with 4 plain parameters.
215
+
216
+ ### Modified: `src/ui/agent-creation-wizard.ts`
217
+
218
+ - Remove `ExtensionContext` import.
219
+ - Add `MenuUI` import from `agent-menu.js`.
220
+ - Add `ParentSnapshot` import from `parent-snapshot.js`.
221
+ - Change all inner function signatures from `(ctx: ExtensionContext)` to `(ui: MenuUI)`.
222
+ - Replace `ctx.ui.xxx(...)` → `ui.xxx(...)`.
223
+ - Change `WizardManager.spawnAndWait` to accept `ParentSnapshot` instead of `ExtensionContext`.
224
+ - Thread `parentSnapshot` as a parameter from `showCreateWizard(ui, parentSnapshot)` → `showGenerateWizard(ui, parentSnapshot, targetDir)`.
225
+ - Destructure `AgentCreationWizardDeps` in signature.
226
+
227
+ ### Modified: `src/tools/get-result-tool.ts`
228
+
229
+ - Dissolve `GetResultDeps`: replace single deps parameter with 4 plain parameters.
230
+
231
+ ### Modified: `src/tools/steer-tool.ts`
232
+
233
+ - Dissolve `SteerToolDeps`: replace single deps parameter with 4 plain parameters.
234
+
235
+ ### Modified: `src/index.ts`
236
+
237
+ - Update `createAgentConfigEditor` call: pass 4 plain args instead of `AgentConfigEditorDeps`.
238
+ - Update `createAgentCreationWizard` call: pass 4 plain args instead of `AgentCreationWizardDeps` (registry is the `WizardRegistry`, not the full `AgentTypeRegistry` — pass `{ reload: () => registry.reload() }`).
239
+ - Update `createGetResultTool` call: pass 4 plain args instead of `GetResultDeps`.
240
+ - Update `createSteerTool` call: pass 4 plain args instead of `SteerToolDeps`.
241
+ - Update `spawnAndWait` in menu handler deps: keep `ParentSnapshot` as first parameter.
242
+ - Update `/agents` command handler to destructure `ctx.ui`, `ctx.modelRegistry`, and `buildParentSnapshot(ctx)`.
243
+
244
+ ### Modified: test files
245
+
246
+ - `test/ui/agent-menu.test.ts` — remove `ctx as any` casts; pass `{ ui: { ... }, modelRegistry: {}, parentSnapshot: {} }`.
247
+ - `test/ui/agent-config-editor.test.ts` — remove `ctx as any` casts; pass `MenuUI` objects directly.
248
+ - `test/ui/agent-creation-wizard.test.ts` — remove `ctx as any` casts; pass `MenuUI` and stub `ParentSnapshot`.
249
+ - `test/tools/get-result-tool.test.ts` — update `makeDeps` and `execute` helpers for dissolved parameters.
250
+ - `test/tools/steer-tool.test.ts` — update `makeDeps` and `execute` helpers for dissolved parameters.
251
+
252
+ ### Unchanged
253
+
254
+ - `src/ui/conversation-viewer.ts` — unrelated; uses its own deps.
255
+ - `src/ui/agent-widget.ts` — already narrow (no `ExtensionContext`).
256
+ - `src/agent-manager.ts` — already accepts `ParentSnapshot` from #145.
257
+ - `src/parent-snapshot.ts` — unchanged.
258
+
259
+ ## Test Impact Analysis
260
+
261
+ 1. **New unit tests enabled:** None — this is a signature change, not an extraction.
262
+ The existing test coverage already exercises menu navigation, editing, creation, and tool operations.
263
+
264
+ 2. **Existing tests that simplify:** All 42 `ctx as any` casts are removed from the three test files.
265
+ `makeCtx()` returns a plain `MenuUI`-shaped object (already structurally compatible).
266
+ The `makeCtx` helper in `agent-menu.test.ts` already returns the right shape — it just needs the cast removed and the handler-call interface updated.
267
+
268
+ 3. **Tests that must stay:** All existing test assertions stay — only the method of constructing the handler input changes.
269
+ `get-result-tool.test.ts` and `steer-tool.test.ts` may need minor updates if the deps dissolve changes the factory call signature, but no assertion changes.
270
+
271
+ ## TDD Order
272
+
273
+ Each step must leave `pnpm run check` green.
274
+ When a step changes a factory signature, it must also update the corresponding `index.ts` call site in the same commit.
275
+
276
+ 1. **Refactor:** Define and export `MenuUI` interface in `agent-menu.ts`.
277
+ No other changes — just add the interface alongside the existing code.
278
+ Commit: `refactor: add MenuUI interface (#146)`
279
+
280
+ 2. **Refactor:** Update `agent-config-editor.ts` — dissolve `AgentConfigEditorDeps` into 4 plain parameters; change `showAgentDetail(ctx)` to `showAgentDetail(ui: MenuUI)`; replace `ctx.ui.xxx` → `ui.xxx`.
281
+ Update `agent-config-editor.test.ts` — remove `ctx as any` casts, pass `MenuUI` objects directly.
282
+ Update `index.ts` — update `createAgentConfigEditor` call to pass 4 plain args.
283
+ Commit: `refactor: dissolve AgentConfigEditorDeps and narrow to MenuUI (#146)`
284
+
285
+ 3. **Refactor:** Update `agent-creation-wizard.ts` — destructure `AgentCreationWizardDeps`; change `showCreateWizard(ctx)` to `showCreateWizard(ui: MenuUI, parentSnapshot: ParentSnapshot)`; thread `parentSnapshot` to `showGenerateWizard`; change `WizardManager.spawnAndWait` to accept `ParentSnapshot`; replace `ctx.ui.xxx` → `ui.xxx`.
286
+ Update `agent-creation-wizard.test.ts` — remove `ctx as any` casts, pass `MenuUI` and stub `ParentSnapshot`.
287
+ Update `index.ts` — update `createAgentCreationWizard` call for destructured params.
288
+ Commit: `refactor: narrow creation wizard to MenuUI and ParentSnapshot (#146)`
289
+
290
+ 4. **Refactor:** Update `agent-menu.ts` — destructure `AgentMenuDeps`; change handler return type to accept `{ ui: MenuUI; modelRegistry: ModelRegistry; parentSnapshot: ParentSnapshot }`; thread `modelRegistry` to `showAllAgentsList`; thread `parentSnapshot` to `wizard.showCreateWizard`; update `AgentMenuManager.spawnAndWait` to accept `ParentSnapshot`; replace `ctx.ui.xxx` → `ui.xxx`.
291
+ Update `agent-menu.test.ts` — remove `ctx as any` casts, pass `{ ui, modelRegistry, parentSnapshot }`.
292
+ Update `index.ts` — update `/agents` handler to destructure `ctx.ui`, `ctx.modelRegistry`, and `buildParentSnapshot(ctx)`.
293
+ Commit: `refactor: narrow agent menu to MenuUI interface (#146)`
294
+
295
+ 5. **Refactor:** Update `get-result-tool.ts` — dissolve `GetResultDeps` into 4 plain parameters.
296
+ Update `test/tools/get-result-tool.test.ts` — update `makeDeps` and `execute` helpers.
297
+ Update `index.ts` — update `createGetResultTool` call to pass 4 plain args.
298
+ Commit: `refactor: dissolve GetResultDeps into plain parameters (#146)`
299
+
300
+ 6. **Refactor:** Update `steer-tool.ts` — dissolve `SteerToolDeps` into 4 plain parameters.
301
+ Update `test/tools/steer-tool.test.ts` — update `makeDeps` and `execute` helpers.
302
+ Update `index.ts` — update `createSteerTool` call to pass 4 plain args.
303
+ Commit: `refactor: dissolve SteerToolDeps into plain parameters (#146)`
304
+
305
+ 7. **Verify:** Run full test suite (`pnpm vitest run`) and type check (`pnpm run check`).
306
+ Confirm zero `ctx as any` in the three menu test files.
307
+ Commit: none (verification only).
308
+
309
+ ## Risks and Mitigations
310
+
311
+ | Risk | Mitigation |
312
+ | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
313
+ | `ctx.ui.custom` signature mismatch between `MenuUI` and real `ExtensionContext.ui` | `MenuUI.custom` uses `any` for the component and options parameters since these are opaque TUI types internal to the SDK. This matches the existing usage where `ctx.ui.custom<undefined>(...)` passes a TUI component constructor. |
314
+ | `ParentSnapshot` threading through menu → wizard call chain | The handler receives `parentSnapshot` from `index.ts` and threads it through `showAgentsMenu` → `showCreateWizard` → `showGenerateWizard`. Only `showGenerateWizard` uses it; the other functions relay it. This is acceptable since the parameter follows the existing `targetDir` threading pattern already in the wizard. |
315
+ | Deps dissolution breaks `index.ts` type check mid-sequence | Each TDD step updates the factory, its test file, AND the `index.ts` call site together, keeping `pnpm run check` green after every commit. |
316
+
317
+ ## Open Questions
318
+
319
+ - None — the design follows the architecture doc's Step N specification and the dependency (#145) is already implemented.
@@ -0,0 +1,39 @@
1
+ ---
2
+ issue: 148
3
+ issue_title: "Split AgentWidget rendering from lifecycle (Phase 9, Step P)"
4
+ ---
5
+
6
+ # Retro: #148 — Split AgentWidget rendering from lifecycle
7
+
8
+ ## Final Retrospective (2026-05-23T06:20:00Z)
9
+
10
+ ### Session summary
11
+
12
+ Extracted pure rendering functions (`renderWidgetLines`, `renderFinishedLine`, `renderRunningLines`) from `AgentWidget` into `ui/widget-renderer.ts`.
13
+ The widget shrank from 374 to 198 lines — now a thin lifecycle wrapper. 23 new unit tests cover all status variants, overflow, tree connectors, and empty states.
14
+ Released as `pi-subagents-v6.16.1`.
15
+
16
+ ### Observations
17
+
18
+ #### What went well
19
+
20
+ - TDD cycles were fast and clean: 9 commits, all tests green on first or second try, zero test regressions across the full 806-test suite.
21
+ - The `WidgetAgent` / `WidgetActivity` interfaces worked well as structural subsets — `AgentRecord` and `AgentActivityTracker` satisfy them without mapping code.
22
+ - The stub `Theme` pattern from `test/renderer.test.ts` (`fg: (c, t) => \`[\${c}:\${t}]\``) transferred cleanly to the new test file, keeping assertions readable.
23
+
24
+ #### What caused friction (agent side)
25
+
26
+ - `missing-context` — The plan's `renderWidgetLines` API spec omitted `theme` from its parameters, even though the heading, tree connectors, and per-agent render calls all require it.
27
+ Caught immediately at step 4 (first `renderWidgetLines` test) and fixed by adding `theme` to the params.
28
+ Impact: deviation note in commit body; no rework.
29
+
30
+ - `missing-context` — Step 3 (`renderRunningLines` implementation) initially missed importing `SPINNER` from `display.ts`.
31
+ The test caught it as a runtime `ReferenceError`, fixed in the same Red→Green cycle.
32
+ Impact: added friction but no rework.
33
+
34
+ - `wrong-abstraction` — Step 8 ("Extract into `widget-renderer.ts`") was a no-op because the module was created incrementally during steps 1–7 (tests must import from the new module to run).
35
+ Impact: step skipped; noted in summary.
36
+
37
+ #### What caused friction (user side)
38
+
39
+ - None observed — the session ran autonomously from plan through ship with no user corrections needed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-subagents",
3
- "version": "6.16.1",
3
+ "version": "6.16.2",
4
4
  "exports": {
5
5
  ".": "./src/service.ts"
6
6
  },
package/src/index.ts CHANGED
@@ -227,21 +227,21 @@ export default function (pi: ExtensionAPI) {
227
227
 
228
228
  // ---- get_subagent_result tool ----
229
229
 
230
- pi.registerTool(defineTool(createGetResultTool({
231
- getRecord: (id) => manager.getRecord(id),
232
- cancelNudge: (key) => notifications.cancelNudge(key),
233
- getConversation: (session) => getAgentConversation(session),
230
+ pi.registerTool(defineTool(createGetResultTool(
231
+ (id) => manager.getRecord(id),
232
+ (key) => notifications.cancelNudge(key),
233
+ (session) => getAgentConversation(session),
234
234
  registry,
235
- })));
235
+ )));
236
236
 
237
237
  // ---- steer_subagent tool ----
238
238
 
239
- pi.registerTool(defineTool(createSteerTool({
240
- getRecord: (id) => manager.getRecord(id),
241
- emitEvent: (name, data) => pi.events.emit(name, data),
242
- steerAgent: (session, message) => steerAgent(session, message),
243
- queueSteer: (id, message) => manager.queueSteer(id, message),
244
- })));
239
+ pi.registerTool(defineTool(createSteerTool(
240
+ (id) => manager.getRecord(id),
241
+ (name, data) => pi.events.emit(name, data),
242
+ (session, message) => steerAgent(session, message),
243
+ (id, message) => manager.queueSteer(id, message),
244
+ )));
245
245
 
246
246
  // ---- /agents interactive menu ----
247
247
 
@@ -249,7 +249,7 @@ export default function (pi: ExtensionAPI) {
249
249
  manager: {
250
250
  listAgents: () => manager.listAgents(),
251
251
  getRecord: (id) => manager.getRecord(id),
252
- spawnAndWait: (ctx, type, prompt, opts) => manager.spawnAndWait(buildParentSnapshot(ctx), type, prompt, opts),
252
+ spawnAndWait: (snapshot, type, prompt, opts) => manager.spawnAndWait(snapshot, type, prompt, opts),
253
253
  },
254
254
  registry,
255
255
  agentActivity: runtime.agentActivity,
@@ -270,6 +270,12 @@ export default function (pi: ExtensionAPI) {
270
270
 
271
271
  pi.registerCommand('agents', {
272
272
  description: 'Manage agents',
273
- handler: async (_args, ctx) => { await agentsMenuHandler(ctx); },
273
+ handler: async (_args, ctx) => {
274
+ await agentsMenuHandler({
275
+ ui: ctx.ui,
276
+ modelRegistry: ctx.modelRegistry,
277
+ parentSnapshot: buildParentSnapshot(ctx),
278
+ });
279
+ },
274
280
  });
275
281
  }
@@ -6,16 +6,13 @@ import { formatDuration, getDisplayName } from "../ui/display.js";
6
6
  import { getSessionContextPercent } from "../usage.js";
7
7
  import { formatLifetimeTokens, textResult } from "./helpers.js";
8
8
 
9
- /** Narrow deps — only the methods this tool's execute callback calls. */
10
- export interface GetResultDeps {
11
- getRecord: (id: string) => AgentRecord | undefined;
12
- cancelNudge: (key: string) => void;
13
- getConversation: (session: AgentSession) => string | undefined;
14
- registry: AgentConfigLookup;
15
- }
16
-
17
9
  /** Create the get_subagent_result tool definition (without Pi SDK wrapper). */
18
- export function createGetResultTool(deps: GetResultDeps) {
10
+ export function createGetResultTool(
11
+ getRecord: (id: string) => AgentRecord | undefined,
12
+ cancelNudge: (key: string) => void,
13
+ getConversation: (session: AgentSession) => string | undefined,
14
+ registry: AgentConfigLookup,
15
+ ) {
19
16
  return {
20
17
  name: "get_subagent_result" as const,
21
18
  label: "Get Agent Result",
@@ -45,7 +42,7 @@ export function createGetResultTool(deps: GetResultDeps) {
45
42
  _onUpdate: unknown,
46
43
  _ctx: unknown,
47
44
  ) => {
48
- const record = deps.getRecord(params.agent_id);
45
+ const record = getRecord(params.agent_id);
49
46
  if (!record) {
50
47
  return textResult(`Agent not found: "${params.agent_id}". It may have been cleaned up.`);
51
48
  }
@@ -58,11 +55,11 @@ export function createGetResultTool(deps: GetResultDeps) {
58
55
  // Pre-mark consumed BEFORE awaiting — onComplete fires inside .then() and
59
56
  // always runs before this await resumes. Prevents a redundant notification.
60
57
  record.notification?.markConsumed();
61
- deps.cancelNudge(params.agent_id);
58
+ cancelNudge(params.agent_id);
62
59
  await record.promise;
63
60
  }
64
61
 
65
- const displayName = getDisplayName(record.type, deps.registry);
62
+ const displayName = getDisplayName(record.type, registry);
66
63
  const duration = formatDuration(record.startedAt, record.completedAt);
67
64
  const tokens = formatLifetimeTokens(record);
68
65
  const contextPercent = getSessionContextPercent(record.session);
@@ -88,12 +85,12 @@ export function createGetResultTool(deps: GetResultDeps) {
88
85
  // Mark result as consumed — suppresses the completion notification
89
86
  if (record.status !== "running" && record.status !== "queued") {
90
87
  record.notification?.markConsumed();
91
- deps.cancelNudge(params.agent_id);
88
+ cancelNudge(params.agent_id);
92
89
  }
93
90
 
94
91
  // Verbose: include full conversation
95
92
  if (params.verbose && record.session) {
96
- const conversation = deps.getConversation(record.session);
93
+ const conversation = getConversation(record.session);
97
94
  if (conversation) {
98
95
  output += `\n\n--- Agent Conversation ---\n${conversation}`;
99
96
  }
@@ -4,17 +4,14 @@ import type { AgentRecord } from "../types.js";
4
4
  import { getSessionContextPercent } from "../usage.js";
5
5
  import { formatLifetimeTokens, textResult } from "./helpers.js";
6
6
 
7
- /** Narrow deps — only the methods this tool's execute callback calls. */
8
- export interface SteerToolDeps {
9
- getRecord: (id: string) => AgentRecord | undefined;
10
- emitEvent: (name: string, data: unknown) => void;
11
- steerAgent: (session: AgentSession, message: string) => Promise<void>;
12
- /** Buffer a steer for an agent whose session isn't ready yet. */
13
- queueSteer: (id: string, message: string) => boolean;
14
- }
15
-
16
7
  /** Create the steer_subagent tool definition (without Pi SDK wrapper). */
17
- export function createSteerTool(deps: SteerToolDeps) {
8
+ export function createSteerTool(
9
+ getRecord: (id: string) => AgentRecord | undefined,
10
+ emitEvent: (name: string, data: unknown) => void,
11
+ steerAgent: (session: AgentSession, message: string) => Promise<void>,
12
+ /** Buffer a steer for an agent whose session isn't ready yet. */
13
+ queueSteer: (id: string, message: string) => boolean,
14
+ ) {
18
15
  return {
19
16
  name: "steer_subagent" as const,
20
17
  label: "Steer Agent",
@@ -38,7 +35,7 @@ export function createSteerTool(deps: SteerToolDeps) {
38
35
  _onUpdate: unknown,
39
36
  _ctx: unknown,
40
37
  ) => {
41
- const record = deps.getRecord(params.agent_id);
38
+ const record = getRecord(params.agent_id);
42
39
  if (!record) {
43
40
  return textResult(
44
41
  `Agent not found: "${params.agent_id}". It may have been cleaned up.`,
@@ -52,16 +49,16 @@ export function createSteerTool(deps: SteerToolDeps) {
52
49
  const session = record.session;
53
50
  if (!session) {
54
51
  // Session not ready yet — queue via manager for delivery once initialized
55
- deps.queueSteer(record.id, params.message);
56
- deps.emitEvent("subagents:steered", { id: record.id, message: params.message });
52
+ queueSteer(record.id, params.message);
53
+ emitEvent("subagents:steered", { id: record.id, message: params.message });
57
54
  return textResult(
58
55
  `Steering message queued for agent ${record.id}. It will be delivered once the session initializes.`,
59
56
  );
60
57
  }
61
58
 
62
59
  try {
63
- await deps.steerAgent(session, params.message);
64
- deps.emitEvent("subagents:steered", { id: record.id, message: params.message });
60
+ await steerAgent(session, params.message);
61
+ emitEvent("subagents:steered", { id: record.id, message: params.message });
65
62
  const tokens = formatLifetimeTokens(record);
66
63
  const contextPercent = getSessionContextPercent(session);
67
64
  const stateParts: string[] = [];