@crouton-kit/crouter 0.3.18 → 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.
Files changed (257) hide show
  1. package/dist/build-root.js +6 -0
  2. package/dist/builtin-memory/crouter-development/marketplaces.md +164 -0
  3. package/dist/builtin-memory/crouter-development/personas/base-prompt.md +58 -0
  4. package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +60 -0
  5. package/dist/builtin-memory/crouter-development/personas.md +117 -0
  6. package/dist/builtin-memory/crouter-development/plugins.md +163 -0
  7. package/dist/builtin-memory/design.md +60 -0
  8. package/dist/builtin-memory/development.md +116 -0
  9. package/dist/builtin-memory/planning.md +66 -0
  10. package/dist/builtin-memory/spec.md +99 -0
  11. package/dist/builtin-personas/design/orchestrator.md +1 -1
  12. package/dist/builtin-personas/developer/orchestrator.md +1 -1
  13. package/dist/builtin-personas/lifecycle/resident.md +1 -1
  14. package/dist/builtin-personas/lifecycle/terminal.md +5 -2
  15. package/dist/builtin-personas/orchestration-kernel.md +15 -24
  16. package/dist/builtin-personas/plan/orchestrator.md +1 -1
  17. package/dist/builtin-personas/runtime-base.md +6 -3
  18. package/dist/builtin-personas/spec/orchestrator.md +1 -1
  19. package/dist/builtin-personas/waiting.md +8 -0
  20. package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +1 -1
  21. package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +2 -2
  22. package/dist/builtin-views/_lib/states.mjs +161 -0
  23. package/dist/builtin-views/canvas/client.mjs +303 -0
  24. package/dist/builtin-views/canvas/view.mjs +576 -0
  25. package/dist/builtin-views/git-pr/client.mjs +440 -0
  26. package/dist/builtin-views/git-pr/view.mjs +675 -0
  27. package/dist/builtin-views/inbox/_lib/render.mjs +177 -0
  28. package/dist/builtin-views/inbox/sources/gmail.mjs +965 -0
  29. package/dist/builtin-views/inbox/sources/linkedin.mjs +427 -0
  30. package/dist/builtin-views/inbox/view.mjs +889 -0
  31. package/dist/builtin-views/linkedin/client.mjs +610 -0
  32. package/dist/builtin-views/linkedin/view.mjs +1171 -0
  33. package/dist/clients/attach/attach-cmd.d.ts +2 -0
  34. package/dist/clients/attach/attach-cmd.js +354 -0
  35. package/dist/clients/attach/chat-view.d.ts +77 -0
  36. package/dist/clients/attach/chat-view.js +450 -0
  37. package/dist/clients/attach/clipboard-image.d.ts +16 -0
  38. package/dist/clients/attach/clipboard-image.js +113 -0
  39. package/dist/clients/attach/config-load.d.ts +31 -0
  40. package/dist/clients/attach/config-load.js +113 -0
  41. package/dist/clients/attach/extension-dialogs.d.ts +29 -0
  42. package/dist/clients/attach/extension-dialogs.js +101 -0
  43. package/dist/clients/attach/input-controller.d.ts +54 -0
  44. package/dist/clients/attach/input-controller.js +204 -0
  45. package/dist/clients/attach/slash-commands.d.ts +36 -0
  46. package/dist/clients/attach/slash-commands.js +200 -0
  47. package/dist/clients/attach/view-socket.d.ts +48 -0
  48. package/dist/clients/attach/view-socket.js +126 -0
  49. package/dist/commands/attention.js +3 -3
  50. package/dist/commands/canvas-prune.js +1 -1
  51. package/dist/commands/daemon.js +3 -2
  52. package/dist/commands/human/prompts.js +1 -1
  53. package/dist/commands/human/queue.js +43 -8
  54. package/dist/commands/human/shared.d.ts +2 -0
  55. package/dist/commands/memory/find.d.ts +1 -0
  56. package/dist/commands/memory/find.js +180 -0
  57. package/dist/commands/memory/lint.d.ts +1 -0
  58. package/dist/commands/memory/lint.js +140 -0
  59. package/dist/commands/memory/list.d.ts +1 -0
  60. package/dist/commands/memory/list.js +79 -0
  61. package/dist/commands/memory/read.js +103 -0
  62. package/dist/commands/memory/shared.d.ts +30 -0
  63. package/dist/commands/memory/shared.js +122 -0
  64. package/dist/commands/memory/write.d.ts +1 -0
  65. package/dist/commands/memory/write.js +85 -0
  66. package/dist/commands/memory.d.ts +2 -0
  67. package/dist/commands/memory.js +27 -0
  68. package/dist/commands/node.d.ts +3 -2
  69. package/dist/commands/node.js +607 -61
  70. package/dist/commands/pkg/market-manage.js +1 -1
  71. package/dist/commands/push.js +6 -6
  72. package/dist/commands/revive.js +1 -1
  73. package/dist/commands/skill/author.js +1 -1
  74. package/dist/commands/skill/shared.d.ts +1 -8
  75. package/dist/commands/skill/shared.js +2 -55
  76. package/dist/commands/skill.js +9 -14
  77. package/dist/commands/sys/doctor.js +1 -1
  78. package/dist/commands/sys/update.js +1 -1
  79. package/dist/commands/view-cycle.d.ts +2 -0
  80. package/dist/commands/view-cycle.js +125 -0
  81. package/dist/commands/view-list.d.ts +2 -0
  82. package/dist/commands/view-list.js +66 -0
  83. package/dist/commands/view-new.d.ts +2 -0
  84. package/dist/commands/view-new.js +70 -0
  85. package/dist/commands/view-pick.d.ts +2 -0
  86. package/dist/commands/view-pick.js +119 -0
  87. package/dist/commands/view-run.d.ts +2 -0
  88. package/dist/commands/view-run.js +191 -0
  89. package/dist/commands/view.d.ts +2 -0
  90. package/dist/commands/view.js +29 -0
  91. package/dist/core/__tests__/broker-lifecycle.test.d.ts +1 -0
  92. package/dist/core/__tests__/broker-lifecycle.test.js +677 -0
  93. package/dist/core/__tests__/broker-sdk-wiring.test.d.ts +1 -0
  94. package/dist/core/__tests__/broker-sdk-wiring.test.js +166 -0
  95. package/dist/core/__tests__/cascade-close.test.js +12 -2
  96. package/dist/core/__tests__/child-death-wake.test.d.ts +1 -0
  97. package/dist/core/__tests__/child-death-wake.test.js +245 -0
  98. package/dist/core/__tests__/context-intro.test.js +76 -62
  99. package/dist/core/__tests__/daemon-boot.test.js +14 -5
  100. package/dist/core/__tests__/daemon-liveness.test.js +34 -9
  101. package/dist/core/__tests__/detach-focus.test.d.ts +1 -0
  102. package/dist/core/__tests__/detach-focus.test.js +206 -0
  103. package/dist/core/__tests__/draw-style.test.d.ts +1 -0
  104. package/dist/core/__tests__/draw-style.test.js +258 -0
  105. package/dist/core/__tests__/fixtures/c3-custom-provider-ext.d.ts +2 -0
  106. package/dist/core/__tests__/fixtures/c3-custom-provider-ext.js +18 -0
  107. package/dist/core/__tests__/fixtures/fake-engine.d.ts +138 -0
  108. package/dist/core/__tests__/fixtures/fake-engine.js +614 -0
  109. package/dist/core/__tests__/fixtures/fake-pi-host.js +1 -0
  110. package/dist/core/__tests__/flagship-lifecycle.test.js +7 -1
  111. package/dist/core/__tests__/frame-decoder-perf.test.d.ts +1 -0
  112. package/dist/core/__tests__/frame-decoder-perf.test.js +198 -0
  113. package/dist/core/__tests__/helpers/harness.d.ts +9 -0
  114. package/dist/core/__tests__/helpers/harness.js +111 -1
  115. package/dist/core/__tests__/home-session.test.js +41 -8
  116. package/dist/core/__tests__/human-new-window-regression.test.d.ts +1 -0
  117. package/dist/core/__tests__/human-new-window-regression.test.js +101 -0
  118. package/dist/core/__tests__/kickoff.test.js +37 -3
  119. package/dist/core/__tests__/live-mutation.test.js +50 -33
  120. package/dist/core/__tests__/memory.test.js +23 -115
  121. package/dist/core/__tests__/placement-focus.test.js +5 -0
  122. package/dist/core/__tests__/placement-teardown.test.js +54 -11
  123. package/dist/core/__tests__/relaunch.test.js +4 -3
  124. package/dist/core/__tests__/review-render-pane-regression.test.d.ts +1 -0
  125. package/dist/core/__tests__/review-render-pane-regression.test.js +133 -0
  126. package/dist/core/__tests__/spawn-root.test.js +10 -0
  127. package/dist/core/__tests__/spike-harness.test.js +1 -0
  128. package/dist/core/__tests__/wake-bearings.test.d.ts +1 -0
  129. package/dist/core/__tests__/wake-bearings.test.js +156 -0
  130. package/dist/core/__tests__/wake-origin.test.d.ts +1 -0
  131. package/dist/core/__tests__/wake-origin.test.js +110 -0
  132. package/dist/core/bootstrap.js +1 -1
  133. package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
  134. package/dist/core/canvas/browse/app.js +24 -2
  135. package/dist/core/canvas/browse/model.d.ts +38 -2
  136. package/dist/core/canvas/browse/model.js +134 -10
  137. package/dist/core/canvas/browse/render.d.ts +6 -12
  138. package/dist/core/canvas/browse/render.js +72 -104
  139. package/dist/core/canvas/canvas.js +15 -22
  140. package/dist/core/canvas/db.js +46 -0
  141. package/dist/core/canvas/index.d.ts +1 -0
  142. package/dist/core/canvas/index.js +1 -0
  143. package/dist/core/canvas/pid.d.ts +4 -0
  144. package/dist/core/canvas/pid.js +23 -0
  145. package/dist/core/canvas/render.d.ts +11 -2
  146. package/dist/core/canvas/render.js +69 -0
  147. package/dist/core/canvas/types.d.ts +85 -3
  148. package/dist/core/canvas/wakeups.d.ts +76 -0
  149. package/dist/core/canvas/wakeups.js +185 -0
  150. package/dist/core/config.js +4 -1
  151. package/dist/core/frontmatter.js +37 -124
  152. package/dist/core/memory-resolver.d.ts +49 -0
  153. package/dist/core/memory-resolver.js +141 -0
  154. package/dist/core/personas/loader.d.ts +7 -0
  155. package/dist/core/personas/loader.js +36 -5
  156. package/dist/core/personas/resolve.js +13 -3
  157. package/dist/core/predicate.d.ts +63 -0
  158. package/dist/core/predicate.js +189 -0
  159. package/dist/core/resolver.js +26 -5
  160. package/dist/core/runtime/bearings.d.ts +53 -12
  161. package/dist/core/runtime/bearings.js +132 -59
  162. package/dist/core/runtime/broker-cli.d.ts +1 -0
  163. package/dist/core/runtime/broker-cli.js +46 -0
  164. package/dist/core/runtime/broker-protocol.d.ts +332 -0
  165. package/dist/core/runtime/broker-protocol.js +153 -0
  166. package/dist/core/runtime/broker-sdk.d.ts +48 -0
  167. package/dist/core/runtime/broker-sdk.js +72 -0
  168. package/dist/core/runtime/broker.d.ts +55 -0
  169. package/dist/core/runtime/broker.js +1128 -0
  170. package/dist/core/runtime/close.js +35 -6
  171. package/dist/core/runtime/host.d.ts +53 -0
  172. package/dist/core/runtime/host.js +186 -0
  173. package/dist/core/runtime/kickoff.d.ts +2 -1
  174. package/dist/core/runtime/kickoff.js +91 -5
  175. package/dist/core/runtime/launch.d.ts +45 -2
  176. package/dist/core/runtime/launch.js +65 -2
  177. package/dist/core/runtime/lifecycle.js +23 -6
  178. package/dist/core/runtime/memory.d.ts +2 -42
  179. package/dist/core/runtime/memory.js +11 -162
  180. package/dist/core/runtime/nodes.d.ts +33 -0
  181. package/dist/core/runtime/nodes.js +59 -1
  182. package/dist/core/runtime/persona.js +21 -11
  183. package/dist/core/runtime/pi-vendored.d.ts +18 -0
  184. package/dist/core/runtime/pi-vendored.js +49 -0
  185. package/dist/core/runtime/placement.d.ts +32 -14
  186. package/dist/core/runtime/placement.js +206 -52
  187. package/dist/core/runtime/promote.d.ts +0 -6
  188. package/dist/core/runtime/promote.js +1 -12
  189. package/dist/core/runtime/{demote.d.ts → recycle.d.ts} +5 -5
  190. package/dist/core/runtime/{demote.js → recycle.js} +27 -11
  191. package/dist/core/runtime/reset.js +8 -6
  192. package/dist/core/runtime/revive.d.ts +2 -0
  193. package/dist/core/runtime/revive.js +34 -34
  194. package/dist/core/runtime/spawn.d.ts +19 -0
  195. package/dist/core/runtime/spawn.js +75 -22
  196. package/dist/core/runtime/stop-guard.d.ts +1 -1
  197. package/dist/core/runtime/stop-guard.js +6 -1
  198. package/dist/core/runtime/tmux-chrome.d.ts +1 -1
  199. package/dist/core/runtime/tmux-chrome.js +1 -1
  200. package/dist/core/runtime/tmux.d.ts +28 -0
  201. package/dist/core/runtime/tmux.js +80 -6
  202. package/dist/core/scope.d.ts +11 -0
  203. package/dist/core/scope.js +39 -0
  204. package/dist/core/spawn.d.ts +5 -0
  205. package/dist/core/spawn.js +20 -2
  206. package/dist/core/substrate/gate.d.ts +13 -0
  207. package/dist/core/substrate/gate.js +21 -0
  208. package/dist/core/substrate/index.d.ts +7 -0
  209. package/dist/core/substrate/index.js +18 -0
  210. package/dist/core/substrate/on-read.d.ts +14 -0
  211. package/dist/core/substrate/on-read.js +292 -0
  212. package/dist/core/substrate/render.d.ts +25 -0
  213. package/dist/core/substrate/render.js +256 -0
  214. package/dist/core/substrate/schema.d.ts +76 -0
  215. package/dist/core/substrate/schema.js +124 -0
  216. package/dist/core/substrate/session-cache.d.ts +30 -0
  217. package/dist/core/substrate/session-cache.js +77 -0
  218. package/dist/core/substrate/subject.d.ts +41 -0
  219. package/dist/core/substrate/subject.js +54 -0
  220. package/dist/core/tui/contract.d.ts +83 -0
  221. package/dist/core/tui/contract.js +8 -0
  222. package/dist/core/tui/draw.d.ts +96 -0
  223. package/dist/core/tui/draw.js +339 -0
  224. package/dist/core/tui/host.d.ts +29 -0
  225. package/dist/core/tui/host.js +379 -0
  226. package/dist/core/tui/loader.d.ts +16 -0
  227. package/dist/core/tui/loader.js +94 -0
  228. package/dist/core/{canvas/browse → tui}/terminal.js +7 -6
  229. package/dist/core/wake.d.ts +86 -0
  230. package/dist/core/wake.js +308 -0
  231. package/dist/daemon/crtrd.d.ts +29 -4
  232. package/dist/daemon/crtrd.js +662 -46
  233. package/dist/pi-extensions/__tests__/canvas-context-intro.test.d.ts +1 -0
  234. package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +171 -0
  235. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +19 -12
  236. package/dist/pi-extensions/canvas-commands.d.ts +3 -0
  237. package/dist/pi-extensions/canvas-commands.js +10 -0
  238. package/dist/pi-extensions/canvas-context-intro.d.ts +17 -0
  239. package/dist/pi-extensions/canvas-context-intro.js +55 -15
  240. package/dist/pi-extensions/canvas-doc-substrate.d.ts +44 -0
  241. package/dist/pi-extensions/canvas-doc-substrate.js +112 -0
  242. package/dist/pi-extensions/canvas-nav.d.ts +3 -0
  243. package/dist/pi-extensions/canvas-nav.js +87 -23
  244. package/dist/pi-extensions/canvas-stophook.js +17 -8
  245. package/dist/pi-extensions/canvas-view.d.ts +21 -0
  246. package/dist/pi-extensions/canvas-view.js +75 -0
  247. package/dist/prompts/skill.js +19 -26
  248. package/dist/prompts/view.d.ts +7 -0
  249. package/dist/prompts/view.js +101 -0
  250. package/dist/types.d.ts +4 -0
  251. package/dist/types.js +1 -0
  252. package/package.json +9 -4
  253. package/dist/commands/skill/find.d.ts +0 -4
  254. package/dist/commands/skill/find.js +0 -257
  255. package/dist/commands/skill/read.js +0 -91
  256. /package/dist/commands/{skill → memory}/read.d.ts +0 -0
  257. /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[];