pi-crew 0.1.32 → 0.1.34

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 (45) hide show
  1. package/docs/architecture.md +3 -3
  2. package/docs/research-phase8-operator-experience-plan.md +819 -0
  3. package/docs/research-phase9-observability-reliability-plan.md +1190 -0
  4. package/docs/research-ui-optimization-plan.md +480 -0
  5. package/package.json +1 -1
  6. package/schema.json +14 -0
  7. package/src/config/config.ts +69 -0
  8. package/src/config/defaults.ts +7 -0
  9. package/src/extension/autonomous-policy.ts +56 -2
  10. package/src/extension/notification-router.ts +116 -0
  11. package/src/extension/notification-sink.ts +51 -0
  12. package/src/extension/register.ts +133 -35
  13. package/src/extension/registration/commands.ts +110 -3
  14. package/src/extension/registration/team-tool.ts +5 -2
  15. package/src/extension/registration/viewers.ts +3 -1
  16. package/src/extension/team-recommendation.ts +16 -8
  17. package/src/runtime/child-pi.ts +1 -0
  18. package/src/runtime/diagnostic-export.ts +107 -0
  19. package/src/runtime/pi-spawn.ts +4 -1
  20. package/src/runtime/task-packet.ts +11 -2
  21. package/src/runtime/task-runner/prompt-builder.ts +3 -0
  22. package/src/schema/config-schema.ts +11 -0
  23. package/src/ui/crew-widget.ts +350 -285
  24. package/src/ui/dashboard-panes/agents-pane.ts +25 -0
  25. package/src/ui/dashboard-panes/health-pane.ts +30 -0
  26. package/src/ui/dashboard-panes/mailbox-pane.ts +10 -0
  27. package/src/ui/dashboard-panes/progress-pane.ts +14 -0
  28. package/src/ui/dashboard-panes/transcript-pane.ts +10 -0
  29. package/src/ui/heartbeat-aggregator.ts +53 -0
  30. package/src/ui/keybinding-map.ts +92 -0
  31. package/src/ui/live-run-sidebar.ts +20 -8
  32. package/src/ui/overlays/agent-picker-overlay.ts +57 -0
  33. package/src/ui/overlays/confirm-overlay.ts +58 -0
  34. package/src/ui/overlays/mailbox-compose-overlay.ts +144 -0
  35. package/src/ui/overlays/mailbox-compose-preview.ts +63 -0
  36. package/src/ui/overlays/mailbox-detail-overlay.ts +122 -0
  37. package/src/ui/pi-ui-compat.ts +57 -0
  38. package/src/ui/powerbar-publisher.ts +128 -94
  39. package/src/ui/render-scheduler.ts +103 -0
  40. package/src/ui/run-action-dispatcher.ts +107 -0
  41. package/src/ui/run-dashboard.ts +418 -372
  42. package/src/ui/run-snapshot-cache.ts +359 -0
  43. package/src/ui/snapshot-types.ts +47 -0
  44. package/src/ui/transcript-cache.ts +94 -0
  45. package/src/ui/transcript-viewer.ts +316 -302
