@crouton-kit/crouter 0.3.70 → 0.3.79
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/README.md +1 -1
- package/dist/build-root.d.ts +12 -4
- package/dist/build-root.js +25 -6
- package/dist/builtin-memory/00-runtime-base.md +3 -10
- package/dist/builtin-memory/04-base-worker.md +18 -0
- package/dist/builtin-memory/04-orchestration-kernel.md +1 -1
- package/dist/builtin-memory/crouter-development/plugins.md +82 -5
- package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +13 -34
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts +1115 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +234 -71
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/statusline.ts +2 -9
- package/dist/builtin-pi-packages/pi-crtr-extensions/lib/subscription-state.ts +2 -516
- package/dist/builtin-pi-packages/pi-crtr-extensions/package-lock.json +2 -2
- package/dist/builtin-pi-packages/pi-mode-switch/README.md +11 -39
- package/dist/builtin-pi-packages/pi-mode-switch/extensions/index.ts +20 -15
- package/dist/builtin-pi-packages/pi-mode-switch/package.json +1 -1
- package/dist/builtin-views/canvas/tui.mjs +8 -9
- package/dist/builtin-views/chat/tui.mjs +12 -12
- package/dist/builtin-views/git-pr/tui.mjs +7 -8
- package/dist/builtin-views/inbox/tui.mjs +27 -41
- package/dist/builtin-views/linkedin/tui.mjs +23 -37
- package/dist/builtin-views/prompt-review/tui.mjs +11 -11
- package/dist/builtin-views/settings/tui.mjs +11 -11
- package/dist/builtin-views/workspace-sidebar/tui.mjs +8 -9
- package/dist/clients/attach/__tests__/attach-chrome-remote.test.js +11 -6
- package/dist/clients/attach/__tests__/attach-keybindings.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/attach-keybindings.test.js +113 -0
- package/dist/clients/attach/__tests__/crtr-output-render.test.js +1 -1
- package/dist/clients/attach/__tests__/mermaid-render.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/mermaid-render.test.js +28 -0
- package/dist/clients/attach/__tests__/oauth-dialog-lifecycle.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/oauth-dialog-lifecycle.test.js +110 -0
- package/dist/clients/attach/attach-cmd.d.ts +9 -1
- package/dist/clients/attach/attach-cmd.js +841 -792
- package/dist/clients/attach/auth-pickers.d.ts +0 -12
- package/dist/clients/attach/auth-pickers.js +64 -15
- package/dist/clients/attach/canvas-panels.js +2 -3
- package/dist/clients/attach/chat-view.d.ts +7 -8
- package/dist/clients/attach/chat-view.js +119 -79
- package/dist/clients/attach/edit-diff-render.d.ts +6 -0
- package/dist/clients/attach/edit-diff-render.js +263 -0
- package/dist/clients/attach/graph-overlay.d.ts +12 -2
- package/dist/clients/attach/graph-overlay.js +83 -33
- package/dist/clients/attach/input-controller.d.ts +28 -2
- package/dist/clients/attach/input-controller.js +38 -5
- package/dist/clients/attach/mermaid-render.js +5 -2
- package/dist/clients/attach/pickers.d.ts +8 -7
- package/dist/clients/attach/pickers.js +11 -17
- package/dist/clients/attach/slash-commands.d.ts +9 -0
- package/dist/clients/attach/slash-commands.js +127 -7
- package/dist/clients/attach/titled-editor.d.ts +12 -1
- package/dist/clients/attach/titled-editor.js +103 -8
- package/dist/commands/canvas-browse.js +2 -2
- package/dist/commands/human/queue.js +3 -4
- package/dist/commands/memory/lint.js +39 -5
- package/dist/commands/memory/write.js +1 -0
- package/dist/commands/node.js +9 -2
- package/dist/commands/pkg/plugin-inspect.js +22 -1
- package/dist/commands/pkg/plugin-manage.js +31 -9
- package/dist/commands/surface-tmux-spread.js +1 -3
- package/dist/commands/sys/__tests__/config-keybindings.test.d.ts +1 -0
- package/dist/commands/sys/__tests__/config-keybindings.test.js +55 -0
- package/dist/commands/sys/__tests__/config-model-ladders.test.d.ts +1 -0
- package/dist/commands/sys/__tests__/config-model-ladders.test.js +121 -0
- package/dist/commands/sys/__tests__/setup-core.test.js +158 -1
- package/dist/commands/sys/config.js +18 -21
- package/dist/commands/sys/doctor.js +42 -4
- package/dist/commands/sys/setup-core.d.ts +49 -1
- package/dist/commands/sys/setup-core.js +161 -4
- package/dist/commands/sys/setup.d.ts +88 -0
- package/dist/commands/sys/setup.js +943 -169
- package/dist/commands/view-pick.d.ts +4 -0
- package/dist/commands/view-pick.js +17 -7
- package/dist/core/__tests__/base-worker-prompt.test.d.ts +1 -0
- package/dist/core/__tests__/base-worker-prompt.test.js +24 -0
- package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +232 -1
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +34 -9
- package/dist/core/__tests__/command-plugins-surfaces.test.d.ts +1 -0
- package/dist/core/__tests__/command-plugins-surfaces.test.js +298 -0
- package/dist/core/__tests__/command-plugins.test.d.ts +1 -0
- package/dist/core/__tests__/command-plugins.test.js +444 -0
- package/dist/core/__tests__/fault-classifier.test.js +15 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +6 -0
- package/dist/core/__tests__/fixtures/fake-engine.js +9 -1
- package/dist/core/__tests__/full/broker-dialogs.test.js +7 -2
- package/dist/core/__tests__/host-teardown-process-group.test.js +15 -4
- package/dist/core/__tests__/preview-registry-sync.test.js +30 -1
- package/dist/core/__tests__/scope-crouter-home-fence.test.d.ts +1 -0
- package/dist/core/__tests__/scope-crouter-home-fence.test.js +55 -0
- package/dist/core/__tests__/stream-watchdog.test.d.ts +1 -0
- package/dist/core/__tests__/stream-watchdog.test.js +70 -0
- package/dist/core/__tests__/tmux-surface.test.js +72 -0
- package/dist/core/canvas/browse/__tests__/model.test.js +23 -5
- package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
- package/dist/core/canvas/browse/app.d.ts +6 -0
- package/dist/core/canvas/browse/app.js +230 -41
- package/dist/core/canvas/browse/model.d.ts +10 -6
- package/dist/core/canvas/browse/model.js +20 -12
- package/dist/core/canvas/browse/pins.d.ts +4 -0
- package/dist/core/canvas/browse/pins.js +29 -0
- package/dist/core/canvas/browse/render.d.ts +13 -1
- package/dist/core/canvas/browse/render.js +72 -15
- package/dist/core/canvas/nav-model.js +5 -11
- package/dist/core/canvas/paths.d.ts +5 -0
- package/dist/core/canvas/paths.js +7 -0
- package/dist/core/command-plugins/adapter.d.ts +15 -0
- package/dist/core/command-plugins/adapter.js +145 -0
- package/dist/core/command-plugins/compose.d.ts +5 -0
- package/dist/core/command-plugins/compose.js +56 -0
- package/dist/core/command-plugins/discovery.d.ts +104 -0
- package/dist/core/command-plugins/discovery.js +565 -0
- package/dist/core/config.d.ts +48 -0
- package/dist/core/config.js +228 -110
- package/dist/core/fault-classifier.js +1 -1
- package/dist/core/fs-utils.d.ts +1 -0
- package/dist/core/fs-utils.js +15 -1
- package/dist/core/keybindings/__tests__/bespoke-consumers.test.d.ts +1 -0
- package/dist/core/keybindings/__tests__/bespoke-consumers.test.js +40 -0
- package/dist/core/keybindings/__tests__/resolve.test.d.ts +1 -0
- package/dist/core/keybindings/__tests__/resolve.test.js +224 -0
- package/dist/core/keybindings/catalog.d.ts +14 -0
- package/dist/core/keybindings/catalog.js +257 -0
- package/dist/core/keybindings/index.d.ts +5 -0
- package/dist/core/keybindings/index.js +4 -0
- package/dist/core/keybindings/match.d.ts +29 -0
- package/dist/core/keybindings/match.js +99 -0
- package/dist/core/keybindings/persistence.d.ts +14 -0
- package/dist/core/keybindings/persistence.js +31 -0
- package/dist/core/keybindings/resolve.d.ts +14 -0
- package/dist/core/keybindings/resolve.js +305 -0
- package/dist/core/keybindings/types.d.ts +37 -0
- package/dist/core/keybindings/types.js +1 -0
- package/dist/core/memory-resolver.d.ts +1 -1
- package/dist/core/memory-resolver.js +10 -3
- package/dist/core/predicate.d.ts +5 -3
- package/dist/core/predicate.js +5 -3
- package/dist/core/profiles/select.d.ts +6 -0
- package/dist/core/profiles/select.js +86 -52
- package/dist/core/provider-management.d.ts +12 -0
- package/dist/core/provider-management.js +24 -0
- package/dist/core/runtime/broker-protocol.d.ts +33 -5
- package/dist/core/runtime/broker.js +164 -14
- package/dist/core/runtime/launch.d.ts +39 -6
- package/dist/core/runtime/launch.js +78 -20
- package/dist/core/runtime/naming.js +3 -3
- package/dist/core/runtime/pi-cli.d.ts +6 -0
- package/dist/core/runtime/pi-cli.js +16 -2
- package/dist/core/runtime/pi-vendored.d.ts +8 -0
- package/dist/core/runtime/pi-vendored.js +14 -0
- package/dist/core/runtime/placement.d.ts +2 -2
- package/dist/core/runtime/placement.js +4 -1
- package/dist/core/runtime/promote.js +4 -0
- package/dist/core/runtime/recap.d.ts +3 -3
- package/dist/core/runtime/recap.js +75 -47
- package/dist/core/runtime/recycle.js +6 -1
- package/dist/core/runtime/reset.js +5 -0
- package/dist/core/runtime/session-list-cache.d.ts +33 -0
- package/dist/core/runtime/session-list-cache.js +338 -0
- package/dist/core/runtime/spawn.js +52 -45
- package/dist/core/runtime/stream-watchdog.d.ts +26 -0
- package/dist/core/runtime/stream-watchdog.js +75 -0
- package/dist/core/runtime/tmux-chrome.d.ts +1 -1
- package/dist/core/runtime/tmux-chrome.js +2 -2
- package/dist/core/runtime/tmux.d.ts +18 -15
- package/dist/core/runtime/tmux.js +217 -110
- package/dist/core/scope.js +27 -4
- package/dist/core/subscription-state.d.ts +90 -0
- package/dist/core/subscription-state.js +762 -0
- package/dist/core/tui/__tests__/host-keybindings.test.d.ts +1 -0
- package/dist/core/tui/__tests__/host-keybindings.test.js +112 -0
- package/dist/core/tui/host.d.ts +30 -1
- package/dist/core/tui/host.js +148 -34
- package/dist/core/view/contract.d.ts +17 -3
- package/dist/daemon/crtrd.js +253 -12
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +41 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-context-nudge.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-context-nudge.test.js +121 -0
- package/dist/pi-extensions/canvas-inbox-watcher.d.ts +1 -1
- package/dist/pi-extensions/canvas-inbox-watcher.js +203 -25
- package/dist/pi-extensions/canvas-recap.d.ts +4 -0
- package/dist/pi-extensions/canvas-recap.js +73 -46
- package/dist/pi-extensions/canvas-stophook.d.ts +11 -0
- package/dist/pi-extensions/canvas-stophook.js +33 -4
- package/dist/types.d.ts +9 -26
- package/dist/types.js +1 -28
- package/dist/web-client/assets/{index-DiFuLcp6.js → index--md2ylfi.js} +1 -1
- package/dist/web-client/index.html +1 -1
- package/dist/web-client/sw.js +1 -1
- package/package.json +8 -4
- package/dist/pi-extensions/canvas-nav.d.ts +0 -43
- package/dist/pi-extensions/canvas-nav.js +0 -640
- package/dist/pi-extensions/widget-order-bus.d.ts +0 -6
- package/dist/pi-extensions/widget-order-bus.js +0 -34
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { resolveKeybindings } from '../../keybindings/index.js';
|
|
4
|
+
import { matchViewBinding, matchesViewHostBinding, parseViewKeypress, resolveViewKeyHints } from '../host.js';
|
|
5
|
+
function key(input = '', patch = {}) {
|
|
6
|
+
return {
|
|
7
|
+
upArrow: false,
|
|
8
|
+
downArrow: false,
|
|
9
|
+
leftArrow: false,
|
|
10
|
+
rightArrow: false,
|
|
11
|
+
return: false,
|
|
12
|
+
escape: false,
|
|
13
|
+
ctrl: false,
|
|
14
|
+
meta: false,
|
|
15
|
+
tab: false,
|
|
16
|
+
shiftTab: false,
|
|
17
|
+
backspace: false,
|
|
18
|
+
...patch,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
test('catalog view bindings use defaults, remaps, and effective hint labels', () => {
|
|
22
|
+
const keymap = [
|
|
23
|
+
{ bindingId: 'crtr.view.canvas.down', intent: 'down', hint: { label: 'move' } },
|
|
24
|
+
];
|
|
25
|
+
const defaults = resolveKeybindings();
|
|
26
|
+
assert.deepEqual(matchViewBinding(keymap, defaults, 'j', key('j'), { row: 0 }), { intent: 'down', payload: undefined });
|
|
27
|
+
assert.deepEqual(resolveViewKeyHints(keymap, defaults, { row: 0 }), [{ keys: 'J / Down', label: 'move' }]);
|
|
28
|
+
const remapped = resolveKeybindings({ 'crtr.view.canvas.down': ['x'] });
|
|
29
|
+
assert.equal(matchViewBinding(keymap, remapped, 'j', key('j'), { row: 0 }), null);
|
|
30
|
+
assert.deepEqual(matchViewBinding(keymap, remapped, 'x', key('x'), { row: 0 }), { intent: 'down', payload: undefined });
|
|
31
|
+
assert.deepEqual(resolveViewKeyHints(keymap, remapped, { row: 0 }), [{ keys: 'X', label: 'move' }]);
|
|
32
|
+
});
|
|
33
|
+
test('custom view literals retain their matching and presenter-authored hints', () => {
|
|
34
|
+
const keymap = [
|
|
35
|
+
{ keys: ['z'], intent: 'custom', hint: { keys: 'z', label: 'custom' } },
|
|
36
|
+
];
|
|
37
|
+
const bindings = resolveKeybindings();
|
|
38
|
+
assert.deepEqual(matchViewBinding(keymap, bindings, 'z', key('z'), undefined), { intent: 'custom', payload: undefined });
|
|
39
|
+
assert.equal(matchViewBinding(keymap, bindings, 'x', key('x'), undefined), null);
|
|
40
|
+
assert.deepEqual(resolveViewKeyHints(keymap, bindings, undefined), [{ keys: 'z', label: 'custom' }]);
|
|
41
|
+
});
|
|
42
|
+
test('Alt+<letter> does not falsely match a bare custom literal', () => {
|
|
43
|
+
const keymap = [{ keys: ['q'], intent: 'quit' }];
|
|
44
|
+
const bindings = resolveKeybindings();
|
|
45
|
+
const altQ = parseViewKeypress(Buffer.from('\x1bq'));
|
|
46
|
+
assert.equal(matchViewBinding(keymap, bindings, altQ.input, altQ.key, undefined, altQ.raw), null);
|
|
47
|
+
const q = parseViewKeypress(Buffer.from('q'));
|
|
48
|
+
assert.deepEqual(matchViewBinding(keymap, bindings, q.input, q.key, undefined, q.raw), { intent: 'quit', payload: undefined });
|
|
49
|
+
});
|
|
50
|
+
test('host force quit and capture deletion follow catalog IDs through raw terminal decoding', () => {
|
|
51
|
+
const defaults = resolveKeybindings();
|
|
52
|
+
const ctrlC = parseViewKeypress(Buffer.from('\x03'));
|
|
53
|
+
const del = parseViewKeypress(Buffer.from('\x7f'), defaults);
|
|
54
|
+
const bs = parseViewKeypress(Buffer.from('\b'), defaults);
|
|
55
|
+
assert.ok(matchesViewHostBinding(defaults, 'crtr.view.host.force-quit', ctrlC.raw));
|
|
56
|
+
assert.ok(matchesViewHostBinding(defaults, 'crtr.view.host.editor.backspace', del.raw));
|
|
57
|
+
// 0x08 stays Backspace under default config so ^H-erase terminals still delete.
|
|
58
|
+
assert.ok(matchesViewHostBinding(defaults, 'crtr.view.host.editor.backspace', bs.raw));
|
|
59
|
+
const remapped = resolveKeybindings({
|
|
60
|
+
'crtr.view.host.force-quit': ['alt+x'],
|
|
61
|
+
'crtr.view.host.editor.backspace': ['ctrl+h'],
|
|
62
|
+
});
|
|
63
|
+
const altX = parseViewKeypress(Buffer.from('\x1bx'), remapped);
|
|
64
|
+
const ctrlH = parseViewKeypress(Buffer.from('\b'), remapped);
|
|
65
|
+
const delRemap = parseViewKeypress(Buffer.from('\x7f'), remapped);
|
|
66
|
+
assert.equal(matchesViewHostBinding(remapped, 'crtr.view.host.force-quit', ctrlC.raw), false);
|
|
67
|
+
assert.ok(matchesViewHostBinding(remapped, 'crtr.view.host.force-quit', altX.raw));
|
|
68
|
+
// With deletion remapped to Ctrl-H, the 0x08 byte deletes and DEL no longer does.
|
|
69
|
+
assert.equal(matchesViewHostBinding(remapped, 'crtr.view.host.editor.backspace', delRemap.raw), false);
|
|
70
|
+
assert.ok(matchesViewHostBinding(remapped, 'crtr.view.host.editor.backspace', ctrlH.raw));
|
|
71
|
+
});
|
|
72
|
+
test('Alt+Backspace deletes like Backspace under default config', () => {
|
|
73
|
+
const defaults = resolveKeybindings();
|
|
74
|
+
const altDel = parseViewKeypress(Buffer.from('\x1b\x7f'), defaults);
|
|
75
|
+
const altBs = parseViewKeypress(Buffer.from('\x1b\b'), defaults);
|
|
76
|
+
assert.ok(matchesViewHostBinding(defaults, 'crtr.view.host.editor.backspace', altDel.raw));
|
|
77
|
+
assert.ok(matchesViewHostBinding(defaults, 'crtr.view.host.editor.backspace', altBs.raw));
|
|
78
|
+
});
|
|
79
|
+
test('raw terminal decoding covers modified arrows and enter/return equivalence', () => {
|
|
80
|
+
const keymap = [{ bindingId: 'crtr.view.settings.edit-or-submit', intent: 'submit' }];
|
|
81
|
+
const bindings = resolveKeybindings();
|
|
82
|
+
const cr = parseViewKeypress(Buffer.from('\r'));
|
|
83
|
+
const lf = parseViewKeypress(Buffer.from('\n'));
|
|
84
|
+
assert.deepEqual(matchViewBinding(keymap, bindings, cr.input, cr.key, undefined, cr.raw), { intent: 'submit', payload: undefined });
|
|
85
|
+
assert.deepEqual(matchViewBinding(keymap, bindings, lf.input, lf.key, undefined, lf.raw), { intent: 'submit', payload: undefined });
|
|
86
|
+
const ctrlUp = parseViewKeypress(Buffer.from('\x1b[1;5A'));
|
|
87
|
+
assert.equal(ctrlUp.raw.upArrow, true);
|
|
88
|
+
assert.equal(ctrlUp.raw.ctrl, true);
|
|
89
|
+
const up = parseViewKeypress(Buffer.from('\x1b[A'));
|
|
90
|
+
assert.equal(up.raw.upArrow, true);
|
|
91
|
+
assert.equal(up.raw.ctrl, false);
|
|
92
|
+
});
|
|
93
|
+
test('a disabled binding neither matches nor renders a hint', () => {
|
|
94
|
+
const keymap = [
|
|
95
|
+
{ bindingId: 'crtr.view.canvas.refresh', intent: 'refresh', hint: { label: 'refresh' } },
|
|
96
|
+
];
|
|
97
|
+
const bindings = resolveKeybindings({ 'crtr.view.canvas.refresh': [] });
|
|
98
|
+
const g = parseViewKeypress(Buffer.from('g'));
|
|
99
|
+
assert.equal(matchViewBinding(keymap, bindings, g.input, g.key, undefined, g.raw), null);
|
|
100
|
+
assert.deepEqual(resolveViewKeyHints(keymap, bindings, undefined), []);
|
|
101
|
+
});
|
|
102
|
+
test('capture hints are host-owned, mode-gated, and keep custom KeyHint keys verbatim', () => {
|
|
103
|
+
const gated = [
|
|
104
|
+
{ capture: 'setDraft', when: (state) => state.compose, hint: { label: 'compose' } },
|
|
105
|
+
];
|
|
106
|
+
assert.deepEqual(resolveViewKeyHints(gated, resolveKeybindings(), { compose: true }), [{ keys: 'type', label: 'compose' }]);
|
|
107
|
+
assert.deepEqual(resolveViewKeyHints(gated, resolveKeybindings(), { compose: false }), []);
|
|
108
|
+
const custom = [
|
|
109
|
+
{ capture: 'setDraft', when: () => true, hint: { keys: 'edit', label: 'compose' } },
|
|
110
|
+
];
|
|
111
|
+
assert.deepEqual(resolveViewKeyHints(custom, resolveKeybindings(), undefined), [{ keys: 'edit', label: 'compose' }]);
|
|
112
|
+
});
|
package/dist/core/tui/host.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { type Key } from './terminal.js';
|
|
1
2
|
import { type Draw, type Rect, type Size } from './draw.js';
|
|
2
|
-
import type { ViewCore, TuiPresenter, TextPresenter, KeyHint, BannerLevel } from '../view/contract.js';
|
|
3
|
+
import type { ViewCore, TuiPresenter, TextPresenter, KeyBinding, KeyHint, BannerLevel } from '../view/contract.js';
|
|
4
|
+
import { type BindingId, type BindingResolution, type RawTerminalInput } from '../keybindings/index.js';
|
|
3
5
|
export interface RunViewOptions {
|
|
4
6
|
/** CLI flags forwarded verbatim to the view via host.options. */
|
|
5
7
|
options?: Record<string, string>;
|
|
@@ -32,6 +34,33 @@ export interface ChromeManifest {
|
|
|
32
34
|
* on the row above it when set. */
|
|
33
35
|
declare function drawChrome(draw: Draw, size: Size, manifest: ChromeManifest, c: Chrome, now?: number): Rect;
|
|
34
36
|
export { drawChrome };
|
|
37
|
+
/** Tokens a keystroke can match a keymap binding's `keys` against. Arrows →
|
|
38
|
+
* up/down/left/right, return → return|enter, escape → escape|esc, a printable
|
|
39
|
+
* char → the char itself (+ 'space' for ' '), ctrl+x → ctrl+x|c-x. */
|
|
40
|
+
export interface ViewKeypress {
|
|
41
|
+
readonly input: string;
|
|
42
|
+
readonly key: Key;
|
|
43
|
+
readonly raw: RawTerminalInput;
|
|
44
|
+
}
|
|
45
|
+
/** Extend the shared legacy parser with the terminal gestures the catalog can
|
|
46
|
+
* express. The raw shape goes only to the catalog matcher; custom literals keep
|
|
47
|
+
* their established Key-based matching. When `bindings` is supplied the ambiguous
|
|
48
|
+
* 0x08 byte resolves to Ctrl-H only if an effective binding claims it. */
|
|
49
|
+
export declare function parseViewKeypress(data: Buffer, bindings?: BindingResolution<BindingId>): ViewKeypress;
|
|
50
|
+
/** Resolve footer hints for the CURRENT state: only bindings whose `when(state)`
|
|
51
|
+
* passes contribute, so mode-gated affordances (compose/react send/cancel/pick)
|
|
52
|
+
* appear exactly while active. Catalog bindings render the effective key label
|
|
53
|
+
* (omitted entirely when disabled); custom-view literal hints stay verbatim. */
|
|
54
|
+
export declare function resolveViewKeyHints<S>(keymap: KeyBinding<S>[], bindings: BindingResolution<BindingId>, state: S): KeyHint[];
|
|
55
|
+
/** The first non-capture binding whose key matches and whose `when` (if any)
|
|
56
|
+
* passes — catalog IDs use the terminal matcher; custom literals retain their
|
|
57
|
+
* presenter-authored matching. */
|
|
58
|
+
export declare function matchViewBinding<S>(keymap: KeyBinding<S>[], bindings: BindingResolution<BindingId>, input: string, key: Key, state: S, raw?: RawTerminalInput): {
|
|
59
|
+
intent: string;
|
|
60
|
+
payload: unknown;
|
|
61
|
+
} | null;
|
|
62
|
+
/** Match a host-owned generic-view action through the same catalog snapshot. */
|
|
63
|
+
export declare function matchesViewHostBinding(bindings: BindingResolution<BindingId>, id: 'crtr.view.host.force-quit' | 'crtr.view.host.editor.backspace', raw: RawTerminalInput): boolean;
|
|
35
64
|
/** Host a dual-target ViewCore + TuiPresenter in the alt screen until it quits
|
|
36
65
|
* (or Ctrl-C). `text` is the optional text presenter for the piped path. */
|
|
37
66
|
export declare function runCoreView<S>(core: ViewCore<S>, tui: TuiPresenter<S>, text: TextPresenter<S> | null, opts?: RunViewOptions): Promise<void>;
|
package/dist/core/tui/host.js
CHANGED
|
@@ -22,6 +22,7 @@ import { setupTerminal, restoreTerminal, getTerminalSize, parseKeypress, } from
|
|
|
22
22
|
import { createDraw, detectColorCaps } from './draw.js';
|
|
23
23
|
import { initialChrome, deriveState } from '../view/chrome.js';
|
|
24
24
|
import { createLocalTransport } from '../view/transport-local.js';
|
|
25
|
+
import { formatBinding, matchesTerminalInput, resolveUserKeybindings, } from '../keybindings/index.js';
|
|
25
26
|
// Numeric SGR codes only (a color NAME emits a broken CSI — see draw.ts guard).
|
|
26
27
|
const FG = { cyan: '36', green: '32', yellow: '33', red: '31', grey: '90' };
|
|
27
28
|
/** The persistent state signal: word + glyph + hue, derived from host signals so
|
|
@@ -179,15 +180,94 @@ function drawChrome(draw, size, manifest, c, now = Date.now()) {
|
|
|
179
180
|
return { row: top, col: 0, width: cols, height };
|
|
180
181
|
}
|
|
181
182
|
export { drawChrome };
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
183
|
+
/** True when some effective binding claims the `ctrl+h` gesture — used to decide
|
|
184
|
+
* whether the ambiguous 0x08 byte is a Ctrl-H remap or a plain Backspace. */
|
|
185
|
+
function bindingSetClaimsCtrlH(bindings) {
|
|
186
|
+
for (const eff of bindings.bindings.values()) {
|
|
187
|
+
if (eff.gestures.includes('ctrl+h'))
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
/** One-line summary of rejected hand-edited overrides, surfaced once per surface. */
|
|
193
|
+
function summarizeKeybindingDiagnostics(diags) {
|
|
194
|
+
if (diags.length === 0)
|
|
195
|
+
return null;
|
|
196
|
+
const extra = diags.length > 1 ? ` (+${diags.length - 1} more)` : '';
|
|
197
|
+
return `keybindings: ${diags[0].message}${extra}`;
|
|
198
|
+
}
|
|
199
|
+
/** Extend the shared legacy parser with the terminal gestures the catalog can
|
|
200
|
+
* express. The raw shape goes only to the catalog matcher; custom literals keep
|
|
201
|
+
* their established Key-based matching. When `bindings` is supplied the ambiguous
|
|
202
|
+
* 0x08 byte resolves to Ctrl-H only if an effective binding claims it. */
|
|
203
|
+
export function parseViewKeypress(data, bindings) {
|
|
204
|
+
const parsed = parseKeypress(data);
|
|
205
|
+
const raw = { ...parsed.key, input: parsed.input };
|
|
206
|
+
const sequence = data.toString('utf8');
|
|
207
|
+
const named = {
|
|
208
|
+
'\x1b[3~': 'delete', '\x1b[5~': 'pageUp', '\x1b[6~': 'pageDown',
|
|
209
|
+
'\x1b[H': 'home', '\x1b[F': 'end', '\x1b[1~': 'home', '\x1b[4~': 'end',
|
|
210
|
+
'\x1bOH': 'home', '\x1bOF': 'end',
|
|
211
|
+
};
|
|
212
|
+
const setModifiers = (code) => {
|
|
213
|
+
const value = Number(code);
|
|
214
|
+
raw.shift = value === 2 || value === 4 || value === 6 || value === 8;
|
|
215
|
+
raw.meta = value === 3 || value === 4 || value === 7 || value === 8;
|
|
216
|
+
raw.ctrl = value === 5 || value === 6 || value === 7 || value === 8;
|
|
217
|
+
};
|
|
218
|
+
const namedKey = named[sequence];
|
|
219
|
+
if (namedKey)
|
|
220
|
+
raw[namedKey] = true;
|
|
221
|
+
const arrow = /^\x1b\[(?:1;)?([2-8])?([ABCD])$/.exec(sequence);
|
|
222
|
+
if (arrow) {
|
|
223
|
+
setModifiers(arrow[1]);
|
|
224
|
+
raw[{ A: 'upArrow', B: 'downArrow', C: 'rightArrow', D: 'leftArrow' }[arrow[2]]] = true;
|
|
225
|
+
}
|
|
226
|
+
const modifiedNamed = /^\x1b\[(3|5|6|1|4);([2-8])~$/.exec(sequence);
|
|
227
|
+
if (modifiedNamed) {
|
|
228
|
+
raw[{ 3: 'delete', 5: 'pageUp', 6: 'pageDown', 1: 'home', 4: 'end' }[modifiedNamed[1]]] = true;
|
|
229
|
+
setModifiers(modifiedNamed[2]);
|
|
230
|
+
}
|
|
231
|
+
const modifiedHomeEnd = /^\x1b\[1;([2-8])([HF])$/.exec(sequence);
|
|
232
|
+
if (modifiedHomeEnd) {
|
|
233
|
+
raw[modifiedHomeEnd[2] === 'H' ? 'home' : 'end'] = true;
|
|
234
|
+
setModifiers(modifiedHomeEnd[1]);
|
|
235
|
+
}
|
|
236
|
+
// Alt+<char> gestures (ESC-prefixed printables/control letters) that the
|
|
237
|
+
// legacy parser leaves as a raw two-byte `input`. Only the raw catalog shape
|
|
238
|
+
// gains the alt modifier; `parsed` keeps its Key-based shape so custom literals
|
|
239
|
+
// still match exactly as before (Alt+<letter> must NOT match a bare literal).
|
|
240
|
+
// Special keys the legacy parser already decoded (Alt+Backspace, arrows, …)
|
|
241
|
+
// return `input: ''`, so `parsed.input === sequence` skips them here.
|
|
242
|
+
if (sequence.length === 2 && sequence.startsWith('\x1b') && parsed.input === sequence) {
|
|
243
|
+
const code = sequence.charCodeAt(1);
|
|
244
|
+
raw.input = code < 32 ? String.fromCharCode(code + 64).toLowerCase() : sequence[1];
|
|
245
|
+
raw.meta = true;
|
|
246
|
+
raw.ctrl = code < 32;
|
|
247
|
+
}
|
|
248
|
+
// Backspace family. \x7f, \b, and their Alt-prefixed forms all mean "delete one
|
|
249
|
+
// char" and canonicalize to plain backspace (there is no alt+backspace catalog
|
|
250
|
+
// action, so Alt+Backspace deletes exactly like Backspace, as before). The 0x08
|
|
251
|
+
// byte is ALSO Ctrl-H: honor a Ctrl-H remap only when an effective binding
|
|
252
|
+
// actually claims `ctrl+h`, otherwise it stays backspace so default deletion
|
|
253
|
+
// keeps working on ^H-erase terminals.
|
|
254
|
+
if (raw.backspace) {
|
|
255
|
+
const ctrlHByte = sequence === '\b' || sequence === '\x1b\b';
|
|
256
|
+
if (ctrlHByte && bindings && bindingSetClaimsCtrlH(bindings)) {
|
|
257
|
+
raw.input = 'h';
|
|
258
|
+
raw.ctrl = true;
|
|
259
|
+
raw.meta = false;
|
|
260
|
+
raw.backspace = false;
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
raw.input = '';
|
|
264
|
+
raw.ctrl = false;
|
|
265
|
+
raw.meta = false;
|
|
266
|
+
raw.backspace = true;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return { ...parsed, raw };
|
|
270
|
+
}
|
|
191
271
|
function keyTokens(input, key) {
|
|
192
272
|
const t = [];
|
|
193
273
|
if (key.upArrow)
|
|
@@ -228,21 +308,53 @@ function isPrintable(input, key) {
|
|
|
228
308
|
function sanitizePrintable(s) {
|
|
229
309
|
return Array.from(s).filter((ch) => ch.charCodeAt(0) >= 32).join('');
|
|
230
310
|
}
|
|
311
|
+
/** Resolve footer hints for the CURRENT state: only bindings whose `when(state)`
|
|
312
|
+
* passes contribute, so mode-gated affordances (compose/react send/cancel/pick)
|
|
313
|
+
* appear exactly while active. Catalog bindings render the effective key label
|
|
314
|
+
* (omitted entirely when disabled); custom-view literal hints stay verbatim. */
|
|
315
|
+
export function resolveViewKeyHints(keymap, bindings, state) {
|
|
316
|
+
const hints = [];
|
|
317
|
+
for (const binding of keymap) {
|
|
318
|
+
if (!binding.hint)
|
|
319
|
+
continue;
|
|
320
|
+
if (binding.when && !binding.when(state))
|
|
321
|
+
continue;
|
|
322
|
+
if ('bindingId' in binding) {
|
|
323
|
+
if (bindings.gestures(binding.bindingId).length === 0)
|
|
324
|
+
continue; // disabled: no hint
|
|
325
|
+
hints.push({ keys: formatBinding(bindings, binding.bindingId), label: binding.hint.label });
|
|
326
|
+
}
|
|
327
|
+
else if ('capture' in binding) {
|
|
328
|
+
hints.push('keys' in binding.hint ? binding.hint : { keys: 'type', label: binding.hint.label });
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
hints.push(binding.hint);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return hints;
|
|
335
|
+
}
|
|
231
336
|
/** The first non-capture binding whose key matches and whose `when` (if any)
|
|
232
|
-
*
|
|
233
|
-
|
|
337
|
+
* passes — catalog IDs use the terminal matcher; custom literals retain their
|
|
338
|
+
* presenter-authored matching. */
|
|
339
|
+
export function matchViewBinding(keymap, bindings, input, key, state, raw = { ...key, input }) {
|
|
234
340
|
const tokens = keyTokens(input, key);
|
|
235
|
-
for (const
|
|
236
|
-
if ('capture' in
|
|
341
|
+
for (const binding of keymap) {
|
|
342
|
+
if ('capture' in binding)
|
|
237
343
|
continue;
|
|
238
|
-
if (
|
|
344
|
+
if (binding.when && !binding.when(state))
|
|
239
345
|
continue;
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
346
|
+
const matches = 'bindingId' in binding
|
|
347
|
+
? matchesTerminalInput(bindings, binding.bindingId, raw)
|
|
348
|
+
: binding.keys.some((literal) => tokens.includes(literal));
|
|
349
|
+
if (matches)
|
|
350
|
+
return { intent: binding.intent, payload: binding.payload ? binding.payload(state) : undefined };
|
|
243
351
|
}
|
|
244
352
|
return null;
|
|
245
353
|
}
|
|
354
|
+
/** Match a host-owned generic-view action through the same catalog snapshot. */
|
|
355
|
+
export function matchesViewHostBinding(bindings, id, raw) {
|
|
356
|
+
return matchesTerminalInput(bindings, id, raw);
|
|
357
|
+
}
|
|
246
358
|
/** The active text-capture binding for this state, if any (compose-mode entry). */
|
|
247
359
|
function activeCapture(keymap, state) {
|
|
248
360
|
for (const b of keymap) {
|
|
@@ -271,9 +383,13 @@ async function runRequest(transport, src, args) {
|
|
|
271
383
|
* (or Ctrl-C). `text` is the optional text presenter for the piped path. */
|
|
272
384
|
export async function runCoreView(core, tui, text, opts = {}) {
|
|
273
385
|
const options = Object.freeze({ ...(opts.options ?? {}) });
|
|
386
|
+
const bindings = resolveUserKeybindings();
|
|
387
|
+
const keybindingNotice = summarizeKeybindingDiagnostics(bindings.diagnostics);
|
|
274
388
|
const transport = createLocalTransport({ cwd: process.cwd() });
|
|
275
389
|
// ── Non-TTY / piped path: init, best-effort refresh, dump, exit 0. ──
|
|
276
390
|
if (!process.stdin.isTTY) {
|
|
391
|
+
if (keybindingNotice)
|
|
392
|
+
process.stderr.write(`${keybindingNotice}\n`);
|
|
277
393
|
let dstate = core.init(options);
|
|
278
394
|
const dchrome = initialChrome();
|
|
279
395
|
const dsignal = {
|
|
@@ -307,13 +423,9 @@ export async function runCoreView(core, tui, text, opts = {}) {
|
|
|
307
423
|
let state = core.init(options);
|
|
308
424
|
const chrome = initialChrome();
|
|
309
425
|
let tick = 0;
|
|
310
|
-
//
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
for (const b of tui.keymap) {
|
|
314
|
-
if (b.hint)
|
|
315
|
-
hints.push(b.hint);
|
|
316
|
-
}
|
|
426
|
+
// Report rejected hand-edited overrides once, as a chrome notice.
|
|
427
|
+
if (keybindingNotice)
|
|
428
|
+
chrome.banner = { msg: keybindingNotice, level: 'action' };
|
|
317
429
|
// Restore the terminal exactly once, however we leave.
|
|
318
430
|
let restored = false;
|
|
319
431
|
const cleanup = () => {
|
|
@@ -329,7 +441,9 @@ export async function runCoreView(core, tui, text, opts = {}) {
|
|
|
329
441
|
const render = () => {
|
|
330
442
|
const size = getTerminalSize();
|
|
331
443
|
const { draw, frame } = createDraw(size, caps);
|
|
332
|
-
|
|
444
|
+
// Footer hints are re-derived per draw so they track the active mode
|
|
445
|
+
// (compose/react affordances appear only while their `when(state)` holds).
|
|
446
|
+
const chromeManifest = { ...core.manifest, keymap: resolveViewKeyHints(tui.keymap, bindings, state) };
|
|
333
447
|
const tickChrome = { ...chrome, tick };
|
|
334
448
|
const content = drawChrome(draw, size, chromeManifest, tickChrome);
|
|
335
449
|
try {
|
|
@@ -525,14 +639,14 @@ export async function runCoreView(core, tui, text, opts = {}) {
|
|
|
525
639
|
return;
|
|
526
640
|
let parsed;
|
|
527
641
|
try {
|
|
528
|
-
parsed =
|
|
642
|
+
parsed = parseViewKeypress(data, bindings);
|
|
529
643
|
}
|
|
530
644
|
catch {
|
|
531
645
|
return;
|
|
532
646
|
}
|
|
533
|
-
const { input, key } = parsed;
|
|
534
|
-
//
|
|
535
|
-
if (
|
|
647
|
+
const { input, key, raw } = parsed;
|
|
648
|
+
// The catalog-backed escape hatch works even mid-flight.
|
|
649
|
+
if (matchesViewHostBinding(bindings, 'crtr.view.host.force-quit', raw)) {
|
|
536
650
|
finish();
|
|
537
651
|
return;
|
|
538
652
|
}
|
|
@@ -544,13 +658,13 @@ export async function runCoreView(core, tui, text, opts = {}) {
|
|
|
544
658
|
// fall through to the keymap so submit/cancel can be bound.
|
|
545
659
|
const cap = activeCapture(tui.keymap, state);
|
|
546
660
|
if (cap) {
|
|
547
|
-
if (
|
|
548
|
-
captureBuf
|
|
661
|
+
if (matchesViewHostBinding(bindings, 'crtr.view.host.editor.backspace', raw)) {
|
|
662
|
+
captureBuf = captureBuf.slice(0, -1);
|
|
549
663
|
await enqueueDispatch(cap.capture, captureBuf);
|
|
550
664
|
return;
|
|
551
665
|
}
|
|
552
|
-
if (key
|
|
553
|
-
captureBuf
|
|
666
|
+
if (isPrintable(input, key)) {
|
|
667
|
+
captureBuf += sanitizePrintable(input);
|
|
554
668
|
await enqueueDispatch(cap.capture, captureBuf);
|
|
555
669
|
return;
|
|
556
670
|
}
|
|
@@ -558,7 +672,7 @@ export async function runCoreView(core, tui, text, opts = {}) {
|
|
|
558
672
|
else {
|
|
559
673
|
captureBuf = '';
|
|
560
674
|
}
|
|
561
|
-
const m =
|
|
675
|
+
const m = matchViewBinding(tui.keymap, bindings, input, key, state, raw);
|
|
562
676
|
if (m)
|
|
563
677
|
await enqueueDispatch(m.intent, m.payload);
|
|
564
678
|
};
|
|
@@ -154,7 +154,20 @@ export interface KeyHint {
|
|
|
154
154
|
keys: string;
|
|
155
155
|
label: string;
|
|
156
156
|
}
|
|
157
|
+
/** A catalog binding supplies only its semantic hint label: the host derives
|
|
158
|
+
* the rendered key label from the effective user binding snapshot. */
|
|
159
|
+
export interface CatalogKeyHint {
|
|
160
|
+
label: string;
|
|
161
|
+
}
|
|
157
162
|
export type KeyBinding<S> = {
|
|
163
|
+
/** Closed crouter catalog action. Shipped presenters use this form. */
|
|
164
|
+
bindingId: import('../keybindings/index.js').BindingId;
|
|
165
|
+
intent: string;
|
|
166
|
+
payload?: (state: S) => unknown;
|
|
167
|
+
when?: (state: S) => boolean;
|
|
168
|
+
hint?: CatalogKeyHint;
|
|
169
|
+
} | {
|
|
170
|
+
/** Literal keys remain owned by custom/plugin views, outside crouter's catalog. */
|
|
158
171
|
keys: string[];
|
|
159
172
|
intent: string;
|
|
160
173
|
payload?: (state: S) => unknown;
|
|
@@ -162,11 +175,12 @@ export type KeyBinding<S> = {
|
|
|
162
175
|
hint?: KeyHint;
|
|
163
176
|
} | {
|
|
164
177
|
/** Text-capture binding: while `when(state)` is true the host runs a
|
|
165
|
-
* built-in line-edit buffer over raw printable
|
|
166
|
-
*
|
|
178
|
+
* built-in line-edit buffer over raw printable keys and dispatches
|
|
179
|
+
* `capture` with the next draft value on each edit. Deletion is the
|
|
180
|
+
* host catalog action `crtr.view.host.editor.backspace`. */
|
|
167
181
|
capture: string;
|
|
168
182
|
when: (state: S) => boolean;
|
|
169
|
-
hint?: KeyHint;
|
|
183
|
+
hint?: KeyHint | CatalogKeyHint;
|
|
170
184
|
};
|
|
171
185
|
export interface TuiPresenter<S = unknown> {
|
|
172
186
|
/** Pure read of state, paints via draw.*; never ANSI. */
|