@cr1ms0n/pi-subagent 0.8.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +352 -0
  2. package/LICENSE +21 -0
  3. package/README.md +543 -0
  4. package/docs/ARCHITECTURE.md +125 -0
  5. package/docs/COST-ACCOUNTING.md +66 -0
  6. package/docs/PLAN.md +325 -0
  7. package/docs/RELEASING.md +32 -0
  8. package/docs/ROADMAP.md +252 -0
  9. package/docs/SECURITY.md +85 -0
  10. package/docs/UI-OVERHAUL.md +186 -0
  11. package/docs/UX.md +141 -0
  12. package/extensions/subagent.ts +1 -0
  13. package/package.json +58 -0
  14. package/skills/subagent/SKILL.md +103 -0
  15. package/src/agents.ts +285 -0
  16. package/src/backend.ts +146 -0
  17. package/src/backends/claude.ts +384 -0
  18. package/src/backends/codex.ts +330 -0
  19. package/src/backends/index.ts +26 -0
  20. package/src/backends/pi.ts +94 -0
  21. package/src/btw.ts +34 -0
  22. package/src/config.ts +254 -0
  23. package/src/distill.ts +222 -0
  24. package/src/extension.ts +1527 -0
  25. package/src/format.ts +365 -0
  26. package/src/index.ts +60 -0
  27. package/src/launch.ts +120 -0
  28. package/src/maintenance.ts +6 -0
  29. package/src/model-policy.ts +157 -0
  30. package/src/notifications.ts +106 -0
  31. package/src/orchestrator.ts +247 -0
  32. package/src/output.ts +124 -0
  33. package/src/persistence.ts +334 -0
  34. package/src/policy.ts +500 -0
  35. package/src/process-lock.ts +687 -0
  36. package/src/protocol.ts +290 -0
  37. package/src/registry.ts +632 -0
  38. package/src/runner.ts +850 -0
  39. package/src/schema.ts +166 -0
  40. package/src/semaphore.ts +123 -0
  41. package/src/structured.ts +169 -0
  42. package/src/transcript.ts +360 -0
  43. package/src/types.ts +197 -0
  44. package/src/ui.ts +545 -0
  45. package/src/usage.ts +274 -0
  46. package/src/worktree.ts +753 -0