@@ -0,0 +1,480 @@
1
+ # Research: UI Optimization Plan
2
+
3
+ > Phase 7 plan derived from `parallel-research` run `team_20260429053958_6497405a`.
4
+ > Source artifacts:
5
+ > - `.crew/artifacts/team_20260429053958_6497405a/shared/research-summary.md`
6
+ > - `.crew/artifacts/team_20260429053958_6497405a/shared/04_synthesize.md`
7
+ > - `.crew/artifacts/team_20260429053958_6497405a/shared/01_discover.md`
8
+ > - `.crew/artifacts/team_20260429053958_6497405a/shared/02_explore-shard-1.md`
9
+ > - `.crew/artifacts/team_20260429053958_6497405a/shared/03_explore-shard-2.md`
10
+
11
+ ## Overview
12
+
13
+ pi-crew already exposes the runtime data needed for a strong TUI: manifests, `tasks.json`, `agents.json`, per-agent `status.json`, `events.jsonl`, `output.log`, transcripts, and durable mailbox state. The gaps are in the UI layer:
14
+
15
+ 1. Widget recreated on every timer tick (`crew-widget.ts:267-272`).
16
+ 2. Live signatures miss `progress / toolUses / usage / recent output` so cached lines stay stale.
17
+ 3. Multiple UI surfaces re-read the same files independently (no shared snapshot).
18
+ 4. `/team-dashboard` is static — only reload via key `r`.
19
+ 5. `transcript-viewer.ts` calls `readFileSync` inside `render()` on every paint.
20
+ 6. Mailbox API/runtime exists but no first-class panel/badges.
21
+ 7. Pi UI integration uses untyped private-like casts (`requestRender`, `setWorkingIndicator`).
22
+
23
+ The plan below sequences fixes for highest ROI and lowest risk first, lockdown the snapshot contract before refactoring surfaces, and defers anything depending on uncertain pi-mono compatibility.
24
+
25
+ ## Implementation Status
26
+
27
+ > Track status here. Use `[x]` for done, `[ ]` for pending, `[-]` for won't-do/deferred.
28
+
29
+ - [x] Phase 0 — Pi UI compatibility shim
30
+ - [x] Phase 1.A — Persistent widget instance
31
+ - [x] Phase 1.B — `RunUiSnapshot` + `RunSnapshotCache`
32
+ - [x] Phase 1.C — Freshness signatures (progress / tool / usage / mtimes)
33
+ - [x] Phase 2 — Refactor widget / sidebar / dashboard / powerbar onto snapshot
34
+ - [x] Phase 3.A — `/team-dashboard` live component
35
+ - [x] Phase 3.B — Dashboard panes (agents, progress, mailbox, transcript)
36
+ - [x] Phase 4.A — Transcript viewer cache (mtime/size keyed)
37
+ - [x] Phase 4.B — Transcript bounded-tail mode
38
+ - [x] Phase 5.A — Adaptive/coalesced render scheduler
39
+ - [x] Phase 5.B — Powerbar fallback strategy + docs
40
+ - [x] Phase 5.C — Performance tests (large runs / large transcripts)
41
+
42
+ ## Roadmap-Level Decisions
43
+
44
+ | Decision | Choice | Rationale |
45
+ |---|---|---|
46
+ | Snapshot contract before refactor | Lock `RunUiSnapshot` interface in Phase 1.B before any consumer refactor | Avoid concurrent rename/conflict in widget/sidebar/dashboard |
47
+ | Persistent widget independent of snapshot | Phase 1.A done before 1.B | Quick win, doesn't block snapshot work, removes biggest CPU/flicker churn |
48
+ | Compatibility shim placed first (Phase 0) | Centralize `requestRender / setStatus / custom / setWidget` casts in `src/ui/pi-ui-compat.ts` | Every later phase consumes it; avoids re-casting in each module |
49
+ | Transcript fix split (4.A then 4.B) | Cache + invalidate first, tail-mode second | Cache by `mtime+size` is S effort and removes blocking `readFileSync` per-render; tail mode is M-L and can land later |
50
+ | Event-driven refresh deferred to Phase 5.A | Subscribe `crew.run.* / crew.subagent.* / crew.mailbox.*` only after snapshot is stable | Avoids listener leak risk during rapid refactor |
51
+ | RPC mode | Best-effort, not first-class | RPC drops function widgets; we emit string fallback via shim |
52
+ | Powerbar | Always-fallback to `setStatus`/widget; document event contract | No confirmed pi-mono consumer found in research |
53
+ | Memory safety | LRU cap 8 active + 16 recent runs in snapshot cache | Prevent leak when user browses many runs |
54
+
55
+ ## Phase 0 — Pi UI Compatibility Shim
56
+
57
+ **Goal:** Eliminate ad-hoc `(ctx.ui as { requestRender?: ... })` casts; provide one typed entry-point per UI capability.
58
+
59
+ **Deliverables:**
60
+ - New file `src/ui/pi-ui-compat.ts` exporting:
61
+ - `requestRender(ctx)` — feature-detected.
62
+ - `setWorkingIndicator(ctx, opts?)` — feature-detected, no-op fallback.
63
+ - `setExtensionWidget(ctx, key, factory, options)` — wraps `setWidget`, accepts `{ persist?: boolean }` flag.
64
+ - `showCustom(ctx, ...)` — wraps `ctx.ui.custom` with overlay options.
65
+ - `setStatusFallback(ctx, key, lines, segment?)` — used when powerbar consumer is absent.
66
+ - Replace existing inline casts in `crew-widget.ts`, `register.ts`, `live-run-sidebar.ts`, `powerbar-publisher.ts`.
67
+
68
+ **Files affected:**
69
+ - `src/ui/pi-ui-compat.ts` (new)
70
+ - `src/ui/crew-widget.ts`
71
+ - `src/ui/live-run-sidebar.ts`
72
+ - `src/ui/powerbar-publisher.ts`
73
+ - `src/extension/register.ts`
74
+
75
+ **Tests:**
76
+ - Unit test asserting fallback when host lacks `requestRender` / `setWorkingIndicator`.
77
+ - Snapshot of cast removal via grep test (no `as { requestRender` left in `src/`).
78
+
79
+ **Effort:** S (0.5–1 day) · **Risk:** Low
80
+
81
+ ## Phase 1.A — Persistent Widget Instance
82
+
83
+ **Goal:** Stop calling `setWidget` every timer tick; only call when placement/visibility/key changes.
84
+
85
+ **Approach:**
86
+ - Extend `CrewWidgetState` with `lastPlacement: string`, `lastVisibility: "hidden" | "visible"`, `lastKey: string`.
87
+ - `updateCrewWidget` decides: if state matches and component instance exists → only invalidate via shim's `requestRender()`; do NOT call `setWidget`.
88
+ - Component reads `runs` lazily inside `render(width)` using existing `activeWidgetRuns` (later replaced by snapshot in Phase 2).
89
+
90
+ **Files affected:**
91
+ - `src/ui/crew-widget.ts`
92
+ - `src/extension/register.ts` (timer interval handler)
93
+
94
+ **Tests (unit):**
95
+ - `updateCrewWidget` called N times with unchanged placement → `setWidget` invoked exactly once (count via mock).
96
+ - Switching placement triggers exactly 1 additional `setWidget`.
97
+ - Hide/clear path still calls `setWidget(WIDGET_KEY, undefined, ...)`.
98
+
99
+ **Effort:** S–M (1 day) · **Risk:** Low
100
+
101
+ ## Phase 1.B — `RunUiSnapshot` + `RunSnapshotCache`
102
+
103
+ **Status:** Done in Wave 2 via `src/ui/snapshot-types.ts` and `src/ui/run-snapshot-cache.ts`.
104
+
105
+ **Goal:** Single read pass per run; share results across widget/sidebar/dashboard/powerbar.
106
+
107
+ **Locked interface (do not change without bumping plan):**
108
+
109
+ ```ts
110
+ export interface RunUiProgress {
111
+ total: number;
112
+ completed: number;
113
+ running: number;
114
+ failed: number;
115
+ queued: number;
116
+ }
117
+
118
+ export interface RunUiUsage {
119
+ tokensIn: number;
120
+ tokensOut: number;
121
+ toolUses: number;
122
+ }
123
+
124
+ export interface RunUiMailbox {
125
+ inboxUnread: number;
126
+ outboxPending: number;
127
+ needsAttention: number;
128
+ }
129
+
130
+ export interface RunUiSnapshot {
131
+ runId: string;
132
+ cwd: string;
133
+ fetchedAt: number;
134
+ signature: string; // stable hash; differs only when content changed
135
+ manifest: TeamRunManifest;
136
+ tasks: TeamTaskState[];
137
+ agents: CrewAgentRecord[];
138
+ progress: RunUiProgress;
139
+ usage: RunUiUsage;
140
+ mailbox: RunUiMailbox;
141
+ recentEvents: TeamEvent[]; // last N (config N=20)
142
+ recentOutputLines: string[]; // last N lines, capped at MAX_TAIL_BYTES
143
+ }
144
+
145
+ export interface RunSnapshotCache {
146
+ get(runId: string): RunUiSnapshot | undefined;
147
+ refresh(runId: string): RunUiSnapshot; // forces re-read
148
+ refreshIfStale(runId: string): RunUiSnapshot; // re-read only if mtime/size changed or TTL exceeded
149
+ invalidate(runId?: string): void; // invalidate one or all
150
+ snapshotsByKey(): Map<string, RunUiSnapshot>; // for dashboard list rendering
151
+ }
152
+ ```
153
+
154
+ **Cache rules:**
155
+ - Key by `runId`.
156
+ - Stored entry includes `tasksMtime`, `tasksSize`, `agentsMtime`, `agentsSize`, `manifestMtime`, `mailboxMtime`, `outputMtime`.
157
+ - TTL = 250ms (matches existing `crew-agent-records` reader cache).
158
+ - LRU: max 8 active + 16 recent entries; evict on insert beyond limit.
159
+ - All `JSON.parse` wrapped in `try/catch`; on parse fail return previous valid entry (never crash render).
160
+
161
+ **Files affected:**
162
+ - `src/ui/run-snapshot.ts` (new)
163
+ - `src/ui/run-snapshot-cache.ts` (new)
164
+ - `src/ui/snapshot-types.ts` (new — exported types)
165
+
166
+ **Tests (unit):**
167
+ - `refreshIfStale` returns same entry when mtimes unchanged.
168
+ - File rewrite changes `signature`.
169
+ - Parse error returns last valid snapshot, no throw.
170
+ - LRU eviction at boundary.
171
+
172
+ **Effort:** M–L (2–3 days) · **Risk:** Medium
173
+
174
+ ## Phase 1.C — Freshness Signatures
175
+
176
+ **Goal:** Make widget/sidebar invalidate when progress/tool/tokens/output change, not just status.
177
+
178
+ **Changes:**
179
+ - `CrewWidgetComponent.buildSignature` includes per-agent `progress.completed`, `progress.total`, `currentTool`, `usage.tokensOut`, `lastOutputMtime`.
180
+ - `LiveRunSidebar.buildSignature` similarly includes progress/tool/usage; add `mailbox.inboxUnread`.
181
+ - Signatures derived from `RunUiSnapshot.signature` once Phase 1.B is in.
182
+
183
+ **Files affected:**
184
+ - `src/ui/crew-widget.ts`
185
+ - `src/ui/live-run-sidebar.ts`
186
+
187
+ **Tests (unit):**
188
+ - Two snapshots with same status but different progress → different signatures.
189
+ - Mock progress event → render output line count/contents change.
190
+
191
+ **Effort:** S (0.5 day) · **Risk:** Low
192
+
193
+ ## Phase 2 — Refactor Surfaces onto Snapshot
194
+
195
+ **Status:** Done in Wave 2 for widget/sidebar/dashboard/powerbar, with fallback direct reads preserved when no cache is supplied.
196
+
197
+ **Goal:** Replace independent FS reads in widget / sidebar / dashboard / powerbar with `RunSnapshotCache`.
198
+
199
+ **Deliverables:**
200
+ - `crew-widget.ts` reads via `cache.refreshIfStale(runId)`.
201
+ - `live-run-sidebar.ts` same.
202
+ - `run-dashboard.ts` calls `cache.snapshotsByKey()` once per render.
203
+ - `powerbar-publisher.ts` derives segment text from snapshot.
204
+ - Remove direct `agentsFor`/`readTasks`/`readManifest` reads from UI modules.
205
+
206
+ **Files affected:**
207
+ - `src/ui/crew-widget.ts`
208
+ - `src/ui/live-run-sidebar.ts`
209
+ - `src/ui/run-dashboard.ts`
210
+ - `src/ui/powerbar-publisher.ts`
211
+
212
+ **Tests (unit):**
213
+ - One render of all four surfaces with N=10 runs triggers ≤ N cache reads (use spy).
214
+ - Snapshot reuse across surfaces in same tick (counter assert).
215
+
216
+ **Effort:** M (2 days) · **Risk:** Medium
217
+
218
+ ## Phase 3.A — Live `/team-dashboard`
219
+
220
+ **Goal:** Dashboard auto-refreshes while open, preserves selection, separates active vs recent runs.
221
+
222
+ **Changes:**
223
+ - Convert `RunDashboard` from one-shot render to TUI overlay component owning its own timer (250–1000ms adaptive).
224
+ - Internal state: `selectedRunId`, `activeTab`, `cachedSnapshots` (via `RunSnapshotCache`).
225
+ - Hotkey `r` no longer needed but kept as manual force-refresh.
226
+
227
+ **Files affected:**
228
+ - `src/ui/run-dashboard.ts`
229
+ - `src/extension/registration/commands.ts` (dashboard handler now overlay-based)
230
+
231
+ **Tests (unit + integration):**
232
+ - Component receives mocked snapshot updates → re-renders without losing `selectedRunId`.
233
+ - Active runs list updates when manifest status flips.
234
+
235
+ **Effort:** M (2 days) · **Risk:** Medium
236
+
237
+ ## Phase 3.B — Dashboard Panes (agents · progress · mailbox · transcript)
238
+
239
+ **Goal:** First-class panel/tabs surfacing data already in snapshot.
240
+
241
+ **Tabs:**
242
+ 1. **Agents** — table (agent · status · current tool · tokens · last activity).
243
+ 2. **Progress / Events** — last N events with role badge and timestamps.
244
+ 3. **Mailbox** — inbox unread, outbox pending, needs-attention; row actions: nudge/ack via existing `team-tool/api.ts` (`send-message`, `ack-message`).
245
+ 4. **Transcript / Output** — opens existing `DurableTranscriptViewer` (post Phase 4.A).
246
+
247
+ **Files affected:**
248
+ - `src/ui/run-dashboard.ts`
249
+ - `src/ui/dashboard-panes/` (new directory: agents-pane, progress-pane, mailbox-pane, transcript-pane)
250
+ - `src/extension/team-tool/api.ts` (no API change; UI calls existing `read-mailbox`, `send-message`, `ack-message`)
251
+
252
+ **Tests (unit):**
253
+ - Mailbox pane shows badge counts from snapshot.
254
+ - Pane switching preserves selection within pane.
255
+ - Action `ack` triggers API call once and refreshes snapshot.
256
+
257
+ **Effort:** M–L (3 days) · **Risk:** Medium
258
+
259
+ ## Phase 4.A — Transcript Viewer Cache
260
+
261
+ **Goal:** Stop blocking `readFileSync` inside `render()`; eliminate full-parse per paint.
262
+
263
+ **Changes:**
264
+ - New `TranscriptCacheEntry { path, mtime, size, lines, parsedAt }` keyed by `(runId, taskId)`.
265
+ - `readRunTranscript` consults cache; only re-reads if `mtime` or `size` changed.
266
+ - `DurableTranscriptViewer.render` reads `cache.lines`, never the disk directly.
267
+ - TTL 500ms safety net.
268
+
269
+ **Files affected:**
270
+ - `src/ui/transcript-viewer.ts`
271
+ - `src/ui/transcript-cache.ts` (new)
272
+
273
+ **Tests (unit):**
274
+ - Two consecutive renders with unchanged file → 1 disk read.
275
+ - File grow → new cached lines, signature changes.
276
+ - Parse failure preserves last good cache.
277
+
278
+ **Effort:** S (0.5 day) · **Risk:** Low
279
+
280
+ ## Phase 4.B — Bounded-Tail Mode
281
+
282
+ **Goal:** Default to last N bytes/events to keep latency bounded for large transcripts.
283
+
284
+ **Approach:**
285
+ - Default `maxTailBytes = 256 KB`.
286
+ - Tail strategy: `fs.statSync` → `fs.openSync` → read last N bytes → discard partial first line if file exceeds N.
287
+ - Add hotkey `f` to "load full transcript on demand"; show byte counter.
288
+ - Auto-scroll toggle (`a`) preserved.
289
+
290
+ **Files affected:**
291
+ - `src/ui/transcript-viewer.ts`
292
+ - `src/ui/transcript-cache.ts` (extend)
293
+
294
+ **Config:**
295
+ - `config.ui.transcriptTailBytes` (optional, default 262144).
296
+
297
+ **Tests (unit):**
298
+ - 1MB file → only ~256KB worth of lines parsed.
299
+ - Force-full mode loads everything.
300
+ - Tail re-aligns when first newline straddles boundary.
301
+
302
+ **Effort:** M (2 days) · **Risk:** Medium
303
+
304
+ ## Phase 5.A — Adaptive Render Scheduler
305
+
306
+ **Goal:** Replace fixed 1000ms timers with event-driven refresh + low-frequency fallback.
307
+
308
+ **Approach:**
309
+ - Single `RenderScheduler` listening on `pi.events` for `crew.run.*`, `crew.subagent.*`, `crew.mailbox.*`.
310
+ - On event → invalidate snapshot + `requestRender` (debounced 50–100ms via animation-frame analog).
311
+ - Fallback timer 750ms (reduced from 1000ms) only triggers if no event in window.
312
+ - All listeners disposed on extension unload + run completion.
313
+
314
+ **Files affected:**
315
+ - `src/ui/render-scheduler.ts` (new)
316
+ - `src/extension/register.ts` (replace `setInterval` block)
317
+
318
+ **Tests (unit):**
319
+ - Event burst coalesces to single `requestRender` within debounce window.
320
+ - Listeners removed after `dispose()` (counter on event emitter).
321
+ - Fallback timer fires only when no events in interval.
322
+
323
+ **Effort:** M (1.5 days) · **Risk:** Low–Medium
324
+
325
+ ## Phase 5.B — Powerbar Fallback Strategy
326
+
327
+ **Goal:** Don't depend on an external `powerbar:*` consumer.
328
+
329
+ **Changes:**
330
+ - Detect listener via `pi.events.listenerCount?.("powerbar:register-segment")`.
331
+ - If 0 listeners: emit AND mirror to `ctx.ui.setStatus("pi-crew", text)`.
332
+ - Document event contract in `docs/architecture.md`.
333
+
334
+ **Files affected:**
335
+ - `src/ui/powerbar-publisher.ts`
336
+ - `docs/architecture.md`
337
+
338
+ **Tests (unit):**
339
+ - No consumer → `setStatus` called.
340
+ - Consumer registered → only event emitted, no `setStatus`.
341
+
342
+ **Effort:** S–M (0.5–1 day) · **Risk:** Medium (depends on listener-count API availability)
343
+
344
+ ## Phase 5.C — Performance Tests
345
+
346
+ **Goal:** Catch regressions on large runs / transcripts.
347
+
348
+ **Suite:**
349
+ - 50 simulated runs, 200 events each → render dashboard, assert ≤ 50 disk reads / render cycle.
350
+ - 5MB transcript → tail mode reads ≤ 1MB, full mode allowed.
351
+ - 100 widget update calls without state change → ≤ 1 `setWidget` invocation.
352
+
353
+ **Files affected:**
354
+ - `test/integration/ui-performance.test.ts` (new)
355
+
356
+ **Effort:** M (1.5 days) · **Risk:** Low
357
+
358
+ ## Implementation Order
359
+
360
+ > Recommended: do quick wins (Phase 0, 1.A, 1.C, 4.A) in parallel as 4 small PRs before starting Phase 1.B (snapshot foundation).
361
+
362
+ ```
363
+ Wave 1 (parallel, all S effort):
364
+ [x] Phase 0 — Pi UI compat shim
365
+ [x] Phase 1.A — Persistent widget
366
+ [x] Phase 1.C — Freshness signatures (use ad-hoc fields until snapshot lands)
367
+ [x] Phase 4.A — Transcript cache
368
+
369
+ Wave 2 (sequential):
370
+ [x] Phase 1.B — RunUiSnapshot foundation
371
+ [x] Phase 2 — Refactor surfaces onto snapshot
372
+ [x] Phase 5.A — Adaptive render scheduler
373
+
374
+ Wave 3 (parallel after Wave 2):
375
+ [x] Phase 3.A — Live dashboard
376
+ [x] Phase 3.B — Dashboard panes
377
+ [x] Phase 4.B — Transcript tail mode
378
+
379
+ Wave 4 (cleanup):
380
+ [x] Phase 5.B — Powerbar fallback
381
+ [x] Phase 5.C — Perf tests
382
+ ```
383
+
384
+ ## Files Affected (grouped)
385
+
386
+ **New files:**
387
+ - `src/ui/pi-ui-compat.ts`
388
+ - `src/ui/run-snapshot.ts`
389
+ - `src/ui/run-snapshot-cache.ts`
390
+ - `src/ui/snapshot-types.ts`
391
+ - `src/ui/transcript-cache.ts`
392
+ - `src/ui/render-scheduler.ts`
393
+ - `src/ui/dashboard-panes/agents-pane.ts`
394
+ - `src/ui/dashboard-panes/progress-pane.ts`
395
+ - `src/ui/dashboard-panes/mailbox-pane.ts`
396
+ - `src/ui/dashboard-panes/transcript-pane.ts`
397
+ - `test/integration/ui-performance.test.ts`
398
+
399
+ **Modified files:**
400
+ - `src/ui/crew-widget.ts`
401
+ - `src/ui/live-run-sidebar.ts`
402
+ - `src/ui/run-dashboard.ts`
403
+ - `src/ui/powerbar-publisher.ts`
404
+ - `src/ui/transcript-viewer.ts`
405
+ - `src/extension/register.ts`
406
+ - `src/extension/registration/commands.ts`
407
+ - `docs/architecture.md`
408
+
409
+ **Read-only references:**
410
+ - `src/runtime/crew-agent-records.ts`
411
+ - `src/state/mailbox.ts`
412
+ - `src/extension/team-tool/api.ts`
413
+
414
+ ## Risk Assessment
415
+
416
+ | Risk | Phase | Likelihood | Impact | Mitigation |
417
+ |---|---|---|---|---|
418
+ | Snapshot cache memory leak with many runs | 1.B | Medium | High | LRU cap (8 active + 16 recent), eviction unit test |
419
+ | Race between `agents.json` rewrite and UI read | 1.B | Medium | Medium | `try/catch JSON.parse` + return last valid snapshot |
420
+ | Listener leak from event-driven refresh | 5.A | Medium | Medium | Centralize in `RenderScheduler.dispose()`, integration test counts listeners post-shutdown |
421
+ | Persistent widget breaks on placement change edge cases | 1.A | Low | Medium | Diff against `lastPlacement/lastKey/lastVisibility` triple |
422
+ | Transcript tail-mode misaligns at chunk boundary | 4.B | Medium | Low | Discard partial-first-line; unit test with files at `n*chunkSize ± 1` |
423
+ | Pi RPC mode silently drops widgets | 0/2 | High | Low | Shim falls back to `setStatus` string lines |
424
+ | Powerbar consumer never appears | 5.B | High | Low | Always emit + always set status fallback |
425
+ | `requestRender` removed in future pi-mono | 0 | Low | Medium | Compat shim already feature-detects |
426
+ | Snapshot signature collision (different state, same hash) | 1.B | Low | Medium | Include mtimes + sizes + counts in hash input |
427
+ | Test suite runtime grows from perf tests | 5.C | Medium | Low | Run perf separately via dedicated script when needed |
428
+ | Concurrent refactor of widget/sidebar/dashboard while contract evolves | 1.B → 2 | Medium | High | Lock interface in 1.B PR before opening Phase 2 PR |
429
+ | Mailbox pane spams renders on incoming messages | 3.B / 5.A | Medium | Low | Debounce via `RenderScheduler`, batch mailbox events |
430
+
431
+ ## Testing Strategy
432
+
433
+ **Unit (Wave 1):**
434
+ - Compat shim feature-detect fallback (Phase 0).
435
+ - `setWidget` called once per state change (Phase 1.A).
436
+ - Signature includes progress/tool/usage diff (Phase 1.C).
437
+ - Transcript cache reuses entry when mtime unchanged (Phase 4.A).
438
+
439
+ **Unit (Wave 2):**
440
+ - Snapshot cache: TTL, LRU, parse-error fallback, signature stability.
441
+ - Surface refactor: 4 surfaces share ≤ 1 read per run per tick.
442
+ - Scheduler: event coalesce, dispose, fallback timer.
443
+
444
+ **Unit (Wave 3):**
445
+ - Dashboard live refresh preserves selection.
446
+ - Pane switching state, mailbox badge counts, ack action.
447
+ - Tail-mode boundary alignment, force-full toggle.
448
+
449
+ **Integration:**
450
+ - 50-run dashboard render ≤ 50 disk reads (Phase 5.C).
451
+ - 5MB transcript tail ≤ 1MB read.
452
+ - Long-lived run (10 min simulated) without listener growth.
453
+
454
+ **Manual smoke:**
455
+ - Open `/team-dashboard`, switch panes, send mailbox message, ack from UI.
456
+ - Resize terminal, switch placement above/below editor.
457
+ - Reload extension; ensure all timers/listeners cleared.
458
+
459
+ **Regression baseline:**
460
+ - Existing 286 unit + 26 integration tests must remain green at every wave.
461
+ - Run `npm run typecheck && npm run test:unit && npm run test:integration` before each PR merge.
462
+
463
+ ## Open Questions
464
+
465
+ 1. **Powerbar consumer status** — is any pi-mono extension/host expected to consume `powerbar:*` events? (Decides Phase 5.B aggressiveness; default plan: always-fallback.)
466
+ 2. **Target scale** — how many concurrent runs / what max transcript size should we optimize for? Plan assumes 8 active runs and 256KB tail by default.
467
+ 3. **RPC mode priority** — must function widgets work in RPC, or is graceful string fallback acceptable? Plan assumes best-effort string fallback.
468
+ 4. **Phase 1.B contract freeze** — once the interface ships, downstream phases depend on it. Should we publish it as `RunUiSnapshotV1` and treat changes as breaking?
469
+
470
+ ## Effort Summary
471
+
472
+ | Wave | Phases | Effort | Dependency |
473
+ |---|---|---|---|
474
+ | 1 (parallel) | 0, 1.A, 1.C, 4.A | ~2.5 days total | None |
475
+ | 2 (sequential) | 1.B → 2 → 5.A | ~5.5 days | Wave 1 done |
476
+ | 3 (parallel) | 3.A, 3.B, 4.B | ~7 days | Wave 2 done |
477
+ | 4 (parallel) | 5.B, 5.C | ~3 days | Wave 3 done |
478
+ | **Total** | 12 phases | **~18 dev-days** | — |
479
+
480
+ > Quick-win path (Wave 1 only) delivers ~70% of perceived UI improvement (no flicker, fresh signatures, no transcript blocking) at <15% of total effort.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-crew",
3
- "version": "0.1.32",
3
+ "version": "0.1.34",
4
4
  "description": "Pi extension for coordinated AI teams, workflows, worktrees, and async task orchestration",
