@crouton-kit/crouter 0.3.27 → 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 (343) 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 +8 -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__/reconnect-giveup.test.js +30 -0
  49. package/dist/clients/attach/__tests__/slash-quit-copy.test.js +42 -0
  50. package/dist/clients/attach/__tests__/titled-editor.test.d.ts +1 -0
  51. package/dist/clients/attach/__tests__/titled-editor.test.js +39 -0
  52. package/dist/clients/attach/attach-cmd.js +445 -41
  53. package/dist/clients/attach/auth-pickers.d.ts +9 -0
  54. package/dist/clients/attach/auth-pickers.js +194 -0
  55. package/dist/clients/attach/canvas-panels.d.ts +12 -0
  56. package/dist/clients/attach/canvas-panels.js +78 -0
  57. package/dist/clients/attach/chat-view.d.ts +27 -2
  58. package/dist/clients/attach/chat-view.js +98 -9
  59. package/dist/clients/attach/clipboard-image.d.ts +47 -8
  60. package/dist/clients/attach/clipboard-image.js +150 -26
  61. package/dist/clients/attach/clipboard-text.d.ts +3 -0
  62. package/dist/clients/attach/clipboard-text.js +39 -0
  63. package/dist/clients/attach/config-load.d.ts +54 -7
  64. package/dist/clients/attach/config-load.js +99 -3
  65. package/dist/clients/attach/context-message.d.ts +12 -0
  66. package/dist/clients/attach/context-message.js +72 -0
  67. package/dist/clients/attach/git-info.d.ts +20 -0
  68. package/dist/clients/attach/git-info.js +44 -0
  69. package/dist/clients/attach/graph-overlay.d.ts +43 -0
  70. package/dist/clients/attach/graph-overlay.js +306 -0
  71. package/dist/clients/attach/input-controller.d.ts +81 -13
  72. package/dist/clients/attach/input-controller.js +307 -67
  73. package/dist/clients/attach/pickers.d.ts +58 -0
  74. package/dist/clients/attach/pickers.js +171 -0
  75. package/dist/clients/attach/slash-commands.d.ts +44 -1
  76. package/dist/clients/attach/slash-commands.js +250 -27
  77. package/dist/clients/attach/titled-editor.d.ts +33 -0
  78. package/dist/clients/attach/titled-editor.js +84 -0
  79. package/dist/clients/attach/view-socket.d.ts +37 -2
  80. package/dist/clients/attach/view-socket.js +131 -7
  81. package/dist/clients/web/dev-server.d.ts +7 -0
  82. package/dist/clients/web/dev-server.js +59 -0
  83. package/dist/clients/web/events.d.ts +14 -0
  84. package/dist/clients/web/events.js +151 -0
  85. package/dist/clients/web/server.d.ts +18 -0
  86. package/dist/clients/web/server.js +450 -0
  87. package/dist/clients/web/web-cmd.d.ts +2 -0
  88. package/dist/clients/web/web-cmd.js +120 -0
  89. package/dist/commands/canvas.js +1 -2
  90. package/dist/commands/chord.js +1 -1
  91. package/dist/commands/dashboard.js +5 -1
  92. package/dist/commands/human/prompts.js +3 -3
  93. package/dist/commands/human/queue.d.ts +17 -0
  94. package/dist/commands/human/queue.js +111 -4
  95. package/dist/commands/memory/__tests__/lint-schema.test.js +27 -4
  96. package/dist/commands/memory/lint.d.ts +5 -4
  97. package/dist/commands/memory/lint.js +23 -7
  98. package/dist/commands/memory/read.js +2 -2
  99. package/dist/commands/memory/shared.d.ts +7 -1
  100. package/dist/commands/memory/shared.js +18 -3
  101. package/dist/commands/memory/write.js +20 -7
  102. package/dist/commands/memory.js +4 -4
  103. package/dist/commands/node.d.ts +1 -1
  104. package/dist/commands/node.js +64 -65
  105. package/dist/commands/pkg.js +1 -2
  106. package/dist/commands/revive.js +11 -3
  107. package/dist/commands/sys/feedback.d.ts +1 -0
  108. package/dist/commands/sys/feedback.js +163 -0
  109. package/dist/commands/sys/sync.d.ts +1 -0
  110. package/dist/commands/sys/sync.js +187 -0
  111. package/dist/commands/sys.js +4 -2
  112. package/dist/commands/view-cycle.js +2 -2
  113. package/dist/commands/view-list.js +8 -8
  114. package/dist/commands/view-new.js +21 -17
  115. package/dist/commands/view-pick.js +1 -1
  116. package/dist/commands/view-run.js +35 -14
  117. package/dist/commands/view.js +8 -6
  118. package/dist/commands/workspace.d.ts +2 -0
  119. package/dist/commands/workspace.js +161 -0
  120. package/dist/core/__tests__/broker-double-spawn.test.d.ts +1 -0
  121. package/dist/core/__tests__/broker-double-spawn.test.js +143 -0
  122. package/dist/core/__tests__/broker-fork-seam.test.d.ts +1 -0
  123. package/dist/core/__tests__/broker-fork-seam.test.js +102 -0
  124. package/dist/core/__tests__/broker-sdk-wiring.test.js +2 -0
  125. package/dist/core/__tests__/broker-snapshot-history.test.d.ts +1 -0
  126. package/dist/core/__tests__/broker-snapshot-history.test.js +105 -0
  127. package/dist/core/__tests__/child-death-wake.test.js +56 -111
  128. package/dist/core/__tests__/close.test.js +14 -20
  129. package/dist/core/__tests__/context-intro.test.js +19 -19
  130. package/dist/core/__tests__/daemon-boot.test.js +30 -23
  131. package/dist/core/__tests__/draw-style.test.js +1 -1
  132. package/dist/core/__tests__/error-stall-recycle.test.d.ts +1 -0
  133. package/dist/core/__tests__/error-stall-recycle.test.js +141 -0
  134. package/dist/core/__tests__/fixtures/fake-engine.d.ts +24 -1
  135. package/dist/core/__tests__/fixtures/fake-engine.js +32 -2
  136. package/dist/core/__tests__/fixtures/fake-pi-host.js +2 -2
  137. package/dist/core/__tests__/focuses.test.js +25 -13
  138. package/dist/core/__tests__/fork.test.js +22 -5
  139. package/dist/core/__tests__/full/broker-control-preempt.test.d.ts +1 -0
  140. package/dist/core/__tests__/full/broker-control-preempt.test.js +61 -0
  141. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.d.ts +1 -0
  142. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.js +57 -0
  143. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.d.ts +1 -0
  144. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.js +80 -0
  145. package/dist/core/__tests__/full/broker-pane-resolution.test.js +90 -0
  146. package/dist/core/__tests__/full/cascade-close.test.js +22 -8
  147. package/dist/core/__tests__/full/detach-focus.test.js +66 -169
  148. package/dist/core/__tests__/helpers/harness.d.ts +6 -9
  149. package/dist/core/__tests__/helpers/harness.js +19 -36
  150. package/dist/core/__tests__/human-node-not-supervised.test.d.ts +1 -0
  151. package/dist/core/__tests__/human-node-not-supervised.test.js +83 -0
  152. package/dist/core/__tests__/human-stranded-deliver.test.d.ts +1 -0
  153. package/dist/core/__tests__/human-stranded-deliver.test.js +108 -0
  154. package/dist/core/__tests__/lifecycle.test.js +1 -19
  155. package/dist/core/__tests__/live-mutation-verbs.test.js +5 -5
  156. package/dist/core/__tests__/memory-resolver.test.js +1 -1
  157. package/dist/core/__tests__/on-read-dedup-resume.test.d.ts +1 -0
  158. package/dist/core/__tests__/on-read-dedup-resume.test.js +81 -0
  159. package/dist/core/__tests__/refresh-stall-recycle.test.d.ts +1 -0
  160. package/dist/core/__tests__/refresh-stall-recycle.test.js +88 -0
  161. package/dist/core/__tests__/relaunch-root.test.d.ts +1 -0
  162. package/dist/core/__tests__/relaunch-root.test.js +114 -0
  163. package/dist/core/__tests__/reset.test.js +19 -79
  164. package/dist/core/__tests__/steer-note.test.js +9 -7
  165. package/dist/core/__tests__/tmux-surface.test.js +13 -7
  166. package/dist/core/bootstrap.d.ts +0 -3
  167. package/dist/core/bootstrap.js +1 -143
  168. package/dist/core/canvas/browse/__tests__/model.test.js +86 -1
  169. package/dist/core/canvas/browse/__tests__/render.test.js +2 -1
  170. package/dist/core/canvas/browse/app.js +131 -8
  171. package/dist/core/canvas/browse/model.d.ts +23 -5
  172. package/dist/core/canvas/browse/model.js +68 -13
  173. package/dist/core/canvas/browse/render.d.ts +7 -1
  174. package/dist/core/canvas/browse/render.js +178 -47
  175. package/dist/core/canvas/canvas.js +1 -1
  176. package/dist/core/canvas/focuses.d.ts +4 -4
  177. package/dist/core/canvas/focuses.js +7 -7
  178. package/dist/core/canvas/nav-model.d.ts +162 -0
  179. package/dist/core/canvas/nav-model.js +487 -0
  180. package/dist/core/canvas/paths.d.ts +7 -0
  181. package/dist/core/canvas/paths.js +9 -0
  182. package/dist/core/canvas/render.d.ts +68 -9
  183. package/dist/core/canvas/render.js +195 -61
  184. package/dist/core/canvas/types.d.ts +16 -23
  185. package/dist/core/config.js +1 -4
  186. package/dist/core/memory-resolver.js +43 -5
  187. package/dist/core/runtime/bearings.d.ts +1 -1
  188. package/dist/core/runtime/bearings.js +8 -8
  189. package/dist/core/runtime/branded-host.d.ts +16 -0
  190. package/dist/core/runtime/branded-host.js +127 -0
  191. package/dist/core/runtime/broker-protocol.d.ts +212 -6
  192. package/dist/core/runtime/broker-sdk.d.ts +0 -12
  193. package/dist/core/runtime/broker-sdk.js +77 -6
  194. package/dist/core/runtime/broker.d.ts +5 -1
  195. package/dist/core/runtime/broker.js +520 -59
  196. package/dist/core/runtime/close.d.ts +12 -2
  197. package/dist/core/runtime/close.js +37 -16
  198. package/dist/core/runtime/front-door.js +14 -7
  199. package/dist/core/runtime/host.d.ts +14 -34
  200. package/dist/core/runtime/host.js +14 -50
  201. package/dist/core/runtime/launch.d.ts +12 -9
  202. package/dist/core/runtime/launch.js +27 -15
  203. package/dist/core/runtime/lifecycle.d.ts +1 -1
  204. package/dist/core/runtime/lifecycle.js +15 -19
  205. package/dist/core/runtime/nodes.d.ts +0 -55
  206. package/dist/core/runtime/nodes.js +6 -74
  207. package/dist/core/runtime/placement.d.ts +91 -334
  208. package/dist/core/runtime/placement.js +262 -828
  209. package/dist/core/runtime/promote.d.ts +2 -0
  210. package/dist/core/runtime/promote.js +24 -6
  211. package/dist/core/runtime/recap.d.ts +8 -0
  212. package/dist/core/runtime/recap.js +107 -0
  213. package/dist/core/runtime/recycle.js +25 -61
  214. package/dist/core/runtime/reset.d.ts +43 -37
  215. package/dist/core/runtime/reset.js +131 -218
  216. package/dist/core/runtime/revive.d.ts +9 -29
  217. package/dist/core/runtime/revive.js +45 -135
  218. package/dist/core/runtime/spawn.d.ts +15 -11
  219. package/dist/core/runtime/spawn.js +168 -136
  220. package/dist/core/runtime/surface-bg.d.ts +11 -0
  221. package/dist/core/runtime/surface-bg.js +68 -0
  222. package/dist/core/runtime/tmux.d.ts +15 -43
  223. package/dist/core/runtime/tmux.js +38 -80
  224. package/dist/core/scope.d.ts +1 -2
  225. package/dist/core/scope.js +1 -5
  226. package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.d.ts +1 -0
  227. package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.js +57 -0
  228. package/dist/core/skill-sync/builtins.d.ts +42 -0
  229. package/dist/core/skill-sync/builtins.js +112 -0
  230. package/dist/core/skill-sync/claude-plugins.d.ts +23 -0
  231. package/dist/core/skill-sync/claude-plugins.js +71 -0
  232. package/dist/core/skill-sync/engine.d.ts +42 -0
  233. package/dist/core/skill-sync/engine.js +633 -0
  234. package/dist/core/skill-sync/export.d.ts +23 -0
  235. package/dist/core/skill-sync/export.js +86 -0
  236. package/dist/core/skill-sync/manifest.d.ts +64 -0
  237. package/dist/core/skill-sync/manifest.js +181 -0
  238. package/dist/core/skill-sync/profile.d.ts +76 -0
  239. package/dist/core/skill-sync/profile.js +173 -0
  240. package/dist/core/skill-sync/snapshot.d.ts +57 -0
  241. package/dist/core/skill-sync/snapshot.js +120 -0
  242. package/dist/core/spawn.d.ts +14 -0
  243. package/dist/core/spawn.js +29 -9
  244. package/dist/core/substrate/index.d.ts +2 -2
  245. package/dist/core/substrate/index.js +7 -7
  246. package/dist/core/substrate/injected-store.d.ts +10 -0
  247. package/dist/core/substrate/injected-store.js +55 -0
  248. package/dist/core/substrate/on-read.js +68 -6
  249. package/dist/core/substrate/render.d.ts +8 -11
  250. package/dist/core/substrate/render.js +29 -43
  251. package/dist/core/substrate/schema.d.ts +16 -11
  252. package/dist/core/substrate/schema.js +32 -31
  253. package/dist/core/tui/host.d.ts +12 -4
  254. package/dist/core/tui/host.js +280 -149
  255. package/dist/core/view/bridge.d.ts +10 -0
  256. package/dist/core/view/bridge.js +31 -0
  257. package/dist/core/view/chrome.d.ts +9 -0
  258. package/dist/core/view/chrome.js +22 -0
  259. package/dist/core/view/contract.d.ts +171 -0
  260. package/dist/core/view/contract.js +23 -0
  261. package/dist/core/view/loader.d.ts +31 -0
  262. package/dist/core/view/loader.js +188 -0
  263. package/dist/core/view/transport-local.d.ts +7 -0
  264. package/dist/core/view/transport-local.js +70 -0
  265. package/dist/core/view/transport.d.ts +4 -0
  266. package/dist/core/view/transport.js +15 -0
  267. package/dist/daemon/crtrd.d.ts +38 -27
  268. package/dist/daemon/crtrd.js +303 -376
  269. package/dist/daemon/manage.js +6 -1
  270. package/dist/index.js +1 -1
  271. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +28 -20
  272. package/dist/pi-extensions/canvas-context-intro.js +4 -4
  273. package/dist/pi-extensions/canvas-doc-substrate.js +29 -20
  274. package/dist/pi-extensions/canvas-nav.js +35 -385
  275. package/dist/pi-extensions/canvas-recap.d.ts +37 -0
  276. package/dist/pi-extensions/canvas-recap.js +236 -0
  277. package/dist/pi-extensions/canvas-resume.js +2 -1
  278. package/dist/pi-extensions/canvas-stophook.d.ts +15 -13
  279. package/dist/pi-extensions/canvas-stophook.js +119 -103
  280. package/dist/pi-extensions/canvas-view.js +2 -1
  281. package/dist/pi-extensions/widget-order-bus.d.ts +6 -0
  282. package/dist/pi-extensions/widget-order-bus.js +34 -0
  283. package/dist/prompts/view.d.ts +2 -2
  284. package/dist/prompts/view.js +145 -73
  285. package/dist/types.d.ts +1 -6
  286. package/dist/types.js +1 -3
  287. package/dist/web/ViewChrome.d.ts +7 -0
  288. package/dist/web/ViewChrome.js +28 -0
  289. package/dist/web/ViewPane.d.ts +39 -0
  290. package/dist/web/ViewPane.js +48 -0
  291. package/dist/web/index.d.ts +6 -0
  292. package/dist/web/index.js +16 -0
  293. package/dist/web/runtime.d.ts +39 -0
  294. package/dist/web/runtime.js +133 -0
  295. package/dist/web/states.d.ts +24 -0
  296. package/dist/web/states.js +24 -0
  297. package/dist/web/transport-http.d.ts +5 -0
  298. package/dist/web/transport-http.js +28 -0
  299. package/dist/web-client/assets/index-BUvQb4hR.css +2 -0
  300. package/dist/web-client/assets/index-ClLQXYAE.js +10 -0
  301. package/dist/web-client/index.html +13 -0
  302. package/package.json +20 -6
  303. package/dist/builtin-views/canvas/client.mjs +0 -303
  304. package/dist/builtin-views/canvas/view.mjs +0 -576
  305. package/dist/builtin-views/git-pr/client.mjs +0 -440
  306. package/dist/builtin-views/git-pr/view.mjs +0 -675
  307. package/dist/builtin-views/inbox/sources/gmail.mjs +0 -965
  308. package/dist/builtin-views/inbox/sources/linkedin.mjs +0 -427
  309. package/dist/builtin-views/inbox/view.mjs +0 -889
  310. package/dist/builtin-views/linkedin/client.mjs +0 -610
  311. package/dist/builtin-views/linkedin/view.mjs +0 -1171
  312. package/dist/commands/pkg/bridge.d.ts +0 -1
  313. package/dist/commands/pkg/bridge.js +0 -137
  314. package/dist/commands/skill/author.d.ts +0 -3
  315. package/dist/commands/skill/author.js +0 -140
  316. package/dist/commands/skill/shared.d.ts +0 -3
  317. package/dist/commands/skill/shared.js +0 -19
  318. package/dist/commands/skill.d.ts +0 -2
  319. package/dist/commands/skill.js +0 -21
  320. package/dist/commands/tmux-spread.d.ts +0 -2
  321. package/dist/commands/tmux-spread.js +0 -144
  322. package/dist/core/__tests__/full/daemon-liveness-pane.full.test.js +0 -292
  323. package/dist/core/__tests__/full/placement-focus.test.js +0 -309
  324. package/dist/core/__tests__/full/placement-reconcile.test.js +0 -212
  325. package/dist/core/__tests__/full/placement-revive.test.js +0 -238
  326. package/dist/core/__tests__/full/placement-teardown.test.js +0 -221
  327. package/dist/core/__tests__/home-session.test.js +0 -186
  328. package/dist/core/__tests__/relaunch.test.js +0 -335
  329. package/dist/core/bridge-map.d.ts +0 -19
  330. package/dist/core/bridge-map.js +0 -73
  331. package/dist/core/tui/contract.d.ts +0 -83
  332. package/dist/core/tui/contract.js +0 -8
  333. package/dist/core/tui/loader.d.ts +0 -16
  334. package/dist/core/tui/loader.js +0 -94
  335. package/dist/prompts/skill.d.ts +0 -2
  336. package/dist/prompts/skill.js +0 -650
  337. /package/dist/{core/__tests__/full/daemon-liveness-pane.full.test.d.ts → clients/attach/__tests__/action-parity.test.d.ts} +0 -0
  338. /package/dist/{core/__tests__/full/placement-focus.test.d.ts → clients/attach/__tests__/clipboard-image.test.d.ts} +0 -0
  339. /package/dist/{core/__tests__/full/placement-reconcile.test.d.ts → clients/attach/__tests__/editor-newline.test.d.ts} +0 -0
  340. /package/dist/{core/__tests__/full/placement-revive.test.d.ts → clients/attach/__tests__/git-info.test.d.ts} +0 -0
  341. /package/dist/{core/__tests__/full/placement-teardown.test.d.ts → clients/attach/__tests__/onrequest-wired.test.d.ts} +0 -0
  342. /package/dist/{core/__tests__/home-session.test.d.ts → clients/attach/__tests__/reconnect-giveup.test.d.ts} +0 -0
  343. /package/dist/{core/__tests__/relaunch.test.d.ts → clients/attach/__tests__/slash-quit-copy.test.d.ts} +0 -0
