@gotgenes/pi-subagents 17.3.0 → 17.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/docs/architecture/architecture.md +47 -9
- package/docs/plans/0462-navigation-renderer-tui-components.md +338 -0
- package/docs/retro/0445-native-session-navigation.md +65 -0
- package/docs/retro/0462-navigation-renderer-tui-components.md +49 -0
- package/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/lifecycle/subagent-session.ts +6 -0
- package/src/lifecycle/subagent.ts +6 -1
- package/src/ui/session-navigation.ts +12 -35
- package/src/ui/session-navigator.ts +187 -31
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,15 @@ 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
|
+
## [17.4.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v17.3.0...pi-subagents-v17.4.0) (2026-06-23)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* add getToolDefinition accessor on subagent record ([#462](https://github.com/gotgenes/pi-packages/issues/462)) ([e0bfdac](https://github.com/gotgenes/pi-packages/commit/e0bfdacec33120d0f52e6294f3676f4776aee4af))
|
|
14
|
+
* expose getToolDefinition on the transcript source seam ([#462](https://github.com/gotgenes/pi-packages/issues/462)) ([669f5ff](https://github.com/gotgenes/pi-packages/commit/669f5ff71523122ebb011adc6875b54d7001d53a))
|
|
15
|
+
* render /subagent-sessions transcript with Pi per-entry components ([#462](https://github.com/gotgenes/pi-packages/issues/462)) ([b832a43](https://github.com/gotgenes/pi-packages/commit/b832a437c97378f701033ef8e98c6f7805d7e7a8))
|
|
16
|
+
|
|
8
17
|
## [17.3.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v17.2.0...pi-subagents-v17.3.0) (2026-06-22)
|
|
9
18
|
|
|
10
19
|
|
|
@@ -918,9 +918,9 @@ The end state deletes `agent-menu.ts` — the god-command that bundles four unre
|
|
|
918
918
|
Rather than surgically mutate that doomed module (and the #1 churn hotspot `index.ts`) once per option, Phase 19 first stands up the replacement surfaces additively, then removes the now-orphaned subtree in a single terminal cut.
|
|
919
919
|
This keeps every responsibility's old surface live until its replacement exists (ADR-0004's no-interim-regression invariant), turns the three replacement steps into genuinely parallel work (none touch `agent-menu.ts`), and reduces `index.ts` edits from four surgical removals to one deregistration.
|
|
920
920
|
|
|
921
|
-
Seven steps in three phases:
|
|
921
|
+
Seven numbered steps in three phases, plus two follow-ups (Steps 4a–4b) carved from the #445 slice:
|
|
922
922
|
|
|
923
|
-
- **Phase A — stand up replacements (additive):** spike, settings command, background widget, native session navigation (Steps 1–4).
|
|
923
|
+
- **Phase A — stand up replacements (additive):** spike, settings command, background widget, native session navigation and its renderer/source follow-ups (Steps 1–4, 4a–4b).
|
|
924
924
|
- **Phase B — dissolve `/agents` (terminal cut):** delete the orphaned subtree in two deletion commits, one per subtree (Steps 5–6).
|
|
925
925
|
- **Phase C — test health:** consolidate the test clones that survive the cut (Step 7).
|
|
926
926
|
|
|
@@ -987,7 +987,7 @@ Outcome: widget shows only background agents; foreground/widget duplication elim
|
|
|
987
987
|
|
|
988
988
|
`Release: independent`
|
|
989
989
|
|
|
990
|
-
### Step 4 — Implement native session navigation ([#445])
|
|
990
|
+
### ✅ Step 4 — Implement native session navigation ([#445])
|
|
991
991
|
|
|
992
992
|
Smell: Category C (coupling) — the bespoke `ConversationViewer` re-implements session-transcript rendering when Pi's own machinery targets the already-persisted child session JSONL.
|
|
993
993
|
This step adds the new surface alongside the existing viewer; it does not touch `agent-menu.ts`.
|
|
@@ -1013,11 +1013,41 @@ Outcome: operator views any subagent's session through Pi's native machinery —
|
|
|
1013
1013
|
Landed ([#445], sliced): #445 shipped the first releasable vertical slice — the `/subagent-sessions` command (`src/ui/session-navigator.ts`), the pure selection/sourcing/text-render core (`src/ui/session-navigation.ts`), and the typed `agentMessages` accessor (`SessionMessage` on `SubagentSession`/`Subagent`).
|
|
1014
1014
|
It is **live-source only** behind a renderer-agnostic `TranscriptSource` seam, rendered via Pi's `serializeConversation` text.
|
|
1015
1015
|
With the `manager.listAgents()`-only candidate set, no listed record is ever session-disposed (dispose-and-delete are atomic), so the file-snapshot branch has no reachable caller and was deferred to keep `fallow dead-code` clean.
|
|
1016
|
-
|
|
1017
|
-
The step heading stays unchecked until both land.
|
|
1016
|
+
Step 4 (#445, the slice) is complete and released (`pi-subagents` v17.3.0); the remaining work is now tracked as two follow-up steps behind the same seam: Step 4a ([#462]) upgrades the renderer from `serializeConversation` text to Pi's per-entry TUI components (gates Step 5 for rendering parity); Step 4b ([#463]) broadens the candidate set to evicted agents and adds the file-snapshot source (`parseSessionEntries` → `buildSessionContext`, independent).
|
|
1018
1017
|
|
|
1019
1018
|
`Release: independent` (spike-gated)
|
|
1020
1019
|
|
|
1020
|
+
### ✅ Step 4a — Upgrade native-navigation renderer to Pi TUI components ([#462])
|
|
1021
|
+
|
|
1022
|
+
Smell: Category C (coupling) — the #445 slice renders the transcript as `serializeConversation` plain text, while the bespoke `ConversationViewer` it replaces renders richer per-message formatting.
|
|
1023
|
+
Until the native renderer reaches parity, the terminal cut (Step 5) cannot delete the bespoke viewer without a fidelity regression.
|
|
1024
|
+
This step swaps the renderer behind the existing `TranscriptSource` seam (`src/ui/session-navigation.ts` / `session-navigator.ts`) for Pi's per-entry components (`AssistantMessageComponent` / `ToolExecutionComponent` / …); selection and sourcing are untouched.
|
|
1025
|
+
|
|
1026
|
+
Gates Step 5: per [ADR-0004]'s no-interim-regression invariant, the native navigator must reach rendering parity with the bespoke viewer before Step 5 deletes it.
|
|
1027
|
+
|
|
1028
|
+
Outcome: native session navigation renders at parity with the removed `ConversationViewer`; Step 5 can delete the bespoke viewer with no fidelity regression.
|
|
1029
|
+
|
|
1030
|
+
Landed ([#462]): the renderer now mounts Pi's per-entry components (`AssistantMessageComponent` / `ToolExecutionComponent` / `BashExecutionComponent` / `UserMessageComponent` / `CompactionSummaryMessageComponent` / `BranchSummaryMessageComponent` / `SkillInvocationMessageComponent`) into a `Container`, mirroring Pi's own `renderSessionContext` mapping.
|
|
1031
|
+
The `TranscriptOverlay` caches that `Container` and rebuilds it on source change only (Pi's `rebuildChatFromMessages` path), keeping the lightweight `◍` streaming indicator.
|
|
1032
|
+
Tool calls render with their real `ToolDefinition`, resolved through a dependency-safe `getToolDefinition` read accessor on the record (mirroring `agentMessages`) surfaced on the `TranscriptSource` seam — no inbound call into the core.
|
|
1033
|
+
The pure `session-navigation.ts` sheds `renderTranscriptLines`/`serializeConversation`; rendering now lives in the SDK/TUI `session-navigator.ts`, which threads `cwd` from the command context.
|
|
1034
|
+
`custom`-role messages are skipped (the bespoke viewer never rendered them either).
|
|
1035
|
+
Selection and sourcing are untouched; native navigation now renders at parity, unblocking Step 5 for rendering fidelity.
|
|
1036
|
+
|
|
1037
|
+
`Release: independent`
|
|
1038
|
+
|
|
1039
|
+
### Step 4b — File-snapshot source for evicted agents ([#463])
|
|
1040
|
+
|
|
1041
|
+
Smell: Category C (coupling) — the #445 slice sources transcripts live from `manager.listAgents()` only; an agent evicted by the 10-minute cleanup sweep has a persisted session JSONL but no live record, so it is unreachable.
|
|
1042
|
+
This step adds the file-snapshot `TranscriptSource` branch (`parseSessionEntries(readFile(outputFile))` → drop the `SessionHeader` → `buildSessionContext(...).messages`) and broadens the candidate set to enumerate evicted agents, behind the same seam; the renderer is untouched.
|
|
1043
|
+
|
|
1044
|
+
Independent: this is a new capability the bespoke viewer never had, so it gates nothing and is not a Step 5 prerequisite.
|
|
1045
|
+
Best sequenced after Step 4a (shared renderer), but carries no hard dependency.
|
|
1046
|
+
|
|
1047
|
+
Outcome: the operator can view a fully-evicted agent's transcript from its persisted session file; the dual-source design recorded in [ADR-0004] Addendum 2 is fully realized.
|
|
1048
|
+
|
|
1049
|
+
`Release: independent`
|
|
1050
|
+
|
|
1021
1051
|
### Step 5 — Dissolve `/agents` and remove the conversation-viewer subtree ([#442])
|
|
1022
1052
|
|
|
1023
1053
|
Smell: Category A (dead subsystem) plus Category B (oversized) — once Steps 2–4 re-home all four menu responsibilities, the `/agents` command and everything reachable only from `agent-menu.ts` is an unreferenced subtree.
|
|
@@ -1079,20 +1109,25 @@ flowchart LR
|
|
|
1079
1109
|
S1["✅ Step 1 - Spike (#446)"]
|
|
1080
1110
|
S2["✅ Step 2 - Settings command (#447)"]
|
|
1081
1111
|
S3["✅ Step 3 - Background widget (#444)"]
|
|
1082
|
-
S4["Step 4 - Native session nav (#445)"]
|
|
1112
|
+
S4["✅ Step 4 - Native session nav slice (#445)"]
|
|
1113
|
+
S4a["✅ Step 4a - Renderer to TUI components (#462)"]
|
|
1114
|
+
S4b["Step 4b - File-snapshot source (#463)"]
|
|
1083
1115
|
S5["Step 5 - Dissolve /agents + viewer (#442)"]
|
|
1084
1116
|
S6["Step 6 - Remove definition mgmt (#441)"]
|
|
1085
1117
|
S7["Step 7 - Test clones (#443)"]
|
|
1086
1118
|
|
|
1087
1119
|
S1 --> S4
|
|
1120
|
+
S4 --> S4a
|
|
1121
|
+
S4 --> S4b
|
|
1088
1122
|
S2 --> S5
|
|
1089
1123
|
S3 --> S5
|
|
1090
|
-
|
|
1124
|
+
S4a --> S5
|
|
1091
1125
|
S5 --> S6
|
|
1092
1126
|
S6 --> S7
|
|
1093
1127
|
```
|
|
1094
1128
|
|
|
1095
|
-
The terminal cut (Step 5) depends on all three replacements — settings (Step 2), widget (Step 3), and session navigation (Step
|
|
1129
|
+
The terminal cut (Step 5) depends on all three replacements — settings (Step 2), widget (Step 3), and session navigation **at rendering parity** (Step 4a, which completes the #445 slice) — because each of the four `/agents` options must have its responsibility re-homed, and the viewer replacement at parity, before its branch can die.
|
|
1130
|
+
Step 4b (file-snapshot source) is a new capability and gates nothing.
|
|
1096
1131
|
The old `S1 → S6 → S7` chain hid the widget dependency; this diagram makes it explicit.
|
|
1097
1132
|
|
|
1098
1133
|
### Parallel tracks
|
|
@@ -1100,6 +1135,7 @@ The old `S1 → S6 → S7` chain hid the widget dependency; this diagram makes i
|
|
|
1100
1135
|
- **Track A — Replacements (Steps 1–4):** the spike gates session navigation (Step 1 → Step 4); settings (Step 2) and the background widget (Step 3) are independent of the spike and of each other.
|
|
1101
1136
|
None of these steps edits `agent-menu.ts`, so they carry no shared-file collision on the menu — genuinely parallelizable, unlike the prior plan's Steps 2/3/5, which all collided on `agent-menu.ts` and `index.ts`.
|
|
1102
1137
|
Steps 2 and 4 each append a command registration to `index.ts` (additive, low-conflict).
|
|
1138
|
+
Steps 4a (renderer parity) and 4b (file-snapshot source) complete the #445 slice behind its `TranscriptSource` seam; Step 4a gates Step 5, Step 4b is independent.
|
|
1103
1139
|
- **Track B — Dissolution (Steps 5 → 6):** the terminal cut, gated on all of Track A landing.
|
|
1104
1140
|
Hub-first ordering is forced: Step 5 deletes `agent-menu.ts` (orphaning the leaves), then Step 6 `git rm`s the now-orphaned definition-management subtree.
|
|
1105
1141
|
- **Track C — Test health (Step 7):** clone consolidation, run after the cut so no surviving helper is extracted into a doomed file.
|
|
@@ -1108,7 +1144,7 @@ The old `S1 → S6 → S7` chain hid the widget dependency; this diagram makes i
|
|
|
1108
1144
|
|
|
1109
1145
|
- **Batch "dissolve-agents":** Steps 5, 6 (ship together; tail = Step 6).
|
|
1110
1146
|
Depends on Steps 2, 3, 4 already merged.
|
|
1111
|
-
- Independently releasable: Steps 1, 2, 3, 4, 7.
|
|
1147
|
+
- Independently releasable: Steps 1, 2, 3, 4, 4a, 4b, 7.
|
|
1112
1148
|
|
|
1113
1149
|
## Refactoring history
|
|
1114
1150
|
|
|
@@ -1230,5 +1266,7 @@ The upstream test suite is run periodically as a regression canary for the sessi
|
|
|
1230
1266
|
[#445]: https://github.com/gotgenes/pi-packages/issues/445
|
|
1231
1267
|
[#446]: https://github.com/gotgenes/pi-packages/issues/446
|
|
1232
1268
|
[#447]: https://github.com/gotgenes/pi-packages/issues/447
|
|
1269
|
+
[#462]: https://github.com/gotgenes/pi-packages/issues/462
|
|
1270
|
+
[#463]: https://github.com/gotgenes/pi-packages/issues/463
|
|
1233
1271
|
[ADR-0002]: ../decisions/0002-extensions-on-a-minimal-core.md
|
|
1234
1272
|
[ADR-0004]: ../decisions/0004-reconsider-ui-direction.md
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 462
|
|
3
|
+
issue_title: "pi-subagents: upgrade /subagent-sessions renderer to Pi per-entry TUI components"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Upgrade `/subagent-sessions` renderer to Pi per-entry TUI components
|
|
7
|
+
|
|
8
|
+
## Release Recommendation
|
|
9
|
+
|
|
10
|
+
**Release:** ship independently
|
|
11
|
+
|
|
12
|
+
Phase 19 Step 4a ([#462]) carries `Release: independent` in the architecture roadmap.
|
|
13
|
+
It swaps the renderer behind the existing `TranscriptSource` seam; it is not a member of Phase 19's only batch ("dissolve-agents", Steps 5–6, [#442]/[#441]).
|
|
14
|
+
Step 4a gates Step 5 ([#442]) for rendering parity, but a gate is a sequencing dependency, not a release batch — this step ships on its own once it lands.
|
|
15
|
+
|
|
16
|
+
## Problem Statement
|
|
17
|
+
|
|
18
|
+
The #445 slice ([#445]) stood up native session navigation — list → pick → read-only scrollable transcript — behind a renderer-agnostic `TranscriptSource` seam, using Pi's `serializeConversation` plain text as the first renderer.
|
|
19
|
+
That text dump is lower fidelity than the bespoke `ConversationViewer` it is slated to replace, which renders richer per-message formatting (tool call/result blocks, bash execution, markdown).
|
|
20
|
+
ADR-0004 Decision B's full intent is native-fidelity rendering through Pi's **own** interactive components ([ADR-0004] Addendum, Finding 1): `AssistantMessageComponent`, `ToolExecutionComponent`, `UserMessageComponent`, `BashExecutionComponent`, `CompactionSummaryMessageComponent`, `BranchSummaryMessageComponent`, and the skill-block components.
|
|
21
|
+
This follow-up swaps the renderer behind the same seam — selection and sourcing are untouched — so the native navigator reaches rendering parity with the bespoke viewer, which unblocks Step 5's deletion of that viewer without a fidelity regression.
|
|
22
|
+
|
|
23
|
+
## Goals
|
|
24
|
+
|
|
25
|
+
- Replace the `serializeConversation` text path with a per-entry Pi TUI component tree, mirroring Pi's own `renderSessionContext` message→component mapping.
|
|
26
|
+
- Drive the component tree by **rebuild-on-change**: on each `TranscriptSource` change, clear and reconstruct the cached component `Container` from the current message snapshot (Pi's own `rebuildChatFromMessages` path); each paint reuses the cached tree, so markdown highlighting does not re-run per frame.
|
|
27
|
+
- Render tool calls at full fidelity by passing each tool's real `ToolDefinition`, sourced through a new dependency-safe `getToolDefinition` read accessor on the record (mirroring the existing `agentMessages` accessor) and surfaced on the `TranscriptSource` seam.
|
|
28
|
+
- Keep the existing lightweight `◍ describeActivity` streaming-indicator line for a running agent's live activity (the high-frequency streaming text it absorbs is why rebuild-on-change stays cheap).
|
|
29
|
+
- Confirm rendering parity with the bespoke `ConversationViewer` so Step 5 ([#442]) can delete it with no fidelity regression.
|
|
30
|
+
|
|
31
|
+
Non-breaking: this is an internal renderer swap behind the seam.
|
|
32
|
+
No public export, command name, default, or observable behavior changes.
|
|
33
|
+
The internal `renderTranscriptLines` export is removed, but its only consumers are within this package (the overlay and its tests).
|
|
34
|
+
|
|
35
|
+
## Non-Goals
|
|
36
|
+
|
|
37
|
+
- Do **not** implement the **file-snapshot** source branch (`parseSessionEntries` → `buildSessionContext`) or broaden the candidate set to evicted agents — that is Step 4b ([#463]), independent and unblocked by this step.
|
|
38
|
+
- Do **not** add in-session steering or any interactive gesture — the overlay stays strictly read-only; steering remains in `steer_subagent` and the widget.
|
|
39
|
+
- Do **not** touch `agent-menu.ts`, `conversation-viewer.ts`, or `message-formatters.ts` — they stay live until the terminal cut (Step 5, [#442]).
|
|
40
|
+
- Do **not** change `listNavigableAgents` selection or the `liveSource` sourcing semantics — only the renderer changes, plus the additive `getToolDefinition` accessor.
|
|
41
|
+
- Do **not** render `custom`-role messages through `CustomMessageComponent` — it requires the session's `extensionRunner.getMessageRenderer`, which the navigator does not hold (see Open Questions); custom messages are skipped, matching the bespoke viewer, which never rendered them either.
|
|
42
|
+
|
|
43
|
+
## Background
|
|
44
|
+
|
|
45
|
+
Relevant existing modules:
|
|
46
|
+
|
|
47
|
+
- `src/ui/session-navigation.ts` (pure) — owns selection and sourcing: `listNavigableAgents`, `liveSource`, and the interfaces `NavigableSubagent`, `NavigationEntry`, `StreamingState`, `TranscriptSource`.
|
|
48
|
+
It currently also owns `renderTranscriptLines(source): string[]` (the `serializeConversation` text path) and its `toMessages` adapter — these are text rendering via a pure SDK utility.
|
|
49
|
+
- `src/ui/session-navigator.ts` (SDK/TUI consumer) — owns `TranscriptOverlay` (a read-only scrollable `Component`) and `SessionNavigatorHandler`.
|
|
50
|
+
`TranscriptOverlay.buildContentLines` currently calls `renderTranscriptLines(this.source)`, then wraps each line with the injected `wrapText`.
|
|
51
|
+
- `src/lifecycle/subagent-session.ts` — `SubagentSession` wraps `_session: AgentSession`.
|
|
52
|
+
It already exposes `get agentMessages(): readonly SessionMessage[]` returning `this._session.messages`.
|
|
53
|
+
The SDK `AgentSession` exposes `getToolDefinition(name: string): ToolDefinition | undefined` (a definition-first registry retained per session).
|
|
54
|
+
- `src/lifecycle/subagent.ts` — `Subagent` delegates `get agentMessages()` to `this.subagentSession?.agentMessages ?? []`; this is the exact pattern the new `getToolDefinition` accessor follows.
|
|
55
|
+
- `src/ui/display.ts` — `describeActivity(activeTools, responseText)`, `getDisplayName`, `formatDuration`.
|
|
56
|
+
- `src/index.ts` — registers the `subagent-sessions` command; the handler already receives `ctx` (which carries `ctx.cwd: string` and `ctx.ui`).
|
|
57
|
+
|
|
58
|
+
Pi SDK facts verified for this plan (against the bundled `@earendil-works/pi-coding-agent`):
|
|
59
|
+
|
|
60
|
+
- The per-entry components are all re-exported from the public root barrel: `AssistantMessageComponent`, `UserMessageComponent`, `ToolExecutionComponent`, `BashExecutionComponent`, `CompactionSummaryMessageComponent`, `BranchSummaryMessageComponent`, `SkillInvocationMessageComponent`, `CustomMessageComponent`.
|
|
61
|
+
`parseSkillBlock`, `getMarkdownTheme`, and the `ToolDefinition` type are also public.
|
|
62
|
+
- Component constructors are heterogeneous and stateful (`Container` subclasses):
|
|
63
|
+
- `AssistantMessageComponent(message?, hideThinkingBlock?, markdownTheme?, hiddenThinkingLabel?)`.
|
|
64
|
+
- `UserMessageComponent(text: string, markdownTheme?)`.
|
|
65
|
+
- `ToolExecutionComponent(toolName, toolCallId, args, options, toolDefinition, ui: TUI, cwd: string)`, with `updateResult(result)` called separately to attach the tool result.
|
|
66
|
+
- `BashExecutionComponent(command, ui: TUI, excludeFromContext?)`, with `appendOutput` / `setComplete`.
|
|
67
|
+
- `CompactionSummaryMessageComponent(message, markdownTheme?)` / `BranchSummaryMessageComponent(message, markdownTheme?)`.
|
|
68
|
+
- Pi's own message→component mapping is `interactive-mode.ts`'s `renderSessionContext(sessionContext)` (and the `addMessageToChat` helper it calls); `rebuildChatFromMessages()` is `chatContainer.clear()` + `renderSessionContext(buildSessionContext())`.
|
|
69
|
+
This plan mirrors that mapping; the `toolResult` → pending-tool match (`renderedPendingTools.get(toolCallId).updateResult(message)`) is reproduced.
|
|
70
|
+
- `Container` (from `@earendil-works/pi-tui`) exposes `addChild`, `clear`, and `render(width): string[]`, which stacks children vertically — so a `Container` of per-entry components renders to the `string[]` the existing scroll overlay already consumes.
|
|
71
|
+
- No higher-level "message list" / "transcript" renderer is exported from the SDK — the mapping must be mirrored, not imported.
|
|
72
|
+
|
|
73
|
+
AGENTS.md / package constraints:
|
|
74
|
+
|
|
75
|
+
- pi-subagents is a minimal core; the navigator is an in-core reactive consumer with **no inbound call** into the core.
|
|
76
|
+
The new `getToolDefinition` accessor is a read-only getter (like `agentMessages`) — it adds no inbound call and preserves the Phase 18 spine invariants ([#422]–[#425]).
|
|
77
|
+
This is the dependency-safe answer to "wire real tool definitions": arrows still point inward (navigator → record getter → wrapped `AgentSession.getToolDefinition`); `SubagentManager` tracks nothing extra and no dependency is inverted.
|
|
78
|
+
- Keep Pi SDK *coupling* out of pure helpers: per-entry components require a `TUI`, `cwd`, and `markdownTheme`, so the component-building renderer lives in the SDK/TUI module `session-navigator.ts`, not in the pure `session-navigation.ts`.
|
|
79
|
+
The `serializeConversation` text renderer leaves the pure module entirely.
|
|
80
|
+
- Use narrow interface types (not the concrete `Subagent`) at the seam — unchanged from the slice.
|
|
81
|
+
|
|
82
|
+
## Design Overview
|
|
83
|
+
|
|
84
|
+
The `TranscriptSource` seam is the change point.
|
|
85
|
+
Its sourcing surface (`getMessages` / `subscribe` / `streaming`) is unchanged except for one additive method (`getToolDefinition`); the **renderer** moves from the pure module into the SDK/TUI module and changes shape from text to a component tree.
|
|
86
|
+
|
|
87
|
+
### Dependency-safe tool definitions
|
|
88
|
+
|
|
89
|
+
The SDK `AgentSession` already retains a definition-first tool registry and exposes `getToolDefinition(name)`.
|
|
90
|
+
A new read accessor surfaces it through the record, mirroring `agentMessages`:
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
// SubagentSession
|
|
94
|
+
getToolDefinition(name: string): ToolDefinition | undefined {
|
|
95
|
+
return this._session.getToolDefinition(name);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Subagent
|
|
99
|
+
getToolDefinition(name: string): ToolDefinition | undefined {
|
|
100
|
+
return this.subagentSession?.getToolDefinition(name);
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
This is a pure outward read — no inbound call, no `SubagentManager` bookkeeping, no dependency inversion.
|
|
105
|
+
|
|
106
|
+
### Seam: `TranscriptSource` gains `getToolDefinition`
|
|
107
|
+
|
|
108
|
+
`TranscriptSource` (and the `NavigableSubagent` record interface it sources from) gain the lookup; `liveSource` delegates to the record:
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
export interface TranscriptSource {
|
|
112
|
+
getMessages(): readonly SessionMessage[];
|
|
113
|
+
subscribe(onChange: () => void): (() => void) | undefined;
|
|
114
|
+
streaming(): StreamingState | undefined;
|
|
115
|
+
getToolDefinition(name: string): ToolDefinition | undefined; // new
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
A future file-snapshot source (Step 4b) returns `undefined` here → generic fallback tool rendering, with no renderer change.
|
|
120
|
+
Placing the lookup on the seam (not on the renderer's separate params) keeps the seam the single change point across both follow-ups.
|
|
121
|
+
|
|
122
|
+
### Renderer: per-entry component tree (`session-navigator.ts`)
|
|
123
|
+
|
|
124
|
+
A new function builds a `Container` of per-entry components from a message snapshot, mirroring `renderSessionContext`:
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
function buildTranscriptComponents(
|
|
128
|
+
messages: readonly SessionMessage[],
|
|
129
|
+
opts: { tui: TUI; cwd: string; markdownTheme: MarkdownTheme; getToolDefinition: (name: string) => ToolDefinition | undefined },
|
|
130
|
+
): Container {
|
|
131
|
+
const container = new Container();
|
|
132
|
+
const pendingTools = new Map<string, ToolExecutionComponent>();
|
|
133
|
+
for (const message of messages) {
|
|
134
|
+
switch (message.role) {
|
|
135
|
+
case "assistant": { /* AssistantMessageComponent + per-toolCall ToolExecutionComponent (track pending by id) */ }
|
|
136
|
+
case "toolResult": { /* pendingTools.get(toolCallId)?.updateResult(message) */ }
|
|
137
|
+
case "user": { /* parseSkillBlock → SkillInvocation(+UserMessage) | UserMessageComponent */ }
|
|
138
|
+
case "bashExecution": { /* BashExecutionComponent + appendOutput + setComplete */ }
|
|
139
|
+
case "compactionSummary": { /* CompactionSummaryMessageComponent */ }
|
|
140
|
+
case "branchSummary": { /* BranchSummaryMessageComponent */ }
|
|
141
|
+
// custom: skipped (no message renderer available — see Open Questions)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return container;
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`ToolExecutionComponent` is constructed with `opts.getToolDefinition(content.name)` for its definition, `opts.tui`, and `opts.cwd`, and `{ showImages: false }` (the read-only viewer does not render images).
|
|
149
|
+
Spacers between entries follow Pi's own spacing in `addMessageToChat`.
|
|
150
|
+
|
|
151
|
+
### Overlay: rebuild-on-change with a cached `Container`
|
|
152
|
+
|
|
153
|
+
`TranscriptOverlay` caches the component tree and rebuilds it only when the source changes — never per frame:
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
// constructor: build once, then subscribe
|
|
157
|
+
this.content = this.rebuild();
|
|
158
|
+
this.unsubscribe = source.subscribe(() => {
|
|
159
|
+
if (this.closed) return;
|
|
160
|
+
this.content = this.rebuild(); // rebuild on change only
|
|
161
|
+
this.tui.requestRender();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
private rebuild(): Container {
|
|
165
|
+
return buildTranscriptComponents(this.source.getMessages(), {
|
|
166
|
+
tui: this.tui, cwd: this.cwd, markdownTheme: this.markdownTheme,
|
|
167
|
+
getToolDefinition: (name) => this.source.getToolDefinition(name),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private buildContentLines(innerW: number): string[] {
|
|
172
|
+
const lines = this.content.render(innerW); // components own their own wrapping
|
|
173
|
+
const streaming = this.source.streaming();
|
|
174
|
+
if (streaming) lines.push("", `◍ ${describeActivity(streaming.activeTools, streaming.responseText)}`);
|
|
175
|
+
return lines.map((l) => truncateToWidth(l, innerW)); // defensive truncate; no re-wrap
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
The per-frame `render(width)` chrome (header/footer/scroll math) is unchanged; only the content source changes from `renderTranscriptLines(source)` to the cached `Container`.
|
|
180
|
+
Because the components already wrap to width, the overlay no longer re-wraps each line — the injected `wrapText` collaborator is removed from `TranscriptOverlayOptions` and the handler call site.
|
|
181
|
+
|
|
182
|
+
### Handler / registration
|
|
183
|
+
|
|
184
|
+
`SessionNavigatorParams` gains `cwd: string`; `index.ts` passes `ctx.cwd`:
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
await sessionNavigator.handle({ ui: ctx.ui, agents: manager.listAgents(), registry, cwd: ctx.cwd });
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
The handler obtains `markdownTheme` via `getMarkdownTheme()` (current theme; Pi has initialized it at startup) and threads `cwd` + `markdownTheme` into the `TranscriptOverlay` it mounts through `ui.custom`.
|
|
191
|
+
`manager.listAgents()` is still called in the registration, never inside the navigator — the navigator stays a reactive consumer with no inbound core call.
|
|
192
|
+
|
|
193
|
+
### Consumer call-site sketch (Tell-Don't-Ask / LoD check)
|
|
194
|
+
|
|
195
|
+
The renderer talks only to `TranscriptSource`: it reads `source.getMessages()` and `source.getToolDefinition(name)`, never reaching through the record to a stranger.
|
|
196
|
+
`liveSource.getToolDefinition` delegates to `record.getToolDefinition` (the record's own getter), which delegates to the wrapped `AgentSession` internally — a one-hop delegation, not a `record.session.x.y` chain.
|
|
197
|
+
No output arguments: the renderer constructs and returns a `Container`; nothing writes back into the record or the source.
|
|
198
|
+
|
|
199
|
+
### Design-review findings
|
|
200
|
+
|
|
201
|
+
| Smell | Location | Evidence | Result |
|
|
202
|
+
| ------------------------------ | ------------------------------ | ---------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
|
|
203
|
+
| Dependency width | `TranscriptSource` (4 methods) | renderer reads `getMessages`/`streaming`/`getToolDefinition`; overlay reads `subscribe` | Acceptable — cohesive sourcing seam; every method is a real read |
|
|
204
|
+
| LoD violation | `liveSource.getToolDefinition` | one-hop delegation `record.getToolDefinition` → `AgentSession.getToolDefinition` | None — delegation through the record's own getter, no chain |
|
|
205
|
+
| Output argument | `buildTranscriptComponents` | returns a `Container`; `updateResult` mutates the tool component it owns, not a received bag | None |
|
|
206
|
+
| Concrete-class leak | seam interfaces | renderer depends on `TranscriptSource`, not the concrete `Subagent` | None — narrow interface |
|
|
207
|
+
| Pi SDK coupling in pure module | `session-navigation.ts` | component rendering moves out to `session-navigator.ts`; pure module keeps selection + sourcing only | Improved — pure module sheds the `serializeConversation` renderer |
|
|
208
|
+
|
|
209
|
+
The new collaborator (`buildTranscriptComponents`) gives behavior to data — it maps message data to stateful components and returns a renderable tree — not procedure-splitting.
|
|
210
|
+
|
|
211
|
+
### Edge cases
|
|
212
|
+
|
|
213
|
+
- Empty history (`getMessages()` is `[]`) → empty `Container`; the overlay renders an empty viewport (no crash); the existing "no sessions" notify path is unchanged at the selection layer.
|
|
214
|
+
- Running agent → component tree for finalized messages plus the appended `◍ describeActivity` indicator; a `subscribeToUpdates` event rebuilds the tree and re-renders.
|
|
215
|
+
- A partially-streamed assistant message present in `getMessages()` → rendered by `AssistantMessageComponent`; the indicator also shows activity (parity with the bespoke viewer, which showed both).
|
|
216
|
+
- `toolResult` with no matching pending tool (out-of-order / truncated history) → no-op (matches Pi's `renderedPendingTools.get` miss).
|
|
217
|
+
- `getToolDefinition` returns `undefined` (definition not registered, or a future file source) → `ToolExecutionComponent` generic fallback rendering.
|
|
218
|
+
- Very narrow terminal width → `render` returns `[]` below the minimum width (unchanged chrome guard).
|
|
219
|
+
|
|
220
|
+
## Module-Level Changes
|
|
221
|
+
|
|
222
|
+
- **Changed** `src/types.ts` — re-export or reference the SDK `ToolDefinition` type for the seam interfaces (import from `@earendil-works/pi-coding-agent`); confirm a consumer imports it from the barrel if added there (no speculative re-export).
|
|
223
|
+
- **Changed** `src/lifecycle/subagent-session.ts` — add `getToolDefinition(name): ToolDefinition | undefined` delegating to `this._session.getToolDefinition(name)`.
|
|
224
|
+
- **Changed** `src/lifecycle/subagent.ts` — add `getToolDefinition(name): ToolDefinition | undefined` delegating to `this.subagentSession?.getToolDefinition(name)`.
|
|
225
|
+
- **Changed** `src/ui/session-navigation.ts` — add `getToolDefinition` to `NavigableSubagent` and `TranscriptSource`; `liveSource` delegates it.
|
|
226
|
+
**Remove** `renderTranscriptLines`, its `toMessages` adapter, and the `serializeConversation` + `describeActivity` imports (rendering moves to `session-navigator.ts`).
|
|
227
|
+
- **Changed** `src/ui/session-navigator.ts` — add `buildTranscriptComponents`; rewrite `TranscriptOverlay` to cache + rebuild the `Container` (drop `wrapText` from `TranscriptOverlayOptions`); add `describeActivity`, `getMarkdownTheme`, `Container`, and the per-entry component imports; add `cwd` (+ `markdownTheme` wiring) to `SessionNavigatorParams`/the overlay.
|
|
228
|
+
- **Changed** `src/index.ts` — pass `cwd: ctx.cwd` into `sessionNavigator.handle(...)`.
|
|
229
|
+
- **Changed** `test/lifecycle/subagent-session.test.ts`, `test/lifecycle/subagent.test.ts` — add `getToolDefinition` accessor tests.
|
|
230
|
+
- **Changed** `test/ui/session-navigation.test.ts` — remove the `renderTranscriptLines` describe block; add `liveSource.getToolDefinition` delegation; add `getToolDefinition` to the `makeNavigable` factory.
|
|
231
|
+
- **Changed** `test/ui/session-navigator.test.ts` — assert component-rendered content (text still appears via markdown), add a tool-call render test (stub `getToolDefinition`), add `getToolDefinition` to `fakeSource`/`makeNavigable`, pass `cwd` in handler tests, drop `wrapText`.
|
|
232
|
+
|
|
233
|
+
Removed-symbol grep: `renderTranscriptLines` is the sole removed export.
|
|
234
|
+
Its consumers are `src/ui/session-navigator.ts` (rewritten this issue) and `test/ui/session-navigation.test.ts` (block removed); confirmed no other `src/`/`test/` references and no `.pi/skills/package-pi-subagents/SKILL.md` mention.
|
|
235
|
+
|
|
236
|
+
Doc / skill updates:
|
|
237
|
+
|
|
238
|
+
- `docs/architecture/architecture.md` — flip Step 4a's status to landed (mirroring the Step 4 "Landed" note style) and update the Phase 19 Mermaid node label `S4a` from pending to done; record that the native navigator now renders at parity, unblocking Step 5.
|
|
239
|
+
Defer the status-line/Mermaid flip to ship time if preferred (matches the [#447] precedent), but the rescoping note belongs in this issue's doc step.
|
|
240
|
+
- `.pi/skills/package-pi-subagents/SKILL.md` — the UI domain count is a coarse summary (no file added or removed here); leave unless the operator asks otherwise.
|
|
241
|
+
|
|
242
|
+
## Test Impact Analysis
|
|
243
|
+
|
|
244
|
+
1. **New tests enabled.**
|
|
245
|
+
The `getToolDefinition` accessor gets direct unit tests on `SubagentSession` and `Subagent` (mirroring `agentMessages`).
|
|
246
|
+
The component renderer is exercised through `TranscriptOverlay` against a `fakeSource` — asserting that message text renders (via the components) and that a tool call renders with its definition.
|
|
247
|
+
2. **Existing tests that become redundant.**
|
|
248
|
+
The `renderTranscriptLines` describe block in `session-navigation.test.ts` is removed — the function it tests is gone (rendering moved and changed shape).
|
|
249
|
+
No other tests become redundant.
|
|
250
|
+
3. **Existing tests that must stay as-is.**
|
|
251
|
+
`listNavigableAgents` and `liveSource` selection/sourcing tests stay (selection and sourcing semantics are unchanged; `liveSource` only gains the `getToolDefinition` delegation).
|
|
252
|
+
All bespoke-viewer and menu tests (`conversation-viewer.test.ts`, `message-formatters.test.ts`, `agent-menu.test.ts`) stay untouched — Step 5 deletes those.
|
|
253
|
+
|
|
254
|
+
## Invariants at risk
|
|
255
|
+
|
|
256
|
+
This issue swaps a renderer behind the seam, adds one read accessor, and threads `cwd`; it does not touch the Phase 18 spine.
|
|
257
|
+
The relevant `Outcome:` invariants and their pins:
|
|
258
|
+
|
|
259
|
+
- **[#422] — runtime holds zero UI state.**
|
|
260
|
+
The overlay holds its own cached `Container` + scroll state; nothing is pushed onto the runtime.
|
|
261
|
+
Pinned by the runtime/service-spine tests; unchanged.
|
|
262
|
+
- **[#423] — UI is a reactive consumer with no inbound calls from core spawn tools.**
|
|
263
|
+
The navigator reads via the injected `manager.listAgents()` snapshot and record getters only; the new `getToolDefinition` accessor is a read-only getter.
|
|
264
|
+
Pinned by the navigator-reads-only test from the slice (extend it to assert the handler reads `getToolDefinition` only through the source).
|
|
265
|
+
- **[#424] — the `subagent` tool depends only on manager/runtime/settings/registry.**
|
|
266
|
+
No tool change.
|
|
267
|
+
- **[#425] — declared event channels equal emitted channels, no vacant hook.**
|
|
268
|
+
The navigator subscribes to existing record updates; it declares/emits no new channel.
|
|
269
|
+
Pinned by the events-contract suite; unchanged.
|
|
270
|
+
|
|
271
|
+
A later phase step (Step 4b file source, Step 5 deletion) must not regress these with a green suite — the navigator-reads-only test pins #423 for the seam.
|
|
272
|
+
|
|
273
|
+
## TDD Order
|
|
274
|
+
|
|
275
|
+
1. **Red→Green: `getToolDefinition` read accessor on the record.**
|
|
276
|
+
Test surface: `test/lifecycle/subagent-session.test.ts`, `test/lifecycle/subagent.test.ts`.
|
|
277
|
+
Tests: `SubagentSession.getToolDefinition` delegates to the wrapped session; `Subagent.getToolDefinition` delegates to the session and returns `undefined` when no session.
|
|
278
|
+
Implementation: reference the SDK `ToolDefinition` type (via `src/types.ts` or a direct import); add the two getters.
|
|
279
|
+
Run `pnpm run check` (verifies `AgentSession.getToolDefinition` resolves).
|
|
280
|
+
Commit: `feat: add getToolDefinition accessor on subagent record (#462)`.
|
|
281
|
+
2. **Red→Green: surface `getToolDefinition` on the seam (additive).**
|
|
282
|
+
Test surface: `test/ui/session-navigation.test.ts`.
|
|
283
|
+
Tests: `liveSource.getToolDefinition` delegates to `record.getToolDefinition`; add `getToolDefinition` to the `makeNavigable` factory.
|
|
284
|
+
Implementation: add `getToolDefinition` to `NavigableSubagent` and `TranscriptSource`; `liveSource` delegates.
|
|
285
|
+
This step is pure addition — `renderTranscriptLines` still exists and compiles.
|
|
286
|
+
Commit: `feat: expose getToolDefinition on the transcript source seam (#462)`.
|
|
287
|
+
3. **Red→Green: component-tree renderer + overlay rewrite + `cwd` wiring.**
|
|
288
|
+
Test surface: `test/ui/session-navigator.test.ts` (+ remove the `renderTranscriptLines` block from `test/ui/session-navigation.test.ts`).
|
|
289
|
+
Tests:
|
|
290
|
+
- `TranscriptOverlay.render` paints component-rendered content for user/assistant messages (text appears); a tool call renders via `ToolExecutionComponent` using a stubbed `getToolDefinition`; the `◍` streaming indicator still appends while running; rebuild-on-change re-renders after a `subscribe` callback; `esc`/`q` calls `done`; `dispose` unsubscribes.
|
|
291
|
+
- `SessionNavigatorHandler.handle`: empty entries → `notify`, no `custom`; cancel → no `custom`; valid pick → `custom` opened with an overlay sourced from the picked record and the passed `cwd`.
|
|
292
|
+
Implementation (one commit — removing `renderTranscriptLines` breaks the overlay and its tests at the type level, so the rewrite, the export removal, the test updates, and the `index.ts` call-site update land together):
|
|
293
|
+
- Add `buildTranscriptComponents` and rewrite `TranscriptOverlay` (cached `Container`, rebuild-on-change, drop `wrapText`) in `session-navigator.ts`.
|
|
294
|
+
- Remove `renderTranscriptLines`/`toMessages`/`serializeConversation`/`describeActivity` from `session-navigation.ts`.
|
|
295
|
+
- Add `cwd` to `SessionNavigatorParams`; update `index.ts` to pass `ctx.cwd`.
|
|
296
|
+
- Remove the `renderTranscriptLines` describe block from `session-navigation.test.ts`.
|
|
297
|
+
Run `pnpm run check` immediately (shared-interface + single-call-site change) and `pnpm fallow dead-code` before pushing (confirms no orphaned helper after the renderer move).
|
|
298
|
+
Commit: `feat: render /subagent-sessions transcript with Pi per-entry components (#462)`.
|
|
299
|
+
4. **Docs: record rendering parity and flip Step 4a.**
|
|
300
|
+
Test surface: none (docs).
|
|
301
|
+
Update `docs/architecture/architecture.md` Step 4a status to landed and the `S4a` Mermaid node; note the native navigator now renders at parity, unblocking Step 5.
|
|
302
|
+
Commit: `docs: mark native-navigation renderer upgrade landed (#462)`.
|
|
303
|
+
|
|
304
|
+
## Risks and Mitigations
|
|
305
|
+
|
|
306
|
+
- **Risk: Pi's per-entry components depend on interactive-mode globals (e.g. `initTheme`) not initialized in this context.**
|
|
307
|
+
Mitigation: the navigator runs inside a live Pi session where theme is already initialized; `getMarkdownTheme()` returns the active theme.
|
|
308
|
+
The overlay-render tests construct components against a mock `TUI` and assert text appears — a smoke test that the components render outside interactive mode.
|
|
309
|
+
If a component throws without a global, narrow the role set or supply the missing default at the boundary.
|
|
310
|
+
- **Risk: rebuilding the component tree re-runs markdown highlighting and is expensive under live streaming.**
|
|
311
|
+
Mitigation: rebuild happens on **source change only** (cached `Container`), not per frame, and the high-frequency streaming text is absorbed by the `◍` indicator (so the tree changes at message granularity).
|
|
312
|
+
Incremental `updateContent`/`updateResult` reconciliation remains a clean follow-up behind the same seam if profiling ever shows cost (track-and-watch).
|
|
313
|
+
- **Risk: `ToolExecutionComponent` needs `cwd`/`tui`/options not previously threaded.**
|
|
314
|
+
Mitigation: `cwd` comes from `ctx.cwd` (verified present on the command context), `tui` from the `ui.custom` factory, options default to `{ showImages: false }`.
|
|
315
|
+
- **Risk: removing `renderTranscriptLines` and its tests leaves orphaned imports.**
|
|
316
|
+
Mitigation: Step 3 removes the `serializeConversation`/`describeActivity` imports from `session-navigation.ts` in the same commit; `pnpm fallow dead-code` and a re-read of the file confirm no orphans (Biome `noUnusedImports` is warning-level).
|
|
317
|
+
- **Risk: parity is asserted only by "text appears", missing richer formatting differences.**
|
|
318
|
+
Mitigation: the goal is *native-fidelity via Pi's own components* — using the same components Pi uses **is** the parity definition; the tests assert the component path is taken (tool call renders via `ToolExecutionComponent`, bash via `BashExecutionComponent`), not byte-equality with the bespoke output.
|
|
319
|
+
|
|
320
|
+
## Open Questions
|
|
321
|
+
|
|
322
|
+
- **`custom`-role messages** — `CustomMessageComponent` requires the session's `extensionRunner.getMessageRenderer(customType)`, which the navigator does not hold.
|
|
323
|
+
This issue skips custom messages (the bespoke viewer never rendered them either, so this is not a parity regression).
|
|
324
|
+
If custom-message fidelity is later wanted, a follow-up would surface a message-renderer lookup on the seam — not filed now (speculative).
|
|
325
|
+
- **Step 4a doc flip timing** — flip the architecture status line and Mermaid node in this issue's doc step, or defer to a Phase 19 doc-sync ([#447] precedent); the plan does it in-issue (TDD step 4) by default.
|
|
326
|
+
|
|
327
|
+
[#441]: https://github.com/gotgenes/pi-packages/issues/441
|
|
328
|
+
[#442]: https://github.com/gotgenes/pi-packages/issues/442
|
|
329
|
+
[#445]: https://github.com/gotgenes/pi-packages/issues/445
|
|
330
|
+
[#447]: https://github.com/gotgenes/pi-packages/issues/447
|
|
331
|
+
[#462]: https://github.com/gotgenes/pi-packages/issues/462
|
|
332
|
+
[#463]: https://github.com/gotgenes/pi-packages/issues/463
|
|
333
|
+
[#422]: https://github.com/gotgenes/pi-packages/issues/422
|
|
334
|
+
[#423]: https://github.com/gotgenes/pi-packages/issues/423
|
|
335
|
+
[#424]: https://github.com/gotgenes/pi-packages/issues/424
|
|
336
|
+
[#425]: https://github.com/gotgenes/pi-packages/issues/425
|
|
337
|
+
|
|
338
|
+
[ADR-0004]: https://github.com/gotgenes/pi-packages/blob/main/packages/pi-subagents/docs/decisions/0004-reconsider-ui-direction.md
|
|
@@ -55,3 +55,68 @@ Pre-completion reviewer returned PASS.
|
|
|
55
55
|
- **Reviewer verdict: PASS.**
|
|
56
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
57
|
- **Open at ship time:** confirm command name `subagent-sessions`; file the two follow-up issues (component renderer; evicted-agent file source).
|
|
58
|
+
|
|
59
|
+
## Stage: User Note (2026-06-22T18:26:37Z)
|
|
60
|
+
|
|
61
|
+
When new issues are identified during planning, file them before leaving the planning stage rather than deferring to ship time.
|
|
62
|
+
Two motivations: the planning session uses a very capable model, and it already has all the context needed to write the issues well.
|
|
63
|
+
Applied here: the two follow-up issues (#462 renderer upgrade, #463 file-snapshot source) were identified during planning but filed only at ship time — they should have been created at the end of the planning commit instead.
|
|
64
|
+
|
|
65
|
+
## Stage: Final Retrospective (2026-06-22T18:28:31Z)
|
|
66
|
+
|
|
67
|
+
### Session summary
|
|
68
|
+
|
|
69
|
+
One continuous session carried #445 from planning through TDD, ship, and a mid-session retro note.
|
|
70
|
+
It delivered the sliced Phase 19 Step 4 — the `/subagent-sessions` command, the live `TranscriptSource`, and the typed `agentMessages` accessor — across three `feat:` commits plus docs, released as `pi-subagents` v17.3.0, with two follow-ups (#462, #463) filed at ship.
|
|
71
|
+
Execution was notably clean: the plan anticipated both hard risks, and every implementation slip was absorbed by the feedback loops within its active TDD cycle.
|
|
72
|
+
|
|
73
|
+
### Observations
|
|
74
|
+
|
|
75
|
+
#### What went well
|
|
76
|
+
|
|
77
|
+
1. **The plan pre-empted the two hardest risks.**
|
|
78
|
+
`AgentMessage`→`Message` non-assignability and the unreachable file-snapshot branch were both called out in planning and both materialized exactly as predicted; the planned mitigations (the `toMessages` adapter; shipping the live source only) applied with zero rework.
|
|
79
|
+
2. **The `TranscriptSource` seam earned its keep.**
|
|
80
|
+
The pure `session-navigation.ts` was unit-tested with light stubs (no full `Subagent`/`TUI`/`AgentSession`), and the seam cleanly hosts both deferred follow-ups — testability justified it independent of the follow-ups.
|
|
81
|
+
3. **Tight, incremental feedback loops.**
|
|
82
|
+
Per-cycle affected-file test runs, `pnpm run check` after every type-touching step, and full suite + lint + `fallow dead-code` after the last step caught every slip within seconds.
|
|
83
|
+
4. **Two `ask_user` rounds during planning converted a large issue into a releasable slice** (the operator's Kent Beck nudge) — the central design win of the issue.
|
|
84
|
+
|
|
85
|
+
#### What caused friction (agent side)
|
|
86
|
+
|
|
87
|
+
1. `missing-context` — wrote test assertions before checking helper behavior: `formatMs` yields `3.0s` not `3s`, `getDisplayName` falls back to `Agent`, `TOOL_DISPLAY` keys are lowercase, and `AgentTypeRegistry` takes `() => new Map()` not `() => []`.
|
|
88
|
+
Impact: ~4 assertion fixes within one Green iteration; no rework beyond the cycle.
|
|
89
|
+
2. `missing-context` — `SessionContext` name collision: imported the SDK type into `types.ts` without noticing a local `SessionContext` interface already existed.
|
|
90
|
+
Impact: one aliasing edit (`SdkSessionContext`), caught immediately by biome `noRedeclare`.
|
|
91
|
+
3. `other` (mechanical) — a stray `}` when wrapping a new `describe` block (AGENTS.md already warns about brace-pair edits), and `SessionMessage` imported from the wrong module in the navigator test.
|
|
92
|
+
Impact: one fix each, caught by autoformat and `tsc`.
|
|
93
|
+
4. `instruction-violation` (self-identified, hook-caught) — two lint nits the `testing` skill already documents: an unnecessary optional chain and an unnecessary `!` on `mock.calls[0][0]`.
|
|
94
|
+
Impact: two inline fixes; no rework.
|
|
95
|
+
|
|
96
|
+
None of the agent-side slips caused rework beyond their active TDD cycle — the autoformat/biome/`tsc`/vitest/pre-commit loops absorbed all of them.
|
|
97
|
+
|
|
98
|
+
#### What caused friction (user side)
|
|
99
|
+
|
|
100
|
+
1. The dominant cross-session finding (recorded separately as the User Note stage): follow-up issues identified during planning (#462, #463) were filed only at ship time.
|
|
101
|
+
The workflow itself encodes the deferral — the plan said "file at ship time" and `/ship-issue` step 4c creates them.
|
|
102
|
+
The operator wants them filed during planning, while the capable planning model still holds full context.
|
|
103
|
+
Opportunity: move follow-up creation into `/plan-issue`.
|
|
104
|
+
|
|
105
|
+
### Diagnostic details
|
|
106
|
+
|
|
107
|
+
- **Model-performance correlation** — the only subagent dispatch was the `pre-completion-reviewer` on `anthropic/claude-sonnet-4-6`, a judgment-heavy review task; appropriate match.
|
|
108
|
+
Planning ran on a capable model, which the operator explicitly values.
|
|
109
|
+
No mismatch.
|
|
110
|
+
- **Escalation-delay tracking** — no rabbit-holes; the longest same-area sequence was 4 assertion fixes inside one Green iteration (under the 5-call flag).
|
|
111
|
+
- **Unused-tool detection** — the `missing-context` slips were caught trivially by running tests; reading `display.ts` helpers first would have pre-empted them, but the cost was negligible and no Explore/`colgrep` dispatch was warranted.
|
|
112
|
+
- **Feedback-loop gap analysis** — verification ran incrementally and completely (per-cycle tests, `check` after type changes, full gate after the last step); no gap.
|
|
113
|
+
This is a win, not a gap.
|
|
114
|
+
|
|
115
|
+
### Changes made
|
|
116
|
+
|
|
117
|
+
Moved follow-up issue creation from ship time to planning time, with the backstop relocated to pre-completion (operator-directed):
|
|
118
|
+
|
|
119
|
+
1. `.pi/prompts/plan-issue.md` — added a `## File follow-up issues` section before `## Commit`: file plan-named follow-ups during planning with `gh issue create` and record each number in the plan; nothing speculative.
|
|
120
|
+
2. `.pi/agents/pre-completion-reviewer.md` — added checklist section `2i. Planned follow-up issues` and its report stanza: WARN when the plan names a follow-up but records no issue number; also added the case to the severity model's WARN list.
|
|
121
|
+
3. `.pi/prompts/ship-issue.md` — removed step `4c. Create planned follow-up issues` (follow-ups are now filed during planning; the close comment in step 5 references the recorded numbers).
|
|
122
|
+
4. `AGENTS.md` — extended the pre-completion-reviewer coverage enumeration to include planned follow-up filing.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 462
|
|
3
|
+
issue_title: "pi-subagents: upgrade /subagent-sessions renderer to Pi per-entry TUI components"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #462 — pi-subagents: upgrade /subagent-sessions renderer to Pi per-entry TUI components
|
|
7
|
+
|
|
8
|
+
## Stage: Planning (2026-06-22T00:00:00Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Planned Phase 19 Step 4a: swap the `/subagent-sessions` renderer from `serializeConversation` plain text to Pi's per-entry TUI components behind the existing `TranscriptSource` seam.
|
|
13
|
+
Verified the SDK surface (per-entry components, `parseSkillBlock`, `getMarkdownTheme`, `ToolDefinition`, `AgentSession.getToolDefinition`) and mirrored Pi's own `renderSessionContext`/`rebuildChatFromMessages` mapping.
|
|
14
|
+
Wrote a four-step TDD plan at `packages/pi-subagents/docs/plans/0462-navigation-renderer-tui-components.md` and committed it.
|
|
15
|
+
|
|
16
|
+
### Observations
|
|
17
|
+
|
|
18
|
+
- Three design decisions were surfaced via `ask_user` and locked by the operator:
|
|
19
|
+
1. **Rebuild-on-change** (mirror Pi's `rebuildChatFromMessages`) over incremental `updateContent`/`updateResult` — the seam exposes only a full `getMessages()` snapshot plus a coarse `onChange`, so incremental would still diff snapshots while replicating Pi's `pendingTools` streaming state machine for marginal benefit.
|
|
20
|
+
2. **Keep the lightweight `◍ describeActivity` text indicator** — this absorbs the high-frequency streaming text, so the persisted component tree only changes at message granularity, which is what keeps rebuild-on-change cheap.
|
|
21
|
+
3. **Wire real tool definitions** — the operator flagged a dependency-inversion risk ("can't `SubagentManager` track this?").
|
|
22
|
+
Resolved dependency-safely: the SDK `AgentSession` already exposes `getToolDefinition(name)`, so a new read accessor on `SubagentSession`/`Subagent` (mirroring the existing `agentMessages` accessor) surfaces it through the record and onto the `TranscriptSource` seam — arrows stay inward, no `SubagentManager` bookkeeping.
|
|
23
|
+
- Key structural move: the component renderer must leave the pure `session-navigation.ts` (per-entry components need `TUI`/`cwd`/`markdownTheme`) and live in the SDK/TUI `session-navigator.ts`.
|
|
24
|
+
The pure module sheds `renderTranscriptLines`/`serializeConversation` entirely, becoming selection + sourcing only.
|
|
25
|
+
- The `renderTranscriptLines` removal, overlay rewrite, `session-navigation.ts` edits, `index.ts` `cwd` wiring, and test updates must land in **one commit** (TDD step 3) — removing the export breaks the overlay and its tests at the type level.
|
|
26
|
+
- Non-breaking internal renderer swap; `Release: independent` per the roadmap.
|
|
27
|
+
Step 4a gates Step 5 ([#442]) for rendering parity but is not part of any release batch.
|
|
28
|
+
- Follow-up [#463] (Step 4b, file-snapshot source) already exists and is open — referenced as a Non-Goal, nothing new to file.
|
|
29
|
+
- Parity is defined as *using Pi's own components*, not byte-equality with the bespoke viewer; `custom`-role messages are skipped (the bespoke viewer never rendered them either), noted as an Open Question.
|
|
30
|
+
|
|
31
|
+
## Stage: Implementation — TDD (2026-06-22T21:30:00Z)
|
|
32
|
+
|
|
33
|
+
### Session summary
|
|
34
|
+
|
|
35
|
+
Executed all four planned TDD steps: the `getToolDefinition` read accessor on `SubagentSession`/`Subagent`, the seam method on `TranscriptSource`/`NavigableSubagent`, the per-entry component renderer + overlay rewrite + `cwd` wiring, and the architecture doc flip (Step 4a `✅`).
|
|
36
|
+
The renderer now mounts Pi's interactive components into a cached `Container` rebuilt on source change, mirroring Pi's `renderSessionContext`.
|
|
37
|
+
Test count went from 1084 to 1088 (+4 net: +4 accessor/seam tests, +3 new navigator tests, −3 removed `renderTranscriptLines` tests), all green; one post-review `test:` commit pins invariant #423.
|
|
38
|
+
|
|
39
|
+
### Observations
|
|
40
|
+
|
|
41
|
+
- The production code (`session-navigation.ts`, `session-navigator.ts`, `index.ts`) type-checked clean on the first `tsc` run — the SDK message-union narrowing (`switch (message.role)`), component construction, and `updateResult(toolResultMessage)` all resolved without casts.
|
|
42
|
+
`SessionMessage` (= the SDK `AgentMessage` union) narrows by `role` even though `@earendil-works/pi-agent-core` is not a direct dependency.
|
|
43
|
+
- Two deviations from the plan's Module-Level Changes, both intentional: (1) `src/types.ts` was *not* changed — `ToolDefinition` is imported directly from `@earendil-works/pi-coding-agent` in each consumer rather than re-exported through the barrel (avoids a speculative re-export `fallow` would flag); (2) `test/helpers/mock-session.ts` gained a `getToolDefinition` stub (needed for the step-1 accessor tests, not listed in the plan).
|
|
44
|
+
- Pi's per-entry components read a *global* interactive theme initialized by `initTheme()`; Pi does this at startup, but tests had to call `initTheme(undefined, false)` in `beforeAll`.
|
|
45
|
+
An early probe with `AssistantMessageComponent` passed without it, but `UserMessageComponent`/`ToolExecutionComponent` throw `"Theme not initialized"` — so the global-theme dependency is component-specific and only surfaced once the broader role set was exercised.
|
|
46
|
+
- `setComplete`'s `TruncationResult` param requires more fields than `BashExecutionMessage` carries, so the bash component is completed with `undefined` truncation info (command + output still render).
|
|
47
|
+
Pi's own JS passes `{ truncated: true }` but that path is untyped.
|
|
48
|
+
- Pre-completion reviewer verdict: **WARN** (no failures; 3 non-blocking findings).
|
|
49
|
+
Reviewer warnings: (1) `package-pi-subagents/SKILL.md` UI module count is stale (10 listed vs 13 actual) — predates this issue, plan left it deliberately; (2) `addMessageComponents` mutates the received `pendingTools` accumulator (output-argument pattern) — intentional, mirrors Pi's own `renderedPendingTools` local and was plan-reviewed; (3) invariant #423 held and is type-enforced but lacked an explicit spy pin — **addressed** by the follow-up `test:` commit asserting the handler never calls `record.getToolDefinition` directly.
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -201,7 +201,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
201
201
|
pi.registerCommand("subagent-sessions", {
|
|
202
202
|
description: "View a subagent's session transcript (read-only)",
|
|
203
203
|
handler: async (_args, ctx) => {
|
|
204
|
-
await sessionNavigator.handle({ ui: ctx.ui, agents: manager.listAgents(), registry });
|
|
204
|
+
await sessionNavigator.handle({ ui: ctx.ui, agents: manager.listAgents(), registry, cwd: ctx.cwd });
|
|
205
205
|
},
|
|
206
206
|
});
|
|
207
207
|
}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import {
|
|
14
14
|
type AgentSession,
|
|
15
15
|
type AgentSessionEvent,
|
|
16
|
+
type ToolDefinition,
|
|
16
17
|
} from "@earendil-works/pi-coding-agent";
|
|
17
18
|
import type { ChildLifecyclePublisher } from "#src/lifecycle/child-lifecycle";
|
|
18
19
|
import { normalizeMaxTurns } from "#src/lifecycle/turn-limits";
|
|
@@ -184,6 +185,11 @@ export class SubagentSession {
|
|
|
184
185
|
return this._session.messages;
|
|
185
186
|
}
|
|
186
187
|
|
|
188
|
+
/** Resolve a registered tool definition by name, for Pi's tool-execution components. */
|
|
189
|
+
getToolDefinition(name: string): ToolDefinition | undefined {
|
|
190
|
+
return this._session.getToolDefinition(name);
|
|
191
|
+
}
|
|
192
|
+
|
|
187
193
|
/** Tear down: session.dispose() + emit `disposed` (registry unregister). */
|
|
188
194
|
dispose(): void {
|
|
189
195
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- dispose may not exist on all session implementations
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { Model } from "@earendil-works/pi-ai";
|
|
10
|
-
import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import type { AgentSessionEvent, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import { debugLog } from "#src/debug";
|
|
12
12
|
import type { CreateSubagentSessionParams } from "#src/lifecycle/create-subagent-session";
|
|
13
13
|
import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
|
|
@@ -167,6 +167,11 @@ export class Subagent {
|
|
|
167
167
|
return this.subagentSession?.agentMessages ?? [];
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
/** Resolve a registered tool definition by name, or undefined if no session. */
|
|
171
|
+
getToolDefinition(name: string): ToolDefinition | undefined {
|
|
172
|
+
return this.subagentSession?.getToolDefinition(name);
|
|
173
|
+
}
|
|
174
|
+
|
|
170
175
|
constructor(init: SubagentInit) {
|
|
171
176
|
// Identity
|
|
172
177
|
this.id = init.id;
|
|
@@ -2,20 +2,21 @@
|
|
|
2
2
|
* session-navigation.ts — Pure selection and transcript-sourcing for native session navigation.
|
|
3
3
|
*
|
|
4
4
|
* Splits the unit-testable core of the `/subagent-sessions` command from its TUI
|
|
5
|
-
* wiring (`session-navigator.ts`): which subagents are navigable
|
|
6
|
-
* agent's transcript is sourced (live, in this slice)
|
|
7
|
-
* renders to plain text via Pi's own `serializeConversation`.
|
|
5
|
+
* wiring (`session-navigator.ts`): which subagents are navigable and how a picked
|
|
6
|
+
* agent's transcript is sourced (live, in this slice).
|
|
8
7
|
*
|
|
9
8
|
* The `TranscriptSource` seam decouples *how messages are sourced* (live record
|
|
10
|
-
* here; a file snapshot in a follow-up) from *how they render*
|
|
11
|
-
*
|
|
9
|
+
* here; a file snapshot in a follow-up) from *how they render* — the renderer
|
|
10
|
+
* (`session-navigator.ts`, which mounts Pi's per-entry components) talks only to
|
|
11
|
+
* this seam. Rendering lives in the SDK/TUI module because the per-entry
|
|
12
|
+
* components require a `TUI`, `cwd`, and markdown theme.
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
|
-
import {
|
|
15
|
+
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
15
16
|
import type { AgentConfigLookup } from "#src/config/agent-types";
|
|
16
17
|
import type { SubagentStatus } from "#src/lifecycle/subagent-state";
|
|
17
18
|
import type { AgentSessionEvent, SessionMessage, SubagentType } from "#src/types";
|
|
18
|
-
import {
|
|
19
|
+
import { formatDuration, getDisplayName } from "#src/ui/display";
|
|
19
20
|
|
|
20
21
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
21
22
|
|
|
@@ -33,6 +34,7 @@ export interface NavigableSubagent {
|
|
|
33
34
|
readonly agentMessages: readonly SessionMessage[];
|
|
34
35
|
isSessionReady(): boolean;
|
|
35
36
|
subscribeToUpdates(fn: (event: AgentSessionEvent) => void): (() => void) | undefined;
|
|
37
|
+
getToolDefinition(name: string): ToolDefinition | undefined;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
/** A navigable entry: a record plus the label shown in the picker. */
|
|
@@ -55,6 +57,8 @@ export interface TranscriptSource {
|
|
|
55
57
|
subscribe(onChange: () => void): (() => void) | undefined;
|
|
56
58
|
/** Running-agent streaming state, or undefined when not streaming. */
|
|
57
59
|
streaming(): StreamingState | undefined;
|
|
60
|
+
/** Resolve a registered tool definition by name, for Pi's tool-execution components. */
|
|
61
|
+
getToolDefinition(name: string): ToolDefinition | undefined;
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
/** Filter the agents to those with a viewable session and label each for the picker. */
|
|
@@ -76,37 +80,10 @@ export function liveSource(record: NavigableSubagent): TranscriptSource {
|
|
|
76
80
|
record.status === "running"
|
|
77
81
|
? { activeTools: record.activeTools, responseText: record.responseText }
|
|
78
82
|
: undefined,
|
|
83
|
+
getToolDefinition: (name) => record.getToolDefinition(name),
|
|
79
84
|
};
|
|
80
85
|
}
|
|
81
86
|
|
|
82
|
-
/** Render a source's transcript to plain text lines via Pi's `serializeConversation`. */
|
|
83
|
-
export function renderTranscriptLines(source: TranscriptSource): string[] {
|
|
84
|
-
const messages = source.getMessages();
|
|
85
|
-
const lines =
|
|
86
|
-
messages.length === 0 ? ["(no messages yet)"] : serializeConversation(toMessages(messages)).split("\n");
|
|
87
|
-
|
|
88
|
-
const streaming = source.streaming();
|
|
89
|
-
if (streaming) {
|
|
90
|
-
lines.push("", `◍ ${describeActivity(streaming.activeTools, streaming.responseText)}`);
|
|
91
|
-
}
|
|
92
|
-
return lines;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Bridge the session's `AgentMessage[]` to `serializeConversation`'s `Message[]`.
|
|
97
|
-
*
|
|
98
|
-
* `AgentMessage` is a superset of `Message` (it adds session-display variants such
|
|
99
|
-
* as `BashExecutionMessage`); `serializeConversation` renders the shared shape and
|
|
100
|
-
* best-effort text for the rest. `Message` is not re-exported from the public
|
|
101
|
-
* `@earendil-works/pi-ai` barrel, so the parameter type is referenced via the
|
|
102
|
-
* function signature rather than imported by name.
|
|
103
|
-
*/
|
|
104
|
-
function toMessages(
|
|
105
|
-
messages: readonly SessionMessage[],
|
|
106
|
-
): Parameters<typeof serializeConversation>[0] {
|
|
107
|
-
return messages as unknown as Parameters<typeof serializeConversation>[0];
|
|
108
|
-
}
|
|
109
|
-
|
|
110
87
|
function buildLabel(record: NavigableSubagent, registry: AgentConfigLookup): string {
|
|
111
88
|
const name = getDisplayName(record.type, registry);
|
|
112
89
|
const duration = formatDuration(record.startedAt, record.completedAt);
|
|
@@ -1,26 +1,46 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* session-navigator.ts — The `/subagent-sessions` command: pick a subagent and
|
|
3
|
-
* read its transcript through Pi's own
|
|
3
|
+
* read its transcript through Pi's own per-entry session components.
|
|
4
4
|
*
|
|
5
5
|
* SDK/TUI consumer half of native session navigation. The unit-testable core
|
|
6
|
-
* (selection, sourcing
|
|
7
|
-
*
|
|
6
|
+
* (selection, sourcing) lives in `session-navigation.ts`; this module wires that
|
|
7
|
+
* core to the command picker and a read-only scrollable overlay, and owns the
|
|
8
|
+
* renderer — it mounts Pi's interactive components (`AssistantMessageComponent`,
|
|
9
|
+
* `ToolExecutionComponent`, …) into a `Container`, mirroring Pi's own
|
|
10
|
+
* `renderSessionContext` mapping. Rendering lives here, not in the pure module,
|
|
11
|
+
* because the components require a `TUI`, `cwd`, and markdown theme.
|
|
8
12
|
*
|
|
9
13
|
* The overlay is strictly read-only — steering stays in the `steer_subagent` tool
|
|
10
|
-
* and the widget. It consumes a `TranscriptSource`, so the
|
|
11
|
-
*
|
|
14
|
+
* and the widget. It consumes a `TranscriptSource`, so the evicted-agent-source
|
|
15
|
+
* follow-up swaps the source without touching the renderer or the overlay.
|
|
12
16
|
*/
|
|
13
17
|
|
|
14
|
-
import { type Component, matchesKey, type TUI, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
15
|
-
import type { AgentConfigLookup } from "#src/config/agent-types";
|
|
16
|
-
import type { Theme } from "#src/ui/display";
|
|
17
18
|
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
AssistantMessageComponent,
|
|
20
|
+
BashExecutionComponent,
|
|
21
|
+
BranchSummaryMessageComponent,
|
|
22
|
+
CompactionSummaryMessageComponent,
|
|
23
|
+
getMarkdownTheme,
|
|
24
|
+
parseSkillBlock,
|
|
25
|
+
SkillInvocationMessageComponent,
|
|
26
|
+
type ToolDefinition,
|
|
27
|
+
ToolExecutionComponent,
|
|
28
|
+
UserMessageComponent,
|
|
29
|
+
} from "@earendil-works/pi-coding-agent";
|
|
30
|
+
import {
|
|
31
|
+
type Component,
|
|
32
|
+
Container,
|
|
33
|
+
type MarkdownTheme,
|
|
34
|
+
matchesKey,
|
|
35
|
+
Spacer,
|
|
36
|
+
type TUI,
|
|
37
|
+
truncateToWidth,
|
|
38
|
+
visibleWidth,
|
|
39
|
+
} from "@earendil-works/pi-tui";
|
|
40
|
+
import type { AgentConfigLookup } from "#src/config/agent-types";
|
|
41
|
+
import type { SessionMessage } from "#src/types";
|
|
42
|
+
import { describeActivity, type Theme } from "#src/ui/display";
|
|
43
|
+
import { listNavigableAgents, liveSource, type NavigableSubagent, type TranscriptSource } from "#src/ui/session-navigation";
|
|
24
44
|
|
|
25
45
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
26
46
|
|
|
@@ -49,6 +69,8 @@ export interface SessionNavigatorParams {
|
|
|
49
69
|
ui: SessionNavigatorUI;
|
|
50
70
|
agents: readonly NavigableSubagent[];
|
|
51
71
|
registry: AgentConfigLookup;
|
|
72
|
+
/** Working directory for tool-call rendering (relative path display). */
|
|
73
|
+
cwd: string;
|
|
52
74
|
}
|
|
53
75
|
|
|
54
76
|
/** Options for the read-only transcript overlay. */
|
|
@@ -57,7 +79,8 @@ export interface TranscriptOverlayOptions {
|
|
|
57
79
|
theme: Theme;
|
|
58
80
|
source: TranscriptSource;
|
|
59
81
|
done: (result: undefined) => void;
|
|
60
|
-
|
|
82
|
+
cwd: string;
|
|
83
|
+
markdownTheme: MarkdownTheme;
|
|
61
84
|
}
|
|
62
85
|
|
|
63
86
|
/**
|
|
@@ -68,7 +91,7 @@ export interface TranscriptOverlayOptions {
|
|
|
68
91
|
* manager, so it stays a reactive consumer with no inbound call into the core.
|
|
69
92
|
*/
|
|
70
93
|
export class SessionNavigatorHandler {
|
|
71
|
-
async handle({ ui, agents, registry }: SessionNavigatorParams): Promise<void> {
|
|
94
|
+
async handle({ ui, agents, registry, cwd }: SessionNavigatorParams): Promise<void> {
|
|
72
95
|
const entries = listNavigableAgents(agents, registry);
|
|
73
96
|
if (entries.length === 0) {
|
|
74
97
|
ui.notify("No subagent sessions to view.", "info");
|
|
@@ -83,9 +106,10 @@ export class SessionNavigatorHandler {
|
|
|
83
106
|
if (!entry) return;
|
|
84
107
|
|
|
85
108
|
const source = liveSource(entry.record);
|
|
109
|
+
const markdownTheme = getMarkdownTheme();
|
|
86
110
|
await ui.custom<undefined>(
|
|
87
111
|
(tui, theme, _keybindings, done) =>
|
|
88
|
-
new TranscriptOverlay({ tui, theme, source, done,
|
|
112
|
+
new TranscriptOverlay({ tui, theme, source, done, cwd, markdownTheme }),
|
|
89
113
|
{
|
|
90
114
|
overlay: true,
|
|
91
115
|
overlayOptions: { anchor: "center", width: "90%", maxHeight: `${VIEWPORT_HEIGHT_PCT}%` },
|
|
@@ -97,9 +121,11 @@ export class SessionNavigatorHandler {
|
|
|
97
121
|
/**
|
|
98
122
|
* Read-only scrollable transcript overlay.
|
|
99
123
|
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
124
|
+
* Caches a `Container` of Pi's per-entry components and rebuilds it only when the
|
|
125
|
+
* source changes (live agents) — each paint reuses the cached tree, so markdown
|
|
126
|
+
* highlighting does not re-run per frame. This class owns scroll state, chrome,
|
|
127
|
+
* and the running-agent streaming indicator; the component mapping lives in
|
|
128
|
+
* `buildTranscriptComponents`.
|
|
103
129
|
*/
|
|
104
130
|
export class TranscriptOverlay implements Component {
|
|
105
131
|
private scrollOffset = 0;
|
|
@@ -111,16 +137,21 @@ export class TranscriptOverlay implements Component {
|
|
|
111
137
|
private readonly theme: Theme;
|
|
112
138
|
private readonly source: TranscriptSource;
|
|
113
139
|
private readonly done: (result: undefined) => void;
|
|
114
|
-
private readonly
|
|
140
|
+
private readonly cwd: string;
|
|
141
|
+
private readonly markdownTheme: MarkdownTheme;
|
|
142
|
+
private content: Container;
|
|
115
143
|
|
|
116
|
-
constructor({ tui, theme, source, done,
|
|
144
|
+
constructor({ tui, theme, source, done, cwd, markdownTheme }: TranscriptOverlayOptions) {
|
|
117
145
|
this.tui = tui;
|
|
118
146
|
this.theme = theme;
|
|
119
147
|
this.source = source;
|
|
120
148
|
this.done = done;
|
|
121
|
-
this.
|
|
149
|
+
this.cwd = cwd;
|
|
150
|
+
this.markdownTheme = markdownTheme;
|
|
151
|
+
this.content = this.rebuild();
|
|
122
152
|
this.unsubscribe = source.subscribe(() => {
|
|
123
153
|
if (this.closed) return;
|
|
154
|
+
this.content = this.rebuild();
|
|
124
155
|
this.tui.requestRender();
|
|
125
156
|
});
|
|
126
157
|
}
|
|
@@ -199,7 +230,7 @@ export class TranscriptOverlay implements Component {
|
|
|
199
230
|
|
|
200
231
|
// fallow-ignore-next-line unused-class-member
|
|
201
232
|
invalidate(): void {
|
|
202
|
-
|
|
233
|
+
this.content.invalidate();
|
|
203
234
|
}
|
|
204
235
|
|
|
205
236
|
// fallow-ignore-next-line unused-class-member
|
|
@@ -224,14 +255,139 @@ export class TranscriptOverlay implements Component {
|
|
|
224
255
|
|
|
225
256
|
private buildContentLines(innerW: number): string[] {
|
|
226
257
|
if (innerW <= 0) return [];
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
258
|
+
const lines = this.content.render(innerW);
|
|
259
|
+
const streaming = this.source.streaming();
|
|
260
|
+
if (streaming) {
|
|
261
|
+
lines.push("", `◍ ${describeActivity(streaming.activeTools, streaming.responseText)}`);
|
|
262
|
+
}
|
|
263
|
+
return lines.map((l) => truncateToWidth(l, innerW));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private rebuild(): Container {
|
|
267
|
+
return buildTranscriptComponents(this.source.getMessages(), {
|
|
268
|
+
tui: this.tui,
|
|
269
|
+
cwd: this.cwd,
|
|
270
|
+
markdownTheme: this.markdownTheme,
|
|
271
|
+
getToolDefinition: (name) => this.source.getToolDefinition(name),
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Dependencies the per-entry component tree needs from the SDK/TUI environment. */
|
|
277
|
+
interface TranscriptRenderOptions {
|
|
278
|
+
tui: TUI;
|
|
279
|
+
cwd: string;
|
|
280
|
+
markdownTheme: MarkdownTheme;
|
|
281
|
+
getToolDefinition: (name: string) => ToolDefinition | undefined;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Build a `Container` of Pi's per-entry components from a message snapshot,
|
|
286
|
+
* mirroring Pi's own interactive-mode `renderSessionContext` mapping. Tool
|
|
287
|
+
* results are matched to their tool-call components by id, exactly as Pi does.
|
|
288
|
+
* `custom`-role messages are skipped — rendering them needs the child session's
|
|
289
|
+
* message-renderer registry, which the navigator does not hold.
|
|
290
|
+
*/
|
|
291
|
+
function buildTranscriptComponents(
|
|
292
|
+
messages: readonly SessionMessage[],
|
|
293
|
+
opts: TranscriptRenderOptions,
|
|
294
|
+
): Container {
|
|
295
|
+
const container = new Container();
|
|
296
|
+
const pendingTools = new Map<string, ToolExecutionComponent>();
|
|
297
|
+
for (const message of messages) {
|
|
298
|
+
addMessageComponents(container, message, pendingTools, opts);
|
|
299
|
+
}
|
|
300
|
+
return container;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function addMessageComponents(
|
|
304
|
+
container: Container,
|
|
305
|
+
message: SessionMessage,
|
|
306
|
+
pendingTools: Map<string, ToolExecutionComponent>,
|
|
307
|
+
opts: TranscriptRenderOptions,
|
|
308
|
+
): void {
|
|
309
|
+
switch (message.role) {
|
|
310
|
+
case "assistant": {
|
|
311
|
+
container.addChild(new AssistantMessageComponent(message, false, opts.markdownTheme));
|
|
312
|
+
for (const content of message.content) {
|
|
313
|
+
if (content.type !== "toolCall") continue;
|
|
314
|
+
const tool = new ToolExecutionComponent(
|
|
315
|
+
content.name,
|
|
316
|
+
content.id,
|
|
317
|
+
content.arguments,
|
|
318
|
+
{ showImages: false },
|
|
319
|
+
opts.getToolDefinition(content.name),
|
|
320
|
+
opts.tui,
|
|
321
|
+
opts.cwd,
|
|
322
|
+
);
|
|
323
|
+
tool.setExpanded(true);
|
|
324
|
+
container.addChild(tool);
|
|
325
|
+
pendingTools.set(content.id, tool);
|
|
232
326
|
}
|
|
233
|
-
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
case "toolResult": {
|
|
330
|
+
pendingTools.get(message.toolCallId)?.updateResult(message);
|
|
331
|
+
pendingTools.delete(message.toolCallId);
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
case "user": {
|
|
335
|
+
addUserComponents(container, message.content, opts.markdownTheme);
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
case "bashExecution": {
|
|
339
|
+
const bash = new BashExecutionComponent(message.command, opts.tui, message.excludeFromContext);
|
|
340
|
+
if (message.output) bash.appendOutput(message.output);
|
|
341
|
+
bash.setComplete(message.exitCode, message.cancelled, undefined, message.fullOutputPath);
|
|
342
|
+
container.addChild(bash);
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
case "compactionSummary": {
|
|
346
|
+
container.addChild(new Spacer(1));
|
|
347
|
+
const summary = new CompactionSummaryMessageComponent(message, opts.markdownTheme);
|
|
348
|
+
summary.setExpanded(true);
|
|
349
|
+
container.addChild(summary);
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
case "branchSummary": {
|
|
353
|
+
container.addChild(new Spacer(1));
|
|
354
|
+
const summary = new BranchSummaryMessageComponent(message, opts.markdownTheme);
|
|
355
|
+
summary.setExpanded(true);
|
|
356
|
+
container.addChild(summary);
|
|
357
|
+
break;
|
|
234
358
|
}
|
|
235
|
-
return wrapped.map((l) => truncateToWidth(l, innerW));
|
|
236
359
|
}
|
|
237
360
|
}
|
|
361
|
+
|
|
362
|
+
/** Render a user message (skill block + text) into the container, mirroring Pi. */
|
|
363
|
+
function addUserComponents(
|
|
364
|
+
container: Container,
|
|
365
|
+
content: string | readonly { type: string; text?: string }[],
|
|
366
|
+
markdownTheme: MarkdownTheme,
|
|
367
|
+
): void {
|
|
368
|
+
const text = userMessageText(content);
|
|
369
|
+
if (!text) return;
|
|
370
|
+
if (container.children.length > 0) container.addChild(new Spacer(1));
|
|
371
|
+
|
|
372
|
+
const skillBlock = parseSkillBlock(text);
|
|
373
|
+
if (!skillBlock) {
|
|
374
|
+
container.addChild(new UserMessageComponent(text, markdownTheme));
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const skill = new SkillInvocationMessageComponent(skillBlock, markdownTheme);
|
|
378
|
+
skill.setExpanded(true);
|
|
379
|
+
container.addChild(skill);
|
|
380
|
+
if (skillBlock.userMessage) {
|
|
381
|
+
container.addChild(new Spacer(1));
|
|
382
|
+
container.addChild(new UserMessageComponent(skillBlock.userMessage, markdownTheme));
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** Concatenate the text blocks of a user message's content (mirrors Pi). */
|
|
387
|
+
function userMessageText(content: string | readonly { type: string; text?: string }[]): string {
|
|
388
|
+
if (typeof content === "string") return content;
|
|
389
|
+
return content
|
|
390
|
+
.filter((block) => block.type === "text")
|
|
391
|
+
.map((block) => block.text ?? "")
|
|
392
|
+
.join("");
|
|
393
|
+
}
|