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
package/src/ui/keybinding-map.ts
CHANGED
|
@@ -18,6 +18,12 @@
|
|
|
18
18
|
* for zero benefit. The input-guard half of L2 is therefore intentionally
|
|
19
19
|
* skipped; only the DRY/data-driven dispatch refactor landed.
|
|
20
20
|
*
|
|
21
|
+
* M2-1 (P1-9) adds the sibling `overlay:*` keyspace below. The migrated
|
|
22
|
+
* overlays dispatch through `overlayActionForKey("<name>", data)` — the same
|
|
23
|
+
* data-driven + override pipeline as the dashboard, but a SEPARATE namespace:
|
|
24
|
+
* an overlay never consults `dashboardActionForKey`, so the no-leak argument
|
|
25
|
+
* above is unchanged.
|
|
26
|
+
*
|
|
21
27
|
* Origin pattern: deer-flow `frontend/src/components/workspace/command-palette.tsx:39-50`
|
|
22
28
|
* drives shortcuts from a single data array consumed by one loop in
|
|
23
29
|
* `use-global-shortcuts.ts:38-61`.
|
|
@@ -44,7 +50,6 @@ export const DASHBOARD_KEYS = {
|
|
|
44
50
|
transcript: ["v"],
|
|
45
51
|
liveConversation: ["V"],
|
|
46
52
|
reload: ["r"],
|
|
47
|
-
progressToggle: ["p"],
|
|
48
53
|
browser: ["b"],
|
|
49
54
|
},
|
|
50
55
|
pane: {
|
|
@@ -106,7 +111,6 @@ export type DashboardKeyAction =
|
|
|
106
111
|
| "transcript"
|
|
107
112
|
| "live-conversation"
|
|
108
113
|
| "reload"
|
|
109
|
-
| "progressToggle"
|
|
110
114
|
| "browser"
|
|
111
115
|
| "pane-agents"
|
|
112
116
|
| "pane-progress"
|
|
@@ -152,11 +156,13 @@ export type DashboardKeyAction =
|
|
|
152
156
|
*
|
|
153
157
|
* NOTE: mailbox action keys A/N/C/P/X (ack/nudge/compose/preview/ackAll) are
|
|
154
158
|
* intentionally NOT dispatched for the mailbox pane by this table. They live
|
|
155
|
-
* in `DASHBOARD_KEYS.mailbox` for reservation
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
159
|
+
* in `DASHBOARD_KEYS.mailbox` for reservation and are resolved by the mailbox
|
|
160
|
+
* overlays' own input handling — since M2-1 through the sibling `overlay:*`
|
|
161
|
+
* keyspace (`OVERLAY_KEYS["mailbox-detail"]` / `OVERLAY_KEYS["mailbox-compose"]`),
|
|
162
|
+
* never by `dashboardActionForKey`. The `plan` group reuses uppercase "A"
|
|
163
|
+
* (approve) pane-scoped to "progress" — it never fires while the mailbox pane
|
|
164
|
+
* (or the mailbox-detail overlay) owns input, so `mailbox.ack` behavior is
|
|
165
|
+
* unchanged.
|
|
160
166
|
*/
|
|
161
167
|
const DEFAULT_BINDINGS: readonly KeyBinding[] = [
|
|
162
168
|
{ keys: DASHBOARD_KEYS.close, action: "close" },
|
|
@@ -245,7 +251,6 @@ const DEFAULT_BINDINGS: readonly KeyBinding[] = [
|
|
|
245
251
|
{ keys: DASHBOARD_KEYS.root.transcript, action: "transcript" },
|
|
246
252
|
{ keys: DASHBOARD_KEYS.root.liveConversation, action: "live-conversation" },
|
|
247
253
|
{ keys: DASHBOARD_KEYS.root.reload, action: "reload" },
|
|
248
|
-
{ keys: DASHBOARD_KEYS.root.progressToggle, action: "progressToggle" },
|
|
249
254
|
// Agents & Jobs browser (one-keypress overlay, mirrors live-conversation).
|
|
250
255
|
// Collision analysis: "b" is unbound everywhere else — root-unscoped is
|
|
251
256
|
// safe; inside the browser overlay itself "p" is free because overlays
|
|
@@ -289,6 +294,103 @@ const KEY_RESERVED = new Set<string>([
|
|
|
289
294
|
|
|
290
295
|
export { KEY_RESERVED };
|
|
291
296
|
|
|
297
|
+
// ─── Overlay keybindings (P1-9 / M2-1) ─────────────────────────────────────
|
|
298
|
+
//
|
|
299
|
+
// Overlays used to hardcode their own key chains — the SAME
|
|
300
|
+
// "↑/↓/Enter/Esc" concept re-implemented in every overlay file, none of them
|
|
301
|
+
// remappable. M2-1 centralises them into an `overlay:<name>:<action>`
|
|
302
|
+
// keyspace that rides the SAME override pipeline as the dashboard table
|
|
303
|
+
// above: `.crew/config.json` → `keybindings` and `PI_CREW_KEYBINDINGS` both
|
|
304
|
+
// accept `"overlay:<name>:<action>": ["<key>", …]`, a colliding override is
|
|
305
|
+
// reverted to its default, and the revert is reported by
|
|
306
|
+
// `getKeybindingOverrideWarnings()`.
|
|
307
|
+
//
|
|
308
|
+
// Overlay bindings are validated INDEPENDENTLY of dashboard bindings: the two
|
|
309
|
+
// namespaces are never live at the same time (the host hands input to exactly
|
|
310
|
+
// one component — an open overlay or the dashboard), so a shared key such as
|
|
311
|
+
// `q`/`escape`/`A` is not a real ambiguity. Within one overlay a shared key IS
|
|
312
|
+
// ambiguous and is treated as a collision.
|
|
313
|
+
|
|
314
|
+
export const OVERLAY_KEYS = {
|
|
315
|
+
"agent-picker": {
|
|
316
|
+
close: ["escape", "q"],
|
|
317
|
+
up: ["k", "up"],
|
|
318
|
+
down: ["j", "down"],
|
|
319
|
+
select: ["return"],
|
|
320
|
+
},
|
|
321
|
+
confirm: {
|
|
322
|
+
confirm: ["y", "Y"],
|
|
323
|
+
/** Enter is dual-role: the overlay resolves it with `defaultAction`. */
|
|
324
|
+
submit: ["return"],
|
|
325
|
+
cancel: ["n", "N", "escape", "q"],
|
|
326
|
+
},
|
|
327
|
+
"mailbox-detail": {
|
|
328
|
+
close: ["escape", "q"],
|
|
329
|
+
toggleSide: ["tab", "\t"],
|
|
330
|
+
up: ["k", "up"],
|
|
331
|
+
down: ["j", "down"],
|
|
332
|
+
toggleDetail: ["return"],
|
|
333
|
+
ack: ["A"],
|
|
334
|
+
nudge: ["N"],
|
|
335
|
+
compose: ["C"],
|
|
336
|
+
ackAll: ["X"],
|
|
337
|
+
},
|
|
338
|
+
"mailbox-compose": {
|
|
339
|
+
cancel: ["escape"],
|
|
340
|
+
preview: ["P"],
|
|
341
|
+
nextField: ["tab", "\t"],
|
|
342
|
+
space: [" "],
|
|
343
|
+
backspace: ["backspace"],
|
|
344
|
+
submit: ["return"],
|
|
345
|
+
},
|
|
346
|
+
} as const;
|
|
347
|
+
|
|
348
|
+
type OverlayDefs = typeof OVERLAY_KEYS;
|
|
349
|
+
|
|
350
|
+
/** Overlay namespaces that opt into the central keybinding map. */
|
|
351
|
+
export type OverlayName = keyof OverlayDefs;
|
|
352
|
+
|
|
353
|
+
/** Every action across every overlay (union). */
|
|
354
|
+
export type OverlayAction = { [N in OverlayName]: keyof OverlayDefs[N] & string }[OverlayName];
|
|
355
|
+
|
|
356
|
+
/** Actions of ONE overlay — the precise return type of `overlayActionForKey`. */
|
|
357
|
+
export type OverlayActionOf<N extends OverlayName> = keyof OverlayDefs[N] & string;
|
|
358
|
+
|
|
359
|
+
/** Override key as written in config/env: `overlay:<name>:<action>`. */
|
|
360
|
+
export type OverlayBindingKey = { [N in OverlayName]: `overlay:${N & string}:${keyof OverlayDefs[N] & string}` }[OverlayName];
|
|
361
|
+
|
|
362
|
+
/** A resolved overlay binding (one action of one overlay). */
|
|
363
|
+
export interface OverlayBinding {
|
|
364
|
+
readonly overlay: OverlayName;
|
|
365
|
+
readonly action: OverlayAction;
|
|
366
|
+
readonly keys: readonly string[];
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** Compose the config/env override key for one overlay action. */
|
|
370
|
+
function overlayBindingKey(overlay: OverlayName, action: OverlayAction): OverlayBindingKey {
|
|
371
|
+
return `overlay:${overlay}:${action}` as OverlayBindingKey;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// `Object.entries` widens the const-typed tables; this cast restores the exact
|
|
375
|
+
// shape (no `any`; the `OverlayBinding` annotation below still checks it).
|
|
376
|
+
type OverlayTableEntry = readonly [OverlayName, Readonly<Record<string, readonly string[]>>];
|
|
377
|
+
|
|
378
|
+
const OVERLAY_TABLE = Object.entries(OVERLAY_KEYS) as readonly OverlayTableEntry[];
|
|
379
|
+
|
|
380
|
+
/** Every valid `overlay:<name>:<action>` override key. */
|
|
381
|
+
const OVERLAY_BINDING_KEYS: ReadonlySet<string> = new Set(
|
|
382
|
+
OVERLAY_TABLE.flatMap(([overlay, actions]) =>
|
|
383
|
+
Object.keys(actions).map((action) => overlayBindingKey(overlay, action as OverlayAction)),
|
|
384
|
+
),
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
const DEFAULT_OVERLAY_BINDINGS: ReadonlyMap<OverlayName, readonly OverlayBinding[]> = new Map<OverlayName, readonly OverlayBinding[]>(
|
|
388
|
+
OVERLAY_TABLE.map(([overlay, actions]) => [
|
|
389
|
+
overlay,
|
|
390
|
+
Object.entries(actions).map(([action, keys]): OverlayBinding => ({ overlay, action: action as OverlayAction, keys })),
|
|
391
|
+
]),
|
|
392
|
+
);
|
|
393
|
+
|
|
292
394
|
// ─── Keybinding overrides (UI-2) ───────────────────────────────────────────
|
|
293
395
|
//
|
|
294
396
|
// The hardcoded DEFAULT_BINDINGS above can be overridden per-action via two
|
|
@@ -309,13 +411,14 @@ export { KEY_RESERVED };
|
|
|
309
411
|
// first-match-wins dispatch unambiguous — a shadowed override never silently
|
|
310
412
|
// changes behaviour.
|
|
311
413
|
|
|
312
|
-
/** Override map: action → replacement keys. `Partial` ⇒ only listed actions.
|
|
313
|
-
|
|
414
|
+
/** Override map: action → replacement keys. `Partial` ⇒ only listed actions.
|
|
415
|
+
* Keys are dashboard actions plus `overlay:<name>:<action>` binding keys. */
|
|
416
|
+
export type KeybindingOverride = Partial<Record<DashboardKeyAction | OverlayBindingKey, readonly string[]>>;
|
|
314
417
|
|
|
315
418
|
const KEYBINDINGS_ENV = "PI_CREW_KEYBINDINGS";
|
|
316
419
|
|
|
317
|
-
/** Every dispatched action is a valid override target. */
|
|
318
|
-
const VALID_OVERRIDE_ACTIONS: ReadonlySet<string> = new Set(DEFAULT_BINDINGS.map((b) => b.action));
|
|
420
|
+
/** Every dispatched action + every `overlay:*` binding is a valid override target. */
|
|
421
|
+
const VALID_OVERRIDE_ACTIONS: ReadonlySet<string> = new Set<string>([...DEFAULT_BINDINGS.map((b) => b.action), ...OVERLAY_BINDING_KEYS]);
|
|
319
422
|
|
|
320
423
|
/** Coerce an unknown parsed value into a safe {@link KeybindingOverride}. */
|
|
321
424
|
function parseKeybindingOverride(raw: unknown): KeybindingOverride {
|
|
@@ -355,42 +458,87 @@ function paneScopesCompatible(a: PaneScope | undefined, b: PaneScope | undefined
|
|
|
355
458
|
|
|
356
459
|
interface EffectiveBindingsResult {
|
|
357
460
|
readonly bindings: readonly KeyBinding[];
|
|
358
|
-
/**
|
|
359
|
-
readonly
|
|
461
|
+
/** Resolved bindings per overlay (defaults + non-colliding overrides). */
|
|
462
|
+
readonly overlayBindings: ReadonlyMap<OverlayName, readonly OverlayBinding[]>;
|
|
463
|
+
/** Override targets whose override was rejected due to a collision. */
|
|
464
|
+
readonly reverted: readonly string[];
|
|
360
465
|
}
|
|
361
466
|
|
|
362
467
|
/**
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
*
|
|
468
|
+
* Does `candidate` share a key with any `other` binding that could be live at
|
|
469
|
+
* the same time? Shared by the dashboard and the overlay namespaces so the
|
|
470
|
+
* collision rule (and its revert semantics) has ONE implementation.
|
|
471
|
+
*/
|
|
472
|
+
function collidesWithOthers<B extends { readonly keys: readonly string[]; readonly action: string }>(
|
|
473
|
+
candidate: B,
|
|
474
|
+
others: readonly B[],
|
|
475
|
+
compatible: (a: B, b: B) => boolean,
|
|
476
|
+
): boolean {
|
|
477
|
+
for (const other of others) {
|
|
478
|
+
if (other.action === candidate.action) continue;
|
|
479
|
+
if (!compatible(candidate, other)) continue;
|
|
480
|
+
if (candidate.keys.some((k) => other.keys.includes(k))) return true;
|
|
481
|
+
}
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Apply `overrides` onto {@link DEFAULT_BINDINGS} + {@link DEFAULT_OVERLAY_BINDINGS}
|
|
487
|
+
* (replace keys per target, preserving each binding's pane scope / overlay) and
|
|
488
|
+
* detect collisions. A colliding override is reverted to its default so the
|
|
489
|
+
* dispatch stays unambiguous. Dashboard and overlay namespaces are validated
|
|
490
|
+
* separately (they are never live simultaneously).
|
|
366
491
|
*/
|
|
367
492
|
function computeEffectiveBindings(overrides: KeybindingOverride): EffectiveBindingsResult {
|
|
493
|
+
// ── dashboard namespace (semantics unchanged since UI-2) ──
|
|
368
494
|
const applied = new Map<DashboardKeyAction, KeyBinding>();
|
|
369
495
|
for (const def of DEFAULT_BINDINGS) {
|
|
370
496
|
const ov = overrides[def.action];
|
|
371
497
|
applied.set(def.action, ov && ov.length > 0 ? { keys: [...ov], action: def.action, pane: def.pane } : def);
|
|
372
498
|
}
|
|
373
499
|
const effective = [...applied.values()];
|
|
374
|
-
const reverted = new Set<
|
|
500
|
+
const reverted = new Set<string>();
|
|
375
501
|
for (const def of DEFAULT_BINDINGS) {
|
|
376
502
|
const ov = overrides[def.action];
|
|
377
503
|
if (!ov || ov.length === 0) continue; // not overridden
|
|
378
504
|
const ob = applied.get(def.action);
|
|
379
505
|
if (!ob) continue;
|
|
380
|
-
|
|
381
|
-
if (other.action === def.action) continue;
|
|
382
|
-
if (!paneScopesCompatible(ob.pane, other.pane)) continue;
|
|
383
|
-
if (ob.keys.some((k) => other.keys.includes(k))) {
|
|
384
|
-
reverted.add(def.action);
|
|
385
|
-
break;
|
|
386
|
-
}
|
|
387
|
-
}
|
|
506
|
+
if (collidesWithOthers(ob, effective, (a, b) => paneScopesCompatible(a.pane, b.pane))) reverted.add(def.action);
|
|
388
507
|
}
|
|
389
508
|
const bindings =
|
|
390
509
|
reverted.size > 0
|
|
391
510
|
? effective.map((b) => (reverted.has(b.action) ? (DEFAULT_BINDINGS.find((d) => d.action === b.action) ?? b) : b))
|
|
392
511
|
: effective;
|
|
393
|
-
|
|
512
|
+
|
|
513
|
+
// ── overlay namespace (M2-1) ──
|
|
514
|
+
const overlayBindings = new Map<OverlayName, readonly OverlayBinding[]>();
|
|
515
|
+
for (const [overlay, defaults] of DEFAULT_OVERLAY_BINDINGS) {
|
|
516
|
+
const perOverlay: OverlayBinding[] = defaults.map((def) => {
|
|
517
|
+
const ov = overrides[overlayBindingKey(overlay, def.action)];
|
|
518
|
+
return ov && ov.length > 0 ? { overlay, action: def.action, keys: [...ov] } : def;
|
|
519
|
+
});
|
|
520
|
+
for (const def of defaults) {
|
|
521
|
+
const key = overlayBindingKey(overlay, def.action);
|
|
522
|
+
const ov = overrides[key];
|
|
523
|
+
if (!ov || ov.length === 0) continue; // not overridden
|
|
524
|
+
const ob = perOverlay.find((b) => b.action === def.action);
|
|
525
|
+
if (!ob) continue;
|
|
526
|
+
// Same overlay ⇒ same input lineage ⇒ any shared key is ambiguous.
|
|
527
|
+
if (collidesWithOthers(ob, perOverlay, () => true)) reverted.add(key);
|
|
528
|
+
}
|
|
529
|
+
if (reverted.size === 0) {
|
|
530
|
+
overlayBindings.set(overlay, perOverlay);
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
overlayBindings.set(
|
|
534
|
+
overlay,
|
|
535
|
+
perOverlay.map((b) =>
|
|
536
|
+
reverted.has(overlayBindingKey(overlay, b.action)) ? (defaults.find((d) => d.action === b.action) ?? b) : b,
|
|
537
|
+
),
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return { bindings, overlayBindings, reverted: [...reverted] };
|
|
394
542
|
}
|
|
395
543
|
|
|
396
544
|
/** Read the `keybindings` section from `<cwd>/.crew/config.json`. */
|
|
@@ -428,15 +576,17 @@ interface EffectiveCache {
|
|
|
428
576
|
readonly configMtime: number | undefined;
|
|
429
577
|
readonly cwd: string;
|
|
430
578
|
readonly bindings: readonly KeyBinding[];
|
|
579
|
+
readonly overlayBindings: ReadonlyMap<OverlayName, readonly OverlayBinding[]>;
|
|
431
580
|
}
|
|
432
581
|
|
|
433
582
|
let _effectiveCache: EffectiveCache | null = null;
|
|
434
583
|
let _overrideWarnings: readonly string[] = [];
|
|
435
584
|
|
|
436
585
|
/**
|
|
437
|
-
* Resolve the effective dispatch
|
|
438
|
-
*
|
|
439
|
-
*
|
|
586
|
+
* Resolve the effective dispatch tables: {@link DEFAULT_BINDINGS} and
|
|
587
|
+
* {@link DEFAULT_OVERLAY_BINDINGS} with config + env overrides applied (env
|
|
588
|
+
* wins per target). Memoised on (env value, config mtime, cwd); a single
|
|
589
|
+
* `statSync` per call detects on-disk config changes.
|
|
440
590
|
*/
|
|
441
591
|
function getEffectiveBindings(cwd: string = process.cwd()): readonly KeyBinding[] {
|
|
442
592
|
const envRaw = getCrewEnv(KEYBINDINGS_ENV);
|
|
@@ -445,12 +595,18 @@ function getEffectiveBindings(cwd: string = process.cwd()): readonly KeyBinding[
|
|
|
445
595
|
return _effectiveCache.bindings;
|
|
446
596
|
}
|
|
447
597
|
const merged: KeybindingOverride = { ...readConfigKeybindings(cwd), ...readEnvKeybindings() };
|
|
448
|
-
const { bindings, reverted } = computeEffectiveBindings(merged);
|
|
598
|
+
const { bindings, overlayBindings, reverted } = computeEffectiveBindings(merged);
|
|
449
599
|
_overrideWarnings = reverted.map((a) => `keybinding override for '${a}' collides with another binding — reverting to default`);
|
|
450
|
-
_effectiveCache = { env: envRaw, configMtime, cwd, bindings };
|
|
600
|
+
_effectiveCache = { env: envRaw, configMtime, cwd, bindings, overlayBindings };
|
|
451
601
|
return bindings;
|
|
452
602
|
}
|
|
453
603
|
|
|
604
|
+
/** Resolved overlay bindings for one overlay (defaults + non-colliding overrides). */
|
|
605
|
+
function getEffectiveOverlayBindings(overlay: OverlayName): readonly OverlayBinding[] {
|
|
606
|
+
getEffectiveBindings(); // ensures the memo is warm + warnings are populated
|
|
607
|
+
return _effectiveCache?.overlayBindings.get(overlay) ?? DEFAULT_OVERLAY_BINDINGS.get(overlay) ?? [];
|
|
608
|
+
}
|
|
609
|
+
|
|
454
610
|
/** Warnings from the most recent override resolution (e.g. collisions). */
|
|
455
611
|
export function getKeybindingOverrideWarnings(): readonly string[] {
|
|
456
612
|
// Ensure a resolution has run so warnings are populated.
|
|
@@ -514,3 +670,45 @@ export function dashboardActionForKey(data: string, activePane?: ActivePane): Da
|
|
|
514
670
|
}
|
|
515
671
|
return undefined;
|
|
516
672
|
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Resolve a raw input `data` string to an OVERLAY action (M2-1 / P1-9).
|
|
676
|
+
*
|
|
677
|
+
* Mirror image of {@link dashboardActionForKey} over the overlay keyspace:
|
|
678
|
+
* same two-pass dispatch (case-sensitive exact match first, then the
|
|
679
|
+
* terminal-aware `matchesKey` normalization), same override pipeline
|
|
680
|
+
* (`.crew/config.json` → `keybindings["overlay:<name>:<action>"]`, then the
|
|
681
|
+
* `PI_CREW_KEYBINDINGS` env var, collision → revert to default).
|
|
682
|
+
*
|
|
683
|
+
* With no override configured the result is identical to the key chains the
|
|
684
|
+
* overlays used to hardcode (asserted by
|
|
685
|
+
* `test/unit/ui/overlay-keybindings.test.ts`).
|
|
686
|
+
*
|
|
687
|
+
* @param overlay Overlay namespace, e.g. `"confirm"`.
|
|
688
|
+
* @param data Raw key input (single char or escape sequence).
|
|
689
|
+
*/
|
|
690
|
+
export function overlayActionForKey<N extends OverlayName>(overlay: N, data: string): OverlayActionOf<N> | undefined {
|
|
691
|
+
const bindings = getEffectiveOverlayBindings(overlay);
|
|
692
|
+
// Pass 1 — exact, case-sensitive match (keeps "y"/"Y" and "A"/"a" distinct).
|
|
693
|
+
for (const binding of bindings) {
|
|
694
|
+
if (binding.keys.includes(data)) return binding.action as OverlayActionOf<N>;
|
|
695
|
+
}
|
|
696
|
+
// Pass 2 — escape-sequence / canonical KeyId normalisation.
|
|
697
|
+
//
|
|
698
|
+
// Single-character candidates are deliberately EXCLUDED here: pi-tui's
|
|
699
|
+
// `matchesKey()` is case-INSENSITIVE for plain ASCII
|
|
700
|
+
// (`matchesKey("a", "A") === true`), which would collapse the deliberate
|
|
701
|
+
// case distinctions this keyspace relies on ("A"=ack vs lowercase "a"=free
|
|
702
|
+
// text in mailbox-compose; "y"/"Y" in confirm). Pass 1 already matched
|
|
703
|
+
// single chars exactly and case-sensitively, mirroring the pre-M2-1 chains
|
|
704
|
+
// (`data === "A"`, `data === "P"`, …).
|
|
705
|
+
const key = keyOf(data);
|
|
706
|
+
for (const binding of bindings) {
|
|
707
|
+
for (const candidate of binding.keys) {
|
|
708
|
+
if (candidate.length === 1) continue;
|
|
709
|
+
if (key === candidate) return binding.action as OverlayActionOf<N>;
|
|
710
|
+
if (matchesKey(data, candidate as KeyId)) return binding.action as OverlayActionOf<N>;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
return undefined;
|
|
714
|
+
}
|
|
@@ -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;
|
|
@@ -46,7 +77,7 @@ export class LiveConversationOverlay {
|
|
|
46
77
|
const text = typeof obj.text === "string" ? obj.text : typeof obj.content === "string" ? obj.content : "";
|
|
47
78
|
if (text.trim()) {
|
|
48
79
|
this.pushLine(text);
|
|
49
|
-
if (this.autoScroll) this.scrollOffset =
|
|
80
|
+
if (this.autoScroll) this.scrollOffset = this.maxScrollOffset();
|
|
50
81
|
}
|
|
51
82
|
});
|
|
52
83
|
} catch {
|
|
@@ -86,83 +117,182 @@ export class LiveConversationOverlay {
|
|
|
86
117
|
|
|
87
118
|
private refreshSummary(): void {
|
|
88
119
|
const act = this.handle.activity;
|
|
89
|
-
const summary = `${LiveConversationOverlay.SUMMARY_PREFIX}[${act.turnCount}
|
|
120
|
+
const summary = `${LiveConversationOverlay.SUMMARY_PREFIX}[${formatCount(act.turnCount ?? 0, "turn")} · ${formatCount(act.toolUses ?? 0, "tool")} · ${(computeLiveDurationMs(act) / 1000).toFixed(1)}s]`;
|
|
90
121
|
const lastLine = this.cachedLines[this.cachedLines.length - 1];
|
|
91
122
|
if (lastLine?.startsWith(LiveConversationOverlay.SUMMARY_PREFIX)) {
|
|
92
123
|
this.cachedLines[this.cachedLines.length - 1] = summary;
|
|
93
124
|
} else {
|
|
94
125
|
this.pushLine(summary);
|
|
95
126
|
}
|
|
96
|
-
if (this.autoScroll) this.scrollOffset =
|
|
127
|
+
if (this.autoScroll) this.scrollOffset = this.maxScrollOffset();
|
|
97
128
|
}
|
|
98
129
|
|
|
99
130
|
private viewportHeight(): number {
|
|
100
131
|
return Math.max(MIN_VIEWPORT, this.rows - CHROME_LINES);
|
|
101
132
|
}
|
|
102
133
|
|
|
134
|
+
/** Largest offset that still shows the newest line in the viewport. */
|
|
135
|
+
private maxScrollOffset(): number {
|
|
136
|
+
return Math.max(0, this.cachedLines.length - this.viewportHeight());
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* autoScroll rule (M1-6):
|
|
141
|
+
* - a manual scroll always recomputes autoScroll from the position — the
|
|
142
|
+
* viewport tails the stream only while it sits at the newest line;
|
|
143
|
+
* - scrolling DOWN to (or past) the newest line therefore restores
|
|
144
|
+
* autoScroll, and scrolling UP away from it pauses it so incoming lines
|
|
145
|
+
* cannot yank the viewport back;
|
|
146
|
+
* - `a` toggles explicitly: turning it on jumps to the newest line,
|
|
147
|
+
* turning it off pauses wherever the viewport currently is.
|
|
148
|
+
*/
|
|
149
|
+
private scrollBy(delta: number): void {
|
|
150
|
+
this.scrollOffset = Math.max(0, Math.min(this.maxScrollOffset(), this.scrollOffset + delta));
|
|
151
|
+
this.autoScroll = this.scrollOffset >= this.maxScrollOffset();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Jump to the oldest buffered line. */
|
|
155
|
+
private scrollHome(): void {
|
|
156
|
+
this.scrollBy(-this.cachedLines.length);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Jump to the newest line (re-enables auto-scroll). */
|
|
160
|
+
private scrollEnd(): void {
|
|
161
|
+
this.scrollBy(this.cachedLines.length);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Toggle auto-scroll; turning it on jumps to the newest line. */
|
|
165
|
+
private toggleAutoScroll(): void {
|
|
166
|
+
if (this.autoScroll) this.autoScroll = false;
|
|
167
|
+
else this.scrollEnd();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Handle a keypress while the overlay has focus. Mirrors the key set of
|
|
172
|
+
* src/ui/inline-panel/agent-view-overlay.ts:160-182:
|
|
173
|
+
*
|
|
174
|
+
* esc / q close the overlay
|
|
175
|
+
* ↓ / j scroll down one line
|
|
176
|
+
* ↑ / k scroll up one line
|
|
177
|
+
* PgDn / PgUp scroll one page
|
|
178
|
+
* g (home) jump to the oldest buffered line
|
|
179
|
+
* G (end) jump to the newest line (re-enables auto-scroll)
|
|
180
|
+
* a toggle auto-scroll
|
|
181
|
+
*
|
|
182
|
+
* The host component in src/extension/registration/viewers.ts intercepts
|
|
183
|
+
* esc/q itself (it owns the `done()` callback) and forwards every other key
|
|
184
|
+
* here; the esc/q branch below keeps this class self-contained for direct
|
|
185
|
+
* callers and for the host contract. Unrecognised keys are ignored — the
|
|
186
|
+
* overlay never leaks keys into the editor underneath.
|
|
187
|
+
*/
|
|
188
|
+
handleInput(data: string): void {
|
|
189
|
+
if (this.closed) return;
|
|
190
|
+
if (matchesKey(data, "escape") || data === "q") {
|
|
191
|
+
this.close();
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (matchesKey(data, "down") || data === "j") {
|
|
195
|
+
this.scrollBy(SCROLL_LINE_STEP);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (matchesKey(data, "up") || data === "k") {
|
|
199
|
+
this.scrollBy(-SCROLL_LINE_STEP);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (matchesKey(data, "pageDown")) {
|
|
203
|
+
this.scrollBy(SCROLL_PAGE_STEP);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (matchesKey(data, "pageUp")) {
|
|
207
|
+
this.scrollBy(-SCROLL_PAGE_STEP);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (matchesKey(data, "home") || data === "g") {
|
|
211
|
+
this.scrollHome();
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (matchesKey(data, "end") || data === "G") {
|
|
215
|
+
this.scrollEnd();
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (data === "a") {
|
|
219
|
+
this.toggleAutoScroll();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Left half of the close cap: the key legend, through `formatHint`. */
|
|
224
|
+
private hintText(): string {
|
|
225
|
+
return formatHint([
|
|
226
|
+
[["up", "down", "pageup", "pagedown", "g", "G"], "scroll"],
|
|
227
|
+
["a", this.autoScroll ? "pause" : "resume"],
|
|
228
|
+
[["escape", "q"], "close"],
|
|
229
|
+
]);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Right half of the close cap: the scroll STATE (dot-led so it can never be
|
|
234
|
+
* the segment that gets truncated). Every key advertised is handled by
|
|
235
|
+
* `handleInput` in BOTH states (P1-3: the old `↑/k ↓/j G/g` hint lived in an
|
|
236
|
+
* autoScroll === false branch that could never be reached, because nothing
|
|
237
|
+
* ever turned autoScroll off).
|
|
238
|
+
*/
|
|
239
|
+
private stateText(): string {
|
|
240
|
+
if (this.autoScroll) return "auto-scroll";
|
|
241
|
+
const total = this.cachedLines.length;
|
|
242
|
+
const from = total === 0 ? 0 : this.scrollOffset + 1;
|
|
243
|
+
const to = Math.min(total, this.scrollOffset + this.viewportHeight());
|
|
244
|
+
return `manual ${from}-${to}/${total}`;
|
|
245
|
+
}
|
|
246
|
+
|
|
103
247
|
render(width?: number): string[] {
|
|
104
248
|
const w = width ?? this.columns;
|
|
105
249
|
if (w < 6) return [];
|
|
106
250
|
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)));
|
|
251
|
+
const budget = w - 2;
|
|
252
|
+
const act = this.handle.activity;
|
|
253
|
+
const slot = statusSlot(this.handle.status);
|
|
112
254
|
|
|
113
255
|
const lines: string[] = [];
|
|
114
|
-
|
|
256
|
+
// Canopy: ┏ LIVE ▸ <agent> (every identity field guarded — the handle is
|
|
257
|
+
// assembled at runtime and a missing name must never print `undefined`).
|
|
258
|
+
lines.push(canopyLine({ word: "LIVE", subject: this.handle.agent ?? this.handle.taskId ?? "?", theme: th, budget }));
|
|
115
259
|
|
|
116
|
-
//
|
|
260
|
+
// Meta row: status glyph, description, then the counters.
|
|
117
261
|
const statusIcon =
|
|
118
262
|
this.handle.status === "running"
|
|
119
|
-
? th.fg("accent", spinnerFrame(this.handle.taskId ?? this.handle.agentId))
|
|
263
|
+
? th.fg("accent", spinnerFrame(this.handle.taskId ?? this.handle.agentId ?? ""))
|
|
120
264
|
: 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
|
|
265
|
+
const desc = this.handle.description ?? this.handle.role ?? "";
|
|
266
|
+
const parts: string[] = [];
|
|
267
|
+
if (act.maxTurns != null) parts.push(`turn ${act.turnCount ?? 0}/${act.maxTurns}`);
|
|
268
|
+
else if ((act.turnCount ?? 0) > 0) parts.push(`turn ${act.turnCount}`);
|
|
269
|
+
if ((act.toolUses ?? 0) > 0) parts.push(formatCount(act.toolUses ?? 0, "tool"));
|
|
270
|
+
parts.push(`${(computeLiveDurationMs(act) / 1000).toFixed(1)}s`);
|
|
131
271
|
try {
|
|
132
272
|
const ctxPct = this.handle.session.getSessionStats?.()?.contextUsage?.percent;
|
|
133
273
|
if (ctxPct != null) {
|
|
134
|
-
const color = ctxPct >= 85 ? "error" : ctxPct >= 70 ? "warning" : "dim";
|
|
135
|
-
|
|
274
|
+
const color: CrewThemeColor = ctxPct >= 85 ? "error" : ctxPct >= 70 ? "warning" : "dim";
|
|
275
|
+
parts.push(th.fg(color, `${gaugeBar(ctxPct / 100, CTX_GAUGE_WIDTH, th, color)} ${Math.round(ctxPct)}% ctx`));
|
|
136
276
|
}
|
|
137
277
|
} catch {
|
|
138
278
|
/* ignore */
|
|
139
279
|
}
|
|
140
|
-
if (act.compactionCount > 0)
|
|
141
|
-
|
|
142
|
-
|
|
280
|
+
if ((act.compactionCount ?? 0) > 0) parts.push(th.fg("dim", `↻${act.compactionCount}`));
|
|
281
|
+
if (this.handle.modelName) parts.push(th.fg("muted", this.handle.modelName));
|
|
282
|
+
const describe = desc ? `${th.fg("muted", desc)} ${th.fg("dim", "·")} ` : "";
|
|
283
|
+
lines.push(railLine(RAIL.body, slot, truncate(`${statusIcon} ${describe}${th.fg("dim", parts.join(" · "))}`, budget), th, budget));
|
|
143
284
|
|
|
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
|
|
285
|
+
// Transcript body.
|
|
153
286
|
const vh = this.viewportHeight();
|
|
154
287
|
const visible = this.cachedLines.slice(this.scrollOffset, this.scrollOffset + vh);
|
|
155
288
|
for (const line of visible) {
|
|
156
|
-
lines.push(
|
|
289
|
+
lines.push(railLine(RAIL.body, "border", th.fg("dim", truncate(line, budget)), th, budget));
|
|
157
290
|
}
|
|
158
291
|
|
|
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);
|
|
292
|
+
// Close cap: hint (left) ···· scroll state (right).
|
|
293
|
+
lines.push(
|
|
294
|
+
railLine(RAIL.close, slot, railLeaders(th.fg("dim", this.hintText()), th.fg("dim", this.stateText()), budget, th), th, budget),
|
|
295
|
+
);
|
|
166
296
|
return lines;
|
|
167
297
|
}
|
|
168
298
|
|