@gotgenes/pi-subagents 17.0.0 → 17.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/docs/architecture/architecture.md +222 -159
- package/docs/architecture/history/phase-18-reconsider-ui.md +166 -0
- package/docs/decisions/0004-reconsider-ui-direction.md +220 -0
- package/docs/plans/0426-consolidate-residual-test-clone-families.md +235 -0
- package/docs/plans/0427-reconsider-ui-direction.md +212 -0
- package/docs/plans/0446-spike-session-navigation-entry-criteria.md +171 -0
- package/docs/plans/0448-block-disabled-agent-spawn.md +207 -0
- package/docs/retro/0425-reconcile-subagent-events-contract.md +55 -0
- package/docs/retro/0426-consolidate-residual-test-clone-families.md +100 -0
- package/docs/retro/0427-reconsider-ui-direction.md +102 -0
- package/docs/retro/0446-spike-session-navigation-entry-criteria.md +99 -0
- package/docs/retro/0448-block-disabled-agent-spawn.md +42 -0
- package/package.json +1 -1
- package/src/tools/helpers.ts +3 -2
- package/src/tools/spawn-config.ts +6 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# Phase 18: Reconsider UI from first principles
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
|
|
5
|
+
Phase 18 disentangled the activity tier from the core and recorded a first-principles decision about the UI's direction and distribution.
|
|
6
|
+
Steps 1–5 (the spine) removed the activity-tier entanglement: run state was consolidated onto `SubagentState`, `AgentActivityTracker` and `ui-observer` were deleted, and the widget was made a pure reactive consumer of lifecycle events.
|
|
7
|
+
Steps 6–7 delivered independent hygiene: the public event contract was reconciled (breaking) and residual test clone families were consolidated.
|
|
8
|
+
Step 8 captured the outcome as [ADR-0004] — a per-component UI decision recorded to gateway Phase 19.
|
|
9
|
+
|
|
10
|
+
All eight steps are closed: [#420], [#421], [#422], [#423], [#424], [#425], [#426], [#427].
|
|
11
|
+
|
|
12
|
+
## Health metrics
|
|
13
|
+
|
|
14
|
+
Baseline at the start of Phase 18 (end of Phase 17):
|
|
15
|
+
|
|
16
|
+
| Metric | Phase 18 start | Phase 18 end |
|
|
17
|
+
| ---------------------------------------- | ------------------------------------------------------------------ | ------------------------------------- |
|
|
18
|
+
| Health score | 78/100 (B) | 78/100 (B) |
|
|
19
|
+
| Source LOC | 7,751 (62 files) | 7,650 (61 files) |
|
|
20
|
+
| Dead code | 0 files, 0 exports | 0 files, 0 exports |
|
|
21
|
+
| Maintainability index | 90.9 (good) | 90.9 (good) |
|
|
22
|
+
| Avg cyclomatic complexity | 1.4 | 1.4 |
|
|
23
|
+
| P90 cyclomatic complexity | 2 | 2 |
|
|
24
|
+
| Fallow refactoring targets | 0 | 0 |
|
|
25
|
+
| Production duplication | 11 lines (1 internal clone group in `agent-config-editor.ts`) | 11 lines (1 internal clone group) |
|
|
26
|
+
| Test duplication | 28 clone groups, 503 lines | 14 clone groups (below <15 target) |
|
|
27
|
+
| Tests | 1,031 passing | 1,047 passing |
|
|
28
|
+
| Activity-tier modules slated for removal | `ui-observer.ts` (61) + `agent-activity-tracker.ts` (84) = 145 LOC | Deleted in Step 3 |
|
|
29
|
+
|
|
30
|
+
## Findings
|
|
31
|
+
|
|
32
|
+
Six findings told one story — the activity tier had metastasized out of the UI and into the core.
|
|
33
|
+
|
|
34
|
+
1. **UI state lives on the core runtime.**
|
|
35
|
+
`SubagentRuntime.agentActivity: Map<string, AgentActivityTracker>` puts a UI streaming-state map on the core composition root.
|
|
36
|
+
`SubagentRuntime` owns session-scoped lifecycle state; `AgentActivityTracker` (active tools, response text, turn count) is pure rendering state the core never reads.
|
|
37
|
+
Category C (coupling).
|
|
38
|
+
2. **Core spawn tools wire UI streaming.**
|
|
39
|
+
`foreground-runner.ts` and `background-spawner.ts` construct `AgentActivityTracker`, call `subscribeUIObserver` (a second session subscription parallel to the core's own `record-observer`), call `setSession`, and populate or delete the activity map and drive the widget (`ensureTimer`, `update`, `markFinished`).
|
|
40
|
+
Their own doc comments list "AgentActivityTracker creation, UI observer subscription" as responsibilities.
|
|
41
|
+
Category C (mixed responsibility, parameter relay).
|
|
42
|
+
3. **The LLM-facing tool depends on the widget.**
|
|
43
|
+
`AgentTool` takes a 4-method `widget` dependency plus `runtime.agentActivity` and calls `this.widget.setUICtx(ctx.ui)`.
|
|
44
|
+
Every `AgentTool` unit test must stub the widget and the activity map (`createToolDeps`), although the tool's real concern is dispatch — testability friction marking a domain seam.
|
|
45
|
+
Category C/D.
|
|
46
|
+
4. **Two parallel observers split the run-metric domain.**
|
|
47
|
+
Each child session gets two subscriptions: `record-observer` accumulates `SubagentState` (tool uses, usage, compactions) and `ui-observer` accumulates `AgentActivityTracker` (active tools, response text, turn count).
|
|
48
|
+
`turnCount` is a genuine run metric that lives only in the UI tracker, so `notification.ts` and the foreground result text reach into the tracker to recover it.
|
|
49
|
+
Category C (anemic split, duplication).
|
|
50
|
+
5. **A core observation concern reaches into UI state.**
|
|
51
|
+
`NotificationManager` holds the activity map and reads `turnCount`/`maxTurns` from it to build notification details, and deletes entries from it.
|
|
52
|
+
Category C (Law of Demeter).
|
|
53
|
+
6. **The public event contract is both vacant and incomplete.**
|
|
54
|
+
`SUBAGENT_EVENTS.ACTIVITY = "subagents:activity"` is declared in the service surface and the lifecycle-events table but is never emitted anywhere — a vacant hook the "no vacant hooks" rule forbids.
|
|
55
|
+
Meanwhile three emitted channels (`subagents:failed`, `subagents:compacted`, `subagents:created`) are absent from the constant map.
|
|
56
|
+
Category A/E.
|
|
57
|
+
|
|
58
|
+
## Steps
|
|
59
|
+
|
|
60
|
+
The spine (Steps 1–5) removes the activity-tier entanglement, leaving the core a pure orchestrator whose run state lives in one place and whose UI is a reactive consumer of broadcast events plus discrete queries.
|
|
61
|
+
Steps 6–7 are independent hygiene.
|
|
62
|
+
Step 8 is the user-driven UI reconsideration the disentangled core finally makes possible — a decision about the UI's distribution once it is substitutable, not optional.
|
|
63
|
+
|
|
64
|
+
The deeper target in the [first-principles refinement] — metrics as a pure observer projection rather than mutable fields — is deliberately **not** forced here.
|
|
65
|
+
Folding the live activity onto the record (the single owner of run state, consistent with Phase 17's `SubagentState`) removes the duplication without inventing the asynchronous-observation seam the `improvement-discovery` skill warns is essential, not structural.
|
|
66
|
+
|
|
67
|
+
1. **✅ Fold run metrics and live activity onto the core record (pure addition) — complete (v16.5.0).**
|
|
68
|
+
([#420]) Target: `lifecycle/subagent-state.ts`, `observation/record-observer.ts`, `lifecycle/subagent.ts`.
|
|
69
|
+
Extend the single owned run-state value object with `turnCount`, active tools, and response text; have the already-subscribed `record-observer` handle `turn_end`, `tool_execution_start`, `message_start`, `message_update`; expose read-only `turnCount`/`maxTurns`/`activeTools`/`responseText` getters on `Subagent`.
|
|
70
|
+
`AgentActivityTracker` still exists; nothing reads the new getters yet (tidy-first).
|
|
71
|
+
Smell: Category C. Outcome: `Subagent` is the single home for all run state; getters available for migration.
|
|
72
|
+
Landed: `SubagentState` owns `turnCount`/`activeTools`/`responseText` plus their transition methods; `record-observer` populates them on a single subscription; `Subagent` exposes the four read-only getters; +27 tests (1031 → 1058).
|
|
73
|
+
2. **✅ Migrate every activity reader to the record getters — complete.**
|
|
74
|
+
([#421]) Target: `ui/widget-renderer.ts`, `ui/conversation-viewer.ts`, `ui/agent-menu.ts`, `tools/foreground-runner.ts`, `observation/notification.ts`.
|
|
75
|
+
Switch each reader from `AgentActivityTracker` to the record getters added in Step 1 (widget-renderer reads activity off `listAgents()`; viewer/menu drop the `activity` param; notification reads `turnCount`/`maxTurns` off the record).
|
|
76
|
+
Smell: Category C (Law of Demeter).
|
|
77
|
+
Outcome: no consumer references `AgentActivityTracker`.
|
|
78
|
+
Landed: `WidgetAgent` folds the live-activity fields and a `contextPercent` projection; `AgentWidget` projects records; `ConversationViewer`, `AgentsMenuHandler`, `buildDetails`, and `buildNotificationDetails` read off the record; `NotificationSystem.cleanupCompleted` removed; `SubagentEventsObserver` returns early on consumed results; +8 tests (1058 → 1066).
|
|
79
|
+
3. **✅ Delete `AgentActivityTracker` and `ui-observer`; drop the activity map from the runtime and spawn tools — complete.**
|
|
80
|
+
([#422]) Target: `ui/agent-activity-tracker.ts` (delete), `ui/ui-observer.ts` (delete), `runtime.ts`, `tools/foreground-runner.ts`, `tools/background-spawner.ts`.
|
|
81
|
+
The spawn tools stop constructing trackers, subscribing, and populating maps; `SubagentRuntime.agentActivity` is removed.
|
|
82
|
+
Smell: Category A + C. Outcome: −145 LOC, one session subscription per child, runtime holds zero UI state.
|
|
83
|
+
Landed: deleted `agent-activity-tracker.ts` (84) + `ui-observer.ts` (61) and their unit suites; dropped the `agentActivity` parameter from `runForeground`/`spawnBackground`, the `AgentActivityAccess` interface, and the `AgentToolRuntime`/`SubagentRuntime` activity-map fields; the foreground `observer.onSessionCreated` keeps `recordRef`/`fgId` binding and `widget.ensureTimer`; −34 tests (1066 → 1032).
|
|
84
|
+
4. **✅ Make the widget self-drive from lifecycle events — complete.**
|
|
85
|
+
([#423]) Target: `ui/agent-widget.ts`, `observation/composite-subagent-observer.ts` (new), `index.ts`.
|
|
86
|
+
The widget starts/stops its timer in response to started/created/completed notifications instead of tool calls; spawn tools no longer call `ensureTimer`/`update`/`markFinished`.
|
|
87
|
+
Smell: Category C (coupling direction).
|
|
88
|
+
Outcome: the widget is a reactive consumer; no inbound calls from core spawn tools.
|
|
89
|
+
Landed: `AgentWidget implements SubagentManagerObserver` (starts the 80 ms loop on `onSubagentStarted`/`onSubagentCreated`, re-renders on `onSubagentCompleted`/`onSubagentCompacted`); a new `CompositeSubagentObserver` fans the manager's single observer slot out to `[eventsObserver, widget]` (wired in `index.ts`, keeping `SubagentManager` unchanged); dropped the `widget` parameter and `ForegroundWidgetDeps`/`BackgroundWidgetDeps` interfaces from both spawners, deleted `AgentWidget.markFinished` (redundant with `seedFinishedAgents`), made `ensureTimer` private, and narrowed `AgentToolWidget` to `setUICtx`; +11 tests (1032 → 1043, then −4 with the dropped spawner widget-driving tests → 1039).
|
|
90
|
+
5. **✅ Drop the widget dependency from the `subagent` tool — complete.**
|
|
91
|
+
([#424]) Target: `tools/agent-tool.ts`, `test/helpers/make-deps.ts`.
|
|
92
|
+
`AgentTool` loses its `widget` constructor param (UICtx capture stays in `ToolStartHandler`); `createToolDeps` sheds the widget stub.
|
|
93
|
+
The activity-map (`agentActivity`) dependency this step originally named was already removed from the tool and runtime in Step 3 ([#422]), so only `widget` remained to drop.
|
|
94
|
+
Smell: Category C/D.
|
|
95
|
+
Outcome: the LLM tool depends only on manager/runtime/settings/registry; fixture drops 1 field.
|
|
96
|
+
Landed: removed the `widget` constructor param and the redundant `this.widget.setUICtx(ctx.ui)` call from `AgentTool.execute`, deleted the `AgentToolWidget` interface and its `UICtx` import, updated the sole `index.ts` call site, and dropped the `widget` field/stub plus two now-obsolete tests (`agent-tool` UICtx capture, `make-deps` widget defaults) — UICtx capture is pinned by `handlers/tool-start.test.ts`; −2 tests (1039 → 1037).
|
|
97
|
+
6. **✅ Reconcile the public event contract — complete.**
|
|
98
|
+
([#425]) Target: `service/service.ts`, the lifecycle-events table in [architecture.md](../architecture.md).
|
|
99
|
+
Remove the vacant `ACTIVITY` channel (or emit a real broadcast for it) and add the emitted `failed`/`compacted`/`created` channels so declared constants match emitted events.
|
|
100
|
+
Smell: Category A/E.
|
|
101
|
+
Outcome: declared channels equal emitted channels; no vacant hook.
|
|
102
|
+
Landed: removed `SUBAGENT_EVENTS.ACTIVITY` (breaking) and added `FAILED`/`COMPACTED`/`CREATED`/`STEERED` — `subagents:steered` (from `steer-tool.ts`) was also emitted-but-undeclared, so all four emitted agent-lifecycle channels are now declared; corrected the stale `subagents:completed` payload in the lifecycle-events table to the real `buildEventData` shape; +1 test (1037 → 1038).
|
|
103
|
+
7. **✅ Consolidate residual test clone families — complete.**
|
|
104
|
+
([#426]) Target: `test/settings.test.ts` + `test/layered-settings.test.ts`, `test/lifecycle/create-subagent-session.test.ts`, `test/ui/agent-config-editor.test.ts`.
|
|
105
|
+
Extract shared fixtures for the clone families fallow reports that the spine does not already rewrite.
|
|
106
|
+
Smell: Category D. Outcome: test clone groups drop below 15.
|
|
107
|
+
Landed: extracted `test/helpers/tmp-settings-dirs.ts` (global+project tmp-dir fixture) and `test/helpers/capture-warn.ts` (a `console.warn` capture helper), each with a paired self-test; table-drove the `create-subagent-session` post-bind membership cases and the `agent-config-editor` menu + confirm-remove cases into `it.each`; pi-subagents test clone groups dropped from 24 to 14 (below the <15 target); +9 tests (1038 → 1047).
|
|
108
|
+
8. **✅ Reconsider the UI direction (first-principles ADR) — complete.**
|
|
109
|
+
([#427]) Target: `docs/decisions/`, `ui/`.
|
|
110
|
+
The spine already made the UI _substitutable_; this step decides its _distribution_, not whether the experiment is possible.
|
|
111
|
+
The goal is **substitutable, not optional**: a human needs some surface, but the specific UI is replaceable — the way Pi ships a default TUI built on the same public API any extension targets.
|
|
112
|
+
The disentangled core stays byte-for-byte identical whether or not a given UI consumer is installed (the composition test), so a replacement UI is a downstream concern even though _some_ UI is not.
|
|
113
|
+
Unlike the worktrees provider seam (generative, rationed — one provider the core consults), the UI is an observational consumer (unlimited, the core never waits on it) reading the broadcast-plus-query surface, which is why packaging it is the secondary question and decoupling it was the real win.
|
|
114
|
+
Two standing concerns are the evidence this decision weighs and the better boundaries the reconsideration is meant to surface:
|
|
115
|
+
- **Truncated transcript.**
|
|
116
|
+
The conversation viewer shows a truncated view, yet `Subagent.messages` already exposes the full history and the core already persists each child as a standard Pi session JSONL (`outputFile`) — the limit is the bespoke overlay's rendering, not data access.
|
|
117
|
+
- **Foreground widget redundancy.**
|
|
118
|
+
In foreground the tool's inline `onUpdate` stream already shows progress, so the above-editor widget duplicates it; the widget earns its keep only for background agents — a per-mode judgment the fused UI cannot vary.
|
|
119
|
+
Candidate redesign to record: replace the bespoke overlay with "open the child session in the same viewer Pi uses for any session," following the recursive-Pi insight and the already-persisted session file.
|
|
120
|
+
Judge the widget, conversation viewer, and `/agents` menu per component — keep, shrink, extract to `@gotgenes/pi-subagents-ui`, or remove — and capture the decision in an ADR that gateways Phase 19.
|
|
121
|
+
Smell: Category E (organization / boundary).
|
|
122
|
+
Outcome: a recorded per-component decision motivated by the two concerns; the inherited UI is substitutable and no longer preserved by default.
|
|
123
|
+
Landed: [ADR-0004] records the per-component decision — (A) shrink the widget to background agents only; (B) remove the bespoke `ConversationViewer`, replacing it with native session navigation over the persisted child JSONL (`switchSession`/`loadEntriesFromFile`, mechanism gated on a Phase 19 spike); (C) dissolve the `/agents` command (remove the create wizard and the agent-types config editor, re-home running-agent visibility onto the widget + session navigation, extract settings to a focused `/subagents:settings` command); (D) keep the surviving UI in-core (substitutable, not extracted).
|
|
124
|
+
The ADR gateways Phase 19, which implements these decisions under its own plan; no runtime code changed in this step.
|
|
125
|
+
|
|
126
|
+
## Step dependency diagram
|
|
127
|
+
|
|
128
|
+
```mermaid
|
|
129
|
+
flowchart TB
|
|
130
|
+
S1["1 — Fold metrics + activity onto record (#420) ✅"]
|
|
131
|
+
S2["2 — Migrate readers to record getters (#421) ✅"]
|
|
132
|
+
S3["3 — Delete tracker + ui-observer, drop activity map (#422) ✅"]
|
|
133
|
+
S4["4 — Widget self-drives on events (#423) ✅"]
|
|
134
|
+
S5["5 — Drop widget dep from subagent tool (#424) ✅"]
|
|
135
|
+
S6["6 — Reconcile public event contract (#425) ✅"]
|
|
136
|
+
S7["7 — Consolidate test clone families (#426) ✅"]
|
|
137
|
+
S8["8 — Reconsider UI direction, ADR (#427) ✅"]
|
|
138
|
+
|
|
139
|
+
S1 --> S2 --> S3 --> S4 --> S5 --> S8
|
|
140
|
+
S6
|
|
141
|
+
S7
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Parallel tracks
|
|
145
|
+
|
|
146
|
+
- **Track A — Disentangle the activity tier (spine):** Steps 1, 2, 3.
|
|
147
|
+
Strictly ordered; each is a safe lift-and-shift over the previous.
|
|
148
|
+
- **Track B — Decouple widget and tool wiring:** Steps 4, 5.
|
|
149
|
+
Begins once the activity map is gone (Step 3).
|
|
150
|
+
- **Track C — Public-contract hygiene:** Step 6.
|
|
151
|
+
Independent; can land any time.
|
|
152
|
+
- **Track D — Test consolidation:** Step 7.
|
|
153
|
+
Independent; can land any time.
|
|
154
|
+
- **Track E — UI reconsideration:** Step 8.
|
|
155
|
+
Gated on the spine and Track B (the UI must be a clean consumer first).
|
|
156
|
+
|
|
157
|
+
[ADR-0004]: ../../decisions/0004-reconsider-ui-direction.md
|
|
158
|
+
[first-principles refinement]: ../architecture.md#first-principles-refinement-and-the-deeper-target
|
|
159
|
+
[#420]: https://github.com/gotgenes/pi-packages/issues/420
|
|
160
|
+
[#421]: https://github.com/gotgenes/pi-packages/issues/421
|
|
161
|
+
[#422]: https://github.com/gotgenes/pi-packages/issues/422
|
|
162
|
+
[#423]: https://github.com/gotgenes/pi-packages/issues/423
|
|
163
|
+
[#424]: https://github.com/gotgenes/pi-packages/issues/424
|
|
164
|
+
[#425]: https://github.com/gotgenes/pi-packages/issues/425
|
|
165
|
+
[#426]: https://github.com/gotgenes/pi-packages/issues/426
|
|
166
|
+
[#427]: https://github.com/gotgenes/pi-packages/issues/427
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
---
|
|
2
|
+
status: accepted
|
|
3
|
+
date: 2026-06-18
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 0004 — Reconsider the UI direction from first principles
|
|
7
|
+
|
|
8
|
+
## Status
|
|
9
|
+
|
|
10
|
+
Accepted.
|
|
11
|
+
Completes Phase 18 (reconsider the UI) and gateways Phase 19 (implement the recorded decisions).
|
|
12
|
+
Decision-only: this ADR changes no runtime code.
|
|
13
|
+
The inherited UI stays live until Phase 19 acts on these decisions.
|
|
14
|
+
|
|
15
|
+
## Context
|
|
16
|
+
|
|
17
|
+
Phase 18's spine (Steps 1–7, #420 through #426) disentangled the activity tier from the core.
|
|
18
|
+
The core now owns all run state in one place (`SubagentState`), the widget self-drives from lifecycle events, the LLM-facing `subagent` tool no longer depends on the widget, and the public event contract's declared channels equal its emitted channels.
|
|
19
|
+
The UI is therefore a pure reactive consumer of the broadcast-plus-query surface — _substitutable_.
|
|
20
|
+
|
|
21
|
+
This final step decides the UI's _direction and distribution_, not whether substitution is possible.
|
|
22
|
+
The goal is **substitutable, not optional**: a human needs some surface, but the specific UI is replaceable — the way Pi ships a default TUI built on the same public API any extension targets.
|
|
23
|
+
The disentangled core stays byte-for-byte identical whether or not a given UI consumer is installed (the composition invariant), so a replacement UI is a downstream concern even though _some_ UI is not.
|
|
24
|
+
|
|
25
|
+
Unlike the worktrees provider seam (generative, rationed — one provider the core consults), the UI is an observational consumer (unlimited, the core never waits on it).
|
|
26
|
+
That asymmetry is why packaging the UI is the secondary question and decoupling it was the real win.
|
|
27
|
+
|
|
28
|
+
Three operator-framed concerns shape the per-component judgment.
|
|
29
|
+
|
|
30
|
+
1. **Foreground progress is already shown by the tool call.**
|
|
31
|
+
In foreground the `subagent` tool's inline `onUpdate` stream renders progress well; the above-editor widget duplicates it.
|
|
32
|
+
2. **Background agents have no tool-call display.**
|
|
33
|
+
When agents run in the background there is no inline stream, so _something_ must indicate their state — and multiple subagents can run in parallel, so that surface must represent N concurrent agents at once.
|
|
34
|
+
3. **Operator visibility into a subagent's session is a distinct, richer need.**
|
|
35
|
+
"Switch into a subagent's session, scroll/read it, switch between subagents, and exit back to root" is a navigation interaction, not a live overlay.
|
|
36
|
+
The core already persists each child as a standalone Pi session JSONL at `Subagent.outputFile`, and `Subagent.messages` exposes the full history — so the data was never the limit; the bespoke, width-capped `ConversationViewer` overlay was.
|
|
37
|
+
|
|
38
|
+
### Relevant Pi SDK surface
|
|
39
|
+
|
|
40
|
+
Verified against `@earendil-works/pi-coding-agent@0.79.1`:
|
|
41
|
+
|
|
42
|
+
- `ExtensionActions.switchSession(sessionPath, { withSession })` switches the **active** session to a different session file.
|
|
43
|
+
It is a full active-session takeover: it fires `session_before_switch` / `session_shutdown`, invalidates the current session context (`setBeforeSessionInvalidate` exists for host-owned UI teardown), and returns `{ cancelled }`.
|
|
44
|
+
The switched-to session is fully interactive — `ReplacedSessionContext` exposes `sendUserMessage`.
|
|
45
|
+
- `session-manager` exports `loadEntriesFromFile(filePath)` / `parseSessionEntries(content)`, which read a session file's entries without switching — the read-only alternative to a full takeover.
|
|
46
|
+
|
|
47
|
+
## Decision
|
|
48
|
+
|
|
49
|
+
Judge each UI component on the first principles above, then record the distribution.
|
|
50
|
+
|
|
51
|
+
### A — Foreground widget: shrink to background agents only
|
|
52
|
+
|
|
53
|
+
The above-editor widget duplicates the foreground tool's inline `onUpdate` stream.
|
|
54
|
+
The widget survives **only** as the background-agent status surface (concern 2): foreground runs suppress it, the inline stream is authoritative there, and the background surface keeps the widget's existing per-agent tree so it represents N parallel agents at once.
|
|
55
|
+
The change is _when_ the widget shows (background-only), not _what_ it shows.
|
|
56
|
+
|
|
57
|
+
### B — Conversation viewer: replace the bespoke overlay with native session navigation
|
|
58
|
+
|
|
59
|
+
Remove the bespoke `ConversationViewer` overlay.
|
|
60
|
+
Operator visibility (concern 3) is served by Pi's own session machinery applied to the already-persisted child session file, not a hand-rolled transcript renderer — the recursive-Pi insight applied to `Subagent.outputFile`.
|
|
61
|
+
|
|
62
|
+
The illustrative call shape (Phase 19, not final):
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
// "View running agents" → pick a child → switch into its persisted session
|
|
66
|
+
const child = manager.getRecord(id);
|
|
67
|
+
if (child?.outputFile) {
|
|
68
|
+
await ctx.switchSession(child.outputFile);
|
|
69
|
+
// operator reads/scrolls in Pi's native viewer; a later switch returns to root
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
This is Tell-Don't-Ask (hand Pi the session path; Pi owns the viewer) and keeps the core free of transcript-rendering code.
|
|
74
|
+
|
|
75
|
+
This decision records the _direction_ (native session machinery over a bespoke renderer), not the _mechanism_.
|
|
76
|
+
`switchSession` is a full active-session takeover and is interactive, so the operator UX is gated on a Phase 19 spike that chooses between (i) true `switchSession` round-trips and (ii) a read-only transcript built from `loadEntriesFromFile` that renders Pi-standard entries without leaving the root session.
|
|
77
|
+
See "Phase 19 entry criteria."
|
|
78
|
+
|
|
79
|
+
### C — `/agents` menu: dissolve the monolithic command into focused surfaces
|
|
80
|
+
|
|
81
|
+
The single `/agents` command bundles four unrelated jobs; split them, and do not keep all in one command.
|
|
82
|
+
Managing agent _definitions_ through the menu earns no keep — creating or editing agents is better done with other tools (directly in Pi, or a real text editor / IDE).
|
|
83
|
+
|
|
84
|
+
- **Create new agent (wizard)** → **remove.**
|
|
85
|
+
An operator generates a new agent `.md` by asking a Pi agent directly (more capable than a fixed wizard) or by writing the file in an editor.
|
|
86
|
+
- **Agent types (list + config editor)** → **remove.**
|
|
87
|
+
Viewing and editing agent definitions is better served by opening the `.md` files directly in an editor/IDE.
|
|
88
|
+
- **Running agents (visibility)** → **keep the responsibility, re-home it.**
|
|
89
|
+
_Something_ must own running-agent visibility; it moves onto the background widget (Decision A) plus the native session navigation (Decision B), not a bespoke in-menu overlay.
|
|
90
|
+
- **Settings (concurrency / max turns / grace turns)** → **extract to a focused command** (e.g. `/subagents:settings`).
|
|
91
|
+
Some value, but it does not belong bundled with agent management.
|
|
92
|
+
|
|
93
|
+
### D — Distribution: keep the surviving UI in-core (substitutable, not extracted)
|
|
94
|
+
|
|
95
|
+
The spine already made the UI substitutable; a replacement UI is a downstream concern that targets the public broadcast-plus-query surface.
|
|
96
|
+
The surviving UI — the background widget, a focused settings command, and the session-navigation glue — **stays in-core** as a reactive consumer.
|
|
97
|
+
Extraction to a separate `@gotgenes/pi-subagents-ui` package is **not** chosen now.
|
|
98
|
+
|
|
99
|
+
This answers the issue's headline question — the UI's _distribution_ — with "keep in core, substitutable," recorded explicitly rather than left implicit.
|
|
100
|
+
Extraction remains an available future option precisely because the composition invariant holds: the core is byte-for-byte identical with or without a given UI consumer.
|
|
101
|
+
It would be revisited if a second, materially different UI consumer appears, or if the in-core UI starts to pull SDK or rendering concerns back into core modules.
|
|
102
|
+
|
|
103
|
+
## Consequences
|
|
104
|
+
|
|
105
|
+
- The inherited UI is no longer preserved by default; each component now has a recorded fate (shrink / replace / dissolve) motivated by the first principles, not by inheritance.
|
|
106
|
+
- Phase 18 is complete.
|
|
107
|
+
This ADR gateways Phase 19, which implements the decisions (background-only widget, native session navigation, `/agents` decomposition, `/subagents:settings` extraction) under its own plan and issues.
|
|
108
|
+
- No interim regression: this ADR removes nothing.
|
|
109
|
+
The widget, the `ConversationViewer`, and the full `/agents` menu stay live until Phase 19 replaces them.
|
|
110
|
+
- Phase 19 must preserve the spine's invariants when it acts on these decisions: the runtime holds zero UI state (#422), the widget is a reactive consumer with no inbound calls from core spawn tools (#423), the LLM tool depends only on manager/runtime/settings/registry (#424), and declared event channels equal emitted channels with no vacant hook (#425).
|
|
111
|
+
These are pinned today by the existing observer/widget/event-contract suites, which Phase 19 inherits.
|
|
112
|
+
|
|
113
|
+
## Phase 19 entry criteria
|
|
114
|
+
|
|
115
|
+
The following are open and must be resolved by a Phase 19 spike before committing to a mechanism; they are deliberately not decided here.
|
|
116
|
+
|
|
117
|
+
- **Root-continuity during a session switch.**
|
|
118
|
+
`switchSession` invalidates the current session context — does the root's in-flight turn survive a switch-out-and-return, and what is the correct "return to root" gesture?
|
|
119
|
+
Resolve before committing to true `switchSession` round-trips.
|
|
120
|
+
- **View-only vs interactive.**
|
|
121
|
+
A switched-to child session is interactive (`sendUserMessage`).
|
|
122
|
+
Decide whether steering a child from its own session is desirable, or whether the viewer should be strictly read-only (favoring the `loadEntriesFromFile` transcript path).
|
|
123
|
+
- **Parallel-agent navigation.**
|
|
124
|
+
With N background agents running, decide the operator's gesture to pick which child to view and to cycle between them — driven from the background widget, a dedicated command, or both.
|
|
125
|
+
- **Settings command namespace.**
|
|
126
|
+
Confirm the final command name/namespace for the extracted settings surface (`/subagents:settings` vs another form) against how sibling packages register namespaced commands.
|
|
127
|
+
|
|
128
|
+
The agent create/edit surfaces are **not** open questions: both are removed (Decision C).
|
|
129
|
+
|
|
130
|
+
## Addendum (2026-06-20): Phase 19 entry-criteria answers ([#446])
|
|
131
|
+
|
|
132
|
+
The Phase 19 Step 1 spike ([#446]) resolved all four entry criteria.
|
|
133
|
+
Evidence comes from the bundled `@earendil-works/pi-coding-agent` SDK surface (`packages/pi-subagents/node_modules/@earendil-works/pi-coding-agent/dist`) and a throwaway vitest harness run against a **real child session JSONL** (a 43-entry subagent session: 1 `session` header carrying a `parentSession` backref, 1 `model_change`, 1 `thinking_level_change`, 40 `message` entries).
|
|
134
|
+
The harness was discarded after observation; no production source changed.
|
|
135
|
+
|
|
136
|
+
### Finding 0 — `loadEntriesFromFile` is not part of the package's public surface
|
|
137
|
+
|
|
138
|
+
The original "Relevant Pi SDK surface" section cited `loadEntriesFromFile` as the read-only alternative to a switch.
|
|
139
|
+
The spike found it is **not reachable** from `@earendil-works/pi-coding-agent`, and that this is not a types/runtime mismatch — the type barrel and the runtime barrel agree, both omitting it.
|
|
140
|
+
`loadEntriesFromFile` is defined in the deep module `core/session-manager.ts` (annotated `/** Exported for testing */`), but the public barrel `src/index.ts` (→ `dist/index.d.ts` + `dist/index.js`) re-exports only a curated subset of that module — including `parseSessionEntries` but **not** `loadEntriesFromFile`.
|
|
141
|
+
The `package.json` `exports` map exposes only `"."` → the barrel, so the deep import `@earendil-works/pi-coding-agent/dist/core/session-manager.js` is not a supported entry point either.
|
|
142
|
+
`tsc` correctly rejects `import { loadEntriesFromFile } from "@earendil-works/pi-coding-agent"` with `TS2305: Module … has no exported member 'loadEntriesFromFile'`; the throwaway Vitest harness only reached a runtime `is not a function` because esbuild strips types without type-checking (the package's own `pnpm run check` would have caught it at compile time).
|
|
143
|
+
This is not version-specific: the barrel omits it identically in both the pinned `0.79.1` and the latest `0.79.8`, so an SDK upgrade does not surface it — Step 4 should not chase one.
|
|
144
|
+
The viable read-only path is therefore `parseSessionEntries(readFileSync(outputFile, "utf8"))` — `parseSessionEntries` _is_ public (both types and runtime) — which the harness confirmed returns the full `FileEntry[]` transcript with no session switch and no active-session mutation.
|
|
145
|
+
Step 4 ([#445]) should read the file itself and call `parseSessionEntries`, not `loadEntriesFromFile`.
|
|
146
|
+
|
|
147
|
+
Upstream references:
|
|
148
|
+
|
|
149
|
+
- Barrel that omits it: [`packages/coding-agent/src/index.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/index.ts).
|
|
150
|
+
- Test-annotated definition: [`packages/coding-agent/src/core/session-manager.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/session-manager.ts).
|
|
151
|
+
|
|
152
|
+
### Finding 1 — the read-only transcript renders entirely on public Pi APIs (no bespoke renderer)
|
|
153
|
+
|
|
154
|
+
The read-only path returns raw `FileEntry[]`, so _something_ must render them.
|
|
155
|
+
The spike found that every piece needed to turn those entries into a Pi-standard transcript is already re-exported through the public root barrel — so Step 4 wires Pi's own machinery rather than re-implementing the formatting the bespoke `ConversationViewer`/`message-formatters.ts` carried.
|
|
156
|
+
This matters because ADR-0004 Decision B's `switchSession` sketch implied "Pi owns the viewer"; the read-only path keeps that property at the _component_ level (Pi renders each entry) without the active-session takeover.
|
|
157
|
+
|
|
158
|
+
The verified public pipeline (all symbols confirmed present in `dist/index.d.ts`, the root barrel):
|
|
159
|
+
|
|
160
|
+
1. **Load** — `parseSessionEntries(readFileSync(record.outputFile, "utf8")): FileEntry[]` (Finding 0).
|
|
161
|
+
2. **Bridge** — `buildSessionContext(entries, leafId?, byId?): SessionContext` turns the entries into `{ messages: AgentMessage[], thinkingLevel, model }`, handling tree traversal, compaction, and branch summaries along the path.
|
|
162
|
+
Note `buildSessionContext` takes `SessionEntry[]`, so drop the leading `SessionHeader` (`type: "session"`) that `parseSessionEntries` includes.
|
|
163
|
+
3. **Render** — one of:
|
|
164
|
+
- **Text:** `serializeConversation(messages: Message[]): string` (from `core/compaction`) for a plain-text dump.
|
|
165
|
+
- **TUI:** the per-entry components `AssistantMessageComponent`, `UserMessageComponent`, `ToolExecutionComponent`, `BashExecutionComponent`, `CompactionSummaryMessageComponent`, `BranchSummaryMessageComponent`, `CustomMessageComponent`, `SkillInvocationMessageComponent` (from `modes/interactive/components`), plus `renderDiff`, for a scrollable native transcript.
|
|
166
|
+
|
|
167
|
+
Unlike `loadEntriesFromFile`, all of these _are_ public (both types and runtime).
|
|
168
|
+
So the read-only viewer is buildable end-to-end on supported APIs, and ADR-0004 Decision B's "keep the core free of transcript-rendering code" holds — the rendering is Pi's, imported, not hand-rolled.
|
|
169
|
+
|
|
170
|
+
Upstream references:
|
|
171
|
+
|
|
172
|
+
- Component barrel: [`packages/coding-agent/src/modes/interactive/components/index.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/modes/interactive/components/index.ts).
|
|
173
|
+
- `serializeConversation`: [`packages/coding-agent/src/core/compaction/utils.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/compaction/utils.ts).
|
|
174
|
+
|
|
175
|
+
### Criterion 1 — Root-continuity during a session switch: avoid the switch
|
|
176
|
+
|
|
177
|
+
`switchSession` is a full active-session takeover: it fires `session_before_switch` (cancellable) and then tears the current runtime down via `session_shutdown` (whose `targetSessionFile` field marks a replacement-driven shutdown).
|
|
178
|
+
The root's in-flight turn does **not** survive the takeover — the runtime that owns that turn is invalidated — and a "return to root" would require a second `switchSession(rootSessionFile)` that re-incurs the teardown on the way back.
|
|
179
|
+
Because background agents run precisely while the operator keeps working at root, a true `switchSession` round-trip is hostile to a root with a turn in flight.
|
|
180
|
+
|
|
181
|
+
**Answer:** do not use `switchSession` for navigation.
|
|
182
|
+
The read-only transcript path (Criterion 2) sidesteps root-continuity entirely — it never touches the active session, so there is no return gesture to get wrong.
|
|
183
|
+
|
|
184
|
+
### Criterion 2 — View-only vs interactive: read-only
|
|
185
|
+
|
|
186
|
+
`ReplacedSessionContext` (handed to a `switchSession` `withSession` callback) extends `ExtensionCommandContext` and exposes `sendUserMessage`/`sendMessage`, so a switched-to child session is interactive.
|
|
187
|
+
But operator visibility (concern 3) is framed as "switch in, scroll/read, switch between, exit back to root" — a navigation interaction, not a live steering overlay — and steering already has a home (`steer_subagent` tool / the widget).
|
|
188
|
+
Adding in-session steering would create a second, redundant steering surface.
|
|
189
|
+
|
|
190
|
+
**Answer:** the viewer is strictly **read-only**, loaded via `parseSessionEntries(readFileSync(record.outputFile))` (Finding 0) and rendered through Pi's public entry components (Finding 1) without leaving the root session.
|
|
191
|
+
This also resolves Criterion 1 by construction.
|
|
192
|
+
|
|
193
|
+
### Criterion 3 — Parallel-agent navigation: command-first
|
|
194
|
+
|
|
195
|
+
With N background agents running, the operator needs a gesture to pick which child to view.
|
|
196
|
+
The background widget (Decision A, [#444]) already represents N parallel agents as a per-agent tree, making it the natural eventual selection surface; a flat command gives a non-widget entry point that lists running background agents and lets the operator pick one keyed on `record.outputFile`.
|
|
197
|
+
|
|
198
|
+
**Answer:** Step 4 ([#445]) ships a **command** as the primary, unit-testable selection surface (list background agents → pick → render that child's transcript read-only), with a widget gesture as an optional later enhancement.
|
|
199
|
+
"Both" remains the eventual target; command-first is the Step 4 starting point because it does not depend on the widget shrink ([#444]) landing first.
|
|
200
|
+
|
|
201
|
+
### Criterion 4 — Settings command name: `/subagents-settings`
|
|
202
|
+
|
|
203
|
+
Sibling packages register flat, hyphenated command names with no `:` namespace: `registerCommand("agents", …)` (this package), `"colgrep-reindex"`, `"permission-system"`.
|
|
204
|
+
A `/subagents:settings` form would be inconsistent with every existing command in the repo, and `/agents-settings` wrongly implies it manages agent definitions (which Decision C removes).
|
|
205
|
+
|
|
206
|
+
**Answer:** confirm **`/subagents-settings`** (flat, hyphenated) for Step 2 ([#447]).
|
|
207
|
+
Reject the tentative `/subagents:settings` and the `/agents-settings` alternative.
|
|
208
|
+
|
|
209
|
+
### Net mechanism for Phase 19
|
|
210
|
+
|
|
211
|
+
- Session navigation (Step 4, [#445]): a read-only transcript, surfaced through a flat command; no `switchSession`, no `loadEntriesFromFile`.
|
|
212
|
+
The pipeline is `parseSessionEntries(readFileSync(record.outputFile, "utf8"))` → drop the `SessionHeader` → `buildSessionContext(...).messages` → render via Pi's public entry components (`AssistantMessageComponent` / `ToolExecutionComponent` / … ) or `serializeConversation` (Findings 0 and 1).
|
|
213
|
+
- Settings command (Step 2, [#447]): `/subagents-settings`.
|
|
214
|
+
|
|
215
|
+
This keeps bespoke transcript rendering out of the package (rendering is Pi's own public components), adds no inbound call from the UI to the core, and preserves the Phase 18 spine invariants (#422–#425).
|
|
216
|
+
|
|
217
|
+
[#444]: https://github.com/gotgenes/pi-packages/issues/444
|
|
218
|
+
[#445]: https://github.com/gotgenes/pi-packages/issues/445
|
|
219
|
+
[#446]: https://github.com/gotgenes/pi-packages/issues/446
|
|
220
|
+
[#447]: https://github.com/gotgenes/pi-packages/issues/447
|