5
5
  "author": "baphuongna",
6
6
  "license": "MIT",
package/schema.json CHANGED
@@ -128,6 +128,7 @@
128
128
  "showModel": { "type": "boolean", "default": true, "description": "Show worker model attempts in dashboard agent rows." },
129
129
  "showTokens": { "type": "boolean", "description": "Show token usage in dashboard agent rows." },
130
130
  "showTools": { "type": "boolean", "description": "Show tool activity in dashboard agent rows." },
131
+ "transcriptTailBytes": { "type": "integer", "minimum": 1024, "default": 262144, "description": "Maximum transcript bytes to parse by default; use viewer hotkey f to load full content." },
131
132
  "mascotStyle": { "type": "string", "enum": ["cat", "armin"] },
132
133
  "mascotEffect": { "type": "string", "enum": ["random", "none", "typewriter", "scanline", "rain", "fade", "crt", "glitch", "dissolve"] }
133
134
  }
@@ -149,6 +150,19 @@
149
150
  "properties": {
150
151
  "enabled": { "type": "boolean", "default": true }
151
152
  }
153
+ },
154
+ "notifications": {
155
+ "type": "object",
156
+ "additionalProperties": false,
157
+ "description": "Operator notification routing, quiet-hours, batching, and JSONL sink settings.",
158
+ "properties": {
159
+ "enabled": { "type": "boolean", "default": true },
160
+ "severityFilter": { "type": "array", "items": { "type": "string", "enum": ["info", "warning", "error", "critical"] }, "default": ["warning", "error", "critical"] },
161
+ "dedupWindowMs": { "type": "integer", "minimum": 1000, "default": 30000 },
162
+ "batchWindowMs": { "type": "integer", "minimum": 0, "default": 0 },
163
+ "quietHours": { "type": "string", "pattern": "^\\d{2}:\\d{2}-\\d{2}:\\d{2}$", "description": "Local HH:MM-HH:MM quiet-hours range; supports cross-day ranges such as 22:00-07:00." },
164
+ "sinkRetentionDays": { "type": "integer", "minimum": 1, "maximum": 90, "default": 7 }
165
+ }
152
166
  }
