@crouton-kit/crouter 0.3.68 → 0.3.78

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.
Files changed (176) hide show
  1. package/README.md +1 -1
  2. package/dist/builtin-memory/00-runtime-base.md +3 -10
  3. package/dist/builtin-memory/04-base-worker.md +18 -0
  4. package/dist/builtin-memory/04-orchestration-kernel.md +1 -1
  5. package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +13 -34
  6. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts +639 -741
  7. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +241 -58
  8. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/statusline.ts +2 -9
  9. package/dist/builtin-pi-packages/pi-crtr-extensions/lib/subscription-state.ts +365 -94
  10. package/dist/builtin-pi-packages/pi-crtr-extensions/package-lock.json +2 -2
  11. package/dist/builtin-pi-packages/pi-mode-switch/README.md +11 -39
  12. package/dist/builtin-pi-packages/pi-mode-switch/extensions/index.ts +20 -15
  13. package/dist/builtin-pi-packages/pi-mode-switch/package.json +1 -1
  14. package/dist/builtin-views/canvas/tui.mjs +8 -9
  15. package/dist/builtin-views/chat/tui.mjs +12 -12
  16. package/dist/builtin-views/git-pr/tui.mjs +7 -8
  17. package/dist/builtin-views/inbox/tui.mjs +27 -41
  18. package/dist/builtin-views/linkedin/tui.mjs +23 -37
  19. package/dist/builtin-views/prompt-review/tui.mjs +11 -11
  20. package/dist/builtin-views/settings/tui.mjs +11 -11
  21. package/dist/builtin-views/workspace-sidebar/tui.mjs +8 -9
  22. package/dist/cli.d.ts +1 -1
  23. package/dist/cli.js +1 -0
  24. package/dist/clients/attach/__tests__/attach-chrome-remote.test.js +11 -7
  25. package/dist/clients/attach/__tests__/crtr-output-render.test.js +1 -1
  26. package/dist/clients/attach/__tests__/mermaid-render.test.d.ts +1 -0
  27. package/dist/clients/attach/__tests__/mermaid-render.test.js +20 -0
  28. package/dist/clients/attach/__tests__/oauth-dialog-lifecycle.test.d.ts +1 -0
  29. package/dist/clients/attach/__tests__/oauth-dialog-lifecycle.test.js +110 -0
  30. package/dist/clients/attach/attach-cmd.d.ts +1 -1
  31. package/dist/clients/attach/attach-cmd.js +733 -730
  32. package/dist/clients/attach/auth-pickers.d.ts +12 -0
  33. package/dist/clients/attach/auth-pickers.js +11 -0
  34. package/dist/clients/attach/canvas-panels.js +2 -3
  35. package/dist/clients/attach/chat-view.d.ts +7 -8
  36. package/dist/clients/attach/chat-view.js +119 -79
  37. package/dist/clients/attach/edit-diff-render.d.ts +6 -0
  38. package/dist/clients/attach/edit-diff-render.js +263 -0
  39. package/dist/clients/attach/input-controller.d.ts +28 -2
  40. package/dist/clients/attach/input-controller.js +38 -5
  41. package/dist/clients/attach/mermaid-render.js +5 -2
  42. package/dist/clients/attach/pickers.d.ts +8 -7
  43. package/dist/clients/attach/pickers.js +13 -17
  44. package/dist/clients/attach/slash-commands.d.ts +9 -0
  45. package/dist/clients/attach/slash-commands.js +128 -7
  46. package/dist/clients/attach/titled-editor.d.ts +12 -1
  47. package/dist/clients/attach/titled-editor.js +103 -8
  48. package/dist/commands/canvas-browse.js +2 -2
  49. package/dist/commands/memory/lint.js +39 -5
  50. package/dist/commands/memory/write.js +1 -0
  51. package/dist/commands/node.js +12 -5
  52. package/dist/commands/profile/new.js +30 -5
  53. package/dist/commands/surface-tmux-spread.js +1 -3
  54. package/dist/commands/sys/__tests__/config-keybindings.test.d.ts +1 -0
  55. package/dist/commands/sys/__tests__/config-keybindings.test.js +55 -0
  56. package/dist/commands/sys/__tests__/config-model-ladders.test.d.ts +1 -0
  57. package/dist/commands/sys/__tests__/config-model-ladders.test.js +121 -0
  58. package/dist/commands/sys/config.js +18 -21
  59. package/dist/commands/sys/setup-core.d.ts +12 -1
  60. package/dist/commands/sys/setup-core.js +25 -3
  61. package/dist/commands/sys/setup.js +166 -20
  62. package/dist/core/__tests__/base-worker-prompt.test.d.ts +1 -0
  63. package/dist/core/__tests__/base-worker-prompt.test.js +24 -0
  64. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +232 -1
  65. package/dist/core/__tests__/fault-classifier.test.js +15 -0
  66. package/dist/core/__tests__/full/broker-dialogs.test.js +7 -2
  67. package/dist/core/__tests__/full/broker-pane-resolution.test.js +9 -6
  68. package/dist/core/__tests__/full/cascade-close.test.js +5 -2
  69. package/dist/core/__tests__/full/dead-pane-regression.test.js +6 -3
  70. package/dist/core/__tests__/full/detach-focus.test.js +10 -5
  71. package/dist/core/__tests__/full/human-new-window-regression.test.js +6 -3
  72. package/dist/core/__tests__/full/review-render-pane-regression.test.js +2 -2
  73. package/dist/core/__tests__/helpers/harness.d.ts +1 -0
  74. package/dist/core/__tests__/helpers/harness.js +28 -4
  75. package/dist/core/__tests__/host-teardown-process-group.test.js +15 -4
  76. package/dist/core/__tests__/live-mutation-verbs.test.js +5 -4
  77. package/dist/core/__tests__/review-model-floor.test.js +1 -1
  78. package/dist/core/__tests__/stream-watchdog.test.d.ts +1 -0
  79. package/dist/core/__tests__/stream-watchdog.test.js +70 -0
  80. package/dist/core/__tests__/tmux-surface.test.js +72 -0
  81. package/dist/core/canvas/__tests__/remote-event-stream.test.js +11 -9
  82. package/dist/core/canvas/__tests__/render-remote.test.js +7 -4
  83. package/dist/core/canvas/browse/__tests__/model.test.js +23 -5
  84. package/dist/core/canvas/browse/__tests__/rebuild-coalescer.test.js +30 -13
  85. package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
  86. package/dist/core/canvas/browse/app.js +164 -5
  87. package/dist/core/canvas/browse/model.d.ts +10 -6
  88. package/dist/core/canvas/browse/model.js +20 -12
  89. package/dist/core/canvas/browse/pins.d.ts +4 -0
  90. package/dist/core/canvas/browse/pins.js +29 -0
  91. package/dist/core/canvas/browse/render.d.ts +13 -1
  92. package/dist/core/canvas/browse/render.js +72 -15
  93. package/dist/core/canvas/nav-model.js +5 -11
  94. package/dist/core/canvas/paths.d.ts +5 -0
  95. package/dist/core/canvas/paths.js +7 -0
  96. package/dist/core/canvas/pid.d.ts +14 -10
  97. package/dist/core/canvas/pid.js +14 -10
  98. package/dist/core/config.d.ts +53 -2
  99. package/dist/core/config.js +228 -110
  100. package/dist/core/fault-classifier.js +1 -1
  101. package/dist/core/fs-utils.d.ts +1 -0
  102. package/dist/core/fs-utils.js +15 -1
  103. package/dist/core/keybindings/__tests__/resolve.test.d.ts +1 -0
  104. package/dist/core/keybindings/__tests__/resolve.test.js +224 -0
  105. package/dist/core/keybindings/catalog.d.ts +14 -0
  106. package/dist/core/keybindings/catalog.js +256 -0
  107. package/dist/core/keybindings/index.d.ts +5 -0
  108. package/dist/core/keybindings/index.js +4 -0
  109. package/dist/core/keybindings/match.d.ts +29 -0
  110. package/dist/core/keybindings/match.js +99 -0
  111. package/dist/core/keybindings/persistence.d.ts +14 -0
  112. package/dist/core/keybindings/persistence.js +31 -0
  113. package/dist/core/keybindings/resolve.d.ts +14 -0
  114. package/dist/core/keybindings/resolve.js +305 -0
  115. package/dist/core/keybindings/types.d.ts +37 -0
  116. package/dist/core/keybindings/types.js +1 -0
  117. package/dist/core/memory-resolver.d.ts +1 -1
  118. package/dist/core/memory-resolver.js +10 -3
  119. package/dist/core/predicate.d.ts +5 -3
  120. package/dist/core/predicate.js +5 -3
  121. package/dist/core/runtime/broker-protocol.d.ts +33 -5
  122. package/dist/core/runtime/broker.js +165 -14
  123. package/dist/core/runtime/launch.d.ts +39 -6
  124. package/dist/core/runtime/launch.js +79 -21
  125. package/dist/core/runtime/naming.js +3 -3
  126. package/dist/core/runtime/pi-cli.d.ts +6 -0
  127. package/dist/core/runtime/pi-cli.js +16 -2
  128. package/dist/core/runtime/placement.d.ts +2 -2
  129. package/dist/core/runtime/placement.js +4 -1
  130. package/dist/core/runtime/promote.js +4 -0
  131. package/dist/core/runtime/recap.d.ts +3 -3
  132. package/dist/core/runtime/recap.js +47 -44
  133. package/dist/core/runtime/recycle.js +6 -1
  134. package/dist/core/runtime/reset.js +5 -0
  135. package/dist/core/runtime/revive.js +28 -18
  136. package/dist/core/runtime/session-list-cache.d.ts +23 -0
  137. package/dist/core/runtime/session-list-cache.js +270 -0
  138. package/dist/core/runtime/spawn.js +51 -32
  139. package/dist/core/runtime/stream-watchdog.d.ts +26 -0
  140. package/dist/core/runtime/stream-watchdog.js +75 -0
  141. package/dist/core/runtime/tmux-chrome.d.ts +1 -1
  142. package/dist/core/runtime/tmux-chrome.js +2 -2
  143. package/dist/core/runtime/tmux.d.ts +18 -15
  144. package/dist/core/runtime/tmux.js +216 -110
  145. package/dist/core/tui/__tests__/host-keybindings.test.d.ts +1 -0
  146. package/dist/core/tui/__tests__/host-keybindings.test.js +112 -0
  147. package/dist/core/tui/host.d.ts +30 -1
  148. package/dist/core/tui/host.js +148 -34
  149. package/dist/core/view/__tests__/transport-remote.test.js +9 -6
  150. package/dist/core/view/contract.d.ts +17 -3
  151. package/dist/daemon/crtrd-cli.d.ts +1 -1
  152. package/dist/daemon/crtrd-cli.js +1 -0
  153. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +41 -0
  154. package/dist/pi-extensions/__tests__/canvas-stophook-context-nudge.test.d.ts +1 -0
  155. package/dist/pi-extensions/__tests__/canvas-stophook-context-nudge.test.js +121 -0
  156. package/dist/pi-extensions/canvas-inbox-watcher.d.ts +1 -1
  157. package/dist/pi-extensions/canvas-inbox-watcher.js +203 -25
  158. package/dist/pi-extensions/canvas-recap.d.ts +4 -0
  159. package/dist/pi-extensions/canvas-recap.js +38 -37
  160. package/dist/pi-extensions/canvas-stophook.d.ts +11 -0
  161. package/dist/pi-extensions/canvas-stophook.js +33 -4
  162. package/dist/suppress-experimental-warnings.d.ts +1 -0
  163. package/dist/suppress-experimental-warnings.js +15 -0
  164. package/dist/types.d.ts +3 -13
  165. package/dist/types.js +12 -36
  166. package/dist/web-client/assets/{index-CnF5r8ky.js → index--md2ylfi.js} +18 -17
  167. package/dist/web-client/assets/index-BUdm9s9s.css +2 -0
  168. package/dist/web-client/index.html +3 -3
  169. package/dist/web-client/sw.js +1 -1
  170. package/package.json +11 -8
  171. package/scripts/postinstall.mjs +1 -1
  172. package/dist/pi-extensions/canvas-nav.d.ts +0 -43
  173. package/dist/pi-extensions/canvas-nav.js +0 -640
  174. package/dist/pi-extensions/widget-order-bus.d.ts +0 -6
  175. package/dist/pi-extensions/widget-order-bus.js +0 -34
  176. package/dist/web-client/assets/index-BnmSLNLa.css +0 -2
