@crouton-kit/crouter 0.3.17 → 0.3.19
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/dist/build-root.js +6 -0
- package/dist/builtin-memory/crouter-development/marketplaces.md +164 -0
- package/dist/builtin-memory/crouter-development/personas/base-prompt.md +58 -0
- package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +60 -0
- package/dist/builtin-memory/crouter-development/personas.md +117 -0
- package/dist/builtin-memory/crouter-development/plugins.md +163 -0
- package/dist/builtin-memory/design.md +60 -0
- package/dist/builtin-memory/development.md +116 -0
- package/dist/builtin-memory/planning.md +66 -0
- package/dist/builtin-memory/spec.md +99 -0
- package/dist/builtin-personas/design/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/design/orchestrator.md +1 -1
- package/dist/builtin-personas/developer/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/developer/orchestrator.md +1 -1
- package/dist/builtin-personas/explore/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/general/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/lifecycle/resident.md +1 -1
- package/dist/builtin-personas/lifecycle/terminal.md +5 -2
- package/dist/builtin-personas/orchestration-kernel.md +15 -24
- package/dist/builtin-personas/plan/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/plan/orchestrator.md +1 -1
- package/dist/builtin-personas/plan/reviewers/architecture-fit/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/plan/reviewers/code-smells/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/plan/reviewers/pattern-consistency/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/plan/reviewers/requirements-coverage/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/plan/reviewers/security/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/review/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/runtime-base.md +6 -3
- package/dist/builtin-personas/spec/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/spec/orchestrator.md +1 -1
- package/dist/builtin-personas/waiting.md +8 -0
- package/dist/builtin-skills/skills/crouter-development/personas/SKILL.md +24 -14
- package/dist/builtin-skills/skills/crouter-development/personas/base-prompt/SKILL.md +4 -4
- package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +1 -1
- package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +2 -2
- package/dist/builtin-views/_lib/states.mjs +161 -0
- package/dist/builtin-views/canvas/client.mjs +303 -0
- package/dist/builtin-views/canvas/view.mjs +576 -0
- package/dist/builtin-views/git-pr/client.mjs +440 -0
- package/dist/builtin-views/git-pr/view.mjs +675 -0
- package/dist/builtin-views/inbox/_lib/render.mjs +177 -0
- package/dist/builtin-views/inbox/sources/gmail.mjs +965 -0
- package/dist/builtin-views/inbox/sources/linkedin.mjs +427 -0
- package/dist/builtin-views/inbox/view.mjs +889 -0
- package/dist/builtin-views/linkedin/client.mjs +610 -0
- package/dist/builtin-views/linkedin/view.mjs +1171 -0
- package/dist/clients/attach/attach-cmd.d.ts +2 -0
- package/dist/clients/attach/attach-cmd.js +354 -0
- package/dist/clients/attach/chat-view.d.ts +77 -0
- package/dist/clients/attach/chat-view.js +450 -0
- package/dist/clients/attach/clipboard-image.d.ts +16 -0
- package/dist/clients/attach/clipboard-image.js +113 -0
- package/dist/clients/attach/config-load.d.ts +31 -0
- package/dist/clients/attach/config-load.js +113 -0
- package/dist/clients/attach/extension-dialogs.d.ts +29 -0
- package/dist/clients/attach/extension-dialogs.js +101 -0
- package/dist/clients/attach/input-controller.d.ts +54 -0
- package/dist/clients/attach/input-controller.js +204 -0
- package/dist/clients/attach/slash-commands.d.ts +36 -0
- package/dist/clients/attach/slash-commands.js +200 -0
- package/dist/clients/attach/view-socket.d.ts +48 -0
- package/dist/clients/attach/view-socket.js +126 -0
- package/dist/commands/attention.js +3 -3
- package/dist/commands/canvas-prune.js +1 -1
- package/dist/commands/daemon.js +4 -3
- package/dist/commands/human/prompts.js +4 -10
- package/dist/commands/human/queue.js +43 -8
- package/dist/commands/human/shared.d.ts +28 -1
- package/dist/commands/human/shared.js +48 -10
- package/dist/commands/memory/find.d.ts +1 -0
- package/dist/commands/memory/find.js +180 -0
- package/dist/commands/memory/lint.d.ts +1 -0
- package/dist/commands/memory/lint.js +140 -0
- package/dist/commands/memory/list.d.ts +1 -0
- package/dist/commands/memory/list.js +79 -0
- package/dist/commands/memory/read.js +103 -0
- package/dist/commands/memory/shared.d.ts +30 -0
- package/dist/commands/memory/shared.js +122 -0
- package/dist/commands/memory/write.d.ts +1 -0
- package/dist/commands/memory/write.js +85 -0
- package/dist/commands/memory.d.ts +2 -0
- package/dist/commands/memory.js +27 -0
- package/dist/commands/node.d.ts +3 -2
- package/dist/commands/node.js +660 -65
- package/dist/commands/pkg/market-manage.js +1 -1
- package/dist/commands/push.js +6 -6
- package/dist/commands/revive.js +1 -1
- package/dist/commands/skill/author.js +1 -1
- package/dist/commands/skill/shared.d.ts +1 -8
- package/dist/commands/skill/shared.js +2 -55
- package/dist/commands/skill.js +9 -14
- package/dist/commands/sys/doctor.js +1 -1
- package/dist/commands/sys/update.js +1 -1
- package/dist/commands/view-cycle.d.ts +2 -0
- package/dist/commands/view-cycle.js +125 -0
- package/dist/commands/view-list.d.ts +2 -0
- package/dist/commands/view-list.js +66 -0
- package/dist/commands/view-new.d.ts +2 -0
- package/dist/commands/view-new.js +70 -0
- package/dist/commands/view-pick.d.ts +2 -0
- package/dist/commands/view-pick.js +119 -0
- package/dist/commands/view-run.d.ts +2 -0
- package/dist/commands/view-run.js +191 -0
- package/dist/commands/view.d.ts +2 -0
- package/dist/commands/view.js +29 -0
- package/dist/core/__tests__/broker-lifecycle.test.d.ts +1 -0
- package/dist/core/__tests__/broker-lifecycle.test.js +677 -0
- package/dist/core/__tests__/broker-sdk-wiring.test.d.ts +1 -0
- package/dist/core/__tests__/broker-sdk-wiring.test.js +166 -0
- package/dist/core/__tests__/cascade-close.test.js +12 -2
- package/dist/core/__tests__/child-death-wake.test.d.ts +1 -0
- package/dist/core/__tests__/child-death-wake.test.js +245 -0
- package/dist/core/__tests__/context-intro.test.js +76 -62
- package/dist/core/__tests__/daemon-boot.test.js +14 -5
- package/dist/core/__tests__/daemon-liveness.test.js +34 -9
- package/dist/core/__tests__/detach-focus.test.d.ts +1 -0
- package/dist/core/__tests__/detach-focus.test.js +206 -0
- package/dist/core/__tests__/draw-style.test.d.ts +1 -0
- package/dist/core/__tests__/draw-style.test.js +258 -0
- package/dist/core/__tests__/fixtures/c3-custom-provider-ext.d.ts +2 -0
- package/dist/core/__tests__/fixtures/c3-custom-provider-ext.js +18 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +138 -0
- package/dist/core/__tests__/fixtures/fake-engine.js +614 -0
- package/dist/core/__tests__/fixtures/fake-pi-host.js +1 -0
- package/dist/core/__tests__/flagship-lifecycle.test.js +7 -1
- package/dist/core/__tests__/frame-decoder-perf.test.d.ts +1 -0
- package/dist/core/__tests__/frame-decoder-perf.test.js +198 -0
- package/dist/core/__tests__/helpers/harness.d.ts +9 -0
- package/dist/core/__tests__/helpers/harness.js +111 -1
- package/dist/core/__tests__/home-session.test.js +41 -8
- package/dist/core/__tests__/human-new-window-regression.test.d.ts +1 -0
- package/dist/core/__tests__/human-new-window-regression.test.js +101 -0
- package/dist/core/__tests__/human-surface-target.test.d.ts +1 -0
- package/dist/core/__tests__/human-surface-target.test.js +98 -0
- package/dist/core/__tests__/kickoff.test.js +37 -3
- package/dist/core/__tests__/live-mutation.test.js +50 -33
- package/dist/core/__tests__/memory.test.js +23 -115
- package/dist/core/__tests__/persona-subkind.test.js +18 -15
- package/dist/core/__tests__/placement-focus.test.js +5 -0
- package/dist/core/__tests__/placement-teardown.test.js +54 -11
- package/dist/core/__tests__/relaunch.test.js +4 -3
- package/dist/core/__tests__/review-render-pane-regression.test.d.ts +1 -0
- package/dist/core/__tests__/review-render-pane-regression.test.js +133 -0
- package/dist/core/__tests__/spawn-root.test.js +10 -0
- package/dist/core/__tests__/spike-harness.test.js +1 -0
- package/dist/core/__tests__/wake-bearings.test.d.ts +1 -0
- package/dist/core/__tests__/wake-bearings.test.js +156 -0
- package/dist/core/__tests__/wake-origin.test.d.ts +1 -0
- package/dist/core/__tests__/wake-origin.test.js +110 -0
- package/dist/core/bootstrap.js +1 -1
- package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
- package/dist/core/canvas/browse/app.js +24 -2
- package/dist/core/canvas/browse/model.d.ts +38 -2
- package/dist/core/canvas/browse/model.js +134 -10
- package/dist/core/canvas/browse/render.d.ts +6 -12
- package/dist/core/canvas/browse/render.js +72 -104
- package/dist/core/canvas/canvas.js +15 -22
- package/dist/core/canvas/db.js +46 -0
- package/dist/core/canvas/index.d.ts +1 -0
- package/dist/core/canvas/index.js +1 -0
- package/dist/core/canvas/paths.d.ts +4 -1
- package/dist/core/canvas/paths.js +10 -4
- package/dist/core/canvas/pid.d.ts +4 -0
- package/dist/core/canvas/pid.js +23 -0
- package/dist/core/canvas/render.d.ts +11 -2
- package/dist/core/canvas/render.js +69 -0
- package/dist/core/canvas/types.d.ts +85 -3
- package/dist/core/canvas/types.js +2 -2
- package/dist/core/canvas/wakeups.d.ts +76 -0
- package/dist/core/canvas/wakeups.js +185 -0
- package/dist/core/config.js +4 -1
- package/dist/core/frontmatter.js +37 -124
- package/dist/core/help.d.ts +6 -0
- package/dist/core/help.js +7 -0
- package/dist/core/memory-resolver.d.ts +49 -0
- package/dist/core/memory-resolver.js +141 -0
- package/dist/core/personas/index.d.ts +4 -3
- package/dist/core/personas/index.js +3 -2
- package/dist/core/personas/loader.d.ts +41 -16
- package/dist/core/personas/loader.js +133 -29
- package/dist/core/personas/resolve.d.ts +4 -4
- package/dist/core/personas/resolve.js +28 -16
- package/dist/core/predicate.d.ts +63 -0
- package/dist/core/predicate.js +189 -0
- package/dist/core/resolver.js +26 -5
- package/dist/core/runtime/bearings.d.ts +53 -12
- package/dist/core/runtime/bearings.js +132 -59
- package/dist/core/runtime/broker-cli.d.ts +1 -0
- package/dist/core/runtime/broker-cli.js +46 -0
- package/dist/core/runtime/broker-protocol.d.ts +332 -0
- package/dist/core/runtime/broker-protocol.js +153 -0
- package/dist/core/runtime/broker-sdk.d.ts +48 -0
- package/dist/core/runtime/broker-sdk.js +72 -0
- package/dist/core/runtime/broker.d.ts +55 -0
- package/dist/core/runtime/broker.js +1128 -0
- package/dist/core/runtime/close.js +35 -6
- package/dist/core/runtime/host.d.ts +53 -0
- package/dist/core/runtime/host.js +186 -0
- package/dist/core/runtime/kickoff.d.ts +2 -1
- package/dist/core/runtime/kickoff.js +91 -5
- package/dist/core/runtime/launch.d.ts +45 -2
- package/dist/core/runtime/launch.js +65 -2
- package/dist/core/runtime/lifecycle.js +23 -6
- package/dist/core/runtime/memory.d.ts +2 -42
- package/dist/core/runtime/memory.js +11 -162
- package/dist/core/runtime/nodes.d.ts +33 -0
- package/dist/core/runtime/nodes.js +59 -1
- package/dist/core/runtime/persona.js +21 -11
- package/dist/core/runtime/pi-vendored.d.ts +18 -0
- package/dist/core/runtime/pi-vendored.js +49 -0
- package/dist/core/runtime/placement.d.ts +42 -14
- package/dist/core/runtime/placement.js +228 -38
- package/dist/core/runtime/promote.d.ts +0 -6
- package/dist/core/runtime/promote.js +1 -12
- package/dist/core/runtime/{demote.d.ts → recycle.d.ts} +5 -5
- package/dist/core/runtime/{demote.js → recycle.js} +27 -11
- package/dist/core/runtime/reset.js +8 -6
- package/dist/core/runtime/revive.d.ts +2 -0
- package/dist/core/runtime/revive.js +34 -34
- package/dist/core/runtime/spawn.d.ts +19 -0
- package/dist/core/runtime/spawn.js +75 -22
- package/dist/core/runtime/stop-guard.d.ts +1 -1
- package/dist/core/runtime/stop-guard.js +6 -1
- package/dist/core/runtime/tmux-chrome.d.ts +1 -1
- package/dist/core/runtime/tmux-chrome.js +1 -1
- package/dist/core/runtime/tmux.d.ts +28 -0
- package/dist/core/runtime/tmux.js +80 -6
- package/dist/core/scope.d.ts +11 -0
- package/dist/core/scope.js +39 -0
- package/dist/core/spawn.d.ts +25 -1
- package/dist/core/spawn.js +72 -7
- package/dist/core/substrate/gate.d.ts +13 -0
- package/dist/core/substrate/gate.js +21 -0
- package/dist/core/substrate/index.d.ts +7 -0
- package/dist/core/substrate/index.js +18 -0
- package/dist/core/substrate/on-read.d.ts +14 -0
- package/dist/core/substrate/on-read.js +292 -0
- package/dist/core/substrate/render.d.ts +25 -0
- package/dist/core/substrate/render.js +256 -0
- package/dist/core/substrate/schema.d.ts +76 -0
- package/dist/core/substrate/schema.js +124 -0
- package/dist/core/substrate/session-cache.d.ts +30 -0
- package/dist/core/substrate/session-cache.js +77 -0
- package/dist/core/substrate/subject.d.ts +41 -0
- package/dist/core/substrate/subject.js +54 -0
- package/dist/core/tui/contract.d.ts +83 -0
- package/dist/core/tui/contract.js +8 -0
- package/dist/core/tui/draw.d.ts +96 -0
- package/dist/core/tui/draw.js +339 -0
- package/dist/core/tui/host.d.ts +29 -0
- package/dist/core/tui/host.js +379 -0
- package/dist/core/tui/loader.d.ts +16 -0
- package/dist/core/tui/loader.js +94 -0
- package/dist/core/{canvas/browse → tui}/terminal.js +7 -6
- package/dist/core/wake.d.ts +86 -0
- package/dist/core/wake.js +308 -0
- package/dist/daemon/crtrd.d.ts +29 -4
- package/dist/daemon/crtrd.js +662 -46
- package/dist/pi-extensions/__tests__/canvas-context-intro.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +171 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +19 -12
- package/dist/pi-extensions/canvas-commands.d.ts +3 -0
- package/dist/pi-extensions/canvas-commands.js +10 -0
- package/dist/pi-extensions/canvas-context-intro.d.ts +17 -0
- package/dist/pi-extensions/canvas-context-intro.js +55 -15
- package/dist/pi-extensions/canvas-doc-substrate.d.ts +44 -0
- package/dist/pi-extensions/canvas-doc-substrate.js +112 -0
- package/dist/pi-extensions/canvas-nav.d.ts +3 -0
- package/dist/pi-extensions/canvas-nav.js +145 -34
- package/dist/pi-extensions/canvas-stophook.js +17 -8
- package/dist/pi-extensions/canvas-view.d.ts +21 -0
- package/dist/pi-extensions/canvas-view.js +75 -0
- package/dist/prompts/skill.js +19 -26
- package/dist/prompts/view.d.ts +7 -0
- package/dist/prompts/view.js +101 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.js +1 -0
- package/package.json +9 -4
- package/dist/commands/skill/find.d.ts +0 -4
- package/dist/commands/skill/find.js +0 -257
- package/dist/commands/skill/read.js +0 -91
- /package/dist/commands/{skill → memory}/read.d.ts +0 -0
- /package/dist/core/{canvas/browse → tui}/terminal.d.ts +0 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// config-load.ts — load the user's pi keybindings + theme for `crtr attach` (M8).
|
|
2
|
+
//
|
|
3
|
+
// These loaders are CLI-layer: pi does the *resolution* (the KeybindingsManager
|
|
4
|
+
// matcher, `initTheme`) but the FILE reads are not reusable from pi, so we own
|
|
5
|
+
// them. We read pi's own config locations so an attach viewer honors the same
|
|
6
|
+
// `~/.pi/agent/keybindings.json` + theme the user already configured for pi.
|
|
7
|
+
//
|
|
8
|
+
// THE KEYBINDINGS MANAGER. pi's app-level KeybindingsManager (`core/keybindings`,
|
|
9
|
+
// with `.create()` + the `app.*` definition table `KEYBINDINGS`) is NOT exported
|
|
10
|
+
// — only its TYPE is re-exported, and the `KEYBINDINGS` const not at all. So we
|
|
11
|
+
// construct pi-tui's KeybindingsManager (the base class CustomEditor actually
|
|
12
|
+
// matches against) over the merged definition set: pi-tui's `TUI_KEYBINDINGS`
|
|
13
|
+
// (editor/cursor/select bindings) + a VENDORED copy of pi's `app.*` editor
|
|
14
|
+
// bindings below (required — CustomEditor.handleInput gates onEscape/onPasteImage/
|
|
15
|
+
// onAction on `keybindings.matches(data, "app.*")`, so without the app defs those
|
|
16
|
+
// never fire). We also register it globally via `setKeybindings` so the reused
|
|
17
|
+
// pi-tui components (SelectList autocomplete, the dialog editor) honor overrides.
|
|
18
|
+
import { readFileSync } from 'node:fs';
|
|
19
|
+
import { homedir } from 'node:os';
|
|
20
|
+
import { join } from 'node:path';
|
|
21
|
+
import { KeybindingsManager, TUI_KEYBINDINGS, setKeybindings, } from '@earendil-works/pi-tui';
|
|
22
|
+
import { initTheme } from '@earendil-works/pi-coding-agent';
|
|
23
|
+
/** pi's user config dir: `~/.pi/agent/`. */
|
|
24
|
+
export function defaultAgentDir() {
|
|
25
|
+
return join(homedir(), '.pi', 'agent');
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* pi's `app.*` editor keybindings — VENDORED from pi `core/keybindings.ts`
|
|
29
|
+
* `KEYBINDINGS` (the const is not exported; review-C1 pattern). This is the
|
|
30
|
+
* subset CustomEditor's main editor consults; the selector-internal bindings
|
|
31
|
+
* (`app.tree.*`, `app.models.*`, session delete/rename) are omitted because the
|
|
32
|
+
* attach viewer does not open those selectors in Phase 4. Re-sync on each SDK
|
|
33
|
+
* bump against `core/keybindings.js`.
|
|
34
|
+
*/
|
|
35
|
+
const APP_KEYBINDINGS = {
|
|
36
|
+
'app.interrupt': { defaultKeys: 'escape', description: 'Cancel or abort' },
|
|
37
|
+
'app.clear': { defaultKeys: 'ctrl+c', description: 'Clear editor' },
|
|
38
|
+
'app.exit': { defaultKeys: 'ctrl+d', description: 'Exit when editor is empty' },
|
|
39
|
+
'app.suspend': { defaultKeys: 'ctrl+z', description: 'Suspend to background' },
|
|
40
|
+
'app.thinking.cycle': { defaultKeys: 'shift+tab', description: 'Cycle thinking level' },
|
|
41
|
+
'app.model.cycleForward': { defaultKeys: 'ctrl+p', description: 'Cycle to next model' },
|
|
42
|
+
'app.model.cycleBackward': { defaultKeys: 'shift+ctrl+p', description: 'Cycle to previous model' },
|
|
43
|
+
'app.model.select': { defaultKeys: 'ctrl+l', description: 'Open model selector' },
|
|
44
|
+
'app.tools.expand': { defaultKeys: 'ctrl+o', description: 'Toggle tool output' },
|
|
45
|
+
'app.thinking.toggle': { defaultKeys: 'ctrl+t', description: 'Toggle thinking blocks' },
|
|
46
|
+
'app.editor.external': { defaultKeys: 'ctrl+g', description: 'Open external editor' },
|
|
47
|
+
'app.message.followUp': { defaultKeys: 'alt+enter', description: 'Queue follow-up message' },
|
|
48
|
+
'app.message.dequeue': { defaultKeys: 'alt+up', description: 'Restore queued messages' },
|
|
49
|
+
'app.clipboard.pasteImage': { defaultKeys: ['alt+v', 'ctrl+v'], description: 'Paste image from clipboard' },
|
|
50
|
+
'app.session.new': { defaultKeys: [], description: 'Start a new session' },
|
|
51
|
+
'app.session.tree': { defaultKeys: [], description: 'Open session tree' },
|
|
52
|
+
'app.session.fork': { defaultKeys: [], description: 'Fork current session' },
|
|
53
|
+
'app.session.resume': { defaultKeys: [], description: 'Resume a session' },
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Read `~/.pi/agent/keybindings.json` (if present) and return the user's binding
|
|
57
|
+
* overrides. Tolerates a flat `{action: keys}` map or a `{keybindings: {...}}`
|
|
58
|
+
* wrapper; a missing/malformed file → `undefined` (fall back to defaults).
|
|
59
|
+
*/
|
|
60
|
+
export function loadUserKeybindings(agentDir = defaultAgentDir()) {
|
|
61
|
+
try {
|
|
62
|
+
const raw = JSON.parse(readFileSync(join(agentDir, 'keybindings.json'), 'utf8'));
|
|
63
|
+
const obj = raw && typeof raw === 'object' && 'keybindings' in raw
|
|
64
|
+
? raw.keybindings
|
|
65
|
+
: raw;
|
|
66
|
+
if (!obj || typeof obj !== 'object')
|
|
67
|
+
return undefined;
|
|
68
|
+
return obj;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Build the KeybindingsManager the attach viewer drives input with: TUI defaults
|
|
76
|
+
* + vendored `app.*` defs, with the user's overrides applied, registered as the
|
|
77
|
+
* global pi-tui keybindings so reused components pick up the same bindings.
|
|
78
|
+
* (`as unknown as` bridges the readonly `TUI_KEYBINDINGS`/string-literal defs to
|
|
79
|
+
* the mutable `KeybindingDefinitions` param — vendoring cast, no behavior change.)
|
|
80
|
+
*/
|
|
81
|
+
export function createKeybindingsManager(agentDir = defaultAgentDir()) {
|
|
82
|
+
const definitions = { ...TUI_KEYBINDINGS, ...APP_KEYBINDINGS };
|
|
83
|
+
const km = new KeybindingsManager(definitions, loadUserKeybindings(agentDir));
|
|
84
|
+
setKeybindings(km);
|
|
85
|
+
return km;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The user's theme name from pi settings — project (`<cwd>/.pi/settings.json`)
|
|
89
|
+
* overrides global (`~/.pi/agent/settings.json`). `undefined` → pi's default.
|
|
90
|
+
*/
|
|
91
|
+
export function loadThemeName(opts) {
|
|
92
|
+
const files = [
|
|
93
|
+
join(opts?.cwd ?? process.cwd(), '.pi', 'settings.json'),
|
|
94
|
+
join(opts?.agentDir ?? defaultAgentDir(), 'settings.json'),
|
|
95
|
+
];
|
|
96
|
+
for (const file of files) {
|
|
97
|
+
try {
|
|
98
|
+
const raw = JSON.parse(readFileSync(file, 'utf8'));
|
|
99
|
+
if (raw && typeof raw === 'object' && typeof raw.theme === 'string') {
|
|
100
|
+
return raw.theme;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
/* missing/malformed — try the next location */
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
/** Resolve + activate the user's theme so the reused pi components (markdown,
|
|
110
|
+
* editor, select-list themes) render at parity with the user's pi. */
|
|
111
|
+
export function applyTheme(opts) {
|
|
112
|
+
initTheme(loadThemeName(opts));
|
|
113
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { KeybindingsManager, type TUI } from '@earendil-works/pi-tui';
|
|
2
|
+
import type { RpcExtensionUIRequest, RpcExtensionUIResponse } from '../../core/runtime/broker-protocol.js';
|
|
3
|
+
/** Handle for a rendered dialog: tear it down (without responding) when the
|
|
4
|
+
* request is superseded — e.g. the broker resolves it on its own timeout, or a
|
|
5
|
+
* control handoff re-routes it. */
|
|
6
|
+
export interface DialogHandle {
|
|
7
|
+
dismiss(): void;
|
|
8
|
+
}
|
|
9
|
+
/** The 4 user-blocking dialog methods. The other 5 RpcExtensionUIRequest methods
|
|
10
|
+
* are non-blocking display ops (notify/setStatus/setWidget/setTitle/
|
|
11
|
+
* set_editor_text) the broker never forwards as a request. */
|
|
12
|
+
export declare function isBlockingDialog(req: RpcExtensionUIRequest): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Render a blocking extension dialog as a focused overlay over `tui`, and resolve
|
|
15
|
+
* by invoking `onRespond` with the matching `extension_ui_response` (or a
|
|
16
|
+
* `cancelled` response on user-cancel/timeout). Returns a {@link DialogHandle}
|
|
17
|
+
* whose `dismiss()` removes the overlay without responding (idempotent; safe to
|
|
18
|
+
* call after the dialog already resolved).
|
|
19
|
+
*
|
|
20
|
+
* @param keybindings - used only by the `editor` dialog, which calls
|
|
21
|
+
* `keybindings.matches(key, "app.editor.external")` for the Ctrl+G external-editor
|
|
22
|
+
* shortcut. Defaults to a fresh `KeybindingsManager(TUI_KEYBINDINGS)` — that
|
|
23
|
+
* knows the base TUI bindings but not pi's app-level `app.editor.external`, so
|
|
24
|
+
* Ctrl+G is inert until the input controller (T6) passes the app keybindings
|
|
25
|
+
* manager. `ExtensionEditorComponent` is typed against pi's CORE
|
|
26
|
+
* KeybindingsManager (a subclass of pi-tui's), so we widen via the constructor's
|
|
27
|
+
* own parameter type rather than importing the core class (type-only export).
|
|
28
|
+
*/
|
|
29
|
+
export declare function renderDialog(tui: TUI, req: RpcExtensionUIRequest, onRespond: (resp: RpcExtensionUIResponse) => void, keybindings?: KeybindingsManager): DialogHandle;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// extension-dialogs.ts — the blocking-dialog renderer for `crtr attach` (T5).
|
|
2
|
+
//
|
|
3
|
+
// The broker forwards an extension's user-blocking UI request as an
|
|
4
|
+
// `extension_ui_request` frame (RpcExtensionUIRequest). The viewer renders it
|
|
5
|
+
// with pi's matching exported component and ships the resolved
|
|
6
|
+
// `extension_ui_response` (RpcExtensionUIResponse) back over the socket.
|
|
7
|
+
//
|
|
8
|
+
// Only the 4 blocking methods carry behavior — and they are the only ones the
|
|
9
|
+
// broker ever routes as a request: `makeBrokerUiContext` (broker.ts) no-ops
|
|
10
|
+
// notify/setStatus/setWidget/setTitle/set_editor_text, so those never arrive as
|
|
11
|
+
// a blocking dialog. `isBlockingDialog` lets the caller filter cleanly; for a
|
|
12
|
+
// non-blocking method, `renderDialog` returns an inert handle (no overlay, no
|
|
13
|
+
// response — there is nothing to answer).
|
|
14
|
+
//
|
|
15
|
+
// Mapping (mirrors interactive-mode.js): select → ExtensionSelectorComponent;
|
|
16
|
+
// confirm → the SAME selector over ["Yes","No"] (pi has no separate confirm
|
|
17
|
+
// component, :1658); input → ExtensionInputComponent; editor →
|
|
18
|
+
// ExtensionEditorComponent. The select/input components run their own
|
|
19
|
+
// timeout/countdown when `req.timeout` is present; editor takes none. The broker
|
|
20
|
+
// ALSO arms a default timeout, so a dialog may be superseded by the broker
|
|
21
|
+
// resolving first — `dismiss()` tears the overlay down cleanly without
|
|
22
|
+
// responding (the response would be a no-op the broker drops by id).
|
|
23
|
+
import { KeybindingsManager, TUI_KEYBINDINGS, } from '@earendil-works/pi-tui';
|
|
24
|
+
import { ExtensionEditorComponent, ExtensionInputComponent, ExtensionSelectorComponent, } from '@earendil-works/pi-coding-agent';
|
|
25
|
+
/** The 4 user-blocking dialog methods. The other 5 RpcExtensionUIRequest methods
|
|
26
|
+
* are non-blocking display ops (notify/setStatus/setWidget/setTitle/
|
|
27
|
+
* set_editor_text) the broker never forwards as a request. */
|
|
28
|
+
export function isBlockingDialog(req) {
|
|
29
|
+
return (req.method === 'select' ||
|
|
30
|
+
req.method === 'confirm' ||
|
|
31
|
+
req.method === 'input' ||
|
|
32
|
+
req.method === 'editor');
|
|
33
|
+
}
|
|
34
|
+
const INERT = { dismiss: () => { } };
|
|
35
|
+
const OVERLAY_OPTIONS = { anchor: 'center', width: '70%', maxHeight: '80%' };
|
|
36
|
+
/**
|
|
37
|
+
* Render a blocking extension dialog as a focused overlay over `tui`, and resolve
|
|
38
|
+
* by invoking `onRespond` with the matching `extension_ui_response` (or a
|
|
39
|
+
* `cancelled` response on user-cancel/timeout). Returns a {@link DialogHandle}
|
|
40
|
+
* whose `dismiss()` removes the overlay without responding (idempotent; safe to
|
|
41
|
+
* call after the dialog already resolved).
|
|
42
|
+
*
|
|
43
|
+
* @param keybindings - used only by the `editor` dialog, which calls
|
|
44
|
+
* `keybindings.matches(key, "app.editor.external")` for the Ctrl+G external-editor
|
|
45
|
+
* shortcut. Defaults to a fresh `KeybindingsManager(TUI_KEYBINDINGS)` — that
|
|
46
|
+
* knows the base TUI bindings but not pi's app-level `app.editor.external`, so
|
|
47
|
+
* Ctrl+G is inert until the input controller (T6) passes the app keybindings
|
|
48
|
+
* manager. `ExtensionEditorComponent` is typed against pi's CORE
|
|
49
|
+
* KeybindingsManager (a subclass of pi-tui's), so we widen via the constructor's
|
|
50
|
+
* own parameter type rather than importing the core class (type-only export).
|
|
51
|
+
*/
|
|
52
|
+
export function renderDialog(tui, req, onRespond, keybindings) {
|
|
53
|
+
if (!isBlockingDialog(req))
|
|
54
|
+
return INERT;
|
|
55
|
+
let done = false;
|
|
56
|
+
let handle;
|
|
57
|
+
let component;
|
|
58
|
+
const teardown = () => {
|
|
59
|
+
handle?.hide();
|
|
60
|
+
const disposable = component;
|
|
61
|
+
try {
|
|
62
|
+
disposable?.dispose?.();
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
/* ignore dispose errors during teardown */
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
const respond = (resp) => {
|
|
69
|
+
if (done)
|
|
70
|
+
return;
|
|
71
|
+
done = true;
|
|
72
|
+
teardown();
|
|
73
|
+
onRespond(resp);
|
|
74
|
+
};
|
|
75
|
+
const cancel = () => respond({ type: 'extension_ui_response', id: req.id, cancelled: true });
|
|
76
|
+
switch (req.method) {
|
|
77
|
+
case 'select':
|
|
78
|
+
component = new ExtensionSelectorComponent(req.title, req.options, (option) => respond({ type: 'extension_ui_response', id: req.id, value: option }), cancel, { tui, timeout: req.timeout });
|
|
79
|
+
break;
|
|
80
|
+
case 'confirm':
|
|
81
|
+
component = new ExtensionSelectorComponent(`${req.title}\n${req.message}`, ['Yes', 'No'], (option) => respond({ type: 'extension_ui_response', id: req.id, confirmed: option === 'Yes' }), cancel, { tui, timeout: req.timeout });
|
|
82
|
+
break;
|
|
83
|
+
case 'input':
|
|
84
|
+
component = new ExtensionInputComponent(req.title, req.placeholder, (value) => respond({ type: 'extension_ui_response', id: req.id, value }), cancel, { tui, timeout: req.timeout });
|
|
85
|
+
break;
|
|
86
|
+
case 'editor':
|
|
87
|
+
component = new ExtensionEditorComponent(tui, (keybindings ?? new KeybindingsManager(TUI_KEYBINDINGS)), req.title, req.prefill, (value) => respond({ type: 'extension_ui_response', id: req.id, value }), cancel);
|
|
88
|
+
break;
|
|
89
|
+
default:
|
|
90
|
+
return INERT;
|
|
91
|
+
}
|
|
92
|
+
handle = tui.showOverlay(component, OVERLAY_OPTIONS);
|
|
93
|
+
return {
|
|
94
|
+
dismiss: () => {
|
|
95
|
+
if (done)
|
|
96
|
+
return;
|
|
97
|
+
done = true;
|
|
98
|
+
teardown();
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { CustomEditor } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
import type { KeybindingsManager, TUI } from '@earendil-works/pi-tui';
|
|
3
|
+
import { type BrokerSnapshot, type ClientToBroker, type RpcExtensionUIRequest, type RpcExtensionUIResponse } from '../../core/runtime/broker-protocol.js';
|
|
4
|
+
export interface InputControllerHooks {
|
|
5
|
+
/** Send a command frame to the broker. */
|
|
6
|
+
onCommand: (frame: ClientToBroker) => void;
|
|
7
|
+
/** Answer a blocking extension dialog. */
|
|
8
|
+
onDialogResponse: (resp: RpcExtensionUIResponse) => void;
|
|
9
|
+
/** OPTIONAL (additive to the fixed interface): surface a transient notice in
|
|
10
|
+
* the viewer. The InputController does not own the layout (T5/T7 do), so it
|
|
11
|
+
* reports notices up the same way it reports commands. Absent → notices are
|
|
12
|
+
* dropped silently. */
|
|
13
|
+
onNotice?: (message: string) => void;
|
|
14
|
+
}
|
|
15
|
+
export declare class InputController {
|
|
16
|
+
private readonly tui;
|
|
17
|
+
private readonly editor;
|
|
18
|
+
private readonly keybindings;
|
|
19
|
+
private readonly hooks;
|
|
20
|
+
/** Images pasted since the last send, attached to the next prompt/follow-up. */
|
|
21
|
+
private pendingImages;
|
|
22
|
+
/** Running base64-byte total of `pendingImages` (review M2 aggregate budget). */
|
|
23
|
+
private pendingImageBytes;
|
|
24
|
+
/** The currently-rendered blocking dialog, if any (for supersede/dismiss). */
|
|
25
|
+
private dialog;
|
|
26
|
+
/** Latest engine state from `welcome`/`session_info_changed` (for `/session`). */
|
|
27
|
+
private state;
|
|
28
|
+
constructor(tui: TUI, editor: CustomEditor, keybindings: KeybindingsManager, hooks: InputControllerHooks);
|
|
29
|
+
/** Render a blocking extension dialog and route its answer to the broker.
|
|
30
|
+
* Supersedes any dialog already on screen (e.g. a re-route on control
|
|
31
|
+
* handoff) by dismissing it first. */
|
|
32
|
+
attachDialog(req: RpcExtensionUIRequest): void;
|
|
33
|
+
/** Feed the latest engine state so read-only commands (e.g. `/session`) report
|
|
34
|
+
* current values. Optional; the controller works without it. */
|
|
35
|
+
setState(state: BrokerSnapshot['state']): void;
|
|
36
|
+
private wire;
|
|
37
|
+
private slashContext;
|
|
38
|
+
private handleSubmit;
|
|
39
|
+
private handleFollowUp;
|
|
40
|
+
/** Send a drive frame iff the WHOLE encoded frame fits under MAX_FRAME_BYTES
|
|
41
|
+
* (the broker destroys the socket on any line over its 24 MiB read cap). Over
|
|
42
|
+
* the ceiling → notify + refuse so the caller leaves the editor + pending
|
|
43
|
+
* images intact for the user to trim, never a socket-destroying overflow. */
|
|
44
|
+
private emitDrive;
|
|
45
|
+
private handlePaste;
|
|
46
|
+
/** Snapshot the pending images as an `images?` payload (undefined when none)
|
|
47
|
+
* WITHOUT clearing — so a frame refused by `emitDrive` (too large) keeps them
|
|
48
|
+
* for the user to trim. The caller clears via `clearPendingImages` only after a
|
|
49
|
+
* successful send. */
|
|
50
|
+
private pendingImagesPayload;
|
|
51
|
+
/** Drop all pending images + reset the aggregate byte counter (after a send). */
|
|
52
|
+
private clearPendingImages;
|
|
53
|
+
private notify;
|
|
54
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// input-controller.ts — the input half of `crtr attach` (T6).
|
|
2
|
+
//
|
|
3
|
+
// Owns the editor's behavior: turns keystrokes + submits into `ClientToBroker`
|
|
4
|
+
// command frames (sent via `hooks.onCommand`), reads pasted clipboard images to
|
|
5
|
+
// attach to the next message (review M1), and renders blocking extension dialogs
|
|
6
|
+
// (delegating to T5's `renderDialog`, routing the answer through
|
|
7
|
+
// `hooks.onDialogResponse`). It NEVER touches the session or spawns pi — it only
|
|
8
|
+
// produces frames; T7's `attach-cmd.ts` constructs it and wires the socket.
|
|
9
|
+
//
|
|
10
|
+
// Keybinding matching is delegated to CustomEditor + its KeybindingsManager
|
|
11
|
+
// (built by config-load with the vendored `app.*` defs + user overrides) — that
|
|
12
|
+
// is what calls `matchesKey` internally, so the controller wires intent via the
|
|
13
|
+
// editor's `onSubmit`/`onEscape`/`onPasteImage` hooks + `onAction(app.*)`
|
|
14
|
+
// registrations rather than matching raw keys itself.
|
|
15
|
+
import { BROKER_READ_CAPS, encodeFrame, } from '../../core/runtime/broker-protocol.js';
|
|
16
|
+
import { renderDialog } from './extension-dialogs.js';
|
|
17
|
+
import { readClipboardImage } from './clipboard-image.js';
|
|
18
|
+
import { dispatchSlashCommand, isSlashCommand } from './slash-commands.js';
|
|
19
|
+
/** Aggregate budget for images held for the next message (review M2). A drained
|
|
20
|
+
* prompt/steer/follow_up frame inlines ALL pending images as base64 in a single
|
|
21
|
+
* frame; these ceilings keep the images[] portion well within the broker's
|
|
22
|
+
* 24 MiB line cap. Each clipboard image is ≤ 3 MiB base64 (clipboard-image
|
|
23
|
+
* MAX_BYTES), so the worst case is MAX_PENDING_IMAGES × 3 MiB = 12 MiB. A paste
|
|
24
|
+
* that would exceed EITHER bound is refused (not accumulated). The WHOLE frame
|
|
25
|
+
* (images + unbounded text + JSON envelope) is then bounded airtight by the
|
|
26
|
+
* MAX_FRAME_BYTES guard in `emitDrive` — this budget alone is not enough, since
|
|
27
|
+
* `text` is otherwise unbounded. */
|
|
28
|
+
const MAX_PENDING_IMAGE_BYTES = 16 * 1024 * 1024;
|
|
29
|
+
const MAX_PENDING_IMAGES = 4;
|
|
30
|
+
/** Largest drive frame the controller will emit. The broker DESTROYS the viewer
|
|
31
|
+
* socket on any client line over BROKER_READ_CAPS.maxLineBytes (24 MiB), so we
|
|
32
|
+
* refuse to emit within a 4 MiB margin of that. `emitDrive` measures the ACTUAL
|
|
33
|
+
* encoded frame (text + base64 images[] + JSON envelope), so the cap holds no
|
|
34
|
+
* matter how the bytes split — closing the gap where a large text paste plus
|
|
35
|
+
* max images could otherwise overflow and tear down the socket. */
|
|
36
|
+
const MAX_FRAME_BYTES = BROKER_READ_CAPS.maxLineBytes - 4 * 1024 * 1024;
|
|
37
|
+
export class InputController {
|
|
38
|
+
tui;
|
|
39
|
+
editor;
|
|
40
|
+
keybindings;
|
|
41
|
+
hooks;
|
|
42
|
+
/** Images pasted since the last send, attached to the next prompt/follow-up. */
|
|
43
|
+
pendingImages = [];
|
|
44
|
+
/** Running base64-byte total of `pendingImages` (review M2 aggregate budget). */
|
|
45
|
+
pendingImageBytes = 0;
|
|
46
|
+
/** The currently-rendered blocking dialog, if any (for supersede/dismiss). */
|
|
47
|
+
dialog;
|
|
48
|
+
/** Latest engine state from `welcome`/`session_info_changed` (for `/session`). */
|
|
49
|
+
state;
|
|
50
|
+
constructor(tui, editor, keybindings, hooks) {
|
|
51
|
+
this.tui = tui;
|
|
52
|
+
this.editor = editor;
|
|
53
|
+
this.keybindings = keybindings;
|
|
54
|
+
this.hooks = hooks;
|
|
55
|
+
this.wire();
|
|
56
|
+
}
|
|
57
|
+
/** Render a blocking extension dialog and route its answer to the broker.
|
|
58
|
+
* Supersedes any dialog already on screen (e.g. a re-route on control
|
|
59
|
+
* handoff) by dismissing it first. */
|
|
60
|
+
attachDialog(req) {
|
|
61
|
+
this.dialog?.dismiss();
|
|
62
|
+
this.dialog = renderDialog(this.tui, req, (resp) => {
|
|
63
|
+
this.dialog = undefined;
|
|
64
|
+
this.hooks.onDialogResponse(resp);
|
|
65
|
+
}, this.keybindings);
|
|
66
|
+
this.tui.requestRender();
|
|
67
|
+
}
|
|
68
|
+
/** Feed the latest engine state so read-only commands (e.g. `/session`) report
|
|
69
|
+
* current values. Optional; the controller works without it. */
|
|
70
|
+
setState(state) {
|
|
71
|
+
this.state = state;
|
|
72
|
+
}
|
|
73
|
+
// -------------------------------------------------------------------------
|
|
74
|
+
wire() {
|
|
75
|
+
this.editor.onSubmit = (text) => this.handleSubmit(text);
|
|
76
|
+
// Esc / app.interrupt → abort the running turn.
|
|
77
|
+
this.editor.onEscape = () => this.hooks.onCommand({ type: 'abort' });
|
|
78
|
+
// Ctrl+V / Alt+V → read clipboard image, hold it for the next message.
|
|
79
|
+
this.editor.onPasteImage = () => {
|
|
80
|
+
void this.handlePaste();
|
|
81
|
+
};
|
|
82
|
+
// Keyboard shortcuts that map 1:1 to a frame needing no engine-side data.
|
|
83
|
+
this.editor.onAction('app.session.new', () => this.hooks.onCommand({ type: 'new_session' }));
|
|
84
|
+
this.editor.onAction('app.model.cycleForward', () => this.hooks.onCommand({ type: 'cycle_model' }));
|
|
85
|
+
this.editor.onAction('app.message.followUp', () => this.handleFollowUp());
|
|
86
|
+
}
|
|
87
|
+
slashContext() {
|
|
88
|
+
return {
|
|
89
|
+
send: (frame) => this.hooks.onCommand(frame),
|
|
90
|
+
notify: (message) => this.notify(message),
|
|
91
|
+
state: this.state,
|
|
92
|
+
cwd: process.cwd(),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
handleSubmit(text) {
|
|
96
|
+
const trimmed = text.trim();
|
|
97
|
+
if (!trimmed && this.pendingImages.length === 0)
|
|
98
|
+
return;
|
|
99
|
+
if (trimmed && isSlashCommand(trimmed)) {
|
|
100
|
+
// Recognized builtin/scoped-out → handled here; unrecognized falls through
|
|
101
|
+
// and is sent to the engine as a prompt (extension command).
|
|
102
|
+
if (dispatchSlashCommand(trimmed, this.slashContext())) {
|
|
103
|
+
this.editor.setText('');
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const images = this.pendingImagesPayload();
|
|
108
|
+
// Submit-while-streaming = steer (pi-native parity): interject into the
|
|
109
|
+
// running turn instead of queueing a fresh prompt. `isStreaming` is the
|
|
110
|
+
// broker snapshot's busy signal; the broker routes `steer` → session.steer()
|
|
111
|
+
// and `prompt` → session.prompt(). Idle / no state yet → prompt (safe default).
|
|
112
|
+
const busy = this.state?.isStreaming === true;
|
|
113
|
+
const frame = busy
|
|
114
|
+
? { type: 'steer', text: trimmed, images }
|
|
115
|
+
: { type: 'prompt', text: trimmed, images };
|
|
116
|
+
if (!this.emitDrive(frame))
|
|
117
|
+
return; // too large — keep editor + pending to trim
|
|
118
|
+
this.clearPendingImages();
|
|
119
|
+
if (trimmed)
|
|
120
|
+
this.editor.addToHistory(trimmed);
|
|
121
|
+
this.editor.setText('');
|
|
122
|
+
}
|
|
123
|
+
handleFollowUp() {
|
|
124
|
+
const text = this.editor.getText().trim();
|
|
125
|
+
if (!text && this.pendingImages.length === 0)
|
|
126
|
+
return;
|
|
127
|
+
const images = this.pendingImagesPayload();
|
|
128
|
+
if (!this.emitDrive({ type: 'follow_up', text, images }))
|
|
129
|
+
return;
|
|
130
|
+
this.clearPendingImages();
|
|
131
|
+
if (text)
|
|
132
|
+
this.editor.addToHistory(text);
|
|
133
|
+
this.editor.setText('');
|
|
134
|
+
}
|
|
135
|
+
/** Send a drive frame iff the WHOLE encoded frame fits under MAX_FRAME_BYTES
|
|
136
|
+
* (the broker destroys the socket on any line over its 24 MiB read cap). Over
|
|
137
|
+
* the ceiling → notify + refuse so the caller leaves the editor + pending
|
|
138
|
+
* images intact for the user to trim, never a socket-destroying overflow. */
|
|
139
|
+
emitDrive(frame) {
|
|
140
|
+
let bytes;
|
|
141
|
+
try {
|
|
142
|
+
bytes = Buffer.byteLength(encodeFrame(frame));
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
this.notify('Message could not be encoded');
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
if (bytes > MAX_FRAME_BYTES) {
|
|
149
|
+
const mib = Math.round(bytes / (1024 * 1024));
|
|
150
|
+
this.notify(`Message too large to send (${mib} MiB) — shorten the text or remove an attached image`);
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
this.hooks.onCommand(frame);
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
async handlePaste() {
|
|
157
|
+
try {
|
|
158
|
+
const result = await readClipboardImage();
|
|
159
|
+
if (!result) {
|
|
160
|
+
this.notify('No image in the clipboard');
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
// The clipboard layer read an image but DROPPED it (over its per-image
|
|
164
|
+
// ceiling) — surface the reason and attach nothing.
|
|
165
|
+
if (!result.image) {
|
|
166
|
+
this.notify(result.note ?? 'Image not attached');
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
// Enforce the aggregate pending-image budget so the eventual drained frame
|
|
170
|
+
// stays under the broker cap: refuse a paste that would breach the count or
|
|
171
|
+
// byte ceiling rather than accumulate an over-cap images[] (review M2).
|
|
172
|
+
const bytes = Buffer.byteLength(result.image.data);
|
|
173
|
+
if (this.pendingImages.length + 1 > MAX_PENDING_IMAGES ||
|
|
174
|
+
this.pendingImageBytes + bytes > MAX_PENDING_IMAGE_BYTES) {
|
|
175
|
+
this.notify('Image not attached: pending image budget exceeded — send your message first');
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
this.pendingImages.push(result.image);
|
|
179
|
+
this.pendingImageBytes += bytes;
|
|
180
|
+
this.notify(result.note
|
|
181
|
+
? `Image attached (${result.note}) — sends with your next message`
|
|
182
|
+
: 'Image attached — sends with your next message');
|
|
183
|
+
this.tui.requestRender();
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
this.notify('Could not read the clipboard image');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/** Snapshot the pending images as an `images?` payload (undefined when none)
|
|
190
|
+
* WITHOUT clearing — so a frame refused by `emitDrive` (too large) keeps them
|
|
191
|
+
* for the user to trim. The caller clears via `clearPendingImages` only after a
|
|
192
|
+
* successful send. */
|
|
193
|
+
pendingImagesPayload() {
|
|
194
|
+
return this.pendingImages.length === 0 ? undefined : this.pendingImages.slice();
|
|
195
|
+
}
|
|
196
|
+
/** Drop all pending images + reset the aggregate byte counter (after a send). */
|
|
197
|
+
clearPendingImages() {
|
|
198
|
+
this.pendingImages = [];
|
|
199
|
+
this.pendingImageBytes = 0;
|
|
200
|
+
}
|
|
201
|
+
notify(message) {
|
|
202
|
+
this.hooks.onNotice?.(message);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { AutocompleteItem, SlashCommand } from '@earendil-works/pi-tui';
|
|
2
|
+
import type { BrokerSnapshot, ClientToBroker } from '../../core/runtime/broker-protocol.js';
|
|
3
|
+
/** Everything a slash handler needs: the frame sink, a notice sink, the latest
|
|
4
|
+
* engine state (for read-only commands like `/session`), and the cwd (for the
|
|
5
|
+
* default `/export` path). */
|
|
6
|
+
export interface SlashContext {
|
|
7
|
+
/** Send a command frame to the broker (= InputController hooks.onCommand). */
|
|
8
|
+
send: (frame: ClientToBroker) => void;
|
|
9
|
+
/** Surface a transient one-line notice in the viewer. */
|
|
10
|
+
notify: (message: string) => void;
|
|
11
|
+
/** Latest `welcome`/`session_info_changed` state, if the controller fed it. */
|
|
12
|
+
state?: BrokerSnapshot['state'];
|
|
13
|
+
/** cwd for the default `/export` path; defaults to `process.cwd()`. */
|
|
14
|
+
cwd?: string;
|
|
15
|
+
}
|
|
16
|
+
/** True if `text` is a leading-slash command (vs. a normal prompt). */
|
|
17
|
+
export declare function isSlashCommand(text: string): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Parse + dispatch a leading-slash command. Returns `true` if it was handled
|
|
20
|
+
* (a recognized builtin or a scoped-out notice) — the caller then clears the
|
|
21
|
+
* editor and sends nothing else. Returns `false` for an UNRECOGNIZED command,
|
|
22
|
+
* which the caller forwards to the engine as a `prompt` (extension command).
|
|
23
|
+
*/
|
|
24
|
+
export declare function dispatchSlashCommand(text: string, ctx: SlashContext): boolean;
|
|
25
|
+
/** Build slash-command autocomplete entries from the merged command list (the
|
|
26
|
+
* broker's `get_commands` result, which T7 may inject) — defaults to the
|
|
27
|
+
* vendored builtins. Shaped for pi-tui's `CombinedAutocompleteProvider`. */
|
|
28
|
+
export declare function slashCommandList(commands?: ReadonlyArray<{
|
|
29
|
+
name: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
}>): SlashCommand[];
|
|
32
|
+
/** Same list as flat autocomplete items (value/label/description). */
|
|
33
|
+
export declare function commandAutocompleteItems(commands?: ReadonlyArray<{
|
|
34
|
+
name: string;
|
|
35
|
+
description?: string;
|
|
36
|
+
}>): AutocompleteItem[];
|