@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.
- package/CHANGELOG.md +352 -0
- package/LICENSE +21 -0
- package/README.md +543 -0
- package/docs/ARCHITECTURE.md +125 -0
- package/docs/COST-ACCOUNTING.md +66 -0
- package/docs/PLAN.md +325 -0
- package/docs/RELEASING.md +32 -0
- package/docs/ROADMAP.md +252 -0
- package/docs/SECURITY.md +85 -0
- package/docs/UI-OVERHAUL.md +186 -0
- package/docs/UX.md +141 -0
- package/extensions/subagent.ts +1 -0
- package/package.json +58 -0
- package/skills/subagent/SKILL.md +103 -0
- package/src/agents.ts +285 -0
- package/src/backend.ts +146 -0
- package/src/backends/claude.ts +384 -0
- package/src/backends/codex.ts +330 -0
- package/src/backends/index.ts +26 -0
- package/src/backends/pi.ts +94 -0
- package/src/btw.ts +34 -0
- package/src/config.ts +254 -0
- package/src/distill.ts +222 -0
- package/src/extension.ts +1527 -0
- package/src/format.ts +365 -0
- package/src/index.ts +60 -0
- package/src/launch.ts +120 -0
- package/src/maintenance.ts +6 -0
- package/src/model-policy.ts +157 -0
- package/src/notifications.ts +106 -0
- package/src/orchestrator.ts +247 -0
- package/src/output.ts +124 -0
- package/src/persistence.ts +334 -0
- package/src/policy.ts +500 -0
- package/src/process-lock.ts +687 -0
- package/src/protocol.ts +290 -0
- package/src/registry.ts +632 -0
- package/src/runner.ts +850 -0
- package/src/schema.ts +166 -0
- package/src/semaphore.ts +123 -0
- package/src/structured.ts +169 -0
- package/src/transcript.ts +360 -0
- package/src/types.ts +197 -0
- package/src/ui.ts +545 -0
- package/src/usage.ts +274 -0
- package/src/worktree.ts +753 -0
package/docs/ROADMAP.md
ADDED
|
@@ -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.
|
package/docs/SECURITY.md
ADDED
|
@@ -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";
|