@@ -0,0 +1,99 @@
1
+ import { normalizeStroke } from './resolve.js';
2
+ const KEY_LABELS = {
3
+ enter: 'Enter',
4
+ escape: 'Esc',
5
+ tab: 'Tab',
6
+ backspace: 'Backspace',
7
+ delete: 'Delete',
8
+ space: 'Space',
9
+ up: 'Up',
10
+ down: 'Down',
11
+ left: 'Left',
12
+ right: 'Right',
13
+ pageup: 'PageUp',
14
+ pagedown: 'PageDown',
15
+ home: 'Home',
16
+ end: 'End',
17
+ };
18
+ export function formatStroke(stroke) {
19
+ let key;
20
+ let modifiers;
21
+ if (stroke === '+') {
22
+ key = '+';
23
+ modifiers = [];
24
+ }
25
+ else if (stroke.endsWith('++')) {
26
+ key = '+';
27
+ modifiers = stroke.slice(0, -2).split('+');
28
+ }
29
+ else {
30
+ const parts = stroke.split('+');
31
+ key = parts.pop();
32
+ modifiers = parts;
33
+ }
34
+ const labels = modifiers.map((modifier) => modifier[0].toUpperCase() + modifier.slice(1));
35
+ labels.push(KEY_LABELS[key] ?? (key.length === 1 ? key.toUpperCase() : key));
36
+ return labels.join('+');
37
+ }
38
+ export function formatGesture(gesture) {
39
+ return gesture.split(' > ').map(formatStroke).join(' then ');
40
+ }
41
+ export function formatBinding(bindingSet, id) {
42
+ const gestures = bindingSet.gestures(id);
43
+ return gestures.length === 0 ? 'Disabled' : gestures.map(formatGesture).join(' / ');
44
+ }
45
+ export function canonicalTerminalStroke(raw) {
46
+ let key = null;
47
+ if (raw.upArrow)
48
+ key = 'up';
49
+ else if (raw.downArrow)
50
+ key = 'down';
51
+ else if (raw.leftArrow)
52
+ key = 'left';
53
+ else if (raw.rightArrow)
54
+ key = 'right';
55
+ else if (raw.return)
56
+ key = 'enter';
57
+ else if (raw.escape)
58
+ key = 'escape';
59
+ else if (raw.shiftTab)
60
+ return 'shift+tab';
61
+ else if (raw.tab)
62
+ key = 'tab';
63
+ else if (raw.backspace)
64
+ key = 'backspace';
65
+ else if (raw.delete)
66
+ key = 'delete';
67
+ else if (raw.pageUp)
68
+ key = 'pageup';
69
+ else if (raw.pageDown)
70
+ key = 'pagedown';
71
+ else if (raw.home)
72
+ key = 'home';
73
+ else if (raw.end)
74
+ key = 'end';
75
+ else if (raw.input?.length === 1)
76
+ key = raw.input === ' ' ? 'space' : raw.input;
77
+ if (key === null)
78
+ return null;
79
+ const modifiers = [];
80
+ if (raw.ctrl)
81
+ modifiers.push('ctrl');
82
+ if (raw.meta)
83
+ modifiers.push('alt');
84
+ if (raw.shift)
85
+ modifiers.push('shift');
86
+ try {
87
+ return normalizeStroke([...modifiers, key].join('+'));
88
+ }
89
+ catch {
90
+ return null;
91
+ }
92
+ }
93
+ export function matchesTerminalInput(bindingSet, id, raw) {
94
+ const stroke = canonicalTerminalStroke(raw);
95
+ return stroke !== null && bindingSet.gestures(id).includes(stroke);
96
+ }
97
+ export function matchesPiTuiInput(bindingSet, id, input, matchesKey) {
98
+ return bindingSet.gestures(id).some((gesture) => !gesture.includes(' > ') && matchesKey(input, gesture));
99
+ }
@@ -0,0 +1,14 @@
1
+ import type { BindingId } from './catalog.js';
2
+ import type { SparseKeybindingOverrides } from './types.js';
3
+ export declare class KeybindingConcurrentEditError extends Error {
4
+ readonly latestKeybindings: unknown;
5
+ constructor(latestKeybindings: unknown);
6
+ }
7
+ export interface PersistKeybindingsOptions {
8
+ readonly expectedKeybindings?: unknown;
9
+ }
10
+ export interface PersistedKeybindings {
11
+ readonly path: string;
12
+ readonly overrides: SparseKeybindingOverrides<BindingId>;
13
+ }
14
+ export declare function persistUserKeybindings(prospectiveOverrides: unknown, options?: PersistKeybindingsOptions): PersistedKeybindings;
@@ -0,0 +1,31 @@
1
+ import { isDeepStrictEqual } from 'node:util';
2
+ import { configPath, updateRawConfigAtomically } from '../config.js';
3
+ import { normalizeSparseOverrides } from './resolve.js';
4
+ export class KeybindingConcurrentEditError extends Error {
5
+ latestKeybindings;
6
+ constructor(latestKeybindings) {
7
+ super('keybindings changed on disk; reload before saving');
8
+ this.name = 'KeybindingConcurrentEditError';
9
+ this.latestKeybindings = latestKeybindings;
10
+ }
11
+ }
12
+ export function persistUserKeybindings(prospectiveOverrides, options = {}) {
13
+ const overrides = normalizeSparseOverrides(prospectiveOverrides);
14
+ const path = configPath('user');
15
+ if (path === null)
16
+ throw new Error('user config path is unavailable');
17
+ updateRawConfigAtomically('user', (latest) => {
18
+ if (Object.hasOwn(options, 'expectedKeybindings')
19
+ && !isDeepStrictEqual(latest['keybindings'], options.expectedKeybindings)) {
20
+ throw new KeybindingConcurrentEditError(latest['keybindings']);
21
+ }
22
+ const next = { ...latest };
23
+ delete next['canvasNav'];
24
+ if (Object.keys(overrides).length === 0)
25
+ delete next['keybindings'];
26
+ else
27
+ next['keybindings'] = overrides;
28
+ return next;
29
+ });
30
+ return Object.freeze({ path, overrides: Object.freeze({ ...overrides }) });
31
+ }
@@ -0,0 +1,14 @@
1
+ import type { BindingId } from './catalog.js';
2
+ import type { BindingDefinition, BindingResolution, KeybindingDiagnostic, SparseKeybindingOverrides } from './types.js';
3
+ export declare class GestureSyntaxError extends Error {
4
+ constructor(message: string);
5
+ }
6
+ export declare function normalizeStroke(raw: string): string;
7
+ export declare function normalizeGesture(raw: string, definition?: Pick<BindingDefinition, 'inputKind'>): string;
8
+ export declare function resolveKeybindings(rawOverrides?: unknown): BindingResolution<BindingId>;
9
+ export declare function resolveUserKeybindings(): BindingResolution<BindingId>;
10
+ export declare function normalizeSparseOverrides(rawOverrides: unknown): SparseKeybindingOverrides<BindingId>;
11
+ export declare class KeybindingValidationError extends Error {
12
+ readonly diagnostics: readonly KeybindingDiagnostic[];
13
+ constructor(diagnostics: readonly KeybindingDiagnostic[]);
14
+ }
@@ -0,0 +1,305 @@
1
+ import { isDeepStrictEqual } from 'node:util';
2
+ import { BINDING_CATALOG, BINDING_IDS, SAFETY_REQUIREMENTS } from './catalog.js';
3
+ import { readRawScopeConfig } from '../config.js';
4
+ const MODIFIER_ORDER = ['ctrl', 'alt', 'shift'];
5
+ const INPUT_MODIFIERS = new Set(['ctrl', 'alt', 'shift', 'meta']);
6
+ const NAMED_KEYS = new Set([
7
+ 'enter', 'escape', 'tab', 'backspace', 'delete', 'space', 'up', 'down',
8
+ 'left', 'right', 'pageup', 'pagedown', 'home', 'end',
9
+ ]);
10
+ const KEY_ALIASES = { return: 'enter', esc: 'escape' };
11
+ export class GestureSyntaxError extends Error {
12
+ constructor(message) {
13
+ super(message);
14
+ this.name = 'GestureSyntaxError';
15
+ }
16
+ }
17
+ function tmuxNotation(value) {
18
+ const match = value.match(/^((?:[CMS]-)+)(.+)$/i);
19
+ if (match === null)
20
+ return value;
21
+ const modifiers = match[1].split('-').filter(Boolean).map((token) => {
22
+ if (token.toUpperCase() === 'C')
23
+ return 'ctrl';
24
+ if (token.toUpperCase() === 'M')
25
+ return 'alt';
26
+ return 'shift';
27
+ });
28
+ return `${modifiers.join('+')}+${match[2]}`;
29
+ }
30
+ export function normalizeStroke(raw) {
31
+ const value = tmuxNotation(raw.trim());
32
+ if (value === '')
33
+ throw new GestureSyntaxError('a stroke cannot be empty');
34
+ let rawKey;
35
+ let modifierParts;
36
+ if (value === '+') {
37
+ rawKey = '+';
38
+ modifierParts = [];
39
+ }
40
+ else if (value.endsWith('++')) {
41
+ rawKey = '+';
42
+ modifierParts = value.slice(0, -2).split('+');
43
+ }
44
+ else {
45
+ const parts = value.split('+');
46
+ if (parts.some((part) => part === ''))
47
+ throw new GestureSyntaxError(`invalid stroke "${raw}"`);
48
+ rawKey = parts.pop();
49
+ modifierParts = parts;
50
+ }
51
+ const inputModifiers = modifierParts.map((part) => part.toLowerCase());
52
+ if (inputModifiers.some((modifier) => !INPUT_MODIFIERS.has(modifier))) {
53
+ throw new GestureSyntaxError(`unsupported modifier in "${raw}"`);
54
+ }
55
+ const modifiers = inputModifiers.map((modifier) => modifier === 'meta' ? 'alt' : modifier);
56
+ if (new Set(modifiers).size !== modifiers.length) {
57
+ throw new GestureSyntaxError(`duplicate modifier in "${raw}"`);
58
+ }
59
+ let key = KEY_ALIASES[rawKey.toLowerCase()] ?? rawKey;
60
+ if (key.length === 1 && key >= 'A' && key <= 'Z') {
61
+ key = key.toLowerCase();
62
+ if (!modifiers.includes('shift'))
63
+ modifiers.push('shift');
64
+ }
65
+ else if (key.length !== 1) {
66
+ key = key.toLowerCase();
67
+ }
68
+ if (!(key.length === 1 && key.charCodeAt(0) >= 32 && key.charCodeAt(0) <= 126) && !NAMED_KEYS.has(key)) {
69
+ throw new GestureSyntaxError(`unsupported key "${rawKey}"`);
70
+ }
71
+ const ordered = MODIFIER_ORDER.filter((modifier) => modifiers.includes(modifier));
72
+ return [...ordered, key].join('+');
73
+ }
74
+ export function normalizeGesture(raw, definition) {
75
+ const parts = raw.trim().split(/\s+>\s+/);
76
+ if (parts.length > 2)
77
+ throw new GestureSyntaxError('a gesture may contain at most two strokes');
78
+ const normalized = parts.map(normalizeStroke);
79
+ if (normalized.length === 2 && definition?.inputKind !== 'tmux-sequence') {
80
+ throw new GestureSyntaxError('two-stroke sequences are supported only by tmux sequence actions');
81
+ }
82
+ if (definition?.inputKind === 'tmux-sequence' && normalized.length !== 2) {
83
+ throw new GestureSyntaxError('this action requires a two-stroke tmux sequence');
84
+ }
85
+ if (definition?.inputKind === 'tmux-menu-selector') {
86
+ if (normalized.length !== 1)
87
+ throw new GestureSyntaxError('a menu selector must be one stroke');
88
+ const stroke = normalized[0];
89
+ const pieces = stroke.split('+');
90
+ const key = pieces.at(-1);
91
+ const modifiers = pieces.slice(0, -1);
92
+ if ((key.length !== 1 && key !== 'space') || modifiers.some((modifier) => modifier !== 'shift')) {
93
+ throw new GestureSyntaxError('a menu selector must be one printable character');
94
+ }
95
+ }
96
+ return normalized.join(' > ');
97
+ }
98
+ function normalizeList(raw, definition) {
99
+ if (!Array.isArray(raw))
100
+ throw new GestureSyntaxError('the override must be an array of gesture strings');
101
+ const normalized = [];
102
+ for (const item of raw) {
103
+ if (typeof item !== 'string')
104
+ throw new GestureSyntaxError('every override entry must be a gesture string');
105
+ const gesture = normalizeGesture(item, definition);
106
+ if (!normalized.includes(gesture))
107
+ normalized.push(gesture);
108
+ }
109
+ return Object.freeze(normalized);
110
+ }
111
+ function contextsOverlap(a, b) {
112
+ return a.contexts.some((context) => b.contexts.includes(context));
113
+ }
114
+ function isTmuxRoot(definition) {
115
+ return definition.contexts.includes('tmux.root');
116
+ }
117
+ function isInPane(definition) {
118
+ return !isTmuxRoot(definition)
119
+ && definition.inputKind !== 'tmux-menu-selector'
120
+ && definition.inputKind !== 'tmux-sequence';
121
+ }
122
+ function firstStroke(gesture) {
123
+ return gesture.split(' > ')[0];
124
+ }
125
+ function gesturesConflict(leftDefinition, leftGesture, rightDefinition, rightGesture) {
126
+ const leftSequence = leftGesture.includes(' > ');
127
+ const rightSequence = rightGesture.includes(' > ');
128
+ if (contextsOverlap(leftDefinition, rightDefinition)) {
129
+ if (leftSequence && rightSequence)
130
+ return leftGesture === rightGesture;
131
+ if (leftSequence || rightSequence)
132
+ return firstStroke(leftGesture) === firstStroke(rightGesture);
133
+ return leftGesture === rightGesture;
134
+ }
135
+ const leftRoot = isTmuxRoot(leftDefinition);
136
+ const rightRoot = isTmuxRoot(rightDefinition);
137
+ if (!leftRoot && !rightRoot)
138
+ return false;
139
+ if (leftRoot && rightRoot && leftSequence && rightSequence)
140
+ return leftGesture === rightGesture;
141
+ if (leftRoot && rightRoot)
142
+ return firstStroke(leftGesture) === firstStroke(rightGesture);
143
+ if (leftRoot && isInPane(rightDefinition))
144
+ return firstStroke(leftGesture) === rightGesture;
145
+ if (rightRoot && isInPane(leftDefinition))
146
+ return leftGesture === firstStroke(rightGesture);
147
+ return false;
148
+ }
149
+ function defaultGestures(definition) {
150
+ return Object.freeze(definition.defaults.map((gesture) => normalizeGesture(gesture, definition)));
151
+ }
152
+ class FrozenBindingMap {
153
+ #map;
154
+ constructor(entries) { this.#map = new Map(entries); }
155
+ get size() { return this.#map.size; }
156
+ get(key) { return this.#map.get(key); }
157
+ has(key) { return this.#map.has(key); }
158
+ entries() { return this.#map.entries(); }
159
+ keys() { return this.#map.keys(); }
160
+ values() { return this.#map.values(); }
161
+ forEach(callbackfn, thisArg) {
162
+ for (const [key, value] of this.#map)
163
+ callbackfn.call(thisArg, value, key, this);
164
+ }
165
+ [Symbol.iterator]() { return this.#map[Symbol.iterator](); }
166
+ }
167
+ function diagnostic(code, message, offendingValue, bindingId, relatedBindingIds) {
168
+ return Object.freeze({ severity: 'error', code, message, offendingValue, ...(bindingId ? { bindingId } : {}), ...(relatedBindingIds ? { relatedBindingIds: Object.freeze([...relatedBindingIds]) } : {}) });
169
+ }
170
+ function collisionPairs(records) {
171
+ const out = [];
172
+ for (let leftIndex = 0; leftIndex < records.length; leftIndex += 1) {
173
+ const left = records[leftIndex];
174
+ for (let rightIndex = leftIndex + 1; rightIndex < records.length; rightIndex += 1) {
175
+ const right = records[rightIndex];
176
+ for (const leftGesture of left.gestures) {
177
+ const conflict = right.gestures.find((rightGesture) => gesturesConflict(left.definition, leftGesture, right.definition, rightGesture));
178
+ if (conflict !== undefined) {
179
+ out.push([left, right, leftGesture.includes(' > ') ? leftGesture : conflict]);
180
+ break;
181
+ }
182
+ }
183
+ }
184
+ }
185
+ return out;
186
+ }
187
+ function requirementSatisfied(records, context, capability) {
188
+ return records.some((record) => record.definition.safety === capability
189
+ && record.gestures.length > 0
190
+ && record.definition.contexts.includes(context));
191
+ }
192
+ function rejectCollidingUserOverrides(records, diagnostics) {
193
+ while (true) {
194
+ const rejected = new Set();
195
+ for (const [left, right, gesture] of collisionPairs(records)) {
196
+ if (left.source === 'user')
197
+ rejected.add(left);
198
+ if (right.source === 'user')
199
+ rejected.add(right);
200
+ if (left.source === 'default' && right.source === 'default') {
201
+ throw new Error(`keybinding catalog default collision: ${left.definition.id} / ${right.definition.id} (${gesture})`);
202
+ }
203
+ diagnostics.push(diagnostic('collision', `${left.definition.id} and ${right.definition.id} both claim ${gesture} in overlapping contexts`, gesture, left.source === 'user' ? left.definition.id : right.definition.id, [left.definition.id, right.definition.id]));
204
+ }
205
+ if (rejected.size === 0)
206
+ return;
207
+ for (const record of rejected) {
208
+ record.gestures = defaultGestures(record.definition);
209
+ record.source = 'default';
210
+ }
211
+ }
212
+ }
213
+ export function resolveKeybindings(rawOverrides = undefined) {
214
+ const diagnostics = [];
215
+ let overrides = {};
216
+ if (rawOverrides !== undefined) {
217
+ if (rawOverrides === null || typeof rawOverrides !== 'object' || Array.isArray(rawOverrides)) {
218
+ diagnostics.push(diagnostic('invalid-keybindings', 'keybindings must be an object keyed by catalog binding ID', rawOverrides));
219
+ }
220
+ else {
221
+ overrides = rawOverrides;
222
+ }
223
+ }
224
+ for (const [id, value] of Object.entries(overrides)) {
225
+ if (!BINDING_IDS.has(id)) {
226
+ diagnostics.push(diagnostic('unknown-binding', `unknown keybinding ID: ${id}`, value, id));
227
+ }
228
+ }
229
+ const records = BINDING_CATALOG.map((definition) => {
230
+ const defaults = defaultGestures(definition);
231
+ if (!Object.hasOwn(overrides, definition.id))
232
+ return { definition, gestures: defaults, source: 'default' };
233
+ const rawOverride = overrides[definition.id];
234
+ try {
235
+ return { definition, gestures: normalizeList(rawOverride, definition), source: 'user', rawOverride };
236
+ }
237
+ catch (error) {
238
+ const message = error instanceof Error ? error.message : String(error);
239
+ diagnostics.push(diagnostic('invalid-override', `${definition.id}: ${message}`, rawOverride, definition.id));
240
+ return { definition, gestures: defaults, source: 'default' };
241
+ }
242
+ });
243
+ rejectCollidingUserOverrides(records, diagnostics);
244
+ for (const requirement of SAFETY_REQUIREMENTS) {
245
+ if (requirementSatisfied(records, requirement.context, requirement.capability))
246
+ continue;
247
+ const offenders = records.filter((record) => record.source === 'user'
248
+ && record.definition.safety === requirement.capability
249
+ && record.definition.contexts.includes(requirement.context));
250
+ diagnostics.push(diagnostic('unreachable', `${requirement.context} must retain an enabled ${requirement.capability} binding`, offenders.map((record) => record.rawOverride), offenders[0]?.definition.id, offenders.map((record) => record.definition.id)));
251
+ for (const offender of offenders) {
252
+ offender.gestures = defaultGestures(offender.definition);
253
+ offender.source = 'default';
254
+ }
255
+ }
256
+ rejectCollidingUserOverrides(records, diagnostics);
257
+ const entries = records.map((record) => {
258
+ const effective = Object.freeze({
259
+ definition: record.definition,
260
+ gestures: record.gestures,
261
+ source: record.source,
262
+ });
263
+ return [record.definition.id, effective];
264
+ });
265
+ const bindings = new FrozenBindingMap(entries);
266
+ const result = {
267
+ bindings,
268
+ diagnostics: Object.freeze(diagnostics),
269
+ get(id) {
270
+ const found = bindings.get(id);
271
+ if (found === undefined)
272
+ throw new Error(`unknown crouter keybinding ID: ${id}`);
273
+ return found;
274
+ },
275
+ gestures(id) { return this.get(id).gestures; },
276
+ };
277
+ return Object.freeze(result);
278
+ }
279
+ export function resolveUserKeybindings() {
280
+ const raw = readRawScopeConfig('user');
281
+ return resolveKeybindings(raw?.keybindings);
282
+ }
283
+ export function normalizeSparseOverrides(rawOverrides) {
284
+ const resolved = resolveKeybindings(rawOverrides);
285
+ if (resolved.diagnostics.length > 0) {
286
+ throw new KeybindingValidationError(resolved.diagnostics);
287
+ }
288
+ const sparse = {};
289
+ for (const definition of BINDING_CATALOG) {
290
+ const effective = resolved.get(definition.id);
291
+ const defaults = defaultGestures(definition);
292
+ if (effective.source === 'user' && !isDeepStrictEqual(effective.gestures, defaults)) {
293
+ sparse[definition.id] = [...effective.gestures];
294
+ }
295
+ }
296
+ return sparse;
297
+ }
298
+ export class KeybindingValidationError extends Error {
299
+ diagnostics;
300
+ constructor(diagnostics) {
301
+ super(diagnostics[0]?.message ?? 'invalid keybindings');
302
+ this.name = 'KeybindingValidationError';
303
+ this.diagnostics = diagnostics;
304
+ }
305
+ }
@@ -0,0 +1,37 @@
1
+ export type BindingInputKind = 'terminal' | 'tmux-sequence' | 'tmux-menu-selector' | 'host-global';
2
+ export type BindingSafetyCapability = 'dismiss-modal' | 'leave-screen' | 'save-keybindings';
3
+ export interface BindingDefinition<Id extends string = string> {
4
+ readonly id: Id;
5
+ readonly group: string;
6
+ readonly name: string;
7
+ readonly description: string;
8
+ readonly defaults: readonly string[];
9
+ readonly inputKind: BindingInputKind;
10
+ readonly contexts: readonly string[];
11
+ readonly safety?: BindingSafetyCapability;
12
+ }
13
+ export interface EffectiveBinding<Id extends string = string> {
14
+ readonly definition: BindingDefinition<Id>;
15
+ readonly gestures: readonly string[];
16
+ readonly source: 'default' | 'user';
17
+ }
18
+ export type KeybindingDiagnosticCode = 'invalid-keybindings' | 'unknown-binding' | 'invalid-override' | 'collision' | 'unreachable';
19
+ export interface KeybindingDiagnostic {
20
+ readonly severity: 'error';
21
+ readonly code: KeybindingDiagnosticCode;
22
+ readonly bindingId?: string;
23
+ readonly relatedBindingIds?: readonly string[];
24
+ readonly offendingValue: unknown;
25
+ readonly message: string;
26
+ }
27
+ export interface SafetyRequirement {
28
+ readonly context: string;
29
+ readonly capability: BindingSafetyCapability;
30
+ }
31
+ export interface BindingResolution<Id extends string = string> {
32
+ readonly bindings: ReadonlyMap<Id, EffectiveBinding<Id>>;
33
+ readonly diagnostics: readonly KeybindingDiagnostic[];
34
+ get(id: Id): EffectiveBinding<Id>;
35
+ gestures(id: Id): readonly string[];
36
+ }
37
+ export type SparseKeybindingOverrides<Id extends string = string> = Partial<Record<Id, string[]>>;
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,4 @@
1
- import type { InstalledPlugin, Scope } from '../types.js';
1
+ import { type InstalledPlugin, type Scope } from '../types.js';
2
2
  /**
3
3
  * Thin memory-document resolver for the document substrate. Precedence is the
4
4
  * OUTER loop, not a global scan: scopes/sources are tried NEAREST-FIRST
@@ -1,5 +1,6 @@
1
1
  import { join, relative, sep } from 'node:path';
2
2
  import { readdirSync } from 'node:fs';
3
+ import { CRTR_DIR_NAME } from '../types.js';
3
4
  import { pathExists, readText, walkFiles } from './fs-utils.js';
4
5
  import { parseFrontmatterGeneric } from './frontmatter.js';
5
6
  import { listInstalledPlugins, listInstalledPluginsInRoot, parseSkillQualifier } from './resolver.js';
@@ -117,8 +118,14 @@ function listMemoryDocsInDir(scope, dir, quiet = false) {
117
118
  continue;
118
119
  }
119
120
  for (const e of entries) {
120
- if (e.isDirectory())
121
- stack.push(join(d, e.name));
121
+ // Enumeration stops at store boundaries: a nested `.crouter/` is another
122
+ // scope's mount point (e.g. a dir-scoped store inside a dir that is itself
123
+ // a store, like builtin-memory in the crouter repo), never this store's
124
+ // own content — walking into it would register a different tier's docs.
125
+ if (e.isDirectory()) {
126
+ if (e.name !== CRTR_DIR_NAME)
127
+ stack.push(join(d, e.name));
128
+ }
122
129
  else if (e.isFile() && e.name.endsWith('.md') && e.name !== 'SKILL.md') {
123
130
  const file = join(d, e.name);
124
131
  const raw = relative(dir, file).replace(/\.md$/i, '').split(sep).join('/');
@@ -182,7 +189,7 @@ export function listPluginMemoryDocs(plugin, scope, quiet = false) {
182
189
  if (!pathExists(dir))
183
190
  return [];
184
191
  const docs = [];
185
- for (const file of walkFiles(dir, (n) => n.endsWith('.md') && n !== 'SKILL.md')) {
192
+ for (const file of walkFiles(dir, (n) => n.endsWith('.md') && n !== 'SKILL.md', (d) => d === CRTR_DIR_NAME)) {
186
193
  const derived = normalizeDocName(relative(dir, file).replace(/\.md$/i, '').split(sep).join('/'));
187
194
  if (!derived)
188
195
  continue;
@@ -2,14 +2,16 @@
2
2
  * Predicate matcher engine — vendored verbatim from pi's frontmatter-rules
3
3
  * extension.
4
4
  *
5
- * Source: pi-personal-extensions/extensions/frontmatter-rules/index.ts
5
+ * Source: src/builtin-pi-packages/pi-crtr-extensions/extensions/frontmatter-rules/index.ts
6
6
  * (the "Condition language" block, functions `asArray` … `evalCondition`).
7
7
  *
8
8
  * The unified document substrate uses this to evaluate a document's `gate`
9
9
  * predicate against a node-config subject (e.g. `orchestration.depth: { gte: 2 }`).
10
10
  *
11
- * This is intentionally a COPY, not an import: the source lives in a separate,
12
- * private, unpublished repo that crtr cannot depend on. The functions are pure
11
+ * This is intentionally a COPY, not an import: the bundled pi package is
12
+ * excluded from crouter's tsc compile unit (tsconfig `exclude`) it is loaded
13
+ * by pi's own TS loader at runtime — so compiled core cannot import from it;
14
+ * cross-boundary imports go extension→core only. The functions are pure
13
15
  * TS over `unknown` / `Record<string, unknown>` with zero external imports, so
14
16
  * vendoring them keeps this module standalone and dependency-free. Keep it in
15
17
  * sync with the source by hand if the matcher language ever changes.
@@ -2,14 +2,16 @@
2
2
  * Predicate matcher engine — vendored verbatim from pi's frontmatter-rules
3
3
  * extension.
4
4
  *
5
- * Source: pi-personal-extensions/extensions/frontmatter-rules/index.ts
5
+ * Source: src/builtin-pi-packages/pi-crtr-extensions/extensions/frontmatter-rules/index.ts
6
6
  * (the "Condition language" block, functions `asArray` … `evalCondition`).
7
7
  *
8
8
  * The unified document substrate uses this to evaluate a document's `gate`
9
9
  * predicate against a node-config subject (e.g. `orchestration.depth: { gte: 2 }`).
10
10
  *
11
- * This is intentionally a COPY, not an import: the source lives in a separate,
12
- * private, unpublished repo that crtr cannot depend on. The functions are pure
11
+ * This is intentionally a COPY, not an import: the bundled pi package is
12
+ * excluded from crouter's tsc compile unit (tsconfig `exclude`) it is loaded
13
+ * by pi's own TS loader at runtime — so compiled core cannot import from it;
14
+ * cross-boundary imports go extension→core only. The functions are pure
13
15
  * TS over `unknown` / `Record<string, unknown>` with zero external imports, so
14
16
  * vendoring them keeps this module standalone and dependency-free. Keep it in
15
17
  * sync with the source by hand if the matcher language ever changes.
@@ -169,6 +169,15 @@ export interface CycleModelFrame {
169
169
  type: 'cycle_model';
170
170
  direction?: 'forward' | 'backward';
171
171
  }
172
+ /** `cycleLadder(direction)` — alt+m forward / alt+shift+m backward. Walks the INTERLEAVED model ladder
173
+ * (anthropic/ultra → openai/ultra → anthropic/strong → openai/strong → …),
174
+ * starting from the current model, applying each rung's own thinking level.
175
+ * Distinct from `cycle_model`, which steps the flat registry list. `direction`
176
+ * defaults to `'forward'` when absent. */
177
+ export interface CycleLadderFrame {
178
+ type: 'cycle_ladder';
179
+ direction?: 'forward' | 'backward';
180
+ }
172
181
  /** `cycleThinkingLevel()` — shift+tab. Cycles to the next thinking level; the new
173
182
  * level reaches viewers via the relayed `thinking_level_changed` event, so the
174
183
  * broker replies a plain `ack` (no payload). Distinct from `set_thinking_level`,
@@ -303,7 +312,7 @@ export interface ShareFrame {
303
312
  export interface ReloadAuthFrame {
304
313
  type: 'reload_auth';
305
314
  }
306
- export type ClientToBroker = HelloFrame | PromptFrame | SteerFrame | FollowUpFrame | AbortFrame | BashFrame | RequestControlFrame | ReleaseControlFrame | ByeFrame | ShutdownFrame | SetModelFrame | CycleModelFrame | CycleThinkingFrame | SetThinkingLevelFrame | DequeueFrame | SetAutoRetryFrame | SetAutoCompactionFrame | CompactFrame | NewSessionFrame | SwitchSessionFrame | ForkFrame | SetSessionNameFrame | GetCommandsFrame | NavigateTreeFrame | ReloadFrame | ExportFrame | ListModelsFrame | ListSessionsFrame | GetTreeFrame | GetSettingsFrame | ListScopedModelsFrame | ExtensionUIResponseFrame | CloneFrame | ShareFrame | ReloadAuthFrame;
315
+ export type ClientToBroker = HelloFrame | PromptFrame | SteerFrame | FollowUpFrame | AbortFrame | BashFrame | RequestControlFrame | ReleaseControlFrame | ByeFrame | ShutdownFrame | SetModelFrame | CycleModelFrame | CycleLadderFrame | CycleThinkingFrame | SetThinkingLevelFrame | DequeueFrame | SetAutoRetryFrame | SetAutoCompactionFrame | CompactFrame | NewSessionFrame | SwitchSessionFrame | ForkFrame | SetSessionNameFrame | GetCommandsFrame | NavigateTreeFrame | ReloadFrame | ExportFrame | ListModelsFrame | ListSessionsFrame | GetTreeFrame | GetSettingsFrame | ListScopedModelsFrame | ExtensionUIResponseFrame | CloneFrame | ShareFrame | ReloadAuthFrame;
307
316
  export interface WelcomeFrame {
308
317
  type: 'welcome';
309
318
  snapshot: BrokerSnapshot;
@@ -330,6 +339,13 @@ export interface ControlChangedFrame {
330
339
  export interface ModelChangedFrame {
331
340
  type: 'model_changed';
332
341
  model: string | undefined;
342
+ /** The full resolved `provider/id[:thinking]` spec (`formatModelSpec`) — what
343
+ * the broker persists into the node's own launch recipe. Viewers use it to
344
+ * persist a non-portable explicit pick as a kind DEFAULT
345
+ * (`persistDefaultKindModel`) without guessing the provider from the bare
346
+ * id. Null when the engine has no model. Optional so older brokers'
347
+ * frames still parse. */
348
+ spec?: string | null;
333
349
  }
334
350
  export interface ErrorFrame {
335
351
  type: 'error';
@@ -380,9 +396,10 @@ export interface WireScopedModel {
380
396
  model: Model<Api>;
381
397
  thinkingLevel?: AgentSession['thinkingLevel'];
382
398
  }
383
- /** Wire form of pi's `SessionInfo`: identical EXCEPT `created`/`modified` are ISO
384
- * strings (pi's are `Date`, which JSON cannot round-trip) the viewer revives
385
- * them with `new Date(...)` before handing them to `SessionSelectorComponent`. */
399
+ /** Wire form of pi's `SessionInfo`: `created`/`modified` are ISO strings (pi's
400
+ * are `Date`, which JSON cannot round-trip), and `allMessagesText` is a bounded
401
+ * opening/latest search excerpt rather than the unbounded transcript text. The
402
+ * viewer revives the dates before handing them to `SessionSelectorComponent`. */
386
403
  export interface WireSessionInfo extends Omit<SessionInfo, 'created' | 'modified'> {
387
404
  created: string;
388
405
  modified: string;
@@ -494,10 +511,21 @@ export interface DisplayTitleFrame {
494
511
  }
495
512
  /** A blocking dialog routed to the controller — pi's public RPC request type. */
496
513
  export type ExtensionUIRequestFrame = RpcExtensionUIRequest;
514
+ /** Broker → client: dismiss a previously-forwarded blocking dialog by request id,
515
+ * WITHOUT a client answer. The broker sends this exactly when it resolves a pending
516
+ * dialog ITSELF rather than the client answering it — the extension aborted the
517
+ * request out-of-band (e.g. a local OAuth loopback callback won the race against a
518
+ * still-open manual-paste dialog) or the broker-side timeout fired. The client tears
519
+ * down ONLY the overlay whose `id` matches; every other dialog stays exactly as it
520
+ * was, so an unrelated request can never dismiss an unrelated blocking dialog. */
521
+ export interface ExtensionUIDismissFrame {
522
+ type: 'extension_ui_dismiss';
523
+ id: string;
524
+ }
497
525
  /** Everything the broker can send. Live `AgentSessionEvent`s are relayed
498
526
  * verbatim (the broker adds nothing); the broker's own control frames carry
499
527
  * non-colliding `type` discriminants. */
500
- export type BrokerToClient = WelcomeFrame | ControlChangedFrame | ModelChangedFrame | ErrorFrame | AckFrame | BrokerDataFrame | DisplayStatusFrame | DisplayWidgetFrame | DisplayTitleFrame | BashStartFrame | BashOutputFrame | BashEndFrame | ExtensionUIRequestFrame | AgentSessionEvent;
528
+ export type BrokerToClient = WelcomeFrame | ControlChangedFrame | ModelChangedFrame | ErrorFrame | AckFrame | BrokerDataFrame | DisplayStatusFrame | DisplayWidgetFrame | DisplayTitleFrame | BashStartFrame | BashOutputFrame | BashEndFrame | ExtensionUIRequestFrame | ExtensionUIDismissFrame | AgentSessionEvent;
501
529
  /** Encode one frame as a single newline-terminated JSON line. */
502
530
  export declare function encodeFrame(frame: ClientToBroker | BrokerToClient): string;
503
531
  /** Byte bounds for a {@link FrameDecoder} (C5). */