@gotgenes/pi-subagents 6.16.3 → 6.17.1
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 +27 -0
- package/docs/architecture/architecture.md +588 -535
- package/docs/architecture/history/phase-1-api-boundary.md +8 -0
- package/docs/architecture/history/phase-2-remove-scheduling.md +9 -0
- package/docs/architecture/history/phase-3-remove-rpc-groupjoin.md +11 -0
- package/docs/architecture/history/phase-4-implement-service.md +8 -0
- package/docs/architecture/history/phase-5-decompose-index.md +42 -0
- package/docs/architecture/history/phase-7-encapsulation.md +173 -0
- package/docs/architecture/history/phase-8-testability.md +103 -0
- package/docs/architecture/history/phase-9-observation-ctx.md +122 -0
- package/docs/plans/0147-inject-wrap-text-into-conversation-viewer.md +166 -0
- package/docs/retro/0147-inject-wrap-text-into-conversation-viewer.md +90 -0
- package/package.json +1 -1
- package/src/agent-manager.ts +11 -11
- package/src/agent-record.ts +6 -6
- package/src/agent-runner.ts +6 -6
- package/src/agent-types.ts +2 -2
- package/src/custom-agents.ts +3 -3
- package/src/default-agents.ts +1 -1
- package/src/env.ts +2 -2
- package/src/handlers/index.ts +2 -2
- package/src/index.ts +26 -26
- package/src/invocation-config.ts +1 -1
- package/src/memory.ts +2 -2
- package/src/notification.ts +4 -4
- package/src/parent-snapshot.ts +1 -1
- package/src/prompts.ts +2 -2
- package/src/record-observer.ts +2 -2
- package/src/renderer.ts +2 -2
- package/src/runtime.ts +2 -2
- package/src/service-adapter.ts +5 -5
- package/src/service.ts +1 -1
- package/src/session-config.ts +5 -5
- package/src/skill-loader.ts +2 -2
- package/src/tools/agent-tool.ts +11 -11
- package/src/tools/background-spawner.ts +8 -8
- package/src/tools/foreground-runner.ts +9 -9
- package/src/tools/get-result-tool.ts +5 -5
- package/src/tools/helpers.ts +4 -4
- package/src/tools/spawn-config.ts +6 -6
- package/src/tools/steer-tool.ts +3 -3
- package/src/types.ts +1 -1
- package/src/ui/agent-activity-tracker.ts +1 -1
- package/src/ui/agent-config-editor.ts +4 -4
- package/src/ui/agent-creation-wizard.ts +5 -5
- package/src/ui/agent-menu.ts +12 -10
- package/src/ui/agent-widget.ts +5 -5
- package/src/ui/conversation-viewer.ts +33 -21
- package/src/ui/display.ts +2 -2
- package/src/ui/ui-observer.ts +1 -1
- package/src/ui/widget-renderer.ts +5 -5
- package/src/worktree-state.ts +1 -1
- package/src/worktree.ts +1 -1
- package/vitest.config.ts +14 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Phase 1: Export SubagentsService from this package
|
|
2
|
+
|
|
3
|
+
Added the `SubagentsService` interface, serializable types, `Symbol.for()` accessor functions, and `SUBAGENT_EVENTS` constants as public exports.
|
|
4
|
+
Wired `service-adapter.ts` to wrap `AgentManager` and call `publishSubagentsService()` at extension init.
|
|
5
|
+
|
|
6
|
+
## Related issues
|
|
7
|
+
|
|
8
|
+
- #48 — Export SubagentsService from this package
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Phase 2: Remove scheduling
|
|
2
|
+
|
|
3
|
+
Deleted `schedule.ts`, `schedule-store.ts`, `ui/schedule-menu.ts`.
|
|
4
|
+
Removed the `schedule` parameter from the `Agent` tool schema.
|
|
5
|
+
Removed scheduler setup and lifecycle hooks from `index.ts`.
|
|
6
|
+
|
|
7
|
+
## Related issues
|
|
8
|
+
|
|
9
|
+
- #52 — Remove scheduling
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Phase 3: Remove group-join, ad-hoc RPC; replace output-file
|
|
2
|
+
|
|
3
|
+
Deleted `group-join.ts`, `cross-extension-rpc.ts` (#49).
|
|
4
|
+
Replaced `output-file.ts` with `SessionManager.create()` + `session-dir.ts` (#61).
|
|
5
|
+
Simplified `index.ts` to use direct individual notifications.
|
|
6
|
+
Lifecycle events emitted on `pi.events` for external consumers.
|
|
7
|
+
|
|
8
|
+
## Related issues
|
|
9
|
+
|
|
10
|
+
- #49 — Remove group-join and ad-hoc RPC
|
|
11
|
+
- #61 — Replace output-file with JSONL session transcripts
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Phase 4: Implement and publish SubagentsService
|
|
2
|
+
|
|
3
|
+
Wired `service-adapter.ts` to wrap `AgentManager` and call `publishSubagentsService()` at extension init.
|
|
4
|
+
Model strings are resolved inside the adapter.
|
|
5
|
+
|
|
6
|
+
## Related issues
|
|
7
|
+
|
|
8
|
+
- #48 — Implement and publish SubagentsService
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Phase 5: Decompose index.ts
|
|
2
|
+
|
|
3
|
+
Extracted tools, notifications, activity tracking, event handlers, and the `/agents` command into separate modules.
|
|
4
|
+
Created `SubagentRuntime` factory to hold session-scoped state.
|
|
5
|
+
|
|
6
|
+
## index.ts decomposition
|
|
7
|
+
|
|
8
|
+
The original monolithic `index.ts` has been decomposed into focused modules:
|
|
9
|
+
|
|
10
|
+
```text
|
|
11
|
+
src/
|
|
12
|
+
├── index.ts - slimmed entry point: init, tool registration
|
|
13
|
+
├── runtime.ts - SubagentRuntime: session-scoped state + methods
|
|
14
|
+
├── tools/
|
|
15
|
+
│ ├── agent-tool.ts - Agent tool definition, parameter validation, dispatch
|
|
16
|
+
│ ├── foreground-runner.ts - foreground execution loop (spinner, streaming, result)
|
|
17
|
+
│ ├── background-spawner.ts - background spawn (activity setup, notification wiring)
|
|
18
|
+
│ ├── get-result-tool.ts - get_subagent_result tool
|
|
19
|
+
│ ├── steer-tool.ts - steer_subagent tool
|
|
20
|
+
│ └── helpers.ts - shared tool utilities (textResult, buildDetails, getStatusNote, ...)
|
|
21
|
+
├── handlers/
|
|
22
|
+
│ ├── lifecycle.ts - session_start, session_before_switch, session_shutdown
|
|
23
|
+
│ └── tool-start.ts - tool_execution_start handler
|
|
24
|
+
├── notification.ts - completion nudges, custom renderer
|
|
25
|
+
├── renderer.ts - notification TUI component
|
|
26
|
+
├── ui/agent-menu.ts - /agents slash command menu (orchestration, listing, settings)
|
|
27
|
+
├── ui/agent-config-editor.ts - agent detail view (edit/delete/eject/disable/enable)
|
|
28
|
+
├── ui/agent-creation-wizard.ts - agent creation (AI-generation and manual-form)
|
|
29
|
+
├── ui/agent-file-ops.ts - AgentFileOps interface + FsAgentFileOps implementation
|
|
30
|
+
├── service-adapter.ts - SubagentsService implementation wrapping AgentManager
|
|
31
|
+
└── (existing domain modules unchanged)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Each extracted module receives narrow constructor-injected dependencies rather than closing over module-level state.
|
|
35
|
+
Handlers call methods on narrow runtime interfaces - no raw field writes, no `widget!` reach-throughs.
|
|
36
|
+
|
|
37
|
+
## Related issues
|
|
38
|
+
|
|
39
|
+
- #54 — Decompose index.ts
|
|
40
|
+
- #69 — SubagentRuntime factory
|
|
41
|
+
- #70 — Handler extraction
|
|
42
|
+
- #87 — Runtime methods
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# Phase 7: Encapsulation and dependency narrowing
|
|
2
|
+
|
|
3
|
+
Every mutable state bag became a class, every dependency bag narrowed to what its consumer uses, every callback became either a method on a collaborator or an event on an observable.
|
|
4
|
+
|
|
5
|
+
## AgentManager decomposition
|
|
6
|
+
|
|
7
|
+
AgentManager was decomposed in three steps to untangle record management, concurrency control, and execution orchestration.
|
|
8
|
+
|
|
9
|
+
### Step 1: Record state machine (#98, #102)
|
|
10
|
+
|
|
11
|
+
Extracted status-transition methods (`markRunning`, `markCompleted`, `markAborted`, `markSteered`, `markError`, `markStopped`, `resetForResume`) onto `AgentRecord`.
|
|
12
|
+
Replaced scattered field writes across 6 sites with encapsulated transition methods.
|
|
13
|
+
Issue #102 consolidated test `AgentRecord` construction into a shared factory.
|
|
14
|
+
|
|
15
|
+
### Step 2: Parent snapshot (#99)
|
|
16
|
+
|
|
17
|
+
Replaced live `ctx: ExtensionContext` capture in `SpawnArgs` with an immutable `ParentSnapshot` data object.
|
|
18
|
+
The snapshot is taken once at spawn time; queued agents execute against frozen state rather than a potentially stale session reference.
|
|
19
|
+
`runAgent()` accepts `ParentSnapshot` instead of `ctx`.
|
|
20
|
+
`pi: ExtensionAPI` was removed from `SpawnArgs` - `runAgent()` accepts a `ShellExec` function instead.
|
|
21
|
+
|
|
22
|
+
### Step 3: Session-event observation (#100)
|
|
23
|
+
|
|
24
|
+
Replaced three-layer callback threading with direct session subscriptions.
|
|
25
|
+
`record-observer.ts` subscribes to the session to update record statistics (tool uses, lifetime usage, compaction count).
|
|
26
|
+
`ui/ui-observer.ts` subscribes to the session to stream UI state (active tools, response text, turn count).
|
|
27
|
+
`SpawnOptions` and `RunOptions` dropped all `on*` callback fields except `onSessionCreated` (which delivers the session object to enable external subscriptions).
|
|
28
|
+
|
|
29
|
+
### Realized impact
|
|
30
|
+
|
|
31
|
+
| Metric | Before | After |
|
|
32
|
+
| --------------------------------- | ------ | ----------------------- |
|
|
33
|
+
| `SpawnOptions` callback fields | 6 | 1 (`onSessionCreated`) |
|
|
34
|
+
| `RunOptions` callback fields | 6 | 1 (`onSessionCreated`) |
|
|
35
|
+
| Callback layers | 3 | 0 (direct subscription) |
|
|
36
|
+
| Live `ctx` references in queue | 1 | 0 (snapshot) |
|
|
37
|
+
| Scattered status-transition sites | 6 | 1 (state machine) |
|
|
38
|
+
|
|
39
|
+
## Encapsulation roadmap
|
|
40
|
+
|
|
41
|
+
Phase 7 encapsulated mutable state into classes, replaced callbacks with semantic components, and narrowed dependency bags.
|
|
42
|
+
|
|
43
|
+
Each step was sequenced so it made the next step easier.
|
|
44
|
+
|
|
45
|
+
### Resolved smells
|
|
46
|
+
|
|
47
|
+
All nine smells identified at the start of Phase 7 were resolved:
|
|
48
|
+
|
|
49
|
+
| Smell | Resolution |
|
|
50
|
+
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
51
|
+
| Global mutable state | `AgentTypeRegistry` class (#108); `reloadCustomAgents` callback removed from dep bags |
|
|
52
|
+
| Closure bag as class | `NotificationManager` class (#116); `pendingNudges` and timer state are private fields |
|
|
53
|
+
| Mutable state bag | `AgentActivityTracker` class (#110); transition methods replace external writes |
|
|
54
|
+
| Settings relay | `SettingsManager` class (#109); 6 callback fields collapsed to one object |
|
|
55
|
+
| Post-construction mutation | `ExecutionState`, `WorktreeState`, `NotificationState` collaborators (#111); stats behind mutation methods |
|
|
56
|
+
| Fire-and-forget callbacks | `AgentManagerObserver` interface (#112); one observer object replaces 3 closure lambdas |
|
|
57
|
+
| Duplicate `SpawnOptions` | Internal type renamed to `AgentSpawnConfig` (#113); public `SpawnOptions` unchanged |
|
|
58
|
+
| Type dumping ground | `NotificationDetails`, `ParentSnapshot`, `EnvInfo` moved to their natural modules (#116); narrow subsets defined |
|
|
59
|
+
| Wide dependency bags | `AgentToolDeps` 9 → 6, `AgentMenuDeps` 8 → 7 (#114); `emitEvent` removed; description text derived from registry; `agentActivity` narrowed |
|
|
60
|
+
|
|
61
|
+
### Step A: Extract state into classes (foundation, parallel)
|
|
62
|
+
|
|
63
|
+
These three extractions are independent and can proceed in any order.
|
|
64
|
+
Each eliminates a category of global/closure state and gives orphaned callbacks a natural home.
|
|
65
|
+
|
|
66
|
+
#### A1. AgentTypeRegistry class (#108)
|
|
67
|
+
|
|
68
|
+
Wrapped the module-scoped `agents` Map and free functions in `agent-types.ts` into an injectable class.
|
|
69
|
+
`reloadCustomAgents` callback removed from `AgentToolDeps` and `AgentMenuDeps`; replaced by `registry.reload()`.
|
|
70
|
+
`DEFAULT_AGENT_NAMES` moved from `types.ts` to the registry.
|
|
71
|
+
|
|
72
|
+
#### A2. SettingsManager class (#109, #118)
|
|
73
|
+
|
|
74
|
+
Encapsulated settings load/save/apply cycle into `SettingsManager` (in `settings.ts`).
|
|
75
|
+
Owns `defaultMaxTurns`, `graceTurns`, `maxConcurrent` with normalizing property accessors.
|
|
76
|
+
Added `applyMaxConcurrent(n)`, `applyDefaultMaxTurns(n)`, `applyGraceTurns(n)` - each owns the full consequence chain: normalize → set in memory → notify callback → persist → emit event → return toast.
|
|
77
|
+
The 6 settings-related fields in `AgentMenuDeps` collapsed to `settings: AgentMenuSettings`.
|
|
78
|
+
|
|
79
|
+
#### A3. AgentActivityTracker class (#110)
|
|
80
|
+
|
|
81
|
+
Wrapped the 7-field mutable `AgentActivity` interface in an `AgentActivityTracker` class (`src/ui/agent-activity-tracker.ts`).
|
|
82
|
+
`ui-observer.ts` calls transition methods; consumers use read-only accessors.
|
|
83
|
+
The shared map on `SubagentRuntime` is `Map<string, AgentActivityTracker>`.
|
|
84
|
+
|
|
85
|
+
### Step B: Split AgentRecord lifecycle state (#111)
|
|
86
|
+
|
|
87
|
+
Split post-construction mutation into phase-specific collaborators, each born complete:
|
|
88
|
+
|
|
89
|
+
- **`ExecutionState`** (`session`, `outputFile`) - constructed in `onSessionCreated`.
|
|
90
|
+
- **`WorktreeState`** (`path`, `branch`, `cleanupResult`) - constructed at worktree setup.
|
|
91
|
+
- **`NotificationState`** (`toolCallId`, `resultConsumed`) - constructed by `AgentManager.spawn()` when `toolCallId` is provided.
|
|
92
|
+
- **`pendingSteers`** moved to `Map<string, string[]>` on `AgentManager`.
|
|
93
|
+
- Stats encapsulated behind mutation methods with read-only getters.
|
|
94
|
+
- `AgentRecordInit` trimmed from 19 optional fields to 4 construction-time fields.
|
|
95
|
+
|
|
96
|
+
### Step C: Replace AgentManager callbacks with observer (#112)
|
|
97
|
+
|
|
98
|
+
`AgentManagerObserver` interface replaces `onStart`/`onComplete`/`onCompact`.
|
|
99
|
+
`index.ts` constructs one observer object instead of 3 closure lambdas.
|
|
100
|
+
`AgentManagerOptions` drops from 9 → 7 fields.
|
|
101
|
+
|
|
102
|
+
### Step D: Disambiguate SpawnOptions and narrow dependency bags
|
|
103
|
+
|
|
104
|
+
#### D1. Disambiguate SpawnOptions (#113)
|
|
105
|
+
|
|
106
|
+
Internal `SpawnOptions` in `agent-manager.ts` renamed to `AgentSpawnConfig`.
|
|
107
|
+
Public `SpawnOptions` in `service.ts` unchanged.
|
|
108
|
+
|
|
109
|
+
#### D2. Narrow AgentToolDeps and AgentMenuDeps (#114)
|
|
110
|
+
|
|
111
|
+
| Bag | Before | After | How |
|
|
112
|
+
| --------------- | -------- | ----- | ----------------------------------------------------------------------------------------------------------- |
|
|
113
|
+
| `AgentToolDeps` | 9 fields | 6 | `emitEvent` → observer; `typeListText`/`availableTypesText` derived from registry; `agentActivity` narrowed |
|
|
114
|
+
| `AgentMenuDeps` | 8 fields | 7 | Dead `emitEvent` removed; `agentActivity` narrowed to read-only `AgentActivityReader` |
|
|
115
|
+
|
|
116
|
+
### Step E: Decompose large files and relocate types
|
|
117
|
+
|
|
118
|
+
#### E1. Split agent-tool.ts foreground/background (#115)
|
|
119
|
+
|
|
120
|
+
Extracted `foreground-runner.ts` (~175 lines) and `background-spawner.ts` (~116 lines).
|
|
121
|
+
`agent-tool.ts` reduced from 579 → 411 lines.
|
|
122
|
+
|
|
123
|
+
#### E2. Type housekeeping (#116)
|
|
124
|
+
|
|
125
|
+
- Moved `NotificationDetails`, `ParentSnapshot`, `EnvInfo` to their natural modules.
|
|
126
|
+
- Converted `createNotificationSystem` closure to `NotificationManager` class.
|
|
127
|
+
- Converted `ConversationViewer` constructor from 7 positional parameters to `ConversationViewerOptions` bag.
|
|
128
|
+
- Defined `AgentIdentity` and `AgentPromptConfig` narrow subsets; `buildAgentPrompt` narrowed to `AgentPromptConfig`.
|
|
129
|
+
|
|
130
|
+
### Phase 7 results
|
|
131
|
+
|
|
132
|
+
| Metric | Before | After |
|
|
133
|
+
| ------------------------------------------ | ------ | ----- |
|
|
134
|
+
| Module-scoped mutable state | 1 | 0 |
|
|
135
|
+
| Closure-bag "classes" | 2 | 0 |
|
|
136
|
+
| Externally-mutated state bags | 2 | 0 |
|
|
137
|
+
| `AgentManagerOptions` fields | 9 | 7 |
|
|
138
|
+
| `AgentToolDeps` fields | 9 | 6 |
|
|
139
|
+
| `AgentMenuDeps` fields | 13 | 7 |
|
|
140
|
+
| `SpawnOptions` callback fields | 6 | 1 |
|
|
141
|
+
| `RunOptions` callback fields | 6 | 1 |
|
|
142
|
+
| Callbacks threaded through deps | 8 | 0 |
|
|
143
|
+
| Types in `types.ts` without a natural home | 4 | 0 |
|
|
144
|
+
|
|
145
|
+
### Dependency graph
|
|
146
|
+
|
|
147
|
+
```mermaid
|
|
148
|
+
flowchart LR
|
|
149
|
+
A1["A1: Registry"] --> D2["D2: Narrow deps"]
|
|
150
|
+
A2["A2: Settings"] --> A2b["A2b: Apply"] --> D2
|
|
151
|
+
A3["A3: Activity Tracker"] --> D2
|
|
152
|
+
B["B: Record lifecycle"] --> D2
|
|
153
|
+
B --> C["C: Observer"] --> D1["D1: SpawnOptions"] --> D2
|
|
154
|
+
D2 --> E1["E1: agent-tool split"]
|
|
155
|
+
A1 --> E2["E2: Type housekeeping"]
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Related issues
|
|
159
|
+
|
|
160
|
+
- #98 — Record state machine
|
|
161
|
+
- #99 — Parent snapshot
|
|
162
|
+
- #100 — Session-event observation
|
|
163
|
+
- #102 — Consolidated test AgentRecord construction
|
|
164
|
+
- #108 — AgentTypeRegistry class
|
|
165
|
+
- #109 — SettingsManager class
|
|
166
|
+
- #110 — AgentActivityTracker class
|
|
167
|
+
- #111 — Split AgentRecord lifecycle state
|
|
168
|
+
- #112 — Replace AgentManager callbacks with observer
|
|
169
|
+
- #113 — Disambiguate SpawnOptions
|
|
170
|
+
- #114 — Narrow AgentToolDeps and AgentMenuDeps
|
|
171
|
+
- #115 — Split agent-tool.ts foreground/background
|
|
172
|
+
- #116 — Type housekeeping
|
|
173
|
+
- #118 — SettingsManager apply methods
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Phase 8: Testability, display extraction, and menu decomposition
|
|
2
|
+
|
|
3
|
+
Eliminated `vi.mock()` module mocking in the two most fragile test suites by injecting IO-touching collaborators; consolidated shared test fixtures; extracted display helpers into a reusable module; decomposed the largest UI file.
|
|
4
|
+
|
|
5
|
+
Steps G and H eliminated 11 of the original 12 `vi.mock()` calls in the runner tests, removing fragile call-sequence assertions in favour of injected stubs.
|
|
6
|
+
Step G resolved `session-config.test.ts`; Step H resolved both `agent-runner.test.ts` and `agent-runner-extension-tools.test.ts`.
|
|
7
|
+
|
|
8
|
+
The display and menu improvements were identified during Phase 7 but deferred because they did not gate encapsulation work.
|
|
9
|
+
The display extraction unblocked menu decomposition.
|
|
10
|
+
|
|
11
|
+
## Test pain points (resolved)
|
|
12
|
+
|
|
13
|
+
| Symptom | Resolution |
|
|
14
|
+
| ------------------------------------------------------------- | -------------------------------------------------------------- |
|
|
15
|
+
| 7 `vi.mock()` calls in `agent-runner.test.ts` | Step H (#133): injected `RunnerIO` stubs |
|
|
16
|
+
| 7 `vi.mock()` calls in `agent-runner-extension-tools.test.ts` | Step H (#133): same |
|
|
17
|
+
| 52 `as any` casts across test suite | Step I (#134): reduced to 15 |
|
|
18
|
+
| 3× duplicated `mockSession()` | Step F (#131): shared `createMockSession()` in `test/helpers/` |
|
|
19
|
+
| 3× duplicated `makeDeps()` | Step F (#131): shared `createToolDeps()` in `test/helpers/` |
|
|
20
|
+
|
|
21
|
+
The well-designed test suites - `agent-manager.test.ts` (1 mock, DI via `AgentRunner` interface), `notification.test.ts` (0 mocks, pure functions + DI), and `agent-tool.test.ts` (0 mocks, tests via deps bag) - confirmed the pattern: modules that accept collaborators through injection produce resilient tests; modules that import collaborators directly produce fragile mock-heavy tests.
|
|
22
|
+
|
|
23
|
+
## Step F: Shared test fixtures (#131)
|
|
24
|
+
|
|
25
|
+
Consolidated duplicated mock factories into `test/helpers/`.
|
|
26
|
+
|
|
27
|
+
1. `createMockSession()` - subscribable event bus with `emit()` helper; replaced 3 hand-rolled copies.
|
|
28
|
+
2. `createToolDeps()` - builds `AgentToolDeps` with sensible defaults and override support; replaced 3 `makeDeps()` copies.
|
|
29
|
+
3. `makeRecord()` - `AgentRecord` factory with sensible defaults; replaced scattered inline construction.
|
|
30
|
+
4. `STUB_CTX` - shared stub `ExtensionContext` constant; centralised unavoidable bridge casts.
|
|
31
|
+
|
|
32
|
+
Impact: reduced test boilerplate; single source of truth for mock shapes; changes to dep interfaces propagate automatically.
|
|
33
|
+
|
|
34
|
+
## Step G: Inject IO collaborators into session-config (#132)
|
|
35
|
+
|
|
36
|
+
`assembleSessionConfig` now accepts `io: AssemblerIO` as a required parameter.
|
|
37
|
+
`index.ts` constructs the real `AssemblerIO` from direct imports via the `RunnerIO.assemblerIO` field (wired in Step H).
|
|
38
|
+
`session-config.test.ts` injects stubs - all 4 `vi.mock()` calls eliminated, assertions shifted to `SessionConfig` output properties.
|
|
39
|
+
|
|
40
|
+
## Step H: Inject SDK boundary into agent-runner (#133)
|
|
41
|
+
|
|
42
|
+
`runAgent()` now accepts `io: RunnerIO` as a required parameter bundling all IO collaborators: `detectEnv`, `getAgentDir`, `createResourceLoader`, `deriveSessionDir`, `createSessionManager`, `createSettingsManager`, `createSession`, and `assemblerIO`.
|
|
43
|
+
|
|
44
|
+
`createAgentRunner(io: RunnerIO): AgentRunner` factory captures the boundary at construction time so `AgentManager` and the `AgentRunner` interface remain unchanged.
|
|
45
|
+
`index.ts` constructs the real `RunnerIO` from Pi SDK imports and sibling modules.
|
|
46
|
+
|
|
47
|
+
Impact: all 7 `vi.mock()` calls eliminated from both `agent-runner.test.ts` and `agent-runner-extension-tools.test.ts`; tests verify behavior (turn limits, tool filtering, response collection) through injected stubs; SDK imports moved to the extension entry point.
|
|
48
|
+
|
|
49
|
+
## Step I: Reduce `as any` casts in tests (#134)
|
|
50
|
+
|
|
51
|
+
Reduced `as any` count from 93 to 15 (plus 13 explicit `as unknown as T` bridge casts).
|
|
52
|
+
|
|
53
|
+
Production changes:
|
|
54
|
+
|
|
55
|
+
- `ResourceLoaderOptions.appendSystemPromptOverride` typed to match `DefaultResourceLoaderOptions`; `createResourceLoader` factory cast removed from `index.ts`.
|
|
56
|
+
- `CreateSessionOptions.settingsManager` / `RunnerIO.createSettingsManager` typed as `SettingsManager`.
|
|
57
|
+
- `WidgetLike` interface in `runtime.ts` narrows the widget field.
|
|
58
|
+
- Local `ToolCallContent` / `BashExecutionMessage` type guards replace `as any` duck-typing in `conversation-viewer.ts` and `agent-runner.ts`.
|
|
59
|
+
- `textResult()` return no longer casts `details as any`.
|
|
60
|
+
- `toAgentSession()` helper and `STUB_CTX` constant centralise unavoidable bridge casts.
|
|
61
|
+
|
|
62
|
+
Remaining 15 `as any` casts are: 8 menu-handler `ctx as any` (deferred - requires `AgentManager.spawn` to accept `ParentSnapshot` directly), 2 `print-mode.test.ts` (same ExtensionContext/API pattern), 2 private-field test access, 1 `createSession` SDK bridge in `index.ts`, 1 `foreground-runner.ts` `AgentToolResult<any>` detail, 1 `stub-ctx.ts` comment.
|
|
63
|
+
|
|
64
|
+
## Step J: Extract display helpers (#135)
|
|
65
|
+
|
|
66
|
+
`ui/display.ts` now contains all pure formatters, display helpers, constants, and shared types (`Theme`, `AgentDetails`).
|
|
67
|
+
`agent-widget.ts` dropped from 522 → ~340 lines.
|
|
68
|
+
All consumer modules (menu, tools, renderer, conversation viewer) import from `ui/display.ts` directly.
|
|
69
|
+
`test/agent-widget.test.ts` renamed to `test/display.test.ts`.
|
|
70
|
+
|
|
71
|
+
## Step K: Decompose agent-menu.ts (#136)
|
|
72
|
+
|
|
73
|
+
`agent-menu.ts` (668 lines) decomposed into four modules:
|
|
74
|
+
|
|
75
|
+
1. `ui/agent-file-ops.ts` - `AgentFileOps` interface (`exists`, `read`, `write`, `remove`, `ensureDir`, `findAgentFile`) + `FsAgentFileOps` production implementation.
|
|
76
|
+
2. `ui/agent-config-editor.ts` - `showAgentDetail` with edit/delete/reset/eject/disable/enable transitions (~200 lines).
|
|
77
|
+
3. `ui/agent-creation-wizard.ts` - AI-generation and manual-form creation paths (~250 lines).
|
|
78
|
+
4. `ui/agent-menu.ts` - menu orchestration, agent listing, running-agent viewer, settings form (~300 lines).
|
|
79
|
+
|
|
80
|
+
Impact: `agent-menu.ts` dropped from 668 → 296 lines; extracted modules receive `AgentFileOps` via injection; `vi.mock("node:fs")` eliminated from `agent-menu.test.ts`.
|
|
81
|
+
|
|
82
|
+
## Step dependencies
|
|
83
|
+
|
|
84
|
+
```mermaid
|
|
85
|
+
flowchart LR
|
|
86
|
+
subgraph testability["Testability track"]
|
|
87
|
+
F["F: Shared fixtures"] --> G["G: session-config IO"] --> H["H: agent-runner SDK"] --> I["I: Reduce as-any"]
|
|
88
|
+
end
|
|
89
|
+
subgraph display["Display track"]
|
|
90
|
+
J["J: Display extraction"] --> K["K: Menu decomposition"]
|
|
91
|
+
end
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The two tracks are independent and can proceed in parallel.
|
|
95
|
+
|
|
96
|
+
## Related issues
|
|
97
|
+
|
|
98
|
+
- #131 — Shared test fixtures
|
|
99
|
+
- #132 — Inject IO collaborators into session-config
|
|
100
|
+
- #133 — Inject SDK boundary into agent-runner
|
|
101
|
+
- #134 — Reduce as-any casts in tests
|
|
102
|
+
- #135 — Extract display helpers
|
|
103
|
+
- #136 — Decompose agent-menu.ts
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Phase 9: Observation consolidation, ctx elimination, and remaining mocks
|
|
2
|
+
|
|
3
|
+
Target: consolidate the dual observation model so stats live in one place; remove `ExtensionContext` from all internal APIs; eliminate remaining `vi.mock()` calls and `as any` casts; split widget rendering from lifecycle; apply dependency bag convention.
|
|
4
|
+
|
|
5
|
+
## Current smells
|
|
6
|
+
|
|
7
|
+
| Smell | Location | Evidence | Severity |
|
|
8
|
+
| ------------------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | -------- |
|
|
9
|
+
| `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 |
|
|
10
|
+
| ~~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 |
|
|
11
|
+
| ~~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 |
|
|
12
|
+
| ~~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 |
|
|
13
|
+
| `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 |
|
|
14
|
+
|
|
15
|
+
## Dependency bag convention
|
|
16
|
+
|
|
17
|
+
Applied incrementally as each step touches a module:
|
|
18
|
+
|
|
19
|
+
- **≤4 fields** - accept as plain parameters; drop the interface.
|
|
20
|
+
- **≥5 fields** - keep a named interface but destructure in the function signature (`{ manager, widget }: ForegroundDeps`) so the function body uses bare names, not `deps.foo`.
|
|
21
|
+
|
|
22
|
+
This eliminates the `deps.` prefix noise across ~124 callsites in 12 modules.
|
|
23
|
+
|
|
24
|
+
## Step L: Consolidate observation model (#144)
|
|
25
|
+
|
|
26
|
+
Removed `_toolUses` and `_lifetimeUsage` from `AgentActivityTracker`.
|
|
27
|
+
UI consumers read stats from `AgentRecord` instead of the tracker.
|
|
28
|
+
The UI observer retains event subscriptions for re-render triggers but no longer accumulates stats independently.
|
|
29
|
+
|
|
30
|
+
Add `session` and `outputFile` convenience getters on `AgentRecord` to hide the `execution?.` traversal.
|
|
31
|
+
The 15+ callsites that navigate `record.execution?.session` simplify to `record.session`.
|
|
32
|
+
|
|
33
|
+
Apply the dependency bag convention to touched modules: `NotificationDeps` (4 fields) becomes plain parameters on `NotificationManager` constructor.
|
|
34
|
+
|
|
35
|
+
Impact: eliminates dual counting; removes `??` fallback pattern from widget and conversation viewer; hides `ExecutionState` structure from consumers.
|
|
36
|
+
|
|
37
|
+
## Step M: Decompose execute and push ExtensionContext to the boundary (#145)
|
|
38
|
+
|
|
39
|
+
Extracted config resolution into `resolveSpawnConfig` (pure function in `spawn-config.ts`).
|
|
40
|
+
Injected three collaborators (`buildSnapshot`, `getModelInfo`, `getSessionInfo`) into `createAgentTool` so `execute` no longer reads `ctx` beyond `ctx.ui` (already delegated to `widget.setUICtx`).
|
|
41
|
+
`AgentManager.spawn()` and `spawnAndWait()` accept `ParentSnapshot` instead of `ExtensionContext`.
|
|
42
|
+
`service-adapter.ts` calls `buildParentSnapshot(session.ctx)` at its boundary.
|
|
43
|
+
`foreground-runner` and `background-spawner` receive `ResolvedSpawnConfig` + domain values (`snapshot`, `parentSessionFile`, `parentSessionId`) instead of `ctx`.
|
|
44
|
+
|
|
45
|
+
Dissolved `ForegroundDeps`, `BackgroundDeps`, and `AdapterDeps` into plain parameters.
|
|
46
|
+
`AgentToolDeps` is destructured in the `createAgentTool` signature.
|
|
47
|
+
|
|
48
|
+
After this step, `ExtensionContext` appears only in:
|
|
49
|
+
|
|
50
|
+
- `index.ts` closures (wired at extension startup)
|
|
51
|
+
- `service-adapter.ts` (cross-extension boundary)
|
|
52
|
+
- Menu handlers (addressed by Step N)
|
|
53
|
+
|
|
54
|
+
Impact: `execute` dropped from ~145 to ~25 lines; eliminated 16-field parameter bags; eliminated `vi.mock("../src/parent-snapshot.js")` in `agent-manager.test.ts`; foreground/background runner tests no longer need `ctx` mocks; `AgentManager` operates entirely on domain types.
|
|
55
|
+
|
|
56
|
+
## Step N: Narrow UI context for menu handlers (#146)
|
|
57
|
+
|
|
58
|
+
Defined `MenuUI` interface (exported from `agent-menu.ts`) with `select`, `confirm`, `input`, `notify`, `editor`, and `custom` methods — the exact subset `ctx.ui` methods used by menu handlers.
|
|
59
|
+
All inner functions in `agent-menu.ts`, `agent-config-editor.ts`, and `agent-creation-wizard.ts` now accept `(ui: MenuUI)` instead of `(ctx: ExtensionContext)`.
|
|
60
|
+
`index.ts` passes `ctx.ui`, `ctx.modelRegistry`, and `buildParentSnapshot(ctx)` to the handler.
|
|
61
|
+
|
|
62
|
+
`AgentMenuManager.spawnAndWait` and `WizardManager.spawnAndWait` both accept `ParentSnapshot` (enabled by Step M).
|
|
63
|
+
Creation wizard threads `parentSnapshot` from `showCreateWizard(ui, parentSnapshot)` → `showGenerateWizard(ui, parentSnapshot, targetDir)` → `manager.spawnAndWait(parentSnapshot, ...)`.
|
|
64
|
+
|
|
65
|
+
Applied the dependency bag convention:
|
|
66
|
+
|
|
67
|
+
- `AgentConfigEditorDeps` (4 fields), `GetResultDeps` (4 fields), `SteerToolDeps` (4 fields) dissolved into plain parameters.
|
|
68
|
+
- `AgentMenuDeps` (8 fields) and `AgentCreationWizardDeps` (5 fields) kept as interfaces, destructured in the function signature.
|
|
69
|
+
|
|
70
|
+
After Steps M and N, `ExtensionContext` appears only at true boundaries: `index.ts` closures and `service-adapter.ts` (cross-extension bridge).
|
|
71
|
+
|
|
72
|
+
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.
|
|
73
|
+
|
|
74
|
+
## Step O: Inject text wrapping into ConversationViewer (#147)
|
|
75
|
+
|
|
76
|
+
Accepted `wrapText: (text: string, width: number) => string[]` via `ConversationViewerOptions`.
|
|
77
|
+
`agent-menu.ts` passes the real `wrapTextWithAnsi` import at the `ConversationViewer` construction site.
|
|
78
|
+
Tests inject a stub or the real function directly via options — no module-level mock needed.
|
|
79
|
+
|
|
80
|
+
Applied the dependency bag convention: `ConversationViewerOptions` is now destructured in the constructor signature.
|
|
81
|
+
|
|
82
|
+
Impact: eliminated the hoisted `vi.mock("@earendil-works/pi-tui")` from `conversation-viewer.test.ts`; 1 test deleted (mock-mechanism sentinel); net −1 test.
|
|
83
|
+
|
|
84
|
+
## Step P: Split AgentWidget rendering (#148)
|
|
85
|
+
|
|
86
|
+
Extracted pure rendering functions (`renderWidgetLines`, `renderFinishedLine`, `renderRunningLines`) from `AgentWidget` into `ui/widget-renderer.ts`.
|
|
87
|
+
The widget is now a thin lifecycle/polling wrapper (198 lines, down from 374) that delegates to pure render functions.
|
|
88
|
+
Rendering functions receive data (agent list, activity map, registry) and return formatted strings - testable without widget lifecycle. 23 new unit tests cover all status variants, overflow, tree connectors, and empty states.
|
|
89
|
+
|
|
90
|
+
## Step dependencies
|
|
91
|
+
|
|
92
|
+
```mermaid
|
|
93
|
+
flowchart LR
|
|
94
|
+
subgraph observation["Observation track"]
|
|
95
|
+
L["L: Consolidate observation #144"] --> P["P: Split widget rendering #148"]
|
|
96
|
+
end
|
|
97
|
+
subgraph ctx["ctx elimination track"]
|
|
98
|
+
M["M: Decompose execute / push ctx #145"] --> N["N: Narrow UI context #146"]
|
|
99
|
+
end
|
|
100
|
+
O["O: Inject text wrapping #147"]
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The three tracks are independent of each other.
|
|
104
|
+
|
|
105
|
+
## Projected impact
|
|
106
|
+
|
|
107
|
+
| Metric | Before | After |
|
|
108
|
+
| ---------------------------------- | ------------------------ | ------------------------ |
|
|
109
|
+
| `vi.mock()` calls remaining | 4 | 1 (`print-mode.test.ts`) |
|
|
110
|
+
| `as any` casts remaining | 45 | ~5 |
|
|
111
|
+
| Independent tool-use counters | 2 | 1 |
|
|
112
|
+
| `record.execution?.` traversals | 15+ | 0 |
|
|
113
|
+
| `ExtensionContext` in domain types | 1 (`AgentManager.spawn`) | 0 |
|
|
114
|
+
| `deps.` prefix accesses | ~124 | 0 |
|
|
115
|
+
|
|
116
|
+
## Related issues
|
|
117
|
+
|
|
118
|
+
- #144 — Consolidate observation model
|
|
119
|
+
- #145 — Decompose execute and push ExtensionContext to boundary
|
|
120
|
+
- #146 — Narrow UI context for menu handlers
|
|
121
|
+
- #147 — Inject text wrapping into ConversationViewer
|
|
122
|
+
- #148 — Split AgentWidget rendering
|
|
@@ -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.
|