@gotgenes/pi-subagents 17.1.0 → 17.3.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.
@@ -0,0 +1,365 @@
1
+ ---
2
+ issue: 445
3
+ issue_title: "pi-subagents: implement native session navigation for any subagent (live or completed)"
4
+ ---
5
+
6
+ # Native session navigation for subagents (live transcript, text renderer)
7
+
8
+ ## Release Recommendation
9
+
10
+ **Release:** ship independently
11
+
12
+ Phase 19 Step 4 ([#445]) carries `Release: independent` (spike-gated) in the architecture roadmap.
13
+ The only batch defined in Phase 19 is "dissolve-agents" (Steps 5–6, [#442]/[#441]); Step 4 is an additive replacement surface that stands up alongside the bespoke viewer and is not part of that batch.
14
+ It ships on its own.
15
+
16
+ ## Problem Statement
17
+
18
+ The operator can view a subagent's conversation only through the bespoke `ConversationViewer` overlay, reachable inside the monolithic `/agents` menu.
19
+ That overlay re-implements session-transcript rendering — message formatting, a streaming indicator, scroll chrome — even though Pi already persists each child as a standalone session JSONL and ships its own session-rendering machinery.
20
+ ADR-0004 Decision B replaces the bespoke overlay with **native session navigation**: a flat command that lists the session's subagents, lets the operator pick one, and renders that child's transcript read-only through Pi's own components.
21
+
22
+ ADR-0004 Addendum 2 (the Step 1 spike, [#446]) refines the sourcing: a file read is a *snapshot* (good for a completed agent, frozen for a running one), so the transcript is **dual-sourced by liveness** — live from the in-memory record for a tracked agent, a file snapshot for an evicted one — rendered through one renderer.
23
+
24
+ This issue is deliberately **sliced** (see Decomposition below).
25
+ It stands up the complete navigation UX — list → pick → read-only, live transcript — behind a renderer-agnostic source seam, using Pi's `serializeConversation` as the first renderer.
26
+ Upgrading that renderer to Pi's per-entry TUI components, and broadening the candidate set to evicted agents, are named follow-ups.
27
+
28
+ ## Goals
29
+
30
+ - Add a flat `/subagent-sessions` command that lists navigable subagents (`manager.listAgents()`, gated on `isSessionReady()`), lets the operator pick one, and renders its transcript **read-only** (non-interactive) in a scrollable overlay.
31
+ - Source the transcript **live** from the in-memory record: history from a new typed `agentMessages` accessor, live re-render via `record.subscribeToUpdates()`, and a running-agent streaming indicator from `record.activeTools` / `record.responseText`.
32
+ - Render through Pi's own `serializeConversation` (no bespoke message-formatting code) behind a `TranscriptSource` seam, so a follow-up swaps in Pi's per-entry TUI components without touching selection or sourcing.
33
+ - Add a typed `agentMessages` accessor on `SubagentSession` and `Subagent` returning `readonly SessionMessage[]` (the boundary currently widens `messages` to `readonly unknown[]`).
34
+ - Stand the new surface up **alongside** the existing `viewAgentConversation` / `ConversationViewer` path — it is removed only by Phase 19 Step 5 ([#442]).
35
+
36
+ Non-breaking: this is pure addition.
37
+ No existing export, command, default, or behavior changes.
38
+
39
+ ## Non-Goals
40
+
41
+ - Do **not** implement the **file-snapshot** source branch (`parseSessionEntries` → `buildSessionContext`) in this issue.
42
+ With the `listAgents()`-only candidate set, no listed record is ever session-disposed (see Background), so the file branch has **no reachable caller** here — implementing it now would be dead code that fails the `fallow dead-code` gate.
43
+ It ships with the follow-up that broadens the candidate set to evicted agents.
44
+ - Do **not** use Pi's per-entry TUI components (`AssistantMessageComponent` / `ToolExecutionComponent` / …) as the renderer in this issue — that is the named renderer follow-up.
45
+ - Do **not** add in-session steering or any interactive gesture — steering stays in `steer_subagent` and the widget; the viewer is strictly read-only.
46
+ - Do **not** use `switchSession` or `loadEntriesFromFile` (per the ADR-0004 addendum).
47
+ - Do **not** touch `agent-menu.ts`, `conversation-viewer.ts`, or `message-formatters.ts` — they stay live until Step 5 ([#442]).
48
+ - Do **not** wire a widget selection gesture — the command is the primary surface; a widget gesture ([#444]) is an optional later enhancement.
49
+
50
+ ## Background
51
+
52
+ Relevant existing modules:
53
+
54
+ - `src/lifecycle/subagent-session.ts` — `SubagentSession` wraps one SDK `AgentSession`.
55
+ Its `get messages(): readonly unknown[]` returns `this._session.messages as readonly unknown[]` — deliberately widened at the core boundary.
56
+ It already exposes `subscribe(fn)`, `outputFile`, `getContextPercent()`.
57
+ - `src/lifecycle/subagent.ts` — `Subagent` delegates `get messages(): readonly unknown[]`, `subscribeToUpdates(fn)`, `get outputFile()`, `isSessionReady()`, `get activeTools()`, `get responseText()`, `get status()` to its `subagentSession` / `state`.
58
+ These are the exact accessors the bespoke `ConversationViewer` reads — the new navigator reads the same surface.
59
+ - `src/lifecycle/subagent-manager.ts` — `listAgents()` returns tracked `Subagent[]` (newest first).
60
+ `cleanup()` (every 60 s) and `clearCompleted()` call `removeRecord(id, record)`, which calls `record.disposeSession()` **then** `agents.delete(id)` atomically.
61
+ `disposeSession()` disposes the wrapped session but does **not** null `subagentSession`; however, because dispose-and-delete are atomic, **no record that remains in `listAgents()` is ever session-disposed**.
62
+ Consequence: with the `listAgents()`-only candidate set, every listed, session-ready record has a live (non-disposed) session — so the live source is always valid and the file-snapshot branch is unreachable (drives the Non-Goal above).
63
+ - `src/ui/conversation-viewer.ts` — the bespoke overlay being replaced (deleted in Step 5).
64
+ Reads `record.messages`, subscribes via `record.subscribeToUpdates()`, renders a streaming indicator from `record.activeTools` / `record.responseText`, and owns its own scroll chrome (up/down/pageUp/pageDown/home/end/esc).
65
+ The new navigator must **not** import from it or from `message-formatters.ts` (both doomed in Step 5).
66
+ - `src/ui/agent-menu.ts` — `viewAgentConversation(ui, record)` gates on `record.isSessionReady()` and opens the overlay via `ui.custom<undefined>((tui, theme, _kb, done) => new ConversationViewer({...}), { overlay: true, overlayOptions: { anchor: "center", width: "90%", maxHeight: "70%" } })`.
67
+ This is the wiring shape the navigator mirrors.
68
+ - `src/ui/subagents-settings.ts` ([#447]) — the precedent for an additive, flat, narrow-interface command extracted from the menu.
69
+ - `src/ui/display.ts` — `getDisplayName(type, registry)`, `formatDuration(startedAt, completedAt)`, `getPromptModeLabel(type, registry)` for option labels.
70
+ - `src/types.ts` — shared type barrel; already imports SDK types (`AgentSessionEvent`, `ThinkingLevel`).
71
+ - `src/index.ts` — constructs `manager`, `registry`, and `settings`, and registers commands (`agents`, `subagents-settings`).
72
+ Sibling commands use flat hyphenated names with no `:` namespace.
73
+
74
+ Pi SDK facts verified for this plan:
75
+
76
+ - `parseSessionEntries`, `buildSessionContext`, `serializeConversation`, `SessionContext`, `FileEntry`, `SessionEntry`, and the per-entry components are all re-exported from the public `@earendil-works/pi-coding-agent` barrel; `loadEntriesFromFile` is **not** (confirmed in `dist/index.d.ts`).
77
+ - `SessionContext.messages` is typed `AgentMessage[]` (`AgentMessage` from `@earendil-works/pi-agent-core`, which is **not** a direct dependency).
78
+ Derive the type from the barrel-exported `SessionContext` rather than adding a dependency: `type SessionMessage = SessionContext["messages"][number]`.
79
+ - `serializeConversation(messages: Message[]): string` takes a **mutable** `Message[]` (from `@earendil-works/pi-ai`).
80
+ The accessor returns `readonly SessionMessage[]`, so the renderer must spread (`serializeConversation([...messages])`); the spread also resolves the readonly→mutable mismatch.
81
+ `AgentMessage` assignability to `Message` is verified in TDD step 2 (Pi itself feeds `buildSessionContext` output to serialization, so it is expected to hold); if it does not, narrow via a typed adapter at the renderer boundary.
82
+
83
+ AGENTS.md / package constraints:
84
+
85
+ - pi-subagents is a minimal core; surviving UI is an **in-core reactive consumer** with no inbound calls into the core.
86
+ The new `agentMessages` accessor is a read-only getter — it adds no inbound call and preserves the Phase 18 spine invariants ([#422]–[#425]).
87
+ - Keep Pi SDK *coupling* out of pure helpers: the pure module uses only Pi's pure utility functions (`serializeConversation`) and an **injected** file/registry, so it stays unit-testable; the overlay component and command handler are SDK/TUI consumers (allowed to import SDK/TUI directly).
88
+ - Use narrow interface types (not the concrete `Subagent` class) at the seam — concrete class types leak private fields into test mocks.
89
+
90
+ ## Design Overview
91
+
92
+ Two new modules split pure logic from SDK/TUI wiring, plus a typed accessor on the record.
93
+
94
+ ### Typed `agentMessages` accessor (boundary narrowing)
95
+
96
+ `src/types.ts` gains a derived alias (no new dependency):
97
+
98
+ ```typescript
99
+ import type { SessionContext } from "@earendil-works/pi-coding-agent";
100
+
101
+ /** One message in a child session's history, typed from Pi's SessionContext. */
102
+ export type SessionMessage = SessionContext["messages"][number];
103
+ ```
104
+
105
+ `SubagentSession` and `Subagent` each gain a typed read accessor alongside the existing widened `messages`:
106
+
107
+ ```typescript
108
+ // SubagentSession
109
+ get agentMessages(): readonly SessionMessage[] {
110
+ return this._session.messages;
111
+ }
112
+
113
+ // Subagent
114
+ get agentMessages(): readonly SessionMessage[] {
115
+ return this.subagentSession?.agentMessages ?? [];
116
+ }
117
+ ```
118
+
119
+ `_session.messages` is already `AgentMessage[]` (= `SessionMessage[]`), so this is a typed view of the same data — no cast through `unknown`.
120
+ The widened `messages` getter stays (other readers depend on it); `agentMessages` is the typed accessor the navigator uses.
121
+
122
+ ### `session-navigation.ts` — pure selection, source, and text rendering
123
+
124
+ A pure module (SDK pure-utility imports only; no TUI, no `ExtensionAPI`).
125
+ It declares narrow interfaces for the record fields it reads (ISP — not the concrete `Subagent`).
126
+
127
+ ```typescript
128
+ /** The record fields the navigator reads to label and source a transcript. */
129
+ export interface NavigableSubagent {
130
+ readonly id: string;
131
+ readonly type: SubagentType;
132
+ readonly description: string;
133
+ readonly status: SubagentStatus;
134
+ readonly startedAt: number;
135
+ readonly completedAt: number | undefined;
136
+ readonly toolUses: number;
137
+ readonly activeTools: ReadonlyMap<string, string>;
138
+ readonly responseText: string;
139
+ readonly agentMessages: readonly SessionMessage[];
140
+ isSessionReady(): boolean;
141
+ subscribeToUpdates(fn: (event: AgentSessionEvent) => void): (() => void) | undefined;
142
+ }
143
+
144
+ /** A navigable entry: the record plus its display label. */
145
+ export interface NavigationEntry {
146
+ readonly record: NavigableSubagent;
147
+ readonly label: string;
148
+ }
149
+
150
+ /** Liveness-agnostic transcript source consumed by the renderer. */
151
+ export interface TranscriptSource {
152
+ /** Current message history. */
153
+ getMessages(): readonly SessionMessage[];
154
+ /** Subscribe to live updates; returns an unsubscribe, or undefined for a static snapshot. */
155
+ subscribe(onChange: () => void): (() => void) | undefined;
156
+ /** Running-agent streaming state, or undefined when not streaming. */
157
+ streaming(): { activeTools: ReadonlyMap<string, string>; responseText: string } | undefined;
158
+ }
159
+ ```
160
+
161
+ Functions:
162
+
163
+ - `listNavigableAgents(agents, registry): NavigationEntry[]` — keep records with `isSessionReady()`, map each to a label (`getDisplayName` · description · status · `formatDuration`).
164
+ - `liveSource(record: NavigableSubagent): TranscriptSource` — `getMessages` → `record.agentMessages`; `subscribe` → `record.subscribeToUpdates`; `streaming` → `{ activeTools, responseText }` when `record.status === "running"`, else `undefined`.
165
+ - `renderTranscriptLines(source, opts): string[]` — `serializeConversation([...source.getMessages()])` split on `\n`, then append a minimal streaming indicator (active tool names + a truncated `responseText` preview) when `source.streaming()` is defined; return `[]`-safe content for an empty history (`"(no messages yet)"`).
166
+
167
+ `renderTranscriptLines` carries **no bespoke message formatting** — the transcript text is Pi's `serializeConversation` output; only the small streaming indicator is local (a few lines), and it does not import the doomed `message-formatters.ts`.
168
+
169
+ ### `session-navigator.ts` — overlay component + command handler
170
+
171
+ SDK/TUI consumer.
172
+
173
+ - `TranscriptOverlay` — a read-only scrollable `Component`: on construct, subscribe via `source.subscribe(() => tui.requestRender())`; each `render(width)` calls `renderTranscriptLines(source)` and paints a scrolling viewport with header/footer chrome and key handling (up/down, pageUp/pageDown, home/end, esc/q to close); `dispose()` unsubscribes.
174
+ Scroll math mirrors the bespoke viewer's behavior but the content comes from `renderTranscriptLines` (the bespoke viewer is deleted in Step 5; this is fresh code with its own tests, not a clone of doomed code).
175
+ - `SessionNavigatorHandler` — `handle({ ui, agents, registry })`:
176
+
177
+ ```typescript
178
+ async handle({ ui, agents, registry }: SessionNavigatorParams): Promise<void> {
179
+ const entries = listNavigableAgents(agents, registry);
180
+ if (entries.length === 0) { ui.notify("No subagent sessions to view.", "info"); return; }
181
+ const choice = await ui.select("Subagent sessions", entries.map(e => e.label));
182
+ const entry = entries.find(e => e.label === choice);
183
+ if (!entry) return;
184
+ const source = liveSource(entry.record);
185
+ await ui.custom<undefined>(
186
+ (tui, theme, _kb, done) => new TranscriptOverlay({ tui, theme, source, done, wrapText: wrapTextWithAnsi }),
187
+ { overlay: true, overlayOptions: { anchor: "center", width: "90%", maxHeight: "70%" } },
188
+ );
189
+ }
190
+ ```
191
+
192
+ `SessionNavigatorParams` / `SessionNavigatorUI` are narrow interfaces declaring only `select`, `notify`, and `custom` — the methods the handler uses.
193
+
194
+ ### Registration call site (`index.ts`)
195
+
196
+ ```typescript
197
+ const sessionNavigator = new SessionNavigatorHandler();
198
+ pi.registerCommand("subagent-sessions", {
199
+ description: "View a subagent's session transcript (read-only)",
200
+ handler: async (_args, ctx) => {
201
+ await sessionNavigator.handle({ ui: ctx.ui, agents: manager.listAgents(), registry });
202
+ },
203
+ });
204
+ ```
205
+
206
+ `manager.listAgents()` is called in the handler registration (the navigator never holds a manager reference — it receives the snapshot), keeping the navigator a pure reactive consumer with no inbound core call.
207
+
208
+ ### Consumer call-site sketch (Tell-Don't-Ask / LoD check)
209
+
210
+ `liveSource` does not reach through the record to a stranger: it reads the record's own getters (`agentMessages`, `status`, `activeTools`, `responseText`) and delegates subscription to `record.subscribeToUpdates`.
211
+ The renderer talks only to `TranscriptSource`, never to the record — so the component renderer follow-up swaps the source's internals (file vs live) and the renderer (text vs components) independently.
212
+ No output arguments: nothing writes back into the record or the source.
213
+
214
+ ### Decomposition and follow-ups (Kent Beck: make the change easy, then make the easy change)
215
+
216
+ This issue is the first releasable vertical slice.
217
+ The `TranscriptSource` seam is the "change made easy" — it decouples *how messages are sourced* from *how they are rendered*, so each follow-up is a localized swap:
218
+
219
+ 1. **Renderer upgrade (named follow-up):** replace `renderTranscriptLines`/`TranscriptOverlay`'s `serializeConversation` text path with Pi's per-entry TUI components (`AssistantMessageComponent` / `ToolExecutionComponent` / …) behind the same `TranscriptSource`.
220
+ Selection and sourcing are untouched.
221
+ 2. **Evicted-agent source (named follow-up):** broaden the candidate set beyond `listAgents()` and add the **file-snapshot** `TranscriptSource` (`parseSessionEntries(readFileSync(outputFile))` → drop `SessionHeader` → `buildSessionContext(...).messages`) for agents no longer tracked.
222
+ This is where the dual-source design lands its second branch; the renderer is untouched.
223
+
224
+ Recommendation: file these two as Phase 19 follow-up issues at ship time, and update the architecture roadmap's Step 4 description (which currently scopes dual-source + components as one step) to reflect the slice.
225
+
226
+ ### Design-review findings
227
+
228
+ | Smell | Location | Evidence | Result |
229
+ | ------------------- | -------------------------------- | -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
230
+ | Wide interface | `NavigableSubagent` (11 members) | `listNavigableAgents` reads 7 label fields; `liveSource` reads `agentMessages`/streaming/sub | Acceptable — two consumers split the surface; both are real reads, no unused field |
231
+ | LoD violation | `session-navigation.ts` | `liveSource` reads record getters + delegates `subscribeToUpdates`; no `record.x.y.z` chains | None |
232
+ | Output argument | `session-navigation.ts` | source/renderer return values; never write back into the record | None |
233
+ | Concrete-class leak | seam interfaces | navigator depends on `NavigableSubagent`/`TranscriptSource`, not the concrete `Subagent` | None — narrow interfaces |
234
+
235
+ The new collaborators each own behavior (`liveSource` returns a stateful subscription view; `TranscriptOverlay` owns scroll state and the subscription lifecycle; `renderTranscriptLines` returns rendered lines) — not procedure-splitting.
236
+
237
+ ### Edge cases
238
+
239
+ - Empty candidate set → `ui.notify("No subagent sessions to view.", "info")`, return.
240
+ - Operator cancels `select` (`undefined`) or the label is stale → return, no overlay.
241
+ - Empty message history (`agentMessages.length === 0`) → render `"(no messages yet)"`.
242
+ - Running agent → streaming indicator appended; a `subscribeToUpdates` event triggers re-render with the latest `agentMessages` + streaming state.
243
+ - Completed tracked agent → `streaming()` returns `undefined` (status not `running`); transcript is the final `agentMessages`, no indicator; `subscribe` still attaches harmlessly.
244
+ - `subscribeToUpdates` returns `undefined` (no live session) → cannot happen for a `listAgents()`-gated, session-ready record, but the overlay tolerates `undefined` (no subscription, static render).
245
+ - Very narrow terminal width → overlay returns `[]` below a minimum width, matching the bespoke viewer.
246
+
247
+ ## Module-Level Changes
248
+
249
+ - **New** `src/ui/session-navigation.ts` — `NavigableSubagent`, `NavigationEntry`, `TranscriptSource` interfaces; `listNavigableAgents`, `liveSource`, `renderTranscriptLines` functions.
250
+ - **New** `src/ui/session-navigator.ts` — `TranscriptOverlay` component, `SessionNavigatorHandler` class, `SessionNavigatorParams` / `SessionNavigatorUI` narrow interfaces.
251
+ - **Changed** `src/types.ts` — add `export type SessionMessage = SessionContext["messages"][number]` and the `SessionContext` type import.
252
+ - **Changed** `src/lifecycle/subagent-session.ts` — add `get agentMessages(): readonly SessionMessage[]`.
253
+ - **Changed** `src/lifecycle/subagent.ts` — add `get agentMessages(): readonly SessionMessage[]`.
254
+ - **Changed** `src/index.ts` — import `SessionNavigatorHandler`, construct it, register the `subagent-sessions` command (sole call site of the new export).
255
+ - **New** `test/ui/session-navigation.test.ts` — unit tests for `listNavigableAgents`, `liveSource`, `renderTranscriptLines`.
256
+ - **New** `test/ui/session-navigator.test.ts` — unit tests for `TranscriptOverlay` render/scroll/key handling and `SessionNavigatorHandler` select→custom wiring.
257
+ - **Changed** `test/lifecycle/subagent-session.test.ts`, `test/lifecycle/subagent.test.ts` — add `agentMessages` accessor tests.
258
+
259
+ No removed or renamed exports — grep for removed symbols is not applicable (pure addition).
260
+
261
+ Doc / skill grep results:
262
+
263
+ - `docs/architecture/architecture.md` Step 4 currently describes the **full** dual-source + components scope and names `src/ui/session-navigator.ts` and the typed accessor.
264
+ This issue narrows that scope (live-source + text renderer).
265
+ Update the Step 4 description to reflect the slice and the two named follow-ups — defer the status-line flip and the rescoping edit to ship time (matches the [#447] precedent), or fold a short rescoping note in this issue if preferred.
266
+ - `.pi/skills/package-pi-subagents/SKILL.md` lists the UI domain by count (`ui/` = 10 modules); this adds two files (→ 12).
267
+ The table is a coarse summary not maintained per-file — leave for a later Phase 19 doc-sync unless the operator asks otherwise.
268
+ - No `src/`/`test/` references to a removed symbol (nothing is removed); the bespoke `ConversationViewer` and `message-formatters.ts` are untouched and keep their tests.
269
+
270
+ ## Test Impact Analysis
271
+
272
+ 1. **New tests enabled.**
273
+ `listNavigableAgents`, `liveSource`, and `renderTranscriptLines` are pure and unit-testable against narrow `NavigableSubagent` / `TranscriptSource` stubs — no full `Subagent`, `TUI`, or `AgentSession` construction.
274
+ This is strictly more focused than `conversation-viewer.test.ts`, which builds a full record + TUI to exercise transcript rendering.
275
+ The `agentMessages` accessor gets direct unit tests on both `SubagentSession` and `Subagent`.
276
+ 2. **Existing tests that become redundant.**
277
+ None in this issue.
278
+ `conversation-viewer.test.ts` and `message-formatters.test.ts` still exercise the live, still-shipping bespoke overlay; they are removed only when Step 5 ([#442]) deletes those modules.
279
+ 3. **Existing tests that must stay as-is.**
280
+ All bespoke-viewer and menu tests stay unchanged — this issue does not touch `agent-menu.ts`, `conversation-viewer.ts`, or `message-formatters.ts`.
281
+
282
+ ## Invariants at risk
283
+
284
+ This issue adds two modules, one accessor, and one command registration; it does not touch the Phase 18 spine that prior steps refactored.
285
+ The relevant `Outcome:` invariants and their pins:
286
+
287
+ - **[#422] — runtime holds zero UI state.**
288
+ The navigator holds its own overlay/scroll state; nothing is pushed onto the runtime.
289
+ Pinned by `runtime.test.ts` / the service spine tests; unchanged.
290
+ - **[#423] — widget/UI is a reactive consumer with no inbound calls from core spawn tools.**
291
+ The navigator reads via the injected `manager.listAgents()` snapshot and record getters only; the new `agentMessages` accessor is a read-only getter.
292
+ No inbound call into the core.
293
+ A focused navigator test asserts the handler reads only through its injected `agents` + record getters.
294
+ - **[#424] — the `subagent` tool depends only on manager/runtime/settings/registry.**
295
+ No tool change.
296
+ - **[#425] — declared event channels equal emitted channels, no vacant hook.**
297
+ The navigator subscribes to existing record updates; it declares/emits no new channel.
298
+ Pinned by the events-contract suite (`test/service`, `test/observation`); unchanged.
299
+
300
+ A later phase step (the renderer / evicted-source follow-ups) must not regress these with a green suite — the navigator-reads-only test pins #423 for the seam.
301
+
302
+ ## TDD Order
303
+
304
+ 1. **Red→Green: typed `agentMessages` accessor.**
305
+ Test surface: `test/lifecycle/subagent-session.test.ts`, `test/lifecycle/subagent.test.ts`.
306
+ Tests: `SubagentSession.agentMessages` returns the wrapped session's typed messages; `Subagent.agentMessages` delegates to the session and returns `[]` when no session.
307
+ Implementation: add `SessionMessage` to `src/types.ts` (with the `SessionContext` import); add the two getters.
308
+ Run `pnpm run check` (verifies `SessionContext["messages"][number]` resolves and `_session.messages` is assignable).
309
+ Commit: `feat: add typed agentMessages accessor on subagent record (#445)`.
310
+ 2. **Red→Green: pure selection, live source, and text rendering.**
311
+ Test surface: `test/ui/session-navigation.test.ts`.
312
+ Tests:
313
+ - `listNavigableAgents` keeps only `isSessionReady()` records and builds the expected labels (name · description · status · duration); empty input → `[]`.
314
+ - `liveSource.getMessages` returns the record's `agentMessages`; `subscribe` delegates to `subscribeToUpdates`; `streaming()` returns state only when `status === "running"`, else `undefined`.
315
+ - `renderTranscriptLines` returns `serializeConversation` output split into lines; appends the streaming indicator when streaming; returns the empty-history placeholder for `[]`.
316
+ Implementation: create `src/ui/session-navigation.ts` with the three interfaces and three functions.
317
+ Verify `AgentMessage`→`Message` assignability for `serializeConversation([...messages])` here; if it fails, add a typed adapter at the boundary.
318
+ Commit: `feat: add subagent session selection and live transcript source (#445)`.
319
+ 3. **Red→Green: read-only overlay component + `/subagent-sessions` command.**
320
+ Test surface: `test/ui/session-navigator.test.ts`.
321
+ Tests:
322
+ - `TranscriptOverlay.render(width)` paints the current `renderTranscriptLines` content with chrome; scroll keys move the viewport; `esc`/`q` calls `done`; `dispose()` unsubscribes; a `subscribe` callback triggers `tui.requestRender`.
323
+ - `SessionNavigatorHandler.handle`: empty entries → `notify` + no `custom`; cancel at `select` → no `custom`; a valid pick → `custom` opened with a `liveSource` for the chosen record (assert via a `makeMenuUI`-style stub).
324
+ Implementation: create `src/ui/session-navigator.ts`; register the command in `src/index.ts` (sole call site).
325
+ Run `pnpm run check` immediately (the registration is the only consumer of the new handler export — both must compile together).
326
+ Run `pnpm fallow dead-code` before pushing (confirms no unreachable file-source branch slipped in).
327
+ Commit: `feat: add /subagent-sessions read-only navigation command (#445)`.
328
+
329
+ Steps 2 and 3 may be folded if preferred, but keeping the pure logic (with its stub-only tests) separate from the TUI/SDK wiring keeps each commit self-contained.
330
+
331
+ ## Risks and Mitigations
332
+
333
+ - **Risk: `AgentMessage` is not assignable to `serializeConversation`'s `Message[]`.**
334
+ Mitigation: verify in TDD step 2 (`pnpm run check`); Pi feeds `buildSessionContext` output to serialization, so it is expected to hold.
335
+ If not, add a typed adapter (`toMessages(messages): Message[]`) at the renderer boundary — the seam isolates the fix to `renderTranscriptLines`.
336
+ - **Risk: implementing the file-snapshot branch creates dead code (no caller under `listAgents()`-only) and fails `fallow dead-code`.**
337
+ Mitigation: this issue ships the **live source only** behind the seam; the file source is a named follow-up.
338
+ `pnpm fallow dead-code` runs before push.
339
+ - **Risk: importing the doomed `message-formatters.ts` / `conversation-viewer.ts` couples the navigator to code Step 5 deletes.**
340
+ Mitigation: the navigator imports neither; the streaming indicator is a small local helper and the transcript text is Pi's `serializeConversation`.
341
+ - **Risk: the new overlay duplicates the bespoke viewer's scroll chrome (apparent duplication).**
342
+ Mitigation: the bespoke viewer is deleted in Step 5, so there is no lasting duplication; the new overlay is fresh code with its own tests and a different content source (`renderTranscriptLines` vs bespoke `formatMessage`).
343
+ - **Risk: command-name choice.**
344
+ Mitigation: `subagent-sessions` follows the flat-hyphenated sibling convention (`agents`, `subagents-settings`); confirm at ship time (see Open Questions).
345
+ - **Risk: the architecture roadmap's Step 4 description diverges from the sliced scope.**
346
+ Mitigation: update the Step 4 description and file the two follow-ups at ship time.
347
+
348
+ ## Open Questions
349
+
350
+ - **Command name** — `subagent-sessions` is proposed (flat, hyphenated, sibling-consistent).
351
+ Alternatives: `subagent-transcript`, `view-subagent`, `subagents-view`.
352
+ Confirm at ship time; the command is not yet shipped, so it is cheaply renamable.
353
+ - **Whether to rescope the architecture roadmap's Step 4 entry in this issue or defer** to a Phase 19 doc-sync — decide at ship time (matches the [#447] precedent).
354
+ - **Whether to file the two named follow-ups (renderer upgrade, evicted-agent source) now or after this slice lands** — recommended at ship time so the roadmap stays accurate.
355
+
356
+ [#441]: https://github.com/gotgenes/pi-packages/issues/441
357
+ [#442]: https://github.com/gotgenes/pi-packages/issues/442
358
+ [#444]: https://github.com/gotgenes/pi-packages/issues/444
359
+ [#445]: https://github.com/gotgenes/pi-packages/issues/445
360
+ [#446]: https://github.com/gotgenes/pi-packages/issues/446
361
+ [#447]: https://github.com/gotgenes/pi-packages/issues/447
362
+ [#422]: https://github.com/gotgenes/pi-packages/issues/422
363
+ [#423]: https://github.com/gotgenes/pi-packages/issues/423
364
+ [#424]: https://github.com/gotgenes/pi-packages/issues/424
365
+ [#425]: https://github.com/gotgenes/pi-packages/issues/425
@@ -0,0 +1,81 @@
1
+ ---
2
+ issue: 444
3
+ issue_title: "pi-subagents: shrink the agent widget to background runs only"
4
+ ---
5
+
6
+ # Retro: #444 — pi-subagents: shrink the agent widget to background runs only
7
+
8
+ ## Stage: Planning (2026-06-20T18:47:05Z)
9
+
10
+ ### Session summary
11
+
12
+ Produced a numbered implementation plan for Phase 19 Step 3: shrink `AgentWidget` to background agents only by funneling both `manager.listAgents()` call sites (`update()` and `renderWidget()`) through a single private `listBackgroundAgents()` accessor that applies `record.invocation?.runInBackground === true` once at the source.
13
+ Confirmed the change is single-package (`pi-subagents`), non-breaking, and `Release: independent` per the architecture roadmap.
14
+
15
+ ### Observations
16
+
17
+ - Issue author is the operator (`gotgenes`) and the proposed change is unambiguous, so the `ask-user` gate was skipped.
18
+ The design is a legitimate improvement (removes the two-site duplication and fixes the latent inconsistency), not procedure-splitting.
19
+ - `widget-renderer.ts` needs **no** code change — it has no foreground-specific path; filtering at the data source in `agent-widget.ts` is sufficient.
20
+ Listed in Module-Level Changes only to record the verification.
21
+ - Key risk is mass test breakage: the existing `update()`-driven fixtures (two `makeWidget` helpers + the projection test's `createTestSubagent`) would all be filtered out once the predicate lands.
22
+ Mitigated with a tidy-first TDD step 1 that adds `invocation: { runInBackground: true }` to fixtures while the filter does not yet exist (inert), so step 2 only adds new tests.
23
+ - `assembleWidgetState` pure-function tests are unaffected — they call the function directly with `AgentSummary[]`, bypassing the accessor.
24
+ - Verified `clearWidget`'s stale-purge has no regression: foreground agents are never seeded into `finishedTurnAge`, so purging against the background-only list is correct.
25
+ - Doc grep surfaced one stale prose line (`README.md:64` "showing all active agents"); SKILL.md and `comparison-with-upstream.md` references remain accurate.
26
+ - Deferred (Open Question): relabeling the widget heading `Agents` → `Background agents` — out of scope for this issue.
27
+
28
+ ## Stage: Implementation — TDD (2026-06-20T19:47:34Z)
29
+
30
+ ### Session summary
31
+
32
+ Executed all three planned TDD cycles plus one reviewer-driven cleanup: (1) `test:` migrated the `agent-widget.test.ts` fixtures to a background invocation, (2) `feat:` added the private `listBackgroundAgents()` accessor and routed both `update()` and `renderWidget()` through it, (3) `docs:` updated `README.md` and marked roadmap Step 3 ✅.
33
+ Test count went from 24 to 26 in `agent-widget.test.ts` (two new background-only filtering tests); full pi-subagents suite is 1064 passing.
34
+
35
+ ### Observations
36
+
37
+ - The tidy-first fixture migration worked exactly as planned — adding `invocation: { runInBackground: true }` was inert (suite stayed green) until the filter landed, so step 2 was a pure addition with no fixture churn.
38
+ - New-test fixtures default the shared `makeWidget` helper to background invocation via `{ invocation: { runInBackground: true }, ...a }`, letting per-agent `invocation` override for the mixed/foreground cases in the new `describe` block.
39
+ - `widget-renderer.ts` needed no change, as the plan predicted — the filter at the single `listBackgroundAgents()` funnel is sufficient.
40
+ - Pre-completion reviewer: PASS.
41
+ One non-blocking naming WARN — `clearWidget`'s `allAgents` parameter and JSDoc were stale after the refactor (it now receives only background agents); fixed in a follow-up `refactor:` commit (`backgroundAgents`).
42
+ Landed as a separate commit rather than amending the feat commit because HEAD was already the `docs:` commit and the fix must not land in a `docs:` commit.
43
+ - All gates green: `pnpm run check`, root `pnpm run lint`, full vitest (1064), `pnpm fallow dead-code`.
44
+
45
+ ## Stage: Final Retrospective (2026-06-20T22:41:58Z)
46
+
47
+ ### Session summary
48
+
49
+ Shipped Phase 19 Step 3 end-to-end across three stages (plan → TDD → ship) in one continuous session, releasing `@gotgenes/pi-subagents` v17.2.0.
50
+ The change funnels both `manager.listAgents()` call sites through a single `listBackgroundAgents()` accessor so the above-editor widget shows only background agents.
51
+ Execution was unusually clean: no rabbit holes, no user corrections, one reviewer-caught naming miss resolved in a single follow-up commit.
52
+
53
+ ### Observations
54
+
55
+ #### What went well
56
+
57
+ - Tidy-first preparatory step paid off as designed.
58
+ TDD step 1 (`test:` migrate fixtures to background invocation) was inert until the filter landed, turning the behavior change in step 2 into a pure addition with zero fixture churn mid-cycle.
59
+ This is the `code-design` skill's "make the change easy, then make the easy change" applied to a test suite — worth reusing whenever a new predicate will retroactively exclude existing fixtures.
60
+ - The pre-completion reviewer caught semantic drift that all four deterministic gates missed.
61
+ `tsc`, `pnpm run lint`, vitest (1064), and `pnpm fallow dead-code` all passed the stale `clearWidget(allAgents)` parameter — a name whose meaning narrowed to background-only but which no tool flags, because a misleading-but-valid identifier is neither a type nor a lint error.
62
+ This is the reviewer's distinct value: naming/comment staleness after a rename.
63
+
64
+ #### What caused friction (agent side)
65
+
66
+ - `other` (incomplete rename) — during the `feat:` step I renamed the local `allAgents` → `backgroundAgents` in `update()` but left the `clearWidget` parameter (a separate scope receiving the same value) named `allAgents`, along with its JSDoc.
67
+ Impact: one extra `refactor:` commit (`319e7df3`); ~2 minutes.
68
+ No rework to logic.
69
+ Reviewer-caught, not self-caught.
70
+
71
+ #### What caused friction (user side)
72
+
73
+ - None.
74
+ The operator delegated the full plan/TDD/ship/retro workflow and it ran without correction or mid-course steering.
75
+ No earlier-context opportunity identified.
76
+
77
+ ### Changes made
78
+
79
+ 1. Added this Final Retrospective stage entry to `packages/pi-subagents/docs/retro/0444-shrink-widget-to-background-agents.md`.
80
+
81
+ No `AGENTS.md` or prompt changes: the one friction point (an incomplete rename) was reviewer-caught with trivial cost, and the wins (tidy-first fixture migration, pre-completion reviewer value) are already covered by the `code-design` and `testing` skills — nothing actionable to encode.
@@ -0,0 +1,57 @@
1
+ ---
2
+ issue: 445
3
+ issue_title: "pi-subagents: implement native session navigation for any subagent (live or completed)"
4
+ ---
5
+
6
+ # Retro: #445 — pi-subagents: implement native session navigation for any subagent (live or completed)
7
+
8
+ ## Stage: Planning (2026-06-22T00:00:00Z)
9
+
10
+ ### Session summary
11
+
12
+ Produced `docs/plans/0445-native-session-navigation.md`, a sliced plan for Phase 19 Step 4.
13
+ The operator (issue author) chose Pi's per-entry TUI-component renderer as the eventual target but explicitly invited a Kent Beck "make the change easy, then make the easy change" breakdown into incremental, releasable additions, and chose `manager.listAgents()`-only as the candidate set.
14
+ The plan scopes #445 to the first releasable vertical slice — full list → pick → read-only live transcript using `serializeConversation` text rendering behind a renderer-agnostic `TranscriptSource` seam — and names two follow-ups (TUI-component renderer; evicted-agent file source).
15
+
16
+ ### Observations
17
+
18
+ - **Two `ask_user` rounds drove scope.**
19
+ Round 1: renderer choice (text vs TUI components) and candidate-set scope.
20
+ Round 2: decomposition strategy.
21
+ The operator's note on round 1 ("If this sounds large, it's probably because it is... what would Kent Beck do") reframed the whole plan from one big issue into a sliced first release.
22
+ - **Key architectural finding — the file-snapshot branch is unreachable in #445.**
23
+ `SubagentManager.removeRecord` calls `record.disposeSession()` then `agents.delete(id)` atomically, and `disposeSession()` does not null `subagentSession`.
24
+ So no record in `listAgents()` is ever session-disposed; with the `listAgents()`-only candidate set, every listed session-ready record has a live session, and the dual-source "evicted/untracked → file" branch has no caller.
25
+ Implementing it now would be dead code that fails the `fallow dead-code` gate.
26
+ This sharpened the slice: #445 ships the **live source only** behind the seam.
27
+ - **Type-boundary plan.** `AgentMessage` is not in the `@earendil-works/pi-coding-agent` barrel and `@earendil-works/pi-agent-core` is not a dependency, so the plan derives `SessionMessage = SessionContext["messages"][number]` from the barrel-exported `SessionContext` rather than adding a dep. `serializeConversation` takes a mutable `Message[]`, so the renderer spreads (`serializeConversation([...messages])`); `AgentMessage`→`Message` assignability is flagged as a TDD step-2 `pnpm run check` verification with a typed-adapter fallback.
28
+ - **Seam justifies its weight via testability, not just the follow-up.** `TranscriptSource` + narrow `NavigableSubagent` interfaces let the pure module be unit-tested with light stubs (no full `Subagent`/`TUI`/`AgentSession`), and decouple the renderer (text→components) from sourcing (live→file) for the two named follow-ups.
29
+ - **Doomed-code avoidance.**
30
+ The navigator must not import `message-formatters.ts` or `conversation-viewer.ts` (both deleted in Step 5, [#442]); the streaming indicator is a small local helper and the transcript text is Pi's `serializeConversation`.
31
+ - **Open item for ship time:** the architecture roadmap's Step 4 description currently scopes full dual-source + components as one step and will need rescoping to match the slice, plus filing the two follow-up issues.
32
+ Command name `subagent-sessions` is proposed but flagged confirmable.
33
+ - Release: ship independently (roadmap Step 4 is `Release: independent`, spike-gated; not part of the "dissolve-agents" batch).
34
+
35
+ ## Stage: Implementation — TDD (2026-06-22T18:03:26Z)
36
+
37
+ ### Session summary
38
+
39
+ Implemented the sliced #445 in three TDD cycles: (1) the typed `agentMessages` accessor (`SessionMessage` on `SubagentSession`/`Subagent`); (2) the pure `session-navigation.ts` (selection, `liveSource`, `renderTranscriptLines`); (3) the `session-navigator.ts` overlay + `/subagent-sessions` command + `index.ts` wiring.
40
+ Test count went 1064 → 1084 (+20).
41
+ Pre-completion reviewer returned PASS.
42
+
43
+ ### Observations
44
+
45
+ - **`AgentMessage` is NOT assignable to `serializeConversation`'s `Message[]`** (the plan's flagged risk materialized): `AgentMessage` is a superset adding session-display variants like `BashExecutionMessage`.
46
+ Applied the planned mitigation — a private `toMessages` adapter casting via `as unknown as Parameters<typeof serializeConversation>[0]` (`Message` is not re-exported from the `@earendil-works/pi-ai` barrel, so the type is referenced through the function signature rather than imported by name).
47
+ - **`SessionContext` name collision:** `types.ts` already declares a local `SessionContext` interface, so the SDK import is aliased — `import type { SessionContext as SdkSessionContext }` — and `SessionMessage = SdkSessionContext["messages"][number]`.
48
+ - **`describeActivity` survives in `display.ts`** (not the doomed `message-formatters.ts`), so the streaming indicator reuses it — the navigator imports neither doomed module, as planned.
49
+ - **File-snapshot branch confirmed unreachable and omitted:** with the `listAgents()`-only candidate set, no listed record is ever session-disposed (dispose-and-delete are atomic), so the live source is always valid; implementing the file branch would have been dead code.
50
+ `fallow dead-code` is clean.
51
+ - **Minor deviation:** `test/helpers/mock-session.ts` gained an `agentMessages` getter on the session stub (needed for the `Subagent.agentMessages` delegation test) — a small fixture addition not itemized in the plan's Module-Level Changes.
52
+ - **Two lint nits caught by the pre-commit hook** (both per the testing skill): an unnecessary optional chain on a destructured array element, and an unnecessary `!` on `mock.calls[0][0]` — both fixed inline.
53
+ - **Architecture doc:** added a "Landed ([#445], sliced)" note to Step 4; did **not** mark the step `✅` because the component renderer and evicted-agent file source are deferred follow-ups.
54
+ `SKILL.md` `ui/` module count (10 → 12) left for a later Phase 19 doc-sync, per the plan.
55
+ - **Reviewer verdict: PASS.**
56
+ Two WARN notes, both intentional and plan-sanctioned: Step 4 heading unchecked (correct for a partial slice) and the deferred `SKILL.md` count.
57
+ - **Open at ship time:** confirm command name `subagent-sessions`; file the two follow-up issues (component renderer; evicted-agent file source).
@@ -44,3 +44,48 @@ Full suite, `tsc`, root lint, and `fallow dead-code` all green.
44
44
  - Pre-completion reviewer: WARN.
45
45
  Reviewer warnings: one non-blocking finding — `.pi/skills/package-pi-subagents/SKILL.md` records `ui/` as 10 modules (now 11); the plan intentionally deferred this coarse-summary update to a later Phase 19 doc-sync.
46
46
  No FAILs; all deterministic checks PASS, verbatim-lift fidelity and ISP of both narrow interfaces confirmed.
47
+
48
+ ## Stage: Final Retrospective (2026-06-20T18:36:58Z)
49
+
50
+ ### Session summary
51
+
52
+ Shipped Phase 19 Step 2 (#447) end to end across planning, TDD, and ship stages, releasing `pi-subagents` `17.1.0`.
53
+ The one off-script event was a user-reported Mermaid syntax error in `architecture.md`, which I localized by extracting all six diagram blocks and running `mmdc` on each, then fixed by quoting the offending flowchart node labels.
54
+ Execution was otherwise clean: two green TDD cycles (+11 tests), a WARN pre-completion review (one intentionally-deferred doc finding), and a correct `UNSTABLE`/`GITHUB_TOKEN` release-merge fallback.
55
+
56
+ ### Observations
57
+
58
+ #### What went well
59
+
60
+ - Localizing the Mermaid error with the diagram name wrong: the user called it the "State dependency diagram" but the broken block was the "Step dependency diagram" flowchart.
61
+ Rather than guess from the ambiguous name (I had glanced at the `stateDiagram-v2` lifecycle and the `classDiagram` first), I scripted extraction of all six `mermaid` fences and ran `mmdc` on each — block 6 failed with `got 'PS'`, pinpointing the exact line.
62
+ Brute-force validation beat name-matching.
63
+ - The release-merge fallback worked exactly as the ship prompt describes: `release_pr_merge` refused on `merge_state: UNSTABLE`, the status-check rollup was empty (the `GITHUB_TOKEN` no-checks case), and `gh pr merge 450 --rebase` + `git pull --ff-only` landed `17.1.0` linearly.
64
+
65
+ #### What caused friction (agent side)
66
+
67
+ - `missing-context` (prior session) — the broken diagram was authored in `74e2374f docs: mark Phase 19 Step 1 spike complete (#446)`: a flowchart node label `S1[✅ Step 1 - Spike (#446)]` with unquoted parentheses, which Mermaid parses as a nested round-node shape (`Expecting ... got 'PS'`).
68
+ It was committed without an `mmdc` pass and slipped through `pnpm run lint` (rumdl validates markdown, not Mermaid semantics) and CI.
69
+ Impact: surfaced two sessions later as a user-caught defect; one extra `docs:` fix commit (`90ca6e2d`) this session.
70
+ Root cause is a coverage gap: the `mermaid` skill's pitfall list does not mention parentheses/shape-delimiter characters in node labels, so the exact failure mode was undocumented.
71
+
72
+ #### What caused friction (user side)
73
+
74
+ - None material.
75
+ The diagram-name mismatch ("State" vs "Step" dependency diagram) added one or two orienting reads but did not cause rework — the all-blocks `mmdc` sweep absorbed the ambiguity.
76
+
77
+ ### Diagnostic details
78
+
79
+ - **Model-performance correlation** — the only subagent dispatch was the `pre-completion-reviewer` on `anthropic/claude-sonnet-4-6` (judgment-heavy review work); appropriate match, no over/under-powered mismatch.
80
+ - **Escalation-delay tracking** — the Mermaid detour resolved in ~5 progress-making tool calls (extract blocks → `mmdc` each → read error → quote labels → re-validate); not a rabbit-hole, no subagent escalation warranted.
81
+ - **Unused-tool detection** — none; `mmdc` (the correct validator) was used directly.
82
+ - **Feedback-loop gap analysis** — verification ran incrementally throughout TDD (per-file `vitest` in each red/green, `pnpm run check` right after the interface-bearing wiring step, full suite + root lint + `fallow dead-code` at the end), not just terminally.
83
+
84
+ ### Changes made
85
+
86
+ 1. `.pi/skills/mermaid/SKILL.md` — added a "Parentheses and special characters in node labels" pitfall (with the `S1[✅ Step 1 - Spike (#446)]` WRONG/RIGHT example from this session) covering the `(`/`[`/`{`/`:` shape-delimiter trap that produced the `got 'PS'` parse error.
87
+
88
+ ### Follow-up (not implemented — suggest a GitHub issue + `/plan-issue`)
89
+
90
+ 1. **CI Mermaid-validation gate** — run `mmdc` over every `mermaid` fence as part of `pnpm run lint` or a pre-commit hook, so a broken diagram (like `74e2374f`'s unquoted-parens node) fails at author time instead of surviving to a user-caught defect.
91
+ Out of retro scope (infra, touches `package.json`/lint config/hooks); record as its own issue.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-subagents",
3
- "version": "17.1.0",
3
+ "version": "17.3.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
package/src/index.ts CHANGED
@@ -47,6 +47,7 @@ import { SteerTool } from "#src/tools/steer-tool";
47
47
  import { FsAgentFileOps } from "#src/ui/agent-file-ops";
48
48
  import { AgentsMenuHandler } from "#src/ui/agent-menu";
49
49
  import { AgentWidget } from "#src/ui/agent-widget";
50
+ import { SessionNavigatorHandler } from "#src/ui/session-navigator";
50
51
  import { SubagentsSettingsHandler } from "#src/ui/subagents-settings";
51
52
 
52
53
  export default function (pi: ExtensionAPI) {
@@ -192,4 +193,15 @@ export default function (pi: ExtensionAPI) {
192
193
  await subagentsSettings.handle({ ui: ctx.ui });
193
194
  },
194
195
  });
196
+
197
+ // ---- /subagent-sessions command ----
198
+
199
+ const sessionNavigator = new SessionNavigatorHandler();
200
+
201
+ pi.registerCommand("subagent-sessions", {
202
+ description: "View a subagent's session transcript (read-only)",
203
+ handler: async (_args, ctx) => {
204
+ await sessionNavigator.handle({ ui: ctx.ui, agents: manager.listAgents(), registry });
205
+ },
206
+ });
195
207
  }
@@ -19,6 +19,7 @@ import { normalizeMaxTurns } from "#src/lifecycle/turn-limits";
19
19
  import { getSessionContextPercent, type SessionStatsLike } from "#src/lifecycle/usage";
20
20
  import { extractText } from "#src/session/context";
21
21
  import { getAgentConversation } from "#src/session/conversation";
22
+ import type { SessionMessage } from "#src/types";
22
23
 
23
24
  /** Outcome of one turn loop. */
24
25
  export interface TurnLoopResult {
@@ -178,6 +179,11 @@ export class SubagentSession {
178
179
  return this._session.messages as readonly unknown[];
179
180
  }
180
181
 
182
+ /** The session's message history, typed for Pi's session-rendering machinery. */
183
+ get agentMessages(): readonly SessionMessage[] {
184
+ return this._session.messages;
185
+ }
186
+
181
187
  /** Tear down: session.dispose() + emit `disposed` (registry unregister). */
182
188
  dispose(): void {
183
189
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- dispose may not exist on all session implementations