@crouton-kit/crouter 0.3.28 → 0.3.29

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 (322) hide show
  1. package/dist/build-root.js +4 -2
  2. package/dist/builtin-memory/crouter-development/marketplaces.md +7 -6
  3. package/dist/builtin-memory/crouter-development/personas/base-prompt.md +1 -1
  4. package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +1 -1
  5. package/dist/builtin-memory/crouter-development/personas.md +1 -1
  6. package/dist/builtin-memory/crouter-development/plugins.md +20 -18
  7. package/dist/builtin-memory/design.md +1 -1
  8. package/dist/builtin-memory/development.md +1 -1
  9. package/dist/builtin-memory/internal/INDEX.md +1 -1
  10. package/dist/builtin-memory/internal/examples/INDEX.md +1 -1
  11. package/dist/builtin-memory/internal/examples/imessage-assistant.md +2 -2
  12. package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
  13. package/dist/builtin-memory/internal/storage-tiers.md +1 -1
  14. package/dist/builtin-memory/planning.md +1 -1
  15. package/dist/builtin-memory/spec.md +6 -4
  16. package/dist/builtin-personas/orchestration-kernel.md +4 -5
  17. package/dist/builtin-personas/runtime-base.md +5 -4
  18. package/dist/builtin-personas/spec/PERSONA.md +8 -4
  19. package/dist/builtin-personas/spec/orchestrator.md +5 -3
  20. package/dist/builtin-personas/spec/requirements/PERSONA.md +10 -0
  21. package/dist/builtin-views/canvas/core.mjs +586 -0
  22. package/dist/builtin-views/canvas/text.mjs +58 -0
  23. package/dist/builtin-views/canvas/tui.mjs +165 -0
  24. package/dist/builtin-views/canvas/web.jsx +120 -0
  25. package/dist/builtin-views/git-pr/core.mjs +673 -0
  26. package/dist/builtin-views/git-pr/text.mjs +84 -0
  27. package/dist/builtin-views/git-pr/tui.mjs +302 -0
  28. package/dist/builtin-views/git-pr/web.jsx +216 -0
  29. package/dist/builtin-views/inbox/_lib/render.mjs +1 -3
  30. package/dist/builtin-views/inbox/core.mjs +1273 -0
  31. package/dist/builtin-views/inbox/text.mjs +73 -0
  32. package/dist/builtin-views/inbox/tui.mjs +312 -0
  33. package/dist/builtin-views/inbox/web.jsx +188 -0
  34. package/dist/builtin-views/linkedin/core.mjs +906 -0
  35. package/dist/builtin-views/linkedin/text.mjs +69 -0
  36. package/dist/builtin-views/linkedin/tui.mjs +427 -0
  37. package/dist/builtin-views/linkedin/web.jsx +206 -0
  38. package/dist/builtin-views/workspace-sidebar/core.mjs +621 -0
  39. package/dist/builtin-views/workspace-sidebar/text.mjs +53 -0
  40. package/dist/builtin-views/workspace-sidebar/tui.mjs +142 -0
  41. package/dist/builtin-views/workspace-sidebar/web.jsx +109 -0
  42. package/dist/cli.js +3 -3
  43. package/dist/clients/attach/__tests__/action-parity.test.js +48 -0
  44. package/dist/clients/attach/__tests__/clipboard-image.test.js +76 -0
  45. package/dist/clients/attach/__tests__/editor-newline.test.js +57 -0
  46. package/dist/clients/attach/__tests__/git-info.test.js +31 -0
  47. package/dist/clients/attach/__tests__/onrequest-wired.test.js +20 -0
  48. package/dist/clients/attach/__tests__/slash-quit-copy.test.js +42 -0
  49. package/dist/clients/attach/__tests__/titled-editor.test.js +39 -0
  50. package/dist/clients/attach/attach-cmd.js +282 -46
  51. package/dist/clients/attach/auth-pickers.d.ts +9 -0
  52. package/dist/clients/attach/auth-pickers.js +194 -0
  53. package/dist/clients/attach/canvas-panels.d.ts +4 -2
  54. package/dist/clients/attach/canvas-panels.js +45 -17
  55. package/dist/clients/attach/chat-view.d.ts +27 -2
  56. package/dist/clients/attach/chat-view.js +98 -9
  57. package/dist/clients/attach/clipboard-image.d.ts +47 -8
  58. package/dist/clients/attach/clipboard-image.js +150 -26
  59. package/dist/clients/attach/clipboard-text.d.ts +3 -0
  60. package/dist/clients/attach/clipboard-text.js +39 -0
  61. package/dist/clients/attach/config-load.d.ts +54 -7
  62. package/dist/clients/attach/config-load.js +99 -3
  63. package/dist/clients/attach/context-message.d.ts +12 -0
  64. package/dist/clients/attach/context-message.js +72 -0
  65. package/dist/clients/attach/git-info.d.ts +20 -0
  66. package/dist/clients/attach/git-info.js +44 -0
  67. package/dist/clients/attach/graph-overlay.d.ts +13 -4
  68. package/dist/clients/attach/graph-overlay.js +59 -19
  69. package/dist/clients/attach/input-controller.d.ts +75 -13
  70. package/dist/clients/attach/input-controller.js +305 -67
  71. package/dist/clients/attach/pickers.d.ts +58 -0
  72. package/dist/clients/attach/pickers.js +171 -0
  73. package/dist/clients/attach/slash-commands.d.ts +22 -0
  74. package/dist/clients/attach/slash-commands.js +91 -25
  75. package/dist/clients/attach/titled-editor.d.ts +33 -0
  76. package/dist/clients/attach/titled-editor.js +84 -0
  77. package/dist/clients/attach/view-socket.d.ts +18 -1
  78. package/dist/clients/attach/view-socket.js +70 -1
  79. package/dist/clients/web/dev-server.d.ts +7 -0
  80. package/dist/clients/web/dev-server.js +59 -0
  81. package/dist/clients/web/events.d.ts +14 -0
  82. package/dist/clients/web/events.js +151 -0
  83. package/dist/clients/web/server.d.ts +18 -0
  84. package/dist/clients/web/server.js +450 -0
  85. package/dist/clients/web/web-cmd.d.ts +2 -0
  86. package/dist/clients/web/web-cmd.js +120 -0
  87. package/dist/commands/canvas.js +1 -2
  88. package/dist/commands/chord.js +1 -1
  89. package/dist/commands/dashboard.js +5 -1
  90. package/dist/commands/memory/__tests__/lint-schema.test.js +6 -6
  91. package/dist/commands/memory/lint.js +14 -2
  92. package/dist/commands/memory/read.js +2 -2
  93. package/dist/commands/memory/shared.d.ts +7 -1
  94. package/dist/commands/memory/shared.js +18 -3
  95. package/dist/commands/memory/write.js +8 -4
  96. package/dist/commands/memory.js +4 -4
  97. package/dist/commands/node.d.ts +1 -1
  98. package/dist/commands/node.js +64 -65
  99. package/dist/commands/pkg.js +1 -2
  100. package/dist/commands/revive.js +11 -3
  101. package/dist/commands/sys/sync.d.ts +1 -0
  102. package/dist/commands/sys/sync.js +187 -0
  103. package/dist/commands/sys.js +3 -2
  104. package/dist/commands/view-cycle.js +2 -2
  105. package/dist/commands/view-list.js +8 -8
  106. package/dist/commands/view-new.js +21 -17
  107. package/dist/commands/view-pick.js +1 -1
  108. package/dist/commands/view-run.js +35 -14
  109. package/dist/commands/view.js +8 -6
  110. package/dist/commands/workspace.d.ts +2 -0
  111. package/dist/commands/workspace.js +161 -0
  112. package/dist/core/__tests__/broker-double-spawn.test.d.ts +1 -0
  113. package/dist/core/__tests__/broker-double-spawn.test.js +143 -0
  114. package/dist/core/__tests__/broker-fork-seam.test.d.ts +1 -0
  115. package/dist/core/__tests__/broker-fork-seam.test.js +102 -0
  116. package/dist/core/__tests__/broker-sdk-wiring.test.js +2 -0
  117. package/dist/core/__tests__/child-death-wake.test.js +56 -111
  118. package/dist/core/__tests__/close.test.js +14 -20
  119. package/dist/core/__tests__/context-intro.test.js +19 -19
  120. package/dist/core/__tests__/daemon-boot.test.js +30 -23
  121. package/dist/core/__tests__/draw-style.test.js +1 -1
  122. package/dist/core/__tests__/error-stall-recycle.test.d.ts +1 -0
  123. package/dist/core/__tests__/error-stall-recycle.test.js +141 -0
  124. package/dist/core/__tests__/fixtures/fake-engine.d.ts +17 -1
  125. package/dist/core/__tests__/fixtures/fake-engine.js +22 -2
  126. package/dist/core/__tests__/fixtures/fake-pi-host.js +2 -2
  127. package/dist/core/__tests__/focuses.test.js +25 -13
  128. package/dist/core/__tests__/fork.test.js +22 -5
  129. package/dist/core/__tests__/full/broker-control-preempt.test.d.ts +1 -0
  130. package/dist/core/__tests__/full/broker-control-preempt.test.js +61 -0
  131. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.d.ts +1 -0
  132. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.js +57 -0
  133. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.d.ts +1 -0
  134. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.js +80 -0
  135. package/dist/core/__tests__/full/broker-pane-resolution.test.js +90 -0
  136. package/dist/core/__tests__/full/cascade-close.test.js +22 -8
  137. package/dist/core/__tests__/full/detach-focus.test.js +66 -169
  138. package/dist/core/__tests__/helpers/harness.d.ts +6 -9
  139. package/dist/core/__tests__/helpers/harness.js +19 -36
  140. package/dist/core/__tests__/human-node-not-supervised.test.d.ts +1 -0
  141. package/dist/core/__tests__/human-node-not-supervised.test.js +83 -0
  142. package/dist/core/__tests__/lifecycle.test.js +1 -19
  143. package/dist/core/__tests__/live-mutation-verbs.test.js +5 -5
  144. package/dist/core/__tests__/memory-resolver.test.js +1 -1
  145. package/dist/core/__tests__/on-read-dedup-resume.test.js +2 -2
  146. package/dist/core/__tests__/refresh-stall-recycle.test.d.ts +1 -0
  147. package/dist/core/__tests__/refresh-stall-recycle.test.js +88 -0
  148. package/dist/core/__tests__/relaunch-root.test.d.ts +1 -0
  149. package/dist/core/__tests__/relaunch-root.test.js +114 -0
  150. package/dist/core/__tests__/reset.test.js +19 -79
  151. package/dist/core/__tests__/steer-note.test.js +9 -7
  152. package/dist/core/__tests__/tmux-surface.test.js +13 -7
  153. package/dist/core/bootstrap.d.ts +0 -3
  154. package/dist/core/bootstrap.js +1 -143
  155. package/dist/core/canvas/browse/__tests__/model.test.js +86 -1
  156. package/dist/core/canvas/browse/__tests__/render.test.js +2 -1
  157. package/dist/core/canvas/browse/app.js +131 -8
  158. package/dist/core/canvas/browse/model.d.ts +23 -5
  159. package/dist/core/canvas/browse/model.js +68 -13
  160. package/dist/core/canvas/browse/render.d.ts +7 -1
  161. package/dist/core/canvas/browse/render.js +178 -47
  162. package/dist/core/canvas/canvas.js +1 -1
  163. package/dist/core/canvas/focuses.d.ts +4 -4
  164. package/dist/core/canvas/focuses.js +7 -7
  165. package/dist/core/canvas/nav-model.js +8 -7
  166. package/dist/core/canvas/render.d.ts +68 -9
  167. package/dist/core/canvas/render.js +195 -61
  168. package/dist/core/canvas/types.d.ts +16 -23
  169. package/dist/core/config.js +1 -4
  170. package/dist/core/memory-resolver.js +43 -5
  171. package/dist/core/runtime/bearings.d.ts +1 -1
  172. package/dist/core/runtime/bearings.js +8 -8
  173. package/dist/core/runtime/branded-host.d.ts +16 -0
  174. package/dist/core/runtime/branded-host.js +127 -0
  175. package/dist/core/runtime/broker-protocol.d.ts +212 -6
  176. package/dist/core/runtime/broker.d.ts +4 -1
  177. package/dist/core/runtime/broker.js +494 -58
  178. package/dist/core/runtime/close.d.ts +12 -2
  179. package/dist/core/runtime/close.js +37 -16
  180. package/dist/core/runtime/front-door.js +6 -14
  181. package/dist/core/runtime/host.d.ts +14 -34
  182. package/dist/core/runtime/host.js +14 -50
  183. package/dist/core/runtime/launch.d.ts +12 -9
  184. package/dist/core/runtime/launch.js +27 -15
  185. package/dist/core/runtime/lifecycle.d.ts +1 -1
  186. package/dist/core/runtime/lifecycle.js +15 -19
  187. package/dist/core/runtime/nodes.d.ts +0 -55
  188. package/dist/core/runtime/nodes.js +6 -74
  189. package/dist/core/runtime/placement.d.ts +91 -335
  190. package/dist/core/runtime/placement.js +262 -840
  191. package/dist/core/runtime/promote.d.ts +2 -0
  192. package/dist/core/runtime/promote.js +24 -6
  193. package/dist/core/runtime/recap.d.ts +8 -0
  194. package/dist/core/runtime/recap.js +107 -0
  195. package/dist/core/runtime/recycle.js +25 -61
  196. package/dist/core/runtime/reset.d.ts +43 -37
  197. package/dist/core/runtime/reset.js +131 -218
  198. package/dist/core/runtime/revive.d.ts +9 -29
  199. package/dist/core/runtime/revive.js +40 -139
  200. package/dist/core/runtime/spawn.d.ts +15 -16
  201. package/dist/core/runtime/spawn.js +165 -194
  202. package/dist/core/runtime/surface-bg.d.ts +11 -0
  203. package/dist/core/runtime/surface-bg.js +68 -0
  204. package/dist/core/runtime/tmux.d.ts +13 -50
  205. package/dist/core/runtime/tmux.js +33 -87
  206. package/dist/core/scope.d.ts +1 -2
  207. package/dist/core/scope.js +1 -5
  208. package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.d.ts +1 -0
  209. package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.js +57 -0
  210. package/dist/core/skill-sync/builtins.d.ts +42 -0
  211. package/dist/core/skill-sync/builtins.js +112 -0
  212. package/dist/core/skill-sync/claude-plugins.d.ts +23 -0
  213. package/dist/core/skill-sync/claude-plugins.js +71 -0
  214. package/dist/core/skill-sync/engine.d.ts +42 -0
  215. package/dist/core/skill-sync/engine.js +633 -0
  216. package/dist/core/skill-sync/export.d.ts +23 -0
  217. package/dist/core/skill-sync/export.js +86 -0
  218. package/dist/core/skill-sync/manifest.d.ts +64 -0
  219. package/dist/core/skill-sync/manifest.js +181 -0
  220. package/dist/core/skill-sync/profile.d.ts +76 -0
  221. package/dist/core/skill-sync/profile.js +173 -0
  222. package/dist/core/skill-sync/snapshot.d.ts +57 -0
  223. package/dist/core/skill-sync/snapshot.js +120 -0
  224. package/dist/core/substrate/index.d.ts +1 -1
  225. package/dist/core/substrate/index.js +1 -1
  226. package/dist/core/substrate/injected-store.js +3 -3
  227. package/dist/core/substrate/on-read.js +68 -6
  228. package/dist/core/substrate/render.d.ts +8 -11
  229. package/dist/core/substrate/render.js +29 -43
  230. package/dist/core/substrate/schema.d.ts +10 -3
  231. package/dist/core/substrate/schema.js +6 -3
  232. package/dist/core/tui/host.d.ts +12 -4
  233. package/dist/core/tui/host.js +280 -149
  234. package/dist/core/view/bridge.d.ts +10 -0
  235. package/dist/core/view/bridge.js +31 -0
  236. package/dist/core/view/chrome.d.ts +9 -0
  237. package/dist/core/view/chrome.js +22 -0
  238. package/dist/core/view/contract.d.ts +171 -0
  239. package/dist/core/view/contract.js +23 -0
  240. package/dist/core/view/loader.d.ts +31 -0
  241. package/dist/core/view/loader.js +188 -0
  242. package/dist/core/view/transport-local.d.ts +7 -0
  243. package/dist/core/view/transport-local.js +70 -0
  244. package/dist/core/view/transport.d.ts +4 -0
  245. package/dist/core/view/transport.js +15 -0
  246. package/dist/daemon/crtrd.d.ts +38 -27
  247. package/dist/daemon/crtrd.js +303 -376
  248. package/dist/daemon/manage.js +6 -1
  249. package/dist/index.js +1 -1
  250. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +28 -20
  251. package/dist/pi-extensions/canvas-context-intro.js +4 -4
  252. package/dist/pi-extensions/canvas-doc-substrate.js +16 -16
  253. package/dist/pi-extensions/canvas-nav.js +5 -0
  254. package/dist/pi-extensions/canvas-recap.d.ts +37 -0
  255. package/dist/pi-extensions/canvas-recap.js +236 -0
  256. package/dist/pi-extensions/canvas-resume.js +2 -1
  257. package/dist/pi-extensions/canvas-stophook.d.ts +14 -12
  258. package/dist/pi-extensions/canvas-stophook.js +87 -101
  259. package/dist/pi-extensions/canvas-view.js +2 -1
  260. package/dist/pi-extensions/widget-order-bus.d.ts +6 -0
  261. package/dist/pi-extensions/widget-order-bus.js +34 -0
  262. package/dist/prompts/view.d.ts +2 -2
  263. package/dist/prompts/view.js +145 -73
  264. package/dist/types.d.ts +1 -6
  265. package/dist/types.js +1 -3
  266. package/dist/web/ViewChrome.d.ts +7 -0
  267. package/dist/web/ViewChrome.js +28 -0
  268. package/dist/web/ViewPane.d.ts +39 -0
  269. package/dist/web/ViewPane.js +48 -0
  270. package/dist/web/index.d.ts +6 -0
  271. package/dist/web/index.js +16 -0
  272. package/dist/web/runtime.d.ts +39 -0
  273. package/dist/web/runtime.js +133 -0
  274. package/dist/web/states.d.ts +24 -0
  275. package/dist/web/states.js +24 -0
  276. package/dist/web/transport-http.d.ts +5 -0
  277. package/dist/web/transport-http.js +28 -0
  278. package/dist/web-client/assets/index-BUvQb4hR.css +2 -0
  279. package/dist/web-client/assets/index-ClLQXYAE.js +10 -0
  280. package/dist/web-client/index.html +13 -0
  281. package/package.json +20 -6
  282. package/dist/builtin-views/canvas/client.mjs +0 -303
  283. package/dist/builtin-views/canvas/view.mjs +0 -576
  284. package/dist/builtin-views/git-pr/client.mjs +0 -440
  285. package/dist/builtin-views/git-pr/view.mjs +0 -675
  286. package/dist/builtin-views/inbox/sources/gmail.mjs +0 -965
  287. package/dist/builtin-views/inbox/sources/linkedin.mjs +0 -427
  288. package/dist/builtin-views/inbox/view.mjs +0 -889
  289. package/dist/builtin-views/linkedin/client.mjs +0 -610
  290. package/dist/builtin-views/linkedin/view.mjs +0 -1171
  291. package/dist/commands/pkg/bridge.d.ts +0 -1
  292. package/dist/commands/pkg/bridge.js +0 -137
  293. package/dist/commands/skill/author.d.ts +0 -3
  294. package/dist/commands/skill/author.js +0 -140
  295. package/dist/commands/skill/shared.d.ts +0 -3
  296. package/dist/commands/skill/shared.js +0 -19
  297. package/dist/commands/skill.d.ts +0 -2
  298. package/dist/commands/skill.js +0 -21
  299. package/dist/commands/tmux-spread.d.ts +0 -2
  300. package/dist/commands/tmux-spread.js +0 -144
  301. package/dist/core/__tests__/full/daemon-liveness-pane.full.test.js +0 -292
  302. package/dist/core/__tests__/full/placement-focus.test.js +0 -309
  303. package/dist/core/__tests__/full/placement-reconcile.test.js +0 -212
  304. package/dist/core/__tests__/full/placement-revive.test.js +0 -238
  305. package/dist/core/__tests__/full/placement-teardown.test.js +0 -297
  306. package/dist/core/__tests__/home-session.test.js +0 -186
  307. package/dist/core/__tests__/relaunch.test.js +0 -335
  308. package/dist/core/bridge-map.d.ts +0 -19
  309. package/dist/core/bridge-map.js +0 -73
  310. package/dist/core/tui/contract.d.ts +0 -83
  311. package/dist/core/tui/contract.js +0 -8
  312. package/dist/core/tui/loader.d.ts +0 -16
  313. package/dist/core/tui/loader.js +0 -94
  314. package/dist/prompts/skill.d.ts +0 -2
  315. package/dist/prompts/skill.js +0 -650
  316. /package/dist/{core/__tests__/full/daemon-liveness-pane.full.test.d.ts → clients/attach/__tests__/action-parity.test.d.ts} +0 -0
  317. /package/dist/{core/__tests__/full/placement-focus.test.d.ts → clients/attach/__tests__/clipboard-image.test.d.ts} +0 -0
  318. /package/dist/{core/__tests__/full/placement-reconcile.test.d.ts → clients/attach/__tests__/editor-newline.test.d.ts} +0 -0
  319. /package/dist/{core/__tests__/full/placement-revive.test.d.ts → clients/attach/__tests__/git-info.test.d.ts} +0 -0
  320. /package/dist/{core/__tests__/full/placement-teardown.test.d.ts → clients/attach/__tests__/onrequest-wired.test.d.ts} +0 -0
  321. /package/dist/{core/__tests__/home-session.test.d.ts → clients/attach/__tests__/slash-quit-copy.test.d.ts} +0 -0
  322. /package/dist/{core/__tests__/relaunch.test.d.ts → clients/attach/__tests__/titled-editor.test.d.ts} +0 -0
