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
|
@@ -3,16 +3,47 @@
|
|
|
3
3
|
*
|
|
4
4
|
* R8: Subscribes to session events for real-time streaming updates.
|
|
5
5
|
* Falls back to polling LiveAgentHandle.activity when subscribe is unavailable.
|
|
6
|
+
*
|
|
7
|
+
* Frame (RAIL design language, M4/E1 2026-09-16):
|
|
8
|
+
*
|
|
9
|
+
* ┏ LIVE ▸ explorer
|
|
10
|
+
* ┃ ◉ building feature · turn 2/10 · 3 tools · 5.1s · ▕██░░░░░░▏ 24% ctx · sonnet
|
|
11
|
+
* ┃ <streamed transcript rows…>
|
|
12
|
+
* ┗ ↑/↓/PgUp/PgDn/G scroll · A pause · Esc/Q close ···· auto-scroll
|
|
13
|
+
*
|
|
14
|
+
* The rounded box (`╭─╮│╰─╯` + its two `│ ──── │` rules) is retired: the canopy
|
|
15
|
+
* opens the surface, every body row is a `┃` rail line, the `┗` close cap
|
|
16
|
+
* carries the hint (`formatHint`, close LAST) on the left and the scroll STATE
|
|
17
|
+
* (`auto-scroll` / `manual 12-30/31`) on the dot-led right segment, so the state
|
|
18
|
+
* is never the part that gets truncated. Chrome therefore drops from 6 rows to 3
|
|
19
|
+
* (canopy + meta + cap) and the viewport gains the three freed rows — keys,
|
|
20
|
+
* scroll rules and clamping are unchanged.
|
|
21
|
+
*
|
|
22
|
+
* Keyboard (P1-3: the overlay captures focus while open — see
|
|
23
|
+
* `handleInput` below for the full key set):
|
|
24
|
+
* esc / q close, back to the main conversation
|
|
25
|
+
* pgup / pgdn / ↑ ↓ / j k / g G scroll the transcript
|
|
26
|
+
* a toggle auto-scroll
|
|
6
27
|
*/
|
|
28
|
+
import { matchesKey } from "@earendil-works/pi-tui";
|
|
7
29
|
import type { LiveAgentHandle } from "../runtime/live-session/live-agent-manager.ts";
|
|
8
|
-
import {
|
|
30
|
+
import { truncate } from "../utils/visual.ts";
|
|
31
|
+
import { formatCount } from "./format-helpers.ts";
|
|
9
32
|
import { computeLiveDurationMs } from "./live-duration.ts";
|
|
33
|
+
import { canopyLine, formatHint, gaugeBar, RAIL, railLeaders, railLine, statusSlot } from "./rail.ts";
|
|
10
34
|
import { spinnerFrame } from "./spinner.ts";
|
|
11
35
|
import { iconForStatus } from "./status-colors.ts";
|
|
12
|
-
import type { CrewTheme } from "./theme-adapter.ts";
|
|
36
|
+
import type { CrewTheme, CrewThemeColor } from "./theme-adapter.ts";
|
|
13
37
|
|
|
14
|
-
|
|
38
|
+
/** Chrome rows: canopy + meta row + close cap. Every other row is transcript. */
|
|
39
|
+
const CHROME_LINES = 3;
|
|
15
40
|
const MIN_VIEWPORT = 3;
|
|
41
|
+
/** Lines moved by one ↑/k or ↓/j press. */
|
|
42
|
+
const SCROLL_LINE_STEP = 1;
|
|
43
|
+
/** Lines moved by one PgUp/PgDn press (same page size as the agent view overlay). */
|
|
44
|
+
const SCROLL_PAGE_STEP = 10;
|
|
45
|
+
/** Bar width of the `% ctx` gauge in the meta row. */
|
|
46
|
+
const CTX_GAUGE_WIDTH = 8;
|
|
16
47
|
|
|
17
48
|
export class LiveConversationOverlay {
|
|
18
49
|
private scrollOffset = 0;
|
|
@@ -31,11 +62,14 @@ export class LiveConversationOverlay {
|
|
|
31
62
|
private handle: LiveAgentHandle;
|
|
32
63
|
private theme: CrewTheme;
|
|
33
64
|
|
|
34
|
-
|
|
65
|
+
private readonly nowMs: () => number;
|
|
66
|
+
|
|
67
|
+
constructor(handle: LiveAgentHandle, theme: CrewTheme, columns = 80, rows = 24, nowMs?: () => number) {
|
|
35
68
|
this.handle = handle;
|
|
36
69
|
this.theme = theme;
|
|
37
70
|
this.columns = columns;
|
|
38
71
|
this.rows = rows;
|
|
72
|
+
this.nowMs = nowMs ?? Date.now;
|
|
39
73
|
// R8: Subscribe to real session events if available
|
|
40
74
|
const session = handle.session as Record<string, unknown>;
|
|
41
75
|
if (typeof session.subscribe === "function") {
|
|
@@ -46,7 +80,7 @@ export class LiveConversationOverlay {
|
|
|
46
80
|
const text = typeof obj.text === "string" ? obj.text : typeof obj.content === "string" ? obj.content : "";
|
|
47
81
|
if (text.trim()) {
|
|
48
82
|
this.pushLine(text);
|
|
49
|
-
if (this.autoScroll) this.scrollOffset =
|
|
83
|
+
if (this.autoScroll) this.scrollOffset = this.maxScrollOffset();
|
|
50
84
|
}
|
|
51
85
|
});
|
|
52
86
|
} catch {
|
|
@@ -86,83 +120,182 @@ export class LiveConversationOverlay {
|
|
|
86
120
|
|
|
87
121
|
private refreshSummary(): void {
|
|
88
122
|
const act = this.handle.activity;
|
|
89
|
-
const summary = `${LiveConversationOverlay.SUMMARY_PREFIX}[${act.turnCount}
|
|
123
|
+
const summary = `${LiveConversationOverlay.SUMMARY_PREFIX}[${formatCount(act.turnCount ?? 0, "turn")} · ${formatCount(act.toolUses ?? 0, "tool")} · ${(computeLiveDurationMs(act, this.nowMs()) / 1000).toFixed(1)}s]`;
|
|
90
124
|
const lastLine = this.cachedLines[this.cachedLines.length - 1];
|
|
91
125
|
if (lastLine?.startsWith(LiveConversationOverlay.SUMMARY_PREFIX)) {
|
|
92
126
|
this.cachedLines[this.cachedLines.length - 1] = summary;
|
|
93
127
|
} else {
|
|
94
128
|
this.pushLine(summary);
|
|
95
129
|
}
|
|
96
|
-
if (this.autoScroll) this.scrollOffset =
|
|
130
|
+
if (this.autoScroll) this.scrollOffset = this.maxScrollOffset();
|
|
97
131
|
}
|
|
98
132
|
|
|
99
133
|
private viewportHeight(): number {
|
|
100
134
|
return Math.max(MIN_VIEWPORT, this.rows - CHROME_LINES);
|
|
101
135
|
}
|
|
102
136
|
|
|
137
|
+
/** Largest offset that still shows the newest line in the viewport. */
|
|
138
|
+
private maxScrollOffset(): number {
|
|
139
|
+
return Math.max(0, this.cachedLines.length - this.viewportHeight());
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* autoScroll rule (M1-6):
|
|
144
|
+
* - a manual scroll always recomputes autoScroll from the position — the
|
|
145
|
+
* viewport tails the stream only while it sits at the newest line;
|
|
146
|
+
* - scrolling DOWN to (or past) the newest line therefore restores
|
|
147
|
+
* autoScroll, and scrolling UP away from it pauses it so incoming lines
|
|
148
|
+
* cannot yank the viewport back;
|
|
149
|
+
* - `a` toggles explicitly: turning it on jumps to the newest line,
|
|
150
|
+
* turning it off pauses wherever the viewport currently is.
|
|
151
|
+
*/
|
|
152
|
+
private scrollBy(delta: number): void {
|
|
153
|
+
this.scrollOffset = Math.max(0, Math.min(this.maxScrollOffset(), this.scrollOffset + delta));
|
|
154
|
+
this.autoScroll = this.scrollOffset >= this.maxScrollOffset();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Jump to the oldest buffered line. */
|
|
158
|
+
private scrollHome(): void {
|
|
159
|
+
this.scrollBy(-this.cachedLines.length);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Jump to the newest line (re-enables auto-scroll). */
|
|
163
|
+
private scrollEnd(): void {
|
|
164
|
+
this.scrollBy(this.cachedLines.length);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Toggle auto-scroll; turning it on jumps to the newest line. */
|
|
168
|
+
private toggleAutoScroll(): void {
|
|
169
|
+
if (this.autoScroll) this.autoScroll = false;
|
|
170
|
+
else this.scrollEnd();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Handle a keypress while the overlay has focus. Mirrors the key set of
|
|
175
|
+
* src/ui/inline-panel/agent-view-overlay.ts:160-182:
|
|
176
|
+
*
|
|
177
|
+
* esc / q close the overlay
|
|
178
|
+
* ↓ / j scroll down one line
|
|
179
|
+
* ↑ / k scroll up one line
|
|
180
|
+
* PgDn / PgUp scroll one page
|
|
181
|
+
* g (home) jump to the oldest buffered line
|
|
182
|
+
* G (end) jump to the newest line (re-enables auto-scroll)
|
|
183
|
+
* a toggle auto-scroll
|
|
184
|
+
*
|
|
185
|
+
* The host component in src/extension/registration/viewers.ts intercepts
|
|
186
|
+
* esc/q itself (it owns the `done()` callback) and forwards every other key
|
|
187
|
+
* here; the esc/q branch below keeps this class self-contained for direct
|
|
188
|
+
* callers and for the host contract. Unrecognised keys are ignored — the
|
|
189
|
+
* overlay never leaks keys into the editor underneath.
|
|
190
|
+
*/
|
|
191
|
+
handleInput(data: string): void {
|
|
192
|
+
if (this.closed) return;
|
|
193
|
+
if (matchesKey(data, "escape") || data === "q") {
|
|
194
|
+
this.close();
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (matchesKey(data, "down") || data === "j") {
|
|
198
|
+
this.scrollBy(SCROLL_LINE_STEP);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (matchesKey(data, "up") || data === "k") {
|
|
202
|
+
this.scrollBy(-SCROLL_LINE_STEP);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (matchesKey(data, "pageDown")) {
|
|
206
|
+
this.scrollBy(SCROLL_PAGE_STEP);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (matchesKey(data, "pageUp")) {
|
|
210
|
+
this.scrollBy(-SCROLL_PAGE_STEP);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (matchesKey(data, "home") || data === "g") {
|
|
214
|
+
this.scrollHome();
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (matchesKey(data, "end") || data === "G") {
|
|
218
|
+
this.scrollEnd();
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (data === "a") {
|
|
222
|
+
this.toggleAutoScroll();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Left half of the close cap: the key legend, through `formatHint`. */
|
|
227
|
+
private hintText(): string {
|
|
228
|
+
return formatHint([
|
|
229
|
+
[["up", "down", "pageup", "pagedown", "g", "G"], "scroll"],
|
|
230
|
+
["a", this.autoScroll ? "pause" : "resume"],
|
|
231
|
+
[["escape", "q"], "close"],
|
|
232
|
+
]);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Right half of the close cap: the scroll STATE (dot-led so it can never be
|
|
237
|
+
* the segment that gets truncated). Every key advertised is handled by
|
|
238
|
+
* `handleInput` in BOTH states (P1-3: the old `↑/k ↓/j G/g` hint lived in an
|
|
239
|
+
* autoScroll === false branch that could never be reached, because nothing
|
|
240
|
+
* ever turned autoScroll off).
|
|
241
|
+
*/
|
|
242
|
+
private stateText(): string {
|
|
243
|
+
if (this.autoScroll) return "auto-scroll";
|
|
244
|
+
const total = this.cachedLines.length;
|
|
245
|
+
const from = total === 0 ? 0 : this.scrollOffset + 1;
|
|
246
|
+
const to = Math.min(total, this.scrollOffset + this.viewportHeight());
|
|
247
|
+
return `manual ${from}-${to}/${total}`;
|
|
248
|
+
}
|
|
249
|
+
|
|
103
250
|
render(width?: number): string[] {
|
|
104
251
|
const w = width ?? this.columns;
|
|
105
252
|
if (w < 6) return [];
|
|
106
253
|
const th = this.theme;
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
const hrBot = th.fg("border", `╰${"─".repeat(w - 2)}╯`);
|
|
111
|
-
const hrMid = row(th.fg("dim", "─".repeat(innerW)));
|
|
254
|
+
const budget = w - 2;
|
|
255
|
+
const act = this.handle.activity;
|
|
256
|
+
const slot = statusSlot(this.handle.status);
|
|
112
257
|
|
|
113
258
|
const lines: string[] = [];
|
|
114
|
-
|
|
259
|
+
// Canopy: ┏ LIVE ▸ <agent> (every identity field guarded — the handle is
|
|
260
|
+
// assembled at runtime and a missing name must never print `undefined`).
|
|
261
|
+
lines.push(canopyLine({ word: "LIVE", subject: this.handle.agent ?? this.handle.taskId ?? "?", theme: th, budget }));
|
|
115
262
|
|
|
116
|
-
//
|
|
263
|
+
// Meta row: status glyph, description, then the counters.
|
|
117
264
|
const statusIcon =
|
|
118
265
|
this.handle.status === "running"
|
|
119
|
-
? th.fg("accent", spinnerFrame(this.handle.taskId ?? this.handle.agentId))
|
|
266
|
+
? th.fg("accent", spinnerFrame(this.handle.taskId ?? this.handle.agentId ?? ""))
|
|
120
267
|
: iconForStatus(this.handle.status);
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (act.
|
|
126
|
-
|
|
127
|
-
if (act.toolUses > 0) headerParts.push(`${act.toolUses} tools`);
|
|
128
|
-
headerParts.push(elapsed);
|
|
129
|
-
|
|
130
|
-
// Context % + compaction
|
|
268
|
+
const desc = this.handle.description ?? this.handle.role ?? "";
|
|
269
|
+
const parts: string[] = [];
|
|
270
|
+
if (act.maxTurns != null) parts.push(`turn ${act.turnCount ?? 0}/${act.maxTurns}`);
|
|
271
|
+
else if ((act.turnCount ?? 0) > 0) parts.push(`turn ${act.turnCount}`);
|
|
272
|
+
if ((act.toolUses ?? 0) > 0) parts.push(formatCount(act.toolUses ?? 0, "tool"));
|
|
273
|
+
parts.push(`${(computeLiveDurationMs(act, this.nowMs()) / 1000).toFixed(1)}s`);
|
|
131
274
|
try {
|
|
132
275
|
const ctxPct = this.handle.session.getSessionStats?.()?.contextUsage?.percent;
|
|
133
276
|
if (ctxPct != null) {
|
|
134
|
-
const color = ctxPct >= 85 ? "error" : ctxPct >= 70 ? "warning" : "dim";
|
|
135
|
-
|
|
277
|
+
const color: CrewThemeColor = ctxPct >= 85 ? "error" : ctxPct >= 70 ? "warning" : "dim";
|
|
278
|
+
parts.push(th.fg(color, `${gaugeBar(ctxPct / 100, CTX_GAUGE_WIDTH, th, color)} ${Math.round(ctxPct)}% ctx`));
|
|
136
279
|
}
|
|
137
280
|
} catch {
|
|
138
281
|
/* ignore */
|
|
139
282
|
}
|
|
140
|
-
if (act.compactionCount > 0)
|
|
141
|
-
|
|
142
|
-
|
|
283
|
+
if ((act.compactionCount ?? 0) > 0) parts.push(th.fg("dim", `↻${act.compactionCount}`));
|
|
284
|
+
if (this.handle.modelName) parts.push(th.fg("muted", this.handle.modelName));
|
|
285
|
+
const describe = desc ? `${th.fg("muted", desc)} ${th.fg("dim", "·")} ` : "";
|
|
286
|
+
lines.push(railLine(RAIL.body, slot, truncate(`${statusIcon} ${describe}${th.fg("dim", parts.join(" · "))}`, budget), th, budget));
|
|
143
287
|
|
|
144
|
-
|
|
145
|
-
lines.push(
|
|
146
|
-
row(
|
|
147
|
-
`${statusIcon} ${th.fg("accent", name)} ${th.fg("muted", desc)} ${th.fg("dim", "·")} ${th.fg("dim", headerParts.join(" · "))}`,
|
|
148
|
-
),
|
|
149
|
-
);
|
|
150
|
-
lines.push(hrMid);
|
|
151
|
-
|
|
152
|
-
// Content
|
|
288
|
+
// Transcript body.
|
|
153
289
|
const vh = this.viewportHeight();
|
|
154
290
|
const visible = this.cachedLines.slice(this.scrollOffset, this.scrollOffset + vh);
|
|
155
291
|
for (const line of visible) {
|
|
156
|
-
lines.push(
|
|
292
|
+
lines.push(railLine(RAIL.body, "border", th.fg("dim", truncate(line, budget)), th, budget));
|
|
157
293
|
}
|
|
158
294
|
|
|
159
|
-
//
|
|
160
|
-
lines.push(
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
: `line ${this.scrollOffset + 1}/${this.cachedLines.length} · ↑/k ↓/j G/g · esc/q close`;
|
|
164
|
-
lines.push(row(th.fg("dim", footerText)));
|
|
165
|
-
lines.push(hrBot);
|
|
295
|
+
// Close cap: hint (left) ···· scroll state (right).
|
|
296
|
+
lines.push(
|
|
297
|
+
railLine(RAIL.close, slot, railLeaders(th.fg("dim", this.hintText()), th.fg("dim", this.stateText()), budget, th), th, budget),
|
|
298
|
+
);
|
|
166
299
|
return lines;
|
|
167
300
|
}
|
|
168
301
|
|
|
@@ -5,9 +5,12 @@ import { readCrewAgents } from "../runtime/crew-agent-records.ts";
|
|
|
5
5
|
import { formatTaskGraphLines, waitingReason } from "../runtime/task-display.ts";
|
|
6
6
|
import { loadRunManifestById } from "../state/stores/state-store.ts";
|
|
7
7
|
import type { TeamTaskState } from "../state/types.ts";
|
|
8
|
-
import { aggregateUsage,
|
|
8
|
+
import { aggregateUsage, formatTokens } from "../state/usage.ts";
|
|
9
9
|
import { readJsonFileCoalesced } from "../utils/file-coalescer.ts";
|
|
10
|
-
import {
|
|
10
|
+
import { truncate } from "../utils/visual.ts";
|
|
11
|
+
import { formatCount, teamWorkflowLabel } from "./format-helpers.ts";
|
|
12
|
+
import { DASHBOARD_KEYS } from "./keybinding-map.ts";
|
|
13
|
+
import { ACTIVE, canopyLine, formatHint, RAIL, type RailSlot, railLine, sectionLine, shortId, statusSlot } from "./rail.ts";
|
|
11
14
|
import type { OverlaySchedulerHandle } from "./shared-overlay-scheduler.ts";
|
|
12
15
|
import { registerOverlayScheduler } from "./shared-overlay-scheduler.ts";
|
|
13
16
|
import type { RunSnapshotCache, RunUiSnapshot } from "./snapshot-types.ts";
|
|
@@ -21,13 +24,11 @@ const TASK_READ_TTL_MS = 200;
|
|
|
21
24
|
|
|
22
25
|
type Done = (value: undefined) => void;
|
|
23
26
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return `${left}${fill.repeat(Math.max(0, width - 2))}${right}`;
|
|
30
|
-
}
|
|
27
|
+
/** The ONE footer hint of the sidebar: keys label pairs, close LAST (RAIL §1). */
|
|
28
|
+
const SIDEBAR_HINT: ReadonlyArray<readonly [string, string]> = [
|
|
29
|
+
["/team-dashboard", "details"],
|
|
30
|
+
[DASHBOARD_KEYS.close[0] ?? "q", "close"],
|
|
31
|
+
];
|
|
31
32
|
|
|
32
33
|
function readTasks(path: string): TeamTaskState[] {
|
|
33
34
|
const parse = () => {
|
|
@@ -43,7 +44,25 @@ function readTasks(path: string): TeamTaskState[] {
|
|
|
43
44
|
|
|
44
45
|
function shortUsage(tasks: TeamTaskState[]): string {
|
|
45
46
|
const usage = aggregateUsage(tasks);
|
|
46
|
-
return usage ?
|
|
47
|
+
return usage ? compactUsage(usage) : "usage=(none)";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* TUI form of a usage record: `↑2.8k ↓3.7k $0.000`.
|
|
52
|
+
*
|
|
53
|
+
* `formatUsage` (state/usage.ts) is the `key=value` form used by CLI/status
|
|
54
|
+
* output (`input=2780, output=3715, cacheRead=57216, cost=0.000000, turns=0`) —
|
|
55
|
+
* correct for a log line, but on a 118-column rail row it buries the numbers
|
|
56
|
+
* the eye actually wants. The card's usage row has always used this compact
|
|
57
|
+
* form; the sidebar now matches it.
|
|
58
|
+
*/
|
|
59
|
+
function compactUsage(usage: ReturnType<typeof aggregateUsage>): string {
|
|
60
|
+
if (!usage) return "usage=(none)";
|
|
61
|
+
const parts: string[] = [];
|
|
62
|
+
if (usage.input !== undefined) parts.push(`↑${formatTokens(usage.input)}`);
|
|
63
|
+
if (usage.output !== undefined) parts.push(`↓${formatTokens(usage.output)}`);
|
|
64
|
+
if (usage.cost !== undefined && Number.isFinite(usage.cost) && usage.cost > 0) parts.push(`$${usage.cost.toFixed(3)}`);
|
|
65
|
+
return parts.length > 0 ? parts.join(" ") : "usage=(none)";
|
|
47
66
|
}
|
|
48
67
|
|
|
49
68
|
export class LiveRunSidebar {
|
|
@@ -147,13 +166,14 @@ export class LiveRunSidebar {
|
|
|
147
166
|
|
|
148
167
|
render(width: number): string[] {
|
|
149
168
|
const w = Math.max(36, width);
|
|
169
|
+
const budget = w - 2;
|
|
150
170
|
|
|
151
171
|
// P0-6: render from snapshots only — never read disk on every render tick.
|
|
152
172
|
// Production wires a snapshotCache (extension/registration/ui.ts). When
|
|
153
|
-
// the cache hasn't populated yet we paint a
|
|
154
|
-
// the pre-load
|
|
155
|
-
//
|
|
156
|
-
//
|
|
173
|
+
// the cache hasn't populated yet we paint a well-formed two-row frame so
|
|
174
|
+
// the pre-load surface still wears the rail; when the cache is undefined
|
|
175
|
+
// (tests/dev) we fall back to direct disk reads so existing unit tests
|
|
176
|
+
// keep working.
|
|
157
177
|
let run: import("../state/types.ts").TeamRunManifest;
|
|
158
178
|
let tasks: TeamTaskState[];
|
|
159
179
|
let rawAgents: ReturnType<typeof readCrewAgents>;
|
|
@@ -165,7 +185,16 @@ export class LiveRunSidebar {
|
|
|
165
185
|
snapshot = undefined;
|
|
166
186
|
}
|
|
167
187
|
if (!snapshot) {
|
|
168
|
-
|
|
188
|
+
// A well-formed pre-load frame: same canopy/cap grammar as the loaded
|
|
189
|
+
// one, with a single dim status row.
|
|
190
|
+
return this.renderFrame(
|
|
191
|
+
[
|
|
192
|
+
canopyLine({ word: "LIVE", subject: shortId(this.runId), theme: this.theme, budget }),
|
|
193
|
+
railLine(RAIL.body, "border", this.theme.fg("dim", "loading…"), this.theme, budget),
|
|
194
|
+
],
|
|
195
|
+
budget,
|
|
196
|
+
"border",
|
|
197
|
+
);
|
|
169
198
|
}
|
|
170
199
|
run = snapshot.manifest;
|
|
171
200
|
tasks = snapshot.tasks;
|
|
@@ -173,14 +202,13 @@ export class LiveRunSidebar {
|
|
|
173
202
|
} else {
|
|
174
203
|
const loaded = loadRunManifestById(this.cwd, this.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency;
|
|
175
204
|
if (!loaded) {
|
|
176
|
-
return
|
|
205
|
+
return this.renderFrame(
|
|
177
206
|
[
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
line("run not found", w),
|
|
181
|
-
border("╰", "─", "╯", w),
|
|
207
|
+
canopyLine({ word: "LIVE", subject: shortId(this.runId), theme: this.theme, budget }),
|
|
208
|
+
railLine(RAIL.body, "border", this.theme.fg("muted", "run not found"), this.theme, budget),
|
|
182
209
|
],
|
|
183
|
-
|
|
210
|
+
budget,
|
|
211
|
+
"border",
|
|
184
212
|
);
|
|
185
213
|
}
|
|
186
214
|
run = loaded.manifest;
|
|
@@ -194,6 +222,10 @@ export class LiveRunSidebar {
|
|
|
194
222
|
const waiting = tasks.filter((task) => task.status === "queued");
|
|
195
223
|
const signature = this.buildSignature(run.updatedAt, tasks, agents, waiting.length, snapshot);
|
|
196
224
|
if (signature !== this.cachedSignature || w !== this.cachedWidth) {
|
|
225
|
+
// RAIL (§2.E): the rounded `╭─╮│╰─╯` box is retired — the surface is a
|
|
226
|
+
// `┏ LIVE ▸ <run8>` canopy, `┃` body rows, `┣ SECTION` headers and a
|
|
227
|
+
// `┗ <hint>` cap. The rail colour carries the run's state.
|
|
228
|
+
const slot: RailSlot = statusSlot(run.status);
|
|
197
229
|
// L-2: surface the cancellation/failure reason for terminal runs so the
|
|
198
230
|
// user sees *why* a run ended without having to switch panes. The reason
|
|
199
231
|
// is already computed on the consumed snapshot (cancellationReason).
|
|
@@ -203,59 +235,98 @@ export class LiveRunSidebar {
|
|
|
203
235
|
? ` · ${truncate(snapshot.cancellationReason, 40)}`
|
|
204
236
|
: "";
|
|
205
237
|
const lines: string[] = [
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
238
|
+
canopyLine({ word: "LIVE", subject: shortId(run.runId), theme: this.theme, budget, slot }),
|
|
239
|
+
railLine(
|
|
240
|
+
RAIL.body,
|
|
241
|
+
"border",
|
|
242
|
+
this.theme.fg(
|
|
243
|
+
"muted",
|
|
244
|
+
`${shortId(run.runId)} · ${run.status ?? "?"}${reasonSuffix} · ${run.workspaceMode ?? "single"}`,
|
|
245
|
+
),
|
|
246
|
+
this.theme,
|
|
247
|
+
budget,
|
|
248
|
+
),
|
|
249
|
+
railLine(
|
|
250
|
+
RAIL.body,
|
|
251
|
+
"border",
|
|
252
|
+
this.theme.fg("muted", `${teamWorkflowLabel(run.team, run.workflow)} · ${shortUsage(tasks)}`),
|
|
253
|
+
this.theme,
|
|
254
|
+
budget,
|
|
255
|
+
),
|
|
256
|
+
sectionLine({ name: "active", subject: formatCount(active.length, "agent"), theme: this.theme, budget, slot }),
|
|
212
257
|
];
|
|
213
258
|
for (const agent of active.slice(0, 8)) {
|
|
214
259
|
const status = iconForStatus(agent.status, {
|
|
215
260
|
runningGlyph: spinnerFrame(agent.taskId),
|
|
216
261
|
});
|
|
217
262
|
const usage = agent.usage
|
|
218
|
-
?
|
|
263
|
+
? compactUsage(agent.usage)
|
|
219
264
|
: agent.progress?.tokens
|
|
220
265
|
? `tokens=${agent.progress.tokens}`
|
|
221
266
|
: "usage=pending";
|
|
222
|
-
|
|
267
|
+
// The record comes from unvalidated `agents.json` — every field is
|
|
268
|
+
// guarded (`?? "?"`) and the legacy `role->agent` separator is `▸`.
|
|
223
269
|
lines.push(
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
270
|
+
railLine(
|
|
271
|
+
RAIL.body,
|
|
272
|
+
"border",
|
|
273
|
+
`${status} ${agent.taskId ?? "?"} ${agent.role ?? "?"} ${ACTIVE} ${agent.agent ?? "?"}`,
|
|
274
|
+
this.theme,
|
|
275
|
+
budget,
|
|
227
276
|
),
|
|
228
277
|
);
|
|
229
278
|
lines.push(
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
279
|
+
railLine(
|
|
280
|
+
RAIL.body,
|
|
281
|
+
"border",
|
|
282
|
+
this.theme.fg(
|
|
283
|
+
"muted",
|
|
284
|
+
` ${agent.routing ? `model ${agent.routing.requested ? `${agent.routing.requested} → ` : ""}${agent.routing.resolved ?? "pending"}` : agent.model ? `model ${agent.model}` : "model pending"}`,
|
|
285
|
+
),
|
|
286
|
+
this.theme,
|
|
287
|
+
budget,
|
|
288
|
+
),
|
|
289
|
+
);
|
|
290
|
+
lines.push(
|
|
291
|
+
railLine(
|
|
292
|
+
RAIL.body,
|
|
293
|
+
"border",
|
|
294
|
+
this.theme.fg(
|
|
295
|
+
"muted",
|
|
296
|
+
` ${agent.progress?.currentTool ? `tool ${agent.progress.currentTool} · ` : ""}${formatCount(agent.toolUses ?? 0, "tool")} · ${usage}`,
|
|
297
|
+
),
|
|
298
|
+
this.theme,
|
|
299
|
+
budget,
|
|
233
300
|
),
|
|
234
301
|
);
|
|
235
302
|
}
|
|
236
|
-
if (!active.length) lines.push(
|
|
237
|
-
lines.push(
|
|
303
|
+
if (!active.length) lines.push(this.bodyRow(this.theme.fg("dim", "none"), budget));
|
|
304
|
+
lines.push(sectionLine({ name: "waiting", subject: formatCount(waiting.length, "task"), theme: this.theme, budget, slot }));
|
|
238
305
|
for (const task of waiting.slice(0, 8)) {
|
|
239
306
|
const status = iconForStatus("queued");
|
|
240
|
-
lines.push(
|
|
307
|
+
lines.push(this.bodyRow(`${status} ${task.id ?? "?"} ${waitingReason(task, tasks) ?? "waiting"}`, budget));
|
|
241
308
|
}
|
|
242
|
-
if (waiting.length === 0) lines.push(
|
|
243
|
-
lines.push(
|
|
309
|
+
if (waiting.length === 0) lines.push(this.bodyRow(this.theme.fg("dim", "none"), budget));
|
|
310
|
+
lines.push(sectionLine({ name: "done", subject: formatCount(completed.length, "agent"), theme: this.theme, budget, slot }));
|
|
244
311
|
for (const agent of completed) {
|
|
245
312
|
const status = iconForStatus(agent.status === "running" ? "stopped" : agent.status);
|
|
246
313
|
lines.push(
|
|
247
|
-
|
|
248
|
-
`${status} ${agent.taskId} ${agent.model ? `· ${agent.model}` : ""}${agent.usage ? ` · ${
|
|
249
|
-
|
|
314
|
+
this.bodyRow(
|
|
315
|
+
`${status} ${agent.taskId ?? "?"} ${agent.model ? `· ${agent.model}` : ""}${agent.usage ? ` · ${compactUsage(agent.usage)}` : ""}`,
|
|
316
|
+
budget,
|
|
250
317
|
),
|
|
251
318
|
);
|
|
252
319
|
}
|
|
253
|
-
if (completed.length === 0) lines.push(
|
|
254
|
-
lines.push(
|
|
255
|
-
for (const entry of formatTaskGraphLines(tasks).slice(0, 6))
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
320
|
+
if (completed.length === 0) lines.push(this.bodyRow(this.theme.fg("dim", "none"), budget));
|
|
321
|
+
lines.push(sectionLine({ name: "tasks", subject: formatCount(tasks.length, "task"), theme: this.theme, budget, slot }));
|
|
322
|
+
for (const entry of formatTaskGraphLines(tasks).slice(0, 6)) {
|
|
323
|
+
// `formatTaskGraphLines` (src/runtime/task-display.ts) still emits the
|
|
324
|
+
// legacy `role->agent` separator; normalize it at the render boundary
|
|
325
|
+
// so no `->` can reach the TUI (§4 consistency fix).
|
|
326
|
+
lines.push(this.bodyRow(this.theme.fg("muted", entry.replace(/->/g, ACTIVE)), budget));
|
|
327
|
+
}
|
|
328
|
+
// F-6: compute the auto-close countdown BEFORE the cap so the countdown
|
|
329
|
+
// renders inside the frame rather than below it.
|
|
259
330
|
// Auto-close logic: if run is terminal and no active agents, close after delay
|
|
260
331
|
const isTerminal = ["completed", "failed", "cancelled", "blocked"].includes(run.status);
|
|
261
332
|
const hasActiveAgents = agents.some((a) => a.status === "running");
|
|
@@ -268,7 +339,7 @@ export class LiveRunSidebar {
|
|
|
268
339
|
this.done(undefined);
|
|
269
340
|
}, autoCloseMs);
|
|
270
341
|
this.autoCloseTimeout?.unref();
|
|
271
|
-
lines.push(
|
|
342
|
+
lines.push(this.bodyRow(this.theme.fg("dim", `auto-close in ${Math.round(autoCloseMs / 1000)}s…`), budget));
|
|
272
343
|
}
|
|
273
344
|
}
|
|
274
345
|
// Clear timeout if conditions change
|
|
@@ -276,17 +347,25 @@ export class LiveRunSidebar {
|
|
|
276
347
|
clearTimeout(this.autoCloseTimeout);
|
|
277
348
|
this.autoCloseTimeout = undefined;
|
|
278
349
|
}
|
|
279
|
-
|
|
280
|
-
this.cachedLines = renderLines(
|
|
281
|
-
lines.map((entry) => this.colorLine(entry)),
|
|
282
|
-
w,
|
|
283
|
-
);
|
|
350
|
+
this.cachedLines = renderLines(this.renderFrame(lines, budget, slot), w);
|
|
284
351
|
this.cachedSignature = signature;
|
|
285
352
|
this.cachedWidth = w;
|
|
286
353
|
}
|
|
287
354
|
return this.cachedLines;
|
|
288
355
|
}
|
|
289
356
|
|
|
357
|
+
/** One `┃` body row (glyph + colour owned by the rail helper). */
|
|
358
|
+
private bodyRow(content: string, budget: number): string {
|
|
359
|
+
return railLine(RAIL.body, "border", content, this.theme, budget);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** Append the `┗ <hint>` cap and colorize the glyphs (F-1 / V-3). */
|
|
363
|
+
private renderFrame(lines: string[], budget: number, slot: RailSlot): string[] {
|
|
364
|
+
const hint = this.theme.fg("dim", formatHint(SIDEBAR_HINT));
|
|
365
|
+
const framed = [...lines, railLine(RAIL.close, slot, hint, this.theme, budget)];
|
|
366
|
+
return framed.map((entry) => this.colorLine(entry));
|
|
367
|
+
}
|
|
368
|
+
|
|
290
369
|
handleInput(data: string): void {
|
|
291
370
|
if (data === "q" || data === "\u001b") this.done(undefined);
|
|
292
371
|
}
|