@gotgenes/pi-subagents 6.11.0 → 6.12.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 CHANGED
@@ -5,6 +5,35 @@ 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.12.1](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.12.0...pi-subagents-v6.12.1) (2026-05-22)
9
+
10
+
11
+ ### Documentation
12
+
13
+ * mark Step J done, add ui/display.ts to module listing ([#135](https://github.com/gotgenes/pi-packages/issues/135)) ([37ced45](https://github.com/gotgenes/pi-packages/commit/37ced45e1a0287aa78e588cb8bc7905c0f969637))
14
+ * plan display helper extraction from agent-widget ([#135](https://github.com/gotgenes/pi-packages/issues/135)) ([9e65e7d](https://github.com/gotgenes/pi-packages/commit/9e65e7d93bf47d4c4582d367d2f31a2386a5cc8c))
15
+ * **retro:** add retro notes for issue [#134](https://github.com/gotgenes/pi-packages/issues/134) ([775ce99](https://github.com/gotgenes/pi-packages/commit/775ce99710153d4ebcf40f773eae21553c7f8a82))
16
+
17
+ ## [6.12.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.11.0...pi-subagents-v6.12.0) (2026-05-22)
18
+
19
+
20
+ ### Features
21
+
22
+ * narrow runtime widget field to WidgetLike interface ([#134](https://github.com/gotgenes/pi-packages/issues/134)) ([afa70ab](https://github.com/gotgenes/pi-packages/commit/afa70ab430109248a8f61ccd182b0f3acd1fa7e1))
23
+ * use SDK types in CreateSessionOptions ([#134](https://github.com/gotgenes/pi-packages/issues/134)) ([c2452af](https://github.com/gotgenes/pi-packages/commit/c2452af0ee3d47d778878443a634ca787f8d0bfb))
24
+
25
+
26
+ ### Bug Fixes
27
+
28
+ * replace message-shape as-any casts with type guards ([#134](https://github.com/gotgenes/pi-packages/issues/134)) ([d7ad65a](https://github.com/gotgenes/pi-packages/commit/d7ad65a61267790ae1ae8414b0c2aa9ebc8ad59c))
29
+
30
+
31
+ ### Documentation
32
+
33
+ * plan as-any cast reduction in test suite ([#134](https://github.com/gotgenes/pi-packages/issues/134)) ([f7cb1aa](https://github.com/gotgenes/pi-packages/commit/f7cb1aac0963021ae0545b73c88f950a7adb5fd2))
34
+ * **retro:** add retro notes for issue [#133](https://github.com/gotgenes/pi-packages/issues/133) ([be32640](https://github.com/gotgenes/pi-packages/commit/be32640048943059a98fc79797a35dfefd70fc34))
35
+ * update architecture doc for Step I completion ([#134](https://github.com/gotgenes/pi-packages/issues/134)) ([fd4aca7](https://github.com/gotgenes/pi-packages/commit/fd4aca79c74da2b8c4e3c58e2376e0612941d7d9))
36
+
8
37
  ## [6.11.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.10.0...pi-subagents-v6.11.0) (2026-05-22)
9
38
 
10
39
 
@@ -68,6 +68,7 @@ notification.ts — completion nudges, custom message renderer
68
68
  renderer.ts — notification TUI component
69
69
  record-observer.ts — session-event observer for record statistics
70
70
 
71
+ ui/display.ts — pure formatters, display helpers, and shared types (Theme, AgentDetails)
71
72
  ui/agent-widget.ts — above-editor live status widget
72
73
  ui/agent-menu.ts — /agents slash command menu
73
74
  ui/conversation-viewer.ts — scrollable session overlay
@@ -517,7 +518,7 @@ They are included here because the display extraction unblocks menu decompositio
517
518
  | ----------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------- |
518
519
  | ~~7 `vi.mock()` calls~~ | ~~`agent-runner.test.ts`~~ | ~~Resolved by Step H (#133)~~ |
519
520
  | ~~7 `vi.mock()` calls~~ | ~~`agent-runner-extension-tools.test.ts`~~ | ~~Resolved by Step H (#133)~~ |
520
- | 52 `as any` casts | Across test suite | SDK session/context interfaces too wide to construct in tests |
521
+ | ~~52 `as any` casts~~ | ~~Across test suite~~ | ~~Reduced to 15 by Step I (#134)~~ |
521
522
  | 3× duplicated `mockSession()` | agent-manager, record-observer, ui-observer tests | No shared test fixture |
522
523
  | 3× duplicated `makeDeps()` | agent-tool, background-spawner, foreground-runner tests | No shared tool-deps fixture |
523
524
  | Weak assertions | lifecycle, renderer, session-config tests | `toHaveBeenCalled()` without args, `toContain()` on large strings |
@@ -549,27 +550,27 @@ Impact: reduces test boilerplate; single source of truth for mock shapes; change
549
550
 
550
551
  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.
551
552
 
552
- ### Step I: Reduce `as any` casts in tests (#134)
553
+ ### Step I: Reduce `as any` casts in tests (#134) ✓ done
553
554
 
554
- With Steps G and H, many `as any` casts disappear because tests construct narrow injectable interfaces instead of wide SDK types.
555
- Remaining casts are addressed by:
555
+ Reduced `as any` count from 93 to 15 (plus 13 explicit `as unknown as T` bridge casts).
556
556
 
557
- 1. Defining a `TestSession` type in `test/helpers/` that satisfies `SubscribableSession` + the fields tests actually read.
558
- 2. Replacing `const mockCtx = { cwd: "/tmp" } as any` with properly typed `AssemblerContext` or `ParentSnapshot` objects.
559
- 3. Using `satisfies` assertions where possible instead of `as any`.
557
+ Production changes:
560
558
 
561
- Target: reduce `as any` count from 52 to under 10.
559
+ - `ResourceLoaderOptions.appendSystemPromptOverride` typed to match `DefaultResourceLoaderOptions`; `createResourceLoader` factory cast removed from `index.ts`.
560
+ - `CreateSessionOptions.settingsManager` / `RunnerIO.createSettingsManager` typed as `SettingsManager`.
561
+ - `WidgetLike` interface in `runtime.ts` narrows the widget field.
562
+ - Local `ToolCallContent` / `BashExecutionMessage` type guards replace `as any` duck-typing in `conversation-viewer.ts` and `agent-runner.ts`.
563
+ - `textResult()` return no longer casts `details as any`.
564
+ - `toAgentSession()` helper and `STUB_CTX` constant centralise unavoidable bridge casts.
562
565
 
563
- ### Step J: Extract display helpers (#135)
566
+ 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.
564
567
 
565
- `agent-widget.ts` (600 lines) exports 11 helper functions and constants that are used by both the widget and the menu.
566
- Extract these into `ui/display.ts`:
568
+ ### Step J: Extract display helpers (#135) done
567
569
 
568
- - Pure formatters: `formatTokens`, `formatSessionTokens`, `formatTurns`, `formatMs`, `formatDuration`.
569
- - Display helpers: `getDisplayName`, `getPromptModeLabel`, `buildInvocationTags`, `describeActivity`.
570
- - Constants: `SPINNER`, `ERROR_STATUSES`, `TOOL_DISPLAY`.
571
-
572
- Impact: `agent-widget.ts` drops from 600 → ~420 lines; shared display logic has a single import point; menu and tool modules stop importing from the widget.
570
+ `ui/display.ts` now contains all pure formatters, display helpers, constants, and shared types (`Theme`, `AgentDetails`).
571
+ `agent-widget.ts` dropped from 522 ~340 lines.
572
+ All consumer modules (menu, tools, renderer, conversation viewer) import from `ui/display.ts` directly.
573
+ `test/agent-widget.test.ts` renamed to `test/display.test.ts`.
573
574
 
574
575
  ### Step K: Decompose agent-menu.ts (#136)
575
576
 
@@ -0,0 +1,366 @@
1
+ ---
2
+ issue: 134
3
+ issue_title: "Reduce `as any` casts in test suite"
4
+ ---
5
+
6
+ # Reduce as-any casts in test suite
7
+
8
+ ## Problem Statement
9
+
10
+ The test suite contains 93 `as any` casts (plus 2 `as any[]`).
11
+ These casts silence the type checker, hiding real errors — if a production interface adds a required field, tests silently pass with incomplete mocks instead of failing at compile time.
12
+ The heaviest offenders are `renderer.test.ts` (29), `runtime.test.ts` (10), `agent-menu.test.ts` (8), and `helpers.test.ts` (7).
13
+ The production source has 8 `as any` casts — 2 SDK bridge casts in `index.ts` and 6 message-shape casts in `conversation-viewer.ts` and `agent-runner.ts`.
14
+
15
+ ## Goals
16
+
17
+ - Target near-zero `as any` casts across both source and test code.
18
+ - Widen `CreateSessionOptions` and `ResourceLoaderOptions` to use SDK types, eliminating the SDK bridge casts in `index.ts`.
19
+ - Define local message-content types and type guards in `conversation-viewer.ts` and `agent-runner.ts`, eliminating polymorphic duck-typing casts.
20
+ - Define narrow production interfaces (`MenuCtx`, `WidgetLike`) where SDK types are too wide for test construction.
21
+ - Define typed test factories where partial mocks currently require casting.
22
+ - Preserve existing test coverage — no behavior changes.
23
+
24
+ ## Non-Goals
25
+
26
+ - Changing production behavior or public API shapes.
27
+ - Reaching literally zero — 1–3 casts may remain for private-field test access (`(manager as any).cleanupInterval`) where the alternative (exposing internals) is worse than the cast.
28
+
29
+ ## Background
30
+
31
+ ### Prerequisites
32
+
33
+ Issues #132 and #133 (IO injection) are both closed.
34
+ These eliminated the `vi.mock()`-heavy test patterns and introduced narrow injectable interfaces (`AssemblerIO`, `RunnerIO`), which already removed some `as any` casts from `agent-runner.test.ts` and `session-config.test.ts`.
35
+
36
+ ### Current as-any inventory by pattern
37
+
38
+ | Pattern | Count | Primary files |
39
+ | ------------------------------------------------ | ----- | ------------------------------------------------------------------------------------------------------------------- |
40
+ | Renderer — theme, message, result access | 29 | `renderer.test.ts` |
41
+ | Context — `ctx as any` for handler/menu | 14 | `agent-menu.test.ts`, `agent-manager.test.ts`, `print-mode.test.ts`, `parent-snapshot.test.ts`, `make-deps.test.ts` |
42
+ | Session — `{ session: {} as any }` | 10 | tool tests, `service-adapter.test.ts`, `print-mode.test.ts`, `agent-manager.test.ts` |
43
+ | Runtime/widget — `fakeWidget as any` | 9 | `runtime.test.ts` |
44
+ | Conversation viewer — message shapes | 8 | `conversation-viewer.test.ts`, `src/conversation-viewer.ts` |
45
+ | Helpers — registry, activity, details | 7 | `helpers.test.ts` |
46
+ | RunOptions — `} as any, io)` | 3 | `agent-runner.test.ts` |
47
+ | Tool execute — `{} as any` for ctx | 5 | `steer-tool.test.ts`, `get-result-tool.test.ts`, `make-deps.test.ts` |
48
+ | SDK bridge — `opts as any` in index.ts | 2 | `src/index.ts` |
49
+ | Other — `messages: [] as any[]`, internal access | 6 | `agent-runner*.test.ts`, `usage.test.ts`, `agent-manager.test.ts` |
50
+
51
+ ### Constraints from AGENTS.md
52
+
53
+ - Avoid `any` unless absolutely necessary.
54
+ - Prefer explicit configuration over hidden behavior.
55
+ - Keep scope tight; prefer small, reversible changes.
56
+
57
+ ## Design Overview
58
+
59
+ ### Production changes that make the test changes easy
60
+
61
+ Three targeted source-code changes remove the structural blockers that force casts elsewhere.
62
+
63
+ #### 1. SDK-typed option interfaces (eliminates 2 source casts)
64
+
65
+ `CreateSessionOptions` and `ResourceLoaderOptions` currently use `unknown` for opaque fields (`settingsManager`, `modelRegistry`, `model`).
66
+ Tests never construct these option objects — they mock `io.createSession` and `io.createResourceLoader` at the function level.
67
+ The `unknown` fields don't buy testability; the function-level mock does.
68
+
69
+ The SDK exports `CreateAgentSessionOptions` with all fields optional, using `ModelRegistry`, `Model<any>`, `ResourceLoader`, `SessionManager`, `SettingsManager`.
70
+ Widening `CreateSessionOptions` to use these SDK types makes the `as any` cast in `index.ts` unnecessary while preserving full test mockability.
71
+
72
+ ```typescript
73
+ // Before (agent-runner.ts)
74
+ export interface CreateSessionOptions {
75
+ settingsManager: unknown;
76
+ modelRegistry: unknown;
77
+ model?: unknown;
78
+ // ...
79
+ }
80
+
81
+ // After — use SDK types (type-only imports)
82
+ import type { Model } from "@earendil-works/pi-ai";
83
+ import type {
84
+ ModelRegistry,
85
+ ResourceLoader,
86
+ SessionManager,
87
+ SettingsManager,
88
+ } from "@earendil-works/pi-coding-agent";
89
+
90
+ export interface CreateSessionOptions {
91
+ settingsManager: SettingsManager;
92
+ modelRegistry: ModelRegistry;
93
+ model?: Model<any>;
94
+ resourceLoader: ResourceLoader;
95
+ sessionManager: SessionManager;
96
+ // ...
97
+ }
98
+ ```
99
+
100
+ The `RunnerIO.createSettingsManager` return type also changes from `unknown` to `SettingsManager`.
101
+
102
+ For `ResourceLoaderOptions`, the options are all primitives and callbacks — the SDK's `DefaultResourceLoader` constructor accepts them structurally.
103
+ The fix is to ensure the option interface matches the constructor's parameter type so `new DefaultResourceLoader(opts)` works without `as any`.
104
+
105
+ #### 2. Local message-content types + type guards (eliminates 6 source casts)
106
+
107
+ `conversation-viewer.ts` and `agent-runner.ts` both do:
108
+
109
+ ```typescript
110
+ (c as any).name ?? (c as any).toolName ?? "unknown"
111
+ (msg as any).role === "bashExecution"
112
+ const bash = msg as any;
113
+ ```
114
+
115
+ Fix: define local discriminated-union types for the content items and message roles the code actually handles, plus type guards:
116
+
117
+ ```typescript
118
+ /** Tool-call content item — SDK doesn't export a narrow type for this variant. */
119
+ interface ToolCallContent {
120
+ type: "toolCall";
121
+ name?: string;
122
+ toolName?: string;
123
+ }
124
+
125
+ function getToolCallName(c: { type: string }): string | undefined {
126
+ if (c.type !== "toolCall") return undefined;
127
+ const tc = c as ToolCallContent;
128
+ return tc.name ?? tc.toolName;
129
+ }
130
+
131
+ /** Bash execution message — not in the standard role union. */
132
+ interface BashExecutionMessage {
133
+ role: "bashExecution";
134
+ command: string;
135
+ output?: string;
136
+ }
137
+
138
+ function isBashExecution(msg: { role: string }): msg is BashExecutionMessage {
139
+ return msg.role === "bashExecution";
140
+ }
141
+ ```
142
+
143
+ These are small, file-local types that document the shapes the code already handles at runtime.
144
+ They make the duck-typing explicit and compile-time checked.
145
+
146
+ #### 3. Narrow production interfaces (`MenuCtx`, `WidgetLike`)
147
+
148
+ Already described in the test-side patterns below.
149
+ These are production-code changes that make the test-side casts disappear.
150
+
151
+ ### Test-side patterns
152
+
153
+ #### Pattern A: renderer.test.ts (29 casts → 0)
154
+
155
+ The renderer already defines narrow interfaces (`RendererMessage`, `RendererTheme`, `RenderOptions`).
156
+ The test casts because it doesn't use them — `stubTheme()` and `{ details: makeDetails() }` already satisfy the interfaces structurally.
157
+ The return type `Text` has a `.text` property — `(result as any).text` can become `result!.text` (non-null assertion after `expect(result).toBeDefined()`).
158
+
159
+ Fix: remove the casts; structural typing handles it.
160
+ For result access, define a minimal `{ text: string }` type or use non-null assertion.
161
+
162
+ #### Pattern B: context casts (14 casts → 0)
163
+
164
+ - `agent-menu.test.ts` (8): define `MenuCtx` in production; type `makeCtx()` return.
165
+ - `agent-manager.test.ts` (1): `mockCtx` is consumed by a mocked `buildParentSnapshot` — type as `unknown`.
166
+ - `parent-snapshot.test.ts` (1): define a test-local interface matching the fields `buildParentSnapshot` reads.
167
+ - `print-mode.test.ts` (2): type `makeHeadlessCtx()` return.
168
+ - `runtime.test.ts` (1): pass a structurally valid `UICtx` (fixed by `WidgetLike` step).
169
+ - `make-deps.test.ts` (3): type ctx args or use `unknown`.
170
+
171
+ #### Pattern C: session casts (10 casts → ~1)
172
+
173
+ Most are `{ session: {} as any, outputFile: ... }` for `record.execution`.
174
+ Fix: expand `createMockSession()` to include the fields these tests need (`dispose`, `steer`, `getSessionStats`), then use it.
175
+ One cast may remain for truly minimal session stubs.
176
+
177
+ #### Pattern D: runtime/widget casts (9 casts → 0)
178
+
179
+ Define `WidgetLike` in `runtime.ts`; change the `widget` field type.
180
+ Use real `AgentActivityTracker` instances (constructor takes only `maxTurns?`).
181
+
182
+ #### Pattern E: tool-execute ctx casts (5 casts → 0)
183
+
184
+ Define a `STUB_CTX` constant in `test/helpers/` satisfying the tool execute's context parameter.
185
+
186
+ #### Pattern F: RunOptions casts (3 casts → 0)
187
+
188
+ Check whether `defaultMaxTurns` and `graceTurns` are on `RunOptions` — they are.
189
+ The `as any` casts are vestigial; remove them.
190
+
191
+ #### Pattern G: helpers.test.ts (7 casts → 0)
192
+
193
+ Construct typed `TypeListRegistry` mocks.
194
+ Fix `textResult` return type to avoid `details as any`.
195
+ Use real `AgentActivityTracker` instances.
196
+
197
+ #### Pattern H: conversation-viewer test casts (4 test casts → ~1)
198
+
199
+ Type message mock objects using the local types from production change #2.
200
+ Keep `(viewer as any).buildContentLines()` (private method access — the alternative of exposing the method is worse).
201
+
202
+ #### Pattern I: other (6 casts → ~2)
203
+
204
+ - `messages: [] as any[]` → type as `unknown[]`.
205
+ - `(manager as any).cleanupInterval` → keep (private field assertion is intentional).
206
+ - `usage.test.ts` (2) → type mock objects.
207
+
208
+ ## Module-Level Changes
209
+
210
+ ### Modified source files
211
+
212
+ 1. `src/agent-runner.ts`
213
+ - Import SDK types (`Model`, `ModelRegistry`, `SettingsManager`, `SessionManager`, `ResourceLoader`) as type-only imports.
214
+ - Widen `CreateSessionOptions` fields from `unknown` to SDK types.
215
+ - Change `RunnerIO.createSettingsManager` return type from `unknown` to `SettingsManager`.
216
+ - Define `getToolCallName()` helper and local `ToolCallContent` interface.
217
+ - Replace `(c as any).name ?? (c as any).toolName` with `getToolCallName(c)` in `getAgentConversation`.
218
+
219
+ 2. `src/ui/conversation-viewer.ts`
220
+ - Define local `ToolCallContent`, `BashExecutionMessage` interfaces and type guards.
221
+ - Replace `(c as any).name ?? (c as any).toolName` with typed helper.
222
+ - Replace `(msg as any).role === "bashExecution"` / `const bash = msg as any` with type guard.
223
+
224
+ 3. `src/ui/agent-menu.ts`
225
+ - Define and export `MenuCtx` interface.
226
+ - Change handler parameter type from `ExtensionContext` to `MenuCtx`.
227
+
228
+ 4. `src/runtime.ts`
229
+ - Define and export `WidgetLike` interface.
230
+ - Change `widget` field type from `AgentWidget | null` to `WidgetLike | null`.
231
+
232
+ 5. `src/tools/helpers.ts`
233
+ - Change `textResult` to avoid `details as any` — type the return properly.
234
+
235
+ 6. `src/index.ts`
236
+ - Remove `opts as any` casts in `createResourceLoader` and `createSession` factories (enabled by SDK-typed option interfaces).
237
+
238
+ ### Modified test files
239
+
240
+ 1. `test/renderer.test.ts` — remove all 29 casts.
241
+ 2. `test/runtime.test.ts` — use `WidgetLike` stubs and real `AgentActivityTracker`.
242
+ 3. `test/ui/agent-menu.test.ts` — type `makeCtx()` as `MenuCtx`.
243
+ 4. `test/tools/helpers.test.ts` — typed registry mocks, real `AgentActivityTracker`.
244
+ 5. `test/conversation-viewer.test.ts` — typed message mocks.
245
+ 6. `test/agent-runner.test.ts` — remove `as any` on RunOptions; type messages.
246
+ 7. `test/agent-runner-extension-tools.test.ts` — type messages.
247
+ 8. `test/agent-manager.test.ts` — type `mockCtx`.
248
+ 9. `test/print-mode.test.ts` — type `makeHeadlessCtx()`.
249
+ 10. `test/parent-snapshot.test.ts` — type mock context.
250
+ 11. `test/service-adapter.test.ts` — use `createMockSession()`.
251
+ 12. `test/tools/steer-tool.test.ts` — stub ctx, use `createMockSession()`.
252
+ 13. `test/tools/get-result-tool.test.ts` — stub ctx, use `createMockSession()`.
253
+ 14. `test/tools/foreground-runner.test.ts` — use `createMockSession()`.
254
+ 15. `test/tools/background-spawner.test.ts` — use `createMockSession()`.
255
+ 16. `test/tools/agent-tool.test.ts` — use `createMockSession()`.
256
+ 17. `test/helpers/make-deps.test.ts` — type ctx args.
257
+ 18. `test/usage.test.ts` — type mock objects.
258
+
259
+ ## Test Impact Analysis
260
+
261
+ 1. No new test coverage — this is a type-safety improvement on existing tests.
262
+ 2. No tests become redundant.
263
+ 3. All existing tests stay as-is in terms of assertions; only type annotations and mock construction change.
264
+ 4. `pnpm run check` is the primary validation — every step must pass type checking since the goal is eliminating type holes.
265
+ 5. Expanding `createMockSession()` affects multiple consumers — diff existing defaults before changing (per testing skill).
266
+
267
+ ## TDD Order
268
+
269
+ Steps are ordered by independence and impact (production changes first, then largest cast-count test reductions).
270
+
271
+ 1. **Widen option interfaces to SDK types; remove index.ts casts (2 source casts → 0).**
272
+ Import SDK types as type-only in `agent-runner.ts`.
273
+ Widen `CreateSessionOptions` fields (`settingsManager`, `modelRegistry`, `model`, `resourceLoader`, `sessionManager`) to SDK types.
274
+ Change `RunnerIO.createSettingsManager` return type to `SettingsManager`.
275
+ Remove `as any` casts from `index.ts` factories.
276
+ Run `pnpm run check` + full suite.
277
+ Commit: `feat: use SDK types in CreateSessionOptions (#134)`
278
+
279
+ 2. **Add message-content type guards; remove viewer and runner source casts (6 source casts → 0).**
280
+ Define `ToolCallContent` interface and `getToolCallName()` helper in `conversation-viewer.ts`.
281
+ Define `BashExecutionMessage` interface and `isBashExecution()` guard in `conversation-viewer.ts`.
282
+ Replace all source `as any` casts in `conversation-viewer.ts`.
283
+ Define the same `getToolCallName()` helper (or extract a shared one) in `agent-runner.ts` and replace its cast.
284
+ Run `pnpm run check` + affected tests.
285
+ Commit: `fix: replace message-shape as-any casts with type guards (#134)`
286
+
287
+ 3. **Remove renderer test casts (29 → 0).**
288
+ Remove all `as any` casts on `stubTheme()`, message objects, and result access.
289
+ Use `result!.text` (non-null assertion after `toBeDefined()` guard) for result access.
290
+ Run `pnpm run check` + `pnpm vitest run test/renderer.test.ts`.
291
+ Commit: `test: remove as-any casts in renderer tests (#134)`
292
+
293
+ 4. **Add `MenuCtx` interface; remove menu-test casts (8 → 0).**
294
+ Define `MenuCtx` in `agent-menu.ts`.
295
+ Change handler parameter from `ExtensionContext` to `MenuCtx`.
296
+ Type `makeCtx()` return in test.
297
+ Run `pnpm run check` + `pnpm vitest run test/ui/agent-menu.test.ts`.
298
+ Commit: `feat: narrow menu handler to MenuCtx interface (#134)`
299
+
300
+ 5. **Add `WidgetLike`; remove runtime casts (9 → 0).**
301
+ Define `WidgetLike` in `runtime.ts`.
302
+ Change `widget` field type.
303
+ Update `runtime.test.ts`: typed stubs, real `AgentActivityTracker` instances.
304
+ Run `pnpm run check` + `pnpm vitest run test/runtime.test.ts`.
305
+ Commit: `feat: narrow runtime widget field to WidgetLike interface (#134)`
306
+
307
+ 6. **Expand `createMockSession`; remove session casts (10 → ~1).**
308
+ Add fields to `createMockSession()` that tool/service tests need.
309
+ Use it in tool tests, service-adapter, agent-manager for `record.execution.session`.
310
+ Run `pnpm run check` + full suite.
311
+ Commit: `test: use createMockSession for session execution casts (#134)`
312
+
313
+ 7. **Remove helpers.test.ts casts (7 → 0).**
314
+ Typed `TypeListRegistry` mocks.
315
+ Fix `textResult` return type in `src/tools/helpers.ts`.
316
+ Real `AgentActivityTracker` instances.
317
+ Run `pnpm run check` + `pnpm vitest run test/tools/helpers.test.ts`.
318
+ Commit: `test: remove as-any casts in helpers tests (#134)`
319
+
320
+ 8. **Remove context casts across remaining test files (6 → 0).**
321
+ Type `mockCtx` in `agent-manager.test.ts`.
322
+ Type `makeHeadlessCtx()` in `print-mode.test.ts`.
323
+ Type mock context in `parent-snapshot.test.ts`.
324
+ Type `{} as any` in `make-deps.test.ts`.
325
+ Run `pnpm run check` + full suite.
326
+ Commit: `test: remove remaining context as-any casts (#134)`
327
+
328
+ 9. **Remove RunOptions and message-array casts (5 → 0).**
329
+ Remove vestigial `as any` on RunOptions objects.
330
+ Type `messages` arrays as `unknown[]`.
331
+ Run `pnpm run check` + affected tests.
332
+ Commit: `test: remove RunOptions and message-array casts (#134)`
333
+
334
+ 10. **Remove tool-execute ctx casts (5 → 0).**
335
+ Define `STUB_CTX` in `test/helpers/`.
336
+ Use it in `steer-tool.test.ts`, `get-result-tool.test.ts`, `make-deps.test.ts`.
337
+ Run `pnpm run check` + affected tests.
338
+ Commit: `test: remove tool-execute context casts (#134)`
339
+
340
+ 11. **Clean up conversation-viewer test and usage casts (6 → ~2).**
341
+ Type message mocks in `conversation-viewer.test.ts`.
342
+ Type usage mock objects in `usage.test.ts`.
343
+ Keep `(viewer as any).buildContentLines()` and `(manager as any).cleanupInterval` (private access, intentional).
344
+ Run `pnpm run check` + full suite.
345
+ Commit: `test: remove conversation-viewer and usage as-any casts (#134)`
346
+
347
+ ## Risks and Mitigations
348
+
349
+ | Risk | Mitigation |
350
+ | ----------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
351
+ | Widening `CreateSessionOptions` to SDK types re-couples `agent-runner.ts` to SDK | These are type-only imports — no runtime coupling. Tests still mock at the `RunnerIO` function level. The `RunnerIO` interface is the decoupling boundary, not the option types. |
352
+ | Narrowing `widget` to `WidgetLike` could break callers accessing `AgentWidget`-specific methods | Grep all `runtime.widget` access sites first; verify they only use `update()`, `markFinished()`, `setUICtx()`. |
353
+ | Narrowing menu handler to `MenuCtx` could break callers passing `ExtensionContext` | `ExtensionContext` is structurally a superset of `MenuCtx` — callers pass it unchanged. |
354
+ | Expanding `createMockSession()` could break consumers with different default expectations | Diff existing consumers' default expectations before adding fields (per testing skill). |
355
+ | Message type guards add code to conversation-viewer and agent-runner | Each guard is 2–4 lines; they replace unsafe `as any` access with documented, compile-checked types. Net code stays similar. |
356
+ | 11-step plan is large | Each step is independently committable. No step depends on a subsequent one. Steps can be reordered or skipped. |
357
+
358
+ ## Open Questions
359
+
360
+ - Should `getToolCallName()` be shared between `conversation-viewer.ts` and `agent-runner.ts`, or duplicated?
361
+ Both files handle the same SDK message shape.
362
+ A shared helper in a common module (e.g., `context.ts` which already has `extractText`) avoids duplication.
363
+ Alternatively, the duplication is cheap (3 lines) and the two files have different concerns.
364
+ - Should `STUB_CTX` live in `test/helpers/stub-ctx.ts` or inline?
365
+ Centralize — 3+ tool tests share the pattern.
366
+ - Estimated final count: 2–3 remaining casts (`(viewer as any).buildContentLines()`, `(manager as any).cleanupInterval`, possibly 1 more for an unexported SDK type).
@@ -0,0 +1,182 @@
1
+ ---
2
+ issue: 135
3
+ issue_title: "Extract display helpers from `agent-widget.ts`"
4
+ ---
5
+
6
+ # Extract display helpers from agent-widget.ts
7
+
8
+ ## Problem Statement
9
+
10
+ `agent-widget.ts` (522 lines) exports 11 helper functions and constants that are general-purpose display utilities with no dependency on the widget's lifecycle or state.
11
+ Six other source modules (`agent-menu.ts`, `conversation-viewer.ts`, `renderer.ts`, `agent-tool.ts`, `foreground-runner.ts`, `get-result-tool.ts`) and two tool support modules (`helpers.ts`, `background-spawner.ts`) import formatting functions or display types from the widget — creating a false dependency on a lifecycle-heavy UI module.
12
+
13
+ This is Phase 8, Step J of the architecture plan.
14
+
15
+ ## Goals
16
+
17
+ - Extract pure formatters, display helpers, constants, and associated types into `ui/display.ts`.
18
+ - Update all import sites to import from `ui/display.ts` instead of `ui/agent-widget.ts`.
19
+ - Reduce `agent-widget.ts` to only the `AgentWidget` class and its immediate dependencies (`UICtx`, private helpers).
20
+ - Unblock Step K (menu decomposition, #136) — extracted menu sub-modules will import display helpers without pulling in the widget.
21
+
22
+ ## Non-Goals
23
+
24
+ - Decomposing `agent-menu.ts` — deferred to #136 (Step K).
25
+ - Changing any runtime behavior or public API.
26
+ - Extracting `UICtx` — it is a widget-lifecycle type used only by `AgentWidget`, `runtime.ts`, and `index.ts`.
27
+
28
+ ## Background
29
+
30
+ The architecture doc (Phase 8 roadmap, Step J) prescribes exactly which symbols to extract.
31
+ The `code-design` skill's "Helpers stay in the file" rule applies: these helpers have accumulated to the point where they warrant their own module and tests.
32
+ AGENTS.md's "one concern per file" constraint also supports the extraction.
33
+
34
+ ### Symbols to extract
35
+
36
+ #### Pure formatters (zero runtime dependencies)
37
+
38
+ 1. `formatTokens(count)` — compact token count ("33.8k token").
39
+ 2. `formatSessionTokens(tokens, percent, theme, compactions)` — annotated token string with threshold colors.
40
+ 3. `formatTurns(turnCount, maxTurns)` — turn counter with optional limit.
41
+ 4. `formatMs(ms)` — milliseconds → "1.2s".
42
+ 5. `formatDuration(startedAt, completedAt)` — timestamp pair → human duration.
43
+
44
+ #### Display helpers (registry lookup only)
45
+
46
+ 6. `getDisplayName(type, registry)` — resolved display name for an agent type.
47
+ 7. `getPromptModeLabel(type, registry)` — "twin" for append mode, undefined otherwise.
48
+ 8. `buildInvocationTags(invocation)` — config tags array from invocation options.
49
+ 9. `describeActivity(activeTools, responseText)` — human-readable activity string.
50
+
51
+ #### Constants
52
+
53
+ 10. `SPINNER` — braille spinner frames.
54
+ 11. `ERROR_STATUSES` — set of error/non-success status strings.
55
+ 12. `TOOL_DISPLAY` — tool name → action verb mapping (private, moves with `describeActivity`).
56
+
57
+ #### Types
58
+
59
+ 13. `Theme` — used in `formatSessionTokens` signature; must co-locate.
60
+ 14. `AgentDetails` — display metadata interface used by tools; no widget dependency.
61
+
62
+ `UICtx` stays in `agent-widget.ts` — it defines the widget's host contract and is only consumed by the widget class, `runtime.ts`, and `index.ts`.
63
+
64
+ ### Current import graph (agent-widget.ts consumers)
65
+
66
+ | Consumer | Symbols imported |
67
+ | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
68
+ | `ui/conversation-viewer.ts` | `buildInvocationTags`, `describeActivity`, `formatDuration`, `formatSessionTokens`, `getDisplayName`, `getPromptModeLabel`, `Theme` |
69
+ | `ui/agent-menu.ts` | `formatDuration`, `getDisplayName` |
70
+ | `tools/agent-tool.ts` | `AgentDetails`, `buildInvocationTags`, `formatMs`, `formatTurns`, `getDisplayName`, `getPromptModeLabel`, `SPINNER`, `UICtx` |
71
+ | `tools/foreground-runner.ts` | `AgentDetails`, `describeActivity`, `formatMs`, `SPINNER` |
72
+ | `tools/get-result-tool.ts` | `formatDuration`, `getDisplayName` |
73
+ | `tools/helpers.ts` | `AgentDetails`, `formatTokens` |
74
+ | `tools/background-spawner.ts` | `AgentDetails` |
75
+ | `renderer.ts` | `formatMs`, `formatTokens`, `formatTurns` |
76
+ | `runtime.ts` | `UICtx` |
77
+ | `index.ts` | `AgentWidget`, `UICtx` |
78
+ | `test/agent-widget.test.ts` | `formatSessionTokens`, `getDisplayName`, `getPromptModeLabel` |
79
+ | `test/conversation-viewer.test.ts` | `Theme` |
80
+
81
+ ### Post-extraction import graph
82
+
83
+ After extraction, `ui/agent-widget.ts` imports `display.ts` for the symbols it still uses internally (e.g., `getDisplayName`, `formatMs`, `formatTurns`, `formatSessionTokens`, `ERROR_STATUSES`, `SPINNER`, `describeActivity`).
84
+ All other consumers switch their imports from `./agent-widget.js` to `./display.js` (or `../ui/display.js` for `tools/` and `renderer.ts`).
85
+
86
+ Only `index.ts` and `runtime.ts` continue to import from `agent-widget.ts` (for `AgentWidget` class and `UICtx` type).
87
+ `tools/agent-tool.ts` splits its import: `UICtx` from `agent-widget.ts`, everything else from `display.ts`.
88
+
89
+ ## Design Overview
90
+
91
+ This is a pure code-motion refactoring — no behavior changes.
92
+
93
+ ### New module: `ui/display.ts`
94
+
95
+ Contains all 12 exported symbols (5 formatters, 4 display helpers, 3 constants) plus 2 types (`Theme`, `AgentDetails`) and 1 private helper (`truncateLine`, used by `describeActivity`).
96
+
97
+ The module's only imports are:
98
+
99
+ - `AgentConfigLookup` from `../agent-types.js` (type-only, for `getDisplayName`/`getPromptModeLabel`).
100
+ - `SubagentType`, `AgentInvocation` from `../types.js` (type-only).
101
+
102
+ No SDK imports, no runtime dependencies — exactly the kind of pure utility module the code-design skill prescribes.
103
+
104
+ ### Residual `agent-widget.ts`
105
+
106
+ After extraction, `agent-widget.ts` contains:
107
+
108
+ - `UICtx` type (widget host contract).
109
+ - `AgentWidget` class (~340 lines) with its private helpers.
110
+ - Imports from `./display.js` for the format/display functions used in rendering.
111
+
112
+ ## Module-Level Changes
113
+
114
+ ### New files
115
+
116
+ | File | Contents |
117
+ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
118
+ | `src/ui/display.ts` | All extracted symbols: `Theme`, `AgentDetails`, `SPINNER`, `ERROR_STATUSES`, `TOOL_DISPLAY`, `formatTokens`, `formatSessionTokens`, `formatTurns`, `formatMs`, `formatDuration`, `getDisplayName`, `getPromptModeLabel`, `buildInvocationTags`, `describeActivity`, private `truncateLine`. |
119
+
120
+ ### Modified files
121
+
122
+ | File | Change |
123
+ | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
124
+ | `src/ui/agent-widget.ts` | Remove all extracted symbols. Add `import` from `./display.js` for symbols `AgentWidget` still uses internally. Keep `UICtx`, `AgentWidget` class, private widget helpers. |
125
+ | `src/ui/conversation-viewer.ts` | Change import path from `./agent-widget.js` to `./display.js`. |
126
+ | `src/ui/agent-menu.ts` | Change import path from `./agent-widget.js` to `./display.js`. |
127
+ | `src/tools/agent-tool.ts` | Split import: `UICtx` from `../ui/agent-widget.js`; all others from `../ui/display.js`. |
128
+ | `src/tools/foreground-runner.ts` | Change import path from `../ui/agent-widget.js` to `../ui/display.js`. |
129
+ | `src/tools/get-result-tool.ts` | Change import path from `../ui/agent-widget.js` to `../ui/display.js`. |
130
+ | `src/tools/helpers.ts` | Change import path from `../ui/agent-widget.js` to `../ui/display.js`. |
131
+ | `src/tools/background-spawner.ts` | Change import path from `../ui/agent-widget.js` to `../ui/display.js`. |
132
+ | `src/renderer.ts` | Change import path from `./ui/agent-widget.js` to `./ui/display.js`. |
133
+ | `test/agent-widget.test.ts` | Change import path to `../src/ui/display.js`. Rename file to `test/display.test.ts` since it tests extracted functions. |
134
+ | `test/conversation-viewer.test.ts` | Change `Theme` import from `../src/ui/agent-widget.js` to `../src/ui/display.js`. |
135
+
136
+ ### Unchanged files
137
+
138
+ | File | Reason |
139
+ | ---------------- | ------------------------------------------------------------------- |
140
+ | `src/runtime.ts` | Imports only `UICtx` — stays in `agent-widget.ts`. |
141
+ | `src/index.ts` | Imports `AgentWidget` and `UICtx` — both stay in `agent-widget.ts`. |
142
+
143
+ ## Test Impact Analysis
144
+
145
+ 1. The extraction enables dedicated `display.test.ts` that tests formatting functions in isolation without any widget class ceremony.
146
+ The existing `agent-widget.test.ts` already tests only extracted functions (`formatSessionTokens`, `getDisplayName`, `getPromptModeLabel`) — it becomes `display.test.ts` with no assertion changes, just a file rename and import path update.
147
+ 2. No existing tests become redundant — the current test file already exercises the extracted layer exclusively.
148
+ 3. No existing tests need assertion changes — this is a pure code-motion refactoring with no behavior change.
149
+
150
+ ## TDD Order
151
+
152
+ 1. **Create `ui/display.ts` with all extracted symbols; update `agent-widget.ts` to import from it.**
153
+ Move the 12 exported symbols, 2 types, and 1 private helper to `ui/display.ts`.
154
+ Remove them from `agent-widget.ts` and add imports from `./display.js` for symbols the `AgentWidget` class still references.
155
+ Commit: `refactor: extract display helpers into ui/display.ts (#135)`
156
+
157
+ 2. **Update all consumer imports to point at `ui/display.ts`.**
158
+ Update the 8 source files (`conversation-viewer.ts`, `agent-menu.ts`, `agent-tool.ts`, `foreground-runner.ts`, `get-result-tool.ts`, `helpers.ts`, `background-spawner.ts`, `renderer.ts`) to import from the new module.
159
+ Commit: `refactor: update imports to use ui/display.ts (#135)`
160
+
161
+ 3. **Rename test file and update test imports.**
162
+ Rename `test/agent-widget.test.ts` → `test/display.test.ts`.
163
+ Update import path to `../src/ui/display.js`.
164
+ Update `Theme` import in `test/conversation-viewer.test.ts`.
165
+ Commit: `test: rename agent-widget test to display test (#135)`
166
+
167
+ 4. **Verify: run `pnpm run check` and `pnpm vitest run`.**
168
+ Confirm type-checking and all tests pass.
169
+ No commit needed — validation step.
170
+
171
+ ## Risks and Mitigations
172
+
173
+ | Risk | Mitigation |
174
+ | ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
175
+ | Missed import site causes runtime `undefined` import | Grep confirmed all 10 source consumers and 2 test consumers above. Step 4 validates with type-check + full test suite. |
176
+ | `TOOL_DISPLAY` made public unintentionally | Keep it non-exported in `display.ts` (only `describeActivity` uses it). |
177
+ | Circular dependency `display.ts` ↔ `agent-widget.ts` | `display.ts` has no imports from `agent-widget.ts`. `agent-widget.ts` imports from `display.ts` — one-directional. |
178
+ | Re-export churn for downstream consumers | No downstream consumers — these are all internal module imports, not public API. |
179
+
180
+ ## Open Questions
181
+
182
+ None — the architecture doc and issue specify the exact extraction set and target module.