pi-crew 0.11.0 → 0.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +155 -9
- package/README.md +161 -1037
- package/agents/verifier.md +18 -7
- package/dist/index.mjs +744 -90644
- package/docs/README.md +57 -46
- package/docs/architecture.md +87 -33
- package/docs/commands-reference.md +9 -5
- package/docs/troubleshooting.md +3 -2
- package/package.json +1 -3
- package/schema.json +39 -0
- package/skills/real-test-pi-crew/REPORT-TEMPLATE.md +2 -0
- package/skills/real-test-pi-crew/SKILL.md +371 -34
- package/src/agents/agent-config.ts +1 -1
- package/src/agents/discover-agents.ts +1 -1
- package/src/config/config-validation.ts +15 -2
- package/src/config/config.ts +47 -13
- package/src/config/defaults.ts +0 -1
- package/src/config/env-vars.ts +35 -0
- package/src/config/types.ts +19 -5
- package/src/errors.ts +2 -2
- package/src/extension/async-notifier.ts +23 -0
- package/src/extension/crew-vibes/config.ts +0 -21
- package/src/extension/crew-vibes/index.ts +0 -2
- package/src/extension/crew-vibes/render.ts +1 -50
- package/src/extension/help.ts +21 -12
- package/src/extension/knowledge-injection.ts +2 -1
- package/src/extension/management.ts +8 -3
- package/src/extension/notification-sink.ts +17 -0
- package/src/extension/register.ts +7 -2
- package/src/extension/registration/command-utils.ts +28 -2
- package/src/extension/registration/commands/dashboard.ts +11 -1
- package/src/extension/registration/commands/manage.ts +36 -19
- package/src/extension/registration/commands/run.ts +24 -2
- package/src/extension/registration/commands/shared.ts +23 -1
- package/src/extension/registration/commands/status.ts +25 -2
- package/src/extension/registration/context-builder.ts +8 -2
- package/src/extension/registration/health-notify-policy.ts +100 -0
- package/src/extension/registration/lazy-configurers.ts +35 -0
- package/src/extension/registration/lifecycle-handlers.ts +91 -30
- package/src/extension/registration/lifecycle.ts +75 -10
- package/src/extension/registration/observability.ts +98 -35
- package/src/extension/registration/registration-types.ts +7 -5
- package/src/extension/registration/runtime-cleanup.ts +9 -3
- package/src/extension/registration/subagent-helpers.ts +38 -0
- package/src/extension/registration/subagent-tools.ts +16 -6
- package/src/extension/registration/team-tool.ts +10 -3
- package/src/extension/registration/terminal-status-wiring.ts +172 -0
- package/src/extension/registration/viewers.ts +6 -0
- package/src/extension/registration/wire-cross-extension.ts +28 -0
- package/src/extension/run-compare.ts +220 -0
- package/src/extension/run-export.ts +37 -5
- package/src/extension/run-maintenance.ts +155 -5
- package/src/extension/team-tool/dispatch/index.ts +3 -2
- package/src/extension/team-tool/dispatch/manage.ts +5 -2
- package/src/extension/team-tool/goal.ts +4 -1
- package/src/extension/team-tool/handle-settings.ts +33 -4
- package/src/extension/team-tool/health-monitor.ts +21 -7
- package/src/extension/team-tool/lifecycle-actions.ts +49 -1
- package/src/extension/team-tool/plan.ts +10 -0
- package/src/extension/team-tool/routing-hint.ts +63 -0
- package/src/extension/team-tool/status.ts +4 -0
- package/src/extension/team-tool.ts +52 -6
- package/src/extension/webhook-notify.ts +382 -0
- package/src/observability/metric-sink.ts +12 -2
- package/src/prompt/prompt-runtime.ts +82 -31
- package/src/prompt/worker-events-channel.ts +12 -0
- package/src/runtime/README.md +1 -1
- package/src/runtime/async-runner.ts +87 -1
- package/src/runtime/background-runner.ts +313 -234
- package/src/runtime/broker/crew-broker.ts +17 -11
- package/src/runtime/broker/delegate/shadow-lifecycle.ts +92 -0
- package/src/runtime/broker/wait-status-cache.ts +1 -1
- package/src/runtime/child-pi/child-pi-timers.ts +1 -1
- package/src/runtime/child-pi/mock-fixtures.ts +48 -0
- package/src/runtime/crew-agent-records.ts +337 -45
- package/src/runtime/deadletter.ts +43 -1
- package/src/runtime/delegate-spawn.ts +5 -1
- package/src/runtime/dispatch-batch.ts +72 -5
- package/src/runtime/goal-workflow/goal-loop-runner.ts +73 -4
- package/src/runtime/heartbeat/heartbeat-watcher.ts +7 -0
- package/src/runtime/model/model-fallback.ts +21 -1
- package/src/runtime/model/pi-args.ts +8 -10
- package/src/runtime/recovery/crash-recovery.ts +25 -1
- package/src/runtime/run-worker.ts +12 -1
- package/src/runtime/scheduling/global-worker-cap.ts +13 -6
- package/src/runtime/scheduling/run-coalesced-task-group.ts +27 -1
- package/src/runtime/scheduling/scheduler.ts +49 -13
- package/src/runtime/scheduling/semaphore.ts +148 -20
- package/src/runtime/scratchpad/README.md +1 -1
- package/src/runtime/scratchpad/protocol.ts +1 -1
- package/src/runtime/settings-store.ts +1 -1
- package/src/runtime/skill-instructions.ts +22 -0
- package/src/runtime/stale-reconciler.ts +85 -13
- package/src/runtime/task-display.ts +1 -1
- package/src/runtime/task-runner/pre-execution.ts +26 -2
- package/src/runtime/task-runner/prompt-builder.ts +142 -45
- package/src/runtime/task-runner.ts +21 -1
- package/src/runtime/team-runner.ts +38 -1
- package/src/runtime/workspace-lock.ts +4 -1
- package/src/schema/config-schema.ts +18 -0
- package/src/schema/team-tool-schema.ts +17 -0
- package/src/state/atomic-write.ts +53 -0
- package/src/state/contracts.ts +109 -0
- package/src/state/coordination/locks.ts +191 -33
- package/src/state/coordination/mailbox.ts +140 -15
- package/src/state/crew-init.ts +87 -12
- package/src/state/event-log/cursor.ts +37 -1
- package/src/state/event-log/event-log-rotation.ts +72 -7
- package/src/state/stores/active-run-registry.ts +13 -1
- package/src/state/stores/state-store.ts +112 -22
- package/src/state/types.ts +4 -0
- package/src/ui/adaptive-card.ts +65 -0
- package/src/ui/agents-jobs-browser.ts +70 -64
- package/src/ui/card-colors.ts +36 -7
- package/src/ui/dashboard-panes/agents-pane.ts +55 -14
- package/src/ui/dashboard-panes/cancellation-pane.ts +0 -42
- package/src/ui/dashboard-panes/health-pane.ts +7 -5
- package/src/ui/dashboard-panes/mailbox-pane.ts +22 -6
- package/src/ui/dashboard-panes/metrics-pane.ts +15 -7
- package/src/ui/dashboard-panes/pane-theme.ts +21 -0
- package/src/ui/dashboard-panes/plan-pane.ts +63 -30
- package/src/ui/dashboard-panes/progress-pane.ts +3 -2
- package/src/ui/dashboard-panes/schedules-pane.ts +44 -21
- package/src/ui/dashboard-panes/transcript-pane.ts +11 -5
- package/src/ui/dwf-phase-display.ts +3 -20
- package/src/ui/format-helpers.ts +22 -0
- package/src/ui/heartbeat-aggregator.ts +34 -0
- package/src/ui/inline-panel/crew-editor.ts +13 -3
- package/src/ui/inline-panel/index.ts +60 -4
- package/src/ui/keybinding-map.ts +251 -35
- package/src/ui/live-conversation-overlay.ts +180 -47
- package/src/ui/live-run-sidebar.ts +134 -55
- package/src/ui/mascot.ts +32 -16
- package/src/ui/overlays/agent-picker-overlay.ts +81 -26
- package/src/ui/overlays/confirm-overlay.ts +55 -29
- package/src/ui/overlays/help-overlay.ts +108 -53
- package/src/ui/overlays/mailbox-compose-overlay.ts +89 -50
- package/src/ui/overlays/mailbox-detail-overlay.ts +137 -57
- package/src/ui/powerbar-publisher.ts +0 -1
- package/src/ui/rail.ts +333 -0
- package/src/ui/run-dashboard.ts +193 -79
- package/src/ui/run-snapshot-cache.ts +18 -1
- package/src/ui/settings-overlay.ts +81 -39
- package/src/ui/spinner.ts +26 -2
- package/src/ui/terminal-status.ts +7 -1
- package/src/ui/theme-adapter.ts +0 -45
- package/src/ui/theme-discovery.ts +12 -6
- package/src/ui/tool-progress-formatter.ts +128 -9
- package/src/ui/tool-renderers/brief-mode.ts +10 -67
- package/src/ui/tool-renderers/index.ts +374 -523
- package/src/ui/transcript-viewer.ts +30 -12
- package/src/ui/widget/index.ts +32 -52
- package/src/ui/widget/task-list.ts +64 -32
- package/src/ui/widget/widget-formatters.ts +3 -402
- package/src/ui/widget/widget-model.ts +28 -7
- package/src/ui/widget/widget-renderer.ts +201 -128
- package/src/ui/widget/widget-types.ts +0 -2
- package/src/utils/incremental-reader.ts +11 -3
- package/src/utils/paths.ts +94 -12
- package/src/utils/project-markers.ts +40 -0
- package/src/utils/visual.ts +0 -4
- package/src/worktree/worktree-manager.ts +206 -26
- package/workflows/distill.workflow.md +3 -3
- package/workflows/fast-fix.workflow.md +1 -1
- package/workflows/plan-execute.workflow.md +1 -1
- package/workflows/review.workflow.md +1 -1
- package/workflows/strict-fast-fix.workflow.md +1 -1
- package/docs/migration-v0.4-v0.5.md +0 -208
- package/docs/runtime-flow.md +0 -148
- package/src/extension/crew-vibes/figures.ts +0 -22
- package/src/extension/crew-vibes/font-detect.ts +0 -71
- package/src/ui/dynamic-border.ts +0 -35
- package/src/ui/loaders.ts +0 -6
- package/src/ui/overlay-stack.ts +0 -148
package/src/ui/rail.ts
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RAIL design system — the shared visual language for EVERY pi-crew surface.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from the tool-card renderer (R3, 2026-09-16) so the widget, the
|
|
5
|
+
* dashboard, the agents & jobs browser, the settings overlay, the mailbox /
|
|
6
|
+
* help / confirm / live-conversation overlays and the dashboard panes all
|
|
7
|
+
* speak one grammar instead of each inventing its own frame.
|
|
8
|
+
*
|
|
9
|
+
* ── Grammar ───────────────────────────────────────────────────────────
|
|
10
|
+
*
|
|
11
|
+
* ┏ ┣ ┃ ┗ one vertical rail down the LEFT edge marks a surface.
|
|
12
|
+
* `┏` opens it (identity), `┣` starts a section, `┃` continues
|
|
13
|
+
* it, `┗` closes it (outcome + end cap). The rail colour
|
|
14
|
+
* carries state: neutral idle, accent active, green success,
|
|
15
|
+
* red failure, warning attention.
|
|
16
|
+
*
|
|
17
|
+
* `NAME ▸ SUBJECT` the identity canopy: `┏ CREW ▸ implementation`,
|
|
18
|
+
* `┏ AGENTS ▸ a3f9c1d2`, `┏ HELP ▸ dashboard`.
|
|
19
|
+
*
|
|
20
|
+
* `······` dot leaders join a left and a right segment
|
|
21
|
+
* (`left ······ right`). They absorb all slack and
|
|
22
|
+
* collapse to a two-space gap on a narrow column, so
|
|
23
|
+
* the right segment (elapsed / key hint) never moves
|
|
24
|
+
* and never wraps.
|
|
25
|
+
*
|
|
26
|
+
* `▕████▎░░▏` gauge with EIGHTH-BLOCK sub-cell precision.
|
|
27
|
+
*
|
|
28
|
+
* `›` cursor · `▸` active/section marker · `▲ n above` / `▼ m below`
|
|
29
|
+
* the single selection, active and overflow dialects.
|
|
30
|
+
*
|
|
31
|
+
* ── Width contract ────────────────────────────────────────────────────
|
|
32
|
+
*
|
|
33
|
+
* Every helper takes an explicit `budget` (content width = render width
|
|
34
|
+
* minus the 2 columns of `glyph + space`) and NEVER emits a line wider than
|
|
35
|
+
* it. Callers that render into a live TUI must defer width through
|
|
36
|
+
* `AdaptiveCard` (see src/ui/adaptive-card.ts) rather than baking a width at
|
|
37
|
+
* build time — that is the R2 lesson: a frame built for 116 columns tears
|
|
38
|
+
* apart at 100.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
42
|
+
import { truncateToWidth } from "../utils/visual.ts";
|
|
43
|
+
import type { CrewTheme, CrewThemeColor } from "./theme-adapter.ts";
|
|
44
|
+
|
|
45
|
+
// ── Glyph vocabulary ───────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
/** Rail glyphs: open, section, continue, close. */
|
|
48
|
+
export const RAIL = { open: "┏", section: "┣", body: "┃", close: "┗" } as const;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Pi's own chord for collapsing/expanding a tool output block — the only key
|
|
52
|
+
* that ACTUALLY expands a pi-crew card, so it is what the card caps advertise.
|
|
53
|
+
*
|
|
54
|
+
* Verified against pi 0.85.1 `docs/keybindings.md`: `app.tools.expand` = `ctrl+o`.
|
|
55
|
+
* The cards used to print `⌘E`, which pi binds to `tui.editor.cursorLineEnd`
|
|
56
|
+
* (move the EDITOR cursor to end of line) — a phantom affordance: pressing it
|
|
57
|
+
* moved the cursor and left the card collapsed (live 2026-09-16).
|
|
58
|
+
*/
|
|
59
|
+
export const PI_EXPAND_CHORD = "ctrl+o";
|
|
60
|
+
|
|
61
|
+
/** Selection cursor (lists) and active/step marker (sections, goals). */
|
|
62
|
+
export const CURSOR = "›";
|
|
63
|
+
export const ACTIVE = "▸";
|
|
64
|
+
|
|
65
|
+
/** Canonical overflow dialect — the ONLY one; the old `↑ 3 more above`
|
|
66
|
+
* / `… 3 above` / `↓2` variants are retired. */
|
|
67
|
+
export const OVERFLOW = { above: "▲", below: "▼" } as const;
|
|
68
|
+
|
|
69
|
+
// ── ANSI-aware width helpers ───────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
/** Pad a string (which may contain ANSI codes) to a target VISUAL width. */
|
|
72
|
+
export function padVisual(str: string, targetWidth: number): string {
|
|
73
|
+
const vw = visibleWidth(str);
|
|
74
|
+
if (vw >= targetWidth) return str;
|
|
75
|
+
return str + " ".repeat(targetWidth - vw);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Truncate a string (which may contain ANSI codes) to a target VISUAL width. */
|
|
79
|
+
export function truncVisual(str: string, maxWidth: number): string {
|
|
80
|
+
if (visibleWidth(str) <= maxWidth) return str;
|
|
81
|
+
// Round 23 (BUG 3): String.slice counts UTF-16 code units — for CJK that
|
|
82
|
+
// overflows by up to 2x and for emoji it splits a surrogate pair. Use the
|
|
83
|
+
// grapheme/ANSI-aware truncateToWidth with empty ellipsis (the caller
|
|
84
|
+
// appends its own '…').
|
|
85
|
+
return truncateToWidth(str, maxWidth, "");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Short id (last 8 chars — the disambiguating suffix). */
|
|
89
|
+
export function shortId(id: string | undefined): string {
|
|
90
|
+
return id ? id.slice(-8) : "????????";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── Rail slots ─────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
export type RailSlot = "border" | "borderAccent" | "success" | "error" | "warning";
|
|
96
|
+
|
|
97
|
+
/** Status → rail colour. One mapping for every surface (was: per-surface maps). */
|
|
98
|
+
export function statusSlot(status: string | undefined): RailSlot {
|
|
99
|
+
switch (status) {
|
|
100
|
+
case "completed":
|
|
101
|
+
case "done":
|
|
102
|
+
case "succeeded":
|
|
103
|
+
case "passed":
|
|
104
|
+
case "healthy":
|
|
105
|
+
return "success";
|
|
106
|
+
case "failed":
|
|
107
|
+
case "cancelled":
|
|
108
|
+
case "error":
|
|
109
|
+
case "stopped":
|
|
110
|
+
case "dead":
|
|
111
|
+
return "error";
|
|
112
|
+
case "running":
|
|
113
|
+
case "in_progress":
|
|
114
|
+
case "streaming":
|
|
115
|
+
return "borderAccent";
|
|
116
|
+
case "waiting":
|
|
117
|
+
case "needs_attention":
|
|
118
|
+
case "stale":
|
|
119
|
+
case "pending":
|
|
120
|
+
return "warning";
|
|
121
|
+
default:
|
|
122
|
+
return "border";
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** One rail line: `┃ <content padded to budget>`. The glyph+space separator is
|
|
127
|
+
* owned HERE so callers pass bare content and no line can spill past the rail. */
|
|
128
|
+
export function railLine(glyph: string, slot: RailSlot, content: string, theme: CrewTheme, budget: number): string {
|
|
129
|
+
return theme.fg(slot, glyph) + " " + padVisual(truncVisual(content, budget), budget);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Content-only rail line (no padding) — for callers that pad themselves. */
|
|
133
|
+
export function railRaw(glyph: string, slot: RailSlot, content: string, theme: CrewTheme): string {
|
|
134
|
+
return `${theme.fg(slot, glyph)} ${content}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Left/right segments joined by dot leaders: `left ······· right`.
|
|
139
|
+
* Leaders absorb all slack; on a tight column the left side is truncated so
|
|
140
|
+
* the right segment (elapsed / key hint) always stays visible.
|
|
141
|
+
*/
|
|
142
|
+
export function railLeaders(left: string, right: string, budget: number, theme: CrewTheme): string {
|
|
143
|
+
const slack = budget - visibleWidth(left) - visibleWidth(right) - 2;
|
|
144
|
+
if (slack >= 3) {
|
|
145
|
+
const dots = theme.fg("dim", "·".repeat(Math.min(slack, 120)));
|
|
146
|
+
return `${left} ${dots} ${right}`;
|
|
147
|
+
}
|
|
148
|
+
if (slack > 0) return `${left} ${right}`;
|
|
149
|
+
const rightW = visibleWidth(right);
|
|
150
|
+
const leftBudget = budget - rightW - 2;
|
|
151
|
+
// When the left segment has to be cut, mark the elision with `…` — a hard
|
|
152
|
+
// cut mid-word reads as a rendering bug (`Esc/Q clos auto-scroll`).
|
|
153
|
+
if (leftBudget > 4) return `${truncVisual(left, leftBudget - 1)}… ${right}`;
|
|
154
|
+
return truncVisual(`${left} ${right}`, budget);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** `┏ NAME ▸ SUBJECT` — the identity canopy, shared by every surface. */
|
|
158
|
+
export function canopyLine(args: {
|
|
159
|
+
word: string;
|
|
160
|
+
subject?: string;
|
|
161
|
+
theme: CrewTheme;
|
|
162
|
+
budget: number;
|
|
163
|
+
slot?: RailSlot;
|
|
164
|
+
glyph?: string;
|
|
165
|
+
/** Trailing right-aligned segment (e.g. `1-8 pane · ? help`), dot-led. */
|
|
166
|
+
right?: string;
|
|
167
|
+
}): string {
|
|
168
|
+
const { word, subject, theme, budget, slot = "border", glyph = RAIL.open, right } = args;
|
|
169
|
+
const tail = subject ? ` ${theme.fg("dim", ACTIVE)} ${theme.fg("toolTitle", theme.bold(subject))}` : "";
|
|
170
|
+
const label = `${theme.fg("accent", theme.bold(word))}${tail}`;
|
|
171
|
+
if (right) return railLine(glyph, slot, railLeaders(label, right, budget, theme), theme, budget);
|
|
172
|
+
return railLine(glyph, slot, label, theme, budget);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** `┣ SECTION ▸ subject` — replaces the legacy `── label ──` inline rule. */
|
|
176
|
+
export function sectionLine(args: {
|
|
177
|
+
name: string;
|
|
178
|
+
subject?: string;
|
|
179
|
+
theme: CrewTheme;
|
|
180
|
+
budget: number;
|
|
181
|
+
slot?: RailSlot;
|
|
182
|
+
right?: string;
|
|
183
|
+
}): string {
|
|
184
|
+
const { name, subject, theme, budget, slot = "border", right } = args;
|
|
185
|
+
const tail = subject ? ` ${theme.fg("dim", ACTIVE)} ${theme.fg("muted", subject)}` : "";
|
|
186
|
+
const label = `${theme.fg("border", theme.bold(name.toUpperCase()))}${tail}`;
|
|
187
|
+
if (right) return railLine(RAIL.section, slot, railLeaders(label, right, budget, theme), theme, budget);
|
|
188
|
+
return railLine(RAIL.section, slot, label, theme, budget);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** `▕████▎░░▏` — eighth-block sub-cell precision, truthful on narrow columns. */
|
|
192
|
+
const EIGHTHS = ["", "▏", "▎", "▍", "▌", "▋", "▊", "▉"] as const;
|
|
193
|
+
|
|
194
|
+
export function gaugeBar(ratio: number, barWidth: number, theme: CrewTheme, fill: CrewThemeColor = "success"): string {
|
|
195
|
+
const clamped = Math.max(0, Math.min(1, ratio));
|
|
196
|
+
const cells = clamped * barWidth;
|
|
197
|
+
const full = Math.floor(cells);
|
|
198
|
+
const partial = EIGHTHS[Math.round((cells - full) * 8)] ?? "";
|
|
199
|
+
const filledCells = full + (partial ? 1 : 0);
|
|
200
|
+
const empty = Math.max(0, barWidth - filledCells);
|
|
201
|
+
const body = theme.fg(fill, "█".repeat(full) + partial);
|
|
202
|
+
const rest = theme.fg("dim", "░".repeat(empty));
|
|
203
|
+
return `${theme.fg("dim", "▕")}${body}${rest}${theme.fg("dim", "▏")}`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Animated scanning gauge for indeterminate progress. */
|
|
207
|
+
export function scanGauge(barWidth: number, elapsedMs: number, theme: CrewTheme): string {
|
|
208
|
+
const pos = Math.floor((elapsedMs / 400) % (barWidth + 6)) - 3;
|
|
209
|
+
const segW = Math.max(3, Math.floor(barWidth * 0.3));
|
|
210
|
+
let bar = "";
|
|
211
|
+
for (let i = 0; i < barWidth; i++) {
|
|
212
|
+
bar += i >= pos && i < pos + segW ? theme.fg("accent", "█") : theme.fg("dim", "░");
|
|
213
|
+
}
|
|
214
|
+
return `${theme.fg("dim", "▕")}${bar}${theme.fg("dim", "▏")}`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Producer emits `role/agent` (e.g. `verifier/verifier`). Collapse the
|
|
219
|
+
* redundant half so a live row reads `verifier` — real run data showed the
|
|
220
|
+
* duplication on every single-agent run.
|
|
221
|
+
*/
|
|
222
|
+
export function dedupeAgentLabel(label: string): string {
|
|
223
|
+
const slash = label.indexOf("/");
|
|
224
|
+
if (slash <= 0) return label;
|
|
225
|
+
const role = label.slice(0, slash);
|
|
226
|
+
const rest = label.slice(slash + 1); // "agent · tool" hoặc "agent"
|
|
227
|
+
const agent = rest.split(" ")[0] ?? "";
|
|
228
|
+
if (agent && role === agent) return `${role}${rest.slice(agent.length)}`;
|
|
229
|
+
return label;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ── Status glyphs ──────────────────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
/** Badge glyph, colour-coded (`●` done, `✖` failed, `◉` running, `○` other). */
|
|
235
|
+
export function statusBadge(status: string, theme: CrewTheme): string {
|
|
236
|
+
switch (statusSlot(status)) {
|
|
237
|
+
case "success":
|
|
238
|
+
return theme.fg("success", "●");
|
|
239
|
+
case "error":
|
|
240
|
+
return theme.fg("error", "✖");
|
|
241
|
+
case "borderAccent":
|
|
242
|
+
return theme.fg("warning", "◉");
|
|
243
|
+
default:
|
|
244
|
+
return theme.fg("dim", "○");
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Compact status icon (`✓` done, `✗` failed, `⟳` running, `○` other). */
|
|
249
|
+
export function statusIcon(status: string, theme: CrewTheme): string {
|
|
250
|
+
switch (statusSlot(status)) {
|
|
251
|
+
case "success":
|
|
252
|
+
return theme.fg("success", "✓");
|
|
253
|
+
case "error":
|
|
254
|
+
return theme.fg("error", "✗");
|
|
255
|
+
case "borderAccent":
|
|
256
|
+
return theme.fg("warning", "⟳");
|
|
257
|
+
default:
|
|
258
|
+
return theme.fg("dim", "○");
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Canonical overflow hint. Replaces four dialects (`↑ N more above`,
|
|
264
|
+
* `▲ N more above`, `… N above · M below`, `↓N`).
|
|
265
|
+
*/
|
|
266
|
+
export function overflowHint(above: number, below: number, theme: CrewTheme): string {
|
|
267
|
+
const parts: string[] = [];
|
|
268
|
+
if (above > 0) parts.push(`${OVERFLOW.above} ${above} above`);
|
|
269
|
+
if (below > 0) parts.push(`${OVERFLOW.below} ${below} below`);
|
|
270
|
+
return parts.length ? theme.fg("dim", parts.join(" · ")) : "";
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ── Hint text (one format for every surface) ───────────────────────────
|
|
274
|
+
|
|
275
|
+
/** `Esc` not `ESC`/`esc`; `Enter` not `⏎`; letter keys bare uppercase. */
|
|
276
|
+
export function keyToken(key: string): string {
|
|
277
|
+
switch (key) {
|
|
278
|
+
case "escape":
|
|
279
|
+
case "esc":
|
|
280
|
+
case "\u001b":
|
|
281
|
+
return "Esc";
|
|
282
|
+
case "return":
|
|
283
|
+
case "enter":
|
|
284
|
+
case "\r":
|
|
285
|
+
case "\n":
|
|
286
|
+
return "Enter";
|
|
287
|
+
case "\t":
|
|
288
|
+
case "tab":
|
|
289
|
+
return "Tab";
|
|
290
|
+
case "up":
|
|
291
|
+
case "down":
|
|
292
|
+
return "↑/↓";
|
|
293
|
+
case "pageup":
|
|
294
|
+
return "PgUp";
|
|
295
|
+
case "pagedown":
|
|
296
|
+
return "PgDn";
|
|
297
|
+
case " ":
|
|
298
|
+
case "space":
|
|
299
|
+
return "Space";
|
|
300
|
+
default:
|
|
301
|
+
return key.length === 1 ? key.toUpperCase() : key.charAt(0).toUpperCase() + key.slice(1);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* `↑/↓ move · Enter select · Esc cancel` — the single hint format.
|
|
307
|
+
*
|
|
308
|
+
* Contract: `keys label` pairs joined by ` · `, the close/cancel action LAST,
|
|
309
|
+
* keys rendered through `keyToken` (so `esc`/`ESC`/`q`-as-close collapse to one
|
|
310
|
+
* spelling). Callers should feed keys from `keybinding-map.ts` so a remap
|
|
311
|
+
* cannot silently desync the footer. Pass `{ exactKeys: true }` for a
|
|
312
|
+
* case-sensitive keyspace (plan approval: `A` vs `n`).
|
|
313
|
+
*/
|
|
314
|
+
export function formatHint(
|
|
315
|
+
pairs: ReadonlyArray<readonly [string | readonly string[], string]>,
|
|
316
|
+
options: { exactKeys?: boolean } = {},
|
|
317
|
+
): string {
|
|
318
|
+
return pairs
|
|
319
|
+
.map(([keys, label]) => {
|
|
320
|
+
const list = Array.isArray(keys) ? keys : [keys as string];
|
|
321
|
+
// Dedupe AFTER mapping: `["up","down"]` both map to `↑/↓` and
|
|
322
|
+
// `["enter","\r"]` both map to `Enter`, so the joined token must
|
|
323
|
+
// collapse instead of printing `↑/↓/↑/↓`.
|
|
324
|
+
//
|
|
325
|
+
// `exactKeys` is for the case-SENSITIVE keyspaces (plan approval
|
|
326
|
+
// binds `A` = approve vs `n` = deny — see keybinding-map.ts:75, and
|
|
327
|
+
// the keyspace deliberately relies on that case distinction).
|
|
328
|
+
// Uppercasing there would advertise a key that does not work.
|
|
329
|
+
const tokens = [...new Set(options.exactKeys ? list : list.map(keyToken))];
|
|
330
|
+
return `${tokens.join("/")} ${label}`;
|
|
331
|
+
})
|
|
332
|
+
.join(" · ");
|
|
333
|
+
}
|