@crouton-kit/crouter 0.3.28 → 0.3.30

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 (324) hide show
  1. package/dist/build-root.d.ts +15 -7
  2. package/dist/build-root.js +48 -34
  3. package/dist/builtin-memory/crouter-development/marketplaces.md +7 -6
  4. package/dist/builtin-memory/crouter-development/personas/base-prompt.md +1 -1
  5. package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +1 -1
  6. package/dist/builtin-memory/crouter-development/personas.md +1 -1
  7. package/dist/builtin-memory/crouter-development/plugins.md +20 -18
  8. package/dist/builtin-memory/design.md +1 -1
  9. package/dist/builtin-memory/development.md +1 -1
  10. package/dist/builtin-memory/internal/INDEX.md +1 -1
  11. package/dist/builtin-memory/internal/examples/INDEX.md +1 -1
  12. package/dist/builtin-memory/internal/examples/imessage-assistant.md +2 -2
  13. package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
  14. package/dist/builtin-memory/internal/storage-tiers.md +1 -1
  15. package/dist/builtin-memory/planning.md +1 -1
  16. package/dist/builtin-memory/spec.md +6 -4
  17. package/dist/builtin-personas/orchestration-kernel.md +4 -5
  18. package/dist/builtin-personas/runtime-base.md +5 -4
  19. package/dist/builtin-personas/spec/PERSONA.md +8 -4
  20. package/dist/builtin-personas/spec/orchestrator.md +5 -3
  21. package/dist/builtin-personas/spec/requirements/PERSONA.md +10 -0
  22. package/dist/builtin-views/canvas/core.mjs +586 -0
  23. package/dist/builtin-views/canvas/text.mjs +58 -0
  24. package/dist/builtin-views/canvas/tui.mjs +165 -0
  25. package/dist/builtin-views/canvas/web.jsx +120 -0
  26. package/dist/builtin-views/git-pr/core.mjs +673 -0
  27. package/dist/builtin-views/git-pr/text.mjs +84 -0
  28. package/dist/builtin-views/git-pr/tui.mjs +302 -0
  29. package/dist/builtin-views/git-pr/web.jsx +216 -0
  30. package/dist/builtin-views/inbox/_lib/render.mjs +1 -3
  31. package/dist/builtin-views/inbox/core.mjs +1273 -0
  32. package/dist/builtin-views/inbox/text.mjs +73 -0
  33. package/dist/builtin-views/inbox/tui.mjs +312 -0
  34. package/dist/builtin-views/inbox/web.jsx +188 -0
  35. package/dist/builtin-views/linkedin/core.mjs +906 -0
  36. package/dist/builtin-views/linkedin/text.mjs +69 -0
  37. package/dist/builtin-views/linkedin/tui.mjs +427 -0
  38. package/dist/builtin-views/linkedin/web.jsx +206 -0
  39. package/dist/builtin-views/workspace-sidebar/core.mjs +621 -0
  40. package/dist/builtin-views/workspace-sidebar/text.mjs +53 -0
  41. package/dist/builtin-views/workspace-sidebar/tui.mjs +142 -0
  42. package/dist/builtin-views/workspace-sidebar/web.jsx +109 -0
  43. package/dist/cli.js +22 -17
  44. package/dist/clients/attach/__tests__/action-parity.test.js +48 -0
  45. package/dist/clients/attach/__tests__/clipboard-image.test.js +76 -0
  46. package/dist/clients/attach/__tests__/editor-newline.test.js +57 -0
  47. package/dist/clients/attach/__tests__/git-info.test.js +31 -0
  48. package/dist/clients/attach/__tests__/onrequest-wired.test.js +20 -0
  49. package/dist/clients/attach/__tests__/slash-quit-copy.test.js +42 -0
  50. package/dist/clients/attach/__tests__/titled-editor.test.js +39 -0
  51. package/dist/clients/attach/attach-cmd.js +282 -46
  52. package/dist/clients/attach/auth-pickers.d.ts +9 -0
  53. package/dist/clients/attach/auth-pickers.js +194 -0
  54. package/dist/clients/attach/canvas-panels.d.ts +4 -2
  55. package/dist/clients/attach/canvas-panels.js +45 -17
  56. package/dist/clients/attach/chat-view.d.ts +27 -2
  57. package/dist/clients/attach/chat-view.js +98 -9
  58. package/dist/clients/attach/clipboard-image.d.ts +47 -8
  59. package/dist/clients/attach/clipboard-image.js +150 -26
  60. package/dist/clients/attach/clipboard-text.d.ts +3 -0
  61. package/dist/clients/attach/clipboard-text.js +39 -0
  62. package/dist/clients/attach/config-load.d.ts +54 -7
  63. package/dist/clients/attach/config-load.js +99 -3
  64. package/dist/clients/attach/context-message.d.ts +12 -0
  65. package/dist/clients/attach/context-message.js +72 -0
  66. package/dist/clients/attach/git-info.d.ts +20 -0
  67. package/dist/clients/attach/git-info.js +44 -0
  68. package/dist/clients/attach/graph-overlay.d.ts +13 -4
  69. package/dist/clients/attach/graph-overlay.js +59 -19
  70. package/dist/clients/attach/input-controller.d.ts +75 -13
  71. package/dist/clients/attach/input-controller.js +305 -67
  72. package/dist/clients/attach/pickers.d.ts +58 -0
  73. package/dist/clients/attach/pickers.js +171 -0
  74. package/dist/clients/attach/slash-commands.d.ts +22 -0
  75. package/dist/clients/attach/slash-commands.js +91 -25
  76. package/dist/clients/attach/titled-editor.d.ts +33 -0
  77. package/dist/clients/attach/titled-editor.js +84 -0
  78. package/dist/clients/attach/view-socket.d.ts +18 -1
  79. package/dist/clients/attach/view-socket.js +70 -1
  80. package/dist/clients/web/dev-server.d.ts +7 -0
  81. package/dist/clients/web/dev-server.js +59 -0
  82. package/dist/clients/web/events.d.ts +14 -0
  83. package/dist/clients/web/events.js +151 -0
  84. package/dist/clients/web/server.d.ts +18 -0
  85. package/dist/clients/web/server.js +450 -0
  86. package/dist/clients/web/web-cmd.d.ts +2 -0
  87. package/dist/clients/web/web-cmd.js +120 -0
  88. package/dist/commands/canvas.js +1 -2
  89. package/dist/commands/chord.js +4 -25
  90. package/dist/commands/dashboard.js +5 -1
  91. package/dist/commands/memory/__tests__/lint-schema.test.js +6 -6
  92. package/dist/commands/memory/lint.js +14 -2
  93. package/dist/commands/memory/read.js +2 -2
  94. package/dist/commands/memory/shared.d.ts +7 -1
  95. package/dist/commands/memory/shared.js +18 -3
  96. package/dist/commands/memory/write.js +8 -4
  97. package/dist/commands/memory.js +4 -4
  98. package/dist/commands/node.d.ts +1 -1
  99. package/dist/commands/node.js +64 -65
  100. package/dist/commands/pkg.js +1 -2
  101. package/dist/commands/revive.js +11 -3
  102. package/dist/commands/sys/sync.d.ts +1 -0
  103. package/dist/commands/sys/sync.js +187 -0
  104. package/dist/commands/sys.js +3 -2
  105. package/dist/commands/view-cycle.js +2 -2
  106. package/dist/commands/view-list.js +8 -8
  107. package/dist/commands/view-new.js +21 -17
  108. package/dist/commands/view-pick.js +1 -1
  109. package/dist/commands/view-run.js +35 -14
  110. package/dist/commands/view.js +8 -6
  111. package/dist/commands/workspace.d.ts +2 -0
  112. package/dist/commands/workspace.js +161 -0
  113. package/dist/core/__tests__/broker-double-spawn.test.d.ts +1 -0
  114. package/dist/core/__tests__/broker-double-spawn.test.js +143 -0
  115. package/dist/core/__tests__/broker-fork-seam.test.d.ts +1 -0
  116. package/dist/core/__tests__/broker-fork-seam.test.js +102 -0
  117. package/dist/core/__tests__/broker-sdk-wiring.test.js +2 -0
  118. package/dist/core/__tests__/child-death-wake.test.js +56 -111
  119. package/dist/core/__tests__/close.test.js +14 -20
  120. package/dist/core/__tests__/context-intro.test.js +19 -19
  121. package/dist/core/__tests__/daemon-boot.test.js +30 -23
  122. package/dist/core/__tests__/draw-style.test.js +1 -1
  123. package/dist/core/__tests__/error-stall-recycle.test.d.ts +1 -0
  124. package/dist/core/__tests__/error-stall-recycle.test.js +141 -0
  125. package/dist/core/__tests__/fixtures/fake-engine.d.ts +17 -1
  126. package/dist/core/__tests__/fixtures/fake-engine.js +22 -2
  127. package/dist/core/__tests__/fixtures/fake-pi-host.js +2 -2
  128. package/dist/core/__tests__/focuses.test.js +25 -13
  129. package/dist/core/__tests__/fork.test.js +22 -5
  130. package/dist/core/__tests__/full/broker-control-preempt.test.d.ts +1 -0
  131. package/dist/core/__tests__/full/broker-control-preempt.test.js +61 -0
  132. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.d.ts +1 -0
  133. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.js +57 -0
  134. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.d.ts +1 -0
  135. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.js +80 -0
  136. package/dist/core/__tests__/full/broker-pane-resolution.test.js +90 -0
  137. package/dist/core/__tests__/full/cascade-close.test.js +22 -8
  138. package/dist/core/__tests__/full/detach-focus.test.js +66 -169
  139. package/dist/core/__tests__/helpers/harness.d.ts +6 -9
  140. package/dist/core/__tests__/helpers/harness.js +19 -36
  141. package/dist/core/__tests__/human-node-not-supervised.test.d.ts +1 -0
  142. package/dist/core/__tests__/human-node-not-supervised.test.js +83 -0
  143. package/dist/core/__tests__/lifecycle.test.js +1 -19
  144. package/dist/core/__tests__/listing-completeness.test.js +2 -2
  145. package/dist/core/__tests__/live-mutation-verbs.test.js +5 -5
  146. package/dist/core/__tests__/memory-resolver.test.js +1 -1
  147. package/dist/core/__tests__/on-read-dedup-resume.test.js +2 -2
  148. package/dist/core/__tests__/refresh-stall-recycle.test.d.ts +1 -0
  149. package/dist/core/__tests__/refresh-stall-recycle.test.js +88 -0
  150. package/dist/core/__tests__/relaunch-root.test.d.ts +1 -0
  151. package/dist/core/__tests__/relaunch-root.test.js +114 -0
  152. package/dist/core/__tests__/reset.test.js +19 -79
  153. package/dist/core/__tests__/steer-note.test.js +9 -7
  154. package/dist/core/__tests__/tmux-surface.test.js +13 -7
  155. package/dist/core/bootstrap.d.ts +0 -3
  156. package/dist/core/bootstrap.js +1 -143
  157. package/dist/core/canvas/browse/__tests__/model.test.js +86 -1
  158. package/dist/core/canvas/browse/__tests__/render.test.js +2 -1
  159. package/dist/core/canvas/browse/app.js +131 -8
  160. package/dist/core/canvas/browse/model.d.ts +23 -5
  161. package/dist/core/canvas/browse/model.js +68 -13
  162. package/dist/core/canvas/browse/render.d.ts +7 -1
  163. package/dist/core/canvas/browse/render.js +178 -47
  164. package/dist/core/canvas/canvas.js +1 -1
  165. package/dist/core/canvas/focuses.d.ts +4 -4
  166. package/dist/core/canvas/focuses.js +7 -7
  167. package/dist/core/canvas/nav-model.js +8 -7
  168. package/dist/core/canvas/render.d.ts +68 -9
  169. package/dist/core/canvas/render.js +195 -61
  170. package/dist/core/canvas/types.d.ts +16 -23
  171. package/dist/core/config.js +1 -4
  172. package/dist/core/memory-resolver.js +43 -5
  173. package/dist/core/runtime/bearings.d.ts +1 -1
  174. package/dist/core/runtime/bearings.js +8 -8
  175. package/dist/core/runtime/branded-host.d.ts +16 -0
  176. package/dist/core/runtime/branded-host.js +127 -0
  177. package/dist/core/runtime/broker-protocol.d.ts +212 -6
  178. package/dist/core/runtime/broker.d.ts +4 -1
  179. package/dist/core/runtime/broker.js +494 -58
  180. package/dist/core/runtime/close.d.ts +12 -2
  181. package/dist/core/runtime/close.js +37 -16
  182. package/dist/core/runtime/front-door.js +6 -14
  183. package/dist/core/runtime/host.d.ts +14 -34
  184. package/dist/core/runtime/host.js +14 -50
  185. package/dist/core/runtime/launch.d.ts +12 -9
  186. package/dist/core/runtime/launch.js +27 -15
  187. package/dist/core/runtime/lifecycle.d.ts +1 -1
  188. package/dist/core/runtime/lifecycle.js +15 -19
  189. package/dist/core/runtime/nodes.d.ts +0 -55
  190. package/dist/core/runtime/nodes.js +6 -74
  191. package/dist/core/runtime/placement.d.ts +91 -335
  192. package/dist/core/runtime/placement.js +291 -858
  193. package/dist/core/runtime/promote.d.ts +2 -0
  194. package/dist/core/runtime/promote.js +24 -6
  195. package/dist/core/runtime/recap.d.ts +8 -0
  196. package/dist/core/runtime/recap.js +107 -0
  197. package/dist/core/runtime/recycle.js +25 -61
  198. package/dist/core/runtime/reset.d.ts +43 -37
  199. package/dist/core/runtime/reset.js +131 -218
  200. package/dist/core/runtime/revive.d.ts +9 -29
  201. package/dist/core/runtime/revive.js +40 -139
  202. package/dist/core/runtime/spawn.d.ts +15 -16
  203. package/dist/core/runtime/spawn.js +165 -194
  204. package/dist/core/runtime/surface-bg.d.ts +11 -0
  205. package/dist/core/runtime/surface-bg.js +68 -0
  206. package/dist/core/runtime/tmux.d.ts +15 -52
  207. package/dist/core/runtime/tmux.js +35 -99
  208. package/dist/core/scope.d.ts +1 -2
  209. package/dist/core/scope.js +1 -5
  210. package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.d.ts +1 -0
  211. package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.js +57 -0
  212. package/dist/core/skill-sync/builtins.d.ts +42 -0
  213. package/dist/core/skill-sync/builtins.js +112 -0
  214. package/dist/core/skill-sync/claude-plugins.d.ts +23 -0
  215. package/dist/core/skill-sync/claude-plugins.js +71 -0
  216. package/dist/core/skill-sync/engine.d.ts +42 -0
  217. package/dist/core/skill-sync/engine.js +633 -0
  218. package/dist/core/skill-sync/export.d.ts +23 -0
  219. package/dist/core/skill-sync/export.js +86 -0
  220. package/dist/core/skill-sync/manifest.d.ts +64 -0
  221. package/dist/core/skill-sync/manifest.js +181 -0
  222. package/dist/core/skill-sync/profile.d.ts +76 -0
  223. package/dist/core/skill-sync/profile.js +173 -0
  224. package/dist/core/skill-sync/snapshot.d.ts +57 -0
  225. package/dist/core/skill-sync/snapshot.js +120 -0
  226. package/dist/core/substrate/index.d.ts +1 -1
  227. package/dist/core/substrate/index.js +1 -1
  228. package/dist/core/substrate/injected-store.js +3 -3
  229. package/dist/core/substrate/on-read.js +68 -6
  230. package/dist/core/substrate/render.d.ts +8 -11
  231. package/dist/core/substrate/render.js +29 -43
  232. package/dist/core/substrate/schema.d.ts +10 -3
  233. package/dist/core/substrate/schema.js +6 -3
  234. package/dist/core/tui/host.d.ts +12 -4
  235. package/dist/core/tui/host.js +280 -149
  236. package/dist/core/view/bridge.d.ts +10 -0
  237. package/dist/core/view/bridge.js +31 -0
  238. package/dist/core/view/chrome.d.ts +9 -0
  239. package/dist/core/view/chrome.js +22 -0
  240. package/dist/core/view/contract.d.ts +171 -0
  241. package/dist/core/view/contract.js +23 -0
  242. package/dist/core/view/loader.d.ts +31 -0
  243. package/dist/core/view/loader.js +188 -0
  244. package/dist/core/view/transport-local.d.ts +7 -0
  245. package/dist/core/view/transport-local.js +70 -0
  246. package/dist/core/view/transport.d.ts +4 -0
  247. package/dist/core/view/transport.js +15 -0
  248. package/dist/daemon/crtrd.d.ts +38 -27
  249. package/dist/daemon/crtrd.js +303 -376
  250. package/dist/daemon/manage.js +6 -1
  251. package/dist/index.js +1 -1
  252. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +28 -20
  253. package/dist/pi-extensions/canvas-context-intro.js +4 -4
  254. package/dist/pi-extensions/canvas-doc-substrate.js +16 -16
  255. package/dist/pi-extensions/canvas-nav.js +5 -0
  256. package/dist/pi-extensions/canvas-recap.d.ts +37 -0
  257. package/dist/pi-extensions/canvas-recap.js +236 -0
  258. package/dist/pi-extensions/canvas-resume.js +2 -1
  259. package/dist/pi-extensions/canvas-stophook.d.ts +14 -12
  260. package/dist/pi-extensions/canvas-stophook.js +87 -101
  261. package/dist/pi-extensions/canvas-view.js +2 -1
  262. package/dist/pi-extensions/widget-order-bus.d.ts +6 -0
  263. package/dist/pi-extensions/widget-order-bus.js +34 -0
  264. package/dist/prompts/view.d.ts +2 -2
  265. package/dist/prompts/view.js +145 -73
  266. package/dist/types.d.ts +1 -6
  267. package/dist/types.js +1 -3
  268. package/dist/web/ViewChrome.d.ts +7 -0
  269. package/dist/web/ViewChrome.js +28 -0
  270. package/dist/web/ViewPane.d.ts +39 -0
  271. package/dist/web/ViewPane.js +48 -0
  272. package/dist/web/index.d.ts +6 -0
  273. package/dist/web/index.js +16 -0
  274. package/dist/web/runtime.d.ts +39 -0
  275. package/dist/web/runtime.js +133 -0
  276. package/dist/web/states.d.ts +24 -0
  277. package/dist/web/states.js +24 -0
  278. package/dist/web/transport-http.d.ts +5 -0
  279. package/dist/web/transport-http.js +28 -0
  280. package/dist/web-client/assets/index-BUvQb4hR.css +2 -0
  281. package/dist/web-client/assets/index-ClLQXYAE.js +10 -0
  282. package/dist/web-client/index.html +13 -0
  283. package/package.json +20 -6
  284. package/dist/builtin-views/canvas/client.mjs +0 -303
  285. package/dist/builtin-views/canvas/view.mjs +0 -576
  286. package/dist/builtin-views/git-pr/client.mjs +0 -440
  287. package/dist/builtin-views/git-pr/view.mjs +0 -675
  288. package/dist/builtin-views/inbox/sources/gmail.mjs +0 -965
  289. package/dist/builtin-views/inbox/sources/linkedin.mjs +0 -427
  290. package/dist/builtin-views/inbox/view.mjs +0 -889
  291. package/dist/builtin-views/linkedin/client.mjs +0 -610
  292. package/dist/builtin-views/linkedin/view.mjs +0 -1171
  293. package/dist/commands/pkg/bridge.d.ts +0 -1
  294. package/dist/commands/pkg/bridge.js +0 -137
  295. package/dist/commands/skill/author.d.ts +0 -3
  296. package/dist/commands/skill/author.js +0 -140
  297. package/dist/commands/skill/shared.d.ts +0 -3
  298. package/dist/commands/skill/shared.js +0 -19
  299. package/dist/commands/skill.d.ts +0 -2
  300. package/dist/commands/skill.js +0 -21
  301. package/dist/commands/tmux-spread.d.ts +0 -2
  302. package/dist/commands/tmux-spread.js +0 -144
  303. package/dist/core/__tests__/full/daemon-liveness-pane.full.test.js +0 -292
  304. package/dist/core/__tests__/full/placement-focus.test.js +0 -309
  305. package/dist/core/__tests__/full/placement-reconcile.test.js +0 -212
  306. package/dist/core/__tests__/full/placement-revive.test.js +0 -238
  307. package/dist/core/__tests__/full/placement-teardown.test.js +0 -297
  308. package/dist/core/__tests__/home-session.test.js +0 -186
  309. package/dist/core/__tests__/relaunch.test.js +0 -335
  310. package/dist/core/bridge-map.d.ts +0 -19
  311. package/dist/core/bridge-map.js +0 -73
  312. package/dist/core/tui/contract.d.ts +0 -83
  313. package/dist/core/tui/contract.js +0 -8
  314. package/dist/core/tui/loader.d.ts +0 -16
  315. package/dist/core/tui/loader.js +0 -94
  316. package/dist/prompts/skill.d.ts +0 -2
  317. package/dist/prompts/skill.js +0 -650
  318. /package/dist/{core/__tests__/full/daemon-liveness-pane.full.test.d.ts → clients/attach/__tests__/action-parity.test.d.ts} +0 -0
  319. /package/dist/{core/__tests__/full/placement-focus.test.d.ts → clients/attach/__tests__/clipboard-image.test.d.ts} +0 -0
  320. /package/dist/{core/__tests__/full/placement-reconcile.test.d.ts → clients/attach/__tests__/editor-newline.test.d.ts} +0 -0
  321. /package/dist/{core/__tests__/full/placement-revive.test.d.ts → clients/attach/__tests__/git-info.test.d.ts} +0 -0
  322. /package/dist/{core/__tests__/full/placement-teardown.test.d.ts → clients/attach/__tests__/onrequest-wired.test.d.ts} +0 -0
  323. /package/dist/{core/__tests__/home-session.test.d.ts → clients/attach/__tests__/slash-quit-copy.test.d.ts} +0 -0
  324. /package/dist/{core/__tests__/relaunch.test.d.ts → clients/attach/__tests__/titled-editor.test.d.ts} +0 -0
