pi-crew 0.11.0 → 0.11.1
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 +116 -0
- package/README.md +0 -1
- package/dist/index.mjs +29936 -29118
- package/package.json +1 -1
- package/schema.json +10 -0
- package/skills/real-test-pi-crew/REPORT-TEMPLATE.md +2 -0
- package/skills/real-test-pi-crew/SKILL.md +181 -1
- package/src/config/config-validation.ts +0 -2
- package/src/config/defaults.ts +0 -1
- package/src/config/types.ts +0 -5
- 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 +0 -2
- package/src/extension/register.ts +7 -2
- package/src/extension/registration/commands/manage.ts +5 -4
- package/src/extension/registration/commands/shared.ts +0 -1
- 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/team-tool/handle-settings.ts +14 -2
- package/src/runtime/task-display.ts +1 -1
- package/src/schema/config-schema.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 +45 -13
- 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/inline-panel/crew-editor.ts +13 -3
- package/src/ui/inline-panel/index.ts +60 -4
- package/src/ui/keybinding-map.ts +230 -32
- package/src/ui/live-conversation-overlay.ts +176 -46
- 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 +95 -74
- package/src/ui/settings-overlay.ts +81 -39
- 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 +126 -8
- package/src/ui/tool-renderers/brief-mode.ts +8 -66
- package/src/ui/tool-renderers/index.ts +372 -521
- 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/visual.ts +0 -4
- 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
|
@@ -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
|
}
|
package/src/ui/mascot.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { matchesKey } from "@earendil-works/pi-tui";
|
|
2
|
-
import {
|
|
3
|
-
import { DynamicCrewBorder } from "./dynamic-border.ts";
|
|
2
|
+
import { canopyLine, formatHint, RAIL, railLeaders, railLine } from "./rail.ts";
|
|
4
3
|
import type { CrewTheme } from "./theme-adapter.ts";
|
|
5
4
|
import { asCrewTheme } from "./theme-adapter.ts";
|
|
6
5
|
|
|
@@ -356,10 +355,8 @@ export class AnimatedMascot {
|
|
|
356
355
|
this.onDone();
|
|
357
356
|
}
|
|
358
357
|
|
|
359
|
-
private formatLine(line: string,
|
|
360
|
-
|
|
361
|
-
const themed = this.theme.fg(color, line);
|
|
362
|
-
return `│ ${pad(themed, contentWidth)} │`;
|
|
358
|
+
private formatLine(line: string, budget: number, color: Parameters<CrewTheme["fg"]>[0] = "accent"): string {
|
|
359
|
+
return railLine(RAIL.body, "border", this.theme.fg(color, line), this.theme, budget);
|
|
363
360
|
}
|
|
364
361
|
|
|
365
362
|
private currentCatFrame(): readonly string[] {
|
|
@@ -397,25 +394,44 @@ export class AnimatedMascot {
|
|
|
397
394
|
return this.cachedLines;
|
|
398
395
|
}
|
|
399
396
|
const safeWidth = Math.max(20, width);
|
|
400
|
-
|
|
397
|
+
// M4/RAIL (2026-09-16): the rounded box + DynamicCrewBorder were retired;
|
|
398
|
+
// this surface paints the same rail frame as every other pi-crew surface.
|
|
399
|
+
const budget = Math.max(4, safeWidth - 2);
|
|
401
400
|
const result: string[] = [
|
|
402
|
-
|
|
403
|
-
this.formatLine(this.theme.bold("
|
|
404
|
-
this.formatLine("",
|
|
401
|
+
canopyLine({ word: "MASCOT", subject: this.style, theme: this.theme, budget }),
|
|
402
|
+
this.formatLine(this.theme.bold("ARMIN SAYS HI"), budget),
|
|
403
|
+
this.formatLine("", budget),
|
|
405
404
|
];
|
|
406
405
|
if (this.style === "armin") {
|
|
407
406
|
for (const row of this.currentArminGrid) {
|
|
408
407
|
const text = row.join("");
|
|
409
|
-
result.push(this.formatLine(text,
|
|
408
|
+
result.push(this.formatLine(text, budget));
|
|
410
409
|
}
|
|
411
410
|
} else {
|
|
412
411
|
const frameLines = this.applyCatEffect(this.currentCatFrame());
|
|
413
|
-
for (const line of frameLines) result.push(this.formatLine(line,
|
|
412
|
+
for (const line of frameLines) result.push(this.formatLine(line, budget));
|
|
414
413
|
}
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
414
|
+
const state = this.style === "armin" ? `effect: ${this.effect}` : "animated preview";
|
|
415
|
+
result.push(
|
|
416
|
+
railLine(
|
|
417
|
+
RAIL.close,
|
|
418
|
+
"border",
|
|
419
|
+
railLeaders(
|
|
420
|
+
this.theme.fg(
|
|
421
|
+
"dim",
|
|
422
|
+
formatHint([
|
|
423
|
+
["esc", "close"],
|
|
424
|
+
["q", "close"],
|
|
425
|
+
]),
|
|
426
|
+
),
|
|
427
|
+
this.theme.fg("dim", state),
|
|
428
|
+
budget,
|
|
429
|
+
this.theme,
|
|
430
|
+
),
|
|
431
|
+
this.theme,
|
|
432
|
+
budget,
|
|
433
|
+
),
|
|
434
|
+
);
|
|
419
435
|
this.cachedWidth = safeWidth;
|
|
420
436
|
this.cachedVersion = this.gridVersion;
|
|
421
437
|
this.cachedFrame = this.frame;
|
|
@@ -1,8 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent picker overlay — RAIL design language (M4/E1, 2026-09-16).
|
|
3
|
+
*
|
|
4
|
+
* ┏ AGENTS ▸ <runId8>
|
|
5
|
+
* ┃ › explorer · running · executor ▸ worker
|
|
6
|
+
* ┗ ↑/↓ move · Enter select · Esc cancel
|
|
7
|
+
*
|
|
8
|
+
* Grammar: `canopyLine` opens the surface, every body row is a `┃` line built
|
|
9
|
+
* by `railLine` (which owns the width budget), the close cap carries the hint
|
|
10
|
+
* built by `formatHint` (close LAST, keys through `keyToken`). The rounded
|
|
11
|
+
* `╭─╮│╰─╯` frame and the hand-typed "↑/↓ move · Enter select · ESC cancel"
|
|
12
|
+
* string are retired.
|
|
13
|
+
*
|
|
14
|
+
* Keys/behaviour are unchanged: same `overlay:*` keyspace dispatch, same
|
|
15
|
+
* selection and clamping.
|
|
16
|
+
*
|
|
17
|
+
* File-sourced risk (audit §4): `agents.json` is NOT schema-validated at read
|
|
18
|
+
* time, so every interpolated record field is guarded (`?? "?"`) before it can
|
|
19
|
+
* reach a line — a missing field must never print as `undefined`.
|
|
20
|
+
*/
|
|
21
|
+
|
|
1
22
|
import { readCrewAgents } from "../../runtime/crew-agent-records.ts";
|
|
2
23
|
import type { CrewAgentRecord } from "../../runtime/crew-agent-runtime.ts";
|
|
3
24
|
import { loadRunManifestById } from "../../state/stores/state-store.ts";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
25
|
+
import { truncate } from "../../utils/visual.ts";
|
|
26
|
+
import { overlayActionForKey } from "../keybinding-map.ts";
|
|
27
|
+
import { ACTIVE, CURSOR, canopyLine, formatHint, RAIL, railLine, shortId } from "../rail.ts";
|
|
6
28
|
import { asCrewTheme, type CrewTheme } from "../theme-adapter.ts";
|
|
7
29
|
|
|
8
30
|
export interface AgentPickerSelection {
|
|
@@ -13,6 +35,7 @@ export class AgentPickerOverlay {
|
|
|
13
35
|
private readonly agents: CrewAgentRecord[];
|
|
14
36
|
private readonly done: (selection: AgentPickerSelection | undefined) => void;
|
|
15
37
|
private readonly theme: CrewTheme;
|
|
38
|
+
private readonly runId: string;
|
|
16
39
|
private selected = 0;
|
|
17
40
|
|
|
18
41
|
constructor(opts: {
|
|
@@ -25,6 +48,7 @@ export class AgentPickerOverlay {
|
|
|
25
48
|
this.agents = loaded ? readCrewAgents(loaded.manifest) : [];
|
|
26
49
|
this.done = opts.done;
|
|
27
50
|
this.theme = asCrewTheme(opts.theme ?? {});
|
|
51
|
+
this.runId = opts.runId;
|
|
28
52
|
}
|
|
29
53
|
|
|
30
54
|
invalidate(): void {
|
|
@@ -32,35 +56,66 @@ export class AgentPickerOverlay {
|
|
|
32
56
|
}
|
|
33
57
|
|
|
34
58
|
render(width: number): string[] {
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
59
|
+
if (width < 6) return [];
|
|
60
|
+
const theme = this.theme;
|
|
61
|
+
const budget = width - 2;
|
|
62
|
+
const lines: string[] = [
|
|
63
|
+
canopyLine({ word: "AGENTS", subject: shortId(this.runId), theme, budget }),
|
|
64
|
+
...this.agents.map((agent, index) =>
|
|
65
|
+
railLine(
|
|
66
|
+
RAIL.body,
|
|
67
|
+
"border",
|
|
68
|
+
truncate(
|
|
69
|
+
`${index === this.selected ? CURSOR : " "} ${agent.taskId ?? "?"} · ${agent.status ?? "?"} · ${
|
|
70
|
+
agent.role ?? "?"
|
|
71
|
+
} ${ACTIVE} ${agent.agent ?? "?"}`,
|
|
72
|
+
budget,
|
|
73
|
+
),
|
|
74
|
+
theme,
|
|
75
|
+
budget,
|
|
76
|
+
),
|
|
42
77
|
),
|
|
43
78
|
];
|
|
44
|
-
if (!this.agents.length)
|
|
45
|
-
|
|
79
|
+
if (!this.agents.length) {
|
|
80
|
+
lines.push(railLine(RAIL.body, "border", theme.fg("dim", "No agents found."), theme, budget));
|
|
81
|
+
}
|
|
82
|
+
lines.push(
|
|
83
|
+
railLine(
|
|
84
|
+
RAIL.close,
|
|
85
|
+
"border",
|
|
86
|
+
theme.fg(
|
|
87
|
+
"dim",
|
|
88
|
+
formatHint([
|
|
89
|
+
[["up", "down"], "move"],
|
|
90
|
+
["enter", "select"],
|
|
91
|
+
["escape", "cancel"],
|
|
92
|
+
]),
|
|
93
|
+
),
|
|
94
|
+
theme,
|
|
95
|
+
budget,
|
|
96
|
+
),
|
|
97
|
+
);
|
|
98
|
+
return lines;
|
|
46
99
|
}
|
|
47
100
|
|
|
48
101
|
handleInput(data: string): void {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
102
|
+
// M2-1: keys come from the central `overlay:*` keyspace (remappable via
|
|
103
|
+
// `.crew/config.json` → keybindings["overlay:agent-picker:<action>"]).
|
|
104
|
+
switch (overlayActionForKey("agent-picker", data)) {
|
|
105
|
+
case "close":
|
|
106
|
+
this.done(undefined);
|
|
107
|
+
return;
|
|
108
|
+
case "up":
|
|
109
|
+
this.selected = Math.max(0, this.selected - 1);
|
|
110
|
+
return;
|
|
111
|
+
case "down":
|
|
112
|
+
this.selected = Math.min(Math.max(0, this.agents.length - 1), this.selected + 1);
|
|
113
|
+
return;
|
|
114
|
+
case "select": {
|
|
115
|
+
const agent = this.agents[this.selected];
|
|
116
|
+
this.done(agent ? { agentId: agent.taskId } : undefined);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
64
119
|
}
|
|
65
120
|
}
|
|
66
121
|
}
|
|
@@ -1,6 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Confirm dialog overlay — RAIL design language (M4/E1, 2026-09-16).
|
|
3
|
+
*
|
|
4
|
+
* ┏ CONFIRM ▸ Delete?
|
|
5
|
+
* ┃ Danger
|
|
6
|
+
* ┗ Y confirm · N/Esc cancel
|
|
7
|
+
*
|
|
8
|
+
* The rounded `╭─╮├─┤│╰─╯` box (and its inline `├───┤` rules) is retired; the
|
|
9
|
+
* surface is now canopy / `┃` body / `┗` close cap. The rail colour carries the
|
|
10
|
+
* danger level (high → error, medium → warning, else neutral border) instead of
|
|
11
|
+
* colouring only the title, and the hint is built by `formatHint` (cancel LAST,
|
|
12
|
+
* keys through `keyToken` → `Esc`, never `ESC`).
|
|
13
|
+
*
|
|
14
|
+
* Keys/behaviour are unchanged: same `overlay:*` keyspace dispatch, Enter stays
|
|
15
|
+
* dual-role exactly as before.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { truncate } from "../../utils/visual.ts";
|
|
19
|
+
import { overlayActionForKey } from "../keybinding-map.ts";
|
|
20
|
+
import { canopyLine, formatHint, RAIL, type RailSlot, railLine } from "../rail.ts";
|
|
4
21
|
import { asCrewTheme, type CrewTheme } from "../theme-adapter.ts";
|
|
5
22
|
|
|
6
23
|
export interface ConfirmOptions {
|
|
@@ -26,36 +43,45 @@ export class ConfirmOverlay {
|
|
|
26
43
|
}
|
|
27
44
|
|
|
28
45
|
render(width: number): string[] {
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
46
|
+
if (width < 6) return [];
|
|
47
|
+
const theme = this.theme;
|
|
48
|
+
const budget = width - 2;
|
|
49
|
+
// Rail colour = state (§1): the danger level is the state of a confirm.
|
|
50
|
+
const slot: RailSlot = this.opts.dangerLevel === "high" ? "error" : this.opts.dangerLevel === "medium" ? "warning" : "border";
|
|
51
|
+
const hint =
|
|
52
|
+
this.opts.defaultAction === "confirm"
|
|
53
|
+
? formatHint([
|
|
54
|
+
[["enter", "y"], "confirm"],
|
|
55
|
+
[["n", "escape"], "cancel"],
|
|
56
|
+
])
|
|
57
|
+
: formatHint([
|
|
58
|
+
["y", "confirm"],
|
|
59
|
+
[["enter", "n", "escape"], "cancel"],
|
|
60
|
+
]);
|
|
33
61
|
const bodyLines = (this.opts.body ?? "").split(/\r?\n/).filter(Boolean);
|
|
34
|
-
const lines = [
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
),
|
|
41
|
-
`├${"─".repeat(innerWidth)}┤`,
|
|
42
|
-
`│ ${pad(truncate(this.theme.fg("dim", hint), innerWidth - 1), innerWidth - 1)}│`,
|
|
43
|
-
`╰${"─".repeat(innerWidth)}╯`,
|
|
44
|
-
];
|
|
45
|
-
const box = new Box(0, 0);
|
|
46
|
-
for (const line of lines) box.addChild(new Text(line));
|
|
47
|
-
return box.render(width);
|
|
62
|
+
const lines: string[] = [canopyLine({ word: "CONFIRM", subject: this.opts.title ?? "?", theme, budget, slot })];
|
|
63
|
+
for (const line of bodyLines.length ? bodyLines : ["Are you sure?"]) {
|
|
64
|
+
lines.push(railLine(RAIL.body, slot, truncate(line, budget), theme, budget));
|
|
65
|
+
}
|
|
66
|
+
lines.push(railLine(RAIL.close, slot, theme.fg("dim", hint), theme, budget));
|
|
67
|
+
return lines;
|
|
48
68
|
}
|
|
49
69
|
|
|
50
70
|
handleInput(data: string): void {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
71
|
+
// M2-1: keys come from the central `overlay:*` keyspace (remappable via
|
|
72
|
+
// `.crew/config.json` → keybindings["overlay:confirm:<action>"]).
|
|
73
|
+
switch (overlayActionForKey("confirm", data)) {
|
|
74
|
+
case "confirm":
|
|
75
|
+
this.done(true);
|
|
76
|
+
return;
|
|
77
|
+
// Enter is dual-role: it confirms when the overlay defaults to confirm
|
|
78
|
+
// and cancels otherwise (byte-identical to the pre-M2-1 chain).
|
|
79
|
+
case "submit":
|
|
80
|
+
this.done(this.opts.defaultAction === "confirm");
|
|
81
|
+
return;
|
|
82
|
+
case "cancel":
|
|
83
|
+
this.done(false);
|
|
84
|
+
return;
|
|
58
85
|
}
|
|
59
|
-
if (data === "n" || data === "N" || matchesKey(data, "escape") || data === "q" || matchesKey(data, "return")) this.done(false);
|
|
60
86
|
}
|
|
61
87
|
}
|