153
167
  }
154
168
  }
@@ -64,6 +64,7 @@ export interface CrewUiConfig {
64
64
  showModel?: boolean;
65
65
  showTokens?: boolean;
66
66
  showTools?: boolean;
67
+ transcriptTailBytes?: number;
67
68
  mascotStyle?: "cat" | "armin";
68
69
  mascotEffect?: "random" | "none" | "typewriter" | "scanline" | "rain" | "fade" | "crt" | "glitch" | "dissolve";
69
70
  }
@@ -91,6 +92,17 @@ export interface CrewTelemetryConfig {
91
92
  enabled?: boolean;
92
93
  }
93
94
 
95
+ export type CrewNotificationSeverity = "info" | "warning" | "error" | "critical";
96
+
97
+ export interface CrewNotificationsConfig {
98
+ enabled?: boolean;
99
+ severityFilter?: CrewNotificationSeverity[];
100
+ dedupWindowMs?: number;
101
+ batchWindowMs?: number;
102
+ quietHours?: string;
103
+ sinkRetentionDays?: number;
104
+ }
105
+
94
106
  export interface PiTeamsConfig {
95
107
  asyncByDefault?: boolean;
96
108
  executeWorkers?: boolean;
@@ -104,6 +116,7 @@ export interface PiTeamsConfig {
104
116
  agents?: CrewAgentsConfig;
105
117
  tools?: CrewToolsConfig;
106
118
  telemetry?: CrewTelemetryConfig;
119
+ notifications?: CrewNotificationsConfig;
107
120
  ui?: CrewUiConfig;
108
121
  }
109
122
 
@@ -210,6 +223,24 @@ function mergeConfig(base: PiTeamsConfig, override: PiTeamsConfig): PiTeamsConfi
210
223
  },
