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
|
@@ -17,22 +17,45 @@
|
|
|
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 { ACTIVE, formatHint, statusIcon } from "../rail.ts";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The ONE plan-approval hint line (keys are case-SENSITIVE per
|
|
24
|
+
* keybinding-map.ts:75 — `A` approves, `n` denies, `X` shows the revision
|
|
25
|
+
* diff). `exactKeys` keeps the printed letters truthful; mapping them through
|
|
26
|
+
* the default `keyToken` would advertise `N`, which is not bound.
|
|
27
|
+
*/
|
|
28
|
+
export const PLAN_APPROVAL_HINT = formatHint(
|
|
29
|
+
[
|
|
30
|
+
["A", "approve"],
|
|
31
|
+
["n", "deny"],
|
|
32
|
+
],
|
|
33
|
+
{ exactKeys: true },
|
|
34
|
+
);
|
|
35
|
+
|
|
20
36
|
import type { RunUiSnapshot } from "../snapshot-types.ts";
|
|
37
|
+
import { PANE_THEME } from "./pane-theme.ts";
|
|
21
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Glyph vocabulary — RAIL (`src/ui/rail.ts`) owns it (design system §2.G).
|
|
41
|
+
* `statusIcon()` covers every state rail names; the four task states it cannot
|
|
42
|
+
* express — `◷` waiting, `⚠` attention, `⊘` cancelled, `·` skipped: rail
|
|
43
|
+
* collapses all four into `○` — stay pane-local ON PURPOSE.
|
|
44
|
+
*/
|
|
22
45
|
const ITEM_GLYPH: Record<PlanItemStatus, string> = {
|
|
23
|
-
pending: "
|
|
24
|
-
active:
|
|
25
|
-
done: "
|
|
26
|
-
dropped: "
|
|
46
|
+
pending: statusIcon("queued", PANE_THEME),
|
|
47
|
+
active: ACTIVE,
|
|
48
|
+
done: statusIcon("completed", PANE_THEME),
|
|
49
|
+
dropped: statusIcon("failed", PANE_THEME),
|
|
27
50
|
};
|
|
28
51
|
|
|
29
52
|
const TASK_GLYPH: Record<string, string> = {
|
|
30
|
-
queued: "
|
|
31
|
-
running:
|
|
53
|
+
queued: statusIcon("queued", PANE_THEME),
|
|
54
|
+
running: ACTIVE,
|
|
32
55
|
waiting: "◷",
|
|
33
56
|
needs_attention: "⚠",
|
|
34
|
-
completed: "
|
|
35
|
-
failed: "
|
|
57
|
+
completed: statusIcon("completed", PANE_THEME),
|
|
58
|
+
failed: statusIcon("failed", PANE_THEME),
|
|
36
59
|
cancelled: "⊘",
|
|
37
60
|
skipped: "·",
|
|
38
61
|
};
|
|
@@ -42,8 +65,8 @@ function taskLine(task: TeamTaskState, indent: string): string {
|
|
|
42
65
|
// T3/R5 depth badge: grandchildren (depth 2+) surface explicitly so the
|
|
43
66
|
// tree shows delegation nesting without another pane.
|
|
44
67
|
const depth = typeof task.depth === "number" && task.depth > 1 ? ` d${task.depth}` : "";
|
|
45
|
-
const role = task.displayName ?? task.role;
|
|
46
|
-
return `${indent}${glyph} ${task.id}${depth} ${role} [${task.status}]`;
|
|
68
|
+
const role = task.displayName ?? task.role ?? "?";
|
|
69
|
+
return `${indent}${glyph} ${task.id ?? "?"}${depth} ${role} [${task.status}]`;
|
|
47
70
|
}
|
|
48
71
|
|
|
49
72
|
export interface PlanPaneOptions {
|
|
@@ -63,23 +86,25 @@ export function planRevisionDiff(snapshot: RunUiSnapshot): string[] {
|
|
|
63
86
|
if (!previous) {
|
|
64
87
|
return [`Plan diff: v${current.version} has no prior revision`];
|
|
65
88
|
}
|
|
66
|
-
|
|
89
|
+
// Disk-sourced plan records are NOT schema-validated at read time — every
|
|
90
|
+
// array/field below is guarded (`?? []` / `?? "?"`).
|
|
91
|
+
const prevItems = new Map((previous.items ?? []).map((i) => [i.id, i]));
|
|
67
92
|
const lines = [`Plan diff: v${previous.version} → v${current.version}`];
|
|
68
|
-
for (const item of current.items) {
|
|
93
|
+
for (const item of current.items ?? []) {
|
|
69
94
|
const before = prevItems.get(item.id);
|
|
70
95
|
if (!before) {
|
|
71
|
-
lines.push(` + ${item.id} ${item.title} [${item.status}]`);
|
|
96
|
+
lines.push(` + ${item.id} ${item.title ?? "?"} [${item.status}]`);
|
|
72
97
|
continue;
|
|
73
98
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
);
|
|
99
|
+
const beforeTasks = before.taskIds?.length ?? 0;
|
|
100
|
+
const currentTasks = item.taskIds?.length ?? 0;
|
|
101
|
+
if (before.status !== item.status || beforeTasks !== currentTasks) {
|
|
102
|
+
lines.push(` ~ ${item.id} ${item.title ?? "?"} [${before.status}→${item.status} · ${beforeTasks}→${currentTasks} tasks]`);
|
|
78
103
|
}
|
|
79
104
|
prevItems.delete(item.id);
|
|
80
105
|
}
|
|
81
106
|
for (const dropped of prevItems.values()) {
|
|
82
|
-
lines.push(` - ${dropped.id} ${dropped.title} (dropped in v${current.version})`);
|
|
107
|
+
lines.push(` - ${dropped.id} ${dropped.title ?? "?"} (dropped in v${current.version})`);
|
|
83
108
|
}
|
|
84
109
|
return lines;
|
|
85
110
|
}
|
|
@@ -97,40 +122,48 @@ export function renderPlanPane(snapshot: RunUiSnapshot, options: PlanPaneOptions
|
|
|
97
122
|
if (!current) return ["Plan pane: no plan records"];
|
|
98
123
|
const progress = deriveItemProgress(current, snapshot.tasks);
|
|
99
124
|
const tasksById = new Map(snapshot.tasks.map((t) => [t.id, t]));
|
|
100
|
-
|
|
125
|
+
// Disk-sourced plan records are NOT schema-validated at read time.
|
|
126
|
+
const phases = current.phases ?? [];
|
|
127
|
+
const items = current.items ?? [];
|
|
128
|
+
const itemById = new Map(items.map((i) => [i.id, i]));
|
|
101
129
|
|
|
102
130
|
const pending = isPlanApprovalPending(snapshot.manifest);
|
|
103
|
-
const header = `Plan pane: ${current.title} @v${current.version} (${
|
|
104
|
-
|
|
131
|
+
const header = `Plan pane: ${current.title ?? "?"} @v${current.version} (${phases.length} phases · ${items.length} items)`;
|
|
132
|
+
// Hint join format is the rail one (` · `, close/deny last). NOT routed
|
|
133
|
+
// through `formatHint` (yet): its `keyToken` upper-cases a bare letter
|
|
134
|
+
// (`n` → `N`) and the locked pane test
|
|
135
|
+
// (test/unit/ui/plan-ui-slice.test.ts:159) asserts the lowercase spelling —
|
|
136
|
+
// see the E3 report / leader decision needed.
|
|
137
|
+
const approval = pending ? [`⚠ plan approval pending — ${PLAN_APPROVAL_HINT}`] : [];
|
|
105
138
|
|
|
106
139
|
const lines: string[] = [header, ...approval];
|
|
107
|
-
for (const phase of
|
|
140
|
+
for (const phase of phases) {
|
|
108
141
|
const glyph = ITEM_GLYPH[phase.status] ?? "?";
|
|
109
|
-
lines.push(`${glyph} ${phase.title}`);
|
|
110
|
-
for (const itemId of phase.itemIds) {
|
|
142
|
+
lines.push(`${glyph} ${phase.title ?? "?"}`);
|
|
143
|
+
for (const itemId of phase.itemIds ?? []) {
|
|
111
144
|
const item = itemById.get(itemId);
|
|
112
145
|
if (!item) continue;
|
|
113
146
|
const p = progress.get(itemId);
|
|
114
147
|
const counts = p ? ` ${p.done}/${p.total}${p.failed ? ` ✗${p.failed}` : ""}${p.running ? ` ▸${p.running}` : ""}` : "";
|
|
115
148
|
const droppedTag = item.status === "dropped" ? " ✗ dropped" : "";
|
|
116
|
-
lines.push(` ${ITEM_GLYPH[item.status] ?? "?"} ${item.title}${counts}${droppedTag}`);
|
|
117
|
-
for (const taskId of item.taskIds) {
|
|
149
|
+
lines.push(` ${ITEM_GLYPH[item.status] ?? "?"} ${item.title ?? "?"}${counts}${droppedTag}`);
|
|
150
|
+
for (const taskId of item.taskIds ?? []) {
|
|
118
151
|
const task = tasksById.get(taskId);
|
|
119
152
|
if (task) lines.push(taskLine(task, " "));
|
|
120
153
|
}
|
|
121
154
|
}
|
|
122
155
|
}
|
|
123
156
|
// Items not linked to any phase (producer-free-format plans) — still visible.
|
|
124
|
-
const phased = new Set(
|
|
125
|
-
const orphans =
|
|
157
|
+
const phased = new Set(phases.flatMap((p) => p.itemIds ?? []));
|
|
158
|
+
const orphans = items.filter((i) => !phased.has(i.id));
|
|
126
159
|
if (orphans.length) {
|
|
127
160
|
lines.push("(unphased)");
|
|
128
161
|
for (const item of orphans) {
|
|
129
162
|
const p = progress.get(item.id);
|
|
130
163
|
const droppedTag = item.status === "dropped" ? " ✗ dropped" : "";
|
|
131
|
-
lines.push(` ${ITEM_GLYPH[item.status] ?? "?"} ${item.title}${p ? ` ${p.done}/${p.total}` : ""}${droppedTag}`);
|
|
164
|
+
lines.push(` ${ITEM_GLYPH[item.status] ?? "?"} ${item.title ?? "?"}${p ? ` ${p.done}/${p.total}` : ""}${droppedTag}`);
|
|
132
165
|
}
|
|
133
166
|
}
|
|
134
|
-
lines.push("X revision diff");
|
|
167
|
+
lines.push(formatHint([["X", "revision diff"]]));
|
|
135
168
|
return lines;
|
|
136
169
|
}
|
|
@@ -2,6 +2,7 @@ import { computePhaseProgress, formatPhaseProgressLine } from "../../runtime/pha
|
|
|
2
2
|
import { isPlanApprovalPending } from "../../runtime/plan-approval.ts";
|
|
3
3
|
import { renderDwfPhaseLines } from "../dwf-phase-display.ts";
|
|
4
4
|
import type { RunUiSnapshot } from "../snapshot-types.ts";
|
|
5
|
+
import { PLAN_APPROVAL_HINT } from "./plan-pane.ts";
|
|
5
6
|
|
|
6
7
|
export function renderProgressPane(snapshot: RunUiSnapshot | undefined): string[] {
|
|
7
8
|
if (!snapshot) return ["Progress pane: snapshot unavailable"];
|
|
@@ -17,7 +18,7 @@ export function renderProgressPane(snapshot: RunUiSnapshot | undefined): string[
|
|
|
17
18
|
? runProgress.phases.map((p) => {
|
|
18
19
|
const done = p.completed + p.failed;
|
|
19
20
|
const status = p.running > 0 ? "running" : p.queued > 0 ? "queued" : done >= p.total ? "done" : "waiting";
|
|
20
|
-
return ` Phase ${p.index + 1} ${p.phase}: ${p.percentage}% (${done}/${p.total}) [${status}]`;
|
|
21
|
+
return ` Phase ${p.index + 1} ${p.phase ?? "?"}: ${p.percentage}% (${done}/${p.total}) [${status}]`;
|
|
21
22
|
})
|
|
22
23
|
: [];
|
|
23
24
|
const phaseHeader = phaseLines.length > 0 ? [formatPhaseProgressLine(runProgress), ...phaseLines] : [];
|
|
@@ -27,7 +28,7 @@ export function renderProgressPane(snapshot: RunUiSnapshot | undefined): string[
|
|
|
27
28
|
// WP-3 (H4-subset): plan-approval gate banner. Mirrors the health-pane
|
|
28
29
|
// hint pattern — plain foreground text, no color codes (pane output is
|
|
29
30
|
// uncolored by design). One line while the run is parked on approval.
|
|
30
|
-
const planBanner = isPlanApprovalPending(snapshot.manifest) ? [
|
|
31
|
+
const planBanner = isPlanApprovalPending(snapshot.manifest) ? [`⚠ plan approval pending — ${PLAN_APPROVAL_HINT}`] : [];
|
|
31
32
|
return [
|
|
32
33
|
`Progress pane: ${progress.completed}/${progress.total} completed · running=${progress.running} queued=${progress.queued} failed=${progress.failed}`,
|
|
33
34
|
...planBanner,
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { humanizeSchedule, type ScheduledJob } from "../../runtime/scheduling/scheduler.ts";
|
|
2
2
|
import { formatRelativeTime } from "../../utils/relative-time.ts";
|
|
3
3
|
import { sanitizeLine, truncate } from "../../utils/visual.ts";
|
|
4
|
+
import { formatCount } from "../format-helpers.ts";
|
|
5
|
+
import { ACTIVE, CURSOR, formatHint, statusIcon } from "../rail.ts";
|
|
6
|
+
import { PANE_THEME } from "./pane-theme.ts";
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
9
|
* Schedules pane (tier A foundation) — pure string[] renderer following
|
|
@@ -32,6 +35,16 @@ export interface SchedulesPaneOptions {
|
|
|
32
35
|
|
|
33
36
|
export const SCHEDULES_EMPTY_STATE = "No scheduled jobs — create via team tool action='schedule'";
|
|
34
37
|
|
|
38
|
+
/** Pane-8 actions in the ONE rail hint format (`formatHint`: keys `label`
|
|
39
|
+
* pairs joined by ` · `). Order preserved from the pre-migration hint. */
|
|
40
|
+
const ACTION_HINTS: ReadonlyArray<readonly [string, string]> = [
|
|
41
|
+
["T", "toggle"],
|
|
42
|
+
["N", "run now"],
|
|
43
|
+
["V", "details"],
|
|
44
|
+
["X", "delete"],
|
|
45
|
+
["R", "refresh"],
|
|
46
|
+
];
|
|
47
|
+
|
|
35
48
|
/** P2-1 hint line (single source of truth — pane table, empty state, and the
|
|
36
49
|
* headless /schedules text block all render THIS text). Empty string when
|
|
37
50
|
* there is nothing to hint (callers render no line). Pure: no clock, no I/O;
|
|
@@ -61,7 +74,7 @@ export function renderSchedulesPane(jobs: ScheduledJob[], now: Date, opts: Sched
|
|
|
61
74
|
}
|
|
62
75
|
if (hint) lines.push(hint);
|
|
63
76
|
if (opts.foreground !== false) {
|
|
64
|
-
lines.push(
|
|
77
|
+
lines.push(`Actions: ${formatHint(ACTION_HINTS)}`);
|
|
65
78
|
}
|
|
66
79
|
return lines;
|
|
67
80
|
}
|
|
@@ -82,26 +95,32 @@ export function renderSchedulesTextBlock(jobs: ScheduledJob[], now: Date, opts:
|
|
|
82
95
|
|
|
83
96
|
function renderJobLines(job: ScheduledJob, now: Date, opts: SchedulesPaneOptions, selected: boolean): string[] {
|
|
84
97
|
const glyph = job.enabled ? "●" : "○";
|
|
85
|
-
const name = truncate(sanitizeLine(job.name), opts.nameWidth ?? 28);
|
|
98
|
+
const name = truncate(sanitizeLine(job.name ?? "?"), opts.nameWidth ?? 28);
|
|
86
99
|
// The humanized schedule embeds the RAW job.schedule ("cron …"/"once at …")
|
|
87
100
|
// — same untrusted persisted field the details view sanitizes, so the
|
|
88
101
|
// main line gets the identical treatment (headless /schedules notify path
|
|
89
102
|
// applies NO downstream sanitization — security review F-1).
|
|
90
|
-
const schedule = sanitizeLine(humanizeSchedule({ kind: job.scheduleType, spec: job.schedule }));
|
|
103
|
+
const schedule = sanitizeLine(humanizeSchedule({ kind: job.scheduleType ?? "once", spec: job.schedule ?? "?" }));
|
|
91
104
|
const next = job.nextRun ? formatRelativeTime(now, new Date(job.nextRun)) : "—";
|
|
92
|
-
// Cursor marker (dashboard pane 8):
|
|
93
|
-
// the others — mirroring runLabel's
|
|
94
|
-
// selectedIndex (headless text block, foundation tests) get the
|
|
95
|
-
// legacy layout byte-for-byte.
|
|
96
|
-
const marker = opts.selectedIndex === undefined ? "" : selected ?
|
|
97
|
-
const main = `${marker}${glyph} ${name} ${schedule} · ${next} · ${statusGlyph(job.lastStatus)} · ${job.runCount}
|
|
105
|
+
// Cursor marker (dashboard pane 8): the RAIL `CURSOR` glyph on the selected
|
|
106
|
+
// job, two spaces on the others — mirroring runLabel's marker. Callers that
|
|
107
|
+
// pass NO selectedIndex (headless text block, foundation tests) get the
|
|
108
|
+
// marker-free legacy layout byte-for-byte.
|
|
109
|
+
const marker = opts.selectedIndex === undefined ? "" : selected ? `${CURSOR} ` : " ";
|
|
110
|
+
const main = `${marker}${glyph} ${name} ${schedule} · ${next} · ${statusGlyph(job.lastStatus)} · ${formatCount(job.runCount ?? 0, "run")}`;
|
|
98
111
|
return [main, renderSubLine(job, now, opts)];
|
|
99
112
|
}
|
|
100
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Last-run glyph from the shared RAIL vocabulary. `lastStatus` spells
|
|
116
|
+
* success/error while `statusSlot`/`statusIcon` key off completed/failed, so
|
|
117
|
+
* the two are translated instead of forked; "never ran" keeps the pane's own
|
|
118
|
+
* `·` (rail has no such slot and would render `○`, the disabled/none glyph).
|
|
119
|
+
*/
|
|
101
120
|
function statusGlyph(status: ScheduledJob["lastStatus"]): string {
|
|
102
|
-
if (status === "success") return "
|
|
103
|
-
if (status === "error") return "
|
|
104
|
-
if (status === "running") return "
|
|
121
|
+
if (status === "success") return statusIcon("completed", PANE_THEME);
|
|
122
|
+
if (status === "error") return statusIcon("failed", PANE_THEME);
|
|
123
|
+
if (status === "running") return statusIcon("running", PANE_THEME);
|
|
105
124
|
return "·";
|
|
106
125
|
}
|
|
107
126
|
|
|
@@ -110,12 +129,13 @@ function renderSubLine(job: ScheduledJob, now: Date, opts: SchedulesPaneOptions)
|
|
|
110
129
|
// the dashboard emit pipeline sanitizes whole lines, but the headless
|
|
111
130
|
// /schedules text block goes to ui.notify verbatim, and subagentType/id
|
|
112
131
|
// from a crafted settings entry must not inject line breaks there.
|
|
113
|
-
|
|
132
|
+
// Persisted jobs are NOT schema-validated at read time, hence the `?? "?"`.
|
|
133
|
+
const parts = [sanitizeLine(job.subagentType ?? "?")];
|
|
114
134
|
parts.push(job.lastRun ? `last: ${formatRelativeTime(now, new Date(job.lastRun))}` : "last: never");
|
|
115
135
|
// Duration: rendered only when the job model carries one ("duration if
|
|
116
136
|
// available" in the approved design). ScheduledJob has no duration field
|
|
117
137
|
// today, so nothing is invented here — a future field slots in below.
|
|
118
|
-
if (opts.includeIds) parts.push(`id: ${sanitizeLine(job.id)}`);
|
|
138
|
+
if (opts.includeIds) parts.push(`id: ${sanitizeLine(job.id ?? "?")}`);
|
|
119
139
|
return ` ◦ ${parts.join(" · ")}`;
|
|
120
140
|
}
|
|
121
141
|
|
|
@@ -124,14 +144,14 @@ function renderSubLine(job: ScheduledJob, now: Date, opts: SchedulesPaneOptions)
|
|
|
124
144
|
* carry free text — fall back to description, then name. */
|
|
125
145
|
function scheduleGoalOf(job: ScheduledJob): string {
|
|
126
146
|
try {
|
|
127
|
-
const parsed = JSON.parse(job.prompt) as { goal?: unknown };
|
|
147
|
+
const parsed = JSON.parse(job.prompt ?? "") as { goal?: unknown };
|
|
128
148
|
if (parsed && typeof parsed === "object" && typeof parsed.goal === "string" && parsed.goal.length > 0) {
|
|
129
149
|
return parsed.goal;
|
|
130
150
|
}
|
|
131
151
|
} catch {
|
|
132
152
|
/* prompt is not JSON — fall through */
|
|
133
153
|
}
|
|
134
|
-
return job.description || job.name;
|
|
154
|
+
return job.description || job.name || "?";
|
|
135
155
|
}
|
|
136
156
|
|
|
137
157
|
/**
|
|
@@ -141,14 +161,17 @@ function scheduleGoalOf(job: ScheduledJob): string {
|
|
|
141
161
|
* dashboard emit pipeline truncates/sanitizes per line.
|
|
142
162
|
*/
|
|
143
163
|
export function renderScheduleDetails(job: ScheduledJob, now: Date): string[] {
|
|
144
|
-
|
|
164
|
+
// Persisted jobs are NOT schema-validated at read time (design system §2.G:
|
|
165
|
+
// "guard every optional field") — sanitize AFTER the `?? "?"` fallback so a
|
|
166
|
+
// missing field renders `?`, never `undefined`.
|
|
167
|
+
const schedule = humanizeSchedule({ kind: job.scheduleType ?? "once", spec: job.schedule ?? "?" });
|
|
145
168
|
const next = job.nextRun ? formatRelativeTime(now, new Date(job.nextRun)) : "—";
|
|
146
169
|
const last = job.lastRun ? formatRelativeTime(now, new Date(job.lastRun)) : "never";
|
|
147
170
|
return [
|
|
148
|
-
|
|
171
|
+
`${ACTIVE} ${sanitizeLine(job.name ?? "?")} — id ${sanitizeLine(job.id ?? "?")}`,
|
|
149
172
|
` goal: ${sanitizeLine(scheduleGoalOf(job))}`,
|
|
150
|
-
` schedule: ${sanitizeLine(job.schedule)} (${sanitizeLine(job.scheduleType)}) · humanized: ${sanitizeLine(schedule)} · next: ${next}`,
|
|
151
|
-
` agent: ${sanitizeLine(job.subagentType)} · runs: ${job.runCount} · last: ${last} · status: ${statusGlyph(job.lastStatus)}`,
|
|
152
|
-
` spawned runs: ${job.spawnedRunIds?.length ? job.spawnedRunIds.map(sanitizeLine).join(", ") : "none"}`,
|
|
173
|
+
` schedule: ${sanitizeLine(job.schedule ?? "?")} (${sanitizeLine(job.scheduleType ?? "?")}) · humanized: ${sanitizeLine(schedule)} · next: ${next}`,
|
|
174
|
+
` agent: ${sanitizeLine(job.subagentType ?? "?")} · runs: ${job.runCount ?? 0} · last: ${last} · status: ${statusGlyph(job.lastStatus)}`,
|
|
175
|
+
` spawned runs: ${job.spawnedRunIds?.length ? job.spawnedRunIds.map((runId) => sanitizeLine(String(runId ?? "?"))).join(", ") : "none"}`,
|
|
153
176
|
];
|
|
154
177
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { formatHint } from "../rail.ts";
|
|
1
2
|
import type { RunUiSnapshot } from "../snapshot-types.ts";
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -21,21 +22,26 @@ function modelAttemptLines(snapshot: RunUiSnapshot): string[] {
|
|
|
21
22
|
const attempts = (task.modelAttempts ?? [])
|
|
22
23
|
.map(
|
|
23
24
|
(attempt) =>
|
|
24
|
-
`${attempt.model} ${attempt.success ? "✓" : `✗${attempt.exitCode !== undefined ? `(${attempt.exitCode})` : ""}`}`,
|
|
25
|
+
`${attempt.model ?? "?"} ${attempt.success ? "✓" : `✗${attempt.exitCode !== undefined ? `(${attempt.exitCode})` : ""}`}`,
|
|
25
26
|
)
|
|
26
27
|
.join(" → ");
|
|
27
28
|
const resolved = task.modelRouting?.resolved ? ` · resolved ${task.modelRouting.resolved}` : "";
|
|
28
|
-
lines.push(` ${task.id} (${task.role})${resolved}: ${attempts}`);
|
|
29
|
+
lines.push(` ${task.id ?? "?"} (${task.role ?? "?"})${resolved}: ${attempts}`);
|
|
29
30
|
}
|
|
30
31
|
return lines;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
export function renderTranscriptPane(snapshot: RunUiSnapshot | undefined): string[] {
|
|
34
35
|
if (!snapshot) return ["Output pane: snapshot unavailable"];
|
|
36
|
+
const lines = snapshot.recentOutputLines ?? [];
|
|
35
37
|
return [
|
|
36
|
-
|
|
38
|
+
// Hint format owned by rail.ts (`formatHint`): keys `label` joined by ` · `.
|
|
39
|
+
`Output pane: ${lines.length} recent lines · press ${formatHint([
|
|
40
|
+
["v", "for transcript viewer"],
|
|
41
|
+
["o", "for raw output"],
|
|
42
|
+
])}`,
|
|
37
43
|
...modelAttemptLines(snapshot),
|
|
38
|
-
...
|
|
39
|
-
...(
|
|
44
|
+
...lines.slice(-12).map((line) => `⎿ ${line}`),
|
|
45
|
+
...(lines.length ? [] : ["No recent output"]),
|
|
40
46
|
];
|
|
41
47
|
}
|
|
@@ -30,11 +30,6 @@ export interface DwfPhaseState {
|
|
|
30
30
|
currentPhase: string | null;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
export interface RenderDwfPhaseOptions {
|
|
34
|
-
/** When true, render ASCII fallback markers instead of Unicode glyphs. */
|
|
35
|
-
ascii?: boolean;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
33
|
// ---------------------------------------------------------------------------
|
|
39
34
|
// Markers
|
|
40
35
|
// ---------------------------------------------------------------------------
|
|
@@ -44,19 +39,9 @@ const MARKER_RUNNING = "▶";
|
|
|
44
39
|
const MARKER_COMPLETED = "✓";
|
|
45
40
|
const MARKER_PENDING = "⏸";
|
|
46
41
|
|
|
47
|
-
// ASCII fallbacks for terminals that mis-render the Unicode glyphs above.
|
|
48
|
-
const MARKER_RUNNING_ASCII = "[>]";
|
|
49
|
-
const MARKER_COMPLETED_ASCII = "[v]";
|
|
50
|
-
const MARKER_PENDING_ASCII = "[ ]";
|
|
51
|
-
|
|
52
42
|
const DWF_PHASE_HEADER = " ── DWF Phases ──";
|
|
53
43
|
|
|
54
|
-
function markerFor(status: DwfPhaseStatus
|
|
55
|
-
if (ascii) {
|
|
56
|
-
if (status === "running") return MARKER_RUNNING_ASCII;
|
|
57
|
-
if (status === "completed") return MARKER_COMPLETED_ASCII;
|
|
58
|
-
return MARKER_PENDING_ASCII;
|
|
59
|
-
}
|
|
44
|
+
function markerFor(status: DwfPhaseStatus): string {
|
|
60
45
|
if (status === "running") return MARKER_RUNNING;
|
|
61
46
|
if (status === "completed") return MARKER_COMPLETED;
|
|
62
47
|
return MARKER_PENDING;
|
|
@@ -136,16 +121,14 @@ export function extractDwfPhaseState(events: TeamEvent[]): DwfPhaseState | null
|
|
|
136
121
|
*
|
|
137
122
|
* - One line per phase: ` ▶ Phase: Scan`, ` ✓ Phase: Scan`, ` ⏸ Phase: Review`.
|
|
138
123
|
* - A grouping header is emitted only when more than one phase is present.
|
|
139
|
-
* - When `options.ascii` is true, ASCII fallback markers are used.
|
|
140
124
|
*
|
|
141
125
|
* Always returns a non-empty array (the caller guarantees a non-null state).
|
|
142
126
|
*/
|
|
143
|
-
export function renderDwfPhaseLines(state: DwfPhaseState
|
|
144
|
-
const ascii = options?.ascii === true;
|
|
127
|
+
export function renderDwfPhaseLines(state: DwfPhaseState): string[] {
|
|
145
128
|
const lines: string[] = [];
|
|
146
129
|
if (state.phases.length > 1) lines.push(DWF_PHASE_HEADER);
|
|
147
130
|
for (const entry of state.phases) {
|
|
148
|
-
lines.push(` ${markerFor(entry.status
|
|
131
|
+
lines.push(` ${markerFor(entry.status)} Phase: ${entry.name}`);
|
|
149
132
|
}
|
|
150
133
|
return lines;
|
|
151
134
|
}
|
package/src/ui/format-helpers.ts
CHANGED
|
@@ -29,3 +29,25 @@ export function truncLine(text: string, maxWidth: number): string {
|
|
|
29
29
|
// respects double-wide CJK + surrogate pairs, adds the '…' ellipsis).
|
|
30
30
|
return truncateToWidth(text, maxWidth);
|
|
31
31
|
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* `1 tool` / `3 tools` — count label with correct singular/plural.
|
|
35
|
+
*
|
|
36
|
+
* Live-run data surfaced `1 tools` in five different surfaces (tool card,
|
|
37
|
+
* dashboard agent rows, live-run sidebar, live conversation overlay): every
|
|
38
|
+
* call site hard-coded the plural. One helper, one rule.
|
|
39
|
+
*/
|
|
40
|
+
export function formatCount(count: number, singular: string, plural?: string): string {
|
|
41
|
+
return `${count} ${count === 1 ? singular : (plural ?? `${singular}s`)}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* `team` / `team/workflow` — collapse the workflow half when it repeats the
|
|
46
|
+
* team. Live runs carry both (`fast-fix` + `fast-fix`) and every surface that
|
|
47
|
+
* printed them naively rendered `fast-fix/fast-fix` (sidebar, dashboard).
|
|
48
|
+
*/
|
|
49
|
+
export function teamWorkflowLabel(team: string | undefined, workflow: string | undefined): string {
|
|
50
|
+
const t = team && team.length > 0 ? team : "?";
|
|
51
|
+
const w = workflow && workflow.length > 0 ? workflow : undefined;
|
|
52
|
+
return !w || w === t ? t : `${t}/${w}`;
|
|
53
|
+
}
|
|
@@ -32,6 +32,32 @@ function isActiveTask(task: TeamTaskState): boolean {
|
|
|
32
32
|
return task.status === "running";
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* FINDING 5 (2026-09-23 battery): health ticks must count task statuses from
|
|
37
|
+
* DISK truth, not the (possibly lagging) snapshot cache. A worker parked on
|
|
38
|
+
* `ask` transitions running → waiting on disk while the cached snapshot can
|
|
39
|
+
* still show `running` — in that window the parked worker counted as
|
|
40
|
+
* active-without-heartbeat and fired a false "dead worker" notification (live:
|
|
41
|
+
* run team_20260923100114, 01_explore parked on ask, later answered+resumed).
|
|
42
|
+
*
|
|
43
|
+
* Overlay FRESH task statuses (and heartbeats) from disk onto the cached
|
|
44
|
+
* snapshot before summarizing. Returns the ORIGINAL snapshot object when
|
|
45
|
+
* nothing diverged (cheap identity check — callers use it to decide cache
|
|
46
|
+
* invalidation), or a shallow copy with the diverging tasks replaced.
|
|
47
|
+
*/
|
|
48
|
+
export function overlayFreshTaskStatuses(snapshot: RunUiSnapshot, freshTasks: TeamTaskState[]): RunUiSnapshot {
|
|
49
|
+
if (freshTasks.length === 0) return snapshot;
|
|
50
|
+
const freshById = new Map(freshTasks.map((task) => [task.id, task]));
|
|
51
|
+
let diverged = false;
|
|
52
|
+
const tasks = snapshot.tasks.map((task) => {
|
|
53
|
+
const fresh = freshById.get(task.id);
|
|
54
|
+
if (!fresh || (fresh.status === task.status && fresh.heartbeat === task.heartbeat)) return task;
|
|
55
|
+
diverged = true;
|
|
56
|
+
return { ...task, status: fresh.status, heartbeat: fresh.heartbeat };
|
|
57
|
+
});
|
|
58
|
+
return diverged ? { ...snapshot, tasks } : snapshot;
|
|
59
|
+
}
|
|
60
|
+
|
|
35
61
|
export function summarizeHeartbeats(snapshot: RunUiSnapshot, opts: HeartbeatSummaryOptions = {}): HeartbeatSummary {
|
|
36
62
|
const staleMs = opts.staleMs ?? 60_000;
|
|
37
63
|
const deadMs = opts.deadMs ?? 5 * 60_000;
|
|
@@ -55,6 +81,14 @@ export function summarizeHeartbeats(snapshot: RunUiSnapshot, opts: HeartbeatSumm
|
|
|
55
81
|
const runTerminal = isTerminalRunStatus(snapshot.manifest.status);
|
|
56
82
|
for (const task of snapshot.tasks) {
|
|
57
83
|
if (runTerminal || !isActiveTask(task)) continue;
|
|
84
|
+
// Guest-child tasks (delegate subagents, agent === "delegate") have no
|
|
85
|
+
// heartbeat channel — they never write task.heartbeat and complete in
|
|
86
|
+
// seconds; their lifecycle is owned by delegate.requested/admitted/
|
|
87
|
+
// completed broker events. Counting them here fired a false "N worker(s)
|
|
88
|
+
// missing heartbeat" ambient for every delegate-using run (live:
|
|
89
|
+
// team_20260926033657_2b6c6d2610b26d9d, 2026-09-26 battery — same
|
|
90
|
+
// gc-blindness root shape as the heartbeat-watcher fix).
|
|
91
|
+
if (task.agent === "delegate") continue;
|
|
58
92
|
const heartbeat = task.heartbeat;
|
|
59
93
|
if (!heartbeat) {
|
|
60
94
|
summary.missing += 1;
|
|
@@ -24,13 +24,23 @@
|
|
|
24
24
|
import type { KeybindingsManager } from "@earendil-works/pi-coding-agent";
|
|
25
25
|
import { CustomEditor } from "@earendil-works/pi-coding-agent";
|
|
26
26
|
import { type EditorTheme, matchesKey, type TUI, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
27
|
-
|
|
27
|
+
import { ACTIVE } from "../rail.ts";
|
|
28
28
|
import type { PanelKeys, PanelTarget } from "./panel-selection.ts";
|
|
29
29
|
import { dispatchPanelKey } from "./panel-selection.ts";
|
|
30
30
|
import { getPanelSelection, getViewedAgent, panelRows, setPanelSelection } from "./panel-store.ts";
|
|
31
31
|
|
|
32
32
|
export const AGENT_LABEL_MAX = 24;
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* `name` as it paints in the editor border: whitespace collapsed to one line
|
|
36
|
+
* (the label is spliced INTO the editor's top border, so a newline would tear
|
|
37
|
+
* the frame) and the legacy `role->agent` separator normalized to the RAIL
|
|
38
|
+
* active marker (§4 consistency fix). The mechanism itself is untouched.
|
|
39
|
+
*/
|
|
40
|
+
export function agentBorderLabel(name: string): string {
|
|
41
|
+
return truncateToWidth(name.replace(/->/g, ACTIVE).replace(/\s+/g, " ").trim(), AGENT_LABEL_MAX);
|
|
42
|
+
}
|
|
43
|
+
|
|
34
44
|
export interface CrewEditorOptions {
|
|
35
45
|
/** Open the transcript pane on the given agent. */
|
|
36
46
|
onOpenPane: (target: PanelTarget) => void;
|
|
@@ -192,9 +202,9 @@ export class CrewInlineEditor extends CustomEditor {
|
|
|
192
202
|
if (!viewed) return lines;
|
|
193
203
|
const rows = panelRows();
|
|
194
204
|
const row = rows.find((r) => r.runId === viewed.runId && r.taskId === viewed.taskId);
|
|
195
|
-
const name = row?.name ?? viewed.taskId.slice(-AGENT_LABEL_MAX);
|
|
205
|
+
const name = agentBorderLabel(row?.name ?? viewed.taskId.slice(-AGENT_LABEL_MAX));
|
|
196
206
|
if (name && lines.length > 0) {
|
|
197
|
-
const label = ` @${
|
|
207
|
+
const label = ` @${name} `;
|
|
198
208
|
const labelWidth = visibleWidth(label);
|
|
199
209
|
if (visibleWidth(lines[0]) >= labelWidth + 4) {
|
|
200
210
|
lines[0] = truncateToWidth(lines[0], width - labelWidth - 2, "") + label + "──";
|
|
@@ -66,6 +66,42 @@ function stopPaneTicker(): void {
|
|
|
66
66
|
livePane = undefined;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Tear the live view down completely: close the overlay (which also detaches
|
|
71
|
+
* its pane), stop the repaint ticker, and null every module-level ref.
|
|
72
|
+
*
|
|
73
|
+
* Why this must run on a session swap (P1-10): the overlay factory and its
|
|
74
|
+
* `close`/`steer` callbacks close over the `ctx` they were opened with. If the
|
|
75
|
+
* overlay survived `session_start`, those closures would keep the OLD
|
|
76
|
+
* session's `ctx.cwd` and its dead `ctx.ui.custom.done` — view/steer would
|
|
77
|
+
* quietly target the previous workspace (`openPane`'s try/catch hides the
|
|
78
|
+
* throw instead of surfacing it). Nothing here touches the run: only the view
|
|
79
|
+
* and its repaint timer.
|
|
80
|
+
*/
|
|
81
|
+
function teardownLiveView(): void {
|
|
82
|
+
const overlay = liveOverlay;
|
|
83
|
+
if (overlay) {
|
|
84
|
+
// The overlay owns its own teardown (refs + viewed agent + host done()).
|
|
85
|
+
// A ctx that is already stale can throw out of done(): log, never rethrow.
|
|
86
|
+
try {
|
|
87
|
+
overlay.requestClose();
|
|
88
|
+
} catch (error) {
|
|
89
|
+
logInternalError("view.teardownOverlay", error);
|
|
90
|
+
}
|
|
91
|
+
// Belt and braces: requestClose only unmounts through the host. dispose()
|
|
92
|
+
// makes render()/handleInput() inert, so even a host that ignored done()
|
|
93
|
+
// cannot route another keystroke into the old session's ctx.
|
|
94
|
+
try {
|
|
95
|
+
overlay.dispose();
|
|
96
|
+
} catch {
|
|
97
|
+
/* already disposed by the host */
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
liveOverlay = undefined;
|
|
101
|
+
// Also clears `livePane`, so a ticker that somehow survived has no target.
|
|
102
|
+
stopPaneTicker();
|
|
103
|
+
}
|
|
104
|
+
|
|
69
105
|
async function runTeamTool(params: Record<string, unknown>, ctx: ExtensionContext): Promise<PiTeamsToolResult> {
|
|
70
106
|
// LAZY: team-tool.ts pulls in the entire runtime chain (same boundary as
|
|
71
107
|
// run-action-dispatcher.ts).
|
|
@@ -275,11 +311,13 @@ export function installInlinePanel(pi: ExtensionAPI, ctx: ExtensionContext, uiCo
|
|
|
275
311
|
pi.on("session_start", () => {
|
|
276
312
|
// pi may have replaced or dropped its editor between sessions; the
|
|
277
313
|
// "installed" flag is reset so the next install re-registers the
|
|
278
|
-
// factory. The open overlay
|
|
279
|
-
//
|
|
280
|
-
//
|
|
314
|
+
// factory. The open overlay does NOT outlive the swap: its
|
|
315
|
+
// close/steer callbacks close over the previous session's ctx
|
|
316
|
+
// (cwd / ui.custom.done), so keeping it would make view and steer
|
|
317
|
+
// target the OLD workspace behind a try/catch. Close + dispose it
|
|
318
|
+
// and null the refs instead.
|
|
281
319
|
editorInstalled = false;
|
|
282
|
-
|
|
320
|
+
teardownLiveView();
|
|
283
321
|
});
|
|
284
322
|
}
|
|
285
323
|
}
|
|
@@ -291,6 +329,24 @@ export function __resetInlinePanelForTest(): void {
|
|
|
291
329
|
stopPaneTicker();
|
|
292
330
|
}
|
|
293
331
|
|
|
332
|
+
/**
|
|
333
|
+
* Test seam: the live-view module state. Exposes the raw refs so a test can
|
|
334
|
+
* assert that a session swap left no `ctx`-holding closure behind, and whether
|
|
335
|
+
* the repaint ticker is still armed.
|
|
336
|
+
*/
|
|
337
|
+
export function __test__liveViewState(): {
|
|
338
|
+
liveOverlay: CrewAgentOverlay | undefined;
|
|
339
|
+
livePane: CrewAgentPane | undefined;
|
|
340
|
+
ticking: boolean;
|
|
341
|
+
} {
|
|
342
|
+
return { liveOverlay, livePane, ticking: paneTickTimer !== undefined };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Test seam: run the session-swap teardown directly (same code the hook runs). */
|
|
346
|
+
export function __test__teardownLiveView(): void {
|
|
347
|
+
teardownLiveView();
|
|
348
|
+
}
|
|
349
|
+
|
|
294
350
|
/** Test seam for openPane (the panel wires it as the dock's Enter action). */
|
|
295
351
|
/** Test seam for closePane. */
|
|
296
352
|
export { closePane as __test__closePane, openPane as __test__openPane };
|