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
|
@@ -4,30 +4,9 @@
|
|
|
4
4
|
* Extracted from crew-widget.ts for reuse and testability.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import { truncateToWidth, visibleWidth } from "../../utils/visual.ts";
|
|
11
|
-
import { computeLiveDurationMs } from "../live-duration.ts";
|
|
12
|
-
|
|
13
|
-
// ── No-color mode (UI-10) ─────────────────────────────────────────────
|
|
14
|
-
// Comprehensive color/emoji suppression. Color + emoji formatting is
|
|
15
|
-
// disabled when (a) NO_COLOR is set to a non-empty value (de-facto standard,
|
|
16
|
-
// https://bixense.com/clicolors/), OR (b) stdout is not a TTY (piped/
|
|
17
|
-
// redirected — escape codes & wide glyphs are noise there). Detected once at
|
|
18
|
-
// module init; `paint()` and the notification-badge emoji decision consult
|
|
19
|
-
// `colorEnabled`, so EVERY formatter in this file emits plain strings when the
|
|
20
|
-
// mode is active. Previously (line ~179) only the notification badge checked
|
|
21
|
-
// `NO_COLOR !== "1"` — a partial check that ignored the broader NO_COLOR-any-
|
|
22
|
-
// value standard and the non-TTY case, so color codes could still leak to
|
|
23
|
-
// pipes/logs. This makes the suppression uniform across the file.
|
|
24
|
-
|
|
25
|
-
const RESET = "\x1b[0m";
|
|
26
|
-
const ANSI_SGR_RE = /\u001b\[[0-9;]*m/g;
|
|
27
|
-
|
|
28
|
-
// Status foreground colors used by the activity formatters below.
|
|
29
|
-
const COLOR_RED = "\x1b[31m"; // failures
|
|
30
|
-
const COLOR_YELLOW = "\x1b[33m"; // needs-attention / warning
|
|
7
|
+
// module init; `notificationBadge`'s emoji decision consults
|
|
8
|
+
// `colorEnabled`, so `notificationBadge` emits a plain string when the
|
|
9
|
+
// mode is active.
|
|
31
10
|
|
|
32
11
|
function computeColorEnabled(): boolean {
|
|
33
12
|
if (process.env.NO_COLOR !== undefined && process.env.NO_COLOR !== "") return false;
|
|
@@ -39,21 +18,6 @@ function computeColorEnabled(): boolean {
|
|
|
39
18
|
|
|
40
19
|
let colorEnabled = computeColorEnabled();
|
|
41
20
|
|
|
42
|
-
/**
|
|
43
|
-
* Wrap `text` in an ANSI SGR sequence (e.g. `\x1b[31m`) when color is active.
|
|
44
|
-
* In no-color mode the text is returned verbatim — no escape sequences — so the
|
|
45
|
-
* formatters are safe to feed into logs, pipes, and other non-TTY sinks.
|
|
46
|
-
*/
|
|
47
|
-
function paint(text: string, sgr: string): string {
|
|
48
|
-
if (!colorEnabled || !text) return text;
|
|
49
|
-
return `${sgr}${text}${RESET}`;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** Strip ANSI SGR escape sequences from a string. */
|
|
53
|
-
export function stripAnsi(text: string): string {
|
|
54
|
-
return text.replace(ANSI_SGR_RE, "");
|
|
55
|
-
}
|
|
56
|
-
|
|
57
21
|
/** Re-evaluate the color mode from the current env/stdout (repeat of init). */
|
|
58
22
|
export function __resetColorMode(): void {
|
|
59
23
|
colorEnabled = computeColorEnabled();
|
|
@@ -66,22 +30,6 @@ export function __setColorModeForTest(enabled: boolean): void {
|
|
|
66
30
|
|
|
67
31
|
// ── Token formatting ──────────────────────────────────────────────────
|
|
68
32
|
|
|
69
|
-
// V-1: fixed visible widths for per-agent numeric metrics so columns don't
|
|
70
|
-
// jitter every tick as values change width (e.g. 9.9s→10.0s, 950→1.0k).
|
|
71
|
-
// alignMetric right-aligns to the width; values wider than the width overflow
|
|
72
|
-
// verbatim (no truncation/crash) — these are rare one-off states, not jitter.
|
|
73
|
-
const TOOLS_METRIC_WIDTH = 8; // "127 tools"
|
|
74
|
-
const TOKENS_METRIC_WIDTH = 10; // "1.2k tok", "12.3M tok"
|
|
75
|
-
const TPS_METRIC_WIDTH = 9; // "411 tok/s"
|
|
76
|
-
const CTX_METRIC_WIDTH = 7; // "100% ctx"
|
|
77
|
-
const DURATION_METRIC_WIDTH = 6; // "120.0s"
|
|
78
|
-
const COST_METRIC_WIDTH = 9; // "$0.001234"
|
|
79
|
-
|
|
80
|
-
function alignMetric(value: string, width: number): string {
|
|
81
|
-
const pad = Math.max(0, width - visibleWidth(value));
|
|
82
|
-
return " ".repeat(pad) + value;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
33
|
export function formatTokensCompact(count: number): string {
|
|
86
34
|
// Display-layer guard: state records have at least once carried the
|
|
87
35
|
// literal "***" in a numeric field (redaction false-positive, fixed at
|
|
@@ -105,353 +53,6 @@ export function elapsed(iso: string | undefined, now = Date.now()): string | und
|
|
|
105
53
|
return `${Math.floor(ms / 3_600_000)}h`;
|
|
106
54
|
}
|
|
107
55
|
|
|
108
|
-
/** pi-subtask's always-on elapsed tail: `0s` from the start (its fork rows
|
|
109
|
-
* flatten sub-second ages to 0 — not the legacy widget's "now"). */
|
|
110
|
-
export function dockElapsed(iso: string | undefined, now = Date.now()): string {
|
|
111
|
-
const value = elapsed(iso, now);
|
|
112
|
-
return value === undefined ? "" : value === "now" ? "0s" : value;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// ── Agent activity description ────────────────────────────────────────
|
|
116
|
-
|
|
117
|
-
const TOOL_LABELS: Record<string, string> = {
|
|
118
|
-
read: "reading",
|
|
119
|
-
bash: "running command",
|
|
120
|
-
edit: "editing",
|
|
121
|
-
write: "writing",
|
|
122
|
-
grep: "searching",
|
|
123
|
-
find: "finding files",
|
|
124
|
-
ls: "listing",
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
const TOOL_ICONS: Record<string, string> = {
|
|
128
|
-
read: "📖",
|
|
129
|
-
bash: ">",
|
|
130
|
-
edit: "✏",
|
|
131
|
-
write: "📝",
|
|
132
|
-
grep: "🔍",
|
|
133
|
-
find: "📁",
|
|
134
|
-
ls: "📋",
|
|
135
|
-
agent: "🤖",
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
export function describeLiveActivity(handle: LiveAgentHandle): string {
|
|
139
|
-
const act = handle.activity;
|
|
140
|
-
if (act.activeTools.size > 0) {
|
|
141
|
-
const groups = new Map<string, number>();
|
|
142
|
-
for (const toolName of act.activeTools.values()) {
|
|
143
|
-
groups.set(toolName, (groups.get(toolName) ?? 0) + 1);
|
|
144
|
-
}
|
|
145
|
-
const parts: string[] = [];
|
|
146
|
-
for (const [toolName, count] of groups) {
|
|
147
|
-
const icon = TOOL_ICONS[toolName] ?? "?";
|
|
148
|
-
const label = TOOL_LABELS[toolName] ?? toolName;
|
|
149
|
-
if (count > 1) {
|
|
150
|
-
parts.push(`${icon}${count} ${label}s`);
|
|
151
|
-
} else {
|
|
152
|
-
parts.push(`${icon} ${label}`);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
return parts.join(", ") + "…";
|
|
156
|
-
}
|
|
157
|
-
if (act.responseText?.trim()) {
|
|
158
|
-
const line =
|
|
159
|
-
act.responseText
|
|
160
|
-
.split("\n")
|
|
161
|
-
.find((l) => l.trim())
|
|
162
|
-
?.trim() ?? "";
|
|
163
|
-
return line.length > 60 ? line.slice(0, 60) + "…" : line;
|
|
164
|
-
}
|
|
165
|
-
return "thinking…";
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export function agentActivity(agent: CrewAgentRecord, liveHandle?: LiveAgentHandle): string {
|
|
169
|
-
if (liveHandle && liveHandle.status === "running") {
|
|
170
|
-
const live = describeLiveActivity(liveHandle);
|
|
171
|
-
if (live === "thinking…" && agent.progress?.currentTool)
|
|
172
|
-
return `${TOOL_LABELS[agent.progress.currentTool] ?? agent.progress.currentTool}…`;
|
|
173
|
-
return live;
|
|
174
|
-
}
|
|
175
|
-
if (agent.progress?.currentTool) return `${TOOL_LABELS[agent.progress.currentTool] ?? agent.progress.currentTool}…`;
|
|
176
|
-
const recent = agent.progress?.recentOutput?.at(-1);
|
|
177
|
-
if (recent) {
|
|
178
|
-
const cleaned = recent.replace(/\s+/g, " ").trim();
|
|
179
|
-
return cleaned.length > 60 ? cleaned.slice(0, 60) + "…" : cleaned;
|
|
180
|
-
}
|
|
181
|
-
if (agent.progress?.activityState === "needs_attention") return paint("needs attention", COLOR_YELLOW);
|
|
182
|
-
if (agent.status === "queued") return "queued";
|
|
183
|
-
if (agent.status === "running") {
|
|
184
|
-
const age = agent.startedAt ? Date.now() - new Date(agent.startedAt).getTime() : Infinity;
|
|
185
|
-
if (age < 5000 && !agent.progress?.currentTool) return "spawning…";
|
|
186
|
-
return "thinking…";
|
|
187
|
-
}
|
|
188
|
-
if (agent.status === "failed") return paint(agent.error ?? "failed", COLOR_RED);
|
|
189
|
-
return "done";
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// ── Per-agent cost ────────────────────────────────────────────────────
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Compact per-agent spend for widget rows, or "" when there is nothing to
|
|
196
|
-
* show. `formatCost`'s 6-decimal sub-cent output (`$0.001000`) wastes a third
|
|
197
|
-
* of a one-line row, so the widget uses a short form: cent precision in
|
|
198
|
-
* dollars, milli-precision below, and a `< $0.001` floor.
|
|
199
|
-
*/
|
|
200
|
-
export function formatCostCompact(cost: number): string {
|
|
201
|
-
if (cost >= 1) return `$${cost.toFixed(2)}`;
|
|
202
|
-
if (cost >= 0.01) return `$${cost.toFixed(3).replace(/0+$/, "").replace(/\.$/, "")}`;
|
|
203
|
-
if (cost >= 0.001) return `$${cost.toFixed(4).replace(/0+$/, "").replace(/\.$/, "")}`;
|
|
204
|
-
return "< $0.001";
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Formatted per-agent spend, or "" when there is nothing to show. The value
|
|
209
|
-
* already lives on the durable task record and the dashboard agents pane has
|
|
210
|
-
* shown it since Round 17; the widget omitted it only by oversight.
|
|
211
|
-
*/
|
|
212
|
-
export function agentCost(agent: CrewAgentRecord): string {
|
|
213
|
-
const cost = agent.usage?.cost;
|
|
214
|
-
if (typeof cost !== "number" || !Number.isFinite(cost) || cost <= 0) return "";
|
|
215
|
-
return formatCostCompact(cost);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// ── pi-subtask dock formatters ─────────────────────────────────────────
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* pi-subtask's FIXED per-status glyphs (`statusIcon` in source/pi-subtask:
|
|
222
|
-
* starting ○, running ✻, done ✓, failed ✗, stopped ■). The compact dock
|
|
223
|
-
* deliberately does NOT spin the running marker like the legacy widget —
|
|
224
|
-
* the row set is stable across ticks, which is what makes the keyboard
|
|
225
|
-
* cursor feel anchored.
|
|
226
|
-
*/
|
|
227
|
-
export function dockStatusIcon(status: string): string {
|
|
228
|
-
switch (status) {
|
|
229
|
-
case "running":
|
|
230
|
-
return "✻";
|
|
231
|
-
case "queued":
|
|
232
|
-
case "waiting":
|
|
233
|
-
return "○";
|
|
234
|
-
case "completed":
|
|
235
|
-
return "✓";
|
|
236
|
-
case "failed":
|
|
237
|
-
return "✗";
|
|
238
|
-
case "needs_attention":
|
|
239
|
-
return "⚠";
|
|
240
|
-
case "cancelled":
|
|
241
|
-
case "stopped":
|
|
242
|
-
return "■";
|
|
243
|
-
default:
|
|
244
|
-
return "?";
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/** pi-subtask uses the status word as a finished row's activity. */
|
|
249
|
-
export function dockStatusLabel(status: string): string {
|
|
250
|
-
switch (status) {
|
|
251
|
-
case "completed":
|
|
252
|
-
return "done";
|
|
253
|
-
case "failed":
|
|
254
|
-
return "failed";
|
|
255
|
-
case "cancelled":
|
|
256
|
-
case "stopped":
|
|
257
|
-
return "stopped";
|
|
258
|
-
case "needs_attention":
|
|
259
|
-
return "needs attention";
|
|
260
|
-
case "queued":
|
|
261
|
-
return "queued";
|
|
262
|
-
case "waiting":
|
|
263
|
-
return "waiting";
|
|
264
|
-
default:
|
|
265
|
-
return status;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/** pi-subtask's formatTokens: raw under 1k, `Nk` under 1M, `N.M` above. */
|
|
270
|
-
export function tokenCountShort(count: number): string {
|
|
271
|
-
if (count < 1_000) return `${count}`;
|
|
272
|
-
if (count < 1_000_000) return `${Math.round(count / 1_000)}k`;
|
|
273
|
-
return `${(count / 1_000_000).toFixed(1)}M`;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
export interface DockUsageOptions {
|
|
277
|
-
/** Pinned render clock (D6-T4): callers that already hold ONE Date per
|
|
278
|
-
* paint pass it so every duration/age in a frame agrees to the tick. */
|
|
279
|
-
nowMs?: number;
|
|
280
|
-
/** While the agent's pane is open: add tok/s + context % like pi-subtask. */
|
|
281
|
-
viewed?: boolean;
|
|
282
|
-
/** Model context window for the `P% / N` gauge; omitted when unknown. */
|
|
283
|
-
contextWindow?: number;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* pi-subtask's footer-style usage (formatUsage): `↑in ↓out Rcache CH% $cost`,
|
|
288
|
-
* plus `N tok/s` and `P% / window` while viewed. Live agents have split
|
|
289
|
-
* input/output/cacheWrite via the usage tracker; non-live ones show the
|
|
290
|
-
* durable token total + cost.
|
|
291
|
-
*/
|
|
292
|
-
export function dockUsageText(agent: CrewAgentRecord, liveHandle?: LiveAgentHandle, options: DockUsageOptions = {}): string {
|
|
293
|
-
const parts: string[] = [];
|
|
294
|
-
if (liveHandle) {
|
|
295
|
-
const usage = getTaskUsage(liveHandle.taskId);
|
|
296
|
-
const input = usage.input ?? 0;
|
|
297
|
-
const output = usage.output ?? 0;
|
|
298
|
-
const cacheWrite = usage.cacheWrite ?? 0;
|
|
299
|
-
if (input > 0) parts.push(`↑${tokenCountShort(input)}`);
|
|
300
|
-
if (output > 0) parts.push(`↓${tokenCountShort(output)}`);
|
|
301
|
-
if (cacheWrite > 0) parts.push(`R${tokenCountShort(cacheWrite)}`);
|
|
302
|
-
const promptTokens = input + cacheWrite;
|
|
303
|
-
if (cacheWrite > 0 && promptTokens > 0) {
|
|
304
|
-
parts.push(`CH${((cacheWrite / promptTokens) * 100).toFixed(1)}%`);
|
|
305
|
-
}
|
|
306
|
-
const cost = agent.usage?.cost;
|
|
307
|
-
if (typeof cost === "number" && Number.isFinite(cost) && cost > 0) parts.push(`$${cost.toFixed(4)}`);
|
|
308
|
-
if (options.viewed) {
|
|
309
|
-
const act = liveHandle.activity;
|
|
310
|
-
const ms = computeLiveDurationMs(act, options.nowMs);
|
|
311
|
-
const totalTokens = input + output + cacheWrite;
|
|
312
|
-
if (totalTokens > 0 && ms > 1000) {
|
|
313
|
-
const tps = Math.round(totalTokens / (ms / 1000));
|
|
314
|
-
if (tps > 0) parts.push(`${tps} tok/s`);
|
|
315
|
-
}
|
|
316
|
-
try {
|
|
317
|
-
const ctxPct = liveHandle.session.getSessionStats?.()?.contextUsage?.percent;
|
|
318
|
-
if (ctxPct != null) {
|
|
319
|
-
const window =
|
|
320
|
-
options.contextWindow && options.contextWindow >= 1_000_000
|
|
321
|
-
? `${(options.contextWindow / 1_000_000).toFixed(1)}M`
|
|
322
|
-
: options.contextWindow
|
|
323
|
-
? tokenCountShort(options.contextWindow)
|
|
324
|
-
: "";
|
|
325
|
-
parts.push(`${Math.round(ctxPct)}%${window ? ` / ${window}` : ""}`);
|
|
326
|
-
}
|
|
327
|
-
} catch {
|
|
328
|
-
/* ignore */
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
return parts.join(" ");
|
|
332
|
-
}
|
|
333
|
-
const tokens = agent.progress?.tokens;
|
|
334
|
-
const tokenCount = typeof tokens === "number" && Number.isFinite(tokens) && tokens > 0 ? tokens : 0;
|
|
335
|
-
if (tokenCount > 0) parts.push(`${tokenCountShort(tokenCount)} tok`);
|
|
336
|
-
const cost = agent.usage?.cost;
|
|
337
|
-
if (typeof cost === "number" && Number.isFinite(cost) && cost > 0) parts.push(`$${cost.toFixed(4)}`);
|
|
338
|
-
return parts.join(" ");
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// ── Adaptive single-line row ───────────────────────────────────────────
|
|
342
|
-
|
|
343
|
-
export interface BudgetedRowParts {
|
|
344
|
-
/** Marker + glyph prefix; never trimmed. */
|
|
345
|
-
lead: string;
|
|
346
|
-
/** Agent label; grows into whatever the activity leaves over. */
|
|
347
|
-
name: string;
|
|
348
|
-
/** Current activity; shrinks first but keeps a readable floor. */
|
|
349
|
-
activity: string;
|
|
350
|
-
/** Metrics tail; never trimmed, so numbers stay comparable across ticks. */
|
|
351
|
-
suffix: string;
|
|
352
|
-
separator?: string;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
/** Smallest activity/name slice still worth showing before we drop the field. */
|
|
356
|
-
const MIN_FIELD_WIDTH = 12;
|
|
357
|
-
|
|
358
|
-
/**
|
|
359
|
-
* Assemble one row that fills `width` without wrapping.
|
|
360
|
-
*
|
|
361
|
-
* `lead` and `suffix` are fixed costs; the remaining budget is split between
|
|
362
|
-
* `name` and `activity`. The activity absorbs the trimming first (it changes
|
|
363
|
-
* every tick anyway) but keeps a MIN_FIELD_WIDTH floor, and the name expands
|
|
364
|
-
* into the rest up to its natural length — so a 200-column terminal shows the
|
|
365
|
-
* full description instead of the same clip an 80-column one gets.
|
|
366
|
-
*
|
|
367
|
-
* The closing truncate is a hard guard, not an optimisation: pi's renderer
|
|
368
|
-
* throws on a line wider than the terminal.
|
|
369
|
-
*/
|
|
370
|
-
export function budgetedRow(parts: BudgetedRowParts, width: number): string {
|
|
371
|
-
const sep = parts.separator ?? " · ";
|
|
372
|
-
const { lead, suffix } = parts;
|
|
373
|
-
const name = parts.name.replace(/\s+/g, " ").trim();
|
|
374
|
-
const activity = parts.activity.replace(/\s+/g, " ").trim();
|
|
375
|
-
if (!name && !activity) return truncateToWidth(lead + suffix, width);
|
|
376
|
-
if (!activity) return fitNameOnly(lead, name, suffix, width);
|
|
377
|
-
if (!name) return fitNameOnly(lead, activity, suffix, width);
|
|
378
|
-
|
|
379
|
-
const budget = width - visibleWidth(lead) - visibleWidth(suffix) - visibleWidth(sep);
|
|
380
|
-
// Too narrow to hold name + activity above their floors: keep the name
|
|
381
|
-
// only, and never let the metrics tail eat the clipping.
|
|
382
|
-
if (budget < MIN_FIELD_WIDTH * 2) return fitNameOnly(lead, name, suffix, width);
|
|
383
|
-
|
|
384
|
-
const nameNatural = visibleWidth(name);
|
|
385
|
-
const activityNatural = visibleWidth(activity);
|
|
386
|
-
const activityRoom = Math.min(activityNatural, Math.max(MIN_FIELD_WIDTH, budget - nameNatural));
|
|
387
|
-
const nameRoom = Math.max(MIN_FIELD_WIDTH, budget - activityRoom);
|
|
388
|
-
const assembled = lead + truncateToWidth(name, nameRoom) + sep + truncateToWidth(activity, activityRoom) + suffix;
|
|
389
|
-
// The room arithmetic keeps this ≤ width by construction (nameRoom +
|
|
390
|
-
// activityRoom ≤ budget in every branch); this guard is the last line of
|
|
391
|
-
// defense because pi's renderer throws on over-width lines.
|
|
392
|
-
if (visibleWidth(assembled) <= width) return assembled;
|
|
393
|
-
return fitNameOnly(lead, name, suffix, width);
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
/**
|
|
397
|
-
* Narrow-terminal fallback: fixed lead + suffix, the name absorbs every
|
|
398
|
-
* remaining column. The metrics tail is sacred — it must stay comparable
|
|
399
|
-
* across ticks — so clipping always happens in the middle fields.
|
|
400
|
-
*/
|
|
401
|
-
function fitNameOnly(lead: string, name: string, suffix: string, width: number): string {
|
|
402
|
-
const fixed = visibleWidth(lead) + visibleWidth(suffix);
|
|
403
|
-
if (fixed >= width) return truncateToWidth(lead + suffix, width);
|
|
404
|
-
return lead + truncateToWidth(name, width - fixed) + suffix;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// ── Agent stats line ──────────────────────────────────────────────────
|
|
408
|
-
|
|
409
|
-
export function agentStats(agent: CrewAgentRecord, liveHandle?: LiveAgentHandle, nowMs?: number): string {
|
|
410
|
-
const parts: string[] = [];
|
|
411
|
-
if (liveHandle) {
|
|
412
|
-
const act = liveHandle.activity;
|
|
413
|
-
if (act.toolUses > 0) parts.push(alignMetric(`${act.toolUses} tools`, TOOLS_METRIC_WIDTH));
|
|
414
|
-
const usage = getTaskUsage(liveHandle.taskId);
|
|
415
|
-
const total = (usage.input ?? 0) + (usage.output ?? 0) + (usage.cacheWrite ?? 0);
|
|
416
|
-
if (total > 0) parts.push(alignMetric(formatTokensCompact(total), TOKENS_METRIC_WIDTH));
|
|
417
|
-
// The live usage tracker carries tokens only (LifetimeUsage has no cost
|
|
418
|
-
// field), so cost always comes off the durable task record.
|
|
419
|
-
const liveCost = agentCost(agent);
|
|
420
|
-
if (liveCost) parts.push(alignMetric(liveCost, COST_METRIC_WIDTH));
|
|
421
|
-
try {
|
|
422
|
-
const stats = liveHandle.session.getSessionStats?.();
|
|
423
|
-
const ctxPct = stats?.contextUsage?.percent;
|
|
424
|
-
if (ctxPct != null) parts.push(alignMetric(`${Math.round(ctxPct)}% ctx`, CTX_METRIC_WIDTH));
|
|
425
|
-
} catch {
|
|
426
|
-
/* ignore */
|
|
427
|
-
}
|
|
428
|
-
const ms = computeLiveDurationMs(act, nowMs);
|
|
429
|
-
if (total > 0 && ms > 1000) {
|
|
430
|
-
const tps = Math.round(total / (ms / 1000));
|
|
431
|
-
if (tps > 0) parts.push(alignMetric(`${formatTokensCompact(tps)}/s`, TPS_METRIC_WIDTH));
|
|
432
|
-
}
|
|
433
|
-
parts.push(alignMetric(`${(ms / 1000).toFixed(1)}s`, DURATION_METRIC_WIDTH));
|
|
434
|
-
} else {
|
|
435
|
-
// Type-narrowed: state has carried the literal "***" (redaction
|
|
436
|
-
// false-positive) in progress.tokens, which is truthy but not a count.
|
|
437
|
-
// Only real numbers produce metrics; formatTokensCompact guards too.
|
|
438
|
-
const tokens = agent.progress?.tokens;
|
|
439
|
-
const tokenCount = typeof tokens === "number" ? tokens : undefined;
|
|
440
|
-
if (agent.toolUses) parts.push(alignMetric(`${agent.toolUses} tools`, TOOLS_METRIC_WIDTH));
|
|
441
|
-
if (tokenCount && tokenCount > 0) parts.push(alignMetric(formatTokensCompact(tokenCount), TOKENS_METRIC_WIDTH));
|
|
442
|
-
const cost = agentCost(agent);
|
|
443
|
-
if (cost) parts.push(alignMetric(cost, COST_METRIC_WIDTH));
|
|
444
|
-
const ageMs = agent.startedAt ? Math.max(0, (nowMs ?? Date.now()) - new Date(agent.startedAt).getTime()) : 0;
|
|
445
|
-
if (tokenCount && tokenCount > 0 && ageMs > 1000) {
|
|
446
|
-
const tps = Math.round(tokenCount / (ageMs / 1000));
|
|
447
|
-
if (tps > 0) parts.push(alignMetric(`${formatTokensCompact(tps)}/s`, TPS_METRIC_WIDTH));
|
|
448
|
-
}
|
|
449
|
-
const age = elapsed(agent.completedAt ?? agent.startedAt);
|
|
450
|
-
if (age) parts.push(alignMetric(age, DURATION_METRIC_WIDTH));
|
|
451
|
-
}
|
|
452
|
-
return parts.join(" · ");
|
|
453
|
-
}
|
|
454
|
-
|
|
455
56
|
// ── Notification badge ────────────────────────────────────────────────
|
|
456
57
|
|
|
457
58
|
// Bug 021: the bell glyph 🔔 was misread as "queued messages" — users saw
|
|
@@ -12,12 +12,16 @@ import type { ManifestCache } from "../../runtime/manifest-cache.ts";
|
|
|
12
12
|
import { isDisplayActiveRun } from "../../runtime/process-status.ts";
|
|
13
13
|
import { reconcileAllStaleRuns } from "../../runtime/recovery/crash-recovery.ts";
|
|
14
14
|
import type { TeamRunManifest } from "../../state/types.ts";
|
|
15
|
+
import { ACTIVE, RAIL, shortId } from "../rail.ts";
|
|
15
16
|
import type { RunSnapshotCache } from "../snapshot-types.ts";
|
|
16
17
|
import type { WidgetRun } from "./widget-types.ts";
|
|
17
18
|
|
|
18
19
|
let lastStaleReconcileAt = 0;
|
|
19
20
|
const STALE_RECONCILE_INTERVAL_MS = 60_000;
|
|
20
21
|
|
|
22
|
+
/** The dock/status-bar identity word (kept next to its only two painters). */
|
|
23
|
+
const IDLE_WORD = "CREW";
|
|
24
|
+
|
|
21
25
|
function agentsFor(run: TeamRunManifest): CrewAgentRecord[] {
|
|
22
26
|
try {
|
|
23
27
|
return readCrewAgents(run);
|
|
@@ -81,7 +85,13 @@ export function activeWidgetRuns(
|
|
|
81
85
|
}
|
|
82
86
|
|
|
83
87
|
/**
|
|
84
|
-
* Build a status summary string for the status bar.
|
|
88
|
+
* Build a status summary string for the status bar (RAIL design system §2.B):
|
|
89
|
+
*
|
|
90
|
+
* `┃ CREW ▸ 2r · 2q · 3/5 done · <model>`
|
|
91
|
+
*
|
|
92
|
+
* The rail glyph + identity word are the same ones the dock paints, so the
|
|
93
|
+
* footer and the dock read as one surface. The multi-run case appends a
|
|
94
|
+
* `N runs` segment (the `r`/`q` counts are AGENT counts, never run counts).
|
|
85
95
|
*/
|
|
86
96
|
export function statusSummary(runs: WidgetRun[]): string {
|
|
87
97
|
const agents = runs.flatMap((item) => item.agents);
|
|
@@ -94,17 +104,28 @@ export function statusSummary(runs: WidgetRun[]): string {
|
|
|
94
104
|
.find((a) => a.model)
|
|
95
105
|
?.model?.split("/")
|
|
96
106
|
.at(-1);
|
|
97
|
-
|
|
107
|
+
// Zero counts are noise on a one-line status row (live 2026-09-16: after a run
|
|
108
|
+
// finished the bar read `┃ CREW ▸ 0r · 3/3 done · MiniMax-M3`). Same rule the
|
|
109
|
+
// dock header follows — a zero segment never prints.
|
|
110
|
+
const parts: string[] = [];
|
|
111
|
+
if (runningAgents > 0) parts.push(`${runningAgents}r`);
|
|
98
112
|
if (queuedAgents > 0) parts.push(`${queuedAgents}q`);
|
|
99
|
-
if (
|
|
100
|
-
if (totalRuns > 1) parts.push(`${totalRuns}runs`);
|
|
113
|
+
if (totalAgents > 0) parts.push(`${completedAgents}/${totalAgents} done`);
|
|
114
|
+
if (totalRuns > 1) parts.push(`${totalRuns} runs`);
|
|
101
115
|
if (model) parts.push(model);
|
|
102
|
-
|
|
116
|
+
if (parts.length === 0) parts.push("idle");
|
|
117
|
+
return `${RAIL.body} ${IDLE_WORD} ${ACTIVE} ${parts.join(" · ")}`;
|
|
103
118
|
}
|
|
104
119
|
|
|
105
120
|
/**
|
|
106
|
-
* Build the short run label (team/workflow)
|
|
121
|
+
* Build the short run label (team/workflow) — collapsed to ONE word when both
|
|
122
|
+
* halves agree (`fast-fix/fast-fix` is what every built-in team produced in
|
|
123
|
+
* live data), and guarded: a manifest read off disk may miss either half, and
|
|
124
|
+
* a literal `undefined` must never reach a rendered line.
|
|
107
125
|
*/
|
|
108
126
|
export function shortRunLabel(run: TeamRunManifest): string {
|
|
109
|
-
|
|
127
|
+
const team = (run.team ?? "").trim();
|
|
128
|
+
const workflow = (run.workflow ?? "").trim();
|
|
129
|
+
if (team && workflow && team !== workflow) return `${team}/${workflow}`;
|
|
130
|
+
return team || workflow || shortId(run.runId);
|
|
110
131
|
}
|