pi-crew 0.11.7 → 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/.githooks/pre-commit +22 -0
- package/.githooks/pre-push +56 -0
- package/CHANGELOG.md +196 -1
- package/dist/index.mjs +533 -515
- package/docs/publishing.md +29 -0
- package/docs/usage.md +1 -1
- package/index.ts +3 -3
- package/install.mjs +4 -1
- package/package.json +8 -5
- package/scripts/install-hooks.mjs +32 -0
- package/skills/real-test-pi-crew/SKILL.md +57 -18
- package/src/config/config-validation.ts +8 -1
- package/src/config/defaults.ts +8 -1
- package/src/config/env-vars.ts +16 -1
- package/src/config/types.ts +28 -4
- package/src/extension/command-completions.ts +44 -22
- package/src/extension/registration/foreground-run-controller.ts +19 -10
- package/src/extension/registration/lifecycle-handlers.ts +6 -491
- package/src/extension/registration/lifecycle.ts +7 -90
- package/src/extension/registration/render-loop.ts +532 -0
- package/src/extension/team-tool/doctor.ts +6 -0
- package/src/extension/team-tool/handle-settings.ts +4 -1
- package/src/runtime/child-pi/child-pi-spawn.ts +7 -0
- package/src/runtime/child-pi/child-pi.ts +37 -2
- package/src/runtime/child-pi/pi-version.ts +187 -0
- package/src/runtime/classifier/classifier-service.ts +298 -0
- package/src/runtime/dispatch-batch.ts +4 -0
- package/src/runtime/live-session/live-session-runtime.ts +31 -0
- package/src/runtime/model/pi-args.ts +62 -3
- package/src/runtime/process/zombie-scanner.ts +168 -7
- package/src/runtime/rpc/README.md +64 -25
- package/src/runtime/rpc/rpc-worker.ts +146 -26
- package/src/runtime/run-worker.ts +89 -15
- package/src/runtime/scheduling/run-coalesced-task-group.ts +3 -0
- package/src/runtime/skill-instructions.ts +66 -1
- package/src/runtime/surface/resolve-surface.ts +34 -19
- package/src/runtime/task-runner/child-executor.ts +132 -0
- package/src/runtime/task-runner/pre-execution.ts +8 -0
- package/src/runtime/task-runner.ts +6 -0
- package/src/schema/config-schema.ts +20 -8
- package/src/schema/team-tool-schema.ts +4 -2
- package/src/state/contracts.ts +5 -0
- 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/format-helpers.ts +11 -0
- 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 +31 -1
- package/src/ui/rail.ts +14 -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 +36 -3
- 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 +22 -7
- package/src/ui/widget/widget-model.ts +5 -4
- package/src/ui/widget/widget-renderer.ts +212 -37
- package/src/utils/relative-time.ts +11 -3
- package/src/utils/socket-path.ts +48 -8
- package/src/utils/visual.ts +32 -2
|
@@ -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,
|
package/src/ui/format-helpers.ts
CHANGED
|
@@ -12,6 +12,17 @@ export function formatTokens(n: number): string {
|
|
|
12
12
|
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Canonical generic duration formatter (QW#5 collision resolution):
|
|
17
|
+
* `45ms` / `12.3s` / `3m5s` / `75m` — minutes are unbounded (no hours
|
|
18
|
+
* bucket) and there is no invalid-input guard. Two same-stemmed helpers are
|
|
19
|
+
* DELIBERATELY separate dialects and must not be merged into this one
|
|
20
|
+
* (their output formats are design-pinned):
|
|
21
|
+
* - `formatTaskElapsed` (ui/widget/task-list.ts) — plan-card elapsed:
|
|
22
|
+
* `2m 49s` / `1h 03m`, empty string on invalid input.
|
|
23
|
+
* - `formatRelativeCompact` (utils/relative-time.ts) — relative-time
|
|
24
|
+
* ladder buckets `84m` / `14h` / `2d14h`.
|
|
25
|
+
*/
|
|
15
26
|
export function formatDuration(ms: number): string {
|
|
16
27
|
if (ms < 1000) return `${ms}ms`;
|
|
17
28
|
if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`;
|
|
@@ -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`.
|
|
@@ -12,9 +12,12 @@ import { formatCount, teamWorkflowLabel } from "./format-helpers.ts";
|
|
|
12
12
|
import { goalFlagSuffix } from "./goal-flag.ts";
|
|
13
13
|
import { DASHBOARD_KEYS } from "./keybinding-map.ts";
|
|
14
14
|
import { ACTIVE, canopyLine, formatHint, RAIL, type RailSlot, railLine, sectionLine, shortId, statusSlot } from "./rail.ts";
|
|
15
|
+
// L5: the EXTENDED cache interface — the base type in snapshot-types.ts does
|
|
16
|
+
// not declare the paint-path accessor (readForRender) or scheduleRefresh.
|
|
17
|
+
import type { RunSnapshotCache } from "./run-snapshot-cache.ts";
|
|
15
18
|
import type { OverlaySchedulerHandle } from "./shared-overlay-scheduler.ts";
|
|
16
19
|
import { registerOverlayScheduler } from "./shared-overlay-scheduler.ts";
|
|
17
|
-
import type {
|
|
20
|
+
import type { RunUiSnapshot } from "./snapshot-types.ts";
|
|
18
21
|
import { spinnerBucket, spinnerFrame } from "./spinner.ts";
|
|
19
22
|
import { colorizeStatusGlyphs, iconForStatus } from "./status-colors.ts";
|
|
20
23
|
import type { CrewTheme } from "./theme-adapter.ts";
|
|
@@ -79,6 +82,9 @@ export class LiveRunSidebar {
|
|
|
79
82
|
private cachedWidth = 0;
|
|
80
83
|
private cachedSignature = "";
|
|
81
84
|
private autoCloseTimeout?: NodeJS.Timeout;
|
|
85
|
+
/** L4 (real-test 2026-10-07): FIXED close deadline (epoch ms), captured
|
|
86
|
+
* ONCE on first eligibility — re-renders must never slide it. */
|
|
87
|
+
private autoCloseDeadline?: number;
|
|
82
88
|
private hasAutoClosed = false;
|
|
83
89
|
|
|
84
90
|
constructor(input: {
|
|
@@ -157,10 +163,7 @@ export class LiveRunSidebar {
|
|
|
157
163
|
// M-10 fix (code-review 2026-06-23): clear the auto-close timer so a
|
|
158
164
|
// disposed sidebar (not closed via the normal path) doesn't fire this.done()
|
|
159
165
|
// on a disposed component.
|
|
160
|
-
|
|
161
|
-
clearTimeout(this.autoCloseTimeout);
|
|
162
|
-
this.autoCloseTimeout = undefined;
|
|
163
|
-
}
|
|
166
|
+
this.clearAutoClose();
|
|
164
167
|
this.unsubscribeTheme();
|
|
165
168
|
this.schedulerHandle.dispose();
|
|
166
169
|
}
|
|
@@ -180,8 +183,15 @@ export class LiveRunSidebar {
|
|
|
180
183
|
let rawAgents: ReturnType<typeof readCrewAgents>;
|
|
181
184
|
let snapshot: RunUiSnapshot | undefined;
|
|
182
185
|
if (this.snapshotCache) {
|
|
186
|
+
// L5 (real-test 2026-10-07): the paint path reads the cache ONLY.
|
|
187
|
+
// refreshIfStale rebuilt synchronously between paints (multiple stats +
|
|
188
|
+
// manifest/tasks parse + sha256), stuttering frame pacing. readForRender
|
|
189
|
+
// returns the cached snapshot and schedules the COALESCED ASYNC refresh
|
|
190
|
+
// when the entry's TTL has lapsed; a missing entry schedules nothing, so
|
|
191
|
+
// kick one here or the pre-load frame below would never fill.
|
|
183
192
|
try {
|
|
184
|
-
snapshot = this.snapshotCache.
|
|
193
|
+
snapshot = this.snapshotCache.readForRender(this.runId);
|
|
194
|
+
if (!snapshot) this.snapshotCache.scheduleRefresh(this.runId);
|
|
185
195
|
} catch {
|
|
186
196
|
snapshot = undefined;
|
|
187
197
|
}
|
|
@@ -331,25 +341,29 @@ export class LiveRunSidebar {
|
|
|
331
341
|
}
|
|
332
342
|
// F-6: compute the auto-close countdown BEFORE the cap so the countdown
|
|
333
343
|
// renders inside the frame rather than below it.
|
|
334
|
-
// Auto-close logic: if run is terminal and no active agents, close after delay
|
|
344
|
+
// Auto-close logic: if run is terminal and no active agents, close after delay.
|
|
345
|
+
// L4 (real-test 2026-10-07): the deadline is captured ONCE on FIRST
|
|
346
|
+
// eligibility and never re-armed while eligibility persists. The old code
|
|
347
|
+
// did clearTimeout+setTimeout(full delay) on every fresh render, so any
|
|
348
|
+
// trailing event < autoCloseMs apart slid the deadline forever and the
|
|
349
|
+
// countdown froze while the sidebar stayed open indefinitely.
|
|
335
350
|
const isTerminal = ["completed", "failed", "cancelled", "blocked"].includes(run.status);
|
|
336
351
|
const hasActiveAgents = agents.some((a) => a.status === "running");
|
|
337
352
|
if (isTerminal && !hasActiveAgents && !this.hasAutoClosed) {
|
|
338
353
|
const autoCloseMs = this.config?.autoCloseDashboardMs ?? 3000;
|
|
339
354
|
if (autoCloseMs > 0) {
|
|
340
|
-
if (this.
|
|
341
|
-
|
|
342
|
-
this.
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
this.
|
|
346
|
-
lines.push(this.bodyRow(this.theme.fg("dim", `auto-close in ${Math.round(autoCloseMs / 1000)}s…`), budget));
|
|
355
|
+
if (this.autoCloseDeadline === undefined) {
|
|
356
|
+
this.autoCloseDeadline = Date.now() + autoCloseMs;
|
|
357
|
+
this.armAutoClose(this.autoCloseDeadline);
|
|
358
|
+
}
|
|
359
|
+
const remainingMs = Math.max(0, this.autoCloseDeadline - Date.now());
|
|
360
|
+
lines.push(this.bodyRow(this.theme.fg("dim", `auto-close in ${Math.ceil(remainingMs / 1000)}s…`), budget));
|
|
347
361
|
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
this.
|
|
362
|
+
} else {
|
|
363
|
+
// Off the terminal window (run resumed, active agents (re)appeared) or
|
|
364
|
+
// already closed: cancel AND forget the deadline — the NEXT eligibility
|
|
365
|
+
// captures a fresh one. hasAutoClosed keeps done() from re-arming.
|
|
366
|
+
this.clearAutoClose();
|
|
353
367
|
}
|
|
354
368
|
this.cachedLines = renderLines(this.renderFrame(lines, budget, slot), w);
|
|
355
369
|
this.cachedSignature = signature;
|
|
@@ -363,13 +377,69 @@ export class LiveRunSidebar {
|
|
|
363
377
|
return railLine(RAIL.body, "border", content, this.theme, budget);
|
|
364
378
|
}
|
|
365
379
|
|
|
366
|
-
/**
|
|
380
|
+
/** L4: arm the close callback for a FIXED deadline. Re-renders never touch
|
|
381
|
+
* it, so the close lands at firstEligibility + autoCloseMs regardless of
|
|
382
|
+
* how many trailing events repaint the frame in between. */
|
|
383
|
+
private armAutoClose(deadline: number): void {
|
|
384
|
+
if (this.autoCloseTimeout) clearTimeout(this.autoCloseTimeout);
|
|
385
|
+
this.autoCloseTimeout = setTimeout(
|
|
386
|
+
() => {
|
|
387
|
+
this.hasAutoClosed = true;
|
|
388
|
+
this.done(undefined);
|
|
389
|
+
},
|
|
390
|
+
Math.max(0, deadline - Date.now()),
|
|
391
|
+
);
|
|
392
|
+
this.autoCloseTimeout?.unref();
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** L4: cancel the fixed deadline — only a NEW eligibility re-arms it. */
|
|
396
|
+
private clearAutoClose(): void {
|
|
397
|
+
if (this.autoCloseTimeout) {
|
|
398
|
+
clearTimeout(this.autoCloseTimeout);
|
|
399
|
+
this.autoCloseTimeout = undefined;
|
|
400
|
+
}
|
|
401
|
+
this.autoCloseDeadline = undefined;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** Append the `┗ <hint>` cap, lock the frame to ONE stable height (L6)
|
|
405
|
+
* and colorize the glyphs (F-1 / V-3). */
|
|
367
406
|
private renderFrame(lines: string[], budget: number, slot: RailSlot): string[] {
|
|
368
407
|
const hint = this.theme.fg("dim", formatHint(SIDEBAR_HINT));
|
|
369
408
|
const framed = [...lines, railLine(RAIL.close, slot, hint, this.theme, budget)];
|
|
409
|
+
this.lockHeight(framed, budget);
|
|
370
410
|
return framed.map((entry) => this.colorLine(entry));
|
|
371
411
|
}
|
|
372
412
|
|
|
413
|
+
/**
|
|
414
|
+
* L6 (real-test 2026-10-07): pad with blank rail rows (cap stays LAST) or
|
|
415
|
+
* slice (cap preserved) so EVERY frame — the 3-line pre-load frame and the
|
|
416
|
+
* 20+ line loaded frame — is exactly targetHeight() lines. The host
|
|
417
|
+
* positions overlays by the returned line count; a frame that flips
|
|
418
|
+
* between heights moves the anchor row and the differential renderer
|
|
419
|
+
* leaves a ghost footprint below the sidebar. Same contract as
|
|
420
|
+
* run-dashboard's targetHeight() clamp.
|
|
421
|
+
*/
|
|
422
|
+
private lockHeight(lines: string[], budget: number): void {
|
|
423
|
+
const target = this.targetHeight();
|
|
424
|
+
if (lines.length < target) {
|
|
425
|
+
const blankRow = railLine(RAIL.body, "border", "", this.theme, budget);
|
|
426
|
+
const bottom = lines.pop();
|
|
427
|
+
while (lines.length < target - 1) lines.push(blankRow);
|
|
428
|
+
if (bottom !== undefined) lines.push(bottom);
|
|
429
|
+
} else if (lines.length > target) {
|
|
430
|
+
const bottom = lines[lines.length - 1];
|
|
431
|
+
lines.length = target - 1;
|
|
432
|
+
lines.push(bottom);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** L6: stable overlay height — clamped like run-dashboard's targetHeight()
|
|
437
|
+
* but scaled for the sidebar's single column (12–30 instead of 12–36). */
|
|
438
|
+
private targetHeight(): number {
|
|
439
|
+
const rows = Number.isFinite(process.stdout?.rows) ? Number(process.stdout?.rows) : 30;
|
|
440
|
+
return Math.max(12, Math.min(30, rows - 2));
|
|
441
|
+
}
|
|
442
|
+
|
|
373
443
|
handleInput(data: string): void {
|
|
374
444
|
if (data === "q" || data === "\u001b") this.done(undefined);
|
|
375
445
|
}
|
|
@@ -14,11 +14,16 @@
|
|
|
14
14
|
* canopy/hint rows are NOT part of the split any more, so the hint keeps its
|
|
15
15
|
* full width instead of being cropped to 60% of the overlay.
|
|
16
16
|
*
|
|
17
|
+
* R3-6 (IME): the ACTIVE field emits pi-tui's zero-width `CURSOR_MARKER` at
|
|
18
|
+
* its value end — the host TUI strips it and parks the hardware cursor there
|
|
19
|
+
* so the IME candidate window follows the logical text cursor (CJK input).
|
|
20
|
+
*
|
|
17
21
|
* Keys/behaviour are unchanged (same `overlay:*` dispatch, same free-text
|
|
18
22
|
* passthrough, same validation errors, same discard confirmation). Hints come
|
|
19
23
|
* from `formatHint` (discard LAST, keys through `keyToken`).
|
|
20
24
|
*/
|
|
21
25
|
|
|
26
|
+
import * as piTui from "@earendil-works/pi-tui";
|
|
22
27
|
import type { MailboxDirection } from "../../state/coordination/mailbox.ts";
|
|
23
28
|
import { pad, sanitizeLine, truncate } from "../../utils/visual.ts";
|
|
24
29
|
import { overlayActionForKey } from "../keybinding-map.ts";
|
|
@@ -27,6 +32,22 @@ import { asCrewTheme, type CrewTheme } from "../theme-adapter.ts";
|
|
|
27
32
|
import { ConfirmOverlay } from "./confirm-overlay.ts";
|
|
28
33
|
import { renderComposePreview } from "./mailbox-compose-preview.ts";
|
|
29
34
|
|
|
35
|
+
/** Resolve pi-tui's zero-width IME cursor anchor defensively (L8, policy
|
|
36
|
+
* W4/G22) — same typeof-guard shape as hyperlink() at widget-renderer.ts:
|
|
37
|
+
* 118-128 and the sibling resolver in settings-overlay.ts. The peer range is
|
|
38
|
+
* `*`, so a host running a pi-tui build without the export must still render
|
|
39
|
+
* instead of failing the named import at link time. Exported for the
|
|
40
|
+
* fallback pin in test/unit/ui/settings-overlay-cursor-marker.test.ts. */
|
|
41
|
+
export function resolveCursorMarker(mod: unknown): string {
|
|
42
|
+
const candidate = (mod as { CURSOR_MARKER?: unknown }).CURSOR_MARKER;
|
|
43
|
+
return typeof candidate === "string" ? candidate : "";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** "" on hosts whose pi-tui predates CURSOR_MARKER — the active field then
|
|
47
|
+
* renders without the IME anchor (marker is zero-width, stripped before
|
|
48
|
+
* painting; pure enhancement, never a layout dependency). */
|
|
49
|
+
const CURSOR_MARKER = resolveCursorMarker(piTui);
|
|
50
|
+
|
|
30
51
|
export interface MailboxComposePayload {
|
|
31
52
|
from: string;
|
|
32
53
|
to: string;
|
|
@@ -117,7 +138,16 @@ export class MailboxComposeOverlay {
|
|
|
117
138
|
// (the field is a single-line cell; multi-line content belongs to the
|
|
118
139
|
// preview column).
|
|
119
140
|
const value = sanitizeLine(this.fields[field] ?? "");
|
|
120
|
-
|
|
141
|
+
// R3-6 (IME): CURSOR_MARKER is pi-tui's zero-width APC sequence — the host
|
|
142
|
+
// TUI finds it, positions the HARDWARE cursor there (IME candidate window
|
|
143
|
+
// anchor for CJK input), and strips it before painting. This overlay's
|
|
144
|
+
// fields append/backspace at end-of-value only, so the logical cursor IS
|
|
145
|
+
// the value end — same pattern as pi-tui's own Input component
|
|
146
|
+
// (marker + fake cursor). The `›` prefix stays: it is the RAIL list-selection
|
|
147
|
+
// glyph, orthogonal to the text cursor. Direction checkbox row gets NO
|
|
148
|
+
// marker — it toggles on space and has no text insertion point.
|
|
149
|
+
const marker = active && !this.confirm ? CURSOR_MARKER : "";
|
|
150
|
+
return `${active ? CURSOR : " "} ${label}: ${truncate(value, Math.max(8, width - label.length - 5))}${marker}`;
|
|
121
151
|
}
|
|
122
152
|
|
|
123
153
|
private activeName(): FieldName {
|
package/src/ui/rail.ts
CHANGED
|
@@ -58,7 +58,20 @@ export const RAIL = { open: "┏", section: "┣", body: "┃", close: "┗" } a
|
|
|
58
58
|
*/
|
|
59
59
|
export const PI_EXPAND_CHORD = "ctrl+o";
|
|
60
60
|
|
|
61
|
-
/** Selection cursor (lists) and active/step marker (sections, goals).
|
|
61
|
+
/** Selection cursor (lists) and active/step marker (sections, goals).
|
|
62
|
+
*
|
|
63
|
+
* R3-6 audit (2026-10-04): this glyph marks LIST SELECTION in dashboards,
|
|
64
|
+
* pickers, settings menus, and compose field rows — a navigation affordance,
|
|
65
|
+
* not a text insertion point. It therefore deliberately does NOT adopt
|
|
66
|
+
* pi-tui's `CURSOR_MARKER` (the zero-width APC sequence that anchors the
|
|
67
|
+
* IME candidate window at a TEXT cursor). Sites that DO own a text cursor —
|
|
68
|
+
* mailbox-compose field values, settings text-input/edit buffers — emit
|
|
69
|
+
* `CURSOR_MARKER` at their buffer end in addition to any visual glyph.
|
|
70
|
+
* Not converted, by design: run-dashboard run rows, settings menu rows,
|
|
71
|
+
* agents-jobs-browser rows, mailbox-detail rows, agent-picker rows,
|
|
72
|
+
* schedules-pane job rows (all list selection); widget dock `❯` (a
|
|
73
|
+
* single-line focus acknowledgement, not an input); mascot `█` frames
|
|
74
|
+
* (animation glyphs). */
|
|
62
75
|
export const CURSOR = "›";
|
|
63
76
|
export const ACTIVE = "▸";
|
|
64
77
|
|
|
@@ -19,6 +19,7 @@ import type { MailboxDirection, MailboxMessage } from "../state/coordination/mai
|
|
|
19
19
|
import { appendEventBuffered } from "../state/event-log/event-log.ts";
|
|
20
20
|
import { loadRunManifestById, saveRunTasks } from "../state/stores/state-store.ts";
|
|
21
21
|
import { logInternalError } from "../utils/internal-error.ts";
|
|
22
|
+
import { formatCount } from "./format-helpers.ts";
|
|
22
23
|
|
|
23
24
|
export interface RunActionResult {
|
|
24
25
|
ok: boolean;
|
|
@@ -107,7 +108,7 @@ export async function dispatchMailboxAckAll(ctx: ExtensionContext, runId: string
|
|
|
107
108
|
}
|
|
108
109
|
return {
|
|
109
110
|
ok: true,
|
|
110
|
-
message: `Acknowledged ${count}
|
|
111
|
+
message: `Acknowledged ${formatCount(count, "message")}.`,
|
|
111
112
|
data: { count },
|
|
112
113
|
};
|
|
113
114
|
}
|
package/src/ui/run-dashboard.ts
CHANGED
|
@@ -28,7 +28,7 @@ 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 { formatCount, teamWorkflowLabel } from "./format-helpers.ts";
|
|
31
|
+
import { formatCount, formatDuration, formatTokens, teamWorkflowLabel } from "./format-helpers.ts";
|
|
32
32
|
import { goalFlagSuffix } from "./goal-flag.ts";
|
|
33
33
|
import { type DashboardKeyAction, dashboardActionForKey } from "./keybinding-map.ts";
|
|
34
34
|
import { HelpOverlay } from "./overlays/help-overlay.ts";
|
|
@@ -228,14 +228,14 @@ function runListWindow(scrollOffset: number, count: number): RunListWindow {
|
|
|
228
228
|
return { slots, hasTop, hasBottom };
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
/** L7 (real-test 2026-10-07): agent age in the shared formatDuration dialect
|
|
232
|
+
* (`5m44s`), rendered bare — the retired `age=` prefix was wire format.
|
|
233
|
+
* Sub-second ages read `now` (quieter than `123ms` on a rail row). */
|
|
231
234
|
function formatAge(iso: string | undefined): string | undefined {
|
|
232
235
|
if (!iso) return undefined;
|
|
233
236
|
const ms = Math.max(0, Date.now() - new Date(iso).getTime());
|
|
234
237
|
if (!Number.isFinite(ms)) return undefined;
|
|
235
|
-
|
|
236
|
-
if (ms < 60_000) return `${Math.floor(ms / 1000)}s`;
|
|
237
|
-
if (ms < 3_600_000) return `${Math.floor(ms / 60_000)}m`;
|
|
238
|
-
return `${Math.floor(ms / 3_600_000)}h`;
|
|
238
|
+
return ms < 1000 ? "now" : formatDuration(ms);
|
|
239
239
|
}
|
|
240
240
|
|
|
241
241
|
function readProgressPreview(run: TeamRunManifest, maxLines = 5, snapshotCache?: RunSnapshotCache, resolve?: SnapshotResolver): string[] {
|
|
@@ -271,17 +271,21 @@ function readProgressPreview(run: TeamRunManifest, maxLines = 5, snapshotCache?:
|
|
|
271
271
|
}
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
-
|
|
274
|
+
/**
|
|
275
|
+
* L7 (real-test 2026-10-07): TUI usage dialect — `↑1.0k ↓250 $0.012` —
|
|
276
|
+
* mirroring the compactUsage pattern in live-run-sidebar.ts (kept local
|
|
277
|
+
* because the sidebar's helper is module-private; do not edit that file).
|
|
278
|
+
* Replaces the retired `tok=…/in=…/out=…/cache=…/$…` wire format, which
|
|
279
|
+
* leaked log-line grammar onto rail rows and buried the numbers behind
|
|
280
|
+
* key=value prefixes. cacheRead/cacheWrite stay hidden (sidebar parity).
|
|
281
|
+
*/
|
|
282
|
+
function compactUsage(usage: UsageState | undefined): string | undefined {
|
|
275
283
|
if (!usage) return undefined;
|
|
276
|
-
const
|
|
277
|
-
if (
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
if (usage.output) parts.push(`out=${usage.output}`);
|
|
282
|
-
if (usage.cacheRead) parts.push(`cache=${usage.cacheRead}`);
|
|
283
|
-
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
|
|
284
|
-
return parts.join("/");
|
|
284
|
+
const parts: string[] = [];
|
|
285
|
+
if (usage.input) parts.push(`↑${formatTokens(usage.input)}`);
|
|
286
|
+
if (usage.output) parts.push(`↓${formatTokens(usage.output)}`);
|
|
287
|
+
if (usage.cost) parts.push(`$${usage.cost.toFixed(3)}`);
|
|
288
|
+
return parts.length ? parts.join(" ") : undefined;
|
|
285
289
|
}
|
|
286
290
|
|
|
287
291
|
function snapshotFor(run: TeamRunManifest, snapshotCache?: RunSnapshotCache): RunUiSnapshot | undefined {
|
|
@@ -292,6 +296,38 @@ function snapshotFor(run: TeamRunManifest, snapshotCache?: RunSnapshotCache): Ru
|
|
|
292
296
|
}
|
|
293
297
|
}
|
|
294
298
|
|
|
299
|
+
/**
|
|
300
|
+
* L5 (real-test 2026-10-07): paint-path snapshot read for the frame-local
|
|
301
|
+
* SnapshotResolver in `renderUnsafe`. Uses the cache's `readForRender`
|
|
302
|
+
* accessor when present — cached snapshot ONLY; a lapsed TTL schedules the
|
|
303
|
+
* coalesced async refresh off the paint path; a missing entry schedules one
|
|
304
|
+
* refresh so the pre-load rows fill on a later frame (sidebar precedent,
|
|
305
|
+
* live-run-sidebar.ts render()). Feature-detected because
|
|
306
|
+
* `RunDashboardOptions.snapshotCache` keeps the BASE interface
|
|
307
|
+
* (snapshot-types.ts): production wires the extended cache, while test mocks
|
|
308
|
+
* and legacy callers may implement only get/refreshIfStale — for those a
|
|
309
|
+
* plain get() keeps the render read-only. Keypress handlers and other
|
|
310
|
+
* read-your-writes sites keep `snapshotFor()` (sync refreshIfStale) — Tier 11a.
|
|
311
|
+
*/
|
|
312
|
+
function snapshotForRender(run: TeamRunManifest, snapshotCache?: RunSnapshotCache): RunUiSnapshot | undefined {
|
|
313
|
+
const cache = snapshotCache as
|
|
314
|
+
| (RunSnapshotCache & {
|
|
315
|
+
readForRender?: (runId: string) => RunUiSnapshot | undefined;
|
|
316
|
+
scheduleRefresh?: (runId: string) => void;
|
|
317
|
+
})
|
|
318
|
+
| undefined;
|
|
319
|
+
if (typeof cache?.readForRender === "function") {
|
|
320
|
+
try {
|
|
321
|
+
const snapshot = cache.readForRender(run.runId);
|
|
322
|
+
if (!snapshot && typeof cache.scheduleRefresh === "function") cache.scheduleRefresh(run.runId);
|
|
323
|
+
return snapshot;
|
|
324
|
+
} catch {
|
|
325
|
+
return snapshotCache?.get(run.runId);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return snapshotCache?.get(run.runId);
|
|
329
|
+
}
|
|
330
|
+
|
|
295
331
|
/**
|
|
296
332
|
* PERF (2026-08-24, task 20) — per-frame snapshot resolver. The render path
|
|
297
333
|
* used to resolve each run's snapshot 6-8 times per frame (refreshRuns,
|
|
@@ -344,16 +380,16 @@ function usageForAgent(agent: CrewAgentRecord, task: TeamTaskState | undefined):
|
|
|
344
380
|
function agentPreviewLine(agent: CrewAgentRecord, task: TeamTaskState | undefined, options: RunDashboardOptions): string {
|
|
345
381
|
const stats = [
|
|
346
382
|
agent.progress?.activityState,
|
|
347
|
-
options.showModel !== false
|
|
383
|
+
options.showModel !== false ? modelForAgent(agent, task) : undefined,
|
|
348
384
|
options.showTokens !== false
|
|
349
|
-
? (
|
|
350
|
-
(agent.progress?.tokens !== undefined ?
|
|
385
|
+
? (compactUsage(usageForAgent(agent, task)) ??
|
|
386
|
+
(agent.progress?.tokens !== undefined ? `↑${formatTokens(agent.progress.tokens)}` : undefined))
|
|
351
387
|
: undefined,
|
|
352
388
|
options.showTools !== false && agent.progress?.currentTool ? `tool=${agent.progress.currentTool}` : undefined,
|
|
353
389
|
options.showTools !== false && agent.toolUses !== undefined ? formatCount(agent.toolUses, "tool") : undefined,
|
|
354
|
-
agent.progress?.turns !== undefined ?
|
|
390
|
+
agent.progress?.turns !== undefined ? formatCount(agent.progress.turns, "turn") : undefined,
|
|
355
391
|
agent.progress?.failedTool ? `failedTool=${agent.progress.failedTool}` : undefined,
|
|
356
|
-
agent.startedAt ?
|
|
392
|
+
agent.startedAt ? formatAge(agent.completedAt ?? agent.startedAt) : undefined,
|
|
357
393
|
].filter((part): part is string => Boolean(part));
|
|
358
394
|
const recent = agent.progress?.recentOutput?.at(-1);
|
|
359
395
|
const icon = iconForStatus(agent.status, {
|
|
@@ -394,7 +430,8 @@ function readAgentPreview(run: TeamRunManifest, maxLines = 5, options: RunDashbo
|
|
|
394
430
|
cost: number;
|
|
395
431
|
},
|
|
396
432
|
);
|
|
397
|
-
const
|
|
433
|
+
const usage = compactUsage(totals);
|
|
434
|
+
const header = usage ? `Agents: ${usage}` : "Agents:";
|
|
398
435
|
return [
|
|
399
436
|
header,
|
|
400
437
|
...agents.slice(0, maxLines).map((agent) => agentPreviewLine(agent, taskForAgent(tasks, agent), options)),
|
|
@@ -834,9 +871,12 @@ export class RunDashboard implements DashboardComponent {
|
|
|
834
871
|
// PERF (2026-08-24): snapshot resolution stat'd 7-8 files per run 6-8
|
|
835
872
|
// times per frame. Resolve once per frame into a local map and thread
|
|
836
873
|
// it through every render-path consumer below.
|
|
874
|
+
// L5 (real-test 2026-10-07): the resolver reads the cache ONLY
|
|
875
|
+
// (snapshotForRender → readForRender) — the sync refreshIfStale rebuild
|
|
876
|
+
// moved off the paint path; refreshes are coalesced + async.
|
|
837
877
|
const frameSnapshots = new Map<string, RunUiSnapshot | undefined>();
|
|
838
878
|
const snapshotOnce: SnapshotResolver = (run) => {
|
|
839
|
-
if (!frameSnapshots.has(run.runId)) frameSnapshots.set(run.runId,
|
|
879
|
+
if (!frameSnapshots.has(run.runId)) frameSnapshots.set(run.runId, snapshotForRender(run, this.options.snapshotCache));
|
|
840
880
|
return frameSnapshots.get(run.runId);
|
|
841
881
|
};
|
|
842
882
|
this.refreshRuns(snapshotOnce);
|
|
@@ -27,6 +27,14 @@ 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
|
+
* L5 (real-test 2026-10-07): paint-path read — cached snapshot only.
|
|
32
|
+
* NEVER sync-rebuilds: a lapsed TTL schedules the coalesced async refresh
|
|
33
|
+
* (scheduleRefresh pipeline) instead of rebuilding on the paint path, and a
|
|
34
|
+
* missing entry returns undefined without building. See the method doc on
|
|
35
|
+
* the implementation for the staleness/Tier-11a contract.
|
|
36
|
+
*/
|
|
37
|
+
readForRender(runId: string): RunUiSnapshot | undefined;
|
|
30
38
|
/**
|
|
31
39
|
* F13 (RR-018): true once dispose() has run. Callers that retain the cache
|
|
32
40
|
* (RegistrationContext.getManifestCache/getRunSnapshotCache) use this to
|
|
@@ -1149,6 +1157,30 @@ export function createRunSnapshotCache(cwd: string, options: RunSnapshotCacheOpt
|
|
|
1149
1157
|
scheduleRefresh(runId: string): void {
|
|
1150
1158
|
scheduleCoalescedRefresh(runId);
|
|
1151
1159
|
},
|
|
1160
|
+
/**
|
|
1161
|
+
* L5 (real-test 2026-10-07): read for the paint path. Returns the
|
|
1162
|
+
* CACHED snapshot (touching LRU/access) and NEVER sync-rebuilds — no
|
|
1163
|
+
* manifest/tasks parse, no agents.json read, no sha256 between paints.
|
|
1164
|
+
* When the entry's TTL has lapsed (the read-time proxy for "stamps may
|
|
1165
|
+
* look stale"), it schedules the EXISTING coalesced async refresh
|
|
1166
|
+
* (scheduleRefresh → 80ms coalesce → preloadStale); the actual stamp
|
|
1167
|
+
* comparison happens inside preloadStale, where a stamp-equal hit just
|
|
1168
|
+
* re-stamps the entry (no rebuild), so a quiet run costs one async stat
|
|
1169
|
+
* round per TTL window, off the paint path. A missing entry returns
|
|
1170
|
+
* undefined without building and without scheduling — callers render
|
|
1171
|
+
* their loading state and may call scheduleRefresh()/preloadStale()
|
|
1172
|
+
* themselves when they want the entry filled.
|
|
1173
|
+
* Tier 11a (read-your-writes) is preserved: refresh()/refreshIfStale()
|
|
1174
|
+
* below keep their sync build paths untouched for callers with a sync
|
|
1175
|
+
* reader immediately after write.
|
|
1176
|
+
*/
|
|
1177
|
+
readForRender(runId: string): RunUiSnapshot | undefined {
|
|
1178
|
+
const entry = entries.get(runId);
|
|
1179
|
+
if (!entry) return undefined;
|
|
1180
|
+
const snapshot = touch(runId, entry);
|
|
1181
|
+
if (Date.now() - entry.loadedAtMs >= ttlMs) scheduleCoalescedRefresh(runId);
|
|
1182
|
+
return snapshot;
|
|
1183
|
+
},
|
|
1152
1184
|
preloadStale,
|
|
1153
1185
|
preloadAllStale,
|
|
1154
1186
|
invalidate(runId?: string): void {
|