pi-crew 0.11.0 → 0.11.2
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 +155 -9
- package/README.md +161 -1037
- package/agents/verifier.md +18 -7
- package/dist/index.mjs +744 -90644
- package/docs/README.md +57 -46
- package/docs/architecture.md +87 -33
- package/docs/commands-reference.md +9 -5
- package/docs/troubleshooting.md +3 -2
- package/package.json +1 -3
- package/schema.json +39 -0
- package/skills/real-test-pi-crew/REPORT-TEMPLATE.md +2 -0
- package/skills/real-test-pi-crew/SKILL.md +371 -34
- package/src/agents/agent-config.ts +1 -1
- package/src/agents/discover-agents.ts +1 -1
- package/src/config/config-validation.ts +15 -2
- package/src/config/config.ts +47 -13
- package/src/config/defaults.ts +0 -1
- package/src/config/env-vars.ts +35 -0
- package/src/config/types.ts +19 -5
- package/src/errors.ts +2 -2
- package/src/extension/async-notifier.ts +23 -0
- package/src/extension/crew-vibes/config.ts +0 -21
- package/src/extension/crew-vibes/index.ts +0 -2
- package/src/extension/crew-vibes/render.ts +1 -50
- package/src/extension/help.ts +21 -12
- package/src/extension/knowledge-injection.ts +2 -1
- package/src/extension/management.ts +8 -3
- package/src/extension/notification-sink.ts +17 -0
- package/src/extension/register.ts +7 -2
- package/src/extension/registration/command-utils.ts +28 -2
- package/src/extension/registration/commands/dashboard.ts +11 -1
- package/src/extension/registration/commands/manage.ts +36 -19
- package/src/extension/registration/commands/run.ts +24 -2
- package/src/extension/registration/commands/shared.ts +23 -1
- package/src/extension/registration/commands/status.ts +25 -2
- package/src/extension/registration/context-builder.ts +8 -2
- package/src/extension/registration/health-notify-policy.ts +100 -0
- package/src/extension/registration/lazy-configurers.ts +35 -0
- package/src/extension/registration/lifecycle-handlers.ts +91 -30
- package/src/extension/registration/lifecycle.ts +75 -10
- package/src/extension/registration/observability.ts +98 -35
- package/src/extension/registration/registration-types.ts +7 -5
- package/src/extension/registration/runtime-cleanup.ts +9 -3
- package/src/extension/registration/subagent-helpers.ts +38 -0
- package/src/extension/registration/subagent-tools.ts +16 -6
- package/src/extension/registration/team-tool.ts +10 -3
- package/src/extension/registration/terminal-status-wiring.ts +172 -0
- package/src/extension/registration/viewers.ts +6 -0
- package/src/extension/registration/wire-cross-extension.ts +28 -0
- package/src/extension/run-compare.ts +220 -0
- package/src/extension/run-export.ts +37 -5
- package/src/extension/run-maintenance.ts +155 -5
- package/src/extension/team-tool/dispatch/index.ts +3 -2
- package/src/extension/team-tool/dispatch/manage.ts +5 -2
- package/src/extension/team-tool/goal.ts +4 -1
- package/src/extension/team-tool/handle-settings.ts +33 -4
- package/src/extension/team-tool/health-monitor.ts +21 -7
- package/src/extension/team-tool/lifecycle-actions.ts +49 -1
- package/src/extension/team-tool/plan.ts +10 -0
- package/src/extension/team-tool/routing-hint.ts +63 -0
- package/src/extension/team-tool/status.ts +4 -0
- package/src/extension/team-tool.ts +52 -6
- package/src/extension/webhook-notify.ts +382 -0
- package/src/observability/metric-sink.ts +12 -2
- package/src/prompt/prompt-runtime.ts +82 -31
- package/src/prompt/worker-events-channel.ts +12 -0
- package/src/runtime/README.md +1 -1
- package/src/runtime/async-runner.ts +87 -1
- package/src/runtime/background-runner.ts +313 -234
- package/src/runtime/broker/crew-broker.ts +17 -11
- package/src/runtime/broker/delegate/shadow-lifecycle.ts +92 -0
- package/src/runtime/broker/wait-status-cache.ts +1 -1
- package/src/runtime/child-pi/child-pi-timers.ts +1 -1
- package/src/runtime/child-pi/mock-fixtures.ts +48 -0
- package/src/runtime/crew-agent-records.ts +337 -45
- package/src/runtime/deadletter.ts +43 -1
- package/src/runtime/delegate-spawn.ts +5 -1
- package/src/runtime/dispatch-batch.ts +72 -5
- package/src/runtime/goal-workflow/goal-loop-runner.ts +73 -4
- package/src/runtime/heartbeat/heartbeat-watcher.ts +7 -0
- package/src/runtime/model/model-fallback.ts +21 -1
- package/src/runtime/model/pi-args.ts +8 -10
- package/src/runtime/recovery/crash-recovery.ts +25 -1
- package/src/runtime/run-worker.ts +12 -1
- package/src/runtime/scheduling/global-worker-cap.ts +13 -6
- package/src/runtime/scheduling/run-coalesced-task-group.ts +27 -1
- package/src/runtime/scheduling/scheduler.ts +49 -13
- package/src/runtime/scheduling/semaphore.ts +148 -20
- package/src/runtime/scratchpad/README.md +1 -1
- package/src/runtime/scratchpad/protocol.ts +1 -1
- package/src/runtime/settings-store.ts +1 -1
- package/src/runtime/skill-instructions.ts +22 -0
- package/src/runtime/stale-reconciler.ts +85 -13
- package/src/runtime/task-display.ts +1 -1
- package/src/runtime/task-runner/pre-execution.ts +26 -2
- package/src/runtime/task-runner/prompt-builder.ts +142 -45
- package/src/runtime/task-runner.ts +21 -1
- package/src/runtime/team-runner.ts +38 -1
- package/src/runtime/workspace-lock.ts +4 -1
- package/src/schema/config-schema.ts +18 -0
- package/src/schema/team-tool-schema.ts +17 -0
- package/src/state/atomic-write.ts +53 -0
- package/src/state/contracts.ts +109 -0
- package/src/state/coordination/locks.ts +191 -33
- package/src/state/coordination/mailbox.ts +140 -15
- package/src/state/crew-init.ts +87 -12
- package/src/state/event-log/cursor.ts +37 -1
- package/src/state/event-log/event-log-rotation.ts +72 -7
- package/src/state/stores/active-run-registry.ts +13 -1
- package/src/state/stores/state-store.ts +112 -22
- package/src/state/types.ts +4 -0
- package/src/ui/adaptive-card.ts +65 -0
- package/src/ui/agents-jobs-browser.ts +70 -64
- package/src/ui/card-colors.ts +36 -7
- package/src/ui/dashboard-panes/agents-pane.ts +55 -14
- package/src/ui/dashboard-panes/cancellation-pane.ts +0 -42
- package/src/ui/dashboard-panes/health-pane.ts +7 -5
- package/src/ui/dashboard-panes/mailbox-pane.ts +22 -6
- package/src/ui/dashboard-panes/metrics-pane.ts +15 -7
- package/src/ui/dashboard-panes/pane-theme.ts +21 -0
- package/src/ui/dashboard-panes/plan-pane.ts +63 -30
- package/src/ui/dashboard-panes/progress-pane.ts +3 -2
- package/src/ui/dashboard-panes/schedules-pane.ts +44 -21
- package/src/ui/dashboard-panes/transcript-pane.ts +11 -5
- package/src/ui/dwf-phase-display.ts +3 -20
- package/src/ui/format-helpers.ts +22 -0
- package/src/ui/heartbeat-aggregator.ts +34 -0
- package/src/ui/inline-panel/crew-editor.ts +13 -3
- package/src/ui/inline-panel/index.ts +60 -4
- package/src/ui/keybinding-map.ts +251 -35
- package/src/ui/live-conversation-overlay.ts +180 -47
- package/src/ui/live-run-sidebar.ts +134 -55
- package/src/ui/mascot.ts +32 -16
- package/src/ui/overlays/agent-picker-overlay.ts +81 -26
- package/src/ui/overlays/confirm-overlay.ts +55 -29
- package/src/ui/overlays/help-overlay.ts +108 -53
- package/src/ui/overlays/mailbox-compose-overlay.ts +89 -50
- package/src/ui/overlays/mailbox-detail-overlay.ts +137 -57
- package/src/ui/powerbar-publisher.ts +0 -1
- package/src/ui/rail.ts +333 -0
- package/src/ui/run-dashboard.ts +193 -79
- package/src/ui/run-snapshot-cache.ts +18 -1
- package/src/ui/settings-overlay.ts +81 -39
- package/src/ui/spinner.ts +26 -2
- package/src/ui/terminal-status.ts +7 -1
- package/src/ui/theme-adapter.ts +0 -45
- package/src/ui/theme-discovery.ts +12 -6
- package/src/ui/tool-progress-formatter.ts +128 -9
- package/src/ui/tool-renderers/brief-mode.ts +10 -67
- package/src/ui/tool-renderers/index.ts +374 -523
- package/src/ui/transcript-viewer.ts +30 -12
- package/src/ui/widget/index.ts +32 -52
- package/src/ui/widget/task-list.ts +64 -32
- package/src/ui/widget/widget-formatters.ts +3 -402
- package/src/ui/widget/widget-model.ts +28 -7
- package/src/ui/widget/widget-renderer.ts +201 -128
- package/src/ui/widget/widget-types.ts +0 -2
- package/src/utils/incremental-reader.ts +11 -3
- package/src/utils/paths.ts +94 -12
- package/src/utils/project-markers.ts +40 -0
- package/src/utils/visual.ts +0 -4
- package/src/worktree/worktree-manager.ts +206 -26
- package/workflows/distill.workflow.md +3 -3
- package/workflows/fast-fix.workflow.md +1 -1
- package/workflows/plan-execute.workflow.md +1 -1
- package/workflows/review.workflow.md +1 -1
- package/workflows/strict-fast-fix.workflow.md +1 -1
- package/docs/migration-v0.4-v0.5.md +0 -208
- package/docs/runtime-flow.md +0 -148
- package/src/extension/crew-vibes/figures.ts +0 -22
- package/src/extension/crew-vibes/font-detect.ts +0 -71
- package/src/ui/dynamic-border.ts +0 -35
- package/src/ui/loaders.ts +0 -6
- package/src/ui/overlay-stack.ts +0 -148
package/src/ui/run-dashboard.ts
CHANGED
|
@@ -10,14 +10,14 @@ import { readCrewAgents } from "../runtime/crew-agent-records.ts";
|
|
|
10
10
|
import type { CrewAgentRecord } from "../runtime/crew-agent-runtime.ts";
|
|
11
11
|
import { getLiveAgentContextPercent } from "../runtime/live-session/live-agent-manager.ts";
|
|
12
12
|
import { isPlanApprovalPending } from "../runtime/plan-approval.ts";
|
|
13
|
-
import { isDisplayActiveRun, isLikelyOrphanedActiveRun } from "../runtime/process-status.ts";
|
|
13
|
+
import { isActiveRunStatus, isDisplayActiveRun, isLikelyOrphanedActiveRun } from "../runtime/process-status.ts";
|
|
14
14
|
import type { ScheduledJob } from "../runtime/scheduling/scheduler.ts";
|
|
15
15
|
import type { TeamRunManifest, TeamTaskState, UsageState } from "../state/types.ts";
|
|
16
16
|
import { aggregateUsage } from "../state/usage.ts";
|
|
17
17
|
import { readJsonFileCoalesced } from "../utils/file-coalescer.ts";
|
|
18
18
|
import { logInternalError } from "../utils/internal-error.ts";
|
|
19
19
|
import { resolveRealContainedPath } from "../utils/safe-paths.ts";
|
|
20
|
-
import {
|
|
20
|
+
import { sanitizeLine, truncate, visibleWidth } from "../utils/visual.ts";
|
|
21
21
|
import type { InteractiveComponent } from "./component.ts";
|
|
22
22
|
import { renderAgentsPane } from "./dashboard-panes/agents-pane.ts";
|
|
23
23
|
import { summarizeTerminalReason } from "./dashboard-panes/cancellation-pane.ts";
|
|
@@ -28,9 +28,10 @@ import { renderPlanPane } from "./dashboard-panes/plan-pane.ts";
|
|
|
28
28
|
import { renderProgressPane } from "./dashboard-panes/progress-pane.ts";
|
|
29
29
|
import { renderScheduleDetails, renderSchedulesPane } from "./dashboard-panes/schedules-pane.ts";
|
|
30
30
|
import { renderTranscriptPane } from "./dashboard-panes/transcript-pane.ts";
|
|
31
|
-
import {
|
|
31
|
+
import { formatCount, teamWorkflowLabel } from "./format-helpers.ts";
|
|
32
32
|
import { type DashboardKeyAction, dashboardActionForKey } from "./keybinding-map.ts";
|
|
33
33
|
import { HelpOverlay } from "./overlays/help-overlay.ts";
|
|
34
|
+
import { ACTIVE, CURSOR, canopyLine, formatHint, gaugeBar, overflowHint, RAIL, railLine, sectionLine } from "./rail.ts";
|
|
34
35
|
import type { OverlaySchedulerHandle } from "./shared-overlay-scheduler.ts";
|
|
35
36
|
import { registerOverlayScheduler } from "./shared-overlay-scheduler.ts";
|
|
36
37
|
import type { RunSnapshotCache, RunUiSnapshot } from "./snapshot-types.ts";
|
|
@@ -56,7 +57,6 @@ function safeRenderPane(name: string, fn: () => string[]): string[] {
|
|
|
56
57
|
interface DashboardComponent extends InteractiveComponent {}
|
|
57
58
|
|
|
58
59
|
export interface RunDashboardOptions {
|
|
59
|
-
placement?: "center" | "right";
|
|
60
60
|
showModel?: boolean;
|
|
61
61
|
showTokens?: boolean;
|
|
62
62
|
showTools?: boolean;
|
|
@@ -94,6 +94,27 @@ export interface RunDashboardOptions {
|
|
|
94
94
|
*/
|
|
95
95
|
let lastActivePane: "agents" | "progress" | "mailbox" | "output" | "health" | "metrics" | "plan" | "schedules" = "agents";
|
|
96
96
|
|
|
97
|
+
/**
|
|
98
|
+
* US-020 AC-2: the selected run SURVIVES dashboard close/reopen (the shared.ts
|
|
99
|
+
* action loop re-creates RunDashboard after every done() selection — enter on
|
|
100
|
+
* a run opens its detail, escape returns, and the cursor is where it was).
|
|
101
|
+
* Persisted per-process like lastActivePane; restored by runId on construct.
|
|
102
|
+
*/
|
|
103
|
+
let lastSelectedRunId: string | undefined;
|
|
104
|
+
|
|
105
|
+
/** @internal — test isolation hook. US-020 AC-2 persists the cursor in
|
|
106
|
+
* module-level state (lastSelectedRunId) so it survives close/reopen cycles,
|
|
107
|
+
* but that state ALSO leaks across RunDashboard instances within one process:
|
|
108
|
+
* an E2E brute-force that constructs a fresh dashboard per iteration inherits
|
|
109
|
+
* the previous iteration's cursor, and with wrap-navigation the driven
|
|
110
|
+
* selection lands somewhere else entirely (measured: count=9 offset=8 restored
|
|
111
|
+
* run-007 → 8 'j' wraps to run-006, not run-008). Tests that need a clean
|
|
112
|
+
* cursor call this before constructing. */
|
|
113
|
+
export function __test__resetDashboardState(): void {
|
|
114
|
+
lastSelectedRunId = undefined;
|
|
115
|
+
lastActivePane = "agents";
|
|
116
|
+
}
|
|
117
|
+
|
|
97
118
|
export type RunDashboardAction =
|
|
98
119
|
| "status"
|
|
99
120
|
| "summary"
|
|
@@ -118,7 +139,8 @@ export type RunDashboardAction =
|
|
|
118
139
|
| "schedule-disable"
|
|
119
140
|
| "schedule-run-now"
|
|
120
141
|
| "schedule-remove"
|
|
121
|
-
| "notifications-dismiss"
|
|
142
|
+
| "notifications-dismiss"
|
|
143
|
+
| "cancel";
|
|
122
144
|
export interface RunDashboardSelection {
|
|
123
145
|
runId: string;
|
|
124
146
|
action: RunDashboardAction;
|
|
@@ -187,12 +209,6 @@ const SIGNATURE_CACHE_TTL_MS = 100;
|
|
|
187
209
|
*/
|
|
188
210
|
const STALE_SNAPSHOT_MS = 15_000;
|
|
189
211
|
|
|
190
|
-
/** Left-pad `value` to a fixed VISIBLE width (ANSI-aware). Used by V-1. */
|
|
191
|
-
function padVis(value: string, width: number): string {
|
|
192
|
-
const current = visibleWidth(value);
|
|
193
|
-
return current >= width ? value : `${" ".repeat(width - current)}${value}`;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
212
|
/**
|
|
197
213
|
* L-1 — compute the run-list window (visible slots + which scroll indicators
|
|
198
214
|
* render) for a given offset. Shared by `ensureRunListWindow()` and the
|
|
@@ -323,7 +339,7 @@ function agentPreviewLine(agent: CrewAgentRecord, task: TeamTaskState | undefine
|
|
|
323
339
|
(agent.progress?.tokens !== undefined ? `tok=${agent.progress.tokens}` : undefined))
|
|
324
340
|
: undefined,
|
|
325
341
|
options.showTools !== false && agent.progress?.currentTool ? `tool=${agent.progress.currentTool}` : undefined,
|
|
326
|
-
options.showTools !== false && agent.toolUses !== undefined ?
|
|
342
|
+
options.showTools !== false && agent.toolUses !== undefined ? formatCount(agent.toolUses, "tool") : undefined,
|
|
327
343
|
agent.progress?.turns !== undefined ? `${agent.progress.turns} turns` : undefined,
|
|
328
344
|
agent.progress?.failedTool ? `failedTool=${agent.progress.failedTool}` : undefined,
|
|
329
345
|
agent.startedAt ? `age=${formatAge(agent.completedAt ?? agent.startedAt)}` : undefined,
|
|
@@ -332,8 +348,11 @@ function agentPreviewLine(agent: CrewAgentRecord, task: TeamTaskState | undefine
|
|
|
332
348
|
const icon = iconForStatus(agent.status, {
|
|
333
349
|
runningGlyph: spinnerFrame(agent.taskId),
|
|
334
350
|
});
|
|
351
|
+
// GUARD (M4, §4): agents.json records are NOT schema-validated at read time,
|
|
352
|
+
// so every interpolated field needs a fallback — `${agent.role}${ACTIVE}${agent.agent}`
|
|
353
|
+
// printed `undefined->undefined` for a partially-written record.
|
|
335
354
|
return sanitizeLine(
|
|
336
|
-
`Agent: ${icon} ${agent.taskId} ${agent.role}
|
|
355
|
+
`Agent: ${icon} ${agent.taskId ?? "?"} ${agent.role ?? "?"}${ACTIVE}${agent.agent ?? "?"}${stats.length ? ` · ${stats.join(" · ")}` : ""}${recent ? ` ⎿ ${recent}` : ""}`,
|
|
337
356
|
);
|
|
338
357
|
}
|
|
339
358
|
|
|
@@ -409,7 +428,7 @@ function runLabel(
|
|
|
409
428
|
? `queued ${queued.taskId}`
|
|
410
429
|
: `agents ${agents.length}`;
|
|
411
430
|
const status: RunStatus = stale ? "stale" : (run.status as RunStatus);
|
|
412
|
-
const marker = selected ?
|
|
431
|
+
const marker = selected ? CURSOR : " ";
|
|
413
432
|
const icon = iconForStatus(status, {
|
|
414
433
|
runningGlyph: spinnerFrame(run.runId),
|
|
415
434
|
});
|
|
@@ -420,20 +439,25 @@ function runLabel(
|
|
|
420
439
|
// enables the goal-aware truncation; legacy 3-arg callers (dev patch
|
|
421
440
|
// scripts) get the untruncated full label.
|
|
422
441
|
const head = `${marker} ${icon} ${run.runId.slice(-8)} ${status}`;
|
|
423
|
-
const meta = `${run.team
|
|
442
|
+
const meta = `${teamWorkflowLabel(run.team, run.workflow)} · ${step}`;
|
|
424
443
|
const goal = sanitizeLine(run.goal ?? "");
|
|
425
444
|
if (maxW === undefined) return sanitizeLine(`${head} · ${meta} · ${goal}`);
|
|
426
445
|
const sepW = 3; // " · "
|
|
427
446
|
if (visibleWidth(head) + sepW + visibleWidth(meta) + sepW + visibleWidth(goal) <= maxW) {
|
|
428
447
|
return sanitizeLine(`${head} · ${meta} · ${goal}`);
|
|
429
448
|
}
|
|
430
|
-
// Not enough room
|
|
431
|
-
//
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
449
|
+
// Not enough room. The STATUS token is the one that must survive — the run
|
|
450
|
+
// list is how a failed/cancelled run gets spotted — so meta clips FIRST,
|
|
451
|
+
// then the goal; the head itself is only clipped if it cannot fit alone.
|
|
452
|
+
// (Live bug 2026-09-16: the old fallback truncated `head · meta` as ONE
|
|
453
|
+
// string, so a 118-column render painted `› ✓ 122ab0dd complete…` — the
|
|
454
|
+
// status cut in half while the meta it should have sacrificed stayed put.)
|
|
455
|
+
const headW = visibleWidth(head);
|
|
456
|
+
if (headW + sepW + 8 <= maxW) {
|
|
457
|
+
const goalBudget = Math.max(8, maxW - headW - sepW);
|
|
458
|
+
return sanitizeLine(`${head} · ${truncate(goal, goalBudget)}`);
|
|
459
|
+
}
|
|
460
|
+
return sanitizeLine(truncate(head, maxW));
|
|
437
461
|
}
|
|
438
462
|
|
|
439
463
|
interface ResolvedRun {
|
|
@@ -482,17 +506,9 @@ function selectedRunFromGrouped(
|
|
|
482
506
|
return groupedRuns(runs, snapshotCache, resolve).filter((row) => row.run)[selected]?.run;
|
|
483
507
|
}
|
|
484
508
|
|
|
485
|
-
function countByStatus(runs: TeamRunManifest[], snapshotCache?: RunSnapshotCache): string {
|
|
486
|
-
const resolved = resolveRuns(runs, snapshotCache);
|
|
487
|
-
const counts = new Map<RunStatus, number>();
|
|
488
|
-
for (const r of resolved.values()) counts.set(r.status, (counts.get(r.status) ?? 0) + 1);
|
|
489
|
-
return [...counts.entries()].map(([status, count]) => `${status}=${count}`).join(", ") || "none";
|
|
490
|
-
}
|
|
491
|
-
|
|
492
509
|
export class RunDashboard implements DashboardComponent {
|
|
493
510
|
private selected = 0;
|
|
494
511
|
private runScrollOffset = 0;
|
|
495
|
-
private showFullProgress = false;
|
|
496
512
|
private showHelp = false;
|
|
497
513
|
private activePane: "agents" | "progress" | "mailbox" | "output" | "health" | "metrics" | "plan" | "schedules" = lastActivePane;
|
|
498
514
|
/** WP-7 (R7): pane-scoped revision-diff toggle (X). */
|
|
@@ -502,6 +518,11 @@ export class RunDashboard implements DashboardComponent {
|
|
|
502
518
|
private scheduleSelected = 0;
|
|
503
519
|
private scheduleDetails = false;
|
|
504
520
|
private scheduleDeleteArmed = false;
|
|
521
|
+
/** US-020 (run cancel): 2-step confirm-gate + refusal status line —
|
|
522
|
+
* mirrors the schedules delete gate (first x arms, second x dispatches,
|
|
523
|
+
* any other key disarms; a non-running selection refuses with a line). */
|
|
524
|
+
private runCancelArmed = false;
|
|
525
|
+
private runCancelRefused = false;
|
|
505
526
|
private scheduleJobsCache: { at: number; jobs: ScheduledJob[]; hiddenCount: number } | undefined;
|
|
506
527
|
private runs: TeamRunManifest[];
|
|
507
528
|
private readonly done: (selection: RunDashboardSelection | undefined) => void;
|
|
@@ -529,6 +550,15 @@ export class RunDashboard implements DashboardComponent {
|
|
|
529
550
|
? runs.filter((run) => !run.ownerSessionId || run.ownerSessionId === options.workspaceId)
|
|
530
551
|
: runs;
|
|
531
552
|
this.runs = filteredRuns;
|
|
553
|
+
// US-020 AC-2: restore the cursor to the previously selected run (if it
|
|
554
|
+
// is still in the list) so escape-return keeps the user's place.
|
|
555
|
+
if (lastSelectedRunId) {
|
|
556
|
+
// groupedRuns rows include GROUP LABELS ("Active"/"Recent") — index
|
|
557
|
+
// into the SELECTABLE rows only (the same projection navigation uses).
|
|
558
|
+
const selectable = groupedRuns(filteredRuns, options.snapshotCache).filter((rowItem) => rowItem.run);
|
|
559
|
+
const idx = selectable.findIndex((rowItem) => rowItem.run?.runId === lastSelectedRunId);
|
|
560
|
+
if (idx >= 0) this.selected = idx;
|
|
561
|
+
}
|
|
532
562
|
this.done = done;
|
|
533
563
|
this.theme = asCrewTheme(theme);
|
|
534
564
|
this.options = options;
|
|
@@ -683,7 +713,7 @@ export class RunDashboard implements DashboardComponent {
|
|
|
683
713
|
const metricsSig =
|
|
684
714
|
this.activePane === "metrics" ? `:metrics=${this.options.registry?.snapshot().length ?? 0}:${spinnerBucket()}` : "";
|
|
685
715
|
const schedulesSig = this.activePane === "schedules" ? `:sched=${this.schedulesSignatureFragment()}` : "";
|
|
686
|
-
const sig = `${this.selected}:${this.showHelp ? 1 : 0}:${this.
|
|
716
|
+
const sig = `${this.selected}:${this.showHelp ? 1 : 0}:${this.activePane}:${statuses}${hasRunning ? `:spin=${spinnerBucket()}` : ""}${metricsSig}${schedulesSig}`;
|
|
687
717
|
this.cachedSignature = sig;
|
|
688
718
|
this.cachedSignatureAt = now;
|
|
689
719
|
return sig;
|
|
@@ -801,17 +831,15 @@ export class RunDashboard implements DashboardComponent {
|
|
|
801
831
|
this.refreshRuns(snapshotOnce);
|
|
802
832
|
const signature = this.buildSignature(snapshotOnce);
|
|
803
833
|
if (signature !== this.cachedVersion || this.cachedWidth !== width) {
|
|
804
|
-
|
|
805
|
-
|
|
834
|
+
// RAIL frame (M4, 2026-09-16) — the rounded box is retired. The frame
|
|
835
|
+
// owner prefixes EVERY body row with `┃` (railLine owns the glyph + space
|
|
836
|
+
// and pads/truncates to `budget`, so no row can spill past the terminal),
|
|
837
|
+
// sections open with `┣` (sectionLine), the header hint rides the
|
|
838
|
+
// canopy's right segment (dot-led) and the surface caps with `┗ <hint>`.
|
|
839
|
+
const budget = Math.max(0, width - 2);
|
|
806
840
|
const fg = (color: Parameters<CrewTheme["fg"]>[0], text: string) => this.theme.fg(color, text);
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
// pass; every border()/sep() call below reuses the cached line.
|
|
810
|
-
const crewBorder = new DynamicCrewBorder(this.theme);
|
|
811
|
-
const borderFill = (count: number) => crewBorder.render(count)[0];
|
|
812
|
-
const border = (left: string, right: string) => `${fg("border", left)}${borderFill(borderWidth)}${fg("border", right)}`;
|
|
813
|
-
const row = (text: string) => `│ ${pad(truncate(text, innerWidth - 1), innerWidth - 1)}│`;
|
|
814
|
-
const sep = () => border("├", "┤");
|
|
841
|
+
const row = (text: string) => railLine(RAIL.body, "border", text, this.theme, budget);
|
|
842
|
+
const section = (name: string, subject?: string) => sectionLine({ name, subject, theme: this.theme, budget });
|
|
815
843
|
|
|
816
844
|
const lines: string[] = [];
|
|
817
845
|
if (this.showHelp) {
|
|
@@ -819,17 +847,48 @@ export class RunDashboard implements DashboardComponent {
|
|
|
819
847
|
lines.push(...new HelpOverlay(this.theme).render(width));
|
|
820
848
|
} else {
|
|
821
849
|
lines.push(
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
850
|
+
canopyLine({
|
|
851
|
+
word: "DASHBOARD",
|
|
852
|
+
subject: formatCount(this.runs.length, "run"),
|
|
853
|
+
// US-020: the cancel hint joins the canopy only when the budget
|
|
854
|
+
// has room — at narrow widths the canopy prefers the run-count
|
|
855
|
+
// subject over hints, and the 5th hint ellipsized it (regression
|
|
856
|
+
// caught by run-dashboard.test.ts's 80-col canonical canopy).
|
|
857
|
+
right: formatHint([
|
|
858
|
+
["1-8", "pane"],
|
|
859
|
+
[["up", "down"], "move"],
|
|
860
|
+
["enter", "select"],
|
|
861
|
+
...(budget >= 96 ? ([["x", "cancel"]] as const) : []),
|
|
862
|
+
["?", "help"],
|
|
863
|
+
]),
|
|
864
|
+
theme: this.theme,
|
|
865
|
+
budget,
|
|
866
|
+
}),
|
|
827
867
|
);
|
|
868
|
+
// US-020: run-cancel confirm gate / refusal status line —
|
|
869
|
+
// rendered right under the canopy so it is visible in EVERY pane
|
|
870
|
+
// (schedule-delete's warning is pane-8-only by design; run cancel
|
|
871
|
+
// is a root action).
|
|
872
|
+
if (this.runCancelArmed) {
|
|
873
|
+
lines.push(row(fg("warning", `⚠ x again to CANCEL '${this.selectedRunId() ?? "(none)"}' · any other key disarms`)));
|
|
874
|
+
} else if (this.runCancelRefused) {
|
|
875
|
+
lines.push(row(fg("dim", "nothing to cancel — the selected run is not active")));
|
|
876
|
+
}
|
|
828
877
|
|
|
829
878
|
if (this.runs.length === 0) {
|
|
830
879
|
// F-7: actionable empty state instead of a bare "No runs.".
|
|
831
880
|
lines.push(row(fg("dim", "No runs yet.")));
|
|
832
|
-
lines.push(
|
|
881
|
+
lines.push(
|
|
882
|
+
row(
|
|
883
|
+
fg(
|
|
884
|
+
"dim",
|
|
885
|
+
`Start one: team action='run' · ${formatHint([
|
|
886
|
+
["r", "reload"],
|
|
887
|
+
["esc", "close"],
|
|
888
|
+
])}`,
|
|
889
|
+
),
|
|
890
|
+
),
|
|
891
|
+
);
|
|
833
892
|
} else {
|
|
834
893
|
// L-1: windowed run list so the selection can never scroll off-screen.
|
|
835
894
|
const allGrouped = groupedRuns(this.runs, this.options.snapshotCache, snapshotOnce);
|
|
@@ -839,9 +898,12 @@ export class RunDashboard implements DashboardComponent {
|
|
|
839
898
|
this.ensureRunListWindow(selectableCount);
|
|
840
899
|
if (selectableCount <= RUN_LIST_MAX) {
|
|
841
900
|
// Common case (≤8 runs): keep the Active/Recent group headers.
|
|
842
|
-
for (const rowItem of allGrouped) {
|
|
901
|
+
for (const [gi, rowItem] of allGrouped.entries()) {
|
|
843
902
|
if (!rowItem.run) {
|
|
844
|
-
|
|
903
|
+
// `┣ ACTIVE ▸ 2` — the group label IS its run count.
|
|
904
|
+
let groupCount = 0;
|
|
905
|
+
for (let j = gi + 1; j < allGrouped.length && allGrouped[j]?.run; j++) groupCount++;
|
|
906
|
+
lines.push(section(rowItem.label, `${groupCount}`));
|
|
845
907
|
continue;
|
|
846
908
|
}
|
|
847
909
|
const idx = selectable.findIndex((c) => c.run?.runId === rowItem.run?.runId);
|
|
@@ -849,13 +911,13 @@ export class RunDashboard implements DashboardComponent {
|
|
|
849
911
|
const run = snap?.manifest ?? rowItem.run;
|
|
850
912
|
const agents = snap?.agents ?? agentsFor(rowItem.run, this.options.snapshotCache, snapshotOnce);
|
|
851
913
|
const status: RunStatus = isLikelyOrphanedActiveRun(run, agents) ? "stale" : (run.status as RunStatus);
|
|
852
|
-
const label = runLabel(run, idx === this.selected, this.options.snapshotCache,
|
|
914
|
+
const label = runLabel(run, idx === this.selected, this.options.snapshotCache, budget, snapshotOnce);
|
|
853
915
|
lines.push(row(applyStatusColor(this.theme, status, label)));
|
|
854
916
|
}
|
|
855
917
|
} else {
|
|
856
918
|
// >8 runs: windowed list (group headers omitted to maximise run rows).
|
|
857
919
|
const win = runListWindow(this.runScrollOffset, selectableCount);
|
|
858
|
-
if (win.hasTop) lines.push(row(
|
|
920
|
+
if (win.hasTop) lines.push(row(overflowHint(this.runScrollOffset, 0, this.theme)));
|
|
859
921
|
for (let gi = this.runScrollOffset; gi < Math.min(this.runScrollOffset + win.slots, selectableCount); gi++) {
|
|
860
922
|
const rowItem = selectable[gi];
|
|
861
923
|
if (!rowItem?.run) continue;
|
|
@@ -863,11 +925,11 @@ export class RunDashboard implements DashboardComponent {
|
|
|
863
925
|
const run = snap?.manifest ?? rowItem.run;
|
|
864
926
|
const agents = snap?.agents ?? agentsFor(rowItem.run, this.options.snapshotCache, snapshotOnce);
|
|
865
927
|
const status: RunStatus = isLikelyOrphanedActiveRun(run, agents) ? "stale" : (run.status as RunStatus);
|
|
866
|
-
const label = runLabel(run, gi === this.selected, this.options.snapshotCache,
|
|
928
|
+
const label = runLabel(run, gi === this.selected, this.options.snapshotCache, budget, snapshotOnce);
|
|
867
929
|
lines.push(row(applyStatusColor(this.theme, status, label)));
|
|
868
930
|
}
|
|
869
931
|
if (win.hasBottom)
|
|
870
|
-
lines.push(row(
|
|
932
|
+
lines.push(row(overflowHint(0, selectableCount - (this.runScrollOffset + win.slots), this.theme)));
|
|
871
933
|
}
|
|
872
934
|
|
|
873
935
|
// Selected run detail — compact. PERF (2026-08-24): reuse the
|
|
@@ -883,8 +945,8 @@ export class RunDashboard implements DashboardComponent {
|
|
|
883
945
|
const agents = snap?.agents ?? agentsFor(selectedRun, this.options.snapshotCache, snapshotOnce);
|
|
884
946
|
const statusStr: RunStatus = isLikelyOrphanedActiveRun(r, agents) ? "stale" : (r.status as RunStatus);
|
|
885
947
|
const selectedTasks = snap?.tasks ?? readRunTasks(r, this.options.snapshotCache, snapshotOnce);
|
|
886
|
-
lines.push(
|
|
887
|
-
lines.push(row(`${fg("accent", "▸")} ${truncate(sanitizeLine(r.goal),
|
|
948
|
+
lines.push(section("run"));
|
|
949
|
+
lines.push(row(`${fg("accent", "▸")} ${truncate(sanitizeLine(r.goal), budget - 4)}`));
|
|
888
950
|
// L-2: surface the failure/cancellation reason inline for terminal runs.
|
|
889
951
|
const isTerminal = statusStr === "failed" || statusStr === "cancelled" || statusStr === "stopped";
|
|
890
952
|
const reason = isTerminal ? summarizeTerminalReason(r, selectedTasks, snap?.cancellationReason) : undefined;
|
|
@@ -894,7 +956,7 @@ export class RunDashboard implements DashboardComponent {
|
|
|
894
956
|
fg(
|
|
895
957
|
"dim",
|
|
896
958
|
sanitizeLine(
|
|
897
|
-
` ${r.team
|
|
959
|
+
` ${teamWorkflowLabel(r.team, r.workflow)} · ${statusStr} · ${r.runId.slice(-8)}${reasonSuffix}`,
|
|
898
960
|
),
|
|
899
961
|
),
|
|
900
962
|
),
|
|
@@ -929,9 +991,9 @@ export class RunDashboard implements DashboardComponent {
|
|
|
929
991
|
];
|
|
930
992
|
const filteredPane = paneLines.filter((l) => l && !l.includes("(none)") && l.trim() !== "");
|
|
931
993
|
if (filteredPane.length > 0) {
|
|
932
|
-
lines.push(
|
|
994
|
+
lines.push(section(this.activePane));
|
|
933
995
|
for (const line of filteredPane.slice(0, 8)) {
|
|
934
|
-
lines.push(colorizeStatusGlyphs(row(truncate(sanitizeLine(line),
|
|
996
|
+
lines.push(colorizeStatusGlyphs(row(truncate(sanitizeLine(line), budget)), this.theme));
|
|
935
997
|
}
|
|
936
998
|
}
|
|
937
999
|
|
|
@@ -961,11 +1023,16 @@ export class RunDashboard implements DashboardComponent {
|
|
|
961
1023
|
}
|
|
962
1024
|
}
|
|
963
1025
|
}
|
|
964
|
-
|
|
1026
|
+
// Footer: tokens + a RAIL gauge for the context window. The number
|
|
1027
|
+
// stays next to the bar — the bar reads the trend, the number is
|
|
1028
|
+
// what the user quotes.
|
|
965
1029
|
const footerFields: string[] = [];
|
|
966
|
-
if (tokStr) footerFields.push(
|
|
967
|
-
if (
|
|
968
|
-
|
|
1030
|
+
if (tokStr) footerFields.push(fg("dim", tokStr));
|
|
1031
|
+
if (ctxPct != null && Number.isFinite(ctxPct)) {
|
|
1032
|
+
const ratio = Math.max(0, Math.min(1, ctxPct / 100));
|
|
1033
|
+
footerFields.push(`${gaugeBar(ratio, 10, this.theme)} ${fg("muted", `${Math.round(ctxPct)}% ctx`)}`);
|
|
1034
|
+
}
|
|
1035
|
+
if (footerFields.length) lines.push(row(footerFields.join(fg("dim", " · "))));
|
|
969
1036
|
}
|
|
970
1037
|
}
|
|
971
1038
|
|
|
@@ -976,8 +1043,7 @@ export class RunDashboard implements DashboardComponent {
|
|
|
976
1043
|
if (this.activePane === "schedules") {
|
|
977
1044
|
const jobs = this.scheduleJobs();
|
|
978
1045
|
const now = this.scheduleNow();
|
|
979
|
-
lines.push(
|
|
980
|
-
lines.push(row(fg("dim", "── schedules ──")));
|
|
1046
|
+
lines.push(section("schedules", `${jobs.length}`));
|
|
981
1047
|
const paneLines = safeRenderPane("schedules", () =>
|
|
982
1048
|
this.scheduleDetails && jobs.length > 0
|
|
983
1049
|
? renderScheduleDetails(jobs[Math.min(this.scheduleSelected, jobs.length - 1)], now)
|
|
@@ -987,7 +1053,7 @@ export class RunDashboard implements DashboardComponent {
|
|
|
987
1053
|
}),
|
|
988
1054
|
);
|
|
989
1055
|
for (const line of paneLines.filter((l) => l && l.trim() !== "").slice(0, SCHEDULES_PANE_MAX_LINES)) {
|
|
990
|
-
lines.push(colorizeStatusGlyphs(row(truncate(sanitizeLine(line),
|
|
1056
|
+
lines.push(colorizeStatusGlyphs(row(truncate(sanitizeLine(line), budget)), this.theme));
|
|
991
1057
|
}
|
|
992
1058
|
if (this.scheduleDeleteArmed) {
|
|
993
1059
|
const victim = jobs[Math.min(this.scheduleSelected, jobs.length - 1)];
|
|
@@ -995,16 +1061,26 @@ export class RunDashboard implements DashboardComponent {
|
|
|
995
1061
|
lines.push(row(fg("warning", `⚠ X again to DELETE '${name}' · any other key cancels`)));
|
|
996
1062
|
}
|
|
997
1063
|
}
|
|
998
|
-
|
|
1064
|
+
// Cap: the end of the surface + the close action (LAST, RAIL hint rule).
|
|
1065
|
+
lines.push(
|
|
1066
|
+
railLine(
|
|
1067
|
+
RAIL.close,
|
|
1068
|
+
"border",
|
|
1069
|
+
formatHint([
|
|
1070
|
+
["r", "reload"],
|
|
1071
|
+
["esc", "close"],
|
|
1072
|
+
]),
|
|
1073
|
+
this.theme,
|
|
1074
|
+
budget,
|
|
1075
|
+
),
|
|
1076
|
+
);
|
|
999
1077
|
}
|
|
1000
1078
|
|
|
1001
1079
|
const target = this.targetHeight();
|
|
1002
1080
|
if (lines.length < target) {
|
|
1003
|
-
const
|
|
1004
|
-
const fg = (color: Parameters<CrewTheme["fg"]>[0], text: string) => this.theme.fg(color, text);
|
|
1005
|
-
const blankRow = `│ ${pad("", innerWidth - 1)}│`;
|
|
1081
|
+
const blankRow = railLine(RAIL.body, "border", "", this.theme, budget);
|
|
1006
1082
|
const bottom = lines.pop();
|
|
1007
|
-
while (lines.length < target - 1) lines.push(
|
|
1083
|
+
while (lines.length < target - 1) lines.push(blankRow);
|
|
1008
1084
|
if (bottom) lines.push(bottom);
|
|
1009
1085
|
} else if (lines.length > target) {
|
|
1010
1086
|
const bottom = lines[lines.length - 1];
|
|
@@ -1050,7 +1126,16 @@ export class RunDashboard implements DashboardComponent {
|
|
|
1050
1126
|
this.scheduleDeleteArmed = false;
|
|
1051
1127
|
this.invalidate();
|
|
1052
1128
|
}
|
|
1129
|
+
// US-020: the run-cancel confirm gate disarms on any non-cancel key
|
|
1130
|
+
// (schedule-delete precedent), and a stale refusal line clears.
|
|
1131
|
+
if (this.runCancelArmed && action !== "cancel") {
|
|
1132
|
+
this.runCancelArmed = false;
|
|
1133
|
+
this.invalidate();
|
|
1134
|
+
}
|
|
1135
|
+
if (this.runCancelRefused && action !== "cancel") this.runCancelRefused = false;
|
|
1053
1136
|
const selectedRunId = this.selectedRunId();
|
|
1137
|
+
// US-020 AC-2: keep the cursor across close/reopen cycles.
|
|
1138
|
+
if (selectedRunId) lastSelectedRunId = selectedRunId;
|
|
1054
1139
|
if (action === "close") {
|
|
1055
1140
|
this.done(undefined);
|
|
1056
1141
|
return;
|
|
@@ -1059,6 +1144,36 @@ export class RunDashboard implements DashboardComponent {
|
|
|
1059
1144
|
this.done(selectedRunId ? { runId: selectedRunId, action: "status" } : undefined);
|
|
1060
1145
|
return;
|
|
1061
1146
|
}
|
|
1147
|
+
// US-020: cancel the SELECTED run — dedicated branch (plan-approve
|
|
1148
|
+
// precedent) so a stray keystroke can never close the dashboard via
|
|
1149
|
+
// done(undefined). 2-step confirm gate: first x arms (render shows the
|
|
1150
|
+
// warning line), second x dispatches the existing cancel channel; a
|
|
1151
|
+
// non-running selection refuses with a status line (AC-3) — the guard
|
|
1152
|
+
// mutation target.
|
|
1153
|
+
if (action === "cancel") {
|
|
1154
|
+
const run = selectedRunFromGrouped(this.runs, this.selected, this.options.snapshotCache);
|
|
1155
|
+
const manifest = run ? (snapshotFor(run, this.options.snapshotCache)?.manifest ?? run) : undefined;
|
|
1156
|
+
if (this.runCancelArmed) {
|
|
1157
|
+
this.runCancelArmed = false;
|
|
1158
|
+
if (manifest && isActiveRunStatus(manifest.status)) {
|
|
1159
|
+
this.done({ runId: manifest.runId, action: "cancel" });
|
|
1160
|
+
} else {
|
|
1161
|
+
this.runCancelRefused = true;
|
|
1162
|
+
this.invalidate();
|
|
1163
|
+
}
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
this.runCancelRefused = manifest ? !isActiveRunStatus(manifest.status) : false;
|
|
1167
|
+
if (this.runCancelRefused) {
|
|
1168
|
+
this.invalidate();
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
if (manifest) {
|
|
1172
|
+
this.runCancelArmed = true;
|
|
1173
|
+
this.invalidate();
|
|
1174
|
+
}
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1062
1177
|
// WP-3 (H4-subset): plan approval keys. Deliberately a DEDICATED branch
|
|
1063
1178
|
// (not the generic union block below) because plan actions must gate on
|
|
1064
1179
|
// the selected run actually being parked on a pending approval — a
|
|
@@ -1139,11 +1254,6 @@ export class RunDashboard implements DashboardComponent {
|
|
|
1139
1254
|
this.done(selectedRunId ? { runId: selectedRunId, action: "browser-open" } : { runId: "", action: "browser-open" });
|
|
1140
1255
|
return;
|
|
1141
1256
|
}
|
|
1142
|
-
if (action === "progressToggle") {
|
|
1143
|
-
this.showFullProgress = !this.showFullProgress;
|
|
1144
|
-
this.invalidate();
|
|
1145
|
-
return;
|
|
1146
|
-
}
|
|
1147
1257
|
if (action === "pane-agents") this.activePane = "agents";
|
|
1148
1258
|
else if (action === "pane-progress") this.activePane = "progress";
|
|
1149
1259
|
else if (action === "pane-mailbox") this.activePane = "mailbox";
|
|
@@ -1156,10 +1266,14 @@ export class RunDashboard implements DashboardComponent {
|
|
|
1156
1266
|
this.planDiff = !this.planDiff;
|
|
1157
1267
|
this.invalidate();
|
|
1158
1268
|
return;
|
|
1159
|
-
} else if (action === "up"
|
|
1160
|
-
|
|
1269
|
+
} else if (action === "up" || action === "down") {
|
|
1270
|
+
// US-020 AC-1: WRAP at the list edges (was: hard clamp). An empty list
|
|
1271
|
+
// must not divide by zero — stay at 0.
|
|
1161
1272
|
const selectableCount = groupedRuns(this.runs, this.options.snapshotCache).filter((row) => row.run).length;
|
|
1162
|
-
|
|
1273
|
+
if (selectableCount > 0) {
|
|
1274
|
+
this.selected =
|
|
1275
|
+
action === "up" ? (this.selected - 1 + selectableCount) % selectableCount : (this.selected + 1) % selectableCount;
|
|
1276
|
+
}
|
|
1163
1277
|
}
|
|
1164
1278
|
if (action) {
|
|
1165
1279
|
lastActivePane = this.activePane;
|
|
@@ -1173,7 +1287,7 @@ export class RunDashboard implements DashboardComponent {
|
|
|
1173
1287
|
* actions + jobId for the extension layer to route through
|
|
1174
1288
|
* handle-schedule.ts subActions — the UI layer never mutates jobs itself. */
|
|
1175
1289
|
private handleSchedulePaneAction(action: DashboardKeyAction): void {
|
|
1176
|
-
// Pane-local state toggles follow the
|
|
1290
|
+
// Pane-local state toggles follow the plan-diff precedent:
|
|
1177
1291
|
// invalidate() so the next render (sync in tests, host-tick in the TUI)
|
|
1178
1292
|
// recomputes immediately.
|
|
1179
1293
|
if (action === "schedule-details") {
|
|
@@ -27,6 +27,13 @@ export interface RunSnapshotCache extends RunSnapshotCacheBase {
|
|
|
27
27
|
preloadAllStale(runIds: string[]): Promise<void>;
|
|
28
28
|
/** Task 17 (perf/review-2026-08-24): watcher-facing coalesced async refresh. */
|
|
29
29
|
scheduleRefresh(runId: string): void;
|
|
30
|
+
/**
|
|
31
|
+
* F13 (RR-018): true once dispose() has run. Callers that retain the cache
|
|
32
|
+
* (RegistrationContext.getManifestCache/getRunSnapshotCache) use this to
|
|
33
|
+
* recreate a FRESH instance even when the cwd is unchanged — a disposed
|
|
34
|
+
* instance must never be handed back just because `cacheCwd` still matches.
|
|
35
|
+
*/
|
|
36
|
+
isDisposed(): boolean;
|
|
30
37
|
}
|
|
31
38
|
|
|
32
39
|
/** WP-7 (R7): the plans slice + Plan pane load only when this flag is set —
|
|
@@ -840,6 +847,9 @@ export function createRunSnapshotCache(cwd: string, options: RunSnapshotCacheOpt
|
|
|
840
847
|
const recentEventsLimit = options.recentEvents ?? DEFAULT_RECENT_EVENTS;
|
|
841
848
|
const recentOutputLimit = options.recentOutputLines ?? DEFAULT_RECENT_OUTPUT_LINES;
|
|
842
849
|
const entries = new Map<string, CacheEntry>();
|
|
850
|
+
// F13 (RR-018): liveness flag — dispose() flips it so cache holders can
|
|
851
|
+
// detect a disposed instance and recreate instead of reusing it.
|
|
852
|
+
let disposed = false;
|
|
843
853
|
|
|
844
854
|
function touch(runId: string, entry: CacheEntry): RunUiSnapshot {
|
|
845
855
|
entry.lastAccessMs = Date.now();
|
|
@@ -883,7 +893,7 @@ export function createRunSnapshotCache(cwd: string, options: RunSnapshotCacheOpt
|
|
|
883
893
|
let tasks: TeamTaskState[];
|
|
884
894
|
let agents: CrewAgentRecord[];
|
|
885
895
|
try {
|
|
886
|
-
// R10-4 (docs/refactor-plan.review.md §ROUND 10): sync/async parity.
|
|
896
|
+
// R10-4 (docs/archive/refactor-plan.review.md §ROUND 10): sync/async parity.
|
|
887
897
|
// loadRunManifestById already returns tasks validated against the
|
|
888
898
|
// current tasks.json (mtime+size+generation check in state-store),
|
|
889
899
|
// so the old readTasks() re-read only doubled tasks.json I/O per
|
|
@@ -1143,9 +1153,16 @@ export function createRunSnapshotCache(cwd: string, options: RunSnapshotCacheOpt
|
|
|
1143
1153
|
return new Map([...entries.entries()].map(([key, entry]) => [key, entry.snapshot]));
|
|
1144
1154
|
},
|
|
1145
1155
|
dispose(): void {
|
|
1156
|
+
// F13: idempotent — a second dispose must be a no-op, never a throw
|
|
1157
|
+
// (cleanup paths can race a lazy cache swap).
|
|
1158
|
+
if (disposed) return;
|
|
1159
|
+
disposed = true;
|
|
1146
1160
|
unsubscribe();
|
|
1147
1161
|
inFlightRefreshes.clear();
|
|
1148
1162
|
entries.clear();
|
|
1149
1163
|
},
|
|
1164
|
+
isDisposed(): boolean {
|
|
1165
|
+
return disposed;
|
|
1166
|
+
},
|
|
1150
1167
|
};
|
|
1151
1168
|
}
|