@gotgenes/pi-subagents 7.2.6 → 7.2.8

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,29 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [7.2.8](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v7.2.7...pi-subagents-v7.2.8) (2026-05-25)
9
+
10
+
11
+ ### Performance Improvements
12
+
13
+ * remove <inherited_system_prompt> wrapper to maximise KV cache reuse ([#180](https://github.com/gotgenes/pi-packages/issues/180)) ([f35e7b1](https://github.com/gotgenes/pi-packages/commit/f35e7b1b4309f91656677932c201d762c4be5cf3))
14
+
15
+
16
+ ### Documentation
17
+
18
+ * **retro:** add retro notes for issue [#206](https://github.com/gotgenes/pi-packages/issues/206) ([f439057](https://github.com/gotgenes/pi-packages/commit/f439057bd42edc018df8ddd94783f8a9c89968e0))
19
+
20
+ ## [7.2.7](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v7.2.6...pi-subagents-v7.2.7) (2026-05-25)
21
+
22
+
23
+ ### Documentation
24
+
25
+ * plan decompose showAgentDetail ([#206](https://github.com/gotgenes/pi-packages/issues/206)) ([fe575a0](https://github.com/gotgenes/pi-packages/commit/fe575a0f2727bdcdaa4dd977f7b2db4d6cb6e9f3))
26
+ * **retro:** add planning stage notes for issue [#206](https://github.com/gotgenes/pi-packages/issues/206) ([057bbb6](https://github.com/gotgenes/pi-packages/commit/057bbb666b8d1c89d70936a837fc028512368fcc))
27
+ * **retro:** add retro notes for issue [#205](https://github.com/gotgenes/pi-packages/issues/205) ([b9abe3b](https://github.com/gotgenes/pi-packages/commit/b9abe3ba050468d71015eb77262afb4093c8289f))
28
+ * **retro:** add TDD stage notes for issue [#206](https://github.com/gotgenes/pi-packages/issues/206) ([88fdfc2](https://github.com/gotgenes/pi-packages/commit/88fdfc222950ce63e0a8f9273d3bff234ffa0538))
29
+ * update complexity table after showAgentDetail decomposition ([8d8a396](https://github.com/gotgenes/pi-packages/commit/8d8a396bbfbe0e4d86dc37aceb53df613868bd26))
30
+
8
31
  ## [7.2.6](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v7.2.5...pi-subagents-v7.2.6) (2026-05-25)
9
32
 
10
33
 
@@ -466,12 +466,10 @@ Bags with 10+ fields are the highest priority for decomposition.
466
466
 
467
467
  Functions with cyclomatic complexity ≥ 21 (critical threshold):
468
468
 
469
- | Function | Cyclomatic | Cognitive | File | Concern |
470
- | ------------------- | ---------- | --------- | --------------------------- | ---------------------------------- |
471
- | `showAgentDetail` | 25 | 33 | `ui/agent-config-editor.ts` | Agent detail/edit view |
472
- | `renderWidgetLines` | 25 | 44 | `ui/widget-renderer.ts` | Renders widget status lines |
473
- | `ejectAgent` | 21 | 20 | `ui/agent-config-editor.ts` | Eject agent to filesystem |
474
- | `update` | 21 | 31 | `ui/agent-widget.ts` | Widget lifecycle + polling |
469
+ | Function | Cyclomatic | Cognitive | File | Concern |
470
+ | ------------------- | ---------- | --------- | --------------------------- | ------------------------------- |
471
+ | `renderWidgetLines` | 25 | 44 | `ui/widget-renderer.ts` | Renders widget status lines |
472
+ | `update` | 21 | 31 | `ui/agent-widget.ts` | Widget lifecycle + polling |
475
473
 
476
474
  ### Churn hotspots
477
475
 
@@ -0,0 +1,211 @@
1
+ ---
2
+ issue: 206
3
+ issue_title: "Decompose showAgentDetail (cognitive 33)"
4
+ ---
5
+
6
+ # Decompose `showAgentDetail`
7
+
8
+ ## Problem Statement
9
+
10
+ `showAgentDetail` in `ui/agent-config-editor.ts` has cognitive complexity 33 (CRITICAL per fallow health).
11
+ It interleaves menu-option computation, user-choice dispatch, and three inline action handlers (edit, delete, reset) in a single 67-line function.
12
+ `ejectAgent` in the same file has cognitive complexity 20 from its 14-branch frontmatter builder.
13
+ Phase 12, Step 2 targets cognitive complexity < 10 per function.
14
+
15
+ ## Goals
16
+
17
+ - Extract menu-option computation and inline action handlers into separate functions, each with cognitive complexity < 10.
18
+ - Extract frontmatter building from `ejectAgent` into a pure function.
19
+ - Preserve all existing behavior — no user-visible or API changes.
20
+ - Add unit tests for the two new exported pure functions.
21
+
22
+ ## Non-Goals
23
+
24
+ - Decomposing `renderWidgetLines` (#205), `update` (#207), or shared test fixtures (#208) — sibling Phase 12 steps.
25
+ - Changing the menu structure, option labels, or action semantics.
26
+ - Decomposing `disableAgent` or `enableAgent` — their cognitive complexity is already manageable (< 15).
27
+
28
+ ## Background
29
+
30
+ `agent-config-editor.ts` was extracted from `agent-menu.ts` in Phase 8 (#136).
31
+ The file exposes a single factory `createAgentConfigEditor` that returns `{ showAgentDetail }`.
32
+ Internally the factory closes over `fileOps`, `registry`, `personalAgentsDir`, and `projectAgentsDir`.
33
+
34
+ Three action handlers already exist as closure-level functions: `ejectAgent`, `disableAgent`, `enableAgent`.
35
+ The remaining three actions — Edit, Delete, and Reset to default — are inlined in `showAgentDetail`'s if/else dispatch chain.
36
+
37
+ The existing test suite (`test/ui/agent-config-editor.test.ts`, 18 tests) covers all menu-option combinations and action branches through the public `showAgentDetail` entry point.
38
+
39
+ ### Complexity sources
40
+
41
+ `showAgentDetail` (cognitive 33):
42
+
43
+ 1. Menu-option building — 4-branch if/else chain with 3 boolean conditions (`disabled`, `isDefault`, `file`).
44
+ 2. Action dispatch — 6-branch if/else chain based on `choice`.
45
+ 3. Inline Edit handler — 3 nested `if` guards (`file`, `content`, `edited !== content`).
46
+ 4. Inline Delete handler — nested `if (file)` + `if (confirmed)`.
47
+ 5. Inline Reset handler — nested `if (file)` + `if (confirmed)`.
48
+
49
+ `ejectAgent` (cognitive 20):
50
+
51
+ 1. Location selection + overwrite check — 2 early returns with nested ifs.
52
+ 2. Frontmatter field building — 14 conditional `if`/`else if` branches.
53
+
54
+ ## Design Overview
55
+
56
+ ### Extracted from `showAgentDetail`
57
+
58
+ #### `buildMenuOptions` (exported, pure)
59
+
60
+ ```typescript
61
+ export function buildMenuOptions(
62
+ cfg: { isDefault?: boolean; enabled?: boolean },
63
+ file: string | undefined,
64
+ ): string[]
65
+ ```
66
+
67
+ Accepts the minimal config shape and file path.
68
+ Returns the menu option array.
69
+ Pure computation — no IO, no side effects.
70
+ Exported for direct unit testing.
71
+
72
+ #### `handleEdit` (closure-internal)
73
+
74
+ Handles the Edit action: reads the file, opens the editor, writes if changed.
75
+ Signature: `(ui: MenuUI, name: string, file: string) => Promise<void>`.
76
+ Called only when `file` is defined (guaranteed by menu-option construction).
77
+
78
+ #### `handleDelete` (closure-internal)
79
+
80
+ Handles the Delete action: confirms with user, removes file, reloads registry.
81
+ Signature: `(ui: MenuUI, name: string, file: string) => Promise<void>`.
82
+
83
+ #### `handleReset` (closure-internal)
84
+
85
+ Handles the Reset to default action: confirms, removes override file, reloads registry.
86
+ Signature: `(ui: MenuUI, name: string, file: string) => Promise<void>`.
87
+
88
+ ### Extracted from `ejectAgent`
89
+
90
+ #### `buildEjectContent` (exported, pure)
91
+
92
+ ```typescript
93
+ export function buildEjectContent(cfg: AgentConfig): string
94
+ ```
95
+
96
+ Builds the full `.md` file content (frontmatter + system prompt) for an ejected agent.
97
+ Pure function — no IO.
98
+ Exported for direct unit testing.
99
+
100
+ ### After refactoring
101
+
102
+ `showAgentDetail` becomes a thin orchestrator (~15 lines):
103
+
104
+ ```typescript
105
+ async function showAgentDetail(ui: MenuUI, name: string) {
106
+ if (registry.resolveType(name) == null) {
107
+ ui.notify(`Agent config not found for "${name}".`, "warning");
108
+ return;
109
+ }
110
+ const cfg = registry.resolveAgentConfig(name);
111
+ const file = fileOps.findAgentFile(name, agentDirs());
112
+
113
+ const choice = await ui.select(name, buildMenuOptions(cfg, file));
114
+ if (!choice || choice === "Back") return;
115
+
116
+ if (choice === "Edit" && file) await handleEdit(ui, name, file);
117
+ else if (choice === "Delete" && file) await handleDelete(ui, name, file);
118
+ else if (choice === "Reset to default" && file) await handleReset(ui, name, file);
119
+ else if (choice.startsWith("Eject")) await ejectAgent(ui, name, cfg);
120
+ else if (choice === "Disable") await disableAgent(ui, name);
121
+ else if (choice === "Enable") await enableAgent(ui, name);
122
+ }
123
+ ```
124
+
125
+ Cognitive complexity: ~5 (one null-check early return + flat dispatch chain with no nesting).
126
+
127
+ `ejectAgent` becomes:
128
+
129
+ ```typescript
130
+ async function ejectAgent(ui: MenuUI, name: string, cfg: AgentConfig) {
131
+ const location = await ui.select("Choose location", [...]);
132
+ if (!location) return;
133
+ const targetDir = location.startsWith("Project") ? projectAgentsDir : personalAgentsDir;
134
+ const targetPath = join(targetDir, `${name}.md`);
135
+ if (fileOps.exists(targetPath)) {
136
+ const overwrite = await ui.confirm("Overwrite", `${targetPath} already exists. Overwrite?`);
137
+ if (!overwrite) return;
138
+ }
139
+ fileOps.write(targetPath, buildEjectContent(cfg));
140
+ registry.reload();
141
+ ui.notify(`Ejected ${name} to ${targetPath}`, "info");
142
+ }
143
+ ```
144
+
145
+ Cognitive complexity: ~6 (two early returns, one ternary, one nested if).
146
+
147
+ ## Module-Level Changes
148
+
149
+ ### Changed: `src/ui/agent-config-editor.ts`
150
+
151
+ - Add exported `buildMenuOptions(cfg, file)` — pure function extracted from `showAgentDetail`.
152
+ - Add exported `buildEjectContent(cfg)` — pure function extracted from `ejectAgent`.
153
+ - Add closure-internal `handleEdit(ui, name, file)` — extracted from `showAgentDetail` inline logic.
154
+ - Add closure-internal `handleDelete(ui, name, file)` — extracted from `showAgentDetail` inline logic.
155
+ - Add closure-internal `handleReset(ui, name, file)` — extracted from `showAgentDetail` inline logic.
156
+ - Simplify `showAgentDetail` to orchestrate: resolve → build menu → select → dispatch.
157
+ - Simplify `ejectAgent` to delegate frontmatter building to `buildEjectContent`.
158
+
159
+ No exports are removed or renamed.
160
+ The public API (`createAgentConfigEditor` returning `{ showAgentDetail }`) is unchanged.
161
+
162
+ ### Changed: `test/ui/agent-config-editor.test.ts`
163
+
164
+ - Add `describe("buildMenuOptions")` with tests for each menu-option combination (5 cases from existing tests, restructured as direct function calls).
165
+ - Add `describe("buildEjectContent")` with tests for minimal config and config with all optional fields.
166
+ - Existing `showAgentDetail` tests remain unchanged as integration coverage.
167
+
168
+ ### Changed: `docs/architecture/architecture.md`
169
+
170
+ - Update the complexity hotspots table: `showAgentDetail` drops from 25/33 to ~5/5; `ejectAgent` drops from 21/20 to ~6/6.
171
+
172
+ ## Test Impact Analysis
173
+
174
+ 1. **New tests enabled:** Direct unit tests for `buildMenuOptions` (pure function with 5 state combinations) and `buildEjectContent` (pure function with many optional fields).
175
+ These were previously impossible to test in isolation because the logic was embedded in async UI flows.
176
+ 2. **Existing tests that stay:** All 18 `showAgentDetail` tests remain as integration coverage — they exercise the full resolve → menu → dispatch → action pipeline.
177
+ 3. **No tests become redundant:** The existing menu-option-structure tests (5 tests) overlap with `buildMenuOptions` unit tests, but they remain valuable as integration tests verifying the full flow produces the correct menu.
178
+
179
+ ## TDD Order
180
+
181
+ 1. **Red → Green:** Add `buildMenuOptions` unit tests (5 cases: default no-file, default with-file, custom with-file, disabled-default with-file, disabled-custom with-file).
182
+ Export `buildMenuOptions` as a pure function.
183
+ Extract the menu-option computation from `showAgentDetail` into it.
184
+ Verify all existing tests pass.
185
+ Commit: `refactor: extract buildMenuOptions from showAgentDetail`
186
+
187
+ 2. **Red → Green:** Extract `handleEdit`, `handleDelete`, `handleReset` as closure-internal functions.
188
+ Simplify `showAgentDetail` dispatch to a flat if/else chain calling named handlers.
189
+ Verify all existing tests pass.
190
+ Commit: `refactor: extract inline handlers from showAgentDetail`
191
+
192
+ 3. **Red → Green:** Add `buildEjectContent` unit tests (minimal config, config with all optional fields, config with array extensions/skills).
193
+ Export `buildEjectContent` as a pure function.
194
+ Extract the frontmatter-building logic from `ejectAgent` into it.
195
+ Verify all existing tests pass.
196
+ Commit: `refactor: extract buildEjectContent from ejectAgent`
197
+
198
+ 4. **Docs:** Update the complexity hotspots table in `docs/architecture/architecture.md`.
199
+ Commit: `docs: update complexity table after showAgentDetail decomposition`
200
+
201
+ ## Risks and Mitigations
202
+
203
+ | Risk | Mitigation |
204
+ | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- |
205
+ | `buildMenuOptions` return order must match existing select mock expectations | Unit tests verify exact array equality; integration tests remain as safety net. |
206
+ | `buildEjectContent` frontmatter field ordering is load-bearing for eject tests | Unit tests verify the full content string; existing eject integration test uses `stringContaining` (flexible). |
207
+ | Closure-internal handlers share mutable `fileOps`/`registry` references | No change from current behavior — they already close over these references. |
208
+
209
+ ## Open Questions
210
+
211
+ None — the decomposition is mechanical extraction of existing code into named functions.
@@ -34,3 +34,31 @@ Full suite (856 tests, 54 files) is green; type check and lint are clean.
34
34
  - The `renderWidgetLines` `else` block removal also required Python because the Edit tool's `oldText` matching is unreliable when the target contains nested template literals with backticks.
35
35
  - Aside from the template-literal matching friction, all extractions were purely mechanical; no logic changes were needed.
36
36
  - The final `renderWidgetLines` is a clean 12-line orchestrator; each helper is well under complexity 10.
37
+
38
+ ## Stage: Final Retrospective (2026-05-25T15:41:48Z)
39
+
40
+ ### Session summary
41
+
42
+ All three stages (planning, TDD implementation, shipping) completed in a single session.
43
+ Four refactor commits extracted `categorizeAgents`, `buildSections`, `assembleWithinBudget`, and `assembleOverflow` from `renderWidgetLines`, reducing cognitive complexity from 44 to <10 per function.
44
+ Released as `pi-subagents-v7.2.6`.
45
+
46
+ ### Observations
47
+
48
+ #### What went well
49
+
50
+ - The planning-through-shipping pipeline was efficient: plan → 4 TDD steps → ship → release in one session with no rework.
51
+ - The plan correctly identified all four extraction targets and ordered TDD steps to avoid intermediate breakage.
52
+ - All 23 existing `widget-renderer.test.ts` tests passed throughout with zero modifications — the existing test coverage was at the right abstraction level for this refactoring.
53
+ - The `architecture.md` Phase 12 update (issue links, refactoring table row) was a clean opportunistic addition.
54
+
55
+ #### What caused friction (agent side)
56
+
57
+ - `other` (Edit tool limitation) — The Edit tool introduced a stray double-backtick when inserting `assembleOverflow`'s body, caused by JSON escaping colliding with nested template literal backticks.
58
+ The same limitation then prevented matching `oldText` containing nested template literals in the `renderWidgetLines` overflow block.
59
+ Required two Python-based line-level fixes via bash.
60
+ Impact: ~3 extra tool calls; self-identified and self-corrected.
61
+
62
+ #### What caused friction (user side)
63
+
64
+ - No friction observed.
@@ -0,0 +1,67 @@
1
+ ---
2
+ issue: 206
3
+ issue_title: "Decompose showAgentDetail (cognitive 33)"
4
+ ---
5
+
6
+ # Retro: #206 — Decompose showAgentDetail (cognitive 33)
7
+
8
+ ## Stage: Planning (2026-05-25T12:00:00Z)
9
+
10
+ ### Session summary
11
+
12
+ Produced a 4-step plan to decompose `showAgentDetail` (cognitive 33) and `ejectAgent` (cognitive 20) in `ui/agent-config-editor.ts`.
13
+ The plan extracts two exported pure functions (`buildMenuOptions`, `buildEjectContent`) with dedicated unit tests, plus three closure-internal handlers (`handleEdit`, `handleDelete`, `handleReset`).
14
+
15
+ ### Observations
16
+
17
+ - Three of the six action handlers (`ejectAgent`, `disableAgent`, `enableAgent`) were already extracted as closure functions — only Edit, Delete, and Reset were inlined in the dispatch chain.
18
+ - `buildMenuOptions` and `buildEjectContent` are ideal pure-function extractions: complex branching logic with no IO dependencies, previously untestable in isolation.
19
+ - The existing 18 integration tests through `showAgentDetail` provide a strong safety net — no risk of behavior regression during extraction.
20
+ - Chose to scope `ejectAgent` decomposition into this issue since the issue's outcome says "< 10 per function" and `ejectAgent` is at cognitive 20 in the same file.
21
+ - `disableAgent` and `enableAgent` were explicitly deferred — their cognitive complexity is manageable and decomposing them would add scope without meaningful benefit.
22
+
23
+ ## Stage: Implementation — TDD (2026-05-25T11:55:00Z)
24
+
25
+ ### Session summary
26
+
27
+ Completed all 4 TDD steps. 3 `refactor:` commits extract `buildMenuOptions`, the three inline handlers, and `buildEjectContent`; 1 `docs:` commit updates the architecture table.
28
+ Test count grew from 21 to 33 (+12 new unit tests for the two exported pure functions).
29
+
30
+ ### Observations
31
+
32
+ - A `newText: null` bug in the Edit tool corrupted `agent-config-editor.ts` during step 1; recovered immediately by rewriting the file with `Write`.
33
+ - The test used `thinking: "auto"` which is not a valid `ThinkingLevel` — fixed by changing to `"low"` before the final commit; the type error was caught by `pnpm run check` after the TDD step.
34
+ - `buildMenuOptions` extracted cleanly with early-return style (no `let menuOptions` intermediate); the refactored function passes all 5 new unit tests and all 21 existing integration tests.
35
+ - `handleEdit`, `handleDelete`, and `handleReset` are closure-internal; they drop the outer `if (file)` guard since the menu only shows those options when `file` is defined.
36
+ - `buildEjectContent` extracted from `ejectAgent` reduces `ejectAgent` to a thin IO function (~10 lines); no behavior change verified by the existing eject integration tests.
37
+
38
+ ## Stage: Final Retrospective (2026-05-25T12:10:00Z)
39
+
40
+ ### Session summary
41
+
42
+ Completed full lifecycle — Planning, TDD (3 `refactor:` + 1 `docs:` commits, +12 tests), Ship, and Release (`pi-subagents-v7.2.7`) — in a single session with no user corrections.
43
+
44
+ ### Observations
45
+
46
+ #### What went well
47
+
48
+ - The three-stage workflow (plan → TDD → ship) executed without any user intervention between stages.
49
+ Each stage's retro notes provided clean context for the next.
50
+ - Pure-function extraction pattern worked cleanly: `buildMenuOptions` and `buildEjectContent` exported for unit testing; `handleEdit`, `handleDelete`, `handleReset` kept as closure-internal, tested via existing integration tests.
51
+ - The existing 21 integration tests caught no regressions across all 3 refactor commits — strong safety net for mechanical extraction.
52
+
53
+ #### What caused friction (agent side)
54
+
55
+ - `other` — Passed `newText: null` to the `Edit` tool during TDD step 1, injecting a literal `null` into `agent-config-editor.ts` and corrupting the file.
56
+ Self-identified immediately via Biome autoformat failure.
57
+ Impact: one wasted tool round-trip; recovered by rewriting the file with `Write`.
58
+ - `missing-context` — Used `thinking: "auto"` in a `buildEjectContent` test fixture without checking `ThinkingLevel` is `"minimal" | "low" | "medium" | "high" | "xhigh"`.
59
+ Self-identified by `pnpm run check` post-TDD gate.
60
+ Impact: one `--amend` fix, no extra commit.
61
+ - `missing-context` — Plan stated "18 tests" as the baseline count; actual baseline was 21.
62
+ Impact: none — just an inaccurate number in the plan text.
63
+
64
+ #### What caused friction (user side)
65
+
66
+ - No user-side friction observed.
67
+ The user triggered each stage sequentially without needing to redirect or correct.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-subagents",
3
- "version": "7.2.6",
3
+ "version": "7.2.8",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/service.ts"
@@ -74,12 +74,13 @@ You are operating as a sub-agent invoked to handle a specific task.
74
74
  : "";
75
75
 
76
76
  // Place shared/stable content first so the LLM's KV cache can reuse the
77
- // inherited prefix across all subagent invocations. The <active_agent> tag
78
- // and env block vary per call and are placed after the cacheable prefix.
77
+ // inherited prefix across all subagent invocations. The parent prompt is
78
+ // placed verbatim (no wrapper tag) so it forms an identical byte prefix
79
+ // with the parent session, maximising KV cache hits. The <active_agent>
80
+ // tag and env block vary per call and are placed after the cached prefix.
79
81
  return (
80
- "<inherited_system_prompt>\n" +
81
82
  identity +
82
- "\n</inherited_system_prompt>\n\n" +
83
+ "\n\n" +
83
84
  bridge +
84
85
  "\n\n" +
85
86
  activeAgentTag +
@@ -12,6 +12,55 @@ import type { AgentConfig } from "#src/types";
12
12
  import type { AgentFileOps } from "#src/ui/agent-file-ops";
13
13
  import type { MenuUI } from "#src/ui/agent-menu";
14
14
 
15
+ // ---- Pure helpers ----
16
+
17
+ /** Compute the menu option list for the agent detail view. */
18
+ export function buildMenuOptions(
19
+ cfg: { isDefault?: boolean; enabled?: boolean },
20
+ file: string | undefined,
21
+ ): string[] {
22
+ const isDefault = cfg.isDefault === true;
23
+ const disabled = cfg.enabled === false;
24
+
25
+ if (disabled && file) {
26
+ return isDefault
27
+ ? ["Enable", "Edit", "Reset to default", "Delete", "Back"]
28
+ : ["Enable", "Edit", "Delete", "Back"];
29
+ }
30
+ if (isDefault && !file) {
31
+ return ["Eject (export as .md)", "Disable", "Back"];
32
+ }
33
+ if (isDefault && file) {
34
+ return ["Edit", "Disable", "Reset to default", "Delete", "Back"];
35
+ }
36
+ return ["Edit", "Disable", "Delete", "Back"];
37
+ }
38
+
39
+ /** Build the `.md` file content (frontmatter + system prompt) for an ejected agent. */
40
+ export function buildEjectContent(cfg: AgentConfig): string {
41
+ const fmFields: string[] = [];
42
+ fmFields.push(`description: ${cfg.description}`);
43
+ if (cfg.displayName) fmFields.push(`display_name: ${cfg.displayName}`);
44
+ fmFields.push(`tools: ${cfg.builtinToolNames?.join(", ") ?? "all"}`);
45
+ if (cfg.model) fmFields.push(`model: ${cfg.model}`);
46
+ if (cfg.thinking) fmFields.push(`thinking: ${cfg.thinking}`);
47
+ if (cfg.maxTurns) fmFields.push(`max_turns: ${cfg.maxTurns}`);
48
+ fmFields.push(`prompt_mode: ${cfg.promptMode}`);
49
+ if (cfg.extensions === false) fmFields.push("extensions: false");
50
+ else if (Array.isArray(cfg.extensions))
51
+ fmFields.push(`extensions: ${cfg.extensions.join(", ")}`);
52
+ if (cfg.skills === false) fmFields.push("skills: false");
53
+ else if (Array.isArray(cfg.skills))
54
+ fmFields.push(`skills: ${cfg.skills.join(", ")}`);
55
+ if (cfg.disallowedTools?.length)
56
+ fmFields.push(`disallowed_tools: ${cfg.disallowedTools.join(", ")}`);
57
+ if (cfg.inheritContext) fmFields.push("inherit_context: true");
58
+ if (cfg.runInBackground) fmFields.push("run_in_background: true");
59
+ if (cfg.isolated) fmFields.push("isolated: true");
60
+ if (cfg.isolation) fmFields.push(`isolation: ${cfg.isolation}`);
61
+ return `---\n${fmFields.join("\n")}\n---\n\n${cfg.systemPrompt}\n`;
62
+ }
63
+
15
64
  // ---- Factory ----
16
65
 
17
66
  export function createAgentConfigEditor(
@@ -30,65 +79,52 @@ export function createAgentConfigEditor(
30
79
  return;
31
80
  }
32
81
  const cfg = registry.resolveAgentConfig(name);
33
-
34
82
  const file = fileOps.findAgentFile(name, agentDirs());
35
- const isDefault = cfg.isDefault === true;
36
- const disabled = cfg.enabled === false;
37
-
38
- let menuOptions: string[];
39
- if (disabled && file) {
40
- menuOptions = isDefault
41
- ? ["Enable", "Edit", "Reset to default", "Delete", "Back"]
42
- : ["Enable", "Edit", "Delete", "Back"];
43
- } else if (isDefault && !file) {
44
- menuOptions = ["Eject (export as .md)", "Disable", "Back"];
45
- } else if (isDefault && file) {
46
- menuOptions = ["Edit", "Disable", "Reset to default", "Delete", "Back"];
47
- } else {
48
- menuOptions = ["Edit", "Disable", "Delete", "Back"];
49
- }
50
83
 
51
- const choice = await ui.select(name, menuOptions);
84
+ const choice = await ui.select(name, buildMenuOptions(cfg, file));
52
85
  if (!choice || choice === "Back") return;
53
86
 
54
- if (choice === "Edit" && file) {
55
- const content = fileOps.read(file);
56
- if (content !== undefined) {
57
- const edited = await ui.editor(`Edit ${name}`, content);
58
- if (edited !== undefined && edited !== content) {
59
- fileOps.write(file, edited);
60
- registry.reload();
61
- ui.notify(`Updated ${file}`, "info");
62
- }
63
- }
64
- } else if (choice === "Delete") {
65
- if (file) {
66
- const confirmed = await ui.confirm(
67
- "Delete agent",
68
- `Delete ${name} (${file})?`,
69
- );
70
- if (confirmed) {
71
- fileOps.remove(file);
72
- registry.reload();
73
- ui.notify(`Deleted ${file}`, "info");
74
- }
75
- }
76
- } else if (choice === "Reset to default" && file) {
77
- const confirmed = await ui.confirm(
78
- "Reset to default",
79
- `Delete override ${file} and restore embedded default?`,
80
- );
81
- if (confirmed) {
82
- fileOps.remove(file);
83
- registry.reload();
84
- ui.notify(`Restored default ${name}`, "info");
85
- }
86
- } else if (choice.startsWith("Eject")) {
87
- await ejectAgent(ui, name, cfg);
88
- } else if (choice === "Disable") {
89
- await disableAgent(ui, name);
90
- } else if (choice === "Enable") {
91
- await enableAgent(ui, name);
87
+ if (choice === "Edit" && file) await handleEdit(ui, name, file);
88
+ else if (choice === "Delete" && file) await handleDelete(ui, name, file);
89
+ else if (choice === "Reset to default" && file)
90
+ await handleReset(ui, name, file);
91
+ else if (choice.startsWith("Eject")) await ejectAgent(ui, name, cfg);
92
+ else if (choice === "Disable") await disableAgent(ui, name);
93
+ else if (choice === "Enable") await enableAgent(ui, name);
94
+ }
95
+
96
+ async function handleEdit(ui: MenuUI, name: string, file: string) {
97
+ const content = fileOps.read(file);
98
+ if (content === undefined) return;
99
+ const edited = await ui.editor(`Edit ${name}`, content);
100
+ if (edited !== undefined && edited !== content) {
101
+ fileOps.write(file, edited);
102
+ registry.reload();
103
+ ui.notify(`Updated ${file}`, "info");
104
+ }
105
+ }
106
+
107
+ async function handleDelete(ui: MenuUI, name: string, file: string) {
108
+ const confirmed = await ui.confirm(
109
+ "Delete agent",
110
+ `Delete ${name} (${file})?`,
111
+ );
112
+ if (confirmed) {
113
+ fileOps.remove(file);
114
+ registry.reload();
115
+ ui.notify(`Deleted ${file}`, "info");
116
+ }
117
+ }
118
+
119
+ async function handleReset(ui: MenuUI, name: string, file: string) {
120
+ const confirmed = await ui.confirm(
121
+ "Reset to default",
122
+ `Delete override ${file} and restore embedded default?`,
123
+ );
124
+ if (confirmed) {
125
+ fileOps.remove(file);
126
+ registry.reload();
127
+ ui.notify(`Restored default ${name}`, "info");
92
128
  }
93
129
  }
94
130
 
@@ -112,30 +148,7 @@ export function createAgentConfigEditor(
112
148
  if (!overwrite) return;
113
149
  }
114
150
 
115
- const fmFields: string[] = [];
116
- fmFields.push(`description: ${cfg.description}`);
117
- if (cfg.displayName) fmFields.push(`display_name: ${cfg.displayName}`);
118
- fmFields.push(`tools: ${cfg.builtinToolNames?.join(", ") ?? "all"}`);
119
- if (cfg.model) fmFields.push(`model: ${cfg.model}`);
120
- if (cfg.thinking) fmFields.push(`thinking: ${cfg.thinking}`);
121
- if (cfg.maxTurns) fmFields.push(`max_turns: ${cfg.maxTurns}`);
122
- fmFields.push(`prompt_mode: ${cfg.promptMode}`);
123
- if (cfg.extensions === false) fmFields.push("extensions: false");
124
- else if (Array.isArray(cfg.extensions))
125
- fmFields.push(`extensions: ${cfg.extensions.join(", ")}`);
126
- if (cfg.skills === false) fmFields.push("skills: false");
127
- else if (Array.isArray(cfg.skills))
128
- fmFields.push(`skills: ${cfg.skills.join(", ")}`);
129
- if (cfg.disallowedTools?.length)
130
- fmFields.push(`disallowed_tools: ${cfg.disallowedTools.join(", ")}`);
131
- if (cfg.inheritContext) fmFields.push("inherit_context: true");
132
- if (cfg.runInBackground) fmFields.push("run_in_background: true");
133
- if (cfg.isolated) fmFields.push("isolated: true");
134
- if (cfg.isolation) fmFields.push(`isolation: ${cfg.isolation}`);
135
-
136
- const content = `---\n${fmFields.join("\n")}\n---\n\n${cfg.systemPrompt}\n`;
137
-
138
- fileOps.write(targetPath, content);
151
+ fileOps.write(targetPath, buildEjectContent(cfg));
139
152
  registry.reload();
140
153
  ui.notify(`Ejected ${name} to ${targetPath}`, "info");
141
154
  }