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
|
@@ -7,15 +7,17 @@
|
|
|
7
7
|
* precedent: a dedicated bottom-anchored overlay, NOT a new dashboard pane —
|
|
8
8
|
* the pane route would churn the ActivePane union + keybinding parity goldens.
|
|
9
9
|
*
|
|
10
|
-
* Layout (width-aware for terminals 80..250):
|
|
10
|
+
* Layout (width-aware for terminals 80..250) — RAIL frame (M4, 2026-09-16):
|
|
11
|
+
* canopy + rail-prefixed body rows + `┗` cap, the two columns joined by DOT
|
|
12
|
+
* LEADERS (no inner `│` frame column):
|
|
11
13
|
*
|
|
12
|
-
*
|
|
13
|
-
* Live agents (2)
|
|
14
|
-
* › ⠋ Explorer · 41 tok/s
|
|
15
|
-
*
|
|
16
|
-
* Scheduled jobs (1)
|
|
17
|
-
*
|
|
18
|
-
*
|
|
14
|
+
* ┏ AGENTS ▸ 2 agents · 1 job ············· ↑/↓ move · Enter transcript
|
|
15
|
+
* ┃ Live agents (2) ··············· ▸ adaptive-01-executor
|
|
16
|
+
* ┃ › ⠋ Explorer · 41 tok/s ··············· 0/1 tasks · 1 agents
|
|
17
|
+
* ┃ ✓ Writer ··············· ⠋ adaptive-01-executor
|
|
18
|
+
* ┃ Scheduled jobs (1) ··············· reading…
|
|
19
|
+
* ┃ ⏰ watch: omo · cron · next 23h ······
|
|
20
|
+
* ┗ Q close
|
|
19
21
|
*
|
|
20
22
|
* LEFT column — unified list: live agents first (status glyph via
|
|
21
23
|
* iconForStatus/spinnerFrame, role name, tok/s while running — the SAME
|
|
@@ -67,10 +69,11 @@ import { pad, sanitizeLine, truncate, visibleWidth } from "../utils/visual.ts";
|
|
|
67
69
|
import { renderAgentsPane } from "./dashboard-panes/agents-pane.ts";
|
|
68
70
|
import { renderScheduleDetails, schedulesHiddenJobsHintLine } from "./dashboard-panes/schedules-pane.ts";
|
|
69
71
|
import { computeLiveDurationMs } from "./live-duration.ts";
|
|
72
|
+
import { CURSOR, canopyLine, formatHint, overflowHint, RAIL, railLeaders, railLine } from "./rail.ts";
|
|
70
73
|
import type { RunUiSnapshot } from "./snapshot-types.ts";
|
|
71
74
|
import { spinnerFrame } from "./spinner.ts";
|
|
72
75
|
import { iconForStatus } from "./status-colors.ts";
|
|
73
|
-
import type
|
|
76
|
+
import { asCrewTheme, type CrewTheme } from "./theme-adapter.ts";
|
|
74
77
|
import { activeWidgetRuns } from "./widget/widget-model.ts";
|
|
75
78
|
|
|
76
79
|
// ─── Public data types ─────────────────────────────────────────────────────
|
|
@@ -182,6 +185,7 @@ function watchAgentTranscriptScript(): string | undefined {
|
|
|
182
185
|
/** tok/s for record-based entries (no live handle in this process). */
|
|
183
186
|
function recordTokPerSec(record: CrewAgentRecord, nowMs: number): number | undefined {
|
|
184
187
|
if (record.status !== "running") return undefined;
|
|
188
|
+
if (!record.taskId) return undefined;
|
|
185
189
|
try {
|
|
186
190
|
const usage = getTaskUsage(record.taskId);
|
|
187
191
|
const total = (usage.input ?? 0) + (usage.output ?? 0) + (usage.cacheWrite ?? 0);
|
|
@@ -228,6 +232,9 @@ function shellQuote(value: string): string {
|
|
|
228
232
|
|
|
229
233
|
export class AgentsJobsBrowser {
|
|
230
234
|
private readonly options: AgentsJobsBrowserOptions;
|
|
235
|
+
/** RAIL helpers need a theme; `asCrewTheme(undefined)` is the no-op theme,
|
|
236
|
+
* so a headless/test render stays plain text exactly as before. */
|
|
237
|
+
private readonly theme: CrewTheme;
|
|
231
238
|
private cachedEntries: AgentsJobsBrowserEntry[] = [];
|
|
232
239
|
private cachedAt = -Infinity;
|
|
233
240
|
private hiddenCount = 0;
|
|
@@ -243,6 +250,7 @@ export class AgentsJobsBrowser {
|
|
|
243
250
|
|
|
244
251
|
constructor(options: AgentsJobsBrowserOptions) {
|
|
245
252
|
this.options = options;
|
|
253
|
+
this.theme = options.theme ?? asCrewTheme(undefined);
|
|
246
254
|
this.refreshData(true);
|
|
247
255
|
this.pollTimer = setInterval(() => {
|
|
248
256
|
if (this.closed) return;
|
|
@@ -408,8 +416,8 @@ export class AgentsJobsBrowser {
|
|
|
408
416
|
const manifest = this.manifestFor(entry.runId);
|
|
409
417
|
if (!record || !manifest) {
|
|
410
418
|
return [
|
|
411
|
-
`▸ ${entry.taskId} (${entry.role})`,
|
|
412
|
-
` status: ${entry.status}`,
|
|
419
|
+
`▸ ${entry.taskId ?? "?"} (${entry.role ?? "?"})`,
|
|
420
|
+
` status: ${entry.status ?? "?"}`,
|
|
413
421
|
" (agent record unavailable — run manifest not found)",
|
|
414
422
|
];
|
|
415
423
|
}
|
|
@@ -626,36 +634,31 @@ export class AgentsJobsBrowser {
|
|
|
626
634
|
* exceed `width` (truncate/visibleWidth from utils/visual — the same
|
|
627
635
|
* width model pi-tui enforces). */
|
|
628
636
|
render(width?: number): string[] {
|
|
629
|
-
//
|
|
630
|
-
//
|
|
631
|
-
//
|
|
632
|
-
//
|
|
633
|
-
//
|
|
634
|
-
//
|
|
635
|
-
//
|
|
636
|
-
// Title + counts embed in the TOP border, key hints in the BOTTOM
|
|
637
|
-
// border (the omp EditorTopBorder idiom) so the body is pure content
|
|
638
|
-
// and the panel reads as one visual unit over the terminal.
|
|
637
|
+
// RAIL frame (M4, 2026-09-16) — the rounded box is retired. Canopy
|
|
638
|
+
// `┏ AGENTS ▸ 2 agents · 1 job` + hint segment, every body row prefixed
|
|
639
|
+
// with the rail, the two columns joined by DOT LEADERS (railLeaders
|
|
640
|
+
// absorbs the slack; NO inner `│` frame column) and the surface capped
|
|
641
|
+
// with `┗ <hint>`. railLine owns the glyph + space and the pad/truncate
|
|
642
|
+
// to `budget`, so no line can exceed `width`.
|
|
639
643
|
const w = Math.max(60, width ?? this.options.columns ?? 80);
|
|
644
|
+
const budget = Math.max(0, w - 2);
|
|
640
645
|
const nowMs = this.nowMs();
|
|
641
|
-
|
|
642
|
-
const inner = w - 7;
|
|
646
|
+
const inner = budget;
|
|
643
647
|
const listWidth = Math.max(20, Math.min(MAX_LIST_WIDTH, Math.round(inner * 0.42)));
|
|
644
|
-
const detailWidth = Math.max(16, inner - listWidth);
|
|
648
|
+
const detailWidth = Math.max(16, Math.max(0, inner - listWidth - 2));
|
|
645
649
|
const maxBody = Math.max(MIN_BODY, Math.min(MAX_BODY, (this.options.rows ?? 24) - 6));
|
|
646
|
-
const dim = (text: string) =>
|
|
647
|
-
const accent = (text: string) => (this.options.theme ? this.options.theme.fg("accent", text) : text);
|
|
648
|
-
const sep = dim("│");
|
|
650
|
+
const dim = (text: string) => this.theme.fg("border", text);
|
|
649
651
|
|
|
650
652
|
const counts = [
|
|
651
653
|
`${this.countAgents()} agent${this.countAgents() === 1 ? "" : "s"}`,
|
|
652
654
|
`${this.countJobs()} job${this.countJobs() === 1 ? "" : "s"}`,
|
|
653
655
|
];
|
|
654
656
|
if (this.hiddenCount > 0) counts.push(`${this.hiddenCount} hidden`);
|
|
655
|
-
|
|
656
|
-
const
|
|
657
|
+
// A transient notice (when one is live) replaces the counts in the canopy.
|
|
658
|
+
const subject = this.notice && this.notice.until > nowMs ? this.notice.text : counts.join(" · ");
|
|
659
|
+
const lines: string[] = [canopyLine({ word: "AGENTS", subject, right: this.hintRow(), theme: this.theme, budget })];
|
|
657
660
|
const hiddenHint = schedulesHiddenJobsHintLine(this.hiddenCount);
|
|
658
|
-
if (hiddenHint) lines.push(
|
|
661
|
+
if (hiddenHint) lines.push(railLine(RAIL.body, "border", dim(truncate(hiddenHint, budget)), this.theme, budget));
|
|
659
662
|
|
|
660
663
|
const left = this.renderListColumn(listWidth, maxBody, nowMs);
|
|
661
664
|
const right = this.renderDetailColumn(detailWidth, maxBody);
|
|
@@ -663,30 +666,20 @@ export class AgentsJobsBrowser {
|
|
|
663
666
|
// as tall as its tallest column, clamped to [5, maxBody].
|
|
664
667
|
const bodyHeight = Math.max(5, Math.min(maxBody, Math.max(left.length, right.length)));
|
|
665
668
|
for (let i = 0; i < bodyHeight; i++) {
|
|
666
|
-
const
|
|
667
|
-
const
|
|
668
|
-
|
|
669
|
+
const rawLeft = left[i] ?? "";
|
|
670
|
+
const rawRight = right[i] ?? "";
|
|
671
|
+
// Filler row (both columns exhausted): a bare rail row — dot leaders
|
|
672
|
+
// across an empty row would just paint noise.
|
|
673
|
+
const content =
|
|
674
|
+
rawLeft.trim() === "" && rawRight.trim() === ""
|
|
675
|
+
? ""
|
|
676
|
+
: railLeaders(pad(rawLeft, listWidth), truncate(rawRight, detailWidth), budget, this.theme);
|
|
677
|
+
lines.push(railLine(RAIL.body, "border", content, this.theme, budget));
|
|
669
678
|
}
|
|
670
|
-
lines.push(
|
|
679
|
+
lines.push(railLine(RAIL.close, "border", this.capHint(), this.theme, budget));
|
|
671
680
|
return lines;
|
|
672
681
|
}
|
|
673
682
|
|
|
674
|
-
/** `╭─ title ── right ─────╮` / `╰─ hint ───────────────╯` — always exactly `w` visible cells. */
|
|
675
|
-
private framedBorderRow(which: "top" | "bottom", title: string, right: string, w: number): string {
|
|
676
|
-
const cornerL = which === "top" ? "╭" : "╰";
|
|
677
|
-
const cornerR = which === "top" ? "╮" : "╯";
|
|
678
|
-
const rule = which === "top" ? "─" : "─";
|
|
679
|
-
const titleCells = visibleWidth(title);
|
|
680
|
-
const rightCells = right ? visibleWidth(right) : 0;
|
|
681
|
-
const budget = w - 2 - titleCells - rightCells;
|
|
682
|
-
if (budget < 1) {
|
|
683
|
-
// Degenerate narrow: title only, truncated.
|
|
684
|
-
const t = truncate(title, w - 2);
|
|
685
|
-
return `${cornerL}${t}${cornerR}`;
|
|
686
|
-
}
|
|
687
|
-
return `${cornerL}${title}${rule.repeat(budget)}${right}${cornerR}`;
|
|
688
|
-
}
|
|
689
|
-
|
|
690
683
|
private countAgents(): number {
|
|
691
684
|
return this.cachedEntries.filter((entry) => entry.kind === "agent").length;
|
|
692
685
|
}
|
|
@@ -695,14 +688,28 @@ export class AgentsJobsBrowser {
|
|
|
695
688
|
return this.cachedEntries.filter((entry) => entry.kind === "job").length;
|
|
696
689
|
}
|
|
697
690
|
|
|
691
|
+
/** Header hint (canopy right segment) — RAIL format: `keys label` pairs. */
|
|
698
692
|
private hintRow(): string {
|
|
699
|
-
|
|
700
|
-
|
|
693
|
+
return this.focus === "list"
|
|
694
|
+
? formatHint([
|
|
695
|
+
[["up", "down"], "move"],
|
|
696
|
+
["enter", "transcript"],
|
|
697
|
+
])
|
|
698
|
+
: formatHint([
|
|
699
|
+
[["up", "down"], "scroll"],
|
|
700
|
+
["enter", "back"],
|
|
701
|
+
]);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/** End cap hint — the close/cancel action, LAST (RAIL hint rule). */
|
|
705
|
+
private capHint(): string {
|
|
706
|
+
return this.focus === "list" ? formatHint([["q", "close"]]) : formatHint([["esc", "back"]]);
|
|
701
707
|
}
|
|
702
708
|
|
|
703
709
|
/** LEFT column: section labels + unified rows, windowed around selection. */
|
|
704
710
|
private renderListColumn(listWidth: number, bodyHeight: number, nowMs: number): string[] {
|
|
705
711
|
const out: string[] = [];
|
|
712
|
+
const dim = (text: string) => this.theme.fg("border", text);
|
|
706
713
|
const push = (text: string) => out.push(truncate(sanitizeLine(text), listWidth));
|
|
707
714
|
if (this.cachedEntries.length === 0) {
|
|
708
715
|
push("No live agents · no scheduled jobs");
|
|
@@ -712,9 +719,9 @@ export class AgentsJobsBrowser {
|
|
|
712
719
|
const jobs = this.cachedEntries.filter((entry): entry is AgentsBrowserJobEntry => entry.kind === "job");
|
|
713
720
|
// Rows: section labels + one row per entry, in merged order.
|
|
714
721
|
const rows: { entry?: AgentsJobsBrowserEntry; text: string }[] = [];
|
|
715
|
-
if (agents.length > 0) rows.push({ text: `Live agents (${agents.length})` });
|
|
722
|
+
if (agents.length > 0) rows.push({ text: dim(`Live agents (${agents.length})`) });
|
|
716
723
|
for (const entry of agents) rows.push({ entry, text: this.agentRow(entry, nowMs) });
|
|
717
|
-
if (jobs.length > 0) rows.push({ text: `Scheduled jobs (${jobs.length})` });
|
|
724
|
+
if (jobs.length > 0) rows.push({ text: dim(`Scheduled jobs (${jobs.length})`) });
|
|
718
725
|
for (const entry of jobs) rows.push({ entry, text: this.jobRow(entry) });
|
|
719
726
|
|
|
720
727
|
// Windowing: keep the selected entry's row inside the visible slot
|
|
@@ -727,7 +734,7 @@ export class AgentsJobsBrowser {
|
|
|
727
734
|
}
|
|
728
735
|
for (const row of rows.slice(this.listScroll, this.listScroll + slots)) {
|
|
729
736
|
const isSelected = row.entry !== undefined && this.indexOf(row.entry) === this.selected;
|
|
730
|
-
const marker = row.entry === undefined ? "" : isSelected ?
|
|
737
|
+
const marker = row.entry === undefined ? "" : isSelected ? `${CURSOR} ` : " ";
|
|
731
738
|
push(`${marker}${row.text}`);
|
|
732
739
|
}
|
|
733
740
|
return out;
|
|
@@ -738,9 +745,10 @@ export class AgentsJobsBrowser {
|
|
|
738
745
|
}
|
|
739
746
|
|
|
740
747
|
private agentRow(entry: AgentsBrowserAgentEntry, nowMs: number): string {
|
|
741
|
-
|
|
748
|
+
// GUARD (M4 §4): these fields come from unvalidated disk JSON.
|
|
749
|
+
const icon = iconForStatus(entry.status, { runningGlyph: spinnerFrame(entry.taskId ?? "", nowMs) });
|
|
742
750
|
const tok = entry.tokPerSec !== undefined && entry.status === "running" ? ` · ${entry.tokPerSec} tok/s` : "";
|
|
743
|
-
return `${icon} ${entry.role}${tok} · ${entry.taskId.slice(-8)}`;
|
|
751
|
+
return `${icon} ${entry.role ?? "?"}${tok} · ${(entry.taskId ?? "").slice(-8)}`;
|
|
744
752
|
}
|
|
745
753
|
|
|
746
754
|
private jobRow(entry: AgentsBrowserJobEntry): string {
|
|
@@ -765,16 +773,14 @@ export class AgentsJobsBrowser {
|
|
|
765
773
|
const maxScroll = Math.max(0, detail.length - (bodyHeight - 1));
|
|
766
774
|
this.detailScroll = Math.max(0, Math.min(this.detailScroll, maxScroll));
|
|
767
775
|
const windowed = detail.slice(this.detailScroll, this.detailScroll + bodyHeight - 1);
|
|
768
|
-
const title =
|
|
776
|
+
const title =
|
|
777
|
+
entry.kind === "agent" ? `▸ ${entry.role ?? "?"} · ${(entry.taskId ?? "").slice(-8)}` : `▸ ${sanitizeLine(entry.job.name)}`;
|
|
769
778
|
const out = [truncate(sanitizeLine(title), detailWidth)];
|
|
770
779
|
for (const line of windowed) out.push(truncate(sanitizeLine(line), detailWidth));
|
|
771
780
|
if (this.detailScroll > 0 || this.detailScroll + windowed.length < detail.length) {
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
detailWidth,
|
|
776
|
-
),
|
|
777
|
-
);
|
|
781
|
+
// RAIL overflow dialect only (`▲ n above · ▼ m below`).
|
|
782
|
+
const below = Math.max(0, detail.length - this.detailScroll - windowed.length);
|
|
783
|
+
out.push(truncate(overflowHint(this.detailScroll, below, this.theme), detailWidth));
|
|
778
784
|
}
|
|
779
785
|
return out;
|
|
780
786
|
}
|
package/src/ui/card-colors.ts
CHANGED
|
@@ -55,6 +55,35 @@ export function mixBg(base: Rgb, accent: Rgb, intensity: number): string {
|
|
|
55
55
|
|
|
56
56
|
const BLACK: Rgb = { r: 0, g: 0, b: 0 };
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* The background slots Pi's Theme actually exposes (`ThemeBg` in
|
|
60
|
+
* node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/theme.d.ts).
|
|
61
|
+
*
|
|
62
|
+
* P1-6 (2026-09-15): this probe used to pass "background", which is NOT a
|
|
63
|
+
* ThemeBg member. Pi's `getBgAnsi` throws `Unknown theme background color:
|
|
64
|
+
* background` for it, the surrounding catch swallowed the throw, and `base`
|
|
65
|
+
* stayed BLACK on every theme — so crew card tints were derived from the wrong
|
|
66
|
+
* base (visibly wrong on light themes). Never pass a slot outside this union.
|
|
67
|
+
* `selectedBg` is the slot whose value is the surrounding card/selection
|
|
68
|
+
* background; it is the one both Pi's `ThemeBg` and pi-crew's `CrewThemeBg`
|
|
69
|
+
* (theme-adapter.ts) define. Enforced by
|
|
70
|
+
* test/unit/round23-ui-unicode.test.ts (a fake theme records every slot).
|
|
71
|
+
*/
|
|
72
|
+
type ThemeBgSlot =
|
|
73
|
+
| "selectedBg"
|
|
74
|
+
| "scrollbarThumb"
|
|
75
|
+
| "userMessageBg"
|
|
76
|
+
| "customMessageBg"
|
|
77
|
+
| "toolPendingBg"
|
|
78
|
+
| "toolSuccessBg"
|
|
79
|
+
| "toolErrorBg";
|
|
80
|
+
|
|
81
|
+
/** Duck-typed view of Pi's Theme methods (CrewTheme does not declare them). */
|
|
82
|
+
type ThemeProbe = {
|
|
83
|
+
getBgAnsi?: (slot: ThemeBgSlot) => string | undefined;
|
|
84
|
+
getFgAnsi?: (slot: string) => string | undefined;
|
|
85
|
+
};
|
|
86
|
+
|
|
58
87
|
// ── Theme → background derivation ───────────────────────────────────────
|
|
59
88
|
|
|
60
89
|
/**
|
|
@@ -76,17 +105,17 @@ export function deriveCardBackground(
|
|
|
76
105
|
): string {
|
|
77
106
|
// Prefer the theme's bg color as base (matches the card's surroundings).
|
|
78
107
|
let base = BLACK;
|
|
79
|
-
const themeAny = theme as unknown as
|
|
80
|
-
getBgAnsi?: (slot: string) => string | undefined;
|
|
81
|
-
getFgAnsi?: (slot: string) => string | undefined;
|
|
82
|
-
};
|
|
108
|
+
const themeAny = theme as unknown as ThemeProbe;
|
|
83
109
|
if (themeAny.getBgAnsi) {
|
|
84
110
|
try {
|
|
85
|
-
|
|
86
|
-
|
|
111
|
+
// "selectedBg" is a valid ThemeBg slot (P1-6). The catch only guards
|
|
112
|
+
// third-party theme shims that throw on the probe; it can no longer
|
|
113
|
+
// hide a permanently-invalid slot, because the slot type and the
|
|
114
|
+
// regression test both pin it to the ThemeBg union.
|
|
115
|
+
const parsed = parseAnsiRgb(themeAny.getBgAnsi("selectedBg"));
|
|
87
116
|
if (parsed) base = parsed;
|
|
88
117
|
} catch {
|
|
89
|
-
/* fall back to black */
|
|
118
|
+
/* shim threw — fall back to black */
|
|
90
119
|
}
|
|
91
120
|
}
|
|
92
121
|
|
|
@@ -2,11 +2,14 @@ import type { CrewAgentRecord } from "../../runtime/crew-agent-runtime.ts";
|
|
|
2
2
|
import { type LiveAgentHandle, listLiveAgents, listLiveAgentsByWorkspace } from "../../runtime/live-session/live-agent-manager.ts";
|
|
3
3
|
import { formatCost } from "../../state/usage.ts";
|
|
4
4
|
import { visibleWidth } from "../../utils/visual.ts";
|
|
5
|
+
import { formatCount, formatDuration } from "../format-helpers.ts";
|
|
5
6
|
import { computeLiveDurationMs } from "../live-duration.ts";
|
|
7
|
+
import { overflowHint, statusIcon } from "../rail.ts";
|
|
6
8
|
import type { RunDashboardOptions } from "../run-dashboard.ts";
|
|
7
9
|
import type { RunUiSnapshot } from "../snapshot-types.ts";
|
|
8
10
|
import { spinnerFrame } from "../spinner.ts";
|
|
9
11
|
import { iconForStatus } from "../status-colors.ts";
|
|
12
|
+
import { PANE_THEME } from "./pane-theme.ts";
|
|
10
13
|
|
|
11
14
|
/**
|
|
12
15
|
* Fixed visible widths for the per-agent numeric metrics (finding V-1).
|
|
@@ -16,7 +19,7 @@ import { iconForStatus } from "../status-colors.ts";
|
|
|
16
19
|
*/
|
|
17
20
|
const TOKENS_METRIC_WIDTH = 5; // `1.2k`, `12.3k`, `123`
|
|
18
21
|
const COST_METRIC_WIDTH = 7; // `$0.0500`, `$1.50`
|
|
19
|
-
const DURATION_METRIC_WIDTH = 6; // `3.0s`, `59.9s`, `
|
|
22
|
+
const DURATION_METRIC_WIDTH = 6; // `3.0s`, `59.9s`, `5m14s` (formatDuration)
|
|
20
23
|
|
|
21
24
|
/**
|
|
22
25
|
* Width-aware right-align (padStart) for a numeric metric so its column stays
|
|
@@ -29,6 +32,30 @@ function alignMetric(value: string, width: number): string {
|
|
|
29
32
|
return gap > 0 ? " ".repeat(gap) + value : value;
|
|
30
33
|
}
|
|
31
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Agent status glyph, from the shared RAIL vocabulary (`statusIcon`). Two
|
|
37
|
+
* deliberate exceptions, both kept because rail.ts cannot express them:
|
|
38
|
+
* - `running` → the animated braille spinner (a live activity frame, not a
|
|
39
|
+
* static status icon);
|
|
40
|
+
* - `waiting`/`needs_attention`/`blocked` → ⏳/⚠/⏸: `statusIcon` has only
|
|
41
|
+
* four outputs (✓ ✗ ⟳ ○) and would collapse all three into `○`, losing
|
|
42
|
+
* the signal; the shared `status-colors` vocabulary is used for exactly
|
|
43
|
+
* these.
|
|
44
|
+
* `queued` now renders rail's `○` instead of the legacy `◦`.
|
|
45
|
+
*/
|
|
46
|
+
function agentGlyph(status: string | undefined, taskId: string | undefined): string {
|
|
47
|
+
switch (status) {
|
|
48
|
+
case "running":
|
|
49
|
+
return spinnerFrame(taskId ?? "");
|
|
50
|
+
case "waiting":
|
|
51
|
+
case "needs_attention":
|
|
52
|
+
case "blocked":
|
|
53
|
+
return iconForStatus(status);
|
|
54
|
+
default:
|
|
55
|
+
return statusIcon(status ?? "unknown", PANE_THEME);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
32
59
|
/**
|
|
33
60
|
* Returns true if this agent did real work (LLM call, tool use, or non-trivial duration).
|
|
34
61
|
* Scaffold-only agents (no tokens, no tools, no turns) are skipped in the agents pane —
|
|
@@ -99,17 +126,18 @@ export function renderAgentsPane(snapshot: RunUiSnapshot | undefined, options: R
|
|
|
99
126
|
const lineCount = Math.min(realAgents.length, 12);
|
|
100
127
|
const label =
|
|
101
128
|
realAgents.length !== snapshot.agents.length
|
|
102
|
-
? `${realAgents.length
|
|
103
|
-
:
|
|
129
|
+
? `${formatCount(realAgents.length, "real agent")} (${formatCount(snapshot.agents.length, "agent")} total)`
|
|
130
|
+
: formatCount(realAgents.length, "agent");
|
|
104
131
|
|
|
132
|
+
// `3/3 tasks · 3 agents` — the label arrives pre-pluralised by the caller
|
|
133
|
+
// ("N agents"); the task tally keeps the slash form on purpose (it reads as
|
|
134
|
+
// progress, not as a count).
|
|
105
135
|
lines.push(`${completed}/${total} tasks · ${label}`);
|
|
106
136
|
|
|
107
137
|
for (const agent of realAgents.slice(0, 12)) {
|
|
108
138
|
const liveHandle = liveForRun.find((h) => h.taskId === agent.taskId);
|
|
109
|
-
const icon =
|
|
110
|
-
|
|
111
|
-
});
|
|
112
|
-
const role = `${agent.role}`;
|
|
139
|
+
const icon = agentGlyph(agent.status, agent.taskId);
|
|
140
|
+
const role = `${agent.role ?? "?"}`;
|
|
113
141
|
|
|
114
142
|
// Compact activity line
|
|
115
143
|
const activity = liveHandle
|
|
@@ -145,23 +173,36 @@ export function renderAgentsPane(snapshot: RunUiSnapshot | undefined, options: R
|
|
|
145
173
|
// fired for EVERY running live agent in the dashboard. Use the shared,
|
|
146
174
|
// validated computeLiveDurationMs (mirrors widget-formatters.ts).
|
|
147
175
|
const ms = computeLiveDurationMs(liveHandle.activity, nowMs);
|
|
148
|
-
stats.push(alignMetric(
|
|
176
|
+
stats.push(alignMetric(formatDuration(ms), DURATION_METRIC_WIDTH));
|
|
149
177
|
if (options.showModel !== false && liveHandle.modelName && liveHandle.modelName !== "default") {
|
|
150
178
|
stats.push(liveHandle.modelName);
|
|
151
179
|
}
|
|
152
180
|
} else if (agent.startedAt) {
|
|
153
|
-
|
|
154
|
-
|
|
181
|
+
// Tier 13 (2026-09-21): a FINISHED agent's span must end at completedAt,
|
|
182
|
+
// never at the wall clock. The naive `nowMs - startedAt` made every
|
|
183
|
+
// completed agent of an old run inflate forever — the dashboard showed
|
|
184
|
+
// `01_explore … 29m39s` while the tool card showed the real `8m22s` for
|
|
185
|
+
// the same agent. Mirror computeLiveDurationMs: prefer a sane completedAt,
|
|
186
|
+
// else fall back to now; never emit a negative or absurd span.
|
|
187
|
+
const startedMs = new Date(agent.startedAt).getTime();
|
|
188
|
+
const rawCompleted = agent.completedAt ? new Date(agent.completedAt).getTime() : Number.NaN;
|
|
189
|
+
const completedMs = Number.isFinite(rawCompleted) && rawCompleted >= startedMs && rawCompleted <= nowMs ? rawCompleted : nowMs;
|
|
190
|
+
const ms = completedMs - startedMs;
|
|
191
|
+
if (Number.isFinite(ms) && ms >= 0) stats.push(alignMetric(formatDuration(ms), DURATION_METRIC_WIDTH));
|
|
155
192
|
}
|
|
156
193
|
|
|
157
194
|
const statsStr = stats.length ? ` · ${stats.join(" ")}` : "";
|
|
158
|
-
lines.push(` ${icon} ${agent.taskId} ${role}${statsStr}`);
|
|
195
|
+
lines.push(` ${icon} ${agent.taskId ?? "?"} ${role}${statsStr}`);
|
|
159
196
|
lines.push(` ${activity}`);
|
|
160
197
|
}
|
|
161
198
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
199
|
+
// Overflow in the ONE rail dialect (`▲ n above` / `▼ m below`), replacing
|
|
200
|
+
// the legacy `… +N more`. The count is derived from the SAME list the cap
|
|
201
|
+
// slices (`realAgents`), not from `snapshot.agents` — the pre-migration
|
|
202
|
+
// line could report rows that were never hidden (scaffold agents filtered
|
|
203
|
+
// out above).
|
|
204
|
+
const hiddenBelow = realAgents.length - 12;
|
|
205
|
+
if (hiddenBelow > 0) lines.push(` ${overflowHint(0, hiddenBelow, PANE_THEME)}`);
|
|
165
206
|
|
|
166
207
|
return lines;
|
|
167
208
|
}
|
|
@@ -1,47 +1,5 @@
|
|
|
1
1
|
import type { TeamRunManifest, TeamTaskState } from "../../state/types.ts";
|
|
2
2
|
|
|
3
|
-
export interface CancellationPaneOptions {
|
|
4
|
-
maxReasons?: number;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export function renderCancellationPane(manifest: TeamRunManifest, tasks: TeamTaskState[], opts: CancellationPaneOptions = {}): string[] {
|
|
8
|
-
const maxReasons = opts.maxReasons ?? 5;
|
|
9
|
-
if (manifest.status !== "cancelled" && manifest.status !== "blocked") {
|
|
10
|
-
const cancellingTasks = tasks.filter((t) => t.status === "cancelled");
|
|
11
|
-
if (cancellingTasks.length === 0) return ["Cancellation pane: no active cancellations"];
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const lines: string[] = ["Cancellation pane"];
|
|
15
|
-
|
|
16
|
-
if (manifest.status === "cancelled") {
|
|
17
|
-
lines.push(` Run status: cancelled`);
|
|
18
|
-
} else if (manifest.status === "blocked") {
|
|
19
|
-
lines.push(` Run status: blocked`);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const cancelledTasks = tasks.filter((t) => t.status === "cancelled");
|
|
23
|
-
if (cancelledTasks.length > 0) {
|
|
24
|
-
lines.push(` Cancelled tasks (${cancelledTasks.length}):`);
|
|
25
|
-
for (const task of cancelledTasks.slice(0, maxReasons)) {
|
|
26
|
-
const reason = task.error ?? "unknown";
|
|
27
|
-
lines.push(` ✗ ${task.id}: ${reason}`);
|
|
28
|
-
}
|
|
29
|
-
if (cancelledTasks.length > maxReasons) {
|
|
30
|
-
lines.push(` ... and ${cancelledTasks.length - maxReasons} more`);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (manifest.policyDecisions?.length) {
|
|
35
|
-
const decisions = manifest.policyDecisions.slice(0, maxReasons);
|
|
36
|
-
lines.push(` Policy decisions (${manifest.policyDecisions.length}):`);
|
|
37
|
-
for (const d of decisions) {
|
|
38
|
-
lines.push(` ${d.action}: ${d.message}`);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return lines;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
3
|
/**
|
|
46
4
|
* D-1 / L-2 — a one-line terminal-run reason for the dashboard detail row.
|
|
47
5
|
*
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { summarizeHeartbeats } from "../heartbeat-aggregator.ts";
|
|
2
|
+
import { formatHint } from "../rail.ts";
|
|
2
3
|
import type { RunUiSnapshot } from "../snapshot-types.ts";
|
|
3
4
|
|
|
4
5
|
export interface HealthPaneOptions {
|
|
@@ -19,12 +20,13 @@ export function renderHealthPane(snapshot: RunUiSnapshot | undefined, opts: Heal
|
|
|
19
20
|
`Health pane: ${summary.healthy}/${summary.totalTasks} healthy · stale=${summary.stale} · dead=${summary.dead} · missing=${summary.missing}`,
|
|
20
21
|
];
|
|
21
22
|
if (summary.worstStaleMs > 0) lines.push(`Worst stale: ${seconds(summary.worstStaleMs)} ago`);
|
|
22
|
-
|
|
23
|
+
// One hint format (rail.ts `formatHint`): keys `label` joined by ` · `.
|
|
24
|
+
const hints: Array<readonly [string, string]> = [];
|
|
23
25
|
const foreground = opts.isForeground !== false;
|
|
24
|
-
if ((summary.dead > 0 || summary.missing > 0) && foreground) hints.push("R recovery");
|
|
25
|
-
if ((summary.dead > 0 || summary.stale > 0) && foreground) hints.push("K kill stale");
|
|
26
|
-
hints.push("D diagnostic export");
|
|
27
|
-
lines.push(`Actions: ${hints
|
|
26
|
+
if ((summary.dead > 0 || summary.missing > 0) && foreground) hints.push(["R", "recovery"]);
|
|
27
|
+
if ((summary.dead > 0 || summary.stale > 0) && foreground) hints.push(["K", "kill stale"]);
|
|
28
|
+
hints.push(["D", "diagnostic export"]);
|
|
29
|
+
lines.push(`Actions: ${formatHint(hints)}`);
|
|
28
30
|
if (!foreground && (summary.dead > 0 || summary.missing > 0 || summary.stale > 0))
|
|
29
31
|
lines.push("Async run: R/K disabled — inspect process manually or use /team-api.");
|
|
30
32
|
return lines;
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
+
import { formatHint } from "../rail.ts";
|
|
1
2
|
import type { RunUiSnapshot } from "../snapshot-types.ts";
|
|
2
3
|
|
|
3
4
|
export function renderMailboxPane(snapshot: RunUiSnapshot | undefined): string[] {
|
|
4
5
|
if (!snapshot) return ["Mailbox pane: snapshot unavailable"];
|
|
5
6
|
const mailbox = snapshot.mailbox;
|
|
6
7
|
const approx = mailbox.approximate ? " · approximate (tail)" : "";
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
const unread = mailbox.inboxUnread ?? 0;
|
|
9
|
+
const outgoing = mailbox.outboxPending ?? 0;
|
|
10
|
+
const attention = mailbox.needsAttention ?? 0;
|
|
11
|
+
const lines: string[] = [`Mailbox pane: inbox unread=${unread} · outbox pending=${outgoing} · attention=${attention}${approx}`];
|
|
10
12
|
// Kind-separated breakdown
|
|
11
13
|
const kindParts: string[] = [];
|
|
12
14
|
const steer = mailbox.steerUnread ?? 0;
|
|
@@ -26,10 +28,24 @@ export function renderMailboxPane(snapshot: RunUiSnapshot | undefined): string[]
|
|
|
26
28
|
lines.push(` 📋 ${followUp} follow-up(s) pending review.`);
|
|
27
29
|
}
|
|
28
30
|
}
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
+
if (attention > 0) {
|
|
32
|
+
// One hint format (rail.ts `formatHint`): keys `label` joined by ` · `.
|
|
33
|
+
lines.push(
|
|
34
|
+
`Needs attention: press ${formatHint([
|
|
35
|
+
["Enter", "for detail"],
|
|
36
|
+
["A", "ack"],
|
|
37
|
+
["N", "nudge"],
|
|
38
|
+
["C", "compose"],
|
|
39
|
+
["X", "ack all"],
|
|
40
|
+
])}.`,
|
|
41
|
+
);
|
|
31
42
|
} else {
|
|
32
|
-
lines.push(
|
|
43
|
+
lines.push(
|
|
44
|
+
`No mailbox items need attention. Press ${formatHint([
|
|
45
|
+
["Enter", "for detail"],
|
|
46
|
+
["C", "compose"],
|
|
47
|
+
])}.`,
|
|
48
|
+
);
|
|
33
49
|
}
|
|
34
50
|
return lines;
|
|
35
51
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { MetricRegistry } from "../../observability/metric-registry.ts";
|
|
2
|
-
import type { HistogramPoint, MetricLabels, MetricPoint } from "../../observability/metrics-primitives.ts";
|
|
2
|
+
import type { HistogramPoint, MetricLabels, MetricLabelValue, MetricPoint } from "../../observability/metrics-primitives.ts";
|
|
3
3
|
import type { RunUiSnapshot } from "../snapshot-types.ts";
|
|
4
4
|
|
|
5
5
|
export interface MetricsPaneOptions {
|
|
@@ -7,9 +7,17 @@ export interface MetricsPaneOptions {
|
|
|
7
7
|
maxCounters?: number;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
// Metric label values come from an unvalidated `Record` (disk/producer fed) —
|
|
11
|
+
// every interpolation is guarded so `undefined` can never reach the pane.
|
|
12
|
+
function labelValue(value: MetricLabelValue | undefined): string {
|
|
13
|
+
if (value === undefined || value === null) return "?";
|
|
14
|
+
if (typeof value === "number" && !Number.isFinite(value)) return "?";
|
|
15
|
+
return `${value}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function labelsText(labels: MetricLabels | undefined): string {
|
|
19
|
+
const entries = Object.entries(labels ?? {});
|
|
20
|
+
return entries.length ? `{${entries.map(([key, value]) => `${key}=${labelValue(value)}`).join(",")}}` : "";
|
|
13
21
|
}
|
|
14
22
|
|
|
15
23
|
function isHistogramPoint(point: MetricPoint | HistogramPoint): point is HistogramPoint {
|
|
@@ -24,14 +32,14 @@ export function renderMetricsPane(_snapshot: RunUiSnapshot | undefined, opts: Me
|
|
|
24
32
|
for (const snapshot of snapshots.slice(0, opts.maxCounters ?? 10)) {
|
|
25
33
|
const first = snapshot.values[0];
|
|
26
34
|
if (!first) {
|
|
27
|
-
lines.push(`${snapshot.name}: empty`);
|
|
35
|
+
lines.push(`${snapshot.name ?? "?"}: empty`);
|
|
28
36
|
continue;
|
|
29
37
|
}
|
|
30
38
|
if (isHistogramPoint(first))
|
|
31
39
|
lines.push(
|
|
32
|
-
`${snapshot.name}${labelsText(first.labels)} count=${first.count} p95=${Number.isFinite(first.quantiles
|
|
40
|
+
`${snapshot.name ?? "?"}${labelsText(first.labels)} count=${first.count ?? 0} p95=${Number.isFinite(first.quantiles?.p95) ? Math.round(first.quantiles.p95) : "n/a"}`,
|
|
33
41
|
);
|
|
34
|
-
else lines.push(`${snapshot.name}${labelsText(first.labels)} ${first.value}`);
|
|
42
|
+
else lines.push(`${snapshot.name ?? "?"}${labelsText(first.labels)} ${labelValue(first.value)}`);
|
|
35
43
|
}
|
|
36
44
|
return lines;
|
|
37
45
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pane-theme.ts — the theme binding dashboard panes render RAIL primitives with.
|
|
3
|
+
*
|
|
4
|
+
* Contract (§2.G of docs/UI-DESIGN-SYSTEM.md): a pane returns CONTENT LINES
|
|
5
|
+
* ONLY and stays uncolored — the frame owner (run-dashboard.ts) prefixes the
|
|
6
|
+
* `┃ ` rail and colorizes embedded status glyphs through
|
|
7
|
+
* `colorizeStatusGlyphs`. The RAIL primitives in `src/ui/rail.ts`
|
|
8
|
+
* (`statusIcon`, `statusSlot`, `overflowHint`, `railLine`, `formatHint`, …)
|
|
9
|
+
* all take a `CrewTheme`, so panes hand them this identity theme: every glyph,
|
|
10
|
+
* overflow dialect and hint still comes from the one shared vocabulary, while
|
|
11
|
+
* no ANSI escapes leak into pane content.
|
|
12
|
+
*
|
|
13
|
+
* Not a fork of a rail primitive — a no-op `CrewTheme` adapter
|
|
14
|
+
* (`asCrewTheme(undefined)` → identity `fg`/`bold`) so uncolored producers can
|
|
15
|
+
* call the colored primitives.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { asCrewTheme } from "../theme-adapter.ts";
|
|
19
|
+
|
|
20
|
+
/** Identity theme: `fg(color, text) === text`, `bold(text) === text`. */
|
|
21
|
+
export const PANE_THEME = asCrewTheme(undefined);
|