211
224
  };
212
225
  }
226
+ if (base.tools || override.tools) {
227
+ merged.tools = {
228
+ ...(base.tools ?? {}),
229
+ ...withoutUndefined((override.tools ?? {}) as Record<string, unknown>),
230
+ };
231
+ }
232
+ if (base.telemetry || override.telemetry) {
233
+ merged.telemetry = {
234
+ ...(base.telemetry ?? {}),
235
+ ...withoutUndefined((override.telemetry ?? {}) as Record<string, unknown>),
236
+ };
237
+ }
238
+ if (base.notifications || override.notifications) {
239
+ merged.notifications = {
240
+ ...(base.notifications ?? {}),
241
+ ...withoutUndefined((override.notifications ?? {}) as Record<string, unknown>),
242
+ };
243
+ }
213
244
  if (merged.agents?.overrides && Object.keys(merged.agents.overrides).length === 0) delete merged.agents.overrides;
214
245
  return merged;
215
246
  }
@@ -386,6 +417,7 @@ function parseUiConfig(value: unknown): CrewUiConfig | undefined {
386
417
  showModel: parseWithSchema(Type.Boolean(), obj.showModel),
387
418
  showTokens: parseWithSchema(Type.Boolean(), obj.showTokens),
388
419
  showTools: parseWithSchema(Type.Boolean(), obj.showTools),
420
+ transcriptTailBytes: parsePositiveInteger(obj.transcriptTailBytes, 50 * 1024 * 1024),
389
421
  mascotStyle: parseWithSchema(Type.Union([Type.Literal("cat"), Type.Literal("armin")]), obj.mascotStyle),
390
422
  mascotEffect: parseWithSchema(Type.Union([Type.Literal("random"), Type.Literal("none"), Type.Literal("typewriter"), Type.Literal("scanline"), Type.Literal("rain"), Type.Literal("fade"), Type.Literal("crt"), Type.Literal("glitch"), Type.Literal("dissolve")]), obj.mascotEffect),
391
423
  };
