pi-crew 0.11.0 → 0.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +155 -9
- package/README.md +161 -1037
- package/agents/verifier.md +18 -7
- package/dist/index.mjs +744 -90644
- package/docs/README.md +57 -46
- package/docs/architecture.md +87 -33
- package/docs/commands-reference.md +9 -5
- package/docs/troubleshooting.md +3 -2
- package/package.json +1 -3
- package/schema.json +39 -0
- package/skills/real-test-pi-crew/REPORT-TEMPLATE.md +2 -0
- package/skills/real-test-pi-crew/SKILL.md +371 -34
- package/src/agents/agent-config.ts +1 -1
- package/src/agents/discover-agents.ts +1 -1
- package/src/config/config-validation.ts +15 -2
- package/src/config/config.ts +47 -13
- package/src/config/defaults.ts +0 -1
- package/src/config/env-vars.ts +35 -0
- package/src/config/types.ts +19 -5
- package/src/errors.ts +2 -2
- package/src/extension/async-notifier.ts +23 -0
- package/src/extension/crew-vibes/config.ts +0 -21
- package/src/extension/crew-vibes/index.ts +0 -2
- package/src/extension/crew-vibes/render.ts +1 -50
- package/src/extension/help.ts +21 -12
- package/src/extension/knowledge-injection.ts +2 -1
- package/src/extension/management.ts +8 -3
- package/src/extension/notification-sink.ts +17 -0
- package/src/extension/register.ts +7 -2
- package/src/extension/registration/command-utils.ts +28 -2
- package/src/extension/registration/commands/dashboard.ts +11 -1
- package/src/extension/registration/commands/manage.ts +36 -19
- package/src/extension/registration/commands/run.ts +24 -2
- package/src/extension/registration/commands/shared.ts +23 -1
- package/src/extension/registration/commands/status.ts +25 -2
- package/src/extension/registration/context-builder.ts +8 -2
- package/src/extension/registration/health-notify-policy.ts +100 -0
- package/src/extension/registration/lazy-configurers.ts +35 -0
- package/src/extension/registration/lifecycle-handlers.ts +91 -30
- package/src/extension/registration/lifecycle.ts +75 -10
- package/src/extension/registration/observability.ts +98 -35
- package/src/extension/registration/registration-types.ts +7 -5
- package/src/extension/registration/runtime-cleanup.ts +9 -3
- package/src/extension/registration/subagent-helpers.ts +38 -0
- package/src/extension/registration/subagent-tools.ts +16 -6
- package/src/extension/registration/team-tool.ts +10 -3
- package/src/extension/registration/terminal-status-wiring.ts +172 -0
- package/src/extension/registration/viewers.ts +6 -0
- package/src/extension/registration/wire-cross-extension.ts +28 -0
- package/src/extension/run-compare.ts +220 -0
- package/src/extension/run-export.ts +37 -5
- package/src/extension/run-maintenance.ts +155 -5
- package/src/extension/team-tool/dispatch/index.ts +3 -2
- package/src/extension/team-tool/dispatch/manage.ts +5 -2
- package/src/extension/team-tool/goal.ts +4 -1
- package/src/extension/team-tool/handle-settings.ts +33 -4
- package/src/extension/team-tool/health-monitor.ts +21 -7
- package/src/extension/team-tool/lifecycle-actions.ts +49 -1
- package/src/extension/team-tool/plan.ts +10 -0
- package/src/extension/team-tool/routing-hint.ts +63 -0
- package/src/extension/team-tool/status.ts +4 -0
- package/src/extension/team-tool.ts +52 -6
- package/src/extension/webhook-notify.ts +382 -0
- package/src/observability/metric-sink.ts +12 -2
- package/src/prompt/prompt-runtime.ts +82 -31
- package/src/prompt/worker-events-channel.ts +12 -0
- package/src/runtime/README.md +1 -1
- package/src/runtime/async-runner.ts +87 -1
- package/src/runtime/background-runner.ts +313 -234
- package/src/runtime/broker/crew-broker.ts +17 -11
- package/src/runtime/broker/delegate/shadow-lifecycle.ts +92 -0
- package/src/runtime/broker/wait-status-cache.ts +1 -1
- package/src/runtime/child-pi/child-pi-timers.ts +1 -1
- package/src/runtime/child-pi/mock-fixtures.ts +48 -0
- package/src/runtime/crew-agent-records.ts +337 -45
- package/src/runtime/deadletter.ts +43 -1
- package/src/runtime/delegate-spawn.ts +5 -1
- package/src/runtime/dispatch-batch.ts +72 -5
- package/src/runtime/goal-workflow/goal-loop-runner.ts +73 -4
- package/src/runtime/heartbeat/heartbeat-watcher.ts +7 -0
- package/src/runtime/model/model-fallback.ts +21 -1
- package/src/runtime/model/pi-args.ts +8 -10
- package/src/runtime/recovery/crash-recovery.ts +25 -1
- package/src/runtime/run-worker.ts +12 -1
- package/src/runtime/scheduling/global-worker-cap.ts +13 -6
- package/src/runtime/scheduling/run-coalesced-task-group.ts +27 -1
- package/src/runtime/scheduling/scheduler.ts +49 -13
- package/src/runtime/scheduling/semaphore.ts +148 -20
- package/src/runtime/scratchpad/README.md +1 -1
- package/src/runtime/scratchpad/protocol.ts +1 -1
- package/src/runtime/settings-store.ts +1 -1
- package/src/runtime/skill-instructions.ts +22 -0
- package/src/runtime/stale-reconciler.ts +85 -13
- package/src/runtime/task-display.ts +1 -1
- package/src/runtime/task-runner/pre-execution.ts +26 -2
- package/src/runtime/task-runner/prompt-builder.ts +142 -45
- package/src/runtime/task-runner.ts +21 -1
- package/src/runtime/team-runner.ts +38 -1
- package/src/runtime/workspace-lock.ts +4 -1
- package/src/schema/config-schema.ts +18 -0
- package/src/schema/team-tool-schema.ts +17 -0
- package/src/state/atomic-write.ts +53 -0
- package/src/state/contracts.ts +109 -0
- package/src/state/coordination/locks.ts +191 -33
- package/src/state/coordination/mailbox.ts +140 -15
- package/src/state/crew-init.ts +87 -12
- package/src/state/event-log/cursor.ts +37 -1
- package/src/state/event-log/event-log-rotation.ts +72 -7
- package/src/state/stores/active-run-registry.ts +13 -1
- package/src/state/stores/state-store.ts +112 -22
- package/src/state/types.ts +4 -0
- package/src/ui/adaptive-card.ts +65 -0
- package/src/ui/agents-jobs-browser.ts +70 -64
- package/src/ui/card-colors.ts +36 -7
- package/src/ui/dashboard-panes/agents-pane.ts +55 -14
- package/src/ui/dashboard-panes/cancellation-pane.ts +0 -42
- package/src/ui/dashboard-panes/health-pane.ts +7 -5
- package/src/ui/dashboard-panes/mailbox-pane.ts +22 -6
- package/src/ui/dashboard-panes/metrics-pane.ts +15 -7
- package/src/ui/dashboard-panes/pane-theme.ts +21 -0
- package/src/ui/dashboard-panes/plan-pane.ts +63 -30
- package/src/ui/dashboard-panes/progress-pane.ts +3 -2
- package/src/ui/dashboard-panes/schedules-pane.ts +44 -21
- package/src/ui/dashboard-panes/transcript-pane.ts +11 -5
- package/src/ui/dwf-phase-display.ts +3 -20
- package/src/ui/format-helpers.ts +22 -0
- package/src/ui/heartbeat-aggregator.ts +34 -0
- package/src/ui/inline-panel/crew-editor.ts +13 -3
- package/src/ui/inline-panel/index.ts +60 -4
- package/src/ui/keybinding-map.ts +251 -35
- package/src/ui/live-conversation-overlay.ts +180 -47
- package/src/ui/live-run-sidebar.ts +134 -55
- package/src/ui/mascot.ts +32 -16
- package/src/ui/overlays/agent-picker-overlay.ts +81 -26
- package/src/ui/overlays/confirm-overlay.ts +55 -29
- package/src/ui/overlays/help-overlay.ts +108 -53
- package/src/ui/overlays/mailbox-compose-overlay.ts +89 -50
- package/src/ui/overlays/mailbox-detail-overlay.ts +137 -57
- package/src/ui/powerbar-publisher.ts +0 -1
- package/src/ui/rail.ts +333 -0
- package/src/ui/run-dashboard.ts +193 -79
- package/src/ui/run-snapshot-cache.ts +18 -1
- package/src/ui/settings-overlay.ts +81 -39
- package/src/ui/spinner.ts +26 -2
- package/src/ui/terminal-status.ts +7 -1
- package/src/ui/theme-adapter.ts +0 -45
- package/src/ui/theme-discovery.ts +12 -6
- package/src/ui/tool-progress-formatter.ts +128 -9
- package/src/ui/tool-renderers/brief-mode.ts +10 -67
- package/src/ui/tool-renderers/index.ts +374 -523
- package/src/ui/transcript-viewer.ts +30 -12
- package/src/ui/widget/index.ts +32 -52
- package/src/ui/widget/task-list.ts +64 -32
- package/src/ui/widget/widget-formatters.ts +3 -402
- package/src/ui/widget/widget-model.ts +28 -7
- package/src/ui/widget/widget-renderer.ts +201 -128
- package/src/ui/widget/widget-types.ts +0 -2
- package/src/utils/incremental-reader.ts +11 -3
- package/src/utils/paths.ts +94 -12
- package/src/utils/project-markers.ts +40 -0
- package/src/utils/visual.ts +0 -4
- package/src/worktree/worktree-manager.ts +206 -26
- package/workflows/distill.workflow.md +3 -3
- package/workflows/fast-fix.workflow.md +1 -1
- package/workflows/plan-execute.workflow.md +1 -1
- package/workflows/review.workflow.md +1 -1
- package/workflows/strict-fast-fix.workflow.md +1 -1
- package/docs/migration-v0.4-v0.5.md +0 -208
- package/docs/runtime-flow.md +0 -148
- package/src/extension/crew-vibes/figures.ts +0 -22
- package/src/extension/crew-vibes/font-detect.ts +0 -71
- package/src/ui/dynamic-border.ts +0 -35
- package/src/ui/loaders.ts +0 -6
- package/src/ui/overlay-stack.ts +0 -148
package/src/ui/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`.
|
|
@@ -27,6 +33,7 @@ import * as fs from "node:fs";
|
|
|
27
33
|
import * as path from "node:path";
|
|
28
34
|
import { type KeyId, matchesKey } from "@earendil-works/pi-tui";
|
|
29
35
|
import { getCrewEnv } from "../config/env-vars.ts";
|
|
36
|
+
import { projectCrewRoot } from "../utils/paths.ts";
|
|
30
37
|
import { keyOf } from "./key-utils.ts";
|
|
31
38
|
|
|
32
39
|
export const DASHBOARD_KEYS = {
|
|
@@ -35,6 +42,8 @@ export const DASHBOARD_KEYS = {
|
|
|
35
42
|
help: ["?"],
|
|
36
43
|
root: {
|
|
37
44
|
summary: ["u"],
|
|
45
|
+
/** US-020: cancel the selected run (2-step confirm in the dashboard). */
|
|
46
|
+
cancel: ["x"],
|
|
38
47
|
artifacts: ["a"],
|
|
39
48
|
api: ["i"],
|
|
40
49
|
agents: ["d"],
|
|
@@ -44,7 +53,6 @@ export const DASHBOARD_KEYS = {
|
|
|
44
53
|
transcript: ["v"],
|
|
45
54
|
liveConversation: ["V"],
|
|
46
55
|
reload: ["r"],
|
|
47
|
-
progressToggle: ["p"],
|
|
48
56
|
browser: ["b"],
|
|
49
57
|
},
|
|
50
58
|
pane: {
|
|
@@ -106,8 +114,8 @@ export type DashboardKeyAction =
|
|
|
106
114
|
| "transcript"
|
|
107
115
|
| "live-conversation"
|
|
108
116
|
| "reload"
|
|
109
|
-
| "progressToggle"
|
|
110
117
|
| "browser"
|
|
118
|
+
| "cancel"
|
|
111
119
|
| "pane-agents"
|
|
112
120
|
| "pane-progress"
|
|
113
121
|
| "pane-mailbox"
|
|
@@ -152,11 +160,13 @@ export type DashboardKeyAction =
|
|
|
152
160
|
*
|
|
153
161
|
* NOTE: mailbox action keys A/N/C/P/X (ack/nudge/compose/preview/ackAll) are
|
|
154
162
|
* intentionally NOT dispatched for the mailbox pane by this table. They live
|
|
155
|
-
* in `DASHBOARD_KEYS.mailbox` for reservation
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
163
|
+
* in `DASHBOARD_KEYS.mailbox` for reservation and are resolved by the mailbox
|
|
164
|
+
* overlays' own input handling — since M2-1 through the sibling `overlay:*`
|
|
165
|
+
* keyspace (`OVERLAY_KEYS["mailbox-detail"]` / `OVERLAY_KEYS["mailbox-compose"]`),
|
|
166
|
+
* never by `dashboardActionForKey`. The `plan` group reuses uppercase "A"
|
|
167
|
+
* (approve) pane-scoped to "progress" — it never fires while the mailbox pane
|
|
168
|
+
* (or the mailbox-detail overlay) owns input, so `mailbox.ack` behavior is
|
|
169
|
+
* unchanged.
|
|
160
170
|
*/
|
|
161
171
|
const DEFAULT_BINDINGS: readonly KeyBinding[] = [
|
|
162
172
|
{ keys: DASHBOARD_KEYS.close, action: "close" },
|
|
@@ -245,12 +255,14 @@ const DEFAULT_BINDINGS: readonly KeyBinding[] = [
|
|
|
245
255
|
{ keys: DASHBOARD_KEYS.root.transcript, action: "transcript" },
|
|
246
256
|
{ keys: DASHBOARD_KEYS.root.liveConversation, action: "live-conversation" },
|
|
247
257
|
{ keys: DASHBOARD_KEYS.root.reload, action: "reload" },
|
|
248
|
-
{ keys: DASHBOARD_KEYS.root.progressToggle, action: "progressToggle" },
|
|
249
258
|
// Agents & Jobs browser (one-keypress overlay, mirrors live-conversation).
|
|
250
259
|
// Collision analysis: "b" is unbound everywhere else — root-unscoped is
|
|
251
260
|
// safe; inside the browser overlay itself "p" is free because overlays
|
|
252
261
|
// are mutually exclusive (see keybinding-map.ts header note).
|
|
253
262
|
{ keys: DASHBOARD_KEYS.root.browser, action: "browser" },
|
|
263
|
+
// US-020: x → cancel the selected run (unscoped; the schedules pane's X
|
|
264
|
+
// delete is pane-scoped uppercase — pass-1 exact match keeps them distinct).
|
|
265
|
+
{ keys: DASHBOARD_KEYS.root.cancel, action: "cancel" },
|
|
254
266
|
{ keys: DASHBOARD_KEYS.pane.agents, action: "pane-agents" },
|
|
255
267
|
{ keys: DASHBOARD_KEYS.pane.progress, action: "pane-progress" },
|
|
256
268
|
{ keys: DASHBOARD_KEYS.pane.mailbox, action: "pane-mailbox" },
|
|
@@ -289,6 +301,103 @@ const KEY_RESERVED = new Set<string>([
|
|
|
289
301
|
|
|
290
302
|
export { KEY_RESERVED };
|
|
291
303
|
|
|
304
|
+
// ─── Overlay keybindings (P1-9 / M2-1) ─────────────────────────────────────
|
|
305
|
+
//
|
|
306
|
+
// Overlays used to hardcode their own key chains — the SAME
|
|
307
|
+
// "↑/↓/Enter/Esc" concept re-implemented in every overlay file, none of them
|
|
308
|
+
// remappable. M2-1 centralises them into an `overlay:<name>:<action>`
|
|
309
|
+
// keyspace that rides the SAME override pipeline as the dashboard table
|
|
310
|
+
// above: `.crew/config.json` → `keybindings` and `PI_CREW_KEYBINDINGS` both
|
|
311
|
+
// accept `"overlay:<name>:<action>": ["<key>", …]`, a colliding override is
|
|
312
|
+
// reverted to its default, and the revert is reported by
|
|
313
|
+
// `getKeybindingOverrideWarnings()`.
|
|
314
|
+
//
|
|
315
|
+
// Overlay bindings are validated INDEPENDENTLY of dashboard bindings: the two
|
|
316
|
+
// namespaces are never live at the same time (the host hands input to exactly
|
|
317
|
+
// one component — an open overlay or the dashboard), so a shared key such as
|
|
318
|
+
// `q`/`escape`/`A` is not a real ambiguity. Within one overlay a shared key IS
|
|
319
|
+
// ambiguous and is treated as a collision.
|
|
320
|
+
|
|
321
|
+
export const OVERLAY_KEYS = {
|
|
322
|
+
"agent-picker": {
|
|
323
|
+
close: ["escape", "q"],
|
|
324
|
+
up: ["k", "up"],
|
|
325
|
+
down: ["j", "down"],
|
|
326
|
+
select: ["return"],
|
|
327
|
+
},
|
|
328
|
+
confirm: {
|
|
329
|
+
confirm: ["y", "Y"],
|
|
330
|
+
/** Enter is dual-role: the overlay resolves it with `defaultAction`. */
|
|
331
|
+
submit: ["return"],
|
|
332
|
+
cancel: ["n", "N", "escape", "q"],
|
|
333
|
+
},
|
|
334
|
+
"mailbox-detail": {
|
|
335
|
+
close: ["escape", "q"],
|
|
336
|
+
toggleSide: ["tab", "\t"],
|
|
337
|
+
up: ["k", "up"],
|
|
338
|
+
down: ["j", "down"],
|
|
339
|
+
toggleDetail: ["return"],
|
|
340
|
+
ack: ["A"],
|
|
341
|
+
nudge: ["N"],
|
|
342
|
+
compose: ["C"],
|
|
343
|
+
ackAll: ["X"],
|
|
344
|
+
},
|
|
345
|
+
"mailbox-compose": {
|
|
346
|
+
cancel: ["escape"],
|
|
347
|
+
preview: ["P"],
|
|
348
|
+
nextField: ["tab", "\t"],
|
|
349
|
+
space: [" "],
|
|
350
|
+
backspace: ["backspace"],
|
|
351
|
+
submit: ["return"],
|
|
352
|
+
},
|
|
353
|
+
} as const;
|
|
354
|
+
|
|
355
|
+
type OverlayDefs = typeof OVERLAY_KEYS;
|
|
356
|
+
|
|
357
|
+
/** Overlay namespaces that opt into the central keybinding map. */
|
|
358
|
+
export type OverlayName = keyof OverlayDefs;
|
|
359
|
+
|
|
360
|
+
/** Every action across every overlay (union). */
|
|
361
|
+
export type OverlayAction = { [N in OverlayName]: keyof OverlayDefs[N] & string }[OverlayName];
|
|
362
|
+
|
|
363
|
+
/** Actions of ONE overlay — the precise return type of `overlayActionForKey`. */
|
|
364
|
+
export type OverlayActionOf<N extends OverlayName> = keyof OverlayDefs[N] & string;
|
|
365
|
+
|
|
366
|
+
/** Override key as written in config/env: `overlay:<name>:<action>`. */
|
|
367
|
+
export type OverlayBindingKey = { [N in OverlayName]: `overlay:${N & string}:${keyof OverlayDefs[N] & string}` }[OverlayName];
|
|
368
|
+
|
|
369
|
+
/** A resolved overlay binding (one action of one overlay). */
|
|
370
|
+
export interface OverlayBinding {
|
|
371
|
+
readonly overlay: OverlayName;
|
|
372
|
+
readonly action: OverlayAction;
|
|
373
|
+
readonly keys: readonly string[];
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** Compose the config/env override key for one overlay action. */
|
|
377
|
+
function overlayBindingKey(overlay: OverlayName, action: OverlayAction): OverlayBindingKey {
|
|
378
|
+
return `overlay:${overlay}:${action}` as OverlayBindingKey;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// `Object.entries` widens the const-typed tables; this cast restores the exact
|
|
382
|
+
// shape (no `any`; the `OverlayBinding` annotation below still checks it).
|
|
383
|
+
type OverlayTableEntry = readonly [OverlayName, Readonly<Record<string, readonly string[]>>];
|
|
384
|
+
|
|
385
|
+
const OVERLAY_TABLE = Object.entries(OVERLAY_KEYS) as readonly OverlayTableEntry[];
|
|
386
|
+
|
|
387
|
+
/** Every valid `overlay:<name>:<action>` override key. */
|
|
388
|
+
const OVERLAY_BINDING_KEYS: ReadonlySet<string> = new Set(
|
|
389
|
+
OVERLAY_TABLE.flatMap(([overlay, actions]) =>
|
|
390
|
+
Object.keys(actions).map((action) => overlayBindingKey(overlay, action as OverlayAction)),
|
|
391
|
+
),
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
const DEFAULT_OVERLAY_BINDINGS: ReadonlyMap<OverlayName, readonly OverlayBinding[]> = new Map<OverlayName, readonly OverlayBinding[]>(
|
|
395
|
+
OVERLAY_TABLE.map(([overlay, actions]) => [
|
|
396
|
+
overlay,
|
|
397
|
+
Object.entries(actions).map(([action, keys]): OverlayBinding => ({ overlay, action: action as OverlayAction, keys })),
|
|
398
|
+
]),
|
|
399
|
+
);
|
|
400
|
+
|
|
292
401
|
// ─── Keybinding overrides (UI-2) ───────────────────────────────────────────
|
|
293
402
|
//
|
|
294
403
|
// The hardcoded DEFAULT_BINDINGS above can be overridden per-action via two
|
|
@@ -309,13 +418,17 @@ export { KEY_RESERVED };
|
|
|
309
418
|
// first-match-wins dispatch unambiguous — a shadowed override never silently
|
|
310
419
|
// changes behaviour.
|
|
311
420
|
|
|
312
|
-
/** Override map: action → replacement keys. `Partial` ⇒ only listed actions.
|
|
313
|
-
|
|
421
|
+
/** Override map: action → replacement keys. `Partial` ⇒ only listed actions.
|
|
422
|
+
* Keys are dashboard actions plus `overlay:<name>:<action>` binding keys. */
|
|
423
|
+
export type KeybindingOverride = Partial<Record<DashboardKeyAction | OverlayBindingKey, readonly string[]>>;
|
|
314
424
|
|
|
315
425
|
const KEYBINDINGS_ENV = "PI_CREW_KEYBINDINGS";
|
|
316
426
|
|
|
317
|
-
/** Every dispatched action is a valid override target. */
|
|
318
|
-
const VALID_OVERRIDE_ACTIONS: ReadonlySet<string> = new Set(
|
|
427
|
+
/** Every dispatched action + every `overlay:*` binding is a valid override target. */
|
|
428
|
+
export const VALID_OVERRIDE_ACTIONS: ReadonlySet<string> = new Set<string>([
|
|
429
|
+
...DEFAULT_BINDINGS.map((b) => b.action),
|
|
430
|
+
...OVERLAY_BINDING_KEYS,
|
|
431
|
+
]);
|
|
319
432
|
|
|
320
433
|
/** Coerce an unknown parsed value into a safe {@link KeybindingOverride}. */
|
|
321
434
|
function parseKeybindingOverride(raw: unknown): KeybindingOverride {
|
|
@@ -355,48 +468,100 @@ function paneScopesCompatible(a: PaneScope | undefined, b: PaneScope | undefined
|
|
|
355
468
|
|
|
356
469
|
interface EffectiveBindingsResult {
|
|
357
470
|
readonly bindings: readonly KeyBinding[];
|
|
358
|
-
/**
|
|
359
|
-
readonly
|
|
471
|
+
/** Resolved bindings per overlay (defaults + non-colliding overrides). */
|
|
472
|
+
readonly overlayBindings: ReadonlyMap<OverlayName, readonly OverlayBinding[]>;
|
|
473
|
+
/** Override targets whose override was rejected due to a collision. */
|
|
474
|
+
readonly reverted: readonly string[];
|
|
360
475
|
}
|
|
361
476
|
|
|
362
477
|
/**
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
*
|
|
478
|
+
* Does `candidate` share a key with any `other` binding that could be live at
|
|
479
|
+
* the same time? Shared by the dashboard and the overlay namespaces so the
|
|
480
|
+
* collision rule (and its revert semantics) has ONE implementation.
|
|
481
|
+
*/
|
|
482
|
+
function collidesWithOthers<B extends { readonly keys: readonly string[]; readonly action: string }>(
|
|
483
|
+
candidate: B,
|
|
484
|
+
others: readonly B[],
|
|
485
|
+
compatible: (a: B, b: B) => boolean,
|
|
486
|
+
): boolean {
|
|
487
|
+
for (const other of others) {
|
|
488
|
+
if (other.action === candidate.action) continue;
|
|
489
|
+
if (!compatible(candidate, other)) continue;
|
|
490
|
+
if (candidate.keys.some((k) => other.keys.includes(k))) return true;
|
|
491
|
+
}
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Apply `overrides` onto {@link DEFAULT_BINDINGS} + {@link DEFAULT_OVERLAY_BINDINGS}
|
|
497
|
+
* (replace keys per target, preserving each binding's pane scope / overlay) and
|
|
498
|
+
* detect collisions. A colliding override is reverted to its default so the
|
|
499
|
+
* dispatch stays unambiguous. Dashboard and overlay namespaces are validated
|
|
500
|
+
* separately (they are never live simultaneously).
|
|
366
501
|
*/
|
|
367
502
|
function computeEffectiveBindings(overrides: KeybindingOverride): EffectiveBindingsResult {
|
|
503
|
+
// ── dashboard namespace (semantics unchanged since UI-2) ──
|
|
368
504
|
const applied = new Map<DashboardKeyAction, KeyBinding>();
|
|
369
505
|
for (const def of DEFAULT_BINDINGS) {
|
|
370
506
|
const ov = overrides[def.action];
|
|
371
507
|
applied.set(def.action, ov && ov.length > 0 ? { keys: [...ov], action: def.action, pane: def.pane } : def);
|
|
372
508
|
}
|
|
373
509
|
const effective = [...applied.values()];
|
|
374
|
-
const reverted = new Set<
|
|
510
|
+
const reverted = new Set<string>();
|
|
375
511
|
for (const def of DEFAULT_BINDINGS) {
|
|
376
512
|
const ov = overrides[def.action];
|
|
377
513
|
if (!ov || ov.length === 0) continue; // not overridden
|
|
378
514
|
const ob = applied.get(def.action);
|
|
379
515
|
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
|
-
}
|
|
516
|
+
if (collidesWithOthers(ob, effective, (a, b) => paneScopesCompatible(a.pane, b.pane))) reverted.add(def.action);
|
|
388
517
|
}
|
|
389
518
|
const bindings =
|
|
390
519
|
reverted.size > 0
|
|
391
520
|
? effective.map((b) => (reverted.has(b.action) ? (DEFAULT_BINDINGS.find((d) => d.action === b.action) ?? b) : b))
|
|
392
521
|
: effective;
|
|
393
|
-
|
|
522
|
+
|
|
523
|
+
// ── overlay namespace (M2-1) ──
|
|
524
|
+
const overlayBindings = new Map<OverlayName, readonly OverlayBinding[]>();
|
|
525
|
+
for (const [overlay, defaults] of DEFAULT_OVERLAY_BINDINGS) {
|
|
526
|
+
const perOverlay: OverlayBinding[] = defaults.map((def) => {
|
|
527
|
+
const ov = overrides[overlayBindingKey(overlay, def.action)];
|
|
528
|
+
return ov && ov.length > 0 ? { overlay, action: def.action, keys: [...ov] } : def;
|
|
529
|
+
});
|
|
530
|
+
for (const def of defaults) {
|
|
531
|
+
const key = overlayBindingKey(overlay, def.action);
|
|
532
|
+
const ov = overrides[key];
|
|
533
|
+
if (!ov || ov.length === 0) continue; // not overridden
|
|
534
|
+
const ob = perOverlay.find((b) => b.action === def.action);
|
|
535
|
+
if (!ob) continue;
|
|
536
|
+
// Same overlay ⇒ same input lineage ⇒ any shared key is ambiguous.
|
|
537
|
+
if (collidesWithOthers(ob, perOverlay, () => true)) reverted.add(key);
|
|
538
|
+
}
|
|
539
|
+
if (reverted.size === 0) {
|
|
540
|
+
overlayBindings.set(overlay, perOverlay);
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
overlayBindings.set(
|
|
544
|
+
overlay,
|
|
545
|
+
perOverlay.map((b) =>
|
|
546
|
+
reverted.has(overlayBindingKey(overlay, b.action)) ? (defaults.find((d) => d.action === b.action) ?? b) : b,
|
|
547
|
+
),
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return { bindings, overlayBindings, reverted: [...reverted] };
|
|
394
552
|
}
|
|
395
553
|
|
|
396
|
-
/**
|
|
554
|
+
/**
|
|
555
|
+
* Read the `keybindings` section from the project config.
|
|
556
|
+
*
|
|
557
|
+
* RR-020 Fix 4: resolve via `projectCrewRoot(cwd)` instead of a literal
|
|
558
|
+
* `<cwd>/.crew` — the config lives in whichever layout the project actually
|
|
559
|
+
* uses (`.crew/` or `.pi/teams/`), so a `.pi/teams` project's keybinding
|
|
560
|
+
* overrides are no longer silently ignored.
|
|
561
|
+
*/
|
|
397
562
|
function readConfigKeybindings(cwd: string): KeybindingOverride {
|
|
398
563
|
try {
|
|
399
|
-
const raw: unknown = JSON.parse(fs.readFileSync(path.join(cwd, "
|
|
564
|
+
const raw: unknown = JSON.parse(fs.readFileSync(path.join(projectCrewRoot(cwd), "config.json"), "utf-8"));
|
|
400
565
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
401
566
|
return parseKeybindingOverride((raw as Record<string, unknown>).keybindings);
|
|
402
567
|
} catch {
|
|
@@ -417,7 +582,8 @@ function readEnvKeybindings(): KeybindingOverride {
|
|
|
417
582
|
|
|
418
583
|
function configKeybindingsMtime(cwd: string): number | undefined {
|
|
419
584
|
try {
|
|
420
|
-
|
|
585
|
+
// RR-020 Fix 4: same layout-resolved config path as readConfigKeybindings.
|
|
586
|
+
return fs.statSync(path.join(projectCrewRoot(cwd), "config.json")).mtimeMs;
|
|
421
587
|
} catch {
|
|
422
588
|
return undefined;
|
|
423
589
|
}
|
|
@@ -428,15 +594,17 @@ interface EffectiveCache {
|
|
|
428
594
|
readonly configMtime: number | undefined;
|
|
429
595
|
readonly cwd: string;
|
|
430
596
|
readonly bindings: readonly KeyBinding[];
|
|
597
|
+
readonly overlayBindings: ReadonlyMap<OverlayName, readonly OverlayBinding[]>;
|
|
431
598
|
}
|
|
432
599
|
|
|
433
600
|
let _effectiveCache: EffectiveCache | null = null;
|
|
434
601
|
let _overrideWarnings: readonly string[] = [];
|
|
435
602
|
|
|
436
603
|
/**
|
|
437
|
-
* Resolve the effective dispatch
|
|
438
|
-
*
|
|
439
|
-
*
|
|
604
|
+
* Resolve the effective dispatch tables: {@link DEFAULT_BINDINGS} and
|
|
605
|
+
* {@link DEFAULT_OVERLAY_BINDINGS} with config + env overrides applied (env
|
|
606
|
+
* wins per target). Memoised on (env value, config mtime, cwd); a single
|
|
607
|
+
* `statSync` per call detects on-disk config changes.
|
|
440
608
|
*/
|
|
441
609
|
function getEffectiveBindings(cwd: string = process.cwd()): readonly KeyBinding[] {
|
|
442
610
|
const envRaw = getCrewEnv(KEYBINDINGS_ENV);
|
|
@@ -445,12 +613,18 @@ function getEffectiveBindings(cwd: string = process.cwd()): readonly KeyBinding[
|
|
|
445
613
|
return _effectiveCache.bindings;
|
|
446
614
|
}
|
|
447
615
|
const merged: KeybindingOverride = { ...readConfigKeybindings(cwd), ...readEnvKeybindings() };
|
|
448
|
-
const { bindings, reverted } = computeEffectiveBindings(merged);
|
|
616
|
+
const { bindings, overlayBindings, reverted } = computeEffectiveBindings(merged);
|
|
449
617
|
_overrideWarnings = reverted.map((a) => `keybinding override for '${a}' collides with another binding — reverting to default`);
|
|
450
|
-
_effectiveCache = { env: envRaw, configMtime, cwd, bindings };
|
|
618
|
+
_effectiveCache = { env: envRaw, configMtime, cwd, bindings, overlayBindings };
|
|
451
619
|
return bindings;
|
|
452
620
|
}
|
|
453
621
|
|
|
622
|
+
/** Resolved overlay bindings for one overlay (defaults + non-colliding overrides). */
|
|
623
|
+
function getEffectiveOverlayBindings(overlay: OverlayName): readonly OverlayBinding[] {
|
|
624
|
+
getEffectiveBindings(); // ensures the memo is warm + warnings are populated
|
|
625
|
+
return _effectiveCache?.overlayBindings.get(overlay) ?? DEFAULT_OVERLAY_BINDINGS.get(overlay) ?? [];
|
|
626
|
+
}
|
|
627
|
+
|
|
454
628
|
/** Warnings from the most recent override resolution (e.g. collisions). */
|
|
455
629
|
export function getKeybindingOverrideWarnings(): readonly string[] {
|
|
456
630
|
// Ensure a resolution has run so warnings are populated.
|
|
@@ -514,3 +688,45 @@ export function dashboardActionForKey(data: string, activePane?: ActivePane): Da
|
|
|
514
688
|
}
|
|
515
689
|
return undefined;
|
|
516
690
|
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Resolve a raw input `data` string to an OVERLAY action (M2-1 / P1-9).
|
|
694
|
+
*
|
|
695
|
+
* Mirror image of {@link dashboardActionForKey} over the overlay keyspace:
|
|
696
|
+
* same two-pass dispatch (case-sensitive exact match first, then the
|
|
697
|
+
* terminal-aware `matchesKey` normalization), same override pipeline
|
|
698
|
+
* (`.crew/config.json` → `keybindings["overlay:<name>:<action>"]`, then the
|
|
699
|
+
* `PI_CREW_KEYBINDINGS` env var, collision → revert to default).
|
|
700
|
+
*
|
|
701
|
+
* With no override configured the result is identical to the key chains the
|
|
702
|
+
* overlays used to hardcode (asserted by
|
|
703
|
+
* `test/unit/ui/overlay-keybindings.test.ts`).
|
|
704
|
+
*
|
|
705
|
+
* @param overlay Overlay namespace, e.g. `"confirm"`.
|
|
706
|
+
* @param data Raw key input (single char or escape sequence).
|
|
707
|
+
*/
|
|
708
|
+
export function overlayActionForKey<N extends OverlayName>(overlay: N, data: string): OverlayActionOf<N> | undefined {
|
|
709
|
+
const bindings = getEffectiveOverlayBindings(overlay);
|
|
710
|
+
// Pass 1 — exact, case-sensitive match (keeps "y"/"Y" and "A"/"a" distinct).
|
|
711
|
+
for (const binding of bindings) {
|
|
712
|
+
if (binding.keys.includes(data)) return binding.action as OverlayActionOf<N>;
|
|
713
|
+
}
|
|
714
|
+
// Pass 2 — escape-sequence / canonical KeyId normalisation.
|
|
715
|
+
//
|
|
716
|
+
// Single-character candidates are deliberately EXCLUDED here: pi-tui's
|
|
717
|
+
// `matchesKey()` is case-INSENSITIVE for plain ASCII
|
|
718
|
+
// (`matchesKey("a", "A") === true`), which would collapse the deliberate
|
|
719
|
+
// case distinctions this keyspace relies on ("A"=ack vs lowercase "a"=free
|
|
720
|
+
// text in mailbox-compose; "y"/"Y" in confirm). Pass 1 already matched
|
|
721
|
+
// single chars exactly and case-sensitively, mirroring the pre-M2-1 chains
|
|
722
|
+
// (`data === "A"`, `data === "P"`, …).
|
|
723
|
+
const key = keyOf(data);
|
|
724
|
+
for (const binding of bindings) {
|
|
725
|
+
for (const candidate of binding.keys) {
|
|
726
|
+
if (candidate.length === 1) continue;
|
|
727
|
+
if (key === candidate) return binding.action as OverlayActionOf<N>;
|
|
728
|
+
if (matchesKey(data, candidate as KeyId)) return binding.action as OverlayActionOf<N>;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
return undefined;
|
|
732
|
+
}
|