@gotgenes/pi-subagents 6.12.1 → 6.13.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,20 @@ 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.13.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.12.1...pi-subagents-v6.13.0) (2026-05-23)
9
+
10
+
11
+ ### Features
12
+
13
+ * add AgentFileOps interface and FsAgentFileOps ([#136](https://github.com/gotgenes/pi-packages/issues/136)) ([9625de6](https://github.com/gotgenes/pi-packages/commit/9625de60ace3cf58ad59c2d533f18fd7cdb8bba9))
14
+
15
+
16
+ ### Documentation
17
+
18
+ * plan agent-menu decomposition ([#136](https://github.com/gotgenes/pi-packages/issues/136)) ([41ca6a6](https://github.com/gotgenes/pi-packages/commit/41ca6a6612cbc167a5b3d336ffb94d3f9666868f))
19
+ * **retro:** add retro notes for issue [#135](https://github.com/gotgenes/pi-packages/issues/135) ([83e255b](https://github.com/gotgenes/pi-packages/commit/83e255b4a5e6a56a287c933e4a5fa0b28121529e))
20
+ * update architecture for agent-menu decomposition ([#136](https://github.com/gotgenes/pi-packages/issues/136)) ([dba90e8](https://github.com/gotgenes/pi-packages/commit/dba90e86693e3480004a7c305d5082cb5a930d3f))
21
+
8
22
  ## [6.12.1](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.12.0...pi-subagents-v6.12.1) (2026-05-22)
9
23
 
10
24
 
@@ -273,7 +273,10 @@ src/
273
273
  │ └── tool-start.ts — tool_execution_start handler
274
274
  ├── notification.ts — completion nudges, custom renderer
275
275
  ├── renderer.ts — notification TUI component
276
- ├── ui/agent-menu.ts — /agents slash command menu
276
+ ├── ui/agent-menu.ts — /agents slash command menu (orchestration, listing, settings)
277
+ ├── ui/agent-config-editor.ts — agent detail view (edit/delete/eject/disable/enable)
278
+ ├── ui/agent-creation-wizard.ts — agent creation (AI-generation and manual-form)
279
+ ├── ui/agent-file-ops.ts — AgentFileOps interface + FsAgentFileOps implementation
277
280
  ├── service-adapter.ts — SubagentsService implementation wrapping AgentManager
278
281
  └── (existing domain modules unchanged)
279
282
  ```
@@ -572,17 +575,16 @@ Remaining 15 `as any` casts are: 8 menu-handler `ctx as any` (deferred — requi
572
575
  All consumer modules (menu, tools, renderer, conversation viewer) import from `ui/display.ts` directly.
573
576
  `test/agent-widget.test.ts` renamed to `test/display.test.ts`.
574
577
 
575
- ### Step K: Decompose agent-menu.ts (#136)
578
+ ### Step K: Decompose agent-menu.ts (#136)
576
579
 
577
- `agent-menu.ts` (650 lines) has 8 distinct responsibilities: menu FSM, agent listing, config editing, agent ejection, two creation wizards, running-agent viewer, and settings form.
578
- Filesystem operations (read/write/delete agent `.md` files) are scattered throughout.
580
+ `agent-menu.ts` (668 lines) decomposed into four modules:
579
581
 
580
- 1. Extract `AgentFileOps` interface `read`, `write`, `delete`, `findAgentFile` abstracting the fs calls.
581
- 2. Extract `ui/agent-config-editor.ts` — `showAgentDetail` with enable/disable/reset/delete transitions.
582
- 3. Extract `ui/agent-creation-wizard.ts` — both AI-generation and manual form paths.
583
- 4. Leave menu orchestration, settings form, and running-agent viewer in `agent-menu.ts` (~200 lines).
582
+ 1. `ui/agent-file-ops.ts` `AgentFileOps` interface (`exists`, `read`, `write`, `remove`, `ensureDir`, `findAgentFile`) + `FsAgentFileOps` production implementation.
583
+ 2. `ui/agent-config-editor.ts` — `showAgentDetail` with edit/delete/reset/eject/disable/enable transitions (~200 lines).
584
+ 3. `ui/agent-creation-wizard.ts` — AI-generation and manual-form creation paths (~250 lines).
585
+ 4. `ui/agent-menu.ts` menu orchestration, agent listing, running-agent viewer, settings form (~300 lines).
584
586
 
585
- Impact: `agent-menu.ts` drops from 650~200 lines; extracted modules receive `AgentFileOps` via injection; wizard logic becomes independently testable.
587
+ Impact: `agent-menu.ts` dropped from 668296 lines; extracted modules receive `AgentFileOps` via injection; `vi.mock("node:fs")` eliminated from `agent-menu.test.ts`.
586
588
 
587
589
  ### Step dependencies
588
590
 
@@ -0,0 +1,300 @@
1
+ ---
2
+ issue: 136
3
+ issue_title: "Decompose `agent-menu.ts`"
4
+ ---
5
+
6
+ # Decompose agent-menu into config editor, creation wizard, and file-ops abstraction
7
+
8
+ ## Problem Statement
9
+
10
+ `agent-menu.ts` (668 lines) has 8 distinct responsibilities: menu FSM orchestration, agent listing, config editing, agent ejection, two creation wizards, running-agent viewer, and settings form.
11
+ Filesystem operations (`readFileSync`, `writeFileSync`, `unlinkSync`, `existsSync`, `mkdirSync`) are scattered across 10+ call sites with no abstraction layer, forcing tests to use `vi.mock("node:fs")` rather than injecting stubs.
12
+
13
+ ## Goals
14
+
15
+ - Extract an `AgentFileOps` interface abstracting all filesystem calls, with a production `FsAgentFileOps` implementation.
16
+ - Extract `ui/agent-config-editor.ts` (~170 lines) containing `showAgentDetail` with eject/disable/enable/edit/delete/reset transitions.
17
+ - Extract `ui/agent-creation-wizard.ts` (~200 lines) containing both the AI-generation and manual-form creation paths.
18
+ - Leave menu FSM, agent listing, running-agent viewer, and settings form in `agent-menu.ts` (~280 lines).
19
+ - Each extracted module receives dependencies via injection — no direct `node:fs` imports outside `FsAgentFileOps`.
20
+ - Enable unit testing of config editor and creation wizard without `vi.mock("node:fs")`.
21
+
22
+ ## Non-Goals
23
+
24
+ - Refactoring the settings form or running-agent viewer — they stay in `agent-menu.ts`.
25
+ - Changing any user-facing behavior or menu flow.
26
+ - Extracting `showAllAgentsList` — it is menu orchestration (presents the list, delegates to editor for detail).
27
+ - Deduplicating the YAML frontmatter builders in eject and manual wizard (structurally different content shapes).
28
+
29
+ ## Background
30
+
31
+ ### Prerequisite
32
+
33
+ Issue #135 (extract display helpers) is **implemented** — `ui/display.ts` exists and provides `formatDuration`, `getDisplayName`, and other formatters.
34
+ Extracted menu sub-modules can import display helpers without pulling in the widget.
35
+
36
+ ### Existing IO-injection convention
37
+
38
+ The codebase uses injectable IO interfaces to decouple domain logic from `node:fs` and the Pi SDK:
39
+
40
+ - `AssemblerIO` in `session-config.ts` — 4 methods for prompt assembly IO.
41
+ - `RunnerIO` in `agent-runner.ts` — 7 methods for session creation IO.
42
+
43
+ Both follow the same pattern: interface defined in the module that uses it, production implementation wired at the edge (`index.ts`), test stubs injected directly.
44
+
45
+ ### Current fs call inventory in agent-menu.ts
46
+
47
+ | Function | fs calls |
48
+ | -------------------------------- | ------------------------------------------------------------- |
49
+ | `findAgentFile` | `existsSync` ×2 |
50
+ | `showAgentDetail` (edit) | `readFileSync`, `writeFileSync` |
51
+ | `showAgentDetail` (delete/reset) | `unlinkSync` |
52
+ | `ejectAgent` | `mkdirSync`, `existsSync`, `writeFileSync` |
53
+ | `disableAgent` | `readFileSync`, `writeFileSync`, `mkdirSync`, `writeFileSync` |
54
+ | `enableAgent` | `readFileSync`, `writeFileSync`, `unlinkSync` |
55
+ | `showGenerateWizard` | `mkdirSync`, `existsSync` ×2 |
56
+ | `showManualWizard` | `mkdirSync`, `existsSync`, `writeFileSync` |
57
+
58
+ ### Current test file
59
+
60
+ `agent-menu.test.ts` (212 lines) uses `vi.mock("node:fs")` with `vi.hoisted` stubs.
61
+ Only 7 tests exist — they cover the top-level menu, agent listing, projectAgentsDir injection, and settings delegation.
62
+ No tests exercise config-editor transitions (edit/delete/eject/disable/enable) or creation wizards.
63
+
64
+ ## Design Overview
65
+
66
+ ### AgentFileOps interface
67
+
68
+ A narrow interface abstracting all agent `.md` file operations:
69
+
70
+ ```typescript
71
+ interface AgentFileOps {
72
+ exists(filePath: string): boolean;
73
+ read(filePath: string): string | undefined;
74
+ write(filePath: string, content: string): void;
75
+ remove(filePath: string): void;
76
+ ensureDir(dirPath: string): void;
77
+ findAgentFile(name: string, dirs: string[]): string | undefined;
78
+ }
79
+ ```
80
+
81
+ Design notes:
82
+
83
+ - `read` returns `undefined` when the file does not exist (wraps `readFileSync` with a try/catch).
84
+ - `write` ensures parent directories exist before writing (internalizes `mkdirSync`).
85
+ - `ensureDir` is needed separately because `showGenerateWizard` creates the directory before spawning an agent that writes via Pi's tool (not via `AgentFileOps.write`).
86
+ - `findAgentFile` takes an ordered list of directories and returns the first matching path.
87
+ The current code returns `{ path, location }` but only `showAgentDetail` uses `location` (for a confirmation dialog) — the full path already conveys the location to the user, so a plain `string | undefined` return suffices.
88
+
89
+ ### FsAgentFileOps production implementation
90
+
91
+ Thin wrapper over `node:fs` synchronous APIs:
92
+
93
+ ```typescript
94
+ class FsAgentFileOps implements AgentFileOps {
95
+ exists(filePath: string): boolean {
96
+ return existsSync(filePath);
97
+ }
98
+ read(filePath: string): string | undefined {
99
+ try { return readFileSync(filePath, "utf-8"); }
100
+ catch { return undefined; }
101
+ }
102
+ write(filePath: string, content: string): void {
103
+ mkdirSync(dirname(filePath), { recursive: true });
104
+ writeFileSync(filePath, content, "utf-8");
105
+ }
106
+ remove(filePath: string): void {
107
+ unlinkSync(filePath);
108
+ }
109
+ ensureDir(dirPath: string): void {
110
+ mkdirSync(dirPath, { recursive: true });
111
+ }
112
+ findAgentFile(name: string, dirs: string[]): string | undefined {
113
+ for (const dir of dirs) {
114
+ const p = join(dir, `${name}.md`);
115
+ if (existsSync(p)) return p;
116
+ }
117
+ return undefined;
118
+ }
119
+ }
120
+ ```
121
+
122
+ ### Extracted module patterns
123
+
124
+ Both extracted modules follow the existing factory-function pattern (`createAgentsMenuHandler(deps)`) with narrow deps interfaces per ISP:
125
+
126
+ Config editor call-site sketch (from orchestrator):
127
+
128
+ ```typescript
129
+ const editor = createAgentConfigEditor({
130
+ fileOps: deps.fileOps,
131
+ registry: deps.registry,
132
+ personalAgentsDir: deps.personalAgentsDir,
133
+ projectAgentsDir: deps.projectAgentsDir,
134
+ });
135
+ // ...
136
+ await editor.showAgentDetail(ctx, agentName);
137
+ ```
138
+
139
+ Creation wizard call-site sketch (from orchestrator):
140
+
141
+ ```typescript
142
+ const wizard = createAgentCreationWizard({
143
+ fileOps: deps.fileOps,
144
+ manager: deps.manager,
145
+ registry: deps.registry,
146
+ personalAgentsDir: deps.personalAgentsDir,
147
+ projectAgentsDir: deps.projectAgentsDir,
148
+ });
149
+ // ...
150
+ await wizard.showCreateWizard(ctx);
151
+ ```
152
+
153
+ ### Deps interfaces (ISP-compliant)
154
+
155
+ ```typescript
156
+ // agent-config-editor.ts
157
+ interface AgentConfigEditorDeps {
158
+ fileOps: AgentFileOps;
159
+ registry: AgentTypeRegistry;
160
+ personalAgentsDir: string;
161
+ projectAgentsDir: string;
162
+ }
163
+
164
+ // agent-creation-wizard.ts
165
+ interface AgentCreationWizardDeps {
166
+ fileOps: AgentFileOps;
167
+ manager: AgentMenuManager;
168
+ registry: AgentTypeRegistry;
169
+ personalAgentsDir: string;
170
+ projectAgentsDir: string;
171
+ }
172
+ ```
173
+
174
+ The config editor does not need `manager` (no agent spawning).
175
+ The creation wizard does not need `agentActivity`, `getModelLabel`, or `settings`.
176
+
177
+ ### Updated AgentMenuDeps
178
+
179
+ ```typescript
180
+ interface AgentMenuDeps {
181
+ manager: AgentMenuManager;
182
+ registry: AgentTypeRegistry;
183
+ agentActivity: AgentActivityReader;
184
+ getModelLabel: (type: string, registry?: ModelRegistry) => string;
185
+ settings: AgentMenuSettings;
186
+ fileOps: AgentFileOps; // ← new
187
+ personalAgentsDir: string;
188
+ projectAgentsDir: string;
189
+ }
190
+ ```
191
+
192
+ The single new field (`fileOps`) replaces all direct `node:fs` imports.
193
+
194
+ ## Module-Level Changes
195
+
196
+ ### New files
197
+
198
+ 1. `src/ui/agent-file-ops.ts` — `AgentFileOps` interface + `FsAgentFileOps` class.
199
+ 2. `src/ui/agent-config-editor.ts` — `AgentConfigEditorDeps` interface, `createAgentConfigEditor` factory.
200
+ Moves in: `showAgentDetail`, `ejectAgent`, `disableAgent`, `enableAgent`.
201
+ 3. `src/ui/agent-creation-wizard.ts` — `AgentCreationWizardDeps` interface, `createAgentCreationWizard` factory.
202
+ Moves in: `showCreateWizard`, `showGenerateWizard`, `showManualWizard`.
203
+ 4. `test/ui/agent-file-ops.test.ts` — unit tests for `FsAgentFileOps`.
204
+ 5. `test/ui/agent-config-editor.test.ts` — unit tests for config editor transitions with injected `AgentFileOps` stubs.
205
+ 6. `test/ui/agent-creation-wizard.test.ts` — unit tests for wizard paths with injected stubs.
206
+
207
+ ### Modified files
208
+
209
+ 1. `src/ui/agent-menu.ts`:
210
+ - Remove `import { existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs"`.
211
+ - Remove `findAgentFile`, `showAgentDetail`, `ejectAgent`, `disableAgent`, `enableAgent`, `showCreateWizard`, `showGenerateWizard`, `showManualWizard` (~375 lines removed).
212
+ - Add `fileOps: AgentFileOps` to `AgentMenuDeps`.
213
+ - Import and instantiate `createAgentConfigEditor` and `createAgentCreationWizard`.
214
+ - Update `showAllAgentsList` to call `editor.showAgentDetail(ctx, agentName)`.
215
+ - Update `showAgentsMenu` to call `wizard.showCreateWizard(ctx)`.
216
+ - Re-export `AgentMenuManager` (still needed by the wizard deps).
217
+ - Net result: ~280 lines (orchestration, listing, running agents, settings).
218
+ 2. `src/index.ts`:
219
+ - Import `FsAgentFileOps` from `./ui/agent-file-ops.js`.
220
+ - Construct `new FsAgentFileOps()` and pass as `fileOps` in the `createAgentsMenuHandler` call.
221
+ 3. `test/ui/agent-menu.test.ts`:
222
+ - Remove `vi.mock("node:fs")` and `vi.hoisted` stubs entirely.
223
+ - Add `fileOps` stub to `makeDeps` factory.
224
+ - Rewrite the "projectAgentsDir injection" test to verify that the orchestrator delegates to the editor (or move to `agent-config-editor.test.ts`).
225
+
226
+ ### Removed symbols
227
+
228
+ Grep confirms these functions are only referenced within `agent-menu.ts` (closures inside `createAgentsMenuHandler`) — no external consumers:
229
+
230
+ - `findAgentFile` — moves to `agent-config-editor.ts` (reimplemented via `AgentFileOps.findAgentFile`).
231
+ - `showAgentDetail`, `ejectAgent`, `disableAgent`, `enableAgent` — move to `agent-config-editor.ts`.
232
+ - `showCreateWizard`, `showGenerateWizard`, `showManualWizard` — move to `agent-creation-wizard.ts`.
233
+
234
+ ## Test Impact Analysis
235
+
236
+ ### New unit tests enabled by extraction
237
+
238
+ 1. **Config editor transitions** — `showAgentDetail` has 6 menu paths (edit, delete, reset, eject, disable, enable) with sub-branches (confirm/cancel, file exists/not-exists, default/custom agent).
239
+ Currently untestable in isolation because the functions are closures inside `createAgentsMenuHandler`.
240
+ After extraction, each transition is testable with injected `AgentFileOps` stubs — no `vi.mock("node:fs")` needed.
241
+ 2. **Creation wizard flows** — generate wizard (spawn + check result) and manual wizard (multi-step form → write file) are currently untested.
242
+ After extraction, both are testable with injected stubs for `AgentFileOps` and `AgentMenuManager.spawnAndWait`.
243
+ 3. **FsAgentFileOps** — thin tests verifying the production fs wrapper (read returns undefined on missing file, write ensures parent dirs, findAgentFile checks directories in order).
244
+
245
+ ### Existing tests that become redundant
246
+
247
+ The "projectAgentsDir injection" test (`agent-menu.test.ts`) navigates through the main menu → agent types → agent detail, then asserts `mockExistsSync` was called with the correct path.
248
+ After extraction, this end-to-end path tests orchestration + editor together; a focused test in `agent-config-editor.test.ts` replaces it.
249
+ The orchestrator test can be simplified to verify it delegates to the editor.
250
+
251
+ ### Existing tests that stay as-is
252
+
253
+ - Menu structure tests (shows options, reload, running agents) — these test the orchestrator directly.
254
+ - Settings delegation tests — these test code that remains in `agent-menu.ts`.
255
+
256
+ ## TDD Order
257
+
258
+ ### Cycle 1: AgentFileOps interface and FsAgentFileOps
259
+
260
+ 1. `test:` write `test/ui/agent-file-ops.test.ts` — tests for `read` (existing file, missing file), `write` (ensures parent dir), `remove`, `exists`, `ensureDir`, `findAgentFile` (first-match ordering, no match).
261
+ 2. `feat:` implement `src/ui/agent-file-ops.ts` — interface + `FsAgentFileOps` class.
262
+ 3. Commit: `feat: add AgentFileOps interface and FsAgentFileOps (#136)`.
263
+
264
+ ### Cycle 2: Extract agent-config-editor
265
+
266
+ 1. `test:` write `test/ui/agent-config-editor.test.ts` — tests for `showAgentDetail` transitions: edit (save/cancel), delete (confirm/cancel), reset-to-default (confirm/cancel), eject (project/personal location, overwrite check), disable (existing file toggle, new disable-only file), enable (remove enabled:false line, remove empty override file).
267
+ All tests inject stub `AgentFileOps` — no `vi.mock`.
268
+ 2. `refactor:` create `src/ui/agent-config-editor.ts` — move `showAgentDetail`, `ejectAgent`, `disableAgent`, `enableAgent` from `agent-menu.ts`.
269
+ Replace direct fs calls with `deps.fileOps.*` calls.
270
+ Replace the closure `findAgentFile` with `deps.fileOps.findAgentFile(name, [deps.projectAgentsDir, deps.personalAgentsDir])`.
271
+ 3. `refactor:` update `src/ui/agent-menu.ts` — add `fileOps` to `AgentMenuDeps`, import `createAgentConfigEditor`, wire into `showAllAgentsList`.
272
+ 4. `refactor:` update `src/index.ts` — import `FsAgentFileOps`, pass `fileOps: new FsAgentFileOps()` in deps.
273
+ 5. `test:` update `test/ui/agent-menu.test.ts` — add `fileOps` stub to `makeDeps`, rewrite the "projectAgentsDir injection" test to verify orchestrator→editor delegation.
274
+ 6. Run `pnpm run check` (interface change).
275
+ 7. Commit: `refactor: extract agent-config-editor from agent-menu (#136)`.
276
+
277
+ ### Cycle 3: Extract agent-creation-wizard
278
+
279
+ 1. `test:` write `test/ui/agent-creation-wizard.test.ts` — tests for `showCreateWizard` (location + method selection), `showGenerateWizard` (spawn success, spawn error, overwrite check), `showManualWizard` (full form flow, overwrite check, tool/model/thinking selections).
280
+ All tests inject stub `AgentFileOps` + `AgentMenuManager` — no `vi.mock`.
281
+ 2. `refactor:` create `src/ui/agent-creation-wizard.ts` — move `showCreateWizard`, `showGenerateWizard`, `showManualWizard` from `agent-menu.ts`.
282
+ Replace direct fs calls with `deps.fileOps.*` calls.
283
+ 3. `refactor:` update `src/ui/agent-menu.ts` — import `createAgentCreationWizard`, wire into `showAgentsMenu`.
284
+ Remove the `import { ... } from "node:fs"` line (no longer needed).
285
+ 4. `test:` remove `vi.mock("node:fs")` and `vi.hoisted` stubs from `test/ui/agent-menu.test.ts` entirely.
286
+ 5. Run `pnpm run check`.
287
+ 6. Commit: `refactor: extract agent-creation-wizard from agent-menu (#136)`.
288
+
289
+ ## Risks and Mitigations
290
+
291
+ | Risk | Mitigation |
292
+ | --------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
293
+ | Circular dependency between orchestrator and editor (editor calling back to menu) | The editor's `showAgentDetail` does not recurse to the agent list — the orchestrator does (`showAllAgentsList` calls editor then recurses itself). No circular dependency. |
294
+ | `FsAgentFileOps.write` silently creates parent directories when callers expect the directory to not exist | `write` mirrors current behavior — every call site already calls `mkdirSync` before `writeFileSync`. The consolidation only changes where the `mkdirSync` happens, not whether it runs. |
295
+ | Adding `fileOps` to `AgentMenuDeps` breaks existing test factory | Cycle 2 updates `makeDeps` in the same commit — the interface change and call-site update land together per the testing skill's single-call-site rule. |
296
+ | Generate wizard spawns an agent that writes via Pi tools, not via `AgentFileOps.write` | The wizard uses `fileOps.ensureDir` for directory creation and `fileOps.exists` for the post-spawn success check. The spawned agent's file write is outside the menu's control and is not abstracted. |
297
+
298
+ ## Open Questions
299
+
300
+ - None — the issue's proposed changes section is unambiguous and the prerequisite (#135) is already implemented.
@@ -0,0 +1,38 @@
1
+ ---
2
+ issue: 135
3
+ issue_title: "Extract display helpers from `agent-widget.ts`"
4
+ ---
5
+
6
+ # Retro: #135 — Extract display helpers from agent-widget.ts
7
+
8
+ ## Final Retrospective (2026-05-22T19:00:00Z)
9
+
10
+ ### Session summary
11
+
12
+ Extracted 11 helper functions, 3 constants, and 2 types from `agent-widget.ts` into a new `ui/display.ts` module.
13
+ Updated 10 source consumers and 2 test consumers.
14
+ Pure code-motion refactoring — no behavior change, no test-count delta (714 tests throughout).
15
+
16
+ ### Observations
17
+
18
+ #### What went well
19
+
20
+ - Plan accurately identified all 10 source and 2 test import sites with no misses — the consumer import table in the plan mapped 1:1 to actual changes.
21
+ - TDD execution was mechanical and smooth: create module → update source imports → rename test file → verify.
22
+ Zero surprises or deviations from the plan.
23
+
24
+ #### What caused friction (agent side)
25
+
26
+ - `rabbit-hole` — During `/ship-issue`, wasted ~6 tool calls investigating whether `pi-subagents-v6.12.0` was at HEAD.
27
+ Ran `git log --oneline HEAD --not --remotes=origin/main` which dumped the entire repo history (50KB truncation), then misread `git describe --tags --abbrev=0` (nearest ancestor tag) as confirming the tag was at HEAD.
28
+ `git tag --points-at HEAD` returned empty, disproving the assumption, but I still spent cycles reasoning about CI release-please behavior.
29
+ Impact: added friction but no rework — the close comment and release-please merge were correct.
30
+
31
+ #### What caused friction (user side)
32
+
33
+ - None observed.
34
+ The issue was unambiguous, the architecture doc prescribed the exact extraction set, and no user intervention was needed during implementation.
35
+
36
+ ### Changes made
37
+
38
+ 1. Created `packages/pi-subagents/docs/retro/0135-extract-display-helpers.md` (this file).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-subagents",
3
- "version": "6.12.1",
3
+ "version": "6.13.0",
4
4
  "exports": {
5
5
  ".": "./src/service.ts"
6
6
  },
package/src/index.ts CHANGED
@@ -41,6 +41,7 @@ import { createAgentTool } from "./tools/agent-tool.js";
41
41
  import { createGetResultTool } from "./tools/get-result-tool.js";
42
42
  import { getModelLabelFromConfig } from "./tools/helpers.js";
43
43
  import { createSteerTool } from "./tools/steer-tool.js";
44
+ import { FsAgentFileOps } from "./ui/agent-file-ops.js";
44
45
  import { createAgentsMenuHandler } from "./ui/agent-menu.js";
45
46
  import {
46
47
  AgentWidget,
@@ -248,6 +249,7 @@ export default function (pi: ExtensionAPI) {
248
249
  return getModelLabelFromConfig(cfg.model);
249
250
  },
250
251
  settings,
252
+ fileOps: new FsAgentFileOps(),
251
253
  personalAgentsDir: join(getAgentDir(), 'agents'),
252
254
  projectAgentsDir: join(process.cwd(), '.pi', 'agents'),
253
255
  });
@@ -0,0 +1,202 @@
1
+ /**
2
+ * agent-config-editor.ts — Agent detail view with edit/delete/eject/disable/enable transitions.
3
+ *
4
+ * Extracted from agent-menu.ts to give each concern a single responsibility.
5
+ * Receives dependencies via injection — no direct `node:fs` imports.
6
+ */
7
+
8
+ import { join } from "node:path";
9
+
10
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
11
+ import type { AgentTypeRegistry } from "../agent-types.js";
12
+ import type { AgentConfig } from "../types.js";
13
+ import type { AgentFileOps } from "./agent-file-ops.js";
14
+
15
+ // ---- Deps interface ----
16
+
17
+ export interface AgentConfigEditorDeps {
18
+ fileOps: AgentFileOps;
19
+ registry: AgentTypeRegistry;
20
+ personalAgentsDir: string;
21
+ projectAgentsDir: string;
22
+ }
23
+
24
+ // ---- Factory ----
25
+
26
+ export function createAgentConfigEditor(deps: AgentConfigEditorDeps) {
27
+ function agentDirs(): string[] {
28
+ return [deps.projectAgentsDir, deps.personalAgentsDir];
29
+ }
30
+
31
+ async function showAgentDetail(ctx: ExtensionContext, name: string) {
32
+ if (deps.registry.resolveType(name) == null) {
33
+ ctx.ui.notify(`Agent config not found for "${name}".`, "warning");
34
+ return;
35
+ }
36
+ const cfg = deps.registry.resolveAgentConfig(name);
37
+
38
+ const file = deps.fileOps.findAgentFile(name, agentDirs());
39
+ const isDefault = cfg.isDefault === true;
40
+ const disabled = cfg.enabled === false;
41
+
42
+ let menuOptions: string[];
43
+ if (disabled && file) {
44
+ menuOptions = isDefault
45
+ ? ["Enable", "Edit", "Reset to default", "Delete", "Back"]
46
+ : ["Enable", "Edit", "Delete", "Back"];
47
+ } else if (isDefault && !file) {
48
+ menuOptions = ["Eject (export as .md)", "Disable", "Back"];
49
+ } else if (isDefault && file) {
50
+ menuOptions = ["Edit", "Disable", "Reset to default", "Delete", "Back"];
51
+ } else {
52
+ menuOptions = ["Edit", "Disable", "Delete", "Back"];
53
+ }
54
+
55
+ const choice = await ctx.ui.select(name, menuOptions);
56
+ if (!choice || choice === "Back") return;
57
+
58
+ if (choice === "Edit" && file) {
59
+ const content = deps.fileOps.read(file);
60
+ if (content !== undefined) {
61
+ const edited = await ctx.ui.editor(`Edit ${name}`, content);
62
+ if (edited !== undefined && edited !== content) {
63
+ deps.fileOps.write(file, edited);
64
+ deps.registry.reload();
65
+ ctx.ui.notify(`Updated ${file}`, "info");
66
+ }
67
+ }
68
+ } else if (choice === "Delete") {
69
+ if (file) {
70
+ const confirmed = await ctx.ui.confirm(
71
+ "Delete agent",
72
+ `Delete ${name} (${file})?`,
73
+ );
74
+ if (confirmed) {
75
+ deps.fileOps.remove(file);
76
+ deps.registry.reload();
77
+ ctx.ui.notify(`Deleted ${file}`, "info");
78
+ }
79
+ }
80
+ } else if (choice === "Reset to default" && file) {
81
+ const confirmed = await ctx.ui.confirm(
82
+ "Reset to default",
83
+ `Delete override ${file} and restore embedded default?`,
84
+ );
85
+ if (confirmed) {
86
+ deps.fileOps.remove(file);
87
+ deps.registry.reload();
88
+ ctx.ui.notify(`Restored default ${name}`, "info");
89
+ }
90
+ } else if (choice.startsWith("Eject")) {
91
+ await ejectAgent(ctx, name, cfg);
92
+ } else if (choice === "Disable") {
93
+ await disableAgent(ctx, name);
94
+ } else if (choice === "Enable") {
95
+ await enableAgent(ctx, name);
96
+ }
97
+ }
98
+
99
+ async function ejectAgent(ctx: ExtensionContext, name: string, cfg: AgentConfig) {
100
+ const location = await ctx.ui.select("Choose location", [
101
+ "Project (.pi/agents/)",
102
+ `Personal (${deps.personalAgentsDir})`,
103
+ ]);
104
+ if (!location) return;
105
+
106
+ const targetDir = location.startsWith("Project")
107
+ ? deps.projectAgentsDir
108
+ : deps.personalAgentsDir;
109
+
110
+ const targetPath = join(targetDir, `${name}.md`);
111
+ if (deps.fileOps.exists(targetPath)) {
112
+ const overwrite = await ctx.ui.confirm(
113
+ "Overwrite",
114
+ `${targetPath} already exists. Overwrite?`,
115
+ );
116
+ if (!overwrite) return;
117
+ }
118
+
119
+ const fmFields: string[] = [];
120
+ fmFields.push(`description: ${cfg.description}`);
121
+ if (cfg.displayName) fmFields.push(`display_name: ${cfg.displayName}`);
122
+ fmFields.push(`tools: ${cfg.builtinToolNames?.join(", ") || "all"}`);
123
+ if (cfg.model) fmFields.push(`model: ${cfg.model}`);
124
+ if (cfg.thinking) fmFields.push(`thinking: ${cfg.thinking}`);
125
+ if (cfg.maxTurns) fmFields.push(`max_turns: ${cfg.maxTurns}`);
126
+ fmFields.push(`prompt_mode: ${cfg.promptMode}`);
127
+ if (cfg.extensions === false) fmFields.push("extensions: false");
128
+ else if (Array.isArray(cfg.extensions))
129
+ fmFields.push(`extensions: ${cfg.extensions.join(", ")}`);
130
+ if (cfg.skills === false) fmFields.push("skills: false");
131
+ else if (Array.isArray(cfg.skills))
132
+ fmFields.push(`skills: ${cfg.skills.join(", ")}`);
133
+ if (cfg.disallowedTools?.length)
134
+ fmFields.push(`disallowed_tools: ${cfg.disallowedTools.join(", ")}`);
135
+ if (cfg.inheritContext) fmFields.push("inherit_context: true");
136
+ if (cfg.runInBackground) fmFields.push("run_in_background: true");
137
+ if (cfg.isolated) fmFields.push("isolated: true");
138
+ if (cfg.memory) fmFields.push(`memory: ${cfg.memory}`);
139
+ if (cfg.isolation) fmFields.push(`isolation: ${cfg.isolation}`);
140
+
141
+ const content = `---\n${fmFields.join("\n")}\n---\n\n${cfg.systemPrompt}\n`;
142
+
143
+ deps.fileOps.write(targetPath, content);
144
+ deps.registry.reload();
145
+ ctx.ui.notify(`Ejected ${name} to ${targetPath}`, "info");
146
+ }
147
+
148
+ async function disableAgent(ctx: ExtensionContext, name: string) {
149
+ const file = deps.fileOps.findAgentFile(name, agentDirs());
150
+ if (file) {
151
+ const content = deps.fileOps.read(file);
152
+ if (content?.includes("\nenabled: false\n")) {
153
+ ctx.ui.notify(`${name} is already disabled.`, "info");
154
+ return;
155
+ }
156
+ if (content) {
157
+ const updated = content.replace(/^---\n/, "---\nenabled: false\n");
158
+ deps.fileOps.write(file, updated);
159
+ deps.registry.reload();
160
+ ctx.ui.notify(`Disabled ${name} (${file})`, "info");
161
+ }
162
+ return;
163
+ }
164
+
165
+ const location = await ctx.ui.select("Choose location", [
166
+ "Project (.pi/agents/)",
167
+ `Personal (${deps.personalAgentsDir})`,
168
+ ]);
169
+ if (!location) return;
170
+
171
+ const targetDir = location.startsWith("Project")
172
+ ? deps.projectAgentsDir
173
+ : deps.personalAgentsDir;
174
+
175
+ const targetPath = join(targetDir, `${name}.md`);
176
+ deps.fileOps.write(targetPath, "---\nenabled: false\n---\n");
177
+ deps.registry.reload();
178
+ ctx.ui.notify(`Disabled ${name} (${targetPath})`, "info");
179
+ }
180
+
181
+ async function enableAgent(ctx: ExtensionContext, name: string) {
182
+ const file = deps.fileOps.findAgentFile(name, agentDirs());
183
+ if (!file) return;
184
+
185
+ const content = deps.fileOps.read(file);
186
+ if (!content) return;
187
+
188
+ const updated = content.replace(/^(---\n)enabled: false\n/, "$1");
189
+
190
+ if (updated.trim() === "---\n---" || updated.trim() === "---\n---\n") {
191
+ deps.fileOps.remove(file);
192
+ deps.registry.reload();
193
+ ctx.ui.notify(`Enabled ${name} (removed ${file})`, "info");
194
+ } else {
195
+ deps.fileOps.write(file, updated);
196
+ deps.registry.reload();
197
+ ctx.ui.notify(`Enabled ${name} (${file})`, "info");
198
+ }
199
+ }
200
+
201
+ return { showAgentDetail };
202
+ }