@@ -1,22 +1,27 @@
1
- // host.ts — the alt-screen loop that hosts a ViewModule.
1
+ // host.ts — the alt-screen loop that hosts a dual-target ViewCore + TuiPresenter
2
+ // (src/core/view/contract.ts).
2
3
  //
3
4
  // Models browse/app.ts's loop, generalized: the host owns the screen, input,
4
- // chrome, and the single-flight async lane; the view paints into the `Draw` it's
5
- // handed and returns a `ViewAction` per keystroke.
5
+ // chrome, and the single-flight async lane. State is immutable (the core's
6
+ // intents update it via `ctx.set`); presenters are pure reads. A keystroke maps
7
+ // through the TuiPresenter's keymap to a named intent the host dispatches.
6
8
  //
7
- // • TTY gate: !process.stdin.isTTY → view.dump(state) to stdout, exit 0.
9
+ // • TTY gate: !process.stdin.isTTY → text.dump(state) (or a synthesized
10
+ // one-line dump) to stdout, exit 0.
8
11
  // • setup/restore terminal (alt-screen, raw); restore exactly once, however we
9
12
  // leave (quit / Ctrl-C / crash / process exit).
10
- // • single-flight async lane: at most ONE async hook (refresh or an async
11
- // onKey) in flight; a busy indicator shows in the chrome; keystrokes that
12
- // arrive mid-flight are DROPPED (this paces fetches/sends).
13
+ // • single-flight async lane: at most ONE async intent in flight; a busy
14
+ // indicator shows in the chrome; keystrokes that arrive mid-flight are
15
+ // DROPPED (this paces fetches/sends).
13
16
  // • chrome: title row + separator on top; an error banner + a footer (status
