@gotgenes/pi-subagents 6.16.2 → 6.17.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,28 @@ 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.17.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.16.3...pi-subagents-v6.17.0) (2026-05-23)
9
+
10
+
11
+ ### Features
12
+
13
+ * inject wrapText into ConversationViewer (Phase 9, Step O) ([#147](https://github.com/gotgenes/pi-packages/issues/147)) ([2522d5b](https://github.com/gotgenes/pi-packages/commit/2522d5b82f2a29b9011f31d49f82b5f914f12f99))
14
+
15
+
16
+ ### Documentation
17
+
18
+ * mark Step O complete in architecture doc ([#147](https://github.com/gotgenes/pi-packages/issues/147)) ([c4a6ee5](https://github.com/gotgenes/pi-packages/commit/c4a6ee5217692fc67b9a9e5b6c8586376e9e7e02))
19
+ * plan inject wrapText into ConversationViewer ([#147](https://github.com/gotgenes/pi-packages/issues/147)) ([fe4dceb](https://github.com/gotgenes/pi-packages/commit/fe4dcebda894b0d641820e55d41e48e4a0c67c3d))
20
+ * **retro:** add planning stage notes for issue [#147](https://github.com/gotgenes/pi-packages/issues/147) ([dce5db2](https://github.com/gotgenes/pi-packages/commit/dce5db2fc2ce2fd530bbfacd61ec40e771005768))
21
+ * **retro:** add TDD stage notes for issue [#147](https://github.com/gotgenes/pi-packages/issues/147) ([2db1238](https://github.com/gotgenes/pi-packages/commit/2db123874d1730ac8811d44731a8f7cb052ee043))
22
+
23
+ ## [6.16.3](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.16.2...pi-subagents-v6.16.3) (2026-05-23)
24
+
25
+
26
+ ### Documentation
27
+
28
+ * **retro:** add retro notes for issue [#146](https://github.com/gotgenes/pi-packages/issues/146) ([720fcb0](https://github.com/gotgenes/pi-packages/commit/720fcb07937fc62a1811e59448343d3dfbc1ab14))
29
+
8
30
  ## [6.16.2](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.16.1...pi-subagents-v6.16.2) (2026-05-23)
9
31
 
10
32
 
@@ -616,13 +616,13 @@ 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`~~ | 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 |
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`~~ | Resolved by #147: `wrapText` injected via `ConversationViewerOptions`; `vi.mock("@earendil-works/pi-tui")` eliminated | Done |
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
 
@@ -683,15 +683,15 @@ After Steps M and N, `ExtensionContext` appears only at true boundaries: `index.
683
683
 
684
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.
685
685
 
686
- ### Step O: Inject text wrapping into ConversationViewer (#147)
686
+ ### Step O: Inject text wrapping into ConversationViewer (#147)
687
687
 
688
- Accept a `wrapText` function via `ConversationViewerOptions`.
689
- `index.ts` passes the real `wrapTextWithAnsi` import.
690
- Tests inject a stub or the real function directly - no module-level mock needed.
688
+ Accepted `wrapText: (text: string, width: number) => string[]` via `ConversationViewerOptions`.
689
+ `agent-menu.ts` passes the real `wrapTextWithAnsi` import at the `ConversationViewer` construction site.
690
+ Tests inject a stub or the real function directly via options — no module-level mock needed.
691
691
 
692
- Apply the dependency bag convention: `ConversationViewerOptions` is destructured in the constructor signature.
692
+ Applied the dependency bag convention: `ConversationViewerOptions` is now destructured in the constructor signature.
693
693
 
694
- Impact: eliminates the hoisted `vi.mock("@earendil-works/pi-tui")` in `conversation-viewer.test.ts`.
694
+ Impact: eliminated the hoisted `vi.mock("@earendil-works/pi-tui")` from `conversation-viewer.test.ts`; 1 test deleted (mock-mechanism sentinel); net −1 test.
695
695
 
696
696
  ### Step P: Split AgentWidget rendering (#148) ✓
697
697
 
@@ -0,0 +1,166 @@
1
+ ---
2
+ issue: 147
3
+ issue_title: "Inject text wrapping into ConversationViewer (Phase 9, Step O)"
4
+ ---
5
+
6
+ # Inject text wrapping into ConversationViewer
7
+
8
+ ## Problem Statement
9
+
10
+ `ConversationViewer` calls `wrapTextWithAnsi` directly from `@earendil-works/pi-tui` in four places inside `buildContentLines`.
11
+ Because the function is a module-level binding, tests must intercept it via a hoisted `vi.mock("@earendil-works/pi-tui")` factory that replaces the entire TUI module.
12
+ This is the last `vi.mock` on an SDK module in the test suite, added specifically to exercise the overwidth-clamping safety net.
13
+
14
+ ## Goals
15
+
16
+ - Accept `wrapText: (text: string, width: number) => string[]` via `ConversationViewerOptions`.
17
+ - Destructure `ConversationViewerOptions` in the constructor signature (dependency bag convention).
18
+ - Replace all four `wrapTextWithAnsi` calls in `buildContentLines` with `this.wrapText`.
19
+ - Remove `wrapTextWithAnsi` from `conversation-viewer.ts`'s `@earendil-works/pi-tui` import.
20
+ - Pass `wrapTextWithAnsi` at the production call site in `agent-menu.ts`.
21
+ - Eliminate the hoisted `vi.mock("@earendil-works/pi-tui")` from `conversation-viewer.test.ts`.
22
+
23
+ ## Non-Goals
24
+
25
+ - Injecting `truncateToWidth` or any other TUI function (only `wrapTextWithAnsi` is relevant here).
26
+ - Changing the overwidth-clamping behavior of `buildContentLines`.
27
+ - Touching `agent-widget.ts` (tracked separately as Issue #148, Step P — already closed).
28
+
29
+ ## Background
30
+
31
+ `ui/conversation-viewer.ts` is the live conversation overlay rendered when a user selects an agent in the `/agents` menu.
32
+ Its `buildContentLines` method formats messages from the agent session into displayable lines, calling `wrapTextWithAnsi` to soft-wrap text to the available terminal width.
33
+
34
+ The overwidth-clamping safety net (`truncateToWidth` applied after `wrapTextWithAnsi`) exists because a prior upstream bug returned lines wider than the requested width.
35
+ The `vi.mock` in the test is the mechanism for simulating that bug by returning overwidth strings from `wrapTextWithAnsi`.
36
+
37
+ The only production call site for `new ConversationViewer(...)` is in the `viewAgentConversation` closure inside `createAgentsMenuHandler` in `ui/agent-menu.ts`.
38
+
39
+ Architecture reference: `docs/architecture/architecture.md` § Phase 9, Step O.
40
+
41
+ ## Design Overview
42
+
43
+ ### `ConversationViewerOptions` — add `wrapText`
44
+
45
+ ```typescript
46
+ export interface ConversationViewerOptions {
47
+ tui: TUI;
48
+ session: AgentSession;
49
+ record: AgentRecord;
50
+ activity: AgentActivityTracker | undefined;
51
+ theme: Theme;
52
+ done: (result: undefined) => void;
53
+ registry: AgentConfigLookup;
54
+ wrapText: (text: string, width: number) => string[];
55
+ }
56
+ ```
57
+
58
+ The field is **required** — it must be supplied at every construction site.
59
+ No default is provided; the default would be an invisible SDK dependency hidden inside the class.
60
+
61
+ ### Constructor destructuring
62
+
63
+ The constructor adopts the dependency bag convention — destructure the options object rather than accessing via `options.*`:
64
+
65
+ ```typescript
66
+ constructor({
67
+ tui, session, record, activity, theme, done, registry, wrapText,
68
+ }: ConversationViewerOptions) {
69
+ this.tui = tui;
70
+ this.session = session;
71
+ // … etc.
72
+ this.wrapText = wrapText;
73
+ this.unsubscribe = session.subscribe(() => { … });
74
+ }
75
+ ```
76
+
77
+ ### Production wiring — `agent-menu.ts`
78
+
79
+ `agent-menu.ts` statically imports `wrapTextWithAnsi` from `@earendil-works/pi-tui` and passes it when constructing `ConversationViewer`:
80
+
81
+ ```typescript
82
+ import { wrapTextWithAnsi } from "@earendil-works/pi-tui";
83
+ // …
84
+ return new ConversationViewer({
85
+ tui, session, record, activity, theme, done, registry,
86
+ wrapText: wrapTextWithAnsi,
87
+ });
88
+ ```
89
+
90
+ Adding `wrapText` to `AgentMenuDeps` and threading it through the closure would violate the Law of Demeter — `AgentMenuDeps` has no conceptual ownership of a text-wrapping function.
91
+ The `viewAgentConversation` closure is the direct consumer; the import belongs there.
92
+
93
+ ### Test strategy
94
+
95
+ After DI, tests pass `wrapText` directly in `ConversationViewerOptions`:
96
+
97
+ - **"render width safety" tests**: pass `wrapText: wrapTextWithAnsi` (real function, statically imported — no mock).
98
+ - **"safety net" tests**: pass a stub `wrapText: () => ["X".repeat(width + 50)]` inline in options — no `vi.mock` needed.
99
+ - The "mock is intercepting wrapTextWithAnsi" test is removed (it verified the mock mechanism, not viewer behavior).
100
+ - The module-level `wrapOverride` variable and the `vi.mock` block are removed entirely.
101
+ - The `await import("@earendil-works/pi-tui")` and `await import("../src/ui/conversation-viewer.js")` dynamic imports are converted to ordinary top-level `import` statements.
102
+
103
+ ## Module-Level Changes
104
+
105
+ | File | Change |
106
+ | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
107
+ | `src/ui/conversation-viewer.ts` | Add `wrapText` field to `ConversationViewerOptions`; add `private wrapText` field; destructure options in constructor; replace 4× `wrapTextWithAnsi(...)` with `this.wrapText(...)`; remove `wrapTextWithAnsi` from the `@earendil-works/pi-tui` import line |
108
+ | `src/ui/agent-menu.ts` | Add `import { wrapTextWithAnsi } from "@earendil-works/pi-tui"`; pass `wrapText: wrapTextWithAnsi` in `viewAgentConversation` |
109
+ | `test/conversation-viewer.test.ts` | Convert top-level dynamic `await import()` to static imports; remove `vi.mock`, `wrapOverride`, and "mock is intercepting" test; add `wrapText` to every `new ConversationViewer({…})` call; update safety-net tests to pass inline stub `wrapText` |
110
+
111
+ No symbols are removed from exported API (`ConversationViewerOptions`, `ConversationViewer`, `VIEWPORT_HEIGHT_PCT` all remain).
112
+ The `wrapText` addition to `ConversationViewerOptions` is a breaking change for any external consumers that construct `ConversationViewer` — check for usages outside this package.
113
+
114
+ Grep check: `ConversationViewer` appears only in `src/ui/conversation-viewer.ts`, `src/ui/agent-menu.ts`, and `test/conversation-viewer.test.ts` — no other files construct it.
115
+
116
+ ## Test Impact Analysis
117
+
118
+ 1. **New unit-test capability**: The safety-net tests can now inject exactly the stub they need without patching a module.
119
+ Each test is self-contained and immediately legible — the stub is declared inline at the call site.
120
+
121
+ 2. **Tests that become redundant**: The "mock is intercepting wrapTextWithAnsi" test verified the test mechanism, not production behavior.
122
+ It is deleted.
123
+ The `wrapOverride` reset in `beforeEach` is no longer needed.
124
+
125
+ 3. **Tests that stay**: All "render width safety" tests and all overwidth-clamping tests remain; they exercise real viewer behavior with real or controlled inputs.
126
+
127
+ ## TDD Order
128
+
129
+ ### Cycle 1 — Add `wrapText` field and update production wiring
130
+
131
+ 1. **Red**: Update one `new ConversationViewer({…})` in the test to include `wrapText: vi.fn()` — TypeScript rejects the unknown field.
132
+ 2. **Green**:
133
+ - Add `wrapText: (text: string, width: number) => string[]` to `ConversationViewerOptions`.
134
+ - Add `private wrapText: (text: string, width: number) => string[]` field to the class.
135
+ - Destructure options in the constructor; assign `this.wrapText = wrapText`.
136
+ - Replace all four `wrapTextWithAnsi(...)` calls with `this.wrapText(...)` in `buildContentLines`.
137
+ - Remove `wrapTextWithAnsi` from the `@earendil-works/pi-tui` import in `conversation-viewer.ts`.
138
+ - In `agent-menu.ts`: add static import of `wrapTextWithAnsi` from `@earendil-works/pi-tui`; pass `wrapText: wrapTextWithAnsi`.
139
+ - Update **all** `new ConversationViewer({…})` calls in `conversation-viewer.test.ts` to include `wrapText`.
140
+ "Render width safety" tests pass `wrapText: wrapTextWithAnsi` (real function, still imported via the existing `vi.mock` shim for now).
141
+ Safety-net tests pass a stub inline: `wrapText: () => ["X".repeat(w + 50)]`.
142
+ - Delete the "mock is intercepting wrapTextWithAnsi" test.
143
+ 3. **Verify**: `pnpm vitest run test/conversation-viewer.test.ts` passes.
144
+ 4. **Commit**: `feat: inject wrapText into ConversationViewer (Phase 9, Step O) (#147)`
145
+
146
+ ### Cycle 2 — Remove the module mock
147
+
148
+ 1. **Red**: Remove the `vi.mock("@earendil-works/pi-tui", …)` block, the `wrapOverride` variable, and the `beforeEach(() => { wrapOverride = null; })` reset.
149
+ Convert `const { visibleWidth } = await import("@earendil-works/pi-tui")` to `import { visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui"`.
150
+ Convert `const { ConversationViewer } = await import("../src/ui/conversation-viewer.js")` to `import { ConversationViewer } from "../src/ui/conversation-viewer.js"`.
151
+ Run tests — they should still pass (the mock was no longer needed after Cycle 1).
152
+ 2. **Green**: Tests pass without any module mock.
153
+ 3. **Verify**: `pnpm vitest run test/conversation-viewer.test.ts` and `pnpm run check` both pass.
154
+ 4. **Commit**: `refactor: remove vi.mock from conversation-viewer tests (#147)`
155
+
156
+ ## Risks and Mitigations
157
+
158
+ | Risk | Mitigation |
159
+ | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
160
+ | Missing a `new ConversationViewer({…})` call site in tests | Grep confirms only `test/conversation-viewer.test.ts` constructs the viewer; all instances are in-file |
161
+ | TypeScript missing the `private wrapText` field type | Use `pnpm run check` after Cycle 1 to verify no type errors |
162
+ | The dynamic `await import()` pattern in tests was necessary for some other reason | The only purpose was to run after `vi.mock` hoisting; once the mock is gone, static imports work fine |
163
+
164
+ ## Open Questions
165
+
166
+ None — the issue's "Changes" section is unambiguous and the call-site inventory is small.
@@ -0,0 +1,70 @@
1
+ ---
2
+ issue: 146
3
+ issue_title: "Narrow UI context for menu handlers (Phase 9, Step N)"
4
+ ---
5
+
6
+ # Retro: #146 — Narrow UI context for menu handlers
7
+
8
+ ## Final Retrospective (2026-05-23T10:00:00Z)
9
+
10
+ ### Session summary
11
+
12
+ Introduced a `MenuUI` interface in `agent-menu.ts` capturing the 6 `ctx.ui` methods used by menu handlers.
13
+ Replaced `ExtensionContext` with `MenuUI` (plus explicit `ModelRegistry` and `ParentSnapshot`) in all inner functions across `agent-menu.ts`, `agent-config-editor.ts`, and `agent-creation-wizard.ts`.
14
+ Dissolved three ≤4-field dependency bags (`AgentConfigEditorDeps`, `GetResultDeps`, `SteerToolDeps`) into plain parameters; destructured `AgentMenuDeps` and `AgentCreationWizardDeps` in their factory signatures.
15
+ Eliminated 42 `ctx as any` casts across 5 test files.
16
+ Released as `pi-subagents-v6.16.2`.
17
+
18
+ ### Observations
19
+
20
+ #### What went well
21
+
22
+ - **User-initiated plan review caught three ordering bugs before implementation.**
23
+ The user asked the agent to "review the plan and judge its quality and clarity" after the initial commit.
24
+ The review identified three execution-blocking problems in the TDD order: `MenuUI` not defined before consumers, `ParentSnapshot` not threaded through the handler return type, and `index.ts` call sites not updated alongside their factories.
25
+ All three were fixed before TDD execution began, saving significant implementation friction.
26
+ - The plan's TDD order (after fixes) worked well for a pure-refactoring change — all 806 tests stayed green throughout every step.
27
+ - The type checker served as an effective safety net: every call-site mismatch was caught immediately by `pnpm run check`, preventing runtime surprises.
28
+
29
+ #### What caused friction (agent side)
30
+
31
+ ##### Planning session
32
+
33
+ - `missing-context` — The initial plan had three TDD ordering bugs: (1) `MenuUI` used before defined, (2) `ParentSnapshot` not threaded through handler return type, (3) `index.ts` call sites not updated alongside factory signature changes.
34
+ These were caught by a user-prompted plan review, not by the planning agent itself.
35
+ Impact: would have caused type-check failures during TDD execution; fixed before implementation started.
36
+
37
+ - `wrong-abstraction` — After fixing the plan, the agent ran `git commit --amend --no-edit` to update the plan commit, but it landed on the wrong commit (a stacked prompt-changes commit from a different session).
38
+ This caused divergent history with origin.
39
+ Recovery required `git reset --hard origin/main` and re-applying the plan fixes as a new commit (`3d5b591`).
40
+ Impact: ~5 minutes of git recovery and re-application of edits.
41
+
42
+ ##### Implementation session
43
+
44
+ - `missing-context` — In Step 3, the plan listed the `WizardManager.spawnAndWait` signature change (to accept `ParentSnapshot`) and the `AgentMenuManager.spawnAndWait` change as separate steps (3 and 4).
45
+ But `agent-menu.ts` passes `deps.manager` (typed as `AgentMenuManager`) to `createAgentCreationWizard`, which expects `WizardManager`.
46
+ Changing `WizardManager` without also changing `AgentMenuManager` broke the type checker.
47
+ I had to pull the `AgentMenuManager` change forward from Step 4 into Step 3.
48
+ Impact: minor — a few minutes of diagnosis and one extra edit, no rework.
49
+
50
+ - `missing-context` — In Step 2, after dissolving `AgentConfigEditorDeps` and changing `showAgentDetail(ctx)` to `showAgentDetail(ui: MenuUI)`, I forgot to update the call site in `agent-menu.ts` from `editor.showAgentDetail(ctx, agentName)` to `editor.showAgentDetail(ctx.ui, agentName)`.
51
+ The type checker caught it immediately.
52
+ Impact: added friction but no rework — one extra `pnpm run check` cycle.
53
+
54
+ - `missing-context` — In Step 4, the `makeUI()` test helper used `modelRegistry: {}` which didn't satisfy the `ModelRegistry` interface (requires `find` and `getAll`).
55
+ Impact: one extra `pnpm run check` cycle and a one-line fix.
56
+
57
+ - `missing-context` — In Step 5, two tests called `createGetResultTool(makeDeps())` directly (not via the `execute()` helper).
58
+ After dissolving deps, the factory signature changed but I only updated `execute()`.
59
+ The type checker caught the two direct calls.
60
+ Impact: one extra `pnpm run check` cycle.
61
+
62
+ #### What caused friction (user side)
63
+
64
+ - None observed — the session ran autonomously with no user corrections needed.
65
+
66
+ #### What caused friction (user side — planning)
67
+
68
+ - The user had to explicitly ask for a plan review (“I'd like you to review the plan and judge its quality and clarity”) to surface three ordering bugs.
69
+ The planning agent should have caught these during plan authoring — the testing skill already contains rules about TDD step ordering and shared interface changes.
70
+ The user's intervention prevented significant implementation friction.
@@ -0,0 +1,50 @@
1
+ ---
2
+ issue: 147
3
+ issue_title: "Inject text wrapping into ConversationViewer (Phase 9, Step O)"
4
+ ---
5
+
6
+ # Retro: #147 — Inject text wrapping into ConversationViewer (Phase 9, Step O)
7
+
8
+ ## Stage: Planning (2026-05-23T00:00:00Z)
9
+
10
+ ### Session summary
11
+
12
+ Read the issue, loaded package-pi-subagents, code-design, testing, and markdown-conventions skills.
13
+ Explored `src/ui/conversation-viewer.ts`, `src/ui/agent-menu.ts`, `test/conversation-viewer.test.ts`, and the Phase 9 architecture roadmap.
14
+ Wrote and committed the plan at `packages/pi-subagents/docs/plans/0147-inject-wrap-text-into-conversation-viewer.md`.
15
+
16
+ ### Observations
17
+
18
+ - The change is tightly scoped: two source files (`conversation-viewer.ts`, `agent-menu.ts`) and one test file.
19
+ - `wrapTextWithAnsi` is called in exactly four places inside `buildContentLines` — all in the same private method, making the replacement straightforward.
20
+ - The only production call site for `new ConversationViewer({…})` is `viewAgentConversation` in `agent-menu.ts`.
21
+ `wrapTextWithAnsi` is added as a static import there and passed as `wrapText` — no threading through `AgentMenuDeps` needed.
22
+ - All `new ConversationViewer({…})` calls in the test file are inline (no shared factory helper), so every call site needs the new `wrapText` field added.
23
+ Grep confirms the count: 11+ calls, all in `test/conversation-viewer.test.ts`.
24
+ - The plan uses 2 TDD cycles: Cycle 1 adds the field and updates all call sites (with the `vi.mock` still present for safety); Cycle 2 removes the mock and converts dynamic `await import()` to static imports.
25
+ This ordering avoids a large simultaneous change and gives the suite a stable intermediate state.
26
+ - The "mock is intercepting wrapTextWithAnsi" test is deleted in Cycle 1 (it verified the mock mechanism, not production behavior).
27
+ - No exported API symbols are removed; `wrapText` is a new required field on `ConversationViewerOptions`, which is a breaking change only for external constructors of `ConversationViewer` — confirmed none exist outside this package.
28
+
29
+ ## Stage: Implementation — TDD (2026-05-23T11:36:00Z)
30
+
31
+ ### Session summary
32
+
33
+ Completed both TDD cycles from the plan.
34
+ Cycle 1 added `wrapText` to `ConversationViewerOptions`, destructured options in the constructor, replaced all four `wrapTextWithAnsi` calls with `this.wrapText`, updated `agent-menu.ts` to import and pass `wrapTextWithAnsi`, and updated all 16 test constructor call sites (11 render-width-safety + 5 safety-net) while keeping the `vi.mock` shim in place.
35
+ Cycle 2 removed the `vi.mock` block, `wrapOverride`, and `beforeEach` reset, then converted the dynamic `await import()` calls to ordinary static imports.
36
+ Test count: 806 → 805 (deleted the mock-mechanism sentinel test).
37
+ Full suite 50 files, 805 tests, all green.
38
+
39
+ ### Observations
40
+
41
+ - The plan said the render-width-safety constructor calls numbered "11+" — the actual count was 16 total (11 render-width-safety + 5 safety-net), all in `test/conversation-viewer.test.ts`.
42
+ No external call sites existed.
43
+ - `wrapTextWithAnsi` needed to be added to the dynamic import in Cycle 1 (`const { visibleWidth, wrapTextWithAnsi } = await import(...)`) because the render-width-safety tests reference it by name.
44
+ The plan didn't call this out explicitly — minor omission.
45
+ - Used a Python script (inline via bash) to make the 17 constructor-call edits rather than 17 separate Edit-tool calls.
46
+ The safety-net tests each had a different stub character (`X`, `Y`, `Z`, `B`, `W`) which required a regex capture group to preserve.
47
+ The script worked on the first attempt.
48
+ - Cycle 2 was a single Edit call replacing the entire mock block + the two dynamic imports + `beforeEach`.
49
+ The autoformatter then cleaned up import ordering automatically.
50
+ - Architecture doc updated: smells table row struck-through and Step O marked ✓.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-subagents",
3
- "version": "6.16.2",
3
+ "version": "6.17.0",
4
4
  "exports": {
5
5
  ".": "./src/service.ts"
6
6
  },
@@ -1,3 +1,4 @@
1
+ import { wrapTextWithAnsi } from "@earendil-works/pi-tui";
1
2
  import { AgentTypeRegistry } from "../agent-types.js";
2
3
  import type { ModelRegistry } from "../model-resolver.js";
3
4
  import type { ParentSnapshot } from "../parent-snapshot.js";
@@ -260,6 +261,7 @@ export function createAgentsMenuHandler({
260
261
  theme,
261
262
  done,
262
263
  registry,
264
+ wrapText: wrapTextWithAnsi,
263
265
  });
264
266
  },
265
267
  {
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import type { AgentSession } from "@earendil-works/pi-coding-agent";
9
- import { type Component, matchesKey, type TUI, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
9
+ import { type Component, matchesKey, type TUI, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
10
10
  import type { AgentConfigLookup } from "../agent-types.js";
11
11
  import { extractText } from "../context.js";
12
12
  import type { AgentRecord } from "../types.js";
@@ -59,6 +59,7 @@ export interface ConversationViewerOptions {
59
59
  theme: Theme;
60
60
  done: (result: undefined) => void;
61
61
  registry: AgentConfigLookup;
62
+ wrapText: (text: string, width: number) => string[];
62
63
  }
63
64
 
64
65
  export class ConversationViewer implements Component {
@@ -75,16 +76,27 @@ export class ConversationViewer implements Component {
75
76
  private theme: Theme;
76
77
  private done: (result: undefined) => void;
77
78
  private registry: AgentConfigLookup;
78
-
79
- constructor(options: ConversationViewerOptions) {
80
- this.tui = options.tui;
81
- this.session = options.session;
82
- this.record = options.record;
83
- this.activity = options.activity;
84
- this.theme = options.theme;
85
- this.done = options.done;
86
- this.registry = options.registry;
87
- this.unsubscribe = options.session.subscribe(() => {
79
+ private wrapText: (text: string, width: number) => string[];
80
+
81
+ constructor({
82
+ tui,
83
+ session,
84
+ record,
85
+ activity,
86
+ theme,
87
+ done,
88
+ registry,
89
+ wrapText,
90
+ }: ConversationViewerOptions) {
91
+ this.tui = tui;
92
+ this.session = session;
93
+ this.record = record;
94
+ this.activity = activity;
95
+ this.theme = theme;
96
+ this.done = done;
97
+ this.registry = registry;
98
+ this.wrapText = wrapText;
99
+ this.unsubscribe = session.subscribe(() => {
88
100
  if (this.closed) return;
89
101
  this.tui.requestRender();
90
102
  });
@@ -253,7 +265,7 @@ export class ConversationViewer implements Component {
253
265
  if (!text.trim()) continue;
254
266
  if (needsSeparator) lines.push(th.fg("dim", "───"));
255
267
  lines.push(th.fg("accent", "[User]"));
256
- for (const line of wrapTextWithAnsi(text.trim(), width)) {
268
+ for (const line of this.wrapText(text.trim(), width)) {
257
269
  lines.push(line);
258
270
  }
259
271
  } else if (msg.role === "assistant") {
@@ -268,7 +280,7 @@ export class ConversationViewer implements Component {
268
280
  if (needsSeparator) lines.push(th.fg("dim", "───"));
269
281
  lines.push(th.bold("[Assistant]"));
270
282
  if (textParts.length > 0) {
271
- for (const line of wrapTextWithAnsi(textParts.join("\n").trim(), width)) {
283
+ for (const line of this.wrapText(textParts.join("\n").trim(), width)) {
272
284
  lines.push(line);
273
285
  }
274
286
  }
@@ -281,7 +293,7 @@ export class ConversationViewer implements Component {
281
293
  if (!truncated.trim()) continue;
282
294
  if (needsSeparator) lines.push(th.fg("dim", "───"));
283
295
  lines.push(th.fg("dim", "[Result]"));
284
- for (const line of wrapTextWithAnsi(truncated.trim(), width)) {
296
+ for (const line of this.wrapText(truncated.trim(), width)) {
285
297
  lines.push(th.fg("dim", line));
286
298
  }
287
299
  } else if (isBashExecution(msg)) {
@@ -291,7 +303,7 @@ export class ConversationViewer implements Component {
291
303
  const out = msg.output.length > 500
292
304
  ? msg.output.slice(0, 500) + "... (truncated)"
293
305
  : msg.output;
294
- for (const line of wrapTextWithAnsi(out.trim(), width)) {
306
+ for (const line of this.wrapText(out.trim(), width)) {
295
307
  lines.push(th.fg("dim", line));
296
308
  }
297
309
  }