@gotgenes/pi-subagents 7.2.4 → 7.2.6

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.
@@ -0,0 +1,140 @@
1
+ ---
2
+ issue: 205
3
+ issue_title: "Decompose renderWidgetLines (cognitive 44)"
4
+ ---
5
+
6
+ # Decompose `renderWidgetLines`
7
+
8
+ ## Problem Statement
9
+
10
+ `renderWidgetLines` in `ui/widget-renderer.ts` has cognitive complexity 44 (CRITICAL per fallow health).
11
+ It handles agent categorization, per-status line building with tree connectors, non-overflow assembly with last-connector fixup, and overflow-budget assembly — all in a single 106-line function.
12
+ This is the highest-complexity function remaining in the codebase (Phase 12, Step 1).
13
+
14
+ ## Goals
15
+
16
+ - Extract distinct concerns into separate pure functions, each with cognitive complexity < 10.
17
+ - Preserve all existing behavior — no visual or behavioral changes.
18
+ - Keep all extracted functions in `widget-renderer.ts` (they are private helpers, not a separate module).
19
+
20
+ ## Non-Goals
21
+
22
+ - Decomposing `showAgentDetail` (#206), `update` (#207), or shared test fixtures (#208) — those are sibling Phase 12 steps.
23
+ - Changing the widget's visual output or tree-connector style.
24
+ - Modifying `renderFinishedLine` or `renderRunningLines` — those are already single-concern functions.
25
+
26
+ ## Background
27
+
28
+ `widget-renderer.ts` was extracted from `AgentWidget` in #148.
29
+ The per-agent renderers (`renderFinishedLine`, `renderRunningLines`) are already clean single-concern functions.
30
+ The remaining complexity lives entirely in `renderWidgetLines`, which orchestrates categorization, section building, and assembly.
31
+
32
+ The function has five interwoven concerns:
33
+
34
+ 1. **Agent categorization** — filtering into running/queued/finished buckets.
35
+ 2. **Section building** — rendering each bucket into pre-formatted line arrays with `├─` tree connectors.
36
+ 3. **Heading construction** — choosing icon/color based on active vs. finished-only.
37
+ 4. **Non-overflow assembly** — concatenating sections when under `MAX_WIDGET_LINES`, then fixing the last connector (`├─` → `└─`).
38
+ 5. **Overflow assembly** — budget-based prioritized assembly (running > queued > finished) with an overflow indicator line.
39
+
40
+ ## Design Overview
41
+
42
+ Extract four helper functions from the body of `renderWidgetLines`:
43
+
44
+ ### `categorizeAgents`
45
+
46
+ Accepts the agents array and `shouldShowFinished` callback.
47
+ Returns `{ running, queued, finished }` arrays.
48
+ Pure filter — no rendering.
49
+
50
+ ### `buildSections`
51
+
52
+ Accepts categorized agents, `activityMap`, `registry`, `spinnerFrame`, `theme`, and a `truncate` function.
53
+ Returns `{ finishedLines, runningLines, queuedLine }` — the pre-formatted line arrays with `├─` connectors.
54
+ Calls `renderFinishedLine` and `renderRunningLines` internally.
55
+
56
+ ### `assembleWithinBudget`
57
+
58
+ Accepts `finishedLines`, `runningLines`, `queuedLine`, and the heading line.
59
+ Handles the non-overflow path: concatenates sections and fixes the last tree connector (`├─` → `└─`, `│` → space).
60
+ Returns the assembled `string[]`.
61
+
62
+ ### `assembleOverflow`
63
+
64
+ Accepts `finishedLines`, `runningLines`, `queuedLine`, heading line, `maxBody` budget, `truncate`, and `theme`.
65
+ Handles the overflow path: budget-based prioritized assembly with an overflow indicator.
66
+ Returns the assembled `string[]`.
67
+
68
+ After extraction, `renderWidgetLines` becomes a thin orchestrator:
69
+
70
+ ```typescript
71
+ export function renderWidgetLines(params: { ... }): string[] {
72
+ const { running, queued, finished } = categorizeAgents(agents, shouldShowFinished);
73
+ if (running.length === 0 && queued.length === 0 && finished.length === 0) return [];
74
+
75
+ const truncate = (line: string) => truncateToWidth(line, terminalWidth);
76
+ const heading = buildHeadingLine(running, queued, truncate, theme);
77
+ const sections = buildSections(running, queued, finished, activityMap, registry, spinnerFrame, theme, truncate);
78
+ const totalBody = sections.finishedLines.length + sections.runningLines.length * 2 + (sections.queuedLine ? 1 : 0);
79
+
80
+ if (totalBody <= MAX_WIDGET_LINES - 1) {
81
+ return assembleWithinBudget(heading, sections);
82
+ }
83
+ return assembleOverflow(heading, sections, MAX_WIDGET_LINES - 1, truncate, theme);
84
+ }
85
+ ```
86
+
87
+ Each helper is a pure function with a single concern and low branching.
88
+
89
+ ## Module-Level Changes
90
+
91
+ ### Changed: `src/ui/widget-renderer.ts`
92
+
93
+ - Add `categorizeAgents` (private) — extracts the three `agents.filter(...)` calls.
94
+ - Add `buildSections` (private) — extracts the three section-building loops.
95
+ - Add `assembleWithinBudget` (private) — extracts the non-overflow assembly + connector fixup.
96
+ - Add `assembleOverflow` (private) — extracts the overflow-budget assembly + indicator line.
97
+ - Simplify `renderWidgetLines` to a thin orchestrator calling the four helpers.
98
+
99
+ No exports are added, removed, or renamed.
100
+ No other files change.
101
+
102
+ ## Test Impact Analysis
103
+
104
+ 1. No new unit tests for the extracted helpers are needed — they are private functions tested through `renderWidgetLines`.
105
+ The existing `renderWidgetLines` tests in `test/widget-renderer.test.ts` (8 tests) cover all branches: single running, mixed agents, filtered finished, overflow priority, empty arrays, dim heading.
106
+ 2. No existing tests become redundant — they all exercise `renderWidgetLines` end-to-end, which is the correct level for assembly logic.
107
+ 3. All existing tests must stay as-is — the extraction is purely internal.
108
+
109
+ ## TDD Order
110
+
111
+ 1. **Red → Green:** Extract `categorizeAgents` and call it from `renderWidgetLines`.
112
+ All existing tests pass (no behavior change).
113
+ Commit: `refactor: extract categorizeAgents from renderWidgetLines`
114
+
115
+ 2. **Red → Green:** Extract `buildSections` and call it from `renderWidgetLines`.
116
+ All existing tests pass.
117
+ Commit: `refactor: extract buildSections from renderWidgetLines`
118
+
119
+ 3. **Red → Green:** Extract `assembleWithinBudget` and call it from `renderWidgetLines`.
120
+ All existing tests pass.
121
+ Commit: `refactor: extract assembleWithinBudget from renderWidgetLines`
122
+
123
+ 4. **Red → Green:** Extract `assembleOverflow` and call it from `renderWidgetLines`.
124
+ All existing tests pass.
125
+ Commit: `refactor: extract assembleOverflow from renderWidgetLines`
126
+
127
+ 5. **Verify:** Run `pnpm run check` and `pnpm vitest run test/widget-renderer.test.ts` to confirm no regressions.
128
+ Commit: n/a (verification only).
129
+
130
+ ## Risks and Mitigations
131
+
132
+ | Risk | Mitigation |
133
+ | --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
134
+ | Tree-connector fixup logic is fragile (string replacement on Unicode chars) | Keep the fixup in `assembleWithinBudget` as-is — same logic, just relocated. Existing tests verify exact connector output. |
135
+ | Extracted helpers have many parameters | Accept a `sections` object from `buildSections` to bundle `finishedLines`, `runningLines`, `queuedLine` — avoids long parameter lists. |
136
+ | Intermediate commits break tests | Each extraction step is self-contained — the function body moves into a helper and the call site replaces it in the same commit. |
137
+
138
+ ## Open Questions
139
+
140
+ None — the decomposition is purely mechanical extraction of existing code into named helpers.
@@ -0,0 +1,73 @@
1
+ ---
2
+ issue: 196
3
+ issue_title: "Convert AgentRunner and AgentsMenuHandler to classes, simplify index.ts"
4
+ ---
5
+
6
+ # Retro: #196 — Convert AgentRunner and AgentsMenuHandler to classes, simplify index.ts
7
+
8
+ ## Stage: Planning (2026-05-25T14:35:46Z)
9
+
10
+ ### Session summary
11
+
12
+ Produced a 6-step TDD plan covering the final two closure-factory-to-class conversions (`createAgentRunner` → `ConcreteAgentRunner`, `createAgentsMenuHandler` → `AgentsMenuHandler`) and the subsequent `index.ts` simplification.
13
+ Confirmed that `AgentManager` structurally satisfies `AgentMenuManager`, enabling direct pass-through without adapter closures.
14
+
15
+ ### Observations
16
+
17
+ - The issue's proposed `AgentsMenuHandler` constructor omits `agentActivity`, but the class needs it for `viewAgentConversation`.
18
+ Plan includes it as a constructor param — minimal deviation from the issue.
19
+ - `getModelLabel` can be internalized into `AgentsMenuHandler` since it only uses two pure imported functions (`resolveModel`, `getModelLabelFromConfig`) plus the registry (already a constructor param).
20
+ This eliminates a 7-line closure from `index.ts`.
21
+ - Tests for `agent-runner` call `runAgent`/`resumeAgent` directly — no test uses `createAgentRunner`, so the runner conversion has zero test impact.
22
+ - The `agent-menu.test.ts` file is 215 lines and needs call-site updates (factory → class constructor + `.handle()`), but no logic changes.
23
+ - After both conversions, `index.ts` loses ~5 imports and ~4 adapter closures.
24
+ The remaining ~15 closures are structural (event registrations, SDK factory callbacks) and cannot be eliminated.
25
+
26
+ ## Stage: Implementation — TDD (2026-05-25T14:56:11Z)
27
+
28
+ ### Session summary
29
+
30
+ Completed all 6 plan steps (collapsed into 5 commits: steps 3 and 5 merged).
31
+ Baseline was 854 tests across 53 files; final suite is 856 tests across 54 files (+2 tests, +1 file for `concrete-agent-runner.test.ts`).
32
+ All type-check, lint, and dead-code gates pass clean.
33
+
34
+ ### Observations
35
+
36
+ - Plan steps 3 and 5 had to be merged into a single commit: removing `createAgentsMenuHandler` immediately broke `index.ts` imports, so the `index.ts` update could not wait for a separate commit.
37
+ This is a known coupling when a factory's only call site is in `index.ts`.
38
+ - The `AgentsMenuHandler` class constructor includes `agentActivity` as planned (the issue's proposed signature omitted it; the plan's deviation was correct).
39
+ - `getModelLabel` internalization was clean: `resolveModel` and `getModelLabelFromConfig` are pure functions the class imports directly.
40
+ - `AgentManager` structurally satisfies `AgentMenuManager` with no adapter closures — confirmed by `pnpm run check` passing immediately.
41
+ - The `agent-menu.test.ts` refactor replaced `Partial<AgentMenuDeps>` overrides with a `makeHandler(opts)` helper that returns both the handler and collaborator stubs, which is cleaner for assertion.
42
+ - `rumdl` emitted 3 warnings in `pnpm run lint` — these are pre-existing and unrelated to this change (lint passes for markdown linting, the warnings are from biome/eslint steps that auto-fixed nothing).
43
+
44
+ ## Stage: Final Retrospective (2026-05-25T15:04:47Z)
45
+
46
+ ### Session summary
47
+
48
+ Completed all three stages (planning, TDD, shipping) in one sitting.
49
+ Issue #196 shipped as `pi-subagents-v7.2.5`.
50
+ All closure factories in pi-subagents are now classes; Phase 11 (Layers 3 + 4) is complete.
51
+
52
+ ### Observations
53
+
54
+ #### What went well
55
+
56
+ - The three-session lifecycle (plan → TDD → ship) completed cleanly in a single sitting with no user corrections.
57
+ - Structural typing confirmation during planning paid off — `AgentManager` satisfied `AgentMenuManager` without adapter closures, and `pnpm run check` passed immediately after the wiring change.
58
+ - The `makeHandler(opts)` test helper pattern (returning handler + collaborator stubs) was cleaner than the `Partial<AgentMenuDeps>` spread approach it replaced.
59
+
60
+ #### What caused friction (agent side)
61
+
62
+ - `wrong-abstraction` — The plan separated factory removal (step 3) from call-site update (step 5), even though the testing skill already has a rule: "When a TDD step changes an interface that has a single call site, the step must include updating that call site."
63
+ The planner treated this as a testing concern and didn't apply it during plan authoring.
64
+ Impact: steps 3 and 5 had to be merged at implementation time, producing a commit message explaining the deviation.
65
+ Added a cross-reference to `plan-issue.md`.
66
+
67
+ #### What caused friction (user side)
68
+
69
+ Nothing notable — the user's issue was well-specified and the three `/` commands ran without intervention.
70
+
71
+ ### Changes made
72
+
73
+ 1. Added single-call-site rule to `.pi/prompts/plan-issue.md` TDD Order section: when a step removes a factory/export with one call site, include the call-site update in the same step.
@@ -0,0 +1,36 @@
1
+ ---
2
+ issue: 205
3
+ issue_title: "Decompose renderWidgetLines (cognitive 44)"
4
+ ---
5
+
6
+ # Retro: #205 — Decompose renderWidgetLines
7
+
8
+ ## Stage: Planning (2026-05-25T15:27:45Z)
9
+
10
+ ### Session summary
11
+
12
+ Planned the decomposition of `renderWidgetLines` (cognitive complexity 44) into four private helper functions: `categorizeAgents`, `buildSections`, `assembleWithinBudget`, and `assembleOverflow`.
13
+ Also updated `architecture.md` Phase 12 steps with issue links (#205–#208) and added a Phase 12 row to the structural refactoring issues table.
14
+
15
+ ### Observations
16
+
17
+ - The function's complexity comes from five interwoven concerns (categorization, section building, heading, non-overflow assembly with connector fixup, overflow-budget assembly) — but the extraction is mechanical since all logic is already pure and stateless.
18
+ - No new tests are needed — the existing 8 tests in `widget-renderer.test.ts` cover all branches end-to-end and remain the correct test level for assembly logic.
19
+ - The tree-connector fixup (swapping `├─` → `└─` via string replacement on Unicode chars) is the most fragile part; it stays as-is inside `assembleWithinBudget` rather than being further decomposed.
20
+ - A `sections` return object from `buildSections` bundles `finishedLines`, `runningLines`, and `queuedLine` to avoid long parameter lists on the assembly helpers.
21
+
22
+ ## Stage: Implementation — TDD (2026-05-25T15:35:10Z)
23
+
24
+ ### Session summary
25
+
26
+ Completed all four TDD steps: extracted `categorizeAgents`, `buildSections`, `assembleWithinBudget`, and `assembleOverflow` from `renderWidgetLines`.
27
+ All 23 `widget-renderer.test.ts` tests pass throughout; no new tests were added.
28
+ Full suite (856 tests, 54 files) is green; type check and lint are clean.
29
+
30
+ ### Observations
31
+
32
+ - **Stray backtick during `assembleOverflow` extraction:** The Edit tool introduced a double-backtick (`\`\``) at the end of the nested template literal on the overflow indicator line — the inner template literal's closing backtick concatenated with the outer template's closing backtick, creating a parse error.
33
+ Required Python-based line-level surgery to fix since the Edit tool cannot reliably match nested template literals through JSON escaping.
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
+ - Aside from the template-literal matching friction, all extractions were purely mechanical; no logic changes were needed.
36
+ - The final `renderWidgetLines` is a clean 12-line orchestrator; each helper is well under complexity 10.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-subagents",
3
- "version": "7.2.4",
3
+ "version": "7.2.6",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/service.ts"
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-argument -- Pi SDK types are not fully exported; see upstream Pi SDK for type improvements */
1
+ /* eslint-disable @typescript-eslint/no-unsafe-argument -- Pi SDK types are not fully exported; see upstream Pi SDK for type improvements */
2
2
  /**
3
3
  * pi-agents — A pi extension providing Claude Code-style autonomous sub-agents.
4
4
  *
@@ -24,7 +24,7 @@ import { AgentTypeRegistry } from "#src/config/agent-types";
24
24
  import { loadCustomAgents } from "#src/config/custom-agents";
25
25
  import { SessionLifecycleHandler, ToolStartHandler } from "#src/handlers/index";
26
26
  import { AgentManager, type AgentManagerObserver } from "#src/lifecycle/agent-manager";
27
- import { createAgentRunner, type RunnerIO } from "#src/lifecycle/agent-runner";
27
+ import { ConcreteAgentRunner, type RunnerIO } from "#src/lifecycle/agent-runner";
28
28
  import { buildParentSnapshot } from "#src/lifecycle/parent-snapshot";
29
29
  import { GitWorktreeManager } from "#src/lifecycle/worktree";
30
30
  import { buildEventData, type NotificationDetails, NotificationManager } from "#src/observation/notification";
@@ -41,10 +41,9 @@ import { preloadSkills } from "#src/session/skill-loader";
41
41
  import { SettingsManager } from "#src/settings";
42
42
  import { AgentTool } from "#src/tools/agent-tool";
43
43
  import { GetResultTool } from "#src/tools/get-result-tool";
44
- import { getModelLabelFromConfig } from "#src/tools/helpers";
45
44
  import { SteerTool } from "#src/tools/steer-tool";
46
45
  import { FsAgentFileOps } from "#src/ui/agent-file-ops";
47
- import { createAgentsMenuHandler } from "#src/ui/agent-menu";
46
+ import { AgentsMenuHandler } from "#src/ui/agent-menu";
48
47
  import { AgentWidget } from "#src/ui/agent-widget";
49
48
 
50
49
  export default function (pi: ExtensionAPI) {
@@ -147,7 +146,7 @@ export default function (pi: ExtensionAPI) {
147
146
  };
148
147
 
149
148
  const manager = new AgentManager({
150
- runner: createAgentRunner(runnerIO),
149
+ runner: new ConcreteAgentRunner(runnerIO),
151
150
  worktrees: new GitWorktreeManager(process.cwd()),
152
151
  exec: (cmd, args, opts) => pi.exec(cmd, args, opts),
153
152
  registry,
@@ -193,33 +192,20 @@ export default function (pi: ExtensionAPI) {
193
192
 
194
193
  // ---- /agents interactive menu ----
195
194
 
196
- const agentsMenuHandler = createAgentsMenuHandler({
197
- manager: {
198
- listAgents: () => manager.listAgents(),
199
- getRecord: (id) => manager.getRecord(id),
200
- spawnAndWait: (snapshot, type, prompt, opts) => manager.spawnAndWait(snapshot, type, prompt, opts),
201
- },
195
+ const agentsMenu = new AgentsMenuHandler(
196
+ manager,
202
197
  registry,
203
- agentActivity: runtime.agentActivity,
204
- getModelLabel: (type, modelRegistry) => {
205
- const cfg = registry.resolveAgentConfig(type);
206
- if (!cfg.model) return 'inherit';
207
- if (modelRegistry) {
208
- const resolved = resolveModel(cfg.model, modelRegistry);
209
- if (typeof resolved === 'string') return 'inherit';
210
- }
211
- return getModelLabelFromConfig(cfg.model);
212
- },
198
+ runtime.agentActivity,
213
199
  settings,
214
- fileOps: new FsAgentFileOps(),
215
- personalAgentsDir: join(getAgentDir(), 'agents'),
216
- projectAgentsDir: join(process.cwd(), '.pi', 'agents'),
217
- });
200
+ new FsAgentFileOps(),
201
+ join(getAgentDir(), "agents"),
202
+ join(process.cwd(), ".pi", "agents"),
203
+ );
218
204
 
219
- pi.registerCommand('agents', {
220
- description: 'Manage agents',
205
+ pi.registerCommand("agents", {
206
+ description: "Manage agents",
221
207
  handler: async (_args, ctx) => {
222
- await agentsMenuHandler({
208
+ await agentsMenu.handle({
223
209
  ui: ctx.ui,
224
210
  modelRegistry: ctx.modelRegistry,
225
211
  parentSnapshot: buildParentSnapshot(ctx),
@@ -202,17 +202,23 @@ export interface AgentRunner {
202
202
  }
203
203
 
204
204
  /**
205
- * Create an AgentRunner backed by the given IO boundary.
205
+ * Concrete AgentRunner backed by a RunnerIO boundary.
206
206
  *
207
207
  * Captures io at construction time so AgentManager remains IO-unaware.
208
208
  */
209
- export function createAgentRunner(io: RunnerIO): AgentRunner {
210
- return {
211
- run: (snapshot, type, prompt, options) => runAgent(snapshot, type, prompt, options, io),
212
- resume: resumeAgent,
213
- };
209
+ export class ConcreteAgentRunner implements AgentRunner {
210
+ constructor(private readonly io: RunnerIO) {}
211
+
212
+ run(snapshot: ParentSnapshot, type: SubagentType, prompt: string, options: RunOptions): Promise<RunResult> {
213
+ return runAgent(snapshot, type, prompt, options, this.io);
214
+ }
215
+
216
+ resume(session: AgentSession, prompt: string, options?: ResumeOptions): Promise<string> {
217
+ return resumeAgent(session, prompt, options);
218
+ }
214
219
  }
215
220
 
221
+
216
222
  // ── Private helpers ───────────────────────────────────────────────────────────
217
223
 
218
224
  /**