14
- // left, keymap hints right) on the bottom; the view gets the content Rect.
15
- // • loop: parseKeypress → view.onKeyViewAction → render; refreshMs polling;
17
+ // left, keymap hints right) on the bottom; the presenter gets the content Rect.
18
+ // • loop: parseKeypress → keymapdispatch(intent) → render; refreshMs polling;
16
19
  // resize → render (NOT refresh) so a resize mid-fetch repaints from current
17
- // state without re-entering the in-flight hook.
20
+ // state without re-entering the in-flight intent.
18
21
  import { setupTerminal, restoreTerminal, getTerminalSize, parseKeypress, } from './terminal.js';
19
22
  import { createDraw, detectColorCaps } from './draw.js';
23
+ import { initialChrome, deriveState } from '../view/chrome.js';
24
+ import { createLocalTransport } from '../view/transport-local.js';
20
25
  // Numeric SGR codes only (a color NAME emits a broken CSI — see draw.ts guard).
21
26
  const FG = { cyan: '36', green: '32', yellow: '33', red: '31', grey: '90' };
22
27
  /** The persistent state signal: word + glyph + hue, derived from host signals so
@@ -43,19 +48,6 @@ const BANNER = {
43
48
  action: { glyph: '▸', fg: FG.yellow },
44
49
  error: { glyph: '✗', fg: FG.red },
45
50
  };
46
- /** Derive the one state chip from host signals (resume guidance): busy→working,
47
- * error banner→blocked, action banner→attention, else ready (once loaded) / idle.
48
- * An info banner does not block — the chip stays ready/idle and the cyan banner
49
- * carries the notice. */
50
- function deriveState(c) {
51
- if (c.busy)
52
- return 'working';
53
- if (c.banner?.level === 'error')
54
- return 'blocked';
55
- if (c.banner?.level === 'action')
56
- return 'attention';
57
- return c.loaded ? 'ready' : 'idle';
58
- }
59
51
  /** Color-code the transient footer status by kind (not dim, so it leads): a
60
52
  * trailing … or an in-progress verb → cyan; a completed verb → green; else plain.
61
53
  * The words carry meaning; the hue is decorative (mono-safe). */