@@ -28,14 +28,13 @@
28
28
  // crouter's own tsc build without a dep on the pi packages.
29
29
  import { writeFileSync, mkdirSync, existsSync, readFileSync } from 'node:fs';
30
30
  import { join } from 'node:path';
31
- import { getNode, jobDir, updateNode, recordPid, subscribersOf, setPresence } from '../core/canvas/index.js';
31
+ import { getNode, jobDir, updateNode, recordPid, setPresence } from '../core/canvas/index.js';
32
32
  import { transition } from '../core/runtime/lifecycle.js';
33
33
  import { markBusy, clearBusy } from '../core/runtime/busy.js';
34
34
  import { evaluateStop } from '../core/runtime/stop-guard.js';
35
35
  import { personaDrift, commitPersonaAck } from '../core/runtime/persona.js';
36
- import { reviveInPlace } from '../core/runtime/revive.js';
37
- import { handleNewSession, markCleanExitDone } from '../core/runtime/reset.js';
38
- import { focusOf, handFocusToManager, tearDownNode, closeFocusToShell } from '../core/runtime/placement.js';
36
+ import { handleNewSession, relaunchRoot, markCleanExitDone } from '../core/runtime/reset.js';
37
+ import { focusOf, tearDownNode } from '../core/runtime/placement.js';
39
38
  /** Condense a tool call into one short line for the activity cue. Picks the most
40
39
  * identifying arg (command / path / pattern / description), collapses whitespace,
41
40
  * and hard-caps the length so a giant bash command can't bloat telemetry.json. */
