@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.
- package/README.md +1 -1
- 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-pi-packages/pi-crtr-extensions/README.md +13 -34
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts +639 -741
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +241 -58
- 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 +365 -94
- 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/cli.d.ts +1 -1
- package/dist/cli.js +1 -0
- package/dist/clients/attach/__tests__/attach-chrome-remote.test.js +11 -7
- 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 +20 -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 +1 -1
- package/dist/clients/attach/attach-cmd.js +733 -730
- package/dist/clients/attach/auth-pickers.d.ts +12 -0
- package/dist/clients/attach/auth-pickers.js +11 -0
- 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/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 +13 -17
- package/dist/clients/attach/slash-commands.d.ts +9 -0
- package/dist/clients/attach/slash-commands.js +128 -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/memory/lint.js +39 -5
- package/dist/commands/memory/write.js +1 -0
- package/dist/commands/node.js +12 -5
- package/dist/commands/profile/new.js +30 -5
- 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/config.js +18 -21
- package/dist/commands/sys/setup-core.d.ts +12 -1
- package/dist/commands/sys/setup-core.js +25 -3
- package/dist/commands/sys/setup.js +166 -20
- 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__/fault-classifier.test.js +15 -0
- package/dist/core/__tests__/full/broker-dialogs.test.js +7 -2
- package/dist/core/__tests__/full/broker-pane-resolution.test.js +9 -6
- package/dist/core/__tests__/full/cascade-close.test.js +5 -2
- package/dist/core/__tests__/full/dead-pane-regression.test.js +6 -3
- package/dist/core/__tests__/full/detach-focus.test.js +10 -5
- package/dist/core/__tests__/full/human-new-window-regression.test.js +6 -3
- package/dist/core/__tests__/full/review-render-pane-regression.test.js +2 -2
- package/dist/core/__tests__/helpers/harness.d.ts +1 -0
- package/dist/core/__tests__/helpers/harness.js +28 -4
- package/dist/core/__tests__/host-teardown-process-group.test.js +15 -4
- package/dist/core/__tests__/live-mutation-verbs.test.js +5 -4
- package/dist/core/__tests__/review-model-floor.test.js +1 -1
- 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/__tests__/remote-event-stream.test.js +11 -9
- package/dist/core/canvas/__tests__/render-remote.test.js +7 -4
- package/dist/core/canvas/browse/__tests__/model.test.js +23 -5
- package/dist/core/canvas/browse/__tests__/rebuild-coalescer.test.js +30 -13
- package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
- package/dist/core/canvas/browse/app.js +164 -5
- 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/canvas/pid.d.ts +14 -10
- package/dist/core/canvas/pid.js +14 -10
- package/dist/core/config.d.ts +53 -2
- 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__/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 +256 -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/runtime/broker-protocol.d.ts +33 -5
- package/dist/core/runtime/broker.js +165 -14
- package/dist/core/runtime/launch.d.ts +39 -6
- package/dist/core/runtime/launch.js +79 -21
- 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/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 +47 -44
- package/dist/core/runtime/recycle.js +6 -1
- package/dist/core/runtime/reset.js +5 -0
- package/dist/core/runtime/revive.js +28 -18
- package/dist/core/runtime/session-list-cache.d.ts +23 -0
- package/dist/core/runtime/session-list-cache.js +270 -0
- package/dist/core/runtime/spawn.js +51 -32
- 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 +216 -110
- 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/__tests__/transport-remote.test.js +9 -6
- package/dist/core/view/contract.d.ts +17 -3
- package/dist/daemon/crtrd-cli.d.ts +1 -1
- package/dist/daemon/crtrd-cli.js +1 -0
- 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 +38 -37
- package/dist/pi-extensions/canvas-stophook.d.ts +11 -0
- package/dist/pi-extensions/canvas-stophook.js +33 -4
- package/dist/suppress-experimental-warnings.d.ts +1 -0
- package/dist/suppress-experimental-warnings.js +15 -0
- package/dist/types.d.ts +3 -13
- package/dist/types.js +12 -36
- package/dist/web-client/assets/{index-CnF5r8ky.js → index--md2ylfi.js} +18 -17
- package/dist/web-client/assets/index-BUdm9s9s.css +2 -0
- package/dist/web-client/index.html +3 -3
- package/dist/web-client/sw.js +1 -1
- package/package.json +11 -8
- package/scripts/postinstall.mjs +1 -1
- 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
- package/dist/web-client/assets/index-BnmSLNLa.css +0 -2
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
// root) or switch-client + select-window (across roots). done/dead nodes close
|
|
9
9
|
// their window; reviving opens a fresh one.
|
|
10
10
|
import { spawnSync } from 'node:child_process';
|
|
11
|
-
import {
|
|
12
|
-
import { defaultCanvasNavConfig } from '../../types.js';
|
|
11
|
+
import { resolveUserKeybindings, } from '../keybindings/index.js';
|
|
13
12
|
import { bundledPiCommandForShell } from './pi-cli.js';
|
|
14
13
|
import { surfaceTmuxStyleArgs } from './surface-bg.js';
|
|
15
14
|
// ---------------------------------------------------------------------------
|
|
@@ -72,13 +71,22 @@ function ensureBracketedPaste() {
|
|
|
72
71
|
// broken `Enbp=E[?2004h` (no ESC) and silently disables bracketed paste.
|
|
73
72
|
tmux(['set-option', '-sga', 'terminal-overrides', '*:Enbp=\\E[?2004h:Dsbp=\\E[?2004l']);
|
|
74
73
|
}
|
|
74
|
+
/** Server-level tmux prerequisites for crtr's viewer surfaces, applied once a
|
|
75
|
+
* server exists (callers invoke via ensureSession). Currently: bracketed paste
|
|
76
|
+
* capability (above) and `focus-events on`, which forwards terminal focus
|
|
77
|
+
* in/out (DECSET 1004) to the program in each pane — the attach viewer uses it
|
|
78
|
+
* to hollow out the editor cursor in blurred panes. */
|
|
79
|
+
function ensureServerOptions() {
|
|
80
|
+
ensureBracketedPaste();
|
|
81
|
+
tmux(['set-option', '-s', 'focus-events', 'on']);
|
|
82
|
+
}
|
|
75
83
|
/** Create a detached session rooted at `cwd` if it doesn't exist. The session
|
|
76
84
|
* name is a root's tmux home; every node under that root is a window in it. */
|
|
77
85
|
export function ensureSession(name, cwd) {
|
|
78
86
|
if (!sessionExists(name)) {
|
|
79
87
|
tmux(['new-session', '-d', '-s', name, '-c', cwd]);
|
|
80
88
|
}
|
|
81
|
-
|
|
89
|
+
ensureServerOptions();
|
|
82
90
|
}
|
|
83
91
|
function envFlags(env) {
|
|
84
92
|
const out = [];
|
|
@@ -400,135 +408,233 @@ export function sendKeysEnter(pane, text) {
|
|
|
400
408
|
return tmux(['send-keys', '-t', pane, text, 'Enter']).ok;
|
|
401
409
|
}
|
|
402
410
|
// ---------------------------------------------------------------------------
|
|
403
|
-
//
|
|
404
|
-
// actions. Installed on the running server at root boot; idempotent (a re-bind
|
|
405
|
-
// overwrites the previous one). Menu entries are data-driven: tmux renders the
|
|
406
|
-
// action tree, and every `crtr` / `popup` action routes through `crtr canvas
|
|
407
|
-
// chord` so node-relative vars resolve in one safe place.
|
|
411
|
+
// Crouter-owned tmux bindings
|
|
408
412
|
// ---------------------------------------------------------------------------
|
|
413
|
+
const TMUX_BINDING_MANIFEST = '@crtr_keybindings_manifest';
|
|
414
|
+
const ROOT_TABLE = 'root';
|
|
415
|
+
const MENU_ACTIONS = {
|
|
416
|
+
'crtr.tmux.menu.promote': { description: 'promote to orchestrator', action: { kind: 'keys', keys: '/promote' } },
|
|
417
|
+
'crtr.tmux.menu.resume': { description: 'resume node', action: { kind: 'keys', keys: '/resume-node' } },
|
|
418
|
+
'crtr.tmux.menu.demote': { description: 'demote to terminal', action: { kind: 'command', run: 'node lifecycle demote --pane {pane}' } },
|
|
419
|
+
'crtr.tmux.menu.detach': { description: 'detach to background', action: { kind: 'command', run: 'node lifecycle demote --pane {pane} --detach' } },
|
|
420
|
+
'crtr.tmux.menu.close-subtree': { description: 'close agent + subtree', action: { kind: 'command', run: 'node lifecycle close --pane {pane}' } },
|
|
421
|
+
'crtr.tmux.menu.context': { description: 'browse context dirs', action: { kind: 'keys', keys: '/context' } },
|
|
422
|
+
'crtr.tmux.menu.graph': { description: 'graph view', action: { kind: 'keys', keys: '/graph' } },
|
|
423
|
+
'crtr.tmux.menu.focus-manager': { description: 'focus manager', action: { kind: 'command', run: 'node focus {manager}' } },
|
|
424
|
+
'crtr.tmux.menu.open': undefined,
|
|
425
|
+
'crtr.tmux.menu.issues': undefined,
|
|
426
|
+
'crtr.tmux.menu.issue.idea': undefined,
|
|
427
|
+
'crtr.tmux.menu.issue.bug': undefined,
|
|
428
|
+
'crtr.tmux.node.next': undefined,
|
|
429
|
+
'crtr.tmux.node.previous': undefined,
|
|
430
|
+
'crtr.tmux.view.next': undefined,
|
|
431
|
+
'crtr.tmux.view.previous': undefined,
|
|
432
|
+
};
|
|
409
433
|
function escapeTmuxFormats(value) {
|
|
410
434
|
return value.replace(/#/g, '##');
|
|
411
435
|
}
|
|
412
|
-
function
|
|
413
|
-
return `run-shell ${shellQuote(shellCmd)}`;
|
|
414
|
-
}
|
|
415
|
-
function tmuxCmd(args) {
|
|
436
|
+
function tmuxCommand(args) {
|
|
416
437
|
return args.map(shellQuote).join(' ');
|
|
417
438
|
}
|
|
418
|
-
function
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
}
|
|
424
|
-
function
|
|
425
|
-
|
|
439
|
+
function chordCommand(run, interactive = false) {
|
|
440
|
+
const command = `crtr canvas chord --pane '#{pane_id}' --run ${shellQuote(escapeTmuxFormats(run))}${interactive ? ' --interactive' : ''}`;
|
|
441
|
+
return interactive
|
|
442
|
+
? `display-popup -E -w 60% -h 50% -d "#{pane_current_path}" ${shellQuote(command)}`
|
|
443
|
+
: `run-shell ${shellQuote(`${command} >/dev/null 2>&1`)}`;
|
|
444
|
+
}
|
|
445
|
+
function renderMenuAction(action) {
|
|
446
|
+
switch (action.kind) {
|
|
447
|
+
case 'keys': return `send-keys -t '#{pane_id}' ${shellQuote(escapeTmuxFormats(action.keys))} Enter`;
|
|
448
|
+
case 'command': return chordCommand(action.run);
|
|
449
|
+
case 'popup': return chordCommand(action.run, true);
|
|
450
|
+
case 'submenu': return tmuxCommand(buildMenuArgs(action.title, action.items));
|
|
451
|
+
}
|
|
426
452
|
}
|
|
427
453
|
function buildMenuArgs(title, items) {
|
|
428
454
|
const safeTitle = escapeTmuxFormats(title);
|
|
455
|
+
const widestItem = items.reduce((width, item) => Math.max(width, escapeTmuxFormats(item.description).length), 0);
|
|
429
456
|
const args = ['display-menu', '-T', `#[align=centre]${safeTitle}`, ...surfaceTmuxStyleArgs()];
|
|
430
|
-
|
|
431
|
-
const nudgeX = 1;
|
|
432
|
-
const nudgeY = 3;
|
|
433
|
-
args.push('-x', `#{e|-:#{pane_right},${boxW + nudgeX}}`, '-y', `#{e|+:#{pane_top},${nudgeY}}`);
|
|
457
|
+
args.push('-x', `#{e|-:#{pane_right},${Math.max(safeTitle.length, widestItem) + 7}}`, '-y', '#{e|+:#{pane_top},3}');
|
|
434
458
|
for (const item of items) {
|
|
435
|
-
args.push(escapeTmuxFormats(item.
|
|
459
|
+
args.push(escapeTmuxFormats(item.description), item.selector, renderMenuAction(item.action));
|
|
436
460
|
}
|
|
437
461
|
args.push('-esc / q to close', '', '');
|
|
438
462
|
return args;
|
|
439
463
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
464
|
+
const TMUX_NAMED_KEYS = {
|
|
465
|
+
enter: 'Enter', escape: 'Escape', tab: 'Tab', backspace: 'BSpace', delete: 'DC',
|
|
466
|
+
space: 'Space', up: 'Up', down: 'Down', left: 'Left', right: 'Right',
|
|
467
|
+
pageup: 'PPage', pagedown: 'NPage', home: 'Home', end: 'End',
|
|
468
|
+
};
|
|
469
|
+
function tmuxKey(stroke) {
|
|
470
|
+
const parts = stroke.split('+');
|
|
471
|
+
let key = parts.pop();
|
|
472
|
+
const modifiers = new Set(parts);
|
|
473
|
+
if (modifiers.has('shift') && key.length === 1 && /[a-z]/.test(key)) {
|
|
474
|
+
key = key.toUpperCase();
|
|
475
|
+
modifiers.delete('shift');
|
|
476
|
+
}
|
|
477
|
+
key = TMUX_NAMED_KEYS[key] ?? key;
|
|
478
|
+
const prefix = [
|
|
479
|
+
modifiers.has('ctrl') ? 'C-' : '',
|
|
480
|
+
modifiers.has('alt') ? 'M-' : '',
|
|
481
|
+
modifiers.has('shift') ? 'S-' : '',
|
|
482
|
+
].join('');
|
|
483
|
+
return `${prefix}${key}`;
|
|
484
|
+
}
|
|
485
|
+
function menuSelector(stroke) {
|
|
486
|
+
const parts = stroke.split('+');
|
|
487
|
+
const key = parts.at(-1);
|
|
488
|
+
return parts.includes('shift') && /[a-z]/.test(key) ? key.toUpperCase() : key === 'space' ? ' ' : key;
|
|
489
|
+
}
|
|
490
|
+
function menuItems(bindings) {
|
|
491
|
+
const issueItems = [];
|
|
492
|
+
for (const [id, description, type] of [
|
|
493
|
+
['crtr.tmux.menu.issue.idea', 'idea', 'idea'],
|
|
494
|
+
['crtr.tmux.menu.issue.bug', 'bug', 'bug'],
|
|
495
|
+
]) {
|
|
496
|
+
for (const gesture of bindings.gestures(id)) {
|
|
497
|
+
issueItems.push({ description, selector: menuSelector(gesture), action: { kind: 'popup', run: `canvas issue new --type ${type}` } });
|
|
498
|
+
}
|
|
453
499
|
}
|
|
500
|
+
const items = [];
|
|
501
|
+
for (const id of [
|
|
502
|
+
'crtr.tmux.menu.promote', 'crtr.tmux.menu.resume', 'crtr.tmux.menu.demote',
|
|
503
|
+
'crtr.tmux.menu.detach', 'crtr.tmux.menu.close-subtree', 'crtr.tmux.menu.context',
|
|
504
|
+
'crtr.tmux.menu.graph', 'crtr.tmux.menu.focus-manager', 'crtr.tmux.menu.issues',
|
|
505
|
+
]) {
|
|
506
|
+
const entry = id === 'crtr.tmux.menu.issues'
|
|
507
|
+
? { description: 'new issue / todo', action: { kind: 'submenu', title: 'new issue / todo', items: issueItems } }
|
|
508
|
+
: MENU_ACTIONS[id];
|
|
509
|
+
if (id === 'crtr.tmux.menu.issues' && issueItems.length === 0)
|
|
510
|
+
continue;
|
|
511
|
+
for (const gesture of bindings.gestures(id)) {
|
|
512
|
+
items.push({ description: entry.description, selector: menuSelector(gesture), action: entry.action });
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return items;
|
|
516
|
+
}
|
|
517
|
+
function bindingAt(table, key) {
|
|
518
|
+
const result = tmux(['list-keys', '-T', table]);
|
|
519
|
+
if (!result.ok)
|
|
520
|
+
return null;
|
|
521
|
+
return result.stdout.split('\n').find((line) => {
|
|
522
|
+
const match = line.match(/^bind-key\s+(?:-r\s+)?-T\s+\S+\s+(\S+)\s+/);
|
|
523
|
+
return match?.[1] === key;
|
|
524
|
+
}) ?? null;
|
|
454
525
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
526
|
+
function sequenceTable(prefix) {
|
|
527
|
+
return `crtr-${Buffer.from(prefix).toString('hex')}`;
|
|
528
|
+
}
|
|
529
|
+
function readManifest() {
|
|
530
|
+
const option = tmux(['show-options', '-s', '-q', TMUX_BINDING_MANIFEST]);
|
|
531
|
+
if (!option.ok)
|
|
532
|
+
return null;
|
|
533
|
+
if (option.stdout === '')
|
|
534
|
+
return { pairs: [], existed: false };
|
|
535
|
+
const result = tmux(['show-options', '-s', '-qv', TMUX_BINDING_MANIFEST]);
|
|
536
|
+
if (!result.ok)
|
|
537
|
+
return null;
|
|
458
538
|
try {
|
|
459
|
-
|
|
539
|
+
const value = JSON.parse(result.stdout);
|
|
540
|
+
if (!Array.isArray(value) || value.some((pair) => typeof pair !== 'object' || pair === null || typeof pair.table !== 'string' || typeof pair.key !== 'string'))
|
|
541
|
+
return null;
|
|
542
|
+
return { pairs: value, existed: true };
|
|
460
543
|
}
|
|
461
544
|
catch {
|
|
462
|
-
|
|
545
|
+
return null;
|
|
463
546
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
547
|
+
}
|
|
548
|
+
const LEGACY_ROOT_BINDINGS = [
|
|
549
|
+
{ key: 'M-c', markers: ['display-menu', 'promote to orchestrator', 'crtr canvas chord', 'new issue / todo'] },
|
|
550
|
+
{ key: 'M-]', markers: ['run-shell', 'crtr node cycle --dir next', "--pane '#{pane_id}'"] },
|
|
551
|
+
{ key: 'M-[', markers: ['run-shell', 'crtr node cycle --dir prev', "--pane '#{pane_id}'"] },
|
|
552
|
+
{ key: 'M-v', markers: ['switch-client', '-T crtr-view'] },
|
|
553
|
+
];
|
|
554
|
+
function adoptLegacyBindings() {
|
|
555
|
+
for (const legacy of LEGACY_ROOT_BINDINGS) {
|
|
556
|
+
const existing = bindingAt(ROOT_TABLE, legacy.key);
|
|
557
|
+
if (existing !== null && legacy.markers.every((marker) => existing.includes(marker))) {
|
|
558
|
+
tmux(['unbind-key', '-T', ROOT_TABLE, legacy.key]);
|
|
559
|
+
}
|
|
470
560
|
}
|
|
471
|
-
return tmux(['bind-key', '-n', 'M-c', ...buildMenuArgs(' crtr ', items)]).ok;
|
|
472
561
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
* false if either bind fails. NOTE: Alt+[ is only delivered cleanly when the
|
|
482
|
-
* terminal/tmux disambiguate it from a raw CSI introducer (`extended-keys on`).
|
|
483
|
-
*/
|
|
484
|
-
export function installNavBindings() {
|
|
485
|
-
const next = tmux([
|
|
486
|
-
'bind-key', '-n', 'M-]', 'run-shell',
|
|
487
|
-
`crtr node cycle --dir next --pane '#{pane_id}' >/dev/null 2>&1`,
|
|
488
|
-
]).ok;
|
|
489
|
-
const prev = tmux([
|
|
490
|
-
'bind-key', '-n', 'M-[', 'run-shell',
|
|
491
|
-
`crtr node cycle --dir prev --pane '#{pane_id}' >/dev/null 2>&1`,
|
|
492
|
-
]).ok;
|
|
493
|
-
return next && prev;
|
|
562
|
+
function actionCommand(id) {
|
|
563
|
+
switch (id) {
|
|
564
|
+
case 'crtr.tmux.node.next': return ['run-shell', `crtr node cycle --dir next --pane '#{pane_id}' >/dev/null 2>&1`];
|
|
565
|
+
case 'crtr.tmux.node.previous': return ['run-shell', `crtr node cycle --dir prev --pane '#{pane_id}' >/dev/null 2>&1`];
|
|
566
|
+
case 'crtr.tmux.view.next': return ['run-shell', `crtr surface view cycle --dir next --pane '#{pane_id}' >/dev/null 2>&1`];
|
|
567
|
+
case 'crtr.tmux.view.previous': return ['run-shell', `crtr surface view cycle --dir prev --pane '#{pane_id}' >/dev/null 2>&1`];
|
|
568
|
+
default: throw new Error(`no tmux command for ${id}`);
|
|
569
|
+
}
|
|
494
570
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
const
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
])
|
|
533
|
-
|
|
571
|
+
/** Reconcile the tmux server with one immutable crouter binding snapshot. */
|
|
572
|
+
export function installTmuxBindings(bindings = resolveUserKeybindings()) {
|
|
573
|
+
const diagnostics = [];
|
|
574
|
+
const installed = [];
|
|
575
|
+
const manifest = readManifest();
|
|
576
|
+
if (manifest === null) {
|
|
577
|
+
diagnostics.push({ code: 'tmux-error', message: `tmux is unavailable or ${TMUX_BINDING_MANIFEST} is malformed` });
|
|
578
|
+
return { ok: false, installed, diagnostics };
|
|
579
|
+
}
|
|
580
|
+
if (!manifest.existed)
|
|
581
|
+
adoptLegacyBindings();
|
|
582
|
+
for (const pair of manifest.pairs) {
|
|
583
|
+
const result = tmux(['unbind-key', '-T', pair.table, pair.key]);
|
|
584
|
+
if (!result.ok && bindingAt(pair.table, pair.key) !== null) {
|
|
585
|
+
diagnostics.push({ code: 'tmux-error', message: `could not remove crouter binding ${pair.table}:${pair.key}: ${result.stderr}` });
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
if (diagnostics.some((diagnostic) => diagnostic.code === 'tmux-error')) {
|
|
589
|
+
return { ok: false, installed, diagnostics };
|
|
590
|
+
}
|
|
591
|
+
const install = (table, key, command, id, gesture) => {
|
|
592
|
+
if (bindingAt(table, key) !== null) {
|
|
593
|
+
diagnostics.push({ code: 'occupied', bindingId: id, gesture, message: `${table}:${key} is already bound by tmux; ${id} was not installed` });
|
|
594
|
+
return false;
|
|
595
|
+
}
|
|
596
|
+
const result = tmux(['bind-key', '-T', table, key, ...command]);
|
|
597
|
+
if (!result.ok) {
|
|
598
|
+
diagnostics.push({ code: 'tmux-error', bindingId: id, gesture, message: `could not install ${id}: ${result.stderr}` });
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
installed.push({ table, key });
|
|
602
|
+
return true;
|
|
603
|
+
};
|
|
604
|
+
const menuCommand = buildMenuArgs(' crtr ', menuItems(bindings));
|
|
605
|
+
for (const gesture of bindings.gestures('crtr.tmux.menu.open')) {
|
|
606
|
+
install(ROOT_TABLE, tmuxKey(gesture), menuCommand, 'crtr.tmux.menu.open', gesture);
|
|
607
|
+
}
|
|
608
|
+
for (const id of ['crtr.tmux.node.next', 'crtr.tmux.node.previous']) {
|
|
609
|
+
for (const gesture of bindings.gestures(id))
|
|
610
|
+
install(ROOT_TABLE, tmuxKey(gesture), actionCommand(id), id, gesture);
|
|
611
|
+
}
|
|
612
|
+
const sequences = new Map();
|
|
613
|
+
for (const id of ['crtr.tmux.view.next', 'crtr.tmux.view.previous']) {
|
|
614
|
+
for (const gesture of bindings.gestures(id)) {
|
|
615
|
+
const [first, second] = gesture.split(' > ');
|
|
616
|
+
const prefix = tmuxKey(first);
|
|
617
|
+
const entries = sequences.get(prefix) ?? [];
|
|
618
|
+
entries.push({ id, gesture, second: tmuxKey(second) });
|
|
619
|
+
sequences.set(prefix, entries);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
for (const [prefix, entries] of sequences) {
|
|
623
|
+
if (bindingAt(ROOT_TABLE, prefix) !== null) {
|
|
624
|
+
for (const entry of entries)
|
|
625
|
+
diagnostics.push({ code: 'occupied', bindingId: entry.id, gesture: entry.gesture, message: `${ROOT_TABLE}:${prefix} is already bound by tmux; ${entry.id} was not installed` });
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
const table = sequenceTable(prefix);
|
|
629
|
+
let children = 0;
|
|
630
|
+
for (const entry of entries)
|
|
631
|
+
if (install(table, entry.second, actionCommand(entry.id), entry.id, entry.gesture))
|
|
632
|
+
children++;
|
|
633
|
+
if (children > 0)
|
|
634
|
+
install(ROOT_TABLE, prefix, ['switch-client', '-T', table], entries[0].id, entries[0].gesture);
|
|
635
|
+
}
|
|
636
|
+
const save = tmux(['set-option', '-s', TMUX_BINDING_MANIFEST, JSON.stringify(installed)]);
|
|
637
|
+
if (!save.ok)
|
|
638
|
+
diagnostics.push({ code: 'tmux-error', message: `could not save crouter tmux ownership manifest: ${save.stderr}` });
|
|
639
|
+
return { ok: diagnostics.every((diagnostic) => diagnostic.code !== 'tmux-error'), installed, diagnostics };
|
|
534
640
|
}
|
|
@@ -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>;
|