@@ -409,6 +441,40 @@ function parseAgentsConfig(value: unknown): CrewAgentsConfig | undefined {
409
441
  return Object.values(agents).some((entry) => entry !== undefined) ? agents : undefined;
410
442
  }
411
443
 
444
+ function parseToolsConfig(value: unknown): CrewToolsConfig | undefined {
445
+ const obj = asRecord(value);
446
+ if (!obj) return undefined;
447
+ const tools: CrewToolsConfig = {
448
+ enableClaudeStyleAliases: parseWithSchema(Type.Boolean(), obj.enableClaudeStyleAliases),
449
+ enableSteer: parseWithSchema(Type.Boolean(), obj.enableSteer),
450
+ terminateOnForeground: parseWithSchema(Type.Boolean(), obj.terminateOnForeground),
451
+ };
452
+ return Object.values(tools).some((entry) => entry !== undefined) ? tools : undefined;
453
+ }
454
+
455
+ function parseTelemetryConfig(value: unknown): CrewTelemetryConfig | undefined {
456
+ const obj = asRecord(value);
457
+ if (!obj) return undefined;
458
+ const telemetry: CrewTelemetryConfig = {
459
+ enabled: parseWithSchema(Type.Boolean(), obj.enabled),
460
+ };
461
+ return Object.values(telemetry).some((entry) => entry !== undefined) ? telemetry : undefined;
462
+ }
463
+
464
+ function parseNotificationsConfig(value: unknown): CrewNotificationsConfig | undefined {
465
+ const obj = asRecord(value);
466
+ if (!obj) return undefined;
467
+ const notifications: CrewNotificationsConfig = {
468
+ enabled: parseWithSchema(Type.Boolean(), obj.enabled),
469
+ severityFilter: parseWithSchema(Type.Array(Type.Union([Type.Literal("info"), Type.Literal("warning"), Type.Literal("error"), Type.Literal("critical")])), obj.severityFilter),
470
+ dedupWindowMs: parsePositiveInteger(obj.dedupWindowMs, 24 * 60 * 60 * 1000),
471
+ batchWindowMs: parseWithSchema(Type.Integer({ minimum: 0, maximum: 60_000 }), obj.batchWindowMs),
472
+ quietHours: parseWithSchema(Type.String({ pattern: "^\\d{2}:\\d{2}-\\d{2}:\\d{2}$" }), obj.quietHours),
473
+ sinkRetentionDays: parsePositiveInteger(obj.sinkRetentionDays, 90),
474
+ };
475
+ return Object.values(notifications).some((entry) => entry !== undefined) ? notifications : undefined;
476
+ }
477
+
412
478
  export function parseConfig(raw: unknown): PiTeamsConfig {
413
479
  const obj = asRecord(raw);
414
480
  if (!obj) return {};
@@ -423,6 +489,9 @@ export function parseConfig(raw: unknown): PiTeamsConfig {
423
489
  control: parseControlConfig(obj.control),
424
490
  worktree: parseWorktreeConfig(obj.worktree),
425
491
  agents: parseAgentsConfig(obj.agents),
492
+ tools: parseToolsConfig(obj.tools),
493
+ telemetry: parseTelemetryConfig(obj.telemetry),
494
+ notifications: parseNotificationsConfig(obj.notifications),
426
495
  ui: parseUiConfig(obj.ui),
427
496
  };
428
497
  }
@@ -55,6 +55,13 @@ export const DEFAULT_UI = {
55
55
  widgetDefaultFrameMs: 1000,
56
56
  };
57
57
 
58
+ export const DEFAULT_NOTIFICATIONS = {
59
+ severityFilter: ["warning", "error", "critical"] as const,
60
+ dedupWindowMs: 30_000,
61
+ batchWindowMs: 0,
62
+ sinkRetentionDays: 7,
63
+ };
64
+
58
65
  export const DEFAULT_CACHE = {
59
66
  manifestMaxEntries: 64,
60
67
  };