@@ -0,0 +1,252 @@
1
+ # Roadmap
2
+
3
+ > Execution details for the remaining phases (work breakdown, acceptance
4
+ > criteria, test plans, release gates) live in [PLAN.md](./PLAN.md). This
5
+ > document holds the rationale, design sketches, and deferral decisions.
6
+
7
+ Prioritized improvement plan, consolidated from four review passes: the
8
+ competitor field study (@tintinweb, nicobailon), the strategic review, the
9
+ fresh-eyes engine review, and the oh-my-pi task-system comparison. Each item
10
+ carries a design sketch grounded in the current code, an effort/impact rating,
11
+ and explicit risks. Ordering within a phase is the intended implementation
12
+ order; phases are independently shippable releases.
13
+
14
+ Legend: effort S (<½ day) / M (1–2 days) / L (3+ days) · impact ▲ high / △ medium
15
+
16
+ ---
17
+
18
+ ## Phase 1 — Structured results (v0.3.0) — ✅ SHIPPED
19
+
20
+ The single biggest remaining quality gap. Free-text child output forces the
21
+ parent to re-parse prose, which is fragile and burns tokens. oh-my-pi's
22
+ `yield` protocol validates the design; we adapt it to our process boundary.
23
+
24
+ > Implemented in `src/structured.ts` + runner/policy/output wiring: 1.1
25
+ > (output_schema with settle-gated validation and one steer-based repair
26
+ > round, agent-file `output_schema:` inline or `@file.json`), 1.2 (validated
27
+ > JSON handoff to synthesis), 1.3 (conservative arg repair). E2E-verified
28
+ > against real Pi.
29
+
30
+ ### 1.1 `output_schema` — validated structured output ▲ M
31
+
32
+ **What:** optional JSON Schema per task. The child must end with a fenced
33
+ ` ```json:result ` block (or a final JSON object) satisfying the schema; we
34
+ validate on our side of the process boundary and surface `result.parsed` in
35
+ details plus a `structured: true` flag.
36
+
37
+ **Design:**
38
+ - Schema field `output_schema: Type.Optional(Type.Object({}, { additionalProperties: true }))`
39
+ on `TaskFields` (`src/schema.ts`). Validate at policy time that it parses as
40
+ a JSON Schema (typebox `Value.Check` against a permissive meta-check).
41
+ - Inject a compact contract into the child via the existing
42
+ `--append-system-prompt` path (`src/runner.ts` `spec.systemPrompt` join):
43
+ "Your final message MUST end with a ```json:result fenced block matching
44
+ this schema: …". Persona prompt first, schema contract last (highest
45
+ salience).
46
+ - Parse in `ProtocolParser.finalize()` (`src/protocol.ts`): scan the final
47
+ assistant text for the fenced block, fall back to a trailing bare JSON
48
+ object. Attach `structuredOutput?: unknown` + `structuredError?: string` to
49
+ `TaskResult` (`src/types.ts`), thread through `toPersistedResult` /
50
+ `compactDetails`.
51
+ - **Retry integration (the payoff):** schema-invalid output is NOT a transient
52
+ failure (no model fallback), but gets one **steer-based repair round** —
53
+ reuse the wrap-up machinery in `src/runner.ts` (`handleBudgetBreach`
54
+ pattern): steer "your result did not validate: <errors>; re-emit the fenced
55
+ block" and allow 1 extra turn. Mirrors oh-my-pi's yield-reminder ladder
56
+ without needing an in-process tool.
57
+ - Delivery: when `output_schema` is set and validation succeeded, `wait`/
58
+ foreground text is the pretty-printed JSON; `details.results[n].parsed`
59
+ carries the object. Validation failure after the repair round → state
60
+ `partial`, `structuredError` set, raw text still delivered (never lose paid
61
+ work).
62
+ - Agent files (`src/agents.ts`): allow `output_schema` in frontmatter as an
63
+ inline JSON value or `@file.json` reference, so personas can carry contracts.
64
+
65
+ **Dependency:** none new — use typebox `Value.Check` with a schema compiled
66
+ via `Type.Unsafe` wrapping, or plain structural validation for the JSON-Schema
67
+ subset we accept (`type/properties/required/items/enum`). Do NOT add ajv.
68
+
69
+ **Risks:** models ignoring the contract (mitigated by the repair steer); the
70
+ fenced-block convention colliding with task content (use a unique fence tag).
71
+
72
+ ### 1.2 `{outputs}` handoff for synthesis + sequential patterns △ S
73
+
74
+ **What:** when parallel tasks used `output_schema`, feed the synthesis child
75
+ the *validated objects* (JSON) instead of prose tails, and include per-task
76
+ validity flags. Pure upgrade to `runSynthesis` (`src/extension.ts`).
77
+
78
+ ### 1.3 Arg repair for mangled task text △ S
79
+
80
+ oh-my-pi's `repair-args.ts` steal. LLMs sometimes double-encode JSON into
81
+ string fields. Detect structural escape patterns (`\"`, `\\n`, `\u00XX`) in
82
+ `task` / `system_prompt` / `synthesis` at policy time and de-mangle once when
83
+ the decoded form parses cleanly. **Never** touch identifier fields (`agent`,
84
+ `model`, `id`) or `tools`. ~50 lines in `src/policy.ts` + table tests.
85
+
86
+ ---
87
+
88
+ ## Phase 2 — Agent ecosystem depth (v0.3.x)
89
+
90
+ Builds on the named-agent files just shipped. All items are additive
91
+ frontmatter/behavior; no format break.
92
+
93
+ ### 2.1 `spawns:` allowlist in agent frontmatter ▲ S
94
+
95
+ **What:** per-agent control over which agents its children may use — finer
96
+ than the global depth cap. oh-my-pi steal (`spawn-policy.ts`).
97
+
98
+ **Design:**
99
+ - Frontmatter `spawns: false | "*" | "a, b"` parsed in `src/agents.ts`.
100
+ - Enforcement at spawn time: the parent's *own* spawn policy travels to the
101
+ child via env (`PI_SUBAGENT_SPAWNS`) next to `PI_SUBAGENT_DEPTH`
102
+ (`src/runner.ts` childEnv). `validateSubagentRequest` (`src/policy.ts`)
103
+ reads it: `false` → reject any spawn; list → the requested `agent:` (or
104
+ agentless composition) must be allowed. Fail closed on malformed env, same
105
+ as `parseDepth`.
106
+ - `spawns: false` children also get the subagent tool filtered out (extend
107
+ the existing `tool !== "subagent"` filter logic to honor policy, not just
108
+ the depth ceiling).
109
+
110
+ **Risk:** env is advisory (children can shell out to `pi` directly) — document
111
+ as accidental-recursion guard, exactly like depth (SECURITY.md item 3).
112
+
113
+ ### 2.2 Resumable-session discovery △ S
114
+
115
+ Fresh-eyes gap G6: resuming requires already knowing the child session id.
116
+ Extend bare `status` output (`src/extension.ts`): completed runs list
117
+ `session <id8> (resumable)` per result, and add a one-line hint to
118
+ `promptGuidelines`. No new action needed — the data is already in snapshots;
119
+ we just don't advertise it.
120
+
121
+ ### 2.3 Dry-run validation (`action: "plan"`) △ S
122
+
123
+ Fresh-eyes gap G11: policy failures (write-guard, unknown agent, bad tools)
124
+ currently surface only after enqueue. Add `action: "plan"` that runs
125
+ `validateSubagentRequest` + worktree/git preflight (`isGitRepo` for
126
+ isolation:'worktree' tasks) and returns the resolved per-task spec
127
+ (`resolutionNotes`, effective model/tools/budgets) without spawning.
128
+ Cheap: the validation path is already pure.
129
+
130
+ ### 2.4 Agent file ergonomics △ S
131
+
132
+ - `system_prompt: "@path.md"` references (relative to the agent file), so
133
+ personas can share prompt fragments. Resolve in `src/agents.ts` with the
134
+ same symlink/size guards.
135
+ - `/subagents` overlay: an `agents` line in the header showing catalog count,
136
+ and unknown-agent errors already list availability (done) — add the catalog
137
+ to `/subagent-cost`'s sibling command? No — keep one surface, skip.
138
+
139
+ ---
140
+
141
+ ## Phase 3 — Engine hardening (v0.4.0)
142
+
143
+ Remaining items from the fresh-eyes review plus oh-my-pi's concurrency lesson.
144
+ Individually small, collectively they close the last known failure modes.
145
+
146
+ ### 3.1 Queue-slot release granularity ▲ M
147
+
148
+ oh-my-pi's deadlock lesson (their issue #3749): a semaphore slot held for a
149
+ child's full lifetime deadlocks spawn trees wider than the limit. We're
150
+ partially exposed: a nested parent (depth 1) holds a per-session slot in *its*
151
+ runtime while its own children queue — different semaphores per process, so
152
+ the per-session limiter is safe, but the **machine-wide `maxGlobalActive`
153
+ slot** (`src/process-lock.ts` global slots) IS held across the child's whole
154
+ run, including while that child waits on its own children's global slots.
155
+
156
+ **Design:** exempt nested parents from double-counting — a child process
157
+ should release/not-hold its global slot while it is itself blocked waiting
158
+ for descendants. Simplest correct fix given our process boundary: count only
159
+ *leaf* work against the global cap by having the runner acquire the global
160
+ slot **after** semaphore acquire and release it during any period the child
161
+ reports zero active LLM streaming (not knowable today) — OR, pragmatically:
162
+ raise the effective cap per depth level (`maxGlobalActive` applies per depth
163
+ tier: `slots(depth) = maxGlobalActive - depth * reserved`). Decide at
164
+ implementation time; write the deadlock repro test first
165
+ (`tests/process-lock.test.ts`: tree of width > cap).
166
+
167
+ ### 3.2 Dirty-baseline worktrees △ M
168
+
169
+ oh-my-pi steal. `worktrees.create` (`src/worktree.ts`) snapshots `HEAD`,
170
+ silently excluding the parent's uncommitted changes — a writer agent asked to
171
+ "fix the bug I'm mid-way through" can't see the WIP.
172
+
173
+ **Design:** optional `include_wip: true` on worktree tasks: after
174
+ `git worktree add`, apply `git diff HEAD` from the base checkout (plus
175
+ untracked files via `git ls-files -o --exclude-standard` copy) into the
176
+ worktree, uncommitted. `diff`/`apply` must then diff against
177
+ `baseCommit + WIP` — capture a synthetic baseline patch in the handle and
178
+ subtract it (oh-my-pi's filtered-delta approach, simplified: store the WIP
179
+ patch, exclude its hunks from `diff` output via `git diff` against a temp
180
+ index). Keep default OFF; document that apply-back of WIP-seeded worktrees
181
+ reports the combined delta if subtraction fails, with a warning.
182
+
183
+ ### 3.3 Lease-expiry reclaim latency △ S
184
+
185
+ Fresh-eyes #10: a crashed same-host owner is reclaimed instantly via
186
+ `isAlive`, but a *cross-host* (or identity-unverifiable) dead owner blocks
187
+ resume for up to 2× lease. Reduce the stale window to
188
+ `leaseExpiresAt < now()` (one lease, not two) when the process identity is
189
+ verifiably dead-or-foreign-host, keeping the 2× grace only when identity is
190
+ unknown. One condition in `src/process-lock.ts:300` + clock-skew comment.
191
+
192
+ ### 3.4 API-consumer orphan records △ S
193
+
194
+ Fresh-eyes #8: `runSubagent()` without `locks`/`runId` writes no run record,
195
+ so its children are invisible to orphan reclaim. Either (a) document loudly in
196
+ the JSDoc that durable reclaim requires `locks` + `runId` (S), or (b) create a
197
+ default ProcessLockManager when omitted (M, changes API behavior). Choose (a)
198
+ now, revisit if the SDK surface grows users.
199
+
200
+ ---
201
+
202
+ ## Phase 4 — Observability polish (v0.4.x)
203
+
204
+ ### 4.1 Live transcript view in `/subagents` △ M
205
+
206
+ The UI-overhaul "stretch" item, now more valuable with steering: the detail
207
+ view shows the *checkpointed* transcript (capped, message-boundary updates).
208
+ Tail the child's session `.jsonl` (we know the path: `sessionDir` +
209
+ `sessionId`) for the selected running run, rendering the last N events live.
210
+ Read-only file tailing; no RPC changes. Pairs with the existing `s` steer key
211
+ to close the observe→steer loop in one surface.
212
+
213
+ ### 4.2 Widget/notification config △ S
214
+
215
+ `widget: "background" | "off"` and `notifications: "batched" | "off"` in
216
+ `SubagentConfig` — some users will want quiet mode. Trivial gates around
217
+ `refreshWidget` / `CompletionBatcher` in `src/extension.ts`.
218
+
219
+ ### 4.3 Stall/attempt events in checkpoints for `status` △ S
220
+
221
+ `stalledSince`/`attempts` already render inline and in details; also include
222
+ them in `formatStatusPreview` so background `status` polling shows
223
+ `[stalled 2m]` / `[attempt 2]` without opening the overlay.
224
+
225
+ ---
226
+
227
+ ## Explicitly deferred (decided, with reasons)
228
+
229
+ | Item | Reason |
230
+ |---|---|
231
+ | Inter-agent messaging (`hub`-style) | Star topology + synthesis + steer covers ~85%; even oh-my-pi (full engine access) scopes it to "quick coordination". Revisit only on demonstrated demand. |
232
+ | CoW filesystem isolation | Requires native code; impossible in a TS-only extension. `git worktree` stays. |
233
+ | In-process execution mode | Would forfeit our moat (crash durability, orphan reclaim, version isolation). |
234
+ | Chains / DAG workflows | The orchestrating LLM sequences calls fine; `resume` + structured outputs (1.1) compose. nicobailon's chain complexity is a warning, not an invitation. |
235
+ | Scheduling (cron) | Different product. A separate extension could call our tool. |
236
+ | Tiny-model label generation | `description` param covers it at zero cost. |
237
+ | Eager-delegation system prompt | Fork-only capability; our `promptGuidelines` already nudge. |
238
+ | Agent management UI (eject/create wizard) | Files are the UI; keep one config surface. |
239
+
240
+ ## Sequencing summary
241
+
242
+ | Release | Contents | Theme |
243
+ |---|---|---|
244
+ | v0.3.0 | 1.1, 1.2, 1.3 | structured results |
245
+ | v0.3.x | 2.1, 2.2, 2.3, 2.4 | agent ecosystem |
246
+ | v0.4.0 | 3.1, 3.2, 3.3, 3.4 | engine hardening |
247
+ | v0.4.x | 4.1, 4.2, 4.3 | observability |
248
+
249
+ Every phase keeps the standing invariants (docs/ARCHITECTURE.md): capability
250
+ profiles fail closed, explicit params beat file/config defaults, delivery-once,
251
+ partial work is never discarded, and no feature may require the parent to be
252
+ alive for a child's work to survive.
@@ -0,0 +1,85 @@
1
+ # Security model
2
+
3
+ Pi packages run with full system access. This extension spawns child `pi`
4
+ processes that inherit the parent environment (including provider credentials)
5
+ and can use tools according to their capability profile.
6
+
7
+ ## What subagents can do
8
+
9
+ | Profile | Default tools | Writes? |
10
+ |---------|---------------|---------|
11
+ | `explore` | `read`, `grep`, `find`, `ls` (+ explicit read-only extras) | No |
12
+ | `review` | same as explore | No |
13
+ | `general` | inherited active tools (may include `bash`/`edit`/`write`) | Yes |
14
+
15
+ Parallel mode defaults to `explore` to avoid concurrent shared writes.
16
+
17
+ ## Hard rules
18
+
19
+ 1. **Read-only means no `bash`.** `bash` can rewrite the disc and is never part of
20
+ an explore/review profile.
21
+ 2. **Parallel writers** require `isolation: "worktree"`, distinct `cwd` values,
22
+ or an explicit `allow_shared_writes: true` opt-in.
23
+ 3. **Depth is capped** (`maxDepth`, default 2). Nested children at the ceiling do
24
+ not re-register the subagent tool. Depth is scheduling metadata — `bash` or an
25
+ env-scrubbing wrapper can still invoke `pi` directly, so treat it as an
26
+ accidental-recursion guard, not a security boundary.
27
+ **Spawn allowlists** (`spawns:` in agent frontmatter, env `PI_SUBAGENT_SPAWNS`)
28
+ refine that same guard: a child may be limited to named personas, or to none
29
+ (tool not registered). Like depth, this is not a sandbox — children can still
30
+ shell out to `pi`.
31
+ 4. **Process caps** limit concurrency both per parent session (`maxActiveProcesses`)
32
+ and machine-wide (`maxGlobalActive`, default 16).
33
+ 5. **Transcripts** under `~/.pi/subagent-sessions` may contain task content, tool
34
+ output, and secrets that appeared in context. Protect that directory. Task text
35
+ is delivered via stdin (not argv) so it stays out of `ps` listings, but it is
36
+ still written into the child session log.
37
+ 6. **Background permission prompts** are limited because children run headless
38
+ (RPC mode). Extension UI dialogs raised inside a child are auto-cancelled so
39
+ they can never hang a run — which also means a child can never obtain
40
+ interactive consent. Prefer restricted tools for async/background runs.
41
+ 11. **Steering messages** (`action: "steer"` and the overlay `s` key) inject text
42
+ into a running child's conversation with user-level authority. Anything that
43
+ can call the subagent tool can steer any live run in the same session.
44
+ 7. **Process cleanup.** On POSIX, children run in their own process group so tree
45
+ kills work for ordinary descendants. Parent (re)start reaps orphans recorded
46
+ under `~/.pi/subagent-locks/runs/` so resume cannot race a still-alive writer.
47
+ Grandchildren that call `setsid()` can still escape a simple process-group kill.
48
+ 8. **Resume exclusivity.** Direct resume takes a durable per-session file lock;
49
+ concurrent parents cannot append to the same child session.
50
+ 9. **Profiles are tool-selection policy, not a sandbox.** Children inherit
51
+ `$HOME`, SSH/cloud credentials, network access, and the parent filesystem.
52
+ Git worktrees only isolate the checkout. For untrusted tasks, use an outer
53
+ container/cgroup/network policy.
54
+ 10. **`max_cost` is accounting, not a hard provider gate.** Usage arrives after a
55
+ turn; orphans may spend money the ledger never sees. Combine with provider
56
+ account budgets for hard spend limits.
57
+
58
+ ## Trust and project cwd
59
+
60
+ If `cwd` points outside the parent project, the child inherits whatever local
61
+ project config/trust applies to that path. Treat external `cwd` as elevated risk
62
+ and prefer read-only profiles when exploring third-party trees.
63
+
64
+ ## Output artifacts
65
+
66
+ `output` files are written by the child. Resolve paths carefully and reject
67
+ duplicate output paths across parallel workers.
68
+
69
+ ## Named agent files
70
+
71
+ Agent files (`.pi/agents/`, `.agents/agents/`, global agent dir) inject their
72
+ body into the child's system prompt and set its model/tools/budgets. A
73
+ project-level agent file shapes subagent behavior the same way project
74
+ extensions and skills do — review them like code when working in untrusted
75
+ repositories. Mitigations: capability profiles still fail closed (an agent
76
+ cannot grant write tools under `explore`/`review`), symlinked agent files are
77
+ skipped, names are validated against traversal characters, and files over
78
+ 64KB are ignored.
79
+
80
+ ## Machine-wide state
81
+
82
+ `~/.pi/subagent-locks/` holds session locks, global concurrency slots, and run
83
+ process identity records. It is per-user (under `$HOME`) and must not be shared
84
+ across untrusted users/containers without care — a compromised client could
85
+ interfere with lock reclaim on the same account.
@@ -0,0 +1,186 @@
1
+ # UI Overhaul Plan — Pi-Native Rendering
2
+
3
+ > **Status: fully implemented.** All five phases are done (`src/format.ts`,
4
+ > `src/ui.ts`, `src/extension.ts`, `src/notifications.ts`). Deviations from
5
+ > the plan: the inline spinner uses a wall-clock frame (Pi's working indicator
6
+ > drives repaints, so no timer is owned); the ledger command landed as
7
+ > `/subagent-cost` via `ctx.ui.notify`. Phase 4 shipped as the background-only
8
+ > ambient widget plus batched `followUp` completion notifications with
9
+ > flush-time wait-suppression (see docs/UX.md). The `description` param is the
10
+ > task label across all surfaces. This document is retained as design rationale.
11
+
12
+ Goal: remove the duplicated cost footer, make inline chat rendering clean and
13
+ smooth, and align every surface with Pi's native conventions. Informed by a
14
+ code-level review against Pi's TUI docs/built-ins and a comparison with
15
+ `@tintinweb/pi-subagents` (Claude Code-style reference), `nicobailon/pi-subagents`
16
+ (highest-download orchestrator), and Claude Code's Task tool.
17
+
18
+ ## Design principles (from the comparison research)
19
+
20
+ 1. **Pi's tool shell owns state signaling.** The Box wrapper already paints
21
+ `toolPendingBg` / `toolSuccessBg` / `toolErrorBg` and animates the working
22
+ indicator. We stop drawing our own ✓/✗/state-colored words and spinners that
23
+ never animate.
24
+ 2. **Cost is a per-run attribute, not a competing ledger.** No package that
25
+ feels native adds a second persistent cost line. Dollar cost appears inside
26
+ the run's own result block and on demand (`/subagent-cost`-style command);
27
+ ambient surfaces show tokens/context-%, which Pi's footer doesn't cover.
28
+ 3. **Fixed-height, mutate-in-place progress.** Streaming blocks never grow and
29
+ never get replaced with differently-shaped text. Repaints are event-gated,
30
+ with a trailing flush so the last update always lands.
31
+ 4. **Three optional layers**: inline block (foreground), ambient widget
32
+ (background only — never double-render), notification on completion.
33
+ Overlay is for depth, not liveness.
34
+
35
+ ---
36
+
37
+ ## Phase 1 — Kill the duplicate cost footer (P0)
38
+
39
+ Files: `src/extension.ts` (`refreshFooter`), `src/ui.ts` (`FooterStatusModel`), `src/usage.ts`.
40
+
41
+ - `setStatus("subagent", …)` shrinks to a terse indicator, themed, only when
42
+ actionable:
43
+ - running: `⚙ 2 running` (`warning`)
44
+ - undelivered results: `· 1 ready` (`success`)
45
+ - nothing running/ready → `setStatus("subagent", undefined)`. Drop the
46
+ `hasUsage` keep-alive entirely.
47
+ - Delete `root $…` / `combined $…` / `subagents $…` from the footer. Pi's
48
+ native footer already shows session cost; child-run cost moves to:
49
+ - the run's own inline result block (`$0.0123` as one dim stat),
50
+ - a new **`/subagent-cost` command** printing parent/children/combined once,
51
+ in-flow (replaces the always-on ledger; reuses `buildUsageLedger`),
52
+ - the `/subagents` overlay header (already there),
53
+ - `status` tool output (unchanged — the model still needs it).
54
+ - Fix `FooterStatusModel.update()` to react to ready-count changes, not just
55
+ running-count. Remove the hardcoded width-160 render; a terse segment needs
56
+ no truncation.
57
+
58
+ ## Phase 2 — Native inline rendering (P0/P1)
59
+
60
+ Files: `src/format.ts`, `src/extension.ts` (renderCall/renderResult), `src/schema.ts`.
61
+
62
+ Adopt the Claude Code / tintinweb two-line collapsed block:
63
+
64
+ ```
65
+ renderCall (static, one line):
66
+ ▸ subagent Find auth middleware and summarize (toolTitle bold + muted preview)
67
+ ▸ subagent 3 parallel tasks
68
+
69
+ renderResult, running (isPartial, fixed height, mutates in place):
70
+ ⠹ ↻3 · 12.4k tok · 8s
71
+ ⎿ reading src/auth/middleware.ts…
72
+
73
+ renderResult, done:
74
+ ↻8 · 33.8k tok · $0.012 · 12.3s
75
+ ⎿ Done — Found 5 middleware call sites… (first line of final output)
76
+
77
+ renderResult, parallel — one block per task:
78
+ task-1 ↻5 · 21k tok · 9s ⎿ editing 2 files…
79
+ task-2 ↻3 · 12k tok · 7s ⎿ running tests…
80
+ ```
81
+
82
+ Concrete changes:
83
+
84
+ - **Add an optional `description` param** (short human label, 3–5 words) to the
85
+ tool schema; fall back to a truncated task preview. This is load-bearing for
86
+ every surface (inline header, widget rows, notifications, overlay list).
87
+ - **`renderCall` = exactly one line.** Delete the title + `Task:` + tasks-preview
88
+ block; delete the fake `expandHint()` literal and use
89
+ `keyHint("app.tools.expand", "to expand")` (only when collapsed).
90
+ - **`renderResult` rebuilt around `options.isPartial` and `context`:**
91
+ - reuse `context.lastComponent` (a `Text`) via `setText()` — stable row
92
+ identity, no per-frame Container/closure rebuild; delete `lineComponent`.
93
+ - partial view: stats line + one `⎿ activity` line derived from live text
94
+ tail. No spinner glyph of our own unless we drive it: keep a timer in
95
+ `context.state` bumping a frame + `context.invalidate()` at ~80–120 ms
96
+ while `isPartial`, disposed when the final render arrives.
97
+ - terminal view: stats (turns · tokens · $cost · fixed duration) + one-line
98
+ summary; `⎿ Stopped` / `⎿ Wrapped up (max turns)` / error text for
99
+ non-success. No ✓/✗ glyphs, no state-colored words — the shell shows it.
100
+ - expanded: final output rendered via `Markdown` + `getMarkdownTheme()`,
101
+ capped ~50 lines with a dim `… use wait/status for full output` trailer.
102
+ - **Delete from `format.ts`:** emoji profile icons (`🔍📋⚡` → single-cell
103
+ themed glyphs if kept at all), duplicated parallel task list in results,
104
+ unconditional elapsed line, `Date.now()`-based elapsed for finished tasks
105
+ (freeze duration at `endedAt - startedAt`).
106
+ - **Bug fix:** `String(wrapTextWithAnsi(...))` comma-join at format.ts:166-169
107
+ (use the returned `string[]` directly).
108
+
109
+ ## Phase 3 — Smooth streaming (P0)
110
+
111
+ Files: `src/extension.ts` (`streamUpdate`), `src/registry.ts` (already coalesced).
112
+
113
+ - Replace the drop-based 250 ms throttle with a **trailing-edge flush**:
114
+ non-structural updates schedule a deferred emit instead of being dropped, so
115
+ the final state of a burst always renders. Structural updates still flush
116
+ immediately.
117
+ - Stop baking time-varying text (`formatStatusPreview` with elapsed) into the
118
+ streamed `content`. Stream stable data in `details` (state, usage, live-text
119
+ tail, activity) and let `renderResult` compute elapsed at render time with
120
+ its `context.state` ticker.
121
+ - Separate LLM-facing `content` (compact status string, updated rarely) from
122
+ render-facing `details` (updated often) so UI smoothness never churns model
123
+ context.
124
+
125
+ ## Phase 4 — Background runs: widget + notifications (P1)
126
+
127
+ Files: `src/extension.ts`, new `src/notifications.ts`.
128
+
129
+ - **Ambient widget** (`ctx.ui.setWidget("subagent", …)`, above editor),
130
+ **background runs only** (foreground already renders inline — avoids the
131
+ double-render bug tintinweb hit):
132
+
133
+ ```
134
+ ● Subagents
135
+ ├─ ⠹ review Audit dependency licenses · ↻4 · 18k tok · 41s
136
+ └─ 1 queued
137
+ ```
138
+
139
+ Cleared when no background runs. Config: `widget: "background" | "off"`.
140
+ - **Completion notifications** via `pi.sendMessage(…, { deliverAs: "followUp",
141
+ triggerTurn: true })` + `registerMessageRenderer`: themed compact box for the
142
+ human (state, stats, first-line preview, artifact/session pointers),
143
+ structured payload for the model. Replaces the bare `ctx.ui.notify` toast.
144
+ - Smart-join: completions within a short window group into one notification;
145
+ failures bypass batching and flush immediately.
146
+ - Consumption suppression: a `wait` that already delivered the result
147
+ suppresses the redundant notification (short hold + delivered flag — we
148
+ already track `delivered`).
149
+
150
+ ## Phase 5 — Overlay polish (P2)
151
+
152
+ Files: `src/ui.ts`.
153
+
154
+ - Rebuild the list on `SelectList` + `DynamicBorder` with injected
155
+ `keybindings` instead of hand-rolled j/k/q handling and the custom `▶`
156
+ prefix.
157
+ - Detail view: render transcript/final output through `Markdown`; themed
158
+ section headers; keep scroll.
159
+ - Header keeps the full cost ledger (this is the right home for it).
160
+ - Stretch (only if demanded): live-follow transcript viewer for a running
161
+ child, reading the child session `.jsonl` we already know the path to.
162
+
163
+ ## Sequencing & risk
164
+
165
+ | Phase | Effort | Risk | User-visible win |
166
+ |-------|--------|------|------------------|
167
+ | 1 footer | S | low | removes the #1 complaint immediately |
168
+ | 3 streaming | S | low | fixes stutter; independent of visual redesign |
169
+ | 2 inline | M | medium (renderer rewrite + tests) | fixes "ugly" |
170
+ | 4 widget/notifications | M | medium (new message type) | background UX |
171
+ | 5 overlay | M | low | polish |
172
+
173
+ Phases 1+3 first (small, independent, biggest complaints), then 2, then 4/5.
174
+
175
+ Testing: extend `tests/format.test.ts` for the new block layouts (collapsed /
176
+ partial / terminal / parallel), add renderer identity tests (same component
177
+ instance reused across partial renders), and a streaming test asserting the
178
+ trailing flush delivers the last update of a burst.
179
+
180
+ ## Anti-goals
181
+
182
+ - No second persistent cost surface anywhere.
183
+ - No growing inline blocks; no appending streamed output to history.
184
+ - No custom expand keybinding; Ctrl+O (`app.tools.expand`) only.
185
+ - No per-completion notification spam in fanouts.
186
+ - Never show raw un-themed strings in any surface.
package/docs/UX.md ADDED
@@ -0,0 +1,141 @@
1
+ # pi-subagent UX
2
+
3
+ ## Overview
4
+ The standalone pi-subagent provides rich TUI support for monitoring, inspecting, and interacting with isolated subagent runs (single and parallel modes): inline streaming blocks, a terse footer, an ambient widget for background runs, batched completion notifications, mid-run steering, a worktree apply loop, and the `/subagents` inspector. UI logic is kept independent from `runner`/`registry` via small structural adapters (`SubagentAdapter`).
5
+
6
+ ## Design principles
7
+
8
+ 1. **Pi's tool shell owns state signaling.** The Box wrapper paints
9
+ `toolPendingBg` / `toolSuccessBg` / `toolErrorBg`, so inline blocks do not
10
+ repeat state words or draw their own success/error framing.
11
+ 2. **Cost is a per-run attribute, not a competing ledger.** Dollar cost appears
12
+ inside the run's own result block; the parent/children/combined ledger is
13
+ available on demand via `/subagent-cost`, in `status` tool output, and in the
14
+ `/subagents` overlay header. The footer never shows cost.
15
+ 3. **Fixed-height, mutate-in-place progress.** Streaming blocks keep a stable
16
+ shape (stats line + one `⎿ activity` line; parallel adds one line per task)
17
+ and the same component identity is reused across partial renders.
18
+ 4. **Trailing-edge streaming flush.** Structural updates (state transition, new
19
+ session id, billed turn) emit immediately; live-text bursts coalesce with a
20
+ deferred flush so the last update of a burst always lands.
21
+
22
+ ## Surfaces
23
+
24
+ ### Inline tool block (foreground runs)
25
+ - `renderCall` is exactly one line: `subagent <task preview>` (or
26
+ `N parallel tasks — first task…`, `wait a1b2c3d4`, `… · background`).
27
+ - `renderResult` while streaming (fixed shape, spinner animates via wall-clock
28
+ frame; Pi's working indicator drives repaints):
29
+ ```
30
+ ⠹ ↻3 · 12.4k tok · 8s
31
+ ⎿ reading src/auth/middleware.ts…
32
+ ```
33
+ - Terminal single run:
34
+ ```
35
+ ↻8 · 33.8k tok · $0.012 · 12s
36
+ ⎿ Found 5 middleware call sites…
37
+ → /tmp/report.md
38
+ ```
39
+ - Parallel: one line per task with a themed state glyph
40
+ (`◌ queued · ⠹ running · ✓ done · ✗ failed · ◐ partial · − cancelled · ◷ timeout`),
41
+ per-task stats, and a one-line tail (live activity or first output line).
42
+ - Expanded (Ctrl+O / `app.tools.expand`): full task output capped with a dim
43
+ `… +N lines` trailer pointing at the artifact/child session.
44
+ - Durations freeze at `endedAt`; running durations tick at render time.
45
+ - Reliability annotations render inline: `[attempt 2]` during a retry,
46
+ `[stalled 2m]` while the stall watchdog is flagging silence, and
47
+ `◐ wrapped up` on budget-stopped runs that concluded gracefully.
48
+
49
+ ### Footer status
50
+ Terse and actionable only: `⚙ 2 running · 1 ready · /subagents`. Cleared when
51
+ nothing is running or ready. No cost — Pi's footer already shows session cost.
52
+
53
+ ### Ambient widget (background runs only)
54
+ An above-editor widget renders while `async: true` runs are live — foreground
55
+ runs already render inline as the tool result, so they never appear here
56
+ (avoids double-render):
57
+
58
+ ```
59
+ ● Subagents
60
+ ├─ ⠼ Audit deps · ↻4 · 18k tok · 41s
61
+ │ ⎿ checking license headers…
62
+ └─ ◌ License scan · 12s
63
+ ```
64
+
65
+ Cleared when the last background run settles. Spinner and elapsed animate on
66
+ a 250ms interval that exists only while background runs are live.
67
+
68
+ ### Completion notifications (background runs only)
69
+ When an async run reaches a terminal state, a `followUp` message (custom type
70
+ `subagent-completion`) notifies the parent LLM so it reacts without polling.
71
+ The human sees a themed compact box (state glyph, label, stats, one-line
72
+ preview, artifact pointers); the LLM sees plain text with run ids and a
73
+ `wait { id }` pointer.
74
+
75
+ - Successes within a short window batch into one message (no fanout spam);
76
+ failures bypass batching and flush immediately, carrying held successes.
77
+ - A `wait` that already delivered the run suppresses the redundant
78
+ notification (delivered-state is re-checked at flush time).
79
+
80
+ ### `/subagents` overlay
81
+ - Header: title + running/ready counters + full usage ledger + rule.
82
+ - List: two lines per run — glyph/id/state/stats, then the task preview.
83
+ Selection cursor `▶`, animated spinner for live runs.
84
+ - Detail: run stats, summary, then per-task sections (glyph, label,
85
+ model/profile/thinking, usage, pointers, transcript/final output/errors),
86
+ scrollable with ↑↓/j/k and PageUp/PageDown.
87
+ - Actions: `c` cancel, `s` steer (prompts for a message, injects it into the
88
+ running child), `d` dismiss, `r` resume, `o` output pointers, `a` apply a
89
+ finished run's changed worktree into the main checkout (confirm dialog),
90
+ `x` discard worktree + branch (confirm dialog), Enter drill-down,
91
+ Esc/b back, Esc/q close.
92
+ - Live transcript (`t` on a **running** run's detail): tails the child's
93
+ session file (`sessionDir/<…sessionId…>.jsonl`) on a 500ms poll while the
94
+ pane is visible — compact role/tool lines, auto-follow unless you scroll
95
+ up (which pauses follow). No RPC reads; hidden/finished runs never poll.
96
+ Missing file shows “waiting for child session…”. `s` steering still works
97
+ from the same pane so observe → steer stays on one surface.
98
+
99
+ ### `/subagent-cost`
100
+ Prints the root/subagents/combined ledger once, on demand.
101
+
102
+ ### Mid-run steering
103
+ Children run in Pi RPC mode, so their stdin stays open as a command channel.
104
+ `action: "steer"` (or `s` in the overlay) queues a message that is delivered
105
+ after the child's current assistant turn, before its next LLM call — course
106
+ correction without cancel + retry. Parallel runs steer one task via `index`.
107
+
108
+ ### Worktree loop
109
+ Finished runs with changed worktrees support `diff` / `apply` / `discard`
110
+ actions (tool) and `a` / `x` keys (overlay). `apply` lands the worktree's
111
+ combined patch (committed + uncommitted + untracked vs base) onto the main
112
+ checkout as **uncommitted working-tree changes** via `git apply --3way`; it
113
+ never commits and never deletes the worktree. `discard` is the explicit
114
+ cleanup step and always confirms first.
115
+
116
+ ### Parallel fan-in
117
+ `synthesis: "<instruction>"` on a parallel run spawns one read-only child
118
+ after all tasks settle that folds their outputs into a single brief, delivered
119
+ first in the result. Synthesis failures degrade silently to raw results.
120
+
121
+ ## States
122
+ - **Queued/Running**: spinner + live stats + activity tail from live text.
123
+ - **Completed/Partial/Failed/Cancelled/Timeout/Lost**: state glyph, frozen
124
+ duration, usage summary, output pointers; failures show the error message.
125
+ - **Delivered vs Undelivered**: footer/overlay track pending delivery.
126
+ - **Notification**: one per terminal transition to avoid spam.
127
+
128
+ ## Integration Notes
129
+ - Extension wires via `ctx.ui.custom((tui, theme, kb, done) => createSubagentsOverlay(tui, theme, adapter, done), {overlay: true})`.
130
+ - Adapter provides getActiveRuns/getCompletedRuns/cancelRun etc. without tight coupling.
131
+ - Inline renderers reuse `context.lastComponent` (a `LineBlock`) so the row keeps
132
+ a stable component identity across partial renders.
133
+ - Streamed tool updates separate LLM-facing `content` (compact status string)
134
+ from render-facing `details` (state, usage, live-text tail, run timing).
135
+ - Tests combine pure UI models with a headless Pi extension harness: format
136
+ helpers, block layouts (collapsed/streaming/terminal/parallel), navigation,
137
+ truncation (ANSI-safe via `visibleWidth`), ready state, lifecycle/disposal.
138
+ - Follows Pi TUI guidelines (render(width), handleInput, invalidate,
139
+ requestRender, dispose). The overlay owns a single animation interval.
140
+
141
+ See ARCHITECTURE.md for ownership boundaries. All rendering respects terminal width and ANSI safety.
@@ -0,0 +1 @@
1
+ export { default } from "../src/extension.js";