@@ -110,18 +109,22 @@ function lastAssistantMessage(messages) {
110
109
  // 185k+ pushy. Keyed on mode, NOT lifecycle: a terminal/orchestrator
111
110
  // yields against its roadmap exactly like a resident one (the
112
111
  // daemon's refresh-revive keys on intent='refresh', not lifecycle).
113
- // resident/base (a root conversation): never promoted ⇒ NO roadmap on disk,
114
- // so it is NOT told to checkpoint/yield against one. Instead: if the
115
- // chat is outgrowing one window into a multi-phase job, promote;
116
- // otherwise wrap up or start fresh. 130k gentle 150k firm → 185k+
112
+ // resident/base (a root conversation): NO roadmap on disk yet, so it is not
113
+ // told to checkpoint one. `crtr node yield` is still its continue
114
+ // verb it auto-promotes + seeds a roadmap transparently — offered
115
+ // against wrap-up, since a root chat can simply end. Gentle → firm →
117
116
  // pushy.
118
- // terminal/base (a worker): 130k/150k suggest promote 170k suggest promote
119
- // (+ "ignore if nearly done") 185k+ pushy.
117
+ // terminal/base (a worker): steered to `crtr node yield` to continue in a
118
+ // fresh window when work remains (yield auto-promotes under the
119
+ // hood), or `push final` when close. Gentle → firm → pushy.
120
120
  //
121
- // The promote/push-final guidance only makes sense for a terminal BASE worker. A
122
- // resident node finishes by yielding or being closed, not `push final`, so it is
123
- // never told to push final; only an ORCHESTRATOR (either lifecycle) has a roadmap
124
- // to yield against, so only it is steered at the roadmap.
121
+ // Every persona's "keep going" verb is `crtr node yield` NEVER a bare `node
122
+ // promote`, which framed promotion as a heavy "become an orchestrator" step and
123
+ // bred yield-avoidance. A base node's yield auto-promotes + seeds the roadmap
124
+ // under the hood, so the agent never decides to promote — it just yields. Only a
125
+ // terminal node is ever told `push final` (a resident finishes by yielding or
126
+ // being closed); only an orchestrator already HAS a roadmap to checkpoint before
127
+ // it yields.
125
128
  // ---------------------------------------------------------------------------
126
129
  const STEER_STEP = 10_000;
127
130
  // Shared escalation schedule (both lifecycles). Tightens as it climbs:
@@ -144,20 +147,22 @@ function steerBand(tokens) {
144
147
  return chosen;
145
148
  }
146
149
  /** The nudge text for a crossed band, specialized to the node's (mode,
147
- * lifecycle) persona + how far along the escalation it is.
150
+ * lifecycle) persona + how far along the escalation it is. `crtr node yield` is
151
+ * the single "keep going past this window" verb for every persona: it refreshes
152
+ * the node into a clean window against a roadmap, and for a base node
153
+ * auto-promotes transparently — so no persona is ever steered at a bare `node
154
+ * promote` (which framed promotion as a heavy "become an orchestrator" step and
155
+ * bred yield-avoidance).
148
156
  *
149
- * - orchestrator (terminal OR resident): checkpoint its roadmap and yield
150
- * (gently firmly → pushy). It has a context/roadmap.md to yield against.
151
- * - resident/base (root conversation): never promoted, so NO roadmap exists
152
- * steer it to PROMOTE if the chat is growing into a multi-phase job (which
153
- * seeds a roadmap), else wrap up / start fresh. Never points at roadmap.md or
154
- * a bare `node yield`, which for a roadmap-less root just drops context.
155
- * - terminal/base (worker): PROMOTE itself — become an orchestrator — when
156
- * work remains, with an "ignore if nearly done, finish with push final" once
157
- * it's deeper in.
157
+ * - orchestrator (terminal OR resident): checkpoint context/roadmap.md, then
158
+ * yield (it already has a roadmap to bring current first).
159
+ * - resident/base (root conversation): yield to continue in a fresh window if
160
+ * the chat is growing into a job, else wrap up — a root can simply end.
161
+ * - terminal/base (worker): yield to continue if work remains, or `push final`
162
+ * when close.
158
163
  *
159
- * At/past 185k every persona goes PUSHY: the context is long enough that
160
- * drifting further risks an overflow. */
164
+ * The wording strengthens with depth: gentle < 150k firm < 185k → pushy at/
165
+ * past 185k, where drifting further risks an overflow. */
161
166
  export function steerNote(at, lifecycle, mode) {
162
167
  const k = Math.round(at / 1000);
163
168
  const pushy = at >= 185_000;
@@ -173,25 +178,31 @@ export function steerNote(at, lifecycle, mode) {
173
178
  return `Context ~${k}k — this is getting long. Stop taking on new work now: checkpoint context/roadmap.md, hand off anything outstanding, and \`crtr node yield\` immediately to refresh before this context overflows.`;
174
179
  }
175
180
  if (lifecycle === 'resident') {
176
- // resident/base — a root conversation. It has no roadmap (only promotion
177
- // seeds one), so steer it toward promote-or-wrap-up, never at roadmap.md.
178
- const grow = `If this is turning into a multi-phase job, \`crtr node promote\` to become a resident orchestrator (seeds a roadmap so you can delegate and \`crtr node yield\` to refresh).`;
181
+ // resident/base — a root conversation with no roadmap yet. `crtr node yield`
182
+ // is still the natural "carry this forward" verb: it refreshes into a clean
183
+ // window against a roadmap and auto-promotes transparently (no scary
184
+ // "become an orchestrator" step). Offer wrap-up as the alternative — a root
185
+ // chat can simply end.
186
+ const grow = `If this is turning into a multi-phase job, \`crtr node yield\` to continue in a fresh window against a roadmap — no need to "become" anything, yield just carries you forward.`;
179
187
  if (at < 150_000) {
180
188
  return `Context ~${k}k and growing. ${grow} Otherwise no rush — wrap up when you reach a good stopping point.`;
181
189
  }
182
190
  if (!pushy) {
183
- return `Context ~${k}k. ${grow} If you're near done, just finish here; if there's more open-ended work, start a fresh \`crtr\` rather than letting this context grow.`;
191
+ return `Context ~${k}k. ${grow} If you're near done, just finish here; if there's more open-ended work, \`crtr node yield\` rather than letting this context grow.`;
184
192
  }
185
- return `Context ~${k}k — this is getting long. Wrap up now before this context overflows: finish what's in hand, or \`crtr node promote\` immediately if substantial work remains, otherwise continue in a fresh \`crtr\`.`;
193
+ return `Context ~${k}k — this is getting long. Wrap up now before this context overflows: finish what's in hand, or \`crtr node yield\` immediately to continue fresh against a roadmap if substantial work remains.`;
194
+ }
195
+ // terminal/base — a worker. The natural "keep going past this window" verb is
196
+ // `crtr node yield`: it refreshes into a clean window against a roadmap and
197
+ // auto-promotes transparently, so there is no separate "become an orchestrator"
198
+ // decision to agonize over. Steer it to yield-or-finish, never a bare promote.
199
+ if (at < 150_000) {
200
+ return `Context ~${k}k and growing. If you're near done, finish with \`crtr push final\`. If real work remains, \`crtr node yield\` to continue in a fresh window against a roadmap — no rush yet.`;
186
201
  }
187
- // terminal — a worker.
188
- const suggest = `If much more work remains than this context can finish, consider \`crtr node promote\` to become an orchestrator (seeds a roadmap, lets you delegate and \`crtr node yield\` to refresh).`;
189
- if (at < 170_000)
190
- return `Context ~${k}k. ${suggest}`;
191
202
  if (!pushy) {
192
- return `Context ~${k}k. ${suggest} If you're nearly done, ignore this suggestion and finish with \`crtr push final\`.`;
203
+ return `Context ~${k}k. If you're close, finish with \`crtr push final\`; otherwise \`crtr node yield\` now to refresh into a clean window and keep going against a roadmap.`;
193
204
  }
194
- return `Context ~${k}k — this is getting long. Wrap up now: \`crtr push final\` if you're close, otherwise \`crtr node promote\` immediately to continue as an orchestrator instead of overflowing this context.`;
205
+ return `Context ~${k}k — this is getting long. Wrap up now before this context overflows: \`crtr push final\` if you're close, otherwise \`crtr node yield\` immediately to continue in a fresh window against a roadmap.`;
195
206
  }
196
207
  // ---------------------------------------------------------------------------
197
208
  // Extension
@@ -240,13 +251,12 @@ export function registerCanvasStophook(pi) {
240
251
  // Why reason, not a closure flag: pi RE-ACTIVATES extensions on every session
241
252
  // swap, so any id we stash on boot is reset to its initial value before the
242
253
  // next session_start fires and can never observe the change — a `/new` then
243
- // looks identical to a boot. (That is exactly the bug this replaced: `/new`
244
- // silently fell back to an in-place reset instead of relaunching, so the node
245
- // id + context dir never changed.) The event reason is delivered fresh on
246
- // every fire and is immune to the re-activation.
254
+ // looks identical to a boot. The event reason is delivered fresh on every fire
255
+ // and is immune to the re-activation.
247
256
  //
248
- // For a root, reason 'new' means a brand-new graph: relaunch (park the old
249
- // root + boot a fresh node in this pane) or, with no pane, an in-place reset.
257
+ // `/new` splits by who ran it: a ROOT relaunches into a genuinely new node
258
+ // (relaunchRoot — park the old root `done`, boot a fresh node, re-point this
259
+ // pane), a non-root CHILD just refreshes its session id (handleNewSession).
250
260
  // ---------------------------------------------------------------------------
251
261
  pi.on('session_start', (event, ctx) => {
252
262
  try {
@@ -258,16 +268,19 @@ export function registerCanvasStophook(pi) {
258
268
  // whereas a bare id is resolved cwd-relative and forks across projects.
259
269
  const filed = ctx?.sessionManager?.getSessionFile?.();
260
270
  const sessionFile = typeof filed === 'string' && filed !== '' ? filed : null;
261
- // `/new` — a brand-new conversation in the same process. Route it: a
262
- // non-root child refreshes its session id; a ROOT in a tmux pane RELAUNCHES
263
- // (parks the old root + boots a fresh node in this pane via respawn-pane
264
- // -k, which tears down THIS pi); a root with no pane falls back to an
265
- // in-place reset. The relaunch's detached respawn may kill this pi before
266
- // the lines after the call run — that's fine; do not rely on anything
267
- // after handleNewSession.
271
+ // `/new` — a brand-new conversation in the same process. The broker already
272
+ // drove the engine-side new_session (the viewer's /new broker
273
+ // new_session frame); the runtime side then splits on root-ness: a ROOT
274
+ // relaunches into a freshly-minted node (park the old root `done`, boot a
275
+ // new broker, re-point the viewer pane), a non-root CHILD just refreshes
276
+ // its session id on the SAME node.
268
277
  if (event?.reason === 'new') {
269
278
  try {
270
- handleNewSession(nodeId, id, process.env['TMUX_PANE'], {}, sessionFile);
279
+ const m = getNode(nodeId);
280
+ if (m?.parent == null)
281
+ relaunchRoot(nodeId);
282
+ else
283
+ handleNewSession(nodeId, id, sessionFile);
271
284
  }
272
285
  catch { /* best-effort */ }
273
286
  // Clear in-memory context-steering so the fresh conversation starts clean.
@@ -277,13 +290,12 @@ export function registerCanvasStophook(pi) {
277
290
  return;
278
291
  }
279
292
  // Boot / startup / resume / reload / fork → (re)bind this process to its
280
- // session id, record our OS pid (the daemon's liveness signal for inline
281
- // roots whose window outlives pi), and CONFIRM any pending refresh-yield.
282
- // Reaching session_start proves a fresh pi actually booted, so it is now
283
- // safe to clear intent='refresh'. reviveInPlace deliberately leaves intent
284
- // set: the detached respawn it dispatches can't confirm itself (it kills
285
- // the caller mid-flight), so a real boot is the only thing allowed to clear
286
- // it — otherwise a failed respawn would look identical to a successful one.
293
+ // session id, record our OS pid (the daemon's broker-liveness signal), and
294
+ // CONFIRM any pending refresh-yield. Reaching session_start proves a fresh
295
+ // engine actually booted, so it is now safe to clear intent='refresh' — a
296
+ // refresh shuts the broker down and the daemon relaunches it FRESH, which
297
+ // leaves intent='refresh' set until this boot clears it (the only proof the
298
+ // relaunch worked; otherwise a failed relaunch would look like a success).
287
299
  const existing = getNode(nodeId);
288
300
  // Identity (session id/file) → meta; runtime (pid, intent) → atomic row setters.
289
301
  updateNode(nodeId, {
@@ -447,32 +459,18 @@ export function registerCanvasStophook(pi) {
447
459
  // transitions node.status → 'done' synchronously. Shut down cleanly.
448
460
  const node = getNode(nodeId);
449
461
  if (node?.status === 'done') {
450
- // TRULY-DONE (pushed `final` this turn). If this node owns the user's
451
- // viewport, its lifecycle successor takes the focus (§1.6):
452
- // handFocusToManager hands the focus row to the manager (the node up
453
- // the subscribes_to spine it reports to) and, when that manager's pi is
454
- // LIVE in the backstage, synchronously swaps it into this now-frozen
455
- // focus pane; a DORMANT manager is revived into the pane by the daemon
456
- // on the `final` it just pushed — either way no new window, no taint.
457
- // No manager (a root) or a manager already focused elsewhere → Q1-close
458
- // this focus AND flip remain-on-exit OFF on %m's window so the pane
459
- // closes when this pi exits (return-to-shell) instead of freezing into
460
- // an orphan. We CANNOT closePane(%m) from inside %m (self-saw), but the
461
- // pi is still alive mid-shutdown, so remain-on-exit-off is safe and
462
- // makes tmux reap the pane on exit. An unfocused done node just shuts
463
- // down (no pane anywhere, Invariant P). M is done → it owns no pane
464
- // (Invariant P), so null its own presence in BOTH sub-branches before
465
- // shutdown.
466
- const f = focusOf(nodeId);
467
- if (f !== null) {
468
- const managerId = node.parent ?? subscribersOf(nodeId)[0]?.node_id ?? null;
469
- if (!handFocusToManager(f.focus_id, managerId)) {
470
- // Q1 return-to-shell, self-saw-safe: close the focus row + disarm the
471
- // pane's freeze so it reaps on exit (we can't closePane our own pane).
472
- closeFocusToShell(f.focus_id, nodeId);
462
+ // TRULY-DONE (pushed `final` this turn). The engine is a detached
463
+ // broker, not a pane, so there is no engine to hand off — just close
464
+ // this node's VIEWER pane (if any) and shut the broker down. The daemon
465
+ // won't revive a done node. tearDownNode closes the viewer focus row +
466
+ // pane; ctx.shutdown() exits the broker process.
467
+ if (focusOf(nodeId) !== null) {
468
+ try {
469
+ tearDownNode(nodeId);
473
470
  }
471
+ catch { /* best-effort */ }
474
472
  }
475
- setPresence(nodeId, { pane: null, tmux_session: null, window: null }); // M done → owns no pane
473
+ setPresence(nodeId, { pane: null, tmux_session: null, window: null });
476
474
  try {
477
475
  ctx?.shutdown?.();
478
476
  }
@@ -480,27 +478,15 @@ export function registerCanvasStophook(pi) {
480
478
  return;
481
479
  }
482
480
  // (b') Refresh-yield: the node ran `crtr node yield` this turn, setting
483
- // intent='refresh'. Re-exec a FRESH pi IN PLACE in this same tmux
484
- // pane (respawn-pane -k) so the node re-reads its roadmap without
485
- // churning its window critically, an interactive/foreground root
486
- // is never dropped to a shell, and no daemon round-trip is needed
487
- // (the old window-death detection silently failed whenever pi
488
- // exited into a persistent shell pane). Falls back to a clean
489
- // shutdown (daemon revives in a new window) only when we're not in
490
- // a tmux pane.
481
+ // intent='refresh'. Just shut the broker down the daemon sees the
482
+ // dead pid + intent='refresh' and reviveNode's relaunches the broker
483
+ // FRESH (no resume) so the node re-reads its roadmap. (The daemon
484
+ // also force-recycles a stuck refresh whose engine never exits, so a
485
+ // stale in-process stophook can't strand the yield — see crtrd.)
491
486
  if (node?.intent === 'refresh') {
492
487
  // A yield is SILENT to subscribers: the node keeps its identity and
493
- // subscription edges across the revive and reports only through its
494
- // own explicit `crtr push` calls, so there is no checkpoint push here
495
- // — just re-exec a fresh pi in place against the roadmap.
496
- const pane = process.env['TMUX_PANE'];
497
- if (pane !== undefined && pane.trim() !== '') {
498
- try {
499
- reviveInPlace(nodeId, pane);
500
- return; // respawn-pane -k tears down this pi and starts the fresh one
501
- }
502
- catch { /* fall through to plain shutdown */ }
503
- }
488
+ // subscription edges across the revive and reports only through its own
489
+ // explicit `crtr push` calls, so there is no checkpoint push here.
504
490
  try {
505
491
  ctx?.shutdown?.();
506
492
  }
@@ -21,6 +21,7 @@
21
21
  // inside crouter's own tsc build without a dep on the pi packages (mirrors
22
22
  // canvas-resume.ts / canvas-nav.ts / canvas-commands.ts).
23
23
  import { execFile } from 'node:child_process';
24
+ import { surfaceTmuxStyleArgs } from '../core/runtime/surface-bg.js';
24
25
  /** Single-quote a string for safe interpolation into a `sh -c` command line —
25
26
  * tmux runs the display-popup trailing string through the shell, so a view
26
27
  * name with spaces or quotes must be escaped. */
@@ -59,7 +60,7 @@ export function registerCanvasView(pi) {
59
60
  // bare /view → the picker; /view <name> → that view directly.
60
61
  const cmd = name === '' ? 'crtr view pick' : `crtr view run ${shellQuote(name)}`;
61
62
  try {
62
- execFile('tmux', ['display-popup', '-E', '-w', '90%', '-h', '85%', cmd], () => { });
63
+ execFile('tmux', ['display-popup', '-E', '-w', '90%', '-h', '85%', ...surfaceTmuxStyleArgs(), cmd], () => { });
63
64
  }
64
65
  catch { /* best-effort */ }
65
66
  return;
@@ -0,0 +1,6 @@
1
+ /** Called by canvas-nav (once per session_start) to expose its scheduleRender. */
2
+ export declare function onNavRerender(cb: () => void): void;
3
+ /** Called by canvas-recap after it sets/clears `crtr-recap` to nudge canvas-nav
4
+ * to re-assert `crtr-managers` below it. No-op if nav has not registered (e.g.
5
+ * nav inert, or recap-only test harness). */
6
+ export declare function requestNavRerender(): void;
@@ -0,0 +1,34 @@
1
+ // widget-order-bus.ts — a tiny in-process bus that lets canvas-recap keep its
2
+ // recap widget pinned ABOVE canvas-nav's manager line.
3
+ //
4
+ // Why this exists: pi's interactive widget store (and the broker's relay) is
5
+ // insertion-ordered, and re-setting an existing widget key moves it to the END
6
+ // (bottom) — see interactive-mode.js `setExtensionWidget` (delete+set). So the
7
+ // LAST extension to call setWidget for an aboveEditor key renders closest to the
8
+ // editor. canvas-recap only paints during idle, when canvas-nav has not re-set
9
+ // `crtr-managers` for a long time, so a naive recap setWidget lands BELOW the
10
+ // manager line. The fix: after canvas-recap changes its widget, it asks
11
+ // canvas-nav to re-assert `crtr-managers`, which re-inserts that key after
12
+ // `crtr-recap` and so drops it below the recap — making the recap the topmost
13
+ // chrome.
14
+ //
15
+ // Both extensions load in the SAME pi process, so this module is a shared
16
+ // singleton via Node's module cache. No @earendil-works/* imports — compiles in
17
+ // crouter's own tsc build.
18
+ /** canvas-nav registers its re-render here on session_start; canvas-recap calls
19
+ * requestNavRerender() after it shows/clears the recap so the manager line
20
+ * re-inserts (and thus orders) below the recap widget. */
21
+ let navRerender;
22
+ /** Called by canvas-nav (once per session_start) to expose its scheduleRender. */
23
+ export function onNavRerender(cb) {
24
+ navRerender = cb;
25
+ }
26
+ /** Called by canvas-recap after it sets/clears `crtr-recap` to nudge canvas-nav
27
+ * to re-assert `crtr-managers` below it. No-op if nav has not registered (e.g.
28
+ * nav inert, or recap-only test harness). */
29
+ export function requestNavRerender() {
30
+ try {
31
+ navRerender?.();
32
+ }
33
+ catch { /* best-effort */ }
34
+ }
@@ -3,5 +3,5 @@ export interface ViewScaffoldVars {
3
3
  title: string;
4
4
  description: string;
5
5
  }
6
- /** Build the `view.mjs` source for a freshly scaffolded view. */
7
- export declare function viewScaffold(v: ViewScaffoldVars): string;
6
+ /** Build every file of a freshly scaffolded dual-target view, keyed by filename. */
7
+ export declare function viewScaffold(v: ViewScaffoldVars): Record<string, string>;
@@ -1,101 +1,173 @@
1
- // Scaffold stub for `crtr view new`. Returns the contents of a runnable
2
- // `view.mjs` a minimal, self-contained ViewModule that runs as-is via
3
- // `crtr view run <name>` and prints a sane snapshot when piped (`| cat`).
1
+ // Scaffold stubs for `crtr view new`. Returns the four files of a dual-target
2
+ // view directory — a portable `core.mjs` plus the TUI / web / text presenters
3
+ // that runs as-is via `crtr view run <name>` (TUI), `crtr view serve <name>`
4
+ // (web), and `crtr view run <name> | cat` (static snapshot).
4
5
  //
5
- // Authoring guidance lives INSIDE the stub (the comment block at the top of the
6
+ // Authoring guidance lives INSIDE each stub (the comment block at the top of the
6
7
  // generated file), so a view author reads the contract right where they edit.
7
8
  /** Escape a value for embedding inside a single-quoted JS string literal. */
8
9
  function jsStr(s) {
9
10
  return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
10
11
  }
11
- /** Build the `view.mjs` source for a freshly scaffolded view. */
12
- export function viewScaffold(v) {
12
+ /** The portable core: manifest · init · sources · intents. Runs in BOTH Node
13
+ * (the TUI host) and the browser (the web bundle) — imports NOTHING. */
14
+ function coreStub(v) {
13
15
  const id = jsStr(v.id);
14
16
  const title = jsStr(v.title);
15
17
  const description = jsStr(v.description);
16
- return `// ${v.title} — a crtr view.
18
+ return `// ${v.title} — portable core (manifest · init · sources · intents).
17
19
  //
18
- // A view is a self-contained ESM module the \`crtr view\` host loads and runs.
19
- // Run it: crtr view run ${v.id} (interactive, inside tmux)
20
- // crtr view run ${v.id} | cat (static snapshot, exits 0)
20
+ // Runs in BOTH Node (the TUI host) and the browser (the web bundle), so it
21
+ // imports NOTHING no \`node:*\`, no crtr. ALL state + behavior live here;
22
+ // presenters (tui.mjs / web.jsx) are pure reads that dispatch named intents.
21
23
  //
22
- // THE CONTRACT (the host injects everything — import NOTHING from crtr):
23
- // manifest { id, title, description, refreshMs?, keymap? }
24
- // id MUST equal this view's directory name.
25
- // init(host) build initial state — keep it CHEAP and sync-ish;
26
- // do NOT do slow fetches here (do them in refresh).
27
- // refresh(state,host) async data load; the host runs it on launch, every
28
- // manifest.refreshMs (if set), and whenever a key handler
29
- // returns { type: 'refresh' }. Mutate state in place.
30
- // render(state,draw,content) paint into the \`content\` Rect via draw.* —
31
- // a pure read of state; NEVER write ANSI yourself.
32
- // onKey(k,state,host) handle one keystroke; mutate state; return a ViewAction.
33
- // dump(state) plain text for the piped / non-TTY path (exit 0).
24
+ // Run it: crtr view run ${v.id} (TUI, inside tmux)
25
+ // crtr view run ${v.id} | cat (static snapshot, exits 0)
26
+ // crtr view serve ${v.id} (React + Tailwind web page)
34
27
  //
35
- // draw API (all absolute cells inside \`content\`):
36
- // draw.text(row,col,text,style?) · draw.spans(row,col,spans,maxWidth?)
37
- // draw.hline(row,fromCol?,toCol?) · draw.box(rect,title?)
38
- // draw.columns(rect,[1,2]) -> Rect[] (proportional split)
39
- // draw.list(rect,items,cursor,scroll) -> { scroll } (windows + cursor row)
40
- // style: { fg, bg, bold, dim, reverse } (fg/bg honored only when color is on)
41
- // host API:
42
- // host.options CLI --flags, camelCased & stringified (e.g. options.port)
43
- // host.setStatus(msg) transient footer-left status; null clears it
44
- // host.setError(msg) sticky red banner above the footer; null clears it
45
- // ViewAction: { type: 'render' | 'refresh' | 'quit' | 'none' }
28
+ // THE CONTRACT:
29
+ // manifest { id, title, description, refreshMs? } — id MUST equal
30
+ // this view's directory name.
31
+ // init(opts) cheap, SYNCHRONOUS initial state — NO fetch, NO screen.
32
+ // CLI flags arrive in \`opts\` (crtr view run --port/--target).
33
+ // sources { request, parse } READ descriptors. The host's transport
34
+ // runs request() (exec/file/http) and hands parse() the bytes
35
+ // your parse is pure, runs in Node AND the browser.
36
+ // intents semantic actions both targets dispatch. State updates are
37
+ // IMMUTABLE via ctx.set; async effects (ctx.resolve a source)
38
+ // live in the same handler. ctx.signal raises chrome signals.
46
39
 
47
- /** @type {import('@crouton-kit/crouter/dist/core/tui/contract.js').ViewModule} */
48
- const view = {
40
+ /** A READ source: a transport-agnostic request + a pure parse. */
41
+ export const clockSource = {
42
+ id: 'clock',
43
+ request: () => ({ kind: 'exec', bin: 'date', args: [] }),
44
+ parse: (raw) =>
45
+ raw.ok && raw.exitCode === 0
46
+ ? { ok: true, data: raw.stdout.trim() }
47
+ : { ok: false, error: { kind: 'clock-failed', display: {
48
+ headline: 'Could not read the clock',
49
+ explanation: raw.stderr || 'the date command failed',
50
+ nextStep: 'Check the \`date\` binary is on PATH.',
51
+ level: 'error', blocking: true } } },
52
+ };
53
+
54
+ /** @type {import('@crouton-kit/crouter/dist/core/view/contract.js').ViewCore} */
55
+ const core = {
49
56
  manifest: {
50
57
  id: '${id}',
51
58
  title: '${title}',
52
59
  description: '${description}',
53
60
  // refreshMs: 30000, // uncomment to auto-poll; omit ⇒ on-demand (g) only
54
- keymap: [
55
- { keys: 'g', label: 'refresh' },
56
- { keys: 'q', label: 'quit' },
57
- ],
58
61
  },
59
62
 
60
- init(host) {
61
- // Cheap, synchronous setup. Stash CLI flags off host.options here.
62
- return { count: 0, port: host.options.port ?? null };
63
- },
63
+ // Cheap, synchronous setup. Stash CLI flags off \`opts\` here.
64
+ init: (opts) => ({ count: 0, clock: null, port: opts.port ?? null }),
64
65
 
65
- async refresh(state, host) {
66
- // Load your data here (shell out, fetch, read files). Mutate \`state\`.
67
- state.count += 1;
68
- host.setStatus(\`refreshed \${state.count}×\`);
69
- },
66
+ sources: { clockSource },
70
67
 
71
- render(state, draw, content) {
72
- draw.text(content.row, content.col, '${title}', { bold: true });
73
- draw.text(content.row + 2, content.col, \`refreshed \${state.count} time(s)\`);
74
- draw.text(content.row + 3, content.col, \`port option: \${state.port ?? '(none)'}\`, { dim: true });
75
- draw.text(
76
- content.row + 5,
77
- content.col,
78
- 'Edit view.mjs to build your view g refreshes, q quits.',
79
- { dim: true },
80
- );
68
+ intents: {
69
+ // Async intent: ctx.set updates state immutably; ctx.resolve runs a source
70
+ // through the host transport. The host runs 'refresh' on launch, every
71
+ // manifest.refreshMs (if set), and on the \`g\` key (see tui.mjs).
72
+ async refresh(ctx) {
73
+ ctx.signal.setStatus('Reading clock…');
74
+ const r = await ctx.resolve(clockSource);
75
+ if (!r.ok) { ctx.signal.setBanner(r.error.display.headline, r.error.display.level); ctx.signal.setStatus(null); return; }
76
+ ctx.set((s) => ({ ...s, count: s.count + 1, clock: r.data }));
77
+ ctx.signal.clearBanner();
78
+ ctx.signal.setStatus(\`refreshed \${ctx.state.count}×\`);
79
+ },
80
+ quit: (ctx) => ctx.signal.quit(),
81
81
  },
82
+ };
82
83
 
83
- onKey(k, state, host) {
84
- if (k.input === 'q') return { type: 'quit' };
85
- if (k.input === 'g') return { type: 'refresh' };
86
- return { type: 'none' };
87
- },
84
+ export default core;
85
+ `;
86
+ }
87
+ /** The TUI presenter: render(state, draw, content) + keymap. Node only. */
88
+ function tuiStub(v) {
89
+ const title = jsStr(v.title);
90
+ return `// ${v.title} — TUI presenter: render(state, draw, content) + keymap.
91
+ //
92
+ // A pure read of state — paint via draw.*; NEVER write ANSI. Keys map to the
93
+ // core's intents (no logic here; the keymap names an intent, the host dispatches).
94
+ //
95
+ // draw API (all absolute cells inside \`content\`):
96
+ // draw.text(row,col,text,style?) · draw.spans(row,col,spans,maxWidth?)
97
+ // draw.hline(row,fromCol?,toCol?) · draw.box(rect,title?)
98
+ // draw.columns(rect,[1,2]) -> Rect[] · draw.list(rect,items,cursor,scroll)
99
+ // style: { fg, bg, bold, dim, reverse } (fg/bg honored only when color is on)
88
100
 
89
- dump(state) {
90
- return [
91
- '${title}',
92
- '',
93
- \`refreshed \${state.count} time(s)\`,
94
- \`port option: \${state.port ?? '(none)'}\`,
95
- ].join('\\n');
96
- },
97
- };
101
+ export const keymap = [
102
+ { keys: ['g'], intent: 'refresh', hint: { keys: 'g', label: 'refresh' } },
103
+ { keys: ['q'], intent: 'quit', hint: { keys: 'q', label: 'quit' } },
104
+ ];
98
105
 
99
- export default view;
106
+ export function render(state, draw, content) {
107
+ draw.text(content.row, content.col, '${title}', { bold: true });
108
+ draw.text(content.row + 2, content.col, \`clock: \${state.clock ?? '(loading…)'}\`);
109
+ draw.text(content.row + 3, content.col, \`refreshed \${state.count} time(s)\`, { dim: true });
110
+ draw.text(content.row + 4, content.col, \`port option: \${state.port ?? '(none)'}\`, { dim: true });
111
+ draw.text(content.row + 6, content.col, 'Edit core.mjs + tui.mjs to build your view — g refreshes, q quits.', { dim: true });
112
+ }
100
113
  `;
101
114
  }
115
+ /** The web presenter: a React + Tailwind component. Browser only (Vite owns the
116
+ * JSX + Tailwind transpile; this file is NEVER Node-imported). */
117
+ function webStub(v) {
118
+ const title = jsStr(v.title);
119
+ return `// ${v.title} — web presenter: a React + Tailwind component.
120
+ //
121
+ // A pure function of \`state\`; DOM events call \`dispatch(intent)\`. Consumed ONLY
122
+ // by \`crtr view serve\` (Vite owns JSX + Tailwind) — never Node-imported. The
123
+ // SAME state + intents the TUI reads; zero shared render code (an intentional fork).
124
+
125
+ export default function View({ state, dispatch }) {
126
+ return (
127
+ <div
128
+ className="font-mono text-sm p-4 outline-none"
129
+ tabIndex={0}
130
+ onKeyDown={(e) => { if (e.key === 'g') dispatch('refresh'); }}
131
+ >
132
+ <h1 className="text-lg font-bold">${title}</h1>
133
+ <p className="mt-2">clock: {state.clock ?? '(loading…)'}</p>
134
+ <p className="text-gray-500">refreshed {state.count} time(s)</p>
135
+ <button
136
+ className="mt-3 rounded bg-blue-600 px-2 py-1 text-white hover:bg-blue-500"
137
+ onClick={() => dispatch('refresh')}
138
+ >
139
+ Refresh
140
+ </button>
141
+ </div>
142
+ );
143
+ }
144
+ `;
145
+ }
146
+ /** The text presenter: the piped / non-TTY snapshot. Node only. Optional — omit
147
+ * it and the host synthesizes a generic one-line dump. */
148
+ function textStub(v) {
149
+ const title = jsStr(v.title);
150
+ return `// ${v.title} — text presenter: the piped / non-TTY snapshot (exits 0).
151
+ //
152
+ // A pure read of current state. Omit this file and the host synthesizes a
153
+ // one-line dump (\`<title> — <n> items\`).
154
+
155
+ export function dump(state) {
156
+ return [
157
+ '${title}',
158
+ '',
159
+ \`clock: \${state.clock ?? '(none)'}\`,
160
+ \`refreshed \${state.count} time(s)\`,
161
+ ].join('\\n');
162
+ }
163
+ `;
164
+ }
165
+ /** Build every file of a freshly scaffolded dual-target view, keyed by filename. */
166
+ export function viewScaffold(v) {
167
+ return {
168
+ 'core.mjs': coreStub(v),
169
+ 'tui.mjs': tuiStub(v),
170
+ 'web.jsx': webStub(v),
171
+ 'text.mjs': textStub(v),
172
+ };
173
+ }