@@ -187,37 +179,140 @@ function drawChrome(draw, size, manifest, c, now = Date.now()) {
187
179
  return { row: top, col: 0, width: cols, height };
188
180
  }
189
181
  export { drawChrome };
190
- /** Host a view in the alt screen until it quits (or Ctrl-C). */
191
- export async function runView(view, opts = {}) {
182
+ // ── Dual-target core host (runCoreView) ─────────────────────────────────────
183
+ //
184
+ // Hosts a ViewCore + TuiPresenter under the dual-target contract
185
+ // (src/core/view/contract.ts). The model is the immutable-state + intents thunk
186
+ // runtime: the core owns all state + behavior, presenters are pure reads, and a
187
+ // keystroke maps through the keymap to a named intent the host dispatches.
188
+ /** Tokens a keystroke can match a keymap binding's `keys` against. Arrows →
189
+ * up/down/left/right, return → return|enter, escape → escape|esc, a printable
190
+ * char → the char itself (+ 'space' for ' '), ctrl+x → ctrl+x|c-x. */
191
+ function keyTokens(input, key) {
192
+ const t = [];
193
+ if (key.upArrow)
194
+ t.push('up');
195
+ if (key.downArrow)
196
+ t.push('down');
197
+ if (key.leftArrow)
198
+ t.push('left');
199
+ if (key.rightArrow)
200
+ t.push('right');
201
+ if (key.return)
202
+ t.push('return', 'enter');
203
+ if (key.escape)
204
+ t.push('escape', 'esc');
205
+ if (key.tab)
206
+ t.push('tab');
207
+ if (key.shiftTab)
208
+ t.push('shifttab');
209
+ if (key.backspace)
210
+ t.push('backspace');
211
+ if (key.ctrl && input)
212
+ t.push(`ctrl+${input}`, `c-${input}`);
213
+ if (!key.ctrl && !key.escape && input.length === 1)
214
+ t.push(input);
215
+ if (input === ' ')
216
+ t.push('space');
217
+ return t;
218
+ }
219
+ /** A printable text edit (for the capture line-editor): a non-empty input with
220
+ * no special-key flag set. */
221
+ function isPrintable(input, key) {
222
+ if (key.ctrl || key.meta || key.escape || key.return || key.tab || key.backspace
223
+ || key.upArrow || key.downArrow || key.leftArrow || key.rightArrow || key.shiftTab)
224
+ return false;
225
+ return input.length >= 1;
226
+ }
227
+ /** Strip control chars so a paste can't smuggle ANSI into the draft buffer. */
228
+ function sanitizePrintable(s) {
229
+ return Array.from(s).filter((ch) => ch.charCodeAt(0) >= 32).join('');
230
+ }
231
+ /** The first non-capture binding whose key matches and whose `when` (if any)
232
+ * passes — returns the intent name + resolved payload. */
233
+ function matchBinding(keymap, input, key, state) {
234
+ const tokens = keyTokens(input, key);
235
+ for (const b of keymap) {
236
+ if ('capture' in b)
237
+ continue;
238
+ if (b.when && !b.when(state))
239
+ continue;
240
+ if (b.keys.some((k) => tokens.includes(k))) {
241
+ return { intent: b.intent, payload: b.payload ? b.payload(state) : undefined };
242
+ }
243
+ }
244
+ return null;
245
+ }
246
+ /** The active text-capture binding for this state, if any (compose-mode entry). */
247
+ function activeCapture(keymap, state) {
248
+ for (const b of keymap) {
249
+ if ('capture' in b && b.when(state))
250
+ return b;
251
+ }
252
+ return null;
253
+ }
254
+ /** Synthesize a one-line dump for a view that ships no text.mjs: `<title>` plus
255
+ * ` — <n> items` if state has an obvious top-level list. */
256
+ function synthDump(title, state) {
257
+ if (state && typeof state === 'object') {
258
+ for (const v of Object.values(state)) {
259
+ if (Array.isArray(v))
260
+ return `${title} — ${v.length} items`;
261
+ }
262
+ }
263
+ return title;
264
+ }
265
+ /** Run a READ source / WRITE command through a transport → typed Result. */
266
+ async function runRequest(transport, src, args) {
267
+ const raw = await transport.send(src.request(args));
268
+ return src.parse(raw);
269
+ }
270
+ /** Host a dual-target ViewCore + TuiPresenter in the alt screen until it quits
271
+ * (or Ctrl-C). `text` is the optional text presenter for the piped path. */
272
+ export async function runCoreView(core, tui, text, opts = {}) {
192
273
  const options = Object.freeze({ ...(opts.options ?? {}) });
193
- const chrome = { status: null, banner: null, busy: false, loaded: false, lastRefresh: 0, tick: 0, subtitle: null, mode: null };
194
- const host = {
195
- options,
196
- setStatus(msg) { chrome.status = msg; },
197
- setBanner(msg, level) { chrome.banner = msg == null ? null : { msg, level }; },
198
- setError(msg) { chrome.banner = msg == null ? null : { msg, level: 'error' }; },
199
- setSubtitle(s) { chrome.subtitle = s; },
200
- setMode(mode) { chrome.mode = mode; },
201
- };
202
- // ── Non-TTY / piped path: build state, best-effort load, dump, exit 0. ──
274
+ const transport = createLocalTransport({ cwd: process.cwd() });
275
+ // ── Non-TTY / piped path: init, best-effort refresh, dump, exit 0. ──
203
276
  if (!process.stdin.isTTY) {
204
- const state = await view.init(host);
205
- if (view.refresh) {
277
+ let dstate = core.init(options);
278
+ const dchrome = initialChrome();
279
+ const dsignal = {
280
+ setStatus() { }, setBanner(m, l) { dchrome.banner = { msg: m, level: l }; }, clearBanner() { dchrome.banner = null; },
281
+ setSubtitle() { }, setMode() { }, quit() { },
282
+ };
283
+ const dctx = {
284
+ get state() { return dstate; },
285
+ set(next) { dstate = typeof next === 'function' ? next(dstate) : next; },
286
+ resolve: (s, a) => runRequest(transport, s, a),
287
+ execute: (c, a) => runRequest(transport, c, a),
288
+ signal: dsignal,
289
+ dispatch: async (name, payload) => { const it = core.intents[name]; if (it)
290
+ await it(dctx, payload); },
291
+ };
292
+ const refresh = core.intents['refresh'];
293
+ if (refresh) {
206
294
  try {
207
- await view.refresh(state, host);
295
+ await refresh(dctx, undefined);
208
296
  }
209
297
  catch { /* dump current state regardless */ }
210
298
  }
211
- // Thread the host's current banner so a view can surface guidance without
212
- // mirroring it into state (older views ignore the arg).
213
- let text = view.dump(state, { banner: chrome.banner });
214
- if (!text.endsWith('\n'))
215
- text += '\n';
216
- process.stdout.write(text);
299
+ let out = text ? text.dump(dstate, { banner: dchrome.banner }) : synthDump(core.manifest.title, dstate);
300
+ if (!out.endsWith('\n'))
301
+ out += '\n';
302
+ process.stdout.write(out);
217
303
  return;
218
304
  }
219
305
  const caps = detectColorCaps();
220
- const state = await view.init(host);
306
+ let state = core.init(options);
307
+ const chrome = initialChrome();
308
+ let tick = 0;
309
+ // Footer hints come from the keymap bindings' `hint` field (single source of
310
+ // truth) — projected into the (ChromeManifest, Chrome) shape drawChrome reads.
311
+ const hints = [];
312
+ for (const b of tui.keymap) {
313
+ if (b.hint)
314
+ hints.push(b.hint);
315
+ }
221
316
  // Restore the terminal exactly once, however we leave.
222
317
  let restored = false;
223
318
  const cleanup = () => {
@@ -233,23 +328,24 @@ export async function runView(view, opts = {}) {
233
328
  const render = () => {
234
329
  const size = getTerminalSize();
235
330
  const { draw, frame } = createDraw(size, caps);
236
- const content = drawChrome(draw, size, view.manifest, chrome);
331
+ const chromeManifest = { ...core.manifest, keymap: hints };
332
+ const tickChrome = { ...chrome, tick };
333
+ const content = drawChrome(draw, size, chromeManifest, tickChrome);
237
334
  try {
238
- view.render(state, draw, content);
335
+ tui.render(state, draw, content);
239
336
  }
240
337
  catch (e) {
241
338
  chrome.banner = { msg: `render error: ${errText(e)}`, level: 'error' };
242
339
  }
243
340
  process.stdout.write(frame());
244
341
  };
245
- // Busy-tick repaint: while an async op is in flight, a ~120ms timer advances
246
- // the spinner + re-renders so live setStatus narration shows. Render-only — it
247
- // NEVER re-enters a hook, and Ctrl-C still escapes (handled before the drop).
342
+ // Busy-tick repaint: advance the spinner + repaint while an async intent runs
343
+ // so live setStatus narration shows. Render-only — never re-enters an intent.
248
344
  let tickTimer;
249
345
  const startBusyTick = () => {
250
346
  if (tickTimer)
251
347
  return;
252
- tickTimer = setInterval(() => { chrome.tick++; render(); }, 120);
348
+ tickTimer = setInterval(() => { tick++; render(); }, 120);
253
349
  };
254
350
  const stopBusyTick = () => {
255
351
  if (tickTimer) {
@@ -257,120 +353,155 @@ export async function runView(view, opts = {}) {
257
353
  tickTimer = undefined;
258
354
  }
259
355
  };
260
- // The single-flight lane. Returns true if the op ran (false if one was already
261
- // in flight). Always repaints around the op so the busy indicator shows.
356
+ // Loop control hoisted so signal.quit() can finish from inside an intent.
357
+ let done = false;
358
+ let resolveLoop = () => { };
359
+ const loopDone = new Promise((res) => { resolveLoop = res; });
360
+ let loopTimer;
361
+ const finish = () => {
362
+ if (done)
363
+ return;
364
+ done = true;
365
+ if (loopTimer)
366
+ clearInterval(loopTimer);
367
+ stopBusyTick();
368
+ cleanup();
369
+ resolveLoop();
370
+ };
371
+ const signal = {
372
+ setStatus(msg) { chrome.status = msg; },
373
+ setBanner(msg, level) { chrome.banner = msg == null ? null : { msg, level }; },
374
+ clearBanner() { chrome.banner = null; },
375
+ setSubtitle(s) { chrome.subtitle = s; },
376
+ setMode(mode) { chrome.mode = mode; },
377
+ quit() { finish(); },
378
+ };
379
+ // The single-flight busy lane lives in dispatch. The INPUT layer drops
380
+ // keystrokes while busy (paces fetches); a chained ctx.dispatch from inside an
381
+ // intent runs inline without re-toggling the lane.
262
382
  let busy = false;
263
- const runRefresh = async () => {
264
- if (!view.refresh || busy)
383
+ const markRefreshed = (name) => {
384
+ if (name === 'refresh') {
385
+ chrome.loaded = true;
386
+ chrome.lastRefresh = Date.now();
387
+ }
388
+ };
389
+ const makeCtx = () => ({
390
+ get state() { return state; },
391
+ set(next) { state = typeof next === 'function' ? next(state) : next; render(); },
392
+ resolve: (s, a) => runRequest(transport, s, a),
393
+ execute: (c, a) => runRequest(transport, c, a),
394
+ signal,
395
+ dispatch: (name, payload) => dispatch(name, payload),
396
+ });
397
+ const dispatch = async (intentName, payload) => {
398
+ if (done)
265
399
  return;
266
- busy = true;
267
- chrome.busy = true;
268
- render();
269
- startBusyTick();
400
+ const intent = core.intents[intentName];
401
+ if (!intent) {
402
+ chrome.banner = { msg: `unknown intent: ${intentName}`, level: 'error' };
403
+ render();
404
+ return;
405
+ }
406
+ const ctx = makeCtx();
407
+ let res;
270
408
  try {
271
- await view.refresh(state, host);
409
+ res = intent(ctx, payload);
272
410
  }
273
411
  catch (e) {
274
412
  chrome.banner = { msg: errText(e), level: 'error' };
275
- }
276
- finally {
277
- busy = false;
278
- chrome.busy = false;
279
- chrome.loaded = true;
280
- chrome.lastRefresh = Date.now();
281
- stopBusyTick();
413
+ markRefreshed(intentName);
282
414
  render();
415
+ return;
283
416
  }
284
- };
285
- setupTerminal();
286
- render(); // initial loading paint (before any fetch)
287
- await runRefresh(); // first data load
288
- await new Promise((resolveLoop) => {
289
- let done = false;
290
- let timer;
291
- const finish = () => {
292
- if (done)
293
- return;
294
- done = true;
295
- if (timer)
296
- clearInterval(timer);
297
- stopBusyTick();
298
- cleanup();
299
- resolveLoop();
300
- };
301
- if (typeof view.manifest.refreshMs === 'number' && view.manifest.refreshMs > 0) {
302
- timer = setInterval(() => { void runRefresh(); }, view.manifest.refreshMs);
303
- }
304
- const apply = async (action) => {
305
- switch (action.type) {
306
- case 'render':
307
- render();
308
- break;
309
- case 'refresh':
310
- await runRefresh();
311
- break;
312
- case 'quit':
313
- finish();
314
- break;
315
- case 'none': break;
417
+ if (res instanceof Promise) {
418
+ const wasBusy = busy;
419
+ if (!wasBusy) {
420
+ busy = true;
421
+ chrome.busy = true;
422
+ render();
423
+ startBusyTick();
316
424
  }
317
- };
318
- const onData = async (data) => {
319
- if (done)
320
- return;
321
- let parsed;
322
425
  try {
323
- parsed = parseKeypress(data);
426
+ await res;
324
427
  }
325
- catch {
326
- return;
428
+ catch (e) {
429
+ chrome.banner = { msg: errText(e), level: 'error' };
327
430
  }
328
- const { input, key } = parsed;
329
- // Ctrl-C is the universal escape hatch — works even mid-flight.
330
- if (key.ctrl && input === 'c') {
331
- finish();
332
- return;
431
+ finally {
432
+ if (!wasBusy) {
433
+ busy = false;
434
+ chrome.busy = false;
435
+ stopBusyTick();
436
+ }
437
+ markRefreshed(intentName);
438
+ render();
333
439
  }
334
- // Drop keystrokes while an async op is in flight (paces fetch/send).
335
- if (busy)
336
- return;
337
- if (!view.onKey) {
338
- if (input === 'q')
339
- finish(); // minimal default so a no-onKey view escapes
440
+ }
441
+ else {
442
+ markRefreshed(intentName);
443
+ render();
444
+ }
445
+ };
446
+ // ── Mount: init → loading frame → first refresh (if the view has one). ──
447
+ setupTerminal();
448
+ render();
449
+ if (core.intents['refresh'])
450
+ await dispatch('refresh');
451
+ // Capture-mode buffer: the host's line-editor draft while a `capture` binding's
452
+ // when(state) is true; reset to '' whenever no capture binding is active.
453
+ let captureBuf = '';
454
+ if (typeof core.manifest.refreshMs === 'number' && core.manifest.refreshMs > 0) {
455
+ loopTimer = setInterval(() => { if (!busy)
456
+ void dispatch('refresh'); }, core.manifest.refreshMs);
457
+ }
458
+ const onData = async (data) => {
459
+ if (done)
460
+ return;
461
+ let parsed;
462
+ try {
463
+ parsed = parseKeypress(data);
464
+ }
465
+ catch {
466
+ return;
467
+ }
468
+ const { input, key } = parsed;
469
+ // Ctrl-C is the universal escape hatch — works even mid-flight.
470
+ if (key.ctrl && input === 'c') {
471
+ finish();
472
+ return;
473
+ }
474
+ // Drop keystrokes while an async intent is in flight (paces fetch/send).
475
+ if (busy)
476
+ return;
477
+ // Text-capture: while a capture binding is active, printable/backspace edit
478
+ // the host buffer and dispatch capture(nextDraft); other keys (return/escape)
479
+ // fall through to the keymap so submit/cancel can be bound.
480
+ const cap = activeCapture(tui.keymap, state);
481
+ if (cap) {
482
+ if (isPrintable(input, key)) {
483
+ captureBuf += sanitizePrintable(input);
484
+ await dispatch(cap.capture, captureBuf);
340
485
  return;
341
486
  }
342
- try {
343
- const r = view.onKey({ input, key }, state, host);
344
- let action;
345
- if (r instanceof Promise) {
346
- busy = true;
347
- chrome.busy = true;
348
- render();
349
- startBusyTick();
350
- try {
351
- action = await r;
352
- }
353
- finally {
354
- busy = false;
355
- chrome.busy = false;
356
- stopBusyTick();
357
- }
358
- }
359
- else {
360
- action = r;
361
- }
362
- await apply(action);
363
- }
364
- catch (e) {
365
- chrome.banner = { msg: errText(e), level: 'error' };
366
- render();
487
+ if (key.backspace) {
488
+ captureBuf = captureBuf.slice(0, -1);
489
+ await dispatch(cap.capture, captureBuf);
490
+ return;
367
491
  }
368
- };
369
- process.stdin.on('data', (d) => { void onData(d); });
370
- // Resize → repaint from current state (never re-enter the in-flight hook).
371
- process.stdout.on('resize', () => { if (!done)
372
- render(); });
373
- });
492
+ }
493
+ else {
494
+ captureBuf = '';
495
+ }
496
+ const m = matchBinding(tui.keymap, input, key, state);
497
+ if (m)
498
+ await dispatch(m.intent, m.payload);
499
+ };
500
+ process.stdin.on('data', (d) => { void onData(d); });
501
+ // Resize → repaint from current state (never re-enter an in-flight intent).
502
+ process.stdout.on('resize', () => { if (!done)
503
+ render(); });
504
+ await loopDone;
374
505
  }
375
506
  function errText(e) {
376
507
  if (e instanceof Error)
@@ -0,0 +1,10 @@
1
+ import type { Transport } from './transport.js';
2
+ export interface BridgeResult {
3
+ status: number;
4
+ body: string;
5
+ }
6
+ /** Decode a POST body as a SourceRequest and run it through `transport`,
7
+ * returning the HTTP status + JSON-encoded RawResponse the bridge should send
8
+ * back. Never throws — a malformed body or a transport failure both come back
9
+ * as a RawResponse with ok:false. */
10
+ export declare function runSourceRequest(transport: Transport, rawBody: string): Promise<BridgeResult>;
@@ -0,0 +1,31 @@
1
+ // bridge.ts — the server side of the local web transport (Node only).
2
+ //
3
+ // `crtr view serve` runs the portable core CLIENT-SIDE in the browser; its
4
+ // sources resolve over an HTTP Transport that POSTs each SourceRequest here.
5
+ // This helper decodes that POST body and runs it through any Transport (the
6
+ // local exec/file/http one today) — the single endpoint a cloud deploy
7
+ // replaces. Kept transport-agnostic + framework-free so it is trivially
8
+ // testable and tear-out-able.
9
+ function bad(stderr) {
10
+ const raw = { ok: false, stdout: '', stderr };
11
+ return { status: 400, body: JSON.stringify(raw) };
12
+ }
13
+ /** Decode a POST body as a SourceRequest and run it through `transport`,
14
+ * returning the HTTP status + JSON-encoded RawResponse the bridge should send
15
+ * back. Never throws — a malformed body or a transport failure both come back
16
+ * as a RawResponse with ok:false. */
17
+ export async function runSourceRequest(transport, rawBody) {
18
+ let req;
19
+ try {
20
+ req = JSON.parse(rawBody);
21
+ }
22
+ catch {
23
+ return bad('bridge: request body is not valid JSON');
24
+ }
25
+ const kind = req.kind;
26
+ if (!req || typeof req !== 'object' || (kind !== 'exec' && kind !== 'file' && kind !== 'http')) {
27
+ return bad(`bridge: request body is not a SourceRequest (kind must be exec|file|http, got ${JSON.stringify(kind)})`);
28
+ }
29
+ const raw = await transport.send(req);
30
+ return { status: 200, body: JSON.stringify(raw) };
31
+ }
@@ -0,0 +1,9 @@
1
+ import type { ChromeState } from './contract.js';
2
+ export type ChipState = 'working' | 'blocked' | 'attention' | 'ready' | 'idle';
3
+ /** Derive the state chip from host signals: busy→working, error banner→blocked,
4
+ * action banner→attention, else ready (once loaded) / idle. An info banner does
5
+ * not block. An explicit interaction mode (setMode) overrides the chip — that
6
+ * precedence is applied by each target's chrome renderer, not here. */
7
+ export declare function deriveState(c: ChromeState): ChipState;
8
+ /** A fresh chrome record (the shape both hosts start from). */
9
+ export declare function initialChrome(): ChromeState;
@@ -0,0 +1,22 @@
1
+ // chrome.ts — portable chrome-state derivation shared by both targets.
2
+ //
3
+ // The TUI's drawChrome and the web's <ViewChrome> both derive the one state
4
+ // chip from the same host signals via deriveState, so the two targets never
5
+ // drift on what "blocked" / "working" / "ready" mean.
6
+ /** Derive the state chip from host signals: busy→working, error banner→blocked,
7
+ * action banner→attention, else ready (once loaded) / idle. An info banner does
8
+ * not block. An explicit interaction mode (setMode) overrides the chip — that
9
+ * precedence is applied by each target's chrome renderer, not here. */
10
+ export function deriveState(c) {
11
+ if (c.busy)
12
+ return 'working';
13
+ if (c.banner?.level === 'error')
14
+ return 'blocked';
15
+ if (c.banner?.level === 'action')
16
+ return 'attention';
17
+ return c.loaded ? 'ready' : 'idle';
18
+ }
19
+ /** A fresh chrome record (the shape both hosts start from). */
20
+ export function initialChrome() {
21
+ return { status: null, banner: null, subtitle: null, mode: null, busy: false, loaded: false, lastRefresh: 0 };
22
+ }