pi-crew 0.11.8 → 0.11.9
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 +75 -0
- package/dist/index.mjs +420 -420
- package/docs/usage.md +1 -1
- package/install.mjs +4 -1
- package/package.json +1 -1
- package/skills/real-test-pi-crew/SKILL.md +26 -0
- package/src/config/defaults.ts +8 -1
- package/src/extension/command-completions.ts +44 -22
- package/src/extension/registration/render-loop.ts +51 -21
- package/src/extension/team-tool/handle-settings.ts +4 -1
- package/src/ui/dashboard-panes/agents-pane.ts +3 -2
- package/src/ui/dashboard-panes/health-pane.ts +6 -1
- package/src/ui/dashboard-panes/mailbox-pane.ts +12 -7
- package/src/ui/dashboard-panes/plan-pane.ts +5 -2
- package/src/ui/dashboard-panes/progress-pane.ts +8 -2
- package/src/ui/live-conversation-overlay.ts +5 -3
- package/src/ui/live-duration.ts +5 -4
- package/src/ui/live-run-sidebar.ts +90 -20
- package/src/ui/overlays/mailbox-compose-overlay.ts +17 -1
- package/src/ui/run-action-dispatcher.ts +2 -1
- package/src/ui/run-dashboard.ts +62 -22
- package/src/ui/run-snapshot-cache.ts +32 -0
- package/src/ui/settings-overlay.ts +26 -2
- package/src/ui/tool-renderers/brief-mode.ts +2 -2
- package/src/ui/transcript-viewer.ts +4 -1
- package/src/ui/widget/index.ts +10 -2
- package/src/ui/widget/task-list.ts +12 -4
- package/src/ui/widget/widget-model.ts +5 -4
- package/src/ui/widget/widget-renderer.ts +164 -38
- package/src/utils/socket-path.ts +48 -8
package/docs/usage.md
CHANGED
package/install.mjs
CHANGED
|
@@ -54,7 +54,10 @@ export const defaultConfig = {
|
|
|
54
54
|
widgetPlacement: "bottom",
|
|
55
55
|
widgetMaxLines: 8,
|
|
56
56
|
powerbar: true,
|
|
57
|
-
|
|
57
|
+
// L3 (2026-10-07 real-test review): tracks DEFAULT_UI.dashboardPlacement —
|
|
58
|
+
// enforced by test/unit/install-defaults-sync.test.ts (mutation: edit
|
|
59
|
+
// DEFAULT_UI without this file and that test goes red).
|
|
60
|
+
dashboardPlacement: "right",
|
|
58
61
|
dashboardWidth: 72,
|
|
59
62
|
dashboardLiveRefreshMs: 1000,
|
|
60
63
|
autoOpenDashboard: false,
|
package/package.json
CHANGED
|
@@ -8,6 +8,10 @@ description: >
|
|
|
8
8
|
|
|
9
9
|
origin: pi-crew
|
|
10
10
|
triggers:
|
|
11
|
+
- "pi crashed"
|
|
12
|
+
- "tmux server died"
|
|
13
|
+
- "crashes.json"
|
|
14
|
+
- "completion popup"
|
|
11
15
|
- "test the change"
|
|
12
16
|
- "verify it works"
|
|
13
17
|
- "is it really working"
|
|
@@ -105,6 +109,7 @@ End-to-end verification discipline for pi-crew changes. Distilled from the broke
|
|
|
105
109
|
| Đổi… | Tier bắt buộc (thứ tự) |
|
|
106
110
|
|---|---|
|
|
107
111
|
| `src/runtime/broker/` | T1 → T7 → T9b-W |
|
|
112
|
+
| `src/extension/command-completions.ts` / `src/extension/registration/commands/*.ts` (autocomplete providers + slash-command handlers) | T1 + pinned `test/unit/extension/core/command-completions*.test.ts` → **T5 completion-popup probe**: trong tmux session thật, gõ `/team-transcript <runId> ` + 1 ký tự nữa cho popup fire — assert (a) pi CÒN SỐNG, (b) gợi ý task id hiện với runId hợp lệ, (c) KHÔNG crash entry mới trong `~/.pi/agent/crashes.json`. Completion callbacks chạy trong `CombinedAutocompleteProvider.getSuggestions` của pi core — nơi KHÔNG catch lỗi extension: một throw = chết cả pi process (P0 live-caught 2026-10-08: `Invalid runId: team_x/team-transcript`, crashes.json có full stack) |
|
|
108
113
|
| `src/ui/` | T1 → (T5|T6) → T13 |
|
|
109
114
|
| `src/config/` (kể cả migration-validator) | T1 → T11c |
|
|
110
115
|
| `src/runtime/child-pi/` (spawn/kill/steer) | T7 → T9g |
|
|
@@ -146,6 +151,7 @@ End-to-end verification discipline for pi-crew changes. Distilled from the broke
|
|
|
146
151
|
|
|
147
152
|
> **2026-10-01 update (SDD-2 W-B — G2 MCP role-gate + G3 spill-always + Phase D bump 0.99.1, commits `66f37875`→`f0b343ed`, execution record §13 `pi-crew-sdd-2026-09-30-buoi1.md`)**: (1) **G2 flips full-loadout sanity semantics** — MCP presence in workers is now ROLE- and PATH-dependent: the live-session SDK path strips MCP (`DefaultResourceLoader({ extensionsOverride: stripMcpExtensions })`) cho read-only roles (`mcpPermittedForRole` — explorer/reviewer/security-reviewer/analyst/critic/planner + unknown default-deny); a read-only worker WITHOUT MCP is CORRECT, a write-role worker without MCP is a regression; nếu strip không arm được (SDK hết export `DefaultResourceLoader`…) → fail-open + warning event `task.mcp_enforcement_degraded` (registry 168 → 169, `check:event-types --enforce` clean). CLI child-process path CHƯA cover (WI-4 defer: `~/.pi/agent/mcp.json` trống + `-ne` giết provider `oc-go` — read-only roles vẫn discover adapter MCP trên đường đó). (2) **G3 spill-always**: MỌI task text qua `task.md` 0600 — argv worker KHÔNG bao giờ chứa task text; probe đọc argv phải expect `@<temp>/task.md`, task ngắn mất prefix `Task: `; `TASK_ARG_LIMIT` đã xóa. (3) Phase D bump `@earendil-works/*` → 0.99.x: SDK bỏ option `enableMCP`, tool seam cần `ExtensionToolContext` (export mới `src/extension/pi-api.ts`). Decision table thêm row `mcp-proxy.ts`/`model/pi-args.ts`; D5 loadout line refs dời `:283-330` → `:280-336`.
|
|
148
153
|
> **2026-10-07 update (0.11.6 → 0.11.7/0.11.8-pre stack, GH#61/#62 + pi 1.0.4 adoption + RPC hardening, HEAD `11e717f5`)**: (1) **GH#61 (0.11.6)** — directory `pi.extensions` entries được normalize sang entry-file identity (`resolveExtensionDirEntry`): child worker không còn double-load directory extension (`Tool "…" conflicts … exit 1` — CHANGELOG 0.11.6 block, `CHANGELOG.md:207`). (2) **pi 1.0.4 adoption** — devDeps `@earendil-works/*` ^1.0.4 (`package.json:153-156`; peerDeps `:136-139` giữ `*`), host pi 1.0.4. (3) **U2-lite MCP parity cut** — parity spawns fold `mcp__*` vào disallow list trên MỌI host (`pi-args.ts:414`) + `--no-mcp` khi host ≥ `PI_NO_MCP_FLOOR="1.0.4"` (`pi-version.ts:76`, gate `pi-args.ts:430`); hermetic spawns MCP-clean qua `--no-extensions` — probe loadout: worker KHÔNG thấy `mcp__*` là ĐÚNG trên cả parity lẫn hermetic path (verdict `docs/reviews/pi-1.0.4-upgrade-notes-2026-10-06.md`). (4) **RPC transport wired (W7)** — `PI_CREW_WORKER_TRANSPORT=rpc` chọn RPC transport tại run-worker seam (`runWorker`, `src/runtime/run-worker.ts:138`; env names `src/runtime/rpc/rpc-worker.ts:43-49`), precedence **env > `runtime.workerTransport` config > stdio**, invalid → stdio (`config-schema.ts:157`); probe recipe = T7 smoke với `PI_CREW_WORKER_TRANSPORT=rpc`; DR1/DR5 exit-race hardening + `PI_CREW_RPC_TURN_TIMEOUT_MS` (default 600_000; dialog policy `PI_CREW_RPC_DIALOG_ANSWER` default `cancel`) — suite `run-worker-rpc-seam.test.ts` (8) có 3 scenario MỚI: DR1 exit-race `:223`, DR5 started-then-fail KHÔNG fallback `:266`, DR1 turn-timeout `:298` (+ truth table `:328`); pin kèm `test/unit/runtime/rpc/rpc-worker.test.ts` (10). RPC vẫn EXPERIMENTAL — fail trước agent output → stdio fallback (structured warn). (5) **Classifier seam DORMANT (P2-1)** — default OFF (`config-schema.ts:95`; `resolveClassifierEnabled`: env `PI_CREW_CLASSIFIER_ENABLED` > config > FALSE, `classifier-service.ts:158`); battery KHÔNG dính trừ khi set `PI_CREW_CLASSIFIER_ENABLED=1` (+ `PI_CREW_CLASSIFIER_MODEL`, default `opencode/jev-1.13-free`) — unit-only (`classifier-service.test.ts` 14 + `child-executor-retry-triage.test.ts` 6), KHÔNG live tier. (6) **Pre-push hook (D3)** — `.githooks/pre-push` qua `git config core.hooksPath .githooks` (wired `package.json` `prepare` → `scripts/install-hooks.mjs`): MỌI push chạy `check:conflict-markers` + typecheck; push `refs/heads/main` TỰ chạy FULL `test:unit` (~13-14 phút) rồi mới in `[pre-push] gates green.`; bypass `git push --no-verify` CHỈ khẩn cấp (CI vẫn gate main). (7) **GH#62 foreground completion** (`62c9ed0d`→`51adb1e1`→`3ed0067e`→`c956b2d4`→`af462d7b`) — foreground-run completion path sống sót qua spread-copy contexts; suite `test/unit/extension/registration/foreground-run-completion.test.ts` (4 tests). (8) **hermeticWorkers DEFAULT ON** (`config-schema.ts:89`; env `PI_CREW_HERMETIC_WORKERS`; `resolveHermeticWorkers` `pi-args.ts:149` — `explicit ?? true`). (9) **U9 version-floor wiring** — suites `test/unit/runtime/child-pi/pi-version.test.ts` (11) + `test/unit/runtime/model/pi-args-version-floor.test.ts` (7) + `test/unit/runtime/child-pi/pi-version-floor-wiring.test.ts` (2). (10) **RT-11 lesson (`48cc088d`)** — static spawn-site scan (`child-pi-spawn-registration.test.ts:33`) đếm CẢ prose trong comment (`spawn (`); khi dash vì comment → **reword comment, KHÔNG relax test**. Suite anchors @ 2026-10-07: unit **8711 tests / 8707 pass / 1 flaky-under-load / 3 skipped** (968 files, ~13 min; flaky = `team-tool-resume-liveness` F1 liveness race — 6/6 pass isolated ×3, KHÔNG phải regression), integration **139 / 135 pass / 4 skipped** (exit 0 — CHỈ khi scrub ĐỦ `PI_CREW_*`/`PI_TEAMS_*`, xem Failure-symptoms row MAX_DEPTH), `test:critical` **120** (~19-20s), bundle **1620.1 KB** (1,658,985 B) md5 `20544cf9ea436c69b0b434462def91b9` (`--committed-hash` OK), event registry **176 registered / 141 emitted** (`check:event-types --enforce` clean), devDeps `@earendil-works/*` ^1.0.4 / host pi **1.0.4**.
|
|
154
|
+
> **2026-10-08 update (L-wave UI instability review + L10 plan flicker + P0 completion crash, commits `28e0d58d`+`c068fc4a`+`c89c759a`, report `docs/real-test/reports/real-test-2026-10-07-ui-instability-review.md` + fix-wave addenda)**: (1) **P0 — completion throw giết cả pi**: `/team-transcript` popup race làm input dính `/<cmd>` suffix vào runId token → `assertSafePathId` throw bên trong `CombinedAutocompleteProvider.getSuggestions` (pi core KHÔNG catch lỗi extension) → uncaught_exception chết cả process. Fix: MỌI completion provider (`suggestTaskIds`, `suggestRunIds`…) try/catch → null; pin test + mutation. **Evidence source mới: `~/.pi/agent/crashes.json`** — khi pi/tmux chết bất ngờ giữa probe, đọc file này TRƯỚC khi đoán (có kind/message/stack qua dist). Decision table có row mới cho command-completions. (2) **L10 — plan card flicker**: tasks variant không được paint spinner động (comment thiết kế của chính nó); signature phải strip `:spin=`. Dock giữ đúng MỘT dòng spinner (chủ ý). (3) **Probe-hygiene lessons** (đều tốn điều tra giả session này): tmux `new-session -t <sess>` tạo GROUPED session (cùng process — vô dụng cho bystander probe; phải `new-session -d -s <tên> "cd … && exec pi"`); `pkill -f <pattern>` từ bash của assistant TỰ MATCH command line của chính nó (chuỗi literal nằm trong args/heredoc — bracket-trick không đủ; kill bằng PID hoặc tên script riêng); so sánh frame theo row-index bị transcript-scroll làm nhiễu — so BLOCK theo marker `┏ NAME ▸`…`┗`, và dock có HAI dạng render (`┏ CREW ▸` mở rộng / `┃ CREW ▸` thu gọn — marker phải match cả hai, từng gây báo động giả "dock biến mất"); dispatch sync run khi có run khác đang chạy → assistant có thể join/wait run cũ thay vì tạo run mới (luôn verify runId mới trong `.crew/state/runs/` trước khi probe). (4) **Load-flake rule**: `test:critical` đỏ 1 suite lạ + treo isolation dưới load 82+ (build song song của user) = starvation, KHÔNG phải regression — check `uptime` TRƯỚC, re-run khi máy êm (session này: symlink-steering treo dưới load 82, 120/120 xanh khi load 0.8). Suite anchors @ 2026-10-08: `test:critical` **120**, bundle **1622.6 KB** md5 `4f76457497bd20779e298cee4d8d982e`.
|
|
149
155
|
> **2026-09-21 update (F-L2 — steer boot-window loss, found by the live feature battery)**: a steer written to `<artifactsRoot>/steering/<taskId>.jsonl` while the worker extension is still LOADING was silently lost forever — `registerPiTeamsPromptRuntime` starts the file poll at load time, pi's loader gates action methods until bind (`sendMessage: notInitialized` THROWS), and the old poll advanced `lastOffset` to `stat.size` BEFORE the line loop while its per-line catch swallowed the throw as "malformed line" (run `team_20260921035840`, root-caused by watcher-driven bisection probes R7-R9: +0.65s post-boot = lost, +2.5s+ = delivered). Fix (`src/prompt/prompt-runtime.ts`): walk the buffer by byte offset, advance only past terminal verdicts (delivered/non-steer/rejected/malformed), rewind to the failing line on a DELIVERY failure, `createSeenSteerIdSet.unmark()` releases the dedup id so the retry is not skipped. Regression suite `test/unit/prompt/prompt-runtime-steer-boot-window.test.ts` (4 tests) — mutation-checked both layers (offset-revert → 4 fail; unmark-removal → exactly the id-bearing test fails). Note: `dist/index.mjs` is UNCHANGED by this fix — child workers load prompt-runtime from the SOURCE path (`pi-args.ts PROMPT_RUNTIME_EXTENSION_PATH`), so the fix is live without a bundle bump.
|
|
150
156
|
>
|
|
151
157
|
> **F-L2 live re-verification (post-restart, 2026-09-21 afternoon)** — two independent proofs: (a) **lab**: pre-write the steer, then `PI_CREW_STEERING_FILE=<file> pi --extension <repo>/src/prompt/prompt-runtime.ts -p '<long task>'` — stderr shows `[pi-crew:prompt-runtime.steer-delivery-failed] Extension runtime not initialized…` ×2 (pre-bind ticks now LOGGED instead of swallowed) then the post-bind tick delivers and the model complies; (b) **real team run**: watcher writes at **+0.51s** after the steering dir appears (the previously-lost window) → worker session log gains the `custom_message` and the probe token lands in `results/<taskId>.txt`. Suite anchors @ 2026-09-21 (post-fix): unit **8006 tests / 8003 pass / 0 fail / 3 skipped** (875 files; 1088s under load), integration **134 / 130 pass / 4 skipped**, `test:critical` **116**, bundle **3360.1 KB** md5 `2f45df91ed0580ee5bb5b9b57a8a8c3f` (UNCHANGED — see the source-path note above).
|
|
@@ -408,6 +414,20 @@ tmux capture-pane -t pi -p > /tmp/screen-after-up.txt
|
|
|
408
414
|
| App-cursor-mode | `\x1bOA` | `\x1bOB` | vim, less, full-screen apps |
|
|
409
415
|
| Kitty protocol | `\x1b[1;2A` (Shift+Up) etc. | — | modern terminals (kitty, foot, ghostty) |
|
|
410
416
|
|
|
417
|
+
**Completion-popup probe (P0 2026-10-08 — dùng khi đụng `command-completions.ts` / slash-command handlers)**: completion callbacks chạy trong `CombinedAutocompleteProvider.getSuggestions` của pi core — nơi KHÔNG catch lỗi extension, một throw = chết cả pi process. Probe:
|
|
418
|
+
|
|
419
|
+
```bash
|
|
420
|
+
# (a) input từng gây crash: runId token dính suffix "/<cmd>" + trailing space cho popup fire
|
|
421
|
+
tmux -S /tmp/sock send-keys -t pi '/team-transcript team_20261008_xxx/team-transcript '
|
|
422
|
+
sleep 2.5; tmux -S /tmp/sock ls >/dev/null && echo ALIVE # phải sống
|
|
423
|
+
# crashes.json phải KHÔNG có entry mới (đọc ~/.pi/agent/crashes.json, lọc timestamp > thời điểm probe)
|
|
424
|
+
# (b) flow hợp lệ: runId thật + space + 1 ký tự → popup gợi ý task id
|
|
425
|
+
tmux -S /tmp/sock send-keys -t pi C-c
|
|
426
|
+
tmux -S /tmp/sock send-keys -t pi '/team-transcript <runId-thật> '
|
|
427
|
+
sleep 2; tmux -S /tmp/sock send-keys -t pi '0'; sleep 2
|
|
428
|
+
tmux -S /tmp/sock capture-pane -t pi -p | grep -aE '0[123]_' | head -3 # kỳ vọng 01_explore/02_execute…
|
|
429
|
+
```
|
|
430
|
+
|
|
411
431
|
**References**:
|
|
412
432
|
|
|
413
433
|
| What | Where |
|
|
@@ -1144,6 +1164,12 @@ When a tier fails, the recovery is usually quick. Match the symptom to the cause
|
|
|
1144
1164
|
|
|
1145
1165
|
| Symptom | Likely cause | Recovery |
|
|
1146
1166
|
|---|---|---|
|
|
1167
|
+
| Pi process / tmux server chết bất ngờ giữa probe (capture-pane báo "no server running") | **Extension giết pi bằng uncaught exception** — pi core không catch lỗi extension trong autocomplete provider / async callback. P0 2026-10-08: `Invalid runId: team_x/team-transcript` từ `assertSafePathId` qua `suggestTaskIds` | Đọc `~/.pi/agent/crashes.json` TRƯỚC khi đoán gì khác — có `kind`/`message`/`stack` (symbol qua `dist/index.mjs` → map lại src). Session mới mở cũng hiện banner "pi crashed on …" kèm message. Fix pattern: callback phải try/catch → degrade, KHÔNG bao giờ throw ra ngoài |
|
|
1168
|
+
| `test:critical` đỏ đúng 1 suite lạ (broker/steering) + treo cả khi chạy isolated, trong khi xanh hoàn toàn vài giờ trước | **Starvation dưới load cao** (build song song, load 80+): broker handshake/timeout-based tests chết đói CPU, KHÔNG phải regression từ thay đổi UI — session 2026-10-08 đỏ symlink-steering dưới load 82, 120/120 xanh lại khi load 0.8 | `uptime` + `ps -eo pcpu --sort=-pcpu | head` TRƯỚC; đợi/re-run khi máy êm; chỉ chẩn đoán regression khi đỏ lại trên máy sạch |
|
|
1169
|
+
| Probe session thứ 2 (bystander) hiển thị y hệt session 1, keystroke session này ảnh hưởng session kia | `tmux new-session -t <sess>` tạo **GROUPED session** — cùng window/cùng process, không phải pi riêng | Bystander phải STANDALONE: `tmux -S <sock> new-session -d -x 150 -y 50 -s by "cd <cwd> && exec pi 2>&1"` (KHÔNG dùng `-t`); verify bằng 2 PID pi riêng trong `ps` |
|
|
1170
|
+
| Bash của assistant tự chết exit 143 ngay sau lệnh `pkill -f <pattern>` | **pkill tự match**: command line của chính bash chứa chuỗi pattern (trong heredoc/args) — bracket-trick `pat[.]tern` KHÔNG đủ khi chuỗi literal nằm trong tham số | Đặt tên script capture riêng (capV7.sh…), kill bằng `pkill -f loopV7` với tên KHÔNG xuất hiện trong lệnh hiện tại, hoặc kill theo PID ghi sẵn |
|
|
1171
|
+
| So sánh frame theo row-index cho hàng trăm "diff" giả (mọi row "đổi" liên tục) | **Transcript scroll**: output worker/stream làm mọi dòng dịch chuyển row — so row-index đếm toàn bộ là đổi | So BLOCK theo marker biên: `┏ PLAN`…`┗`, `┏ DASHBOARD`…, và LƯU Ý dock có 2 dạng: mở rộng `┏ CREW ▸` (có progress bar) / thu gọn `┃ CREW ▸` (1 dòng) — match cả hai, từng gây báo động giả "dock biến mất" |
|
|
1172
|
+
| Dispatch sync run nhưng không có run mới trong `.crew/state/runs/`, transcript nói "ran synchronously despite async=true" | Assistant/agent JOIN hoặc WAIT vào run đang chạy thay vì tạo run mới | Luôn verify runId mới xuất hiện (`ls -t .crew/state/runs/`) sau lệnh dispatch trước khi probe; dispatch khi không run nào active |
|
|
1147
1173
|
| `test:critical` returns `# fail N>0` | Regression in touched source | Read the failing test's name + assertion; fix the source; rerun |
|
|
1148
1174
|
| `test:critical` hangs >60s | One test opened a socket/pty that didn't close | Run individual file: `node --import tsx/esm --test --test-force-exit test/unit/<file>.test.ts`; check for missing `await` or unclosed handle |
|
|
1149
1175
|
| `typecheck` fails with `TS2xxx` | TS type drift after src/ edit | Fix the type error; do not commit until exit 0 |
|
package/src/config/defaults.ts
CHANGED
|
@@ -138,7 +138,14 @@ export const DEFAULT_UI = {
|
|
|
138
138
|
widgetMaxLines: 8,
|
|
139
139
|
inlinePanel: true,
|
|
140
140
|
powerbar: true,
|
|
141
|
-
|
|
141
|
+
// L3 (2026-10-07 real-test review): default "right" — the centered 90%
|
|
142
|
+
// overlay leaves both margins showing chopped background words (pi-tui's
|
|
143
|
+
// compositeTuiLine keeps base.before/base.after visible outside overlay
|
|
144
|
+
// bounds by design); the right-anchored panel covers them. Mirror updates:
|
|
145
|
+
// install.mjs + the two EFFECTIVE_DEFAULTS maps (handle-settings.ts,
|
|
146
|
+
// settings-overlay.ts) — install-defaults-sync.test.ts enforces the first,
|
|
147
|
+
// G17 lesson enforces the rest.
|
|
148
|
+
dashboardPlacement: "right" as const,
|
|
142
149
|
dashboardWidth: 72,
|
|
143
150
|
dashboardLiveRefreshMs: 1000,
|
|
144
151
|
autoOpenDashboard: false,
|
|
@@ -53,31 +53,53 @@ function statusIcon(status: TeamRunManifest["status"]): string {
|
|
|
53
53
|
* process cwd matches the session cwd.
|
|
54
54
|
*/
|
|
55
55
|
export function suggestRunIds(_prefix: string, cwd?: string): AutocompleteItem[] | null {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
56
|
+
// L-crash (2026-10-08): completion callbacks run inside pi's autocomplete
|
|
57
|
+
// provider, which does not catch extension errors — never throw here.
|
|
58
|
+
try {
|
|
59
|
+
const resolvedCwd = cwd ?? process.cwd();
|
|
60
|
+
const runs = listRecentRuns(resolvedCwd, MAX_RUN_SUGGESTIONS);
|
|
61
|
+
if (runs.length === 0) return null;
|
|
62
|
+
const items: AutocompleteItem[] = runs.map((run) => ({
|
|
63
|
+
value: run.runId,
|
|
64
|
+
label: run.runId,
|
|
65
|
+
description: `${statusIcon(run.status)} ${run.status} · ${run.team} · ${(run.goal ?? "").slice(0, 48)}`,
|
|
66
|
+
}));
|
|
67
|
+
return filterByPrefix(items, _prefix);
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
65
71
|
}
|
|
66
72
|
|
|
67
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* Suggest task IDs within a specific run (for /team-result <runId> <taskId>).
|
|
75
|
+
*
|
|
76
|
+
* L-crash (2026-10-08, P0): this runs inside pi's autocomplete provider
|
|
77
|
+
* (`CombinedAutocompleteProvider.getSuggestions`), which does NOT catch
|
|
78
|
+
* extension errors — a throw here kills the whole pi process (live-caught:
|
|
79
|
+
* uncaught_exception "Invalid runId: team_x/team-transcript" after the
|
|
80
|
+
* autocomplete popup raced typed input and glued a "/…" suffix onto the
|
|
81
|
+
* runId token; assertSafePathId correctly rejected it, but the throw
|
|
82
|
+
* escaped). Completion callbacks must NEVER throw: any failure -> null.
|
|
83
|
+
*/
|
|
68
84
|
export async function suggestTaskIds(runId: string, prefix: string, cwd?: string): Promise<AutocompleteItem[] | null> {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
85
|
+
try {
|
|
86
|
+
const resolvedCwd = cwd ?? process.cwd();
|
|
87
|
+
// Dynamic import to avoid pulling state-store into the hot command-registration path.
|
|
88
|
+
// LAZY: defer dynamic import of ../state/stores/state-store.ts to its call site.
|
|
89
|
+
const { loadRunManifestById } = await import("../state/stores/state-store.ts");
|
|
90
|
+
const loaded = loadRunManifestById(resolvedCwd, runId);
|
|
91
|
+
if (!loaded) return null;
|
|
92
|
+
const items: AutocompleteItem[] = loaded.tasks.map((task) => ({
|
|
93
|
+
value: task.id,
|
|
94
|
+
label: task.id,
|
|
95
|
+
description: `${task.status} · ${task.role} · ${task.title?.slice(0, 40) ?? ""}`,
|
|
96
|
+
}));
|
|
97
|
+
return filterByPrefix(items, prefix);
|
|
98
|
+
} catch {
|
|
99
|
+
// Invalid runId (path-unsafe chars), import/state-store/cache failure —
|
|
100
|
+
// degrade to "no suggestions" instead of crashing the host process.
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
81
103
|
}
|
|
82
104
|
|
|
83
105
|
/** Suggest available teams for /team-run <team>. */
|
|
@@ -319,12 +319,34 @@ export function setupRenderLoop(
|
|
|
319
319
|
);
|
|
320
320
|
}
|
|
321
321
|
};
|
|
322
|
+
// L2 (real-test 2026-10-07, report L2): rebuild-in-place, NEVER
|
|
323
|
+
// hard-delete. The health gates below used `invalidate(runId)`
|
|
324
|
+
// (entries.delete) on every tick for terminal/divergent runs — exactly
|
|
325
|
+
// what the FLICKER FIX invariant (run-snapshot-cache.ts scheduleRefresh
|
|
326
|
+
// docs + the onRunChange wiring above) forbids: for 1-n frames
|
|
327
|
+
// `snapshotCache.get(runId)` returned undefined and the widget/powerbar
|
|
328
|
+
// fell back to their disk-read branch around every run completion
|
|
329
|
+
// (visible flicker). Route through the coalesced ASYNC rebuild instead —
|
|
330
|
+
// the entry stays populated until the rebuild re-sets it in place. No
|
|
331
|
+
// renderScheduler.schedule() here: unlike the fs.watch onRunChange path
|
|
332
|
+
// we are ALREADY inside a renderTick, and list(20) keeps terminal runs
|
|
333
|
+
// in sessionManifests for many ticks, so a schedule() from the gates
|
|
334
|
+
// would reset lastEventAt on every tick and defeat the R1 idle stop
|
|
335
|
+
// (pinned by preload-idle-render.test.ts).
|
|
336
|
+
const refreshSnapshotInPlace = (runId: string): void => {
|
|
337
|
+
try {
|
|
338
|
+
snapshotCache.scheduleRefresh(runId);
|
|
339
|
+
} catch (error) {
|
|
340
|
+
logInternalError("register.renderLoop.snapshotRefresh", error, runId);
|
|
341
|
+
}
|
|
342
|
+
};
|
|
322
343
|
for (const run of sessionManifests) {
|
|
323
344
|
if (run.status !== "running") {
|
|
324
|
-
// GATE 1 — preloaded manifest says terminal.
|
|
325
|
-
// and clear previously-emitted health notifications
|
|
326
|
-
// stays clean (belt-and-suspenders with the FIX #1
|
|
327
|
-
|
|
345
|
+
// GATE 1 — preloaded manifest says terminal. Refresh the snapshot in
|
|
346
|
+
// place (never delete) and clear previously-emitted health notifications
|
|
347
|
+
// so the dashboard stays clean (belt-and-suspenders with the FIX #1
|
|
348
|
+
// fresh-read gate).
|
|
349
|
+
refreshSnapshotInPlace(run.runId);
|
|
328
350
|
clearHealthNotifications(run.runId);
|
|
329
351
|
continue;
|
|
330
352
|
}
|
|
@@ -333,20 +355,25 @@ export function setupRenderLoop(
|
|
|
333
355
|
// (from lastPreloadedManifests) can lag the on-disk terminal
|
|
334
356
|
// transition; the manifest cache has a 500ms TTL + file watcher so it
|
|
335
357
|
// is the source of truth. A terminal run must NEVER reach
|
|
336
|
-
// maybeNotifyHealth. Also
|
|
337
|
-
// previously-emitted health
|
|
358
|
+
// maybeNotifyHealth. Also refresh the stale snapshot in place
|
|
359
|
+
// (never delete) and clear any previously-emitted health
|
|
360
|
+
// notification for this run.
|
|
338
361
|
const freshManifest = ctx.getManifestCache(extensionCtx.cwd).get(run.runId);
|
|
339
362
|
if (freshManifest?.status !== "running") {
|
|
340
|
-
|
|
363
|
+
refreshSnapshotInPlace(run.runId);
|
|
341
364
|
clearHealthNotifications(run.runId);
|
|
342
365
|
continue;
|
|
343
366
|
}
|
|
344
|
-
|
|
367
|
+
// L5: paint-path read through the read-only accessor — cached
|
|
368
|
+
// snapshot only; a lapsed TTL schedules the coalesced async refresh
|
|
369
|
+
// instead of ever rebuilding synchronously inside renderTick.
|
|
370
|
+
const snapshot = snapshotCache.readForRender(run.runId);
|
|
345
371
|
if (!snapshot) continue;
|
|
346
372
|
if (snapshot.manifest.status !== "running") {
|
|
347
|
-
// GATE 2 — a running
|
|
348
|
-
// stale.
|
|
349
|
-
|
|
373
|
+
// GATE 2 — a running manifest paired with a now-terminal snapshot is
|
|
374
|
+
// stale. Refresh it in place so subsequent ticks get the fresh view,
|
|
375
|
+
// and clear.
|
|
376
|
+
refreshSnapshotInPlace(run.runId);
|
|
350
377
|
clearHealthNotifications(run.runId);
|
|
351
378
|
continue;
|
|
352
379
|
}
|
|
@@ -357,10 +384,10 @@ export function setupRenderLoop(
|
|
|
357
384
|
// worker counted as active-without-heartbeat and fired a false
|
|
358
385
|
// "dead worker" (live: team_20260923100114, 01_explore parked on
|
|
359
386
|
// ask). Overlay FRESH statuses before summarizing; divergence also
|
|
360
|
-
//
|
|
387
|
+
// schedules an in-place rebuild so the cache syncs off the tick.
|
|
361
388
|
const freshTasks = loadTasksWithRecovery(freshManifest.tasksPath, freshManifest.eventsPath, run.runId);
|
|
362
389
|
const overlaid = overlayFreshTaskStatuses(snapshot, freshTasks);
|
|
363
|
-
if (overlaid !== snapshot)
|
|
390
|
+
if (overlaid !== snapshot) refreshSnapshotInPlace(run.runId);
|
|
364
391
|
const summary = summarizeHeartbeats(overlaid, { now });
|
|
365
392
|
const fingerprint = healthNotifyFingerprint(summary, overlaid.tasks.length);
|
|
366
393
|
const maybeNotifyHealth = (kind: string, count: number, title: string, body: string): void => {
|
|
@@ -424,13 +451,16 @@ export function setupRenderLoop(
|
|
|
424
451
|
// The next `renderTick` then saw `get() === undefined` for every run,
|
|
425
452
|
// so `activeWidgetRuns` dropped them to "(loading…)" until the async
|
|
426
453
|
// preload rebuilt the cache — an endless visible flicker. For a
|
|
427
|
-
// specific runId we
|
|
428
|
-
//
|
|
429
|
-
//
|
|
430
|
-
//
|
|
454
|
+
// specific runId we schedule the coalesced ASYNC refresh (L5,
|
|
455
|
+
// real-test 2026-10-07): refreshIfStale() did a SYNC full rebuild
|
|
456
|
+
// between paints (and a sync build for missing entries), blocking the
|
|
457
|
+
// event loop mid-render; scheduleRefresh keeps the entry populated and
|
|
458
|
+
// rebuilds off the tick. A no-runId tick does nothing (renderTick
|
|
459
|
+
// itself repaints; the cache's own run:state/worker:lifecycle
|
|
460
|
+
// subscription refreshes affected runs).
|
|
431
461
|
if (!runId) return;
|
|
432
462
|
try {
|
|
433
|
-
ctx.getRunSnapshotCache(extensionCtx.cwd).
|
|
463
|
+
ctx.getRunSnapshotCache(extensionCtx.cwd).scheduleRefresh(runId);
|
|
434
464
|
} catch (error) {
|
|
435
465
|
logInternalError("register.renderScheduler.refresh", error, runId);
|
|
436
466
|
}
|
|
@@ -442,9 +472,9 @@ export function setupRenderLoop(
|
|
|
442
472
|
const unsubscribeRunEvents = runEventBus.onAny((event) => {
|
|
443
473
|
// bug-026 sub-issue C: evict terminal runs from the preloaded manifest
|
|
444
474
|
// frame so a stale "running" entry cannot persist for the session
|
|
445
|
-
// lifetime. Additive to the renderTick GATE 1 / FIX #1 / GATE 2
|
|
446
|
-
//
|
|
447
|
-
// touch lastPreloadedManifests itself.
|
|
475
|
+
// lifetime. Additive to the renderTick GATE 1 / FIX #1 / GATE 2
|
|
476
|
+
// snapshot refreshes (which rebuild the snapshot cache entries in
|
|
477
|
+
// place) — those gates do not touch lastPreloadedManifests itself.
|
|
448
478
|
lastPreloadedManifests = applyTerminalRunEventToManifests(lastPreloadedManifests, event);
|
|
449
479
|
sched.schedule({
|
|
450
480
|
runId: event.runId,
|
|
@@ -35,7 +35,10 @@ const EFFECTIVE_DEFAULTS: Record<string, unknown> = {
|
|
|
35
35
|
"ui.showModel": true,
|
|
36
36
|
"ui.showTokens": true,
|
|
37
37
|
"ui.showTools": true,
|
|
38
|
-
|
|
38
|
+
// L3 (2026-10-07): mirrors DEFAULT_UI.dashboardPlacement = "right"
|
|
39
|
+
// (defaults.ts) — G17: duplicated EFFECTIVE_DEFAULTS maps must track the
|
|
40
|
+
// canonical default or the settings overlay shows a stale value.
|
|
41
|
+
"ui.dashboardPlacement": "right",
|
|
39
42
|
"ui.dashboardWidth": 72,
|
|
40
43
|
"ui.autoOpenDashboard": false,
|
|
41
44
|
// G17 sync (2026-09-10 review): canonical DEFAULT_UI.widgetPlacement =
|
|
@@ -134,8 +134,9 @@ export function renderAgentsPane(snapshot: RunUiSnapshot | undefined, options: R
|
|
|
134
134
|
|
|
135
135
|
// `3/3 tasks · 3 agents` — the label arrives pre-pluralised by the caller
|
|
136
136
|
// ("N agents"); the task tally keeps the slash form on purpose (it reads as
|
|
137
|
-
// progress, not as a count)
|
|
138
|
-
|
|
137
|
+
// progress, not as a count), with the noun pluralised by the TOTAL
|
|
138
|
+
// (formatCount, L9): `1/1 task`, `3/3 tasks`.
|
|
139
|
+
lines.push(`${completed}/${formatCount(total, "task")} · ${label}`);
|
|
139
140
|
|
|
140
141
|
for (const agent of realAgents.slice(0, 12)) {
|
|
141
142
|
const liveHandle = liveByTaskId.get(agent.taskId);
|
|
@@ -16,8 +16,13 @@ function seconds(ms: number): string {
|
|
|
16
16
|
export function renderHealthPane(snapshot: RunUiSnapshot | undefined, opts: HealthPaneOptions = {}): string[] {
|
|
17
17
|
if (!snapshot) return ["Health pane: snapshot unavailable"];
|
|
18
18
|
const summary = summarizeHeartbeats(snapshot, opts);
|
|
19
|
+
// L7 judgment: this pane IS the diagnostic surface — `stale`/`dead`/
|
|
20
|
+
// `missing` are the exact vocabulary the K kill-stale / R recovery actions
|
|
21
|
+
// and the diagnostic export act on, so the LABELS stay. But the wire
|
|
22
|
+
// separator goes (report L7 flags `stale=`): the line now speaks the same
|
|
23
|
+
// count-first tally dialect as `1/3 healthy`, not `stale=2` key=value.
|
|
19
24
|
const lines = [
|
|
20
|
-
`Health pane: ${summary.healthy}/${summary.totalTasks} healthy ·
|
|
25
|
+
`Health pane: ${summary.healthy}/${summary.totalTasks} healthy · ${summary.stale} stale · ${summary.dead} dead · ${summary.missing} missing`,
|
|
21
26
|
];
|
|
22
27
|
if (summary.worstStaleMs > 0) lines.push(`Worst stale: ${seconds(summary.worstStaleMs)} ago`);
|
|
23
28
|
// One hint format (rail.ts `formatHint`): keys `label` joined by ` · `.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { formatCount } from "../format-helpers.ts";
|
|
1
2
|
import { formatHint } from "../rail.ts";
|
|
2
3
|
import type { RunUiSnapshot } from "../snapshot-types.ts";
|
|
3
4
|
|
|
@@ -8,24 +9,28 @@ export function renderMailboxPane(snapshot: RunUiSnapshot | undefined): string[]
|
|
|
8
9
|
const unread = mailbox.inboxUnread ?? 0;
|
|
9
10
|
const outgoing = mailbox.outboxPending ?? 0;
|
|
10
11
|
const attention = mailbox.needsAttention ?? 0;
|
|
11
|
-
|
|
12
|
-
//
|
|
12
|
+
// L7 dialect: counts speak the compactUsage TUI form (the `↑2.8k ↓3.7k`
|
|
13
|
+
// pattern from live-run-sidebar.ts), NOT the `unread=2` wire format. ↓ =
|
|
14
|
+
// received (inbox), ↑ = sent (outbox pending); ⚠ marks attention (the same
|
|
15
|
+
// pane-local warning glyph the steering line below already uses).
|
|
16
|
+
const lines: string[] = [`Mailbox pane: ↓${unread} unread · ↑${outgoing} pending · ⚠${attention} attention${approx}`];
|
|
17
|
+
// Kind-separated breakdown — every kind here is an inbox (received) count.
|
|
13
18
|
const kindParts: string[] = [];
|
|
14
19
|
const steer = mailbox.steerUnread ?? 0;
|
|
15
20
|
const followUp = mailbox.followUpUnread ?? 0;
|
|
16
21
|
const response = mailbox.responseUnread ?? 0;
|
|
17
22
|
const message = mailbox.messageUnread ?? 0;
|
|
18
|
-
if (steer > 0) kindParts.push(
|
|
19
|
-
if (followUp > 0) kindParts.push(
|
|
20
|
-
if (response > 0) kindParts.push(
|
|
21
|
-
if (message > 0) kindParts.push(
|
|
23
|
+
if (steer > 0) kindParts.push(`↓${steer} steer`);
|
|
24
|
+
if (followUp > 0) kindParts.push(`↓${followUp} follow-up`);
|
|
25
|
+
if (response > 0) kindParts.push(`↓${response} response`);
|
|
26
|
+
if (message > 0) kindParts.push(`↓${message} message`);
|
|
22
27
|
if (kindParts.length > 0) {
|
|
23
28
|
lines.push(` Breakdown: ${kindParts.join(" · ")}`);
|
|
24
29
|
if (steer > 0) {
|
|
25
30
|
lines.push(" ⚠ Urgent: steering messages require immediate attention.");
|
|
26
31
|
}
|
|
27
32
|
if (followUp > 0) {
|
|
28
|
-
lines.push(` 📋 ${followUp
|
|
33
|
+
lines.push(` 📋 ${formatCount(followUp, "follow-up")} pending review.`);
|
|
29
34
|
}
|
|
30
35
|
}
|
|
31
36
|
if (attention > 0) {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { isPlanApprovalPending } from "../../runtime/plan-approval.ts";
|
|
18
18
|
import { deriveItemProgress } from "../../state/stores/plan-store.ts";
|
|
19
19
|
import type { PlanItemStatus, TeamTaskState } from "../../state/types.ts";
|
|
20
|
+
import { formatCount } from "../format-helpers.ts";
|
|
20
21
|
import { ACTIVE, formatHint, statusIcon } from "../rail.ts";
|
|
21
22
|
|
|
22
23
|
/**
|
|
@@ -99,7 +100,9 @@ export function planRevisionDiff(snapshot: RunUiSnapshot): string[] {
|
|
|
99
100
|
const beforeTasks = before.taskIds?.length ?? 0;
|
|
100
101
|
const currentTasks = item.taskIds?.length ?? 0;
|
|
101
102
|
if (before.status !== item.status || beforeTasks !== currentTasks) {
|
|
102
|
-
lines.push(
|
|
103
|
+
lines.push(
|
|
104
|
+
` ~ ${item.id} ${item.title ?? "?"} [${before.status}→${item.status} · ${beforeTasks}→${formatCount(currentTasks, "task")}]`,
|
|
105
|
+
);
|
|
103
106
|
}
|
|
104
107
|
prevItems.delete(item.id);
|
|
105
108
|
}
|
|
@@ -128,7 +131,7 @@ export function renderPlanPane(snapshot: RunUiSnapshot, options: PlanPaneOptions
|
|
|
128
131
|
const itemById = new Map(items.map((i) => [i.id, i]));
|
|
129
132
|
|
|
130
133
|
const pending = isPlanApprovalPending(snapshot.manifest);
|
|
131
|
-
const header = `Plan pane: ${current.title ?? "?"} @v${current.version} (${phases.length}
|
|
134
|
+
const header = `Plan pane: ${current.title ?? "?"} @v${current.version} (${formatCount(phases.length, "phase")} · ${formatCount(items.length, "item")})`;
|
|
132
135
|
// Hint join format is the rail one (` · `, close/deny last). NOT routed
|
|
133
136
|
// through `formatHint` (yet): its `keyToken` upper-cases a bare letter
|
|
134
137
|
// (`n` → `N`) and the locked pane test
|
|
@@ -8,8 +8,14 @@ export function renderProgressPane(snapshot: RunUiSnapshot | undefined): string[
|
|
|
8
8
|
if (!snapshot) return ["Progress pane: snapshot unavailable"];
|
|
9
9
|
const progress = snapshot.progress;
|
|
10
10
|
const groupJoins = snapshot.groupJoins ?? [];
|
|
11
|
+
// L7 judgment: the header and group-join lines speak the count-first tally
|
|
12
|
+
// dialect (`1 running · 0 queued`, `req-123 · acknowledged`), not the
|
|
13
|
+
// `running=1`/`ack=acknowledged` wire format. The ONE deliberate
|
|
14
|
+
// key=value survivor is `reason=` below: the cancellation reason is a
|
|
15
|
+
// machine code (`leader_interrupted`) — diagnostic data, not prose, and
|
|
16
|
+
// the shape (not the wording) is what the operator greps for.
|
|
11
17
|
const groupJoinLines = groupJoins.length
|
|
12
|
-
? groupJoins.map((item) => `group join ${item.partial ? "partial" : "completed"}: ${item.requestId}
|
|
18
|
+
? groupJoins.map((item) => `group join ${item.partial ? "partial" : "completed"}: ${item.requestId} · ${item.ack}`)
|
|
13
19
|
: ["group joins: none"];
|
|
14
20
|
const cancellationLine = snapshot.cancellationReason ? [`cancelled: reason=${snapshot.cancellationReason}`] : [];
|
|
15
21
|
const runProgress = computePhaseProgress(snapshot.tasks);
|
|
@@ -30,7 +36,7 @@ export function renderProgressPane(snapshot: RunUiSnapshot | undefined): string[
|
|
|
30
36
|
// uncolored by design). One line while the run is parked on approval.
|
|
31
37
|
const planBanner = isPlanApprovalPending(snapshot.manifest) ? [`⚠ plan approval pending — ${PLAN_APPROVAL_HINT}`] : [];
|
|
32
38
|
return [
|
|
33
|
-
`Progress pane: ${progress.completed}/${progress.total} completed ·
|
|
39
|
+
`Progress pane: ${progress.completed}/${progress.total} completed · ${progress.running} running · ${progress.queued} queued · ${progress.failed} failed`,
|
|
34
40
|
...planBanner,
|
|
35
41
|
...dwfPhaseLines,
|
|
36
42
|
...phaseHeader,
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
import { matchesKey } from "@earendil-works/pi-tui";
|
|
29
29
|
import type { LiveAgentHandle } from "../runtime/live-session/live-agent-manager.ts";
|
|
30
30
|
import { truncate } from "../utils/visual.ts";
|
|
31
|
-
import { formatCount } from "./format-helpers.ts";
|
|
31
|
+
import { formatCount, formatDuration } from "./format-helpers.ts";
|
|
32
32
|
import { computeLiveDurationMs } from "./live-duration.ts";
|
|
33
33
|
import { canopyLine, formatHint, gaugeBar, RAIL, railLeaders, railLine, statusSlot } from "./rail.ts";
|
|
34
34
|
import { spinnerFrame } from "./spinner.ts";
|
|
@@ -120,7 +120,9 @@ export class LiveConversationOverlay {
|
|
|
120
120
|
|
|
121
121
|
private refreshSummary(): void {
|
|
122
122
|
const act = this.handle.activity;
|
|
123
|
-
|
|
123
|
+
// L9: durations speak formatDuration (`5m44s`), never raw
|
|
124
|
+
// `(ms/1000).toFixed(1)s` seconds (`344.0s`).
|
|
125
|
+
const summary = `${LiveConversationOverlay.SUMMARY_PREFIX}[${formatCount(act.turnCount ?? 0, "turn")} · ${formatCount(act.toolUses ?? 0, "tool")} · ${formatDuration(computeLiveDurationMs(act, this.nowMs()))}]`;
|
|
124
126
|
const lastLine = this.cachedLines[this.cachedLines.length - 1];
|
|
125
127
|
if (lastLine?.startsWith(LiveConversationOverlay.SUMMARY_PREFIX)) {
|
|
126
128
|
this.cachedLines[this.cachedLines.length - 1] = summary;
|
|
@@ -270,7 +272,7 @@ export class LiveConversationOverlay {
|
|
|
270
272
|
if (act.maxTurns != null) parts.push(`turn ${act.turnCount ?? 0}/${act.maxTurns}`);
|
|
271
273
|
else if ((act.turnCount ?? 0) > 0) parts.push(`turn ${act.turnCount}`);
|
|
272
274
|
if ((act.toolUses ?? 0) > 0) parts.push(formatCount(act.toolUses ?? 0, "tool"));
|
|
273
|
-
parts.push(
|
|
275
|
+
parts.push(formatDuration(computeLiveDurationMs(act, this.nowMs())));
|
|
274
276
|
try {
|
|
275
277
|
const ctxPct = this.handle.session.getSessionStats?.()?.contextUsage?.percent;
|
|
276
278
|
if (ctxPct != null) {
|
package/src/ui/live-duration.ts
CHANGED
|
@@ -49,7 +49,8 @@ export function computeLiveDurationMs(activity: LiveActivity, nowMs: number = Da
|
|
|
49
49
|
return Number.isFinite(ms) && ms >= 0 ? ms : 0;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
// L9 note: the `formatLiveDuration` export was deleted (2026-10-07). It had
|
|
53
|
+
// zero production importers — its raw-seconds `(ms/1000).toFixed(1)s` output
|
|
54
|
+
// was exactly the wire format the dialect sweep retires; every live surface
|
|
55
|
+
// now formats via `formatDuration` (format-helpers.ts) over
|
|
56
|
+
// `computeLiveDurationMs`.
|