@@ -0,0 +1,142 @@
1
+ // @ts-check
2
+ /**
3
+ * Workspace Sidebar — the TUI presenter (`render` + `keymap`) for the
4
+ * `workspace-sidebar` view. Node-only (it uses the host's `Draw` API + the
5
+ * `_lib/states.mjs` draw helpers).
6
+ *
7
+ * `render` paints a two-section rail; keystrokes map to named intents through
8
+ * `keymap`. All state + data logic lives in `core.mjs`; this file is a pure
9
+ * read of the logical `RailRow[]`.
10
+ *
11
+ * VISUAL LANGUAGE (mirrors the `canvas` view): hierarchy via weight + hue +
12
+ * position, never boxes. Status glyph hues match `canvas browse` (active=green,
13
+ * idle=yellow, done=cyan, dead=red, canceled=grey; asks=bright-yellow). Color
14
+ * never carries meaning alone — every hue pairs with a glyph/weight (NO_COLOR-
15
+ * safe). The attached node carries a `▸` accent + bold name. ⚑N attention flags
16
+ * right-flush into a clean column.
17
+ *
18
+ * @module workspace-sidebar/tui
19
+ */
20
+
21
+ import { loadingState, emptyState, notReadyState } from '../_lib/states.mjs';
22
+
23
+ /** @typedef {import('./core.mjs').SidebarState} SidebarState */
24
+ /** @typedef {import('./core.mjs').RailRow} RailRow */
25
+
26
+ // ── Status vocabulary (single source: core/canvas/browse/render.ts) ───────────
27
+
28
+ /** @type {Record<string,string>} */
29
+ const STATUS_GLYPH = { active: '●', idle: '○', done: '✓', dead: '✗', canceled: '⊘' };
30
+
31
+ /** Load-bearing status hue — NUMERIC SGR, matching `canvas browse` exactly. */
32
+ /** @type {Record<string,string>} */
33
+ const STATUS_FG = { active: '32', idle: '33', done: '36', dead: '31', canceled: '90' };
34
+
35
+ /** @param {string} status @returns {import('../../core/tui/draw.js').Style|undefined} */
36
+ function glyphStyle(status) {
37
+ const fg = STATUS_FG[status];
38
+ return fg ? { fg } : undefined; // hue only — the glyph SHAPE is the mono carrier
39
+ }
40
+
41
+ /** Name weight = hierarchy: live work LEADS in bold; terminal nodes recede dim.
42
+ * @param {string} status @param {boolean} attached
43
+ * @returns {import('../../core/tui/draw.js').Style|undefined} */
44
+ function nameStyle(status, attached) {
45
+ if (attached) return { fg: '36', bold: true }; // the attached node — cyan accent
46
+ if (status === 'active') return { bold: true };
47
+ if (status === 'done' || status === 'dead' || status === 'canceled') return { dim: true };
48
+ return undefined;
49
+ }
50
+
51
+ // ── RailRow → ListItemRow (left spans + right-flushed ⚑N) ──────────────────────
52
+
53
+ /**
54
+ * Build one rail list row. A node row: 1-cell gutter (`▸` accent on the attached
55
+ * node, else blank), optional dim tree prefix, the status glyph (hue), the name
56
+ * (weight = status / accent when attached), and a right-flushed bright-yellow ⚑N
57
+ * when the node has pending asks.
58
+ * @param {RailRow} row
59
+ * @returns {import('../../core/tui/draw.js').ListItemRow}
60
+ */
61
+ function rowToItem(row) {
62
+ if (row.kind === 'header') {
63
+ return { spans: [{ text: row.text, style: { fg: '36', bold: true } }] };
64
+ }
65
+ if (row.kind === 'chrome') {
66
+ return { spans: [{ text: row.text, style: { dim: true } }] };
67
+ }
68
+ // row.kind === 'node'
69
+ /** @type {import('../../core/tui/draw.js').Span[]} */
70
+ const spans = [
71
+ row.attached ? { text: '▸', style: { fg: '36', bold: true } } : { text: ' ' },
72
+ { text: ' ' },
73
+ ];
74
+ if (row.prefix) spans.push({ text: row.prefix, style: { dim: true } });
75
+ spans.push({ text: STATUS_GLYPH[row.status] || '?', style: glyphStyle(row.status) });
76
+ spans.push({ text: ' ' });
77
+ spans.push({ text: row.name, style: nameStyle(row.status, row.attached) });
78
+ if (row.asks > 0) {
79
+ return { spans, right: [{ text: `⚑${row.asks}`, style: { fg: '93', bold: true } }] };
80
+ }
81
+ return { spans };
82
+ }
83
+
84
+ // ── render ─────────────────────────────────────────────────────────────────────
85
+
86
+ /**
87
+ * Paint the rail, or one of the four standard states. Pure (reads state, calls
88
+ * draw.*); the only write is storing draw.list's adjusted scroll.
89
+ * @param {SidebarState} state
90
+ * @param {import('../../core/tui/draw.js').Draw} draw
91
+ * @param {import('../../core/tui/draw.js').Rect} content
92
+ */
93
+ export function render(state, draw, content) {
94
+ if (content.width <= 0 || content.height <= 0) return;
95
+
96
+ if (state.rows.length === 0) {
97
+ // The copy comes from the typed SourceError's `display` VERBATIM (the
98
+ // contract display/kind split — we never branch on `kind`); only the glyph +
99
+ // hue are a presentation map off `display.level`.
100
+ if (state.srcError) {
101
+ const d = state.srcError.display;
102
+ const g = d.level === 'action' ? { glyph: '⊙', fg: '33' } : { glyph: '⚠', fg: '31' };
103
+ notReadyState(draw, content, {
104
+ glyph: g.glyph,
105
+ glyphFg: g.fg,
106
+ headline: d.headline,
107
+ explanation: d.explanation,
108
+ nextStep: d.nextStep,
109
+ });
110
+ return;
111
+ }
112
+ if (state.lastFetch === 0) {
113
+ loadingState(draw, content, { rows: Math.min(5, content.height), label: 'Loading…' });
114
+ return;
115
+ }
116
+ emptyState(draw, content, {
117
+ headline: 'No agents here',
118
+ secondary: ['Nothing started in this cwd.', 'Press g to refresh.'],
119
+ });
120
+ return;
121
+ }
122
+
123
+ const items = state.rows.map((r) => rowToItem(r));
124
+ const res = draw.list(content, items, state.cursor, state.scroll);
125
+ state.scroll = res.scroll; // store adjusted scroll back (Draw.list contract)
126
+ }
127
+
128
+ // ── keymap ───────────────────────────────────────────────────────────────
129
+
130
+ /**
131
+ * j/k move the cursor over node rows, ↵ swaps the selected node into the chat
132
+ * pane (the controller action), g refreshes, q quits. Footer hints come from
133
+ * these bindings' `hint` fields (the single source of truth).
134
+ * @type {import('../../core/view/contract.js').KeyBinding<SidebarState>[]}
135
+ */
136
+ export const keymap = [
137
+ { keys: ['j', 'down'], intent: 'cursorDown', hint: { keys: 'j/k', label: 'move' } },
138
+ { keys: ['k', 'up'], intent: 'cursorUp' },
139
+ { keys: ['return', 'enter'], intent: 'open', hint: { keys: '↵', label: 'open' } },
140
+ { keys: ['g', 'r'], intent: 'refresh', hint: { keys: 'g', label: 'refresh' } },
141
+ { keys: ['q'], intent: 'quit', hint: { keys: 'q', label: 'quit' } },
142
+ ];
@@ -0,0 +1,109 @@
1
+ // @ts-check
2
+ /**
3
+ * Workspace Sidebar — the WEB presenter for the `workspace-sidebar` view (React
4
+ * + Tailwind). Browser-only: consumed solely by the web serve path (Vite owns
5
+ * JSX + Tailwind); NEVER Node-imported. The default export is a pure function of
6
+ * `state`; DOM events call `dispatch(intentName, payload?)`.
7
+ *
8
+ * Same logical model as the TUI presenter (`state.rows` + `state.cursor`) read
9
+ * from the SAME portable `core.mjs` — zero shared rendering code with `tui.mjs`.
10
+ * On the web there is no chat pane to drive, so the rail renders as a plain
11
+ * styled node list (this becomes the sidebar of the future crouter web UI): a
12
+ * click selects a row, ↵ no-ops via the core's `open` banner. The outer chrome
13
+ * (title / status / banner / chip) is rendered by `<ViewChrome>`, which wraps
14
+ * this component — do NOT render it here.
15
+ *
16
+ * @module workspace-sidebar/web
17
+ */
18
+
19
+ import { Loading, Empty, ErrorState, NotReady } from '@crouton-kit/crouter/web';
20
+
21
+ /** @typedef {import('./core.mjs').SidebarState} SidebarState */
22
+ /** @typedef {import('./core.mjs').RailRow} RailRow */
23
+
24
+ // ── Status vocabulary (the web analog of tui.mjs's NUMERIC-SGR maps) ──────────
25
+
26
+ const STATUS = {
27
+ active: { glyph: '●', cls: 'text-green-600' },
28
+ idle: { glyph: '○', cls: 'text-amber-600' },
29
+ done: { glyph: '✓', cls: 'text-cyan-600' },
30
+ dead: { glyph: '✗', cls: 'text-red-600' },
31
+ canceled: { glyph: '⊘', cls: 'text-slate-400' },
32
+ };
33
+
34
+ /** @param {string} status @param {boolean} attached @returns {string} */
35
+ function nameCls(status, attached) {
36
+ if (attached) return 'font-bold text-cyan-700';
37
+ if (status === 'active') return 'font-semibold';
38
+ if (status === 'done' || status === 'dead' || status === 'canceled') return 'text-slate-400';
39
+ return '';
40
+ }
41
+
42
+ // ── Rows (the web analog of tui.mjs's rowToItem) ───────────────────────────────
43
+
44
+ /** @param {{ row: RailRow, index: number, selected: boolean, onClick: () => void }} props */
45
+ function Row({ row, selected, onClick }) {
46
+ if (row.kind === 'header') {
47
+ return <li className="px-2 pt-3 pb-0.5 font-semibold text-cyan-700">{row.text}</li>;
48
+ }
49
+ if (row.kind === 'chrome') {
50
+ return <li className="px-2 text-slate-400">{row.text || '\u00a0'}</li>;
51
+ }
52
+ // row.kind === 'node'
53
+ const st = STATUS[row.status] || { glyph: '?', cls: 'text-slate-400' };
54
+ const base = `flex items-baseline gap-1 rounded px-2 py-0.5 cursor-pointer ${selected ? 'bg-slate-200' : 'hover:bg-slate-100'}`;
55
+ return (
56
+ <li className={base} onClick={onClick}>
57
+ <span className={`w-3 shrink-0 ${row.attached ? 'font-bold text-cyan-700' : ''}`}>{row.attached ? '▸' : '\u00a0'}</span>
58
+ {row.prefix ? <span className="whitespace-pre text-slate-400">{row.prefix}</span> : null}
59
+ <span className={st.cls}>{st.glyph}</span>
60
+ <span className={nameCls(row.status, row.attached)}>{row.name}</span>
61
+ {row.asks > 0 ? <span className="ml-auto shrink-0 font-bold text-amber-500">⚑{row.asks}</span> : null}
62
+ </li>
63
+ );
64
+ }
65
+
66
+ // ── The view ───────────────────────────────────────────────────────────────────
67
+
68
+ /**
69
+ * @param {import('../../core/view/contract.js').ViewProps<SidebarState>} props
70
+ */
71
+ export default function WorkspaceSidebar({ state, dispatch }) {
72
+ if (state.rows.length === 0) {
73
+ // The copy comes from the typed SourceError's `display` VERBATIM (the
74
+ // contract display/kind split — never branch on `kind`); only the four-state
75
+ // component is a presentation map off `display.level` so the hue matches the
76
+ // TUI (error → red ErrorState, action → amber NotReady).
77
+ if (state.srcError) {
78
+ const d = state.srcError.display;
79
+ const Takeover = d.level === 'error' ? ErrorState : NotReady;
80
+ return (
81
+ <Takeover
82
+ headline={d.headline}
83
+ explanation={d.explanation}
84
+ nextStep={d.nextStep}
85
+ onRetry={() => dispatch('refresh')}
86
+ />
87
+ );
88
+ }
89
+ if (state.lastFetch === 0) return <Loading label="Loading…" />;
90
+ return <Empty label="No agents in this cwd." />;
91
+ }
92
+
93
+ return (
94
+ <ul
95
+ className="font-mono text-sm outline-none"
96
+ tabIndex={0}
97
+ onKeyDown={(e) => {
98
+ if (e.key === 'j' || e.key === 'ArrowDown') { dispatch('cursorDown'); e.preventDefault(); }
99
+ else if (e.key === 'k' || e.key === 'ArrowUp') { dispatch('cursorUp'); e.preventDefault(); }
100
+ else if (e.key === 'Enter') { dispatch('open'); e.preventDefault(); }
101
+ else if (e.key === 'g') { dispatch('refresh'); }
102
+ }}
103
+ >
104
+ {state.rows.map((row, i) => (
105
+ <Row key={i} row={row} index={i} selected={i === state.cursor} onClick={() => dispatch('select', i)} />
106
+ ))}
107
+ </ul>
108
+ );
109
+ }
package/dist/cli.js CHANGED
@@ -3,7 +3,8 @@ import { runCli } from './core/command.js';
3
3
  import { buildRoot } from './build-root.js';
4
4
  import { maybeBootRoot } from './core/runtime/front-door.js';
5
5
  import { maybeAutoUpdate } from './core/auto-update.js';
6
- import { ensureBootSkill, ensureOfficialMarketplace, ensureProjectScope, ensureSlashCommands } from './core/bootstrap.js';
6
+ import { ensureOfficialMarketplace, ensureProjectScope } from './core/bootstrap.js';
7
+ import { provisionExports } from './core/skill-sync/export.js';
7
8
  // The full command tree is assembled in build-root.ts (shared with the
8
9
  // listing-completeness test). Root owns only the tagline; every subtree
9
10
  // declares its own representation.
@@ -15,8 +16,7 @@ if (maybeBootRoot(root, process.argv)) {
15
16
  // bootRoot exec'd pi inline and exited; unreachable.
16
17
  }
17
18
  ensureOfficialMarketplace(process.argv);
18
- ensureBootSkill(process.argv);
19
- ensureSlashCommands(root, process.argv);
19
+ provisionExports(root);
20
20
  ensureProjectScope(process.argv);
21
21
  maybeAutoUpdate(process.argv);
22
22
  runCli(root, process.argv).catch((err) => {
@@ -0,0 +1,48 @@
1
+ // Regression: the `crtr attach` viewer's InputController wired only a subset of
2
+ // pi's editor `app.*` actions, so several keys that work in pi were dead in the
3
+ // attach surface — most visibly Ctrl+O (toggle tool output) and Ctrl+T (toggle
4
+ // thinking blocks), but also Ctrl+G (external editor), Shift+Tab (cycle thinking
5
+ // level), Shift+Ctrl+P (cycle model backward), and Alt+Up (dequeue). The broker
6
+ // already supported every one of these frames; only the client-side onAction
7
+ // wiring was missing. The fix wires them in input-controller's wire() and adds
8
+ // the ChatView render toggles for the two display-only ones. This assembly lives
9
+ // inside TUI-constructing code with no unit seam, so we guard the wiring at the
10
+ // source level (mirrors onrequest-wired.test.ts).
11
+ // See src/clients/attach/{input-controller,chat-view,attach-cmd}.ts.
12
+ import { test } from 'node:test';
13
+ import assert from 'node:assert/strict';
14
+ import { readFileSync } from 'node:fs';
15
+ import { fileURLToPath } from 'node:url';
16
+ import { dirname, join } from 'node:path';
17
+ const here = dirname(fileURLToPath(import.meta.url));
18
+ const read = (file) => readFileSync(join(here, '..', file), 'utf8').replace(/\s+/g, ' ');
19
+ const inputController = read('input-controller.ts');
20
+ const chatView = read('chat-view.ts');
21
+ const attachCmd = read('attach-cmd.ts');
22
+ test('input-controller wires every previously-dead app.* editor action', () => {
23
+ for (const action of [
24
+ 'app.tools.expand', // Ctrl+O — toggle tool output
25
+ 'app.thinking.toggle', // Ctrl+T — toggle thinking blocks
26
+ 'app.thinking.cycle', // Shift+Tab — cycle thinking level
27
+ 'app.model.cycleBackward', // Shift+Ctrl+P — cycle model backward
28
+ 'app.message.dequeue', // Alt+Up — restore queued messages
29
+ 'app.editor.external', // Ctrl+G — external editor
30
+ ]) {
31
+ assert.match(inputController, new RegExp(`onAction\\(\\s*'${action.replace(/\./g, '\\.')}'`), `input-controller must register onAction('${action}')`);
32
+ }
33
+ });
34
+ test('cycle_model carries an explicit direction in both directions', () => {
35
+ assert.match(inputController, /type:\s*'cycle_model',\s*direction:\s*'forward'/);
36
+ assert.match(inputController, /type:\s*'cycle_model',\s*direction:\s*'backward'/);
37
+ });
38
+ test('ChatView exposes the Ctrl+O / Ctrl+T render toggles', () => {
39
+ assert.match(chatView, /toggleToolsExpanded\(\)/, 'ChatView needs toggleToolsExpanded()');
40
+ assert.match(chatView, /toggleThinking\(\)/, 'ChatView needs toggleThinking()');
41
+ // The toggles must actually fan out to the rendered children, not just flip a flag.
42
+ assert.match(chatView, /child\.setExpanded\(this\.toolOutputExpanded\)/);
43
+ assert.match(chatView, /child\.setHideThinkingBlock\(this\.hideThinking\)/);
44
+ });
45
+ test('attach-cmd connects the ChatView toggles to the input controller', () => {
46
+ assert.match(attachCmd, /onToggleToolsExpand:\s*\(\)\s*=>[\s\S]*chatView\.toggleToolsExpanded\(\)/);
47
+ assert.match(attachCmd, /onToggleThinking:\s*\(\)\s*=>[\s\S]*chatView\.toggleThinking\(\)/);
48
+ });
@@ -0,0 +1,76 @@
1
+ // Bug-regression: `crtr attach` Ctrl+V reported "no image in the clipboard" even
2
+ // when the clipboard genuinely held an image. Root cause — the macOS reader only
3
+ // tried `pngpaste -`, which is NOT a built-in (`brew install pngpaste`); on a
4
+ // stock Mac its absence returned null, indistinguishable from an empty clipboard.
5
+ //
6
+ // These lock in the platform dispatch (clipboard-image.ts): a MISSING pngpaste
7
+ // must FALL THROUGH to the native osascript read (not collapse to "no image"),
8
+ // and the reader must DISTINGUISH "no clipboard tool" from "clipboard has no
9
+ // image" so the user-facing notice is accurate. The decision logic is pure +
10
+ // injectable precisely so it can be unit-tested without a real clipboard.
11
+ import { test } from 'node:test';
12
+ import assert from 'node:assert/strict';
13
+ import { selectMacOutcome, selectLinuxOutcome, } from '../clipboard-image.js';
14
+ const bytes = (s) => Buffer.from(s);
15
+ test('macOS: missing pngpaste falls through to the native osascript read (the bug)', () => {
16
+ let nativeCalled = false;
17
+ const runTool = (cmd) => {
18
+ assert.equal(cmd, 'pngpaste');
19
+ return { missing: true }; // pngpaste not installed (stock Mac)
20
+ };
21
+ const readNative = () => {
22
+ nativeCalled = true;
23
+ return { image: { bytes: bytes('PNGDATA'), mimeType: 'image/png' } };
24
+ };
25
+ const out = selectMacOutcome(runTool, readNative);
26
+ assert.equal(nativeCalled, true, 'native osascript path must run when pngpaste is absent');
27
+ assert.equal(out.image?.mimeType, 'image/png');
28
+ assert.equal(out.image?.bytes.toString(), 'PNGDATA');
29
+ });
30
+ test('macOS: pngpaste present takes the fast path and skips osascript', () => {
31
+ let nativeCalled = false;
32
+ const runTool = () => ({ bytes: bytes('FASTPNG') });
33
+ const readNative = () => {
34
+ nativeCalled = true;
35
+ return {};
36
+ };
37
+ const out = selectMacOutcome(runTool, readNative);
38
+ assert.equal(nativeCalled, false, 'fast path must not invoke osascript');
39
+ assert.equal(out.image?.bytes.toString(), 'FASTPNG');
40
+ });
41
+ test('macOS: pngpaste present but clipboard empty still defers to osascript', () => {
42
+ // pngpaste exits non-zero on an empty clipboard → {} (ran, no image). The
43
+ // native path is authoritative for the empty-vs-failure distinction.
44
+ let nativeCalled = false;
45
+ const runTool = () => ({}); // ran, no bytes
46
+ const readNative = () => {
47
+ nativeCalled = true;
48
+ return {}; // osascript: NO_IMAGE
49
+ };
50
+ const out = selectMacOutcome(runTool, readNative);
51
+ assert.equal(nativeCalled, true);
52
+ assert.equal(out.image, undefined);
53
+ assert.equal(out.note, undefined, 'empty clipboard → no note (caller shows the generic notice)');
54
+ });
55
+ test('Linux: no clipboard tool installed surfaces a precise note, not "no image"', () => {
56
+ const runTool = () => ({ missing: true }); // neither xclip nor wl-paste
57
+ const out = selectLinuxOutcome(runTool, /* wayland */ false);
58
+ assert.equal(out.image, undefined);
59
+ assert.match(out.note ?? '', /install xclip/);
60
+ });
61
+ test('Linux/Wayland: no tool installed names wl-clipboard', () => {
62
+ const out = selectLinuxOutcome(() => ({ missing: true }), /* wayland */ true);
63
+ assert.match(out.note ?? '', /wl-clipboard/);
64
+ });
65
+ test('Linux: tool present but clipboard has no image → empty (no note)', () => {
66
+ const runTool = () => ({}); // xclip ran, no image of that type
67
+ const out = selectLinuxOutcome(runTool, /* wayland */ false);
68
+ assert.equal(out.image, undefined);
69
+ assert.equal(out.note, undefined, 'a tool that ran but found no image is genuinely empty');
70
+ });
71
+ test('Linux: xclip returns PNG bytes', () => {
72
+ const runTool = (_cmd, args) => args.includes('image/png') ? { bytes: bytes('XPNG') } : {};
73
+ const out = selectLinuxOutcome(runTool, /* wayland */ false);
74
+ assert.equal(out.image?.mimeType, 'image/png');
75
+ assert.equal(out.image?.bytes.toString(), 'XPNG');
76
+ });
@@ -0,0 +1,57 @@
1
+ // Regression: in the attach viewer, Alt+Enter did NOTHING — not a newline, not a
2
+ // submit. The editor was SWALLOWING it. Root cause: pi-coding-agent's
3
+ // CustomEditor resolves `@earendil-works/pi-tui` from its OWN node_modules, a
4
+ // SEPARATE module instance from the one `config-load` imports (a non-deduped
5
+ // install). config-load registered the KeybindingsManager only on its copy, but
6
+ // the editor's super reads `getKeybindings()` from ITS instance — stuck at the
7
+ // default `tui.input.newLine = shift+enter`. So the attach `alt+enter → newLine`
8
+ // binding was invisible to the editor: Alt+Enter (`\x1b[13;3u`) matched neither
9
+ // newLine nor submit and fell through. `mirrorKeybindingsToEditor` registers the
10
+ // SAME manager on the editor's pi-tui instance; this test feeds the exact
11
+ // Alt+Enter / Enter byte sequences a terminal sends and asserts the behavior.
12
+ // See mirrorKeybindingsToEditor in src/clients/attach/config-load.ts.
13
+ import { test } from 'node:test';
14
+ import assert from 'node:assert/strict';
15
+ import { mkdtempSync } from 'node:fs';
16
+ import { tmpdir } from 'node:os';
17
+ import { join } from 'node:path';
18
+ import { TUI, ProcessTerminal } from '@earendil-works/pi-tui';
19
+ import { TitledEditor } from '../titled-editor.js';
20
+ import { createKeybindingsManager, mirrorKeybindingsToEditor } from '../config-load.js';
21
+ // Kitty CSI-u: `\x1b[<codepoint>;<1+mods>u`. enter=13, alt=2 → mods byte 3.
22
+ const ALT_ENTER = '\x1b[13;3u';
23
+ const ENTER = '\r';
24
+ async function buildEditor() {
25
+ // Empty agent dir → no user keybindings.json, so the binding under test is the
26
+ // attach DEFAULT (`ATTACH_KEYBINDING_OVERRIDES` adds alt+enter to newLine).
27
+ const agentDir = mkdtempSync(join(tmpdir(), 'crtr-kb-'));
28
+ const km = createKeybindingsManager(agentDir);
29
+ await mirrorKeybindingsToEditor(km); // the fix
30
+ const tui = new TUI(new ProcessTerminal());
31
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
32
+ return new TitledEditor(tui, { borderColor: (s) => s, selectList: {} }, km, {
33
+ paddingX: 1,
34
+ });
35
+ }
36
+ test('attach editor inserts a newline on Alt+Enter (cross-instance binding reaches the editor)', async () => {
37
+ const editor = await buildEditor();
38
+ let submitted = null;
39
+ editor.onSubmit = (t) => {
40
+ submitted = t;
41
+ };
42
+ editor.setText('hello');
43
+ editor.handleInput(ALT_ENTER);
44
+ assert.equal(editor.getText(), 'hello\n', 'Alt+Enter must insert a newline, not be swallowed');
45
+ assert.equal(submitted, null, 'Alt+Enter must not submit');
46
+ });
47
+ test('attach editor still submits on plain Enter', async () => {
48
+ const editor = await buildEditor();
49
+ let submitted = null;
50
+ editor.onSubmit = (t) => {
51
+ submitted = t;
52
+ };
53
+ editor.setText('hello');
54
+ editor.handleInput(ENTER);
55
+ assert.equal(submitted, 'hello', 'plain Enter must submit');
56
+ assert.equal(editor.getText(), '', 'editor clears after submit');
57
+ });
@@ -0,0 +1,31 @@
1
+ // Unit coverage for the pure git-status parser that feeds the attach editor's
2
+ // border (cwd · branch · status symbols). The shell-out is non-blocking and
3
+ // untestable in a unit, but the porcelain-v1 --branch parse is pure and the
4
+ // part that can silently mis-read state, so it is locked in here.
5
+ // See src/clients/attach/git-info.ts.
6
+ import { test } from 'node:test';
7
+ import assert from 'node:assert/strict';
8
+ import { parseGitStatus } from '../git-info.js';
9
+ test('clean tree on a tracked branch', () => {
10
+ const info = parseGitStatus('repo', '## main...origin/main\n');
11
+ assert.deepEqual(info, { dir: 'repo', branch: 'main', dirty: false, ahead: 0, behind: 0 });
12
+ });
13
+ test('dirty tree with ahead/behind counts', () => {
14
+ const info = parseGitStatus('repo', '## feature...origin/feature [ahead 2, behind 3]\n M src/a.ts\n?? new.ts\n');
15
+ assert.equal(info.branch, 'feature');
16
+ assert.equal(info.dirty, true);
17
+ assert.equal(info.ahead, 2);
18
+ assert.equal(info.behind, 3);
19
+ });
20
+ test('untracked-only branch (no upstream) is still dirty, no counts', () => {
21
+ const info = parseGitStatus('repo', '## main\n?? scratch.md\n');
22
+ assert.equal(info.branch, 'main');
23
+ assert.equal(info.dirty, true);
24
+ assert.equal(info.ahead, 0);
25
+ assert.equal(info.behind, 0);
26
+ });
27
+ test('detached HEAD reports no branch', () => {
28
+ const info = parseGitStatus('repo', '## HEAD (no branch)\n');
29
+ assert.equal(info.branch, undefined);
30
+ assert.equal(info.dirty, false);
31
+ });
@@ -0,0 +1,20 @@
1
+ // Regression: the `crtr attach` viewer built its InputController WITHOUT the
2
+ // `onRequest` hook, so the correlated read-op channel was never wired even
3
+ // though ViewSocketClient.request() was fully implemented. Every native picker
4
+ // (/model, /resume, /fork, /tree, /settings, /scoped-models) therefore degraded
5
+ // to the "isn't available in this viewer" notice — /model couldn't open the
6
+ // model picker. The fix wires `onRequest: (frame) => socket.request(frame)` in
7
+ // attach-cmd. This assembly lives inside a big TUI-constructing function with no
8
+ // seam to unit-test, so this guards the exact wiring at the source level.
9
+ // See src/clients/attach/{attach-cmd,input-controller,slash-commands}.ts.
10
+ import { test } from 'node:test';
11
+ import assert from 'node:assert/strict';
12
+ import { readFileSync } from 'node:fs';
13
+ import { fileURLToPath } from 'node:url';
14
+ import { dirname, join } from 'node:path';
15
+ const src = readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'attach-cmd.ts'), 'utf8');
16
+ test('attach-cmd wires onRequest to socket.request so native pickers work', () => {
17
+ // The InputController must receive the read-op channel; without it
18
+ // openPicker() short-circuits to the "isn't available in this viewer" notice.
19
+ assert.match(src.replace(/\s+/g, ' '), /onRequest:\s*\(frame\)\s*=>\s*socket\.request\(frame\)/, 'attach-cmd must pass onRequest: (frame) => socket.request(frame) to InputController');
20
+ });
@@ -0,0 +1,30 @@
1
+ // Regression: the `crtr attach` viewer used to ALWAYS teardown ("broker gone")
2
+ // on a socket close, so a resident broker node's yield→revive cycle dropped the
3
+ // viewer to a shell. The fix holds the pane and re-dials the SAME view.sock
4
+ // while the node is still alive, giving up only when it is genuinely gone. This
5
+ // locks the give-up/keep-reconnecting decision (the bug-prone core) without a
6
+ // socket or tmux. See src/clients/attach/{view-socket,attach-cmd}.ts.
7
+ import { test } from 'node:test';
8
+ import assert from 'node:assert/strict';
9
+ import { reconnectShouldGiveUp } from '../view-socket.js';
10
+ const rowWith = (status) => ({
11
+ node_id: 'n',
12
+ status,
13
+ host_kind: 'broker',
14
+ intent: null,
15
+ pi_pid: null,
16
+ });
17
+ test('keeps reconnecting while the node is still alive (active = mid yield/revive)', () => {
18
+ // A yield leaves status='active' (intent='refresh') — must NOT give up.
19
+ assert.equal(reconnectShouldGiveUp(rowWith('active')), false);
20
+ // idle-release revives on the next inbox wake — keep trying (bounded by the
21
+ // supervisor's own ~30s deadline), do not give up.
22
+ assert.equal(reconnectShouldGiveUp(rowWith('idle')), false);
23
+ });
24
+ test('gives up the instant the node is genuinely gone', () => {
25
+ assert.equal(reconnectShouldGiveUp(rowWith('done')), true);
26
+ assert.equal(reconnectShouldGiveUp(rowWith('dead')), true);
27
+ assert.equal(reconnectShouldGiveUp(rowWith('canceled')), true);
28
+ // A reaped row (null) → give up.
29
+ assert.equal(reconnectShouldGiveUp(null), true);
30
+ });
@@ -0,0 +1,42 @@
1
+ // Regression: `/quit` in the attach viewer only sent a `bye` frame + a notice.
2
+ // The broker answers `bye` with socket.end(), which the viewer's close handler
3
+ // treats as a dropped connection and RECONNECTS — so `/quit` never detached (it
4
+ // silently reconnected). The fix routes `/quit` through an `onQuit` hook wired to
5
+ // teardown('detach'). Likewise `/copy` was a dead "not available" stub; it now
6
+ // runs through an `onCopy` hook. This guards the dispatch contract for both.
7
+ // See src/clients/attach/{slash-commands,input-controller,attach-cmd}.ts.
8
+ import { test } from 'node:test';
9
+ import assert from 'node:assert/strict';
10
+ import { dispatchSlashCommand } from '../slash-commands.js';
11
+ function ctx(overrides = {}) {
12
+ const sent = [];
13
+ const notices = [];
14
+ return {
15
+ send: (f) => sent.push(f),
16
+ notify: (m) => notices.push(m),
17
+ sent,
18
+ notices,
19
+ ...overrides,
20
+ };
21
+ }
22
+ test('/quit detaches via onQuit and does NOT send a bare bye (reconnect-loop bug)', () => {
23
+ let quit = 0;
24
+ const c = ctx({ onQuit: () => quit++ });
25
+ const handled = dispatchSlashCommand('/quit', c);
26
+ assert.equal(handled, true);
27
+ assert.equal(quit, 1, '/quit must invoke onQuit (teardown), not just send bye');
28
+ assert.equal(c.sent.length, 0, '/quit must not send a bare bye frame when onQuit is wired');
29
+ });
30
+ test('/quit falls back to a bye frame when onQuit is unwired', () => {
31
+ const c = ctx();
32
+ dispatchSlashCommand('/quit', c);
33
+ assert.deepEqual(c.sent, [{ type: 'bye' }]);
34
+ });
35
+ test('/copy runs through onCopy instead of the dead "not available" stub', () => {
36
+ let copy = 0;
37
+ const c = ctx({ onCopy: () => copy++ });
38
+ const handled = dispatchSlashCommand('/copy', c);
39
+ assert.equal(handled, true);
40
+ assert.equal(copy, 1, '/copy must invoke onCopy');
41
+ assert.equal(c.notices.length, 0, '/copy must not fall through to a notice when onCopy is wired');
42
+ });
@@ -0,0 +1,39 @@
1
+ // Regression: TitledEditor's top border dropped the git-info chip only when the
2
+ // title chip left fewer than 4 free columns — when BOTH chips fit individually
3
+ // but their sum exceeded the terminal width, the fill clamped to 0 and the line
4
+ // came out chipW+infoW wide. pi-tui hard-crashes on any over-wide line:
5
+ // "Error: Rendered line 3192 exceeds terminal width (75 > 70)"
6
+ // (title chip " ⬢ general (base) continue-crouter-development 0 " = 50 cols,
7
+ // info "crouter · ⎇ main · ●⇡5⇣1" = 25 cols, terminal 70). The fix truncates
8
+ // the info chip to the space the title leaves instead of all-or-nothing.
9
+ // See composeTopBorder in src/clients/attach/titled-editor.ts.
10
+ import { test } from 'node:test';
11
+ import assert from 'node:assert/strict';
12
+ import { visibleWidth } from '@earendil-works/pi-tui';
13
+ import { composeTopBorder } from '../titled-editor.js';
14
+ const titleStyle = (s) => `\x1b[7m ${s} \x1b[27m`;
15
+ const borderColor = (s) => `\x1b[38;2;128;128;128m${s}\x1b[39m`;
16
+ const compose = (width, title, info) => composeTopBorder(width, title, info, titleStyle, borderColor);
17
+ test('crash scenario: chip + info that each fit but overflow together stays at width', () => {
18
+ const title = '⬢ general (base) continue-crouter-development 0'; // chip = 50 cols
19
+ const info = `\x1b[38;2;128;128;128mcrouter · \x1b[39m\x1b[38;2;129;162;190m⎇ main\x1b[39m\x1b[38;2;128;128;128m · \x1b[39m\x1b[33m●\x1b[39m⇡5⇣1`; // 25 cols
20
+ const line = compose(70, title, info);
21
+ assert.equal(visibleWidth(line), 70, 'line must be exactly terminal width, not 75');
22
+ });
23
+ test('top border never exceeds width across title/info length combinations', () => {
24
+ const longInfo = `\x1b[33m${'status-info-'.repeat(8)}\x1b[39m`;
25
+ for (const width of [20, 40, 70, 120]) {
26
+ for (const title of ['', 'a', 'session', 'x'.repeat(200)]) {
27
+ for (const info of ['', 'crouter · main', longInfo]) {
28
+ const line = compose(width, title, info);
29
+ assert.ok(visibleWidth(line) <= width, `overflow at width=${width} titleLen=${title.length} infoW=${visibleWidth(info)}: got ${visibleWidth(line)}`);
30
+ }
31
+ }
32
+ }
33
+ });
34
+ test('info chip survives intact when there is room for it', () => {
35
+ const info = 'crouter · main';
36
+ const line = compose(80, 'sess', info);
37
+ assert.ok(line.includes(info), 'fitting info must not be truncated or dropped');
38
+ assert.equal(visibleWidth(line), 80);
39
+ });