@crouton-kit/crouter 0.3.31 → 0.3.33

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 (443) hide show
  1. package/README.md +8 -0
  2. package/dist/build-root.js +1 -0
  3. package/dist/builtin-memory/crouter-development/marketplaces.md +2 -2
  4. package/dist/builtin-memory/crouter-development/personas/base-prompt.md +2 -2
  5. package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +5 -5
  6. package/dist/builtin-memory/crouter-development/personas.md +3 -4
  7. package/dist/builtin-memory/crouter-development/plugins.md +18 -18
  8. package/dist/builtin-memory/design.md +4 -1
  9. package/dist/builtin-memory/development.md +4 -1
  10. package/dist/builtin-memory/internal/INDEX.md +1 -1
  11. package/dist/builtin-memory/internal/nodes-and-canvas.md +6 -6
  12. package/dist/builtin-memory/internal/storage-tiers.md +5 -5
  13. package/dist/builtin-memory/planning.md +4 -1
  14. package/dist/builtin-memory/product.md +80 -0
  15. package/dist/builtin-memory/spec.md +4 -1
  16. package/dist/builtin-personas/advisor/PERSONA.md +10 -0
  17. package/dist/builtin-personas/design/PERSONA.md +2 -2
  18. package/dist/builtin-personas/design/orchestrator.md +3 -3
  19. package/dist/builtin-personas/developer/PERSONA.md +1 -1
  20. package/dist/builtin-personas/developer/orchestrator.md +2 -2
  21. package/dist/builtin-personas/explore/PERSONA.md +3 -3
  22. package/dist/builtin-personas/explore/orchestrator.md +4 -2
  23. package/dist/builtin-personas/general/PERSONA.md +1 -1
  24. package/dist/builtin-personas/general/orchestrator.md +1 -1
  25. package/dist/builtin-personas/orchestration-kernel.md +7 -14
  26. package/dist/builtin-personas/plan/PERSONA.md +1 -1
  27. package/dist/builtin-personas/plan/orchestrator.md +2 -2
  28. package/dist/builtin-personas/plan/reviewers/architecture-fit/PERSONA.md +1 -1
  29. package/dist/builtin-personas/plan/reviewers/code-smells/PERSONA.md +1 -1
  30. package/dist/builtin-personas/plan/reviewers/pattern-consistency/PERSONA.md +1 -1
  31. package/dist/builtin-personas/plan/reviewers/requirements-coverage/PERSONA.md +1 -1
  32. package/dist/builtin-personas/plan/reviewers/security/PERSONA.md +1 -1
  33. package/dist/builtin-personas/product/PERSONA.md +18 -0
  34. package/dist/builtin-personas/product/orchestrator.md +14 -0
  35. package/dist/builtin-personas/product/teardown/PERSONA.md +13 -0
  36. package/dist/builtin-personas/review/PERSONA.md +1 -1
  37. package/dist/builtin-personas/review/orchestrator.md +1 -1
  38. package/dist/builtin-personas/runtime-base.md +5 -0
  39. package/dist/builtin-personas/spec/PERSONA.md +1 -1
  40. package/dist/builtin-personas/spec/orchestrator.md +2 -2
  41. package/dist/builtin-personas/spec/requirements/PERSONA.md +1 -1
  42. package/dist/builtin-views/canvas/core.mjs +82 -1
  43. package/dist/builtin-views/canvas/tui.mjs +9 -5
  44. package/dist/builtin-views/canvas/web.jsx +3 -2
  45. package/dist/builtin-views/chat/core.mjs +725 -0
  46. package/dist/builtin-views/chat/text.mjs +101 -0
  47. package/dist/builtin-views/chat/tui.mjs +368 -0
  48. package/dist/builtin-views/chat/web.jsx +367 -0
  49. package/dist/builtin-views/prompt-review/core.mjs +863 -0
  50. package/dist/builtin-views/prompt-review/text.mjs +15 -0
  51. package/dist/builtin-views/prompt-review/tui.mjs +196 -0
  52. package/dist/builtin-views/prompt-review/web.jsx +484 -0
  53. package/dist/builtin-views/settings/core.mjs +397 -0
  54. package/dist/builtin-views/settings/text.mjs +40 -0
  55. package/dist/builtin-views/settings/tui.mjs +95 -0
  56. package/dist/builtin-views/settings/web.jsx +167 -0
  57. package/dist/cli.js +16 -3
  58. package/dist/clients/attach/__tests__/autocomplete-and-bash-mode.test.js +96 -0
  59. package/dist/clients/attach/__tests__/bash-bang-routing.test.js +58 -0
  60. package/dist/clients/attach/__tests__/bundle-pi-tui-dedup.test.d.ts +1 -0
  61. package/dist/clients/attach/__tests__/bundle-pi-tui-dedup.test.js +56 -0
  62. package/dist/clients/attach/__tests__/titled-editor.test.js +22 -1
  63. package/dist/clients/attach/attach-cmd.d.ts +18 -0
  64. package/dist/clients/attach/attach-cmd.js +1723 -795
  65. package/dist/clients/attach/canvas-panels.js +2 -2
  66. package/dist/clients/attach/chat-view.d.ts +10 -0
  67. package/dist/clients/attach/chat-view.js +23 -0
  68. package/dist/clients/attach/config-load.d.ts +8 -4
  69. package/dist/clients/attach/config-load.js +2 -0
  70. package/dist/clients/attach/extension-dialogs.d.ts +2 -3
  71. package/dist/clients/attach/extension-dialogs.js +7 -9
  72. package/dist/clients/attach/graph-overlay.js +3 -2
  73. package/dist/clients/attach/input-controller.d.ts +4 -3
  74. package/dist/clients/attach/input-controller.js +24 -3
  75. package/dist/clients/attach/slash-commands.d.ts +7 -7
  76. package/dist/clients/attach/slash-commands.js +38 -14
  77. package/dist/clients/attach/titled-editor.js +18 -14
  78. package/dist/clients/attach/view-socket.d.ts +2 -1
  79. package/dist/clients/attach/view-socket.js +27 -8
  80. package/dist/clients/web/__tests__/source-cache.test.d.ts +1 -0
  81. package/dist/clients/web/__tests__/source-cache.test.js +32 -0
  82. package/dist/clients/web/dev-server.js +1 -0
  83. package/dist/clients/web/events.js +9 -11
  84. package/dist/clients/web/server.d.ts +4 -3
  85. package/dist/clients/web/server.js +138 -31
  86. package/dist/clients/web/source-cache.d.ts +10 -0
  87. package/dist/clients/web/source-cache.js +57 -0
  88. package/dist/clients/web/web-cmd.js +28 -9
  89. package/dist/commands/__tests__/human.test.js +30 -28
  90. package/dist/commands/canvas-history/search.js +1 -1
  91. package/dist/commands/canvas-history.js +5 -8
  92. package/dist/commands/canvas-issue.d.ts +2 -0
  93. package/dist/commands/canvas-issue.js +148 -0
  94. package/dist/commands/canvas-prune.js +19 -0
  95. package/dist/commands/canvas-rebuild-index.d.ts +2 -0
  96. package/dist/commands/canvas-rebuild-index.js +57 -0
  97. package/dist/commands/canvas-snapshot.d.ts +2 -0
  98. package/dist/commands/canvas-snapshot.js +48 -0
  99. package/dist/commands/canvas-tmux-spread.d.ts +2 -0
  100. package/dist/commands/canvas-tmux-spread.js +188 -0
  101. package/dist/commands/canvas.d.ts +1 -0
  102. package/dist/commands/canvas.js +16 -1
  103. package/dist/commands/chord.js +143 -48
  104. package/dist/commands/daemon.js +3 -3
  105. package/dist/commands/human/prompts.d.ts +0 -1
  106. package/dist/commands/human/prompts.js +39 -54
  107. package/dist/commands/human/queue.d.ts +12 -1
  108. package/dist/commands/human/queue.js +468 -73
  109. package/dist/commands/human/shared.d.ts +3 -4
  110. package/dist/commands/human/shared.js +3 -3
  111. package/dist/commands/human.js +11 -10
  112. package/dist/commands/memory/find.js +3 -2
  113. package/dist/commands/memory/list.js +4 -3
  114. package/dist/commands/memory/read.js +3 -3
  115. package/dist/commands/memory/shared.d.ts +1 -4
  116. package/dist/commands/memory/shared.js +6 -9
  117. package/dist/commands/memory/write.js +3 -3
  118. package/dist/commands/memory.js +4 -7
  119. package/dist/commands/node-context.d.ts +2 -0
  120. package/dist/commands/node-context.js +172 -0
  121. package/dist/commands/node-snapshot.d.ts +2 -0
  122. package/dist/commands/node-snapshot.js +123 -0
  123. package/dist/commands/node.js +228 -52
  124. package/dist/commands/pkg/plugin-inspect.js +6 -6
  125. package/dist/commands/pkg/plugin-manage.js +1 -1
  126. package/dist/commands/pkg/plugin.js +2 -2
  127. package/dist/commands/pkg.js +2 -2
  128. package/dist/commands/push.d.ts +2 -0
  129. package/dist/commands/push.js +79 -11
  130. package/dist/commands/revive.js +74 -1
  131. package/dist/commands/search/answer.d.ts +1 -0
  132. package/dist/commands/search/answer.js +50 -0
  133. package/dist/commands/search/contents.d.ts +1 -0
  134. package/dist/commands/search/contents.js +96 -0
  135. package/dist/commands/search/exa.d.ts +53 -0
  136. package/dist/commands/search/exa.js +155 -0
  137. package/dist/commands/search/puremd.d.ts +11 -0
  138. package/dist/commands/search/puremd.js +53 -0
  139. package/dist/commands/search/web.d.ts +1 -0
  140. package/dist/commands/search/web.js +65 -0
  141. package/dist/commands/search.d.ts +2 -0
  142. package/dist/commands/search.js +24 -0
  143. package/dist/commands/sys/__tests__/sync-import.test.d.ts +1 -0
  144. package/dist/commands/sys/__tests__/sync-import.test.js +72 -0
  145. package/dist/commands/sys/config.js +59 -10
  146. package/dist/commands/sys/doctor.js +56 -4
  147. package/dist/commands/sys/prompt-review.d.ts +1 -0
  148. package/dist/commands/sys/prompt-review.js +178 -0
  149. package/dist/commands/sys/promptstudio.d.ts +2 -0
  150. package/dist/commands/sys/promptstudio.js +65 -0
  151. package/dist/commands/sys/settings.d.ts +2 -0
  152. package/dist/commands/sys/settings.js +16 -0
  153. package/dist/commands/sys/sync.js +311 -159
  154. package/dist/commands/sys/sysprompt.d.ts +1 -0
  155. package/dist/commands/sys/sysprompt.js +86 -0
  156. package/dist/commands/sys.js +9 -5
  157. package/dist/commands/view-new.js +2 -2
  158. package/dist/core/__tests__/artifact-paths.test.d.ts +1 -0
  159. package/dist/core/__tests__/artifact-paths.test.js +44 -0
  160. package/dist/core/__tests__/broker-preflight.test.d.ts +1 -0
  161. package/dist/core/__tests__/broker-preflight.test.js +85 -0
  162. package/dist/core/__tests__/broker-sdk-wiring.test.js +15 -0
  163. package/dist/core/__tests__/canvas.test.js +37 -1
  164. package/dist/core/__tests__/chat-view-reconnect.test.d.ts +1 -0
  165. package/dist/core/__tests__/chat-view-reconnect.test.js +70 -0
  166. package/dist/core/__tests__/child-death-wake.test.js +11 -2
  167. package/dist/core/__tests__/close.test.js +39 -1
  168. package/dist/core/__tests__/connection-reconnect-resume.test.d.ts +1 -0
  169. package/dist/core/__tests__/connection-reconnect-resume.test.js +37 -0
  170. package/dist/core/__tests__/context-intro.test.js +48 -14
  171. package/dist/core/__tests__/daemon-boot.test.js +182 -9
  172. package/dist/core/__tests__/editor-label.test.d.ts +1 -0
  173. package/dist/core/__tests__/editor-label.test.js +26 -0
  174. package/dist/core/__tests__/fault-marker.test.d.ts +1 -0
  175. package/dist/core/__tests__/fault-marker.test.js +112 -0
  176. package/dist/core/__tests__/fault-retry-rewind.test.d.ts +1 -0
  177. package/dist/core/__tests__/fault-retry-rewind.test.js +123 -0
  178. package/dist/core/__tests__/fixtures/fake-engine.d.ts +30 -4
  179. package/dist/core/__tests__/fixtures/fake-engine.js +156 -17
  180. package/dist/core/__tests__/fixtures/fake-pi-host.js +10 -1
  181. package/dist/core/__tests__/full/human-new-window-regression.test.js +1 -1
  182. package/dist/core/__tests__/helpers/harness.d.ts +1 -1
  183. package/dist/core/__tests__/helpers/harness.js +9 -3
  184. package/dist/core/__tests__/human-stranded-deliver.test.js +38 -1
  185. package/dist/core/__tests__/human-surface-target.test.js +1 -1
  186. package/dist/core/__tests__/kickoff.test.js +18 -19
  187. package/dist/core/__tests__/memory-resolver.test.js +12 -1
  188. package/dist/core/__tests__/migration.test.js +49 -14
  189. package/dist/core/__tests__/model-ladders.test.d.ts +1 -0
  190. package/dist/core/__tests__/model-ladders.test.js +160 -0
  191. package/dist/core/__tests__/node-env.test.d.ts +1 -0
  192. package/dist/core/__tests__/node-env.test.js +26 -0
  193. package/dist/core/__tests__/push-final-guard.test.js +7 -1
  194. package/dist/core/__tests__/relaunch-root.test.js +9 -9
  195. package/dist/core/__tests__/revive.test.js +36 -6
  196. package/dist/core/__tests__/spawn-root.test.js +24 -1
  197. package/dist/core/__tests__/stop-guard.test.js +10 -0
  198. package/dist/core/__tests__/stranded-relaunch.test.d.ts +1 -0
  199. package/dist/core/__tests__/stranded-relaunch.test.js +72 -0
  200. package/dist/core/__tests__/tmux-surface.test.js +5 -1
  201. package/dist/core/__tests__/unknown-path.test.js +19 -19
  202. package/dist/core/__tests__/yield-ensures-daemon.test.d.ts +1 -0
  203. package/dist/core/__tests__/yield-ensures-daemon.test.js +54 -0
  204. package/dist/core/artifact.d.ts +2 -33
  205. package/dist/core/artifact.js +27 -87
  206. package/dist/core/bootstrap.js +2 -0
  207. package/dist/core/canvas/browse/__tests__/model.test.js +35 -1
  208. package/dist/core/canvas/browse/app.js +87 -17
  209. package/dist/core/canvas/browse/model.d.ts +10 -1
  210. package/dist/core/canvas/browse/model.js +37 -1
  211. package/dist/core/canvas/browse/render.js +35 -33
  212. package/dist/core/canvas/canvas.d.ts +7 -0
  213. package/dist/core/canvas/canvas.js +54 -5
  214. package/dist/core/canvas/db.js +35 -8
  215. package/dist/core/canvas/labels.d.ts +6 -8
  216. package/dist/core/canvas/labels.js +8 -11
  217. package/dist/core/canvas/nav-model.d.ts +5 -4
  218. package/dist/core/canvas/nav-model.js +31 -17
  219. package/dist/core/canvas/paths.d.ts +18 -1
  220. package/dist/core/canvas/paths.js +37 -2
  221. package/dist/core/canvas/render.d.ts +25 -0
  222. package/dist/core/canvas/render.js +96 -13
  223. package/dist/core/canvas/status-glyph.d.ts +35 -0
  224. package/dist/core/canvas/status-glyph.js +104 -0
  225. package/dist/core/canvas/types.d.ts +2 -2
  226. package/dist/core/config.js +114 -16
  227. package/dist/core/fault-classifier.d.ts +41 -0
  228. package/dist/core/fault-classifier.js +110 -0
  229. package/dist/core/feed/__tests__/empty-node-guard.test.d.ts +1 -0
  230. package/dist/core/feed/__tests__/empty-node-guard.test.js +45 -0
  231. package/dist/core/feed/feed.js +8 -0
  232. package/dist/core/feed/inbox.d.ts +15 -0
  233. package/dist/core/feed/inbox.js +40 -14
  234. package/dist/core/frontmatter.d.ts +1 -11
  235. package/dist/core/frontmatter.js +1 -55
  236. package/dist/core/hearth/config.d.ts +2 -0
  237. package/dist/core/hearth/config.js +75 -0
  238. package/dist/core/hearth/guest-env.d.ts +2 -0
  239. package/dist/core/hearth/guest-env.js +11 -0
  240. package/dist/core/hearth/index.d.ts +5 -0
  241. package/dist/core/hearth/index.js +5 -0
  242. package/dist/core/hearth/provider.d.ts +21 -0
  243. package/dist/core/hearth/provider.js +10 -0
  244. package/dist/core/hearth/providers/blaxel-bootstrap.d.ts +10 -0
  245. package/dist/core/hearth/providers/blaxel-bootstrap.js +39 -0
  246. package/dist/core/hearth/providers/blaxel-home.d.ts +11 -0
  247. package/dist/core/hearth/providers/blaxel-home.js +126 -0
  248. package/dist/core/hearth/providers/blaxel.d.ts +26 -0
  249. package/dist/core/hearth/providers/blaxel.js +208 -0
  250. package/dist/core/hearth/providers/types.d.ts +72 -0
  251. package/dist/core/hearth/providers/types.js +1 -0
  252. package/dist/core/hearth/registry.d.ts +15 -0
  253. package/dist/core/hearth/registry.js +179 -0
  254. package/dist/core/hearth/types.d.ts +121 -0
  255. package/dist/core/hearth/types.js +1 -0
  256. package/dist/core/host-exports/__tests__/export-prunes-boot-skill.test.d.ts +1 -0
  257. package/dist/core/host-exports/__tests__/export-prunes-boot-skill.test.js +79 -0
  258. package/dist/core/{skill-sync → host-exports}/builtins.d.ts +21 -13
  259. package/dist/core/host-exports/builtins.js +68 -0
  260. package/dist/core/{skill-sync → host-exports}/export.d.ts +9 -6
  261. package/dist/core/{skill-sync → host-exports}/export.js +37 -14
  262. package/dist/core/io.js +1 -1
  263. package/dist/core/log.d.ts +9 -0
  264. package/dist/core/log.js +113 -0
  265. package/dist/core/memory-resolver.d.ts +8 -8
  266. package/dist/core/memory-resolver.js +15 -28
  267. package/dist/core/personas/index.d.ts +4 -4
  268. package/dist/core/personas/index.js +2 -2
  269. package/dist/core/personas/loader.d.ts +51 -0
  270. package/dist/core/personas/loader.js +54 -50
  271. package/dist/core/personas/resolve.d.ts +43 -27
  272. package/dist/core/personas/resolve.js +336 -94
  273. package/dist/core/runtime/auth-reload.d.ts +7 -0
  274. package/dist/core/runtime/auth-reload.js +69 -0
  275. package/dist/core/runtime/bearings.d.ts +35 -15
  276. package/dist/core/runtime/bearings.js +305 -44
  277. package/dist/core/runtime/branded-host.d.ts +4 -2
  278. package/dist/core/runtime/branded-host.js +66 -4
  279. package/dist/core/runtime/broker-protocol.d.ts +38 -3
  280. package/dist/core/runtime/broker.js +343 -18
  281. package/dist/core/runtime/close.js +37 -29
  282. package/dist/core/runtime/connectivity.d.ts +12 -0
  283. package/dist/core/runtime/connectivity.js +73 -0
  284. package/dist/core/runtime/fault-recovery-nudge.d.ts +2 -0
  285. package/dist/core/runtime/fault-recovery-nudge.js +2 -0
  286. package/dist/core/runtime/fault-recovery.d.ts +23 -0
  287. package/dist/core/runtime/fault-recovery.js +74 -0
  288. package/dist/core/runtime/fault.d.ts +25 -0
  289. package/dist/core/runtime/fault.js +176 -0
  290. package/dist/core/runtime/front-door.d.ts +1 -1
  291. package/dist/core/runtime/front-door.js +2 -2
  292. package/dist/core/runtime/host.d.ts +10 -0
  293. package/dist/core/runtime/host.js +71 -9
  294. package/dist/core/runtime/kickoff.js +31 -45
  295. package/dist/core/runtime/launch.d.ts +32 -18
  296. package/dist/core/runtime/launch.js +148 -43
  297. package/dist/core/runtime/model-swap.d.ts +18 -0
  298. package/dist/core/runtime/model-swap.js +95 -0
  299. package/dist/core/runtime/naming.d.ts +7 -0
  300. package/dist/core/runtime/naming.js +61 -9
  301. package/dist/core/runtime/nodes.js +6 -1
  302. package/dist/core/runtime/persona.js +16 -17
  303. package/dist/core/runtime/placement.d.ts +36 -31
  304. package/dist/core/runtime/placement.js +149 -64
  305. package/dist/core/runtime/promote.d.ts +2 -0
  306. package/dist/core/runtime/promote.js +23 -3
  307. package/dist/core/runtime/recycle.js +6 -4
  308. package/dist/core/runtime/reset.d.ts +5 -2
  309. package/dist/core/runtime/reset.js +2 -2
  310. package/dist/core/runtime/revive.d.ts +4 -0
  311. package/dist/core/runtime/revive.js +27 -9
  312. package/dist/core/runtime/roadmap.d.ts +1 -1
  313. package/dist/core/runtime/roadmap.js +1 -1
  314. package/dist/core/runtime/spawn.d.ts +2 -2
  315. package/dist/core/runtime/spawn.js +30 -6
  316. package/dist/core/runtime/stop-guard.js +6 -1
  317. package/dist/core/runtime/tmux-chrome.d.ts +1 -1
  318. package/dist/core/runtime/tmux-chrome.js +1 -1
  319. package/dist/core/runtime/tmux.d.ts +29 -6
  320. package/dist/core/runtime/tmux.js +134 -80
  321. package/dist/core/runtime/view-socket-client.d.ts +46 -0
  322. package/dist/core/runtime/view-socket-client.js +203 -0
  323. package/dist/core/substrate/index.d.ts +1 -1
  324. package/dist/core/substrate/index.js +1 -1
  325. package/dist/core/substrate/on-read.js +16 -4
  326. package/dist/core/substrate/render.d.ts +10 -13
  327. package/dist/core/substrate/render.js +56 -51
  328. package/dist/core/tui/host.js +83 -18
  329. package/dist/core/view/__tests__/transport-cache.test.d.ts +1 -0
  330. package/dist/core/view/__tests__/transport-cache.test.js +62 -0
  331. package/dist/core/view/contract.d.ts +20 -0
  332. package/dist/core/view/stream-local.d.ts +3 -0
  333. package/dist/core/view/stream-local.js +184 -0
  334. package/dist/core/view/transport-cache.d.ts +8 -0
  335. package/dist/core/view/transport-cache.js +38 -0
  336. package/dist/core/view/transport-local.js +4 -0
  337. package/dist/core/view/transport.d.ts +7 -1
  338. package/dist/daemon/crtrd.d.ts +3 -19
  339. package/dist/daemon/crtrd.js +139 -178
  340. package/dist/daemon/manage.d.ts +18 -1
  341. package/dist/daemon/manage.js +54 -9
  342. package/dist/hearth/wake-proxy/__tests__/anthropic-oauth.test.d.ts +1 -0
  343. package/dist/hearth/wake-proxy/__tests__/anthropic-oauth.test.js +289 -0
  344. package/dist/hearth/wake-proxy/__tests__/config-timeout.test.d.ts +1 -0
  345. package/dist/hearth/wake-proxy/__tests__/config-timeout.test.js +34 -0
  346. package/dist/hearth/wake-proxy/__tests__/guest-source.test.d.ts +1 -0
  347. package/dist/hearth/wake-proxy/__tests__/guest-source.test.js +203 -0
  348. package/dist/hearth/wake-proxy/__tests__/hardening.test.d.ts +1 -0
  349. package/dist/hearth/wake-proxy/__tests__/hardening.test.js +59 -0
  350. package/dist/hearth/wake-proxy/__tests__/hearth-status-route.test.d.ts +1 -0
  351. package/dist/hearth/wake-proxy/__tests__/hearth-status-route.test.js +372 -0
  352. package/dist/hearth/wake-proxy/__tests__/http-running-route-fast-path.test.d.ts +1 -0
  353. package/dist/hearth/wake-proxy/__tests__/http-running-route-fast-path.test.js +258 -0
  354. package/dist/hearth/wake-proxy/__tests__/model-auth-routes.test.d.ts +1 -0
  355. package/dist/hearth/wake-proxy/__tests__/model-auth-routes.test.js +437 -0
  356. package/dist/hearth/wake-proxy/__tests__/source-bridge-gate.test.d.ts +1 -0
  357. package/dist/hearth/wake-proxy/__tests__/source-bridge-gate.test.js +455 -0
  358. package/dist/hearth/wake-proxy/__tests__/static-asset-allowlist.test.d.ts +1 -0
  359. package/dist/hearth/wake-proxy/__tests__/static-asset-allowlist.test.js +15 -0
  360. package/dist/hearth/wake-proxy/__tests__/warm-path-concurrency.test.d.ts +1 -0
  361. package/dist/hearth/wake-proxy/__tests__/warm-path-concurrency.test.js +141 -0
  362. package/dist/hearth/wake-proxy/__tests__/ws-teardown-accounting.test.d.ts +1 -0
  363. package/dist/hearth/wake-proxy/__tests__/ws-teardown-accounting.test.js +143 -0
  364. package/dist/hearth/wake-proxy/anthropic-oauth.d.ts +58 -0
  365. package/dist/hearth/wake-proxy/anthropic-oauth.js +189 -0
  366. package/dist/hearth/wake-proxy/auth.d.ts +22 -0
  367. package/dist/hearth/wake-proxy/auth.js +128 -0
  368. package/dist/hearth/wake-proxy/config.d.ts +2 -0
  369. package/dist/hearth/wake-proxy/config.js +151 -0
  370. package/dist/hearth/wake-proxy/guest-source.d.ts +14 -0
  371. package/dist/hearth/wake-proxy/guest-source.js +130 -0
  372. package/dist/hearth/wake-proxy/hearth-status.d.ts +44 -0
  373. package/dist/hearth/wake-proxy/hearth-status.js +267 -0
  374. package/dist/hearth/wake-proxy/home.d.ts +67 -0
  375. package/dist/hearth/wake-proxy/home.js +297 -0
  376. package/dist/hearth/wake-proxy/main.d.ts +1 -0
  377. package/dist/hearth/wake-proxy/main.js +134 -0
  378. package/dist/hearth/wake-proxy/model-auth.d.ts +13 -0
  379. package/dist/hearth/wake-proxy/model-auth.js +345 -0
  380. package/dist/hearth/wake-proxy/proxy.d.ts +35 -0
  381. package/dist/hearth/wake-proxy/proxy.js +716 -0
  382. package/dist/hearth/wake-proxy/public-source-gate.d.ts +9 -0
  383. package/dist/hearth/wake-proxy/public-source-gate.js +409 -0
  384. package/dist/hearth/wake-proxy/redact.d.ts +1 -0
  385. package/dist/hearth/wake-proxy/redact.js +17 -0
  386. package/dist/hearth/wake-proxy/server.d.ts +11 -0
  387. package/dist/hearth/wake-proxy/server.js +142 -0
  388. package/dist/hearth/wake-proxy/state.d.ts +18 -0
  389. package/dist/hearth/wake-proxy/state.js +342 -0
  390. package/dist/hearth/wake-proxy/types.d.ts +76 -0
  391. package/dist/hearth/wake-proxy/types.js +1 -0
  392. package/dist/index.js +3 -2
  393. package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +6 -5
  394. package/dist/pi-extensions/canvas-context-intro.js +7 -7
  395. package/dist/pi-extensions/canvas-doc-substrate.js +19 -22
  396. package/dist/pi-extensions/canvas-nav.js +15 -8
  397. package/dist/pi-extensions/canvas-stophook.d.ts +1 -1
  398. package/dist/pi-extensions/canvas-stophook.js +78 -11
  399. package/dist/types.d.ts +39 -20
  400. package/dist/types.js +38 -13
  401. package/dist/web/runtime.js +141 -42
  402. package/dist/web/transport-http.js +7 -5
  403. package/dist/web/transport-stream.d.ts +3 -0
  404. package/dist/web/transport-stream.js +204 -0
  405. package/dist/web-client/assets/fragment-mono-latin-400-normal-BYwT3kSJ.woff +0 -0
  406. package/dist/web-client/assets/fragment-mono-latin-400-normal-yxdJ5AmL.woff2 +0 -0
  407. package/dist/web-client/assets/fragment-mono-latin-ext-400-normal-BbKYyvR9.woff2 +0 -0
  408. package/dist/web-client/assets/fragment-mono-latin-ext-400-normal-CT4YFKeK.woff +0 -0
  409. package/dist/web-client/assets/fraunces-latin-ext-wght-normal-Ca2vKHc0.woff2 +0 -0
  410. package/dist/web-client/assets/fraunces-latin-wght-normal-ukD16Tqj.woff2 +0 -0
  411. package/dist/web-client/assets/fraunces-vietnamese-wght-normal-CnvboYUG.woff2 +0 -0
  412. package/dist/web-client/assets/index-BZUxTkv5.css +2 -0
  413. package/dist/web-client/assets/index-D36PNBj4.js +80 -0
  414. package/dist/web-client/assets/instrument-sans-latin-ext-wght-normal-B5bTHO_g.woff2 +0 -0
  415. package/dist/web-client/assets/instrument-sans-latin-wght-normal-BbzFLZTg.woff2 +0 -0
  416. package/dist/web-client/assets/martian-mono-cyrillic-wght-normal-B84CD5C_.woff2 +0 -0
  417. package/dist/web-client/assets/martian-mono-latin-ext-wght-normal-DlL6xMw5.woff2 +0 -0
  418. package/dist/web-client/assets/martian-mono-latin-wght-normal-5W32yIyr.woff2 +0 -0
  419. package/dist/web-client/index.html +3 -2
  420. package/package.json +32 -10
  421. package/dist/core/__tests__/error-stall-recycle.test.js +0 -141
  422. package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.js +0 -57
  423. package/dist/core/skill-sync/builtins.js +0 -112
  424. package/dist/core/skill-sync/claude-plugins.d.ts +0 -23
  425. package/dist/core/skill-sync/claude-plugins.js +0 -71
  426. package/dist/core/skill-sync/engine.d.ts +0 -42
  427. package/dist/core/skill-sync/engine.js +0 -633
  428. package/dist/core/skill-sync/manifest.d.ts +0 -64
  429. package/dist/core/skill-sync/manifest.js +0 -181
  430. package/dist/core/skill-sync/profile.d.ts +0 -76
  431. package/dist/core/skill-sync/profile.js +0 -173
  432. package/dist/core/skill-sync/snapshot.d.ts +0 -57
  433. package/dist/core/skill-sync/snapshot.js +0 -120
  434. package/dist/pi-extensions/canvas-commands.d.ts +0 -37
  435. package/dist/pi-extensions/canvas-commands.js +0 -113
  436. package/dist/pi-extensions/canvas-resume.d.ts +0 -21
  437. package/dist/pi-extensions/canvas-resume.js +0 -83
  438. package/dist/pi-extensions/canvas-view.d.ts +0 -21
  439. package/dist/pi-extensions/canvas-view.js +0 -76
  440. package/dist/web-client/assets/index-BUvQb4hR.css +0 -2
  441. package/dist/web-client/assets/index-ClLQXYAE.js +0 -10
  442. /package/dist/{core/__tests__/error-stall-recycle.test.d.ts → clients/attach/__tests__/autocomplete-and-bash-mode.test.d.ts} +0 -0
  443. /package/dist/{core/skill-sync/__tests__/dry-run-wrote-count.test.d.ts → clients/attach/__tests__/bash-bang-routing.test.d.ts} +0 -0
@@ -1,37 +1,6 @@
1
- export type ArtifactKind = 'plans' | 'specs';
2
1
  export declare function mangleCwd(cwd?: string): string;
3
- export declare function artifactsRoot(kind: ArtifactKind, cwd?: string): string;
4
- /** Per-cwd interactions root, mirroring artifactsRoot construction.
5
- * `~/.crouter/<mangled-cwd>/interactions/`. */
2
+ export declare function workspaceRoot(cwd?: string): string;
6
3
  export declare function interactionsRoot(cwd?: string): string;
7
- /** Directory for one interaction. `id` is sanitized like an artifact name. */
8
4
  export declare function interactionDir(id: string, cwd?: string): string;
9
5
  export declare function sanitizeName(raw: string): string;
10
- export declare function artifactPath(kind: ArtifactKind, name: string, cwd?: string): string;
11
- /** Lines above this threshold trigger an oversize advisory in follow_up. */
12
- export declare const OVERSIZE_WARN_LINES = 200;
13
- export interface SaveArtifactResult {
14
- path: string;
15
- oversize: boolean;
16
- lineCount: number;
17
- }
18
- /**
19
- * Atomically write an artifact. Prepends a minimal frontmatter block when
20
- * `meta` is non-empty so readers can extract structured fields without
21
- * parsing the full body. Returns the written path and oversize status.
22
- */
23
- export declare function saveArtifact(kind: ArtifactKind, name: string, body: string, meta?: Record<string, string>): SaveArtifactResult;
24
- export interface ArtifactRecord {
25
- name: string;
26
- path: string;
27
- body: string;
28
- /** Present only in plan artifacts; null if not set. */
29
- spec: string | null;
30
- }
31
- export declare function readArtifact(kind: ArtifactKind, name: string): ArtifactRecord;
32
- export interface ArtifactListItem {
33
- name: string;
34
- path: string;
35
- updated_at: string;
36
- }
37
- export declare function listArtifacts(kind: ArtifactKind): ArtifactListItem[];
6
+ export declare function migrateLegacyWorkspaceDirs(): void;
@@ -1,24 +1,20 @@
1
- // Core artifact primitives for plan and spec subtrees.
1
+ // Core per-cwd workspace primitives for human interaction artifacts.
2
2
  // No commander, no spawn, no output helpers — pure data logic only.
3
- // Old registerArtifactCommand (commander-based) is removed; callers ported to JSON I/O.
4
3
  import { homedir } from 'node:os';
5
- import { join, dirname } from 'node:path';
6
- import { writeFileSync, statSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { renameSync } from 'node:fs';
7
6
  import { CRTR_DIR_NAME } from '../types.js';
8
- import { ensureDir, pathExists, readText, walkFiles } from './fs-utils.js';
9
- import { notFound, usage } from './errors.js';
7
+ import { ensureDir, listDirs, pathExists, removePath } from './fs-utils.js';
8
+ import { usage } from './errors.js';
10
9
  export function mangleCwd(cwd = process.cwd()) {
11
10
  return cwd.replace(/\//g, '-');
12
11
  }
13
- export function artifactsRoot(kind, cwd) {
14
- return join(homedir(), CRTR_DIR_NAME, mangleCwd(cwd), kind);
12
+ export function workspaceRoot(cwd) {
13
+ return join(homedir(), CRTR_DIR_NAME, 'workspaces', mangleCwd(cwd));
15
14
  }
16
- /** Per-cwd interactions root, mirroring artifactsRoot construction.
17
- * `~/.crouter/<mangled-cwd>/interactions/`. */
18
15
  export function interactionsRoot(cwd) {
19
- return join(homedir(), CRTR_DIR_NAME, mangleCwd(cwd), 'interactions');
16
+ return join(workspaceRoot(cwd), 'interactions');
20
17
  }
21
- /** Directory for one interaction. `id` is sanitized like an artifact name. */
22
18
  export function interactionDir(id, cwd) {
23
19
  return join(interactionsRoot(cwd), sanitizeName(id));
24
20
  }
@@ -34,84 +30,28 @@ export function sanitizeName(raw) {
34
30
  }
35
31
  return trimmed;
36
32
  }
37
- export function artifactPath(kind, name, cwd) {
38
- return join(artifactsRoot(kind, cwd), `${sanitizeName(name)}.md`);
39
- }
40
- /** Lines above this threshold trigger an oversize advisory in follow_up. */
41
- export const OVERSIZE_WARN_LINES = 200;
42
- /**
43
- * Atomically write an artifact. Prepends a minimal frontmatter block when
44
- * `meta` is non-empty so readers can extract structured fields without
45
- * parsing the full body. Returns the written path and oversize status.
46
- */
47
- export function saveArtifact(kind, name, body, meta = {}) {
48
- const filePath = artifactPath(kind, name);
49
- ensureDir(dirname(filePath));
50
- const metaKeys = Object.keys(meta);
51
- let content;
52
- if (metaKeys.length > 0) {
53
- const fm = '---\n' + metaKeys.map((k) => `${k}: ${meta[k]}`).join('\n') + '\n---\n';
54
- content = fm + body;
55
- }
56
- else {
57
- content = body;
58
- }
59
- const finalContent = content.endsWith('\n') ? content : content + '\n';
60
- writeFileSync(filePath, finalContent, 'utf8');
61
- const lineCount = finalContent.split('\n').length - 1;
62
- return { path: filePath, oversize: lineCount > OVERSIZE_WARN_LINES, lineCount };
63
- }
64
- const FRONTMATTER_BLOCK_RE = /^---\s*\r?\n([\s\S]*?)\r?\n---\s*\r?\n?/;
65
- function parseArtifactFrontmatter(source) {
66
- const match = source.match(FRONTMATTER_BLOCK_RE);
67
- if (!match)
68
- return { fields: {}, body: source };
69
- const raw = match[1];
70
- const body = source.slice(match[0].length);
71
- const fields = {};
72
- for (const line of raw.split(/\r?\n/)) {
73
- const colon = line.indexOf(':');
74
- if (colon === -1)
33
+ const LEGACY_WORKSPACE_DIR_RE = /^-[A-Za-z0-9].*/;
34
+ export function migrateLegacyWorkspaceDirs() {
35
+ const dataHome = join(homedir(), CRTR_DIR_NAME);
36
+ const workspaceHome = join(dataHome, 'workspaces');
37
+ if (!pathExists(dataHome))
38
+ return;
39
+ ensureDir(workspaceHome);
40
+ for (const entry of listDirs(dataHome)) {
41
+ if (entry === 'workspaces' || !LEGACY_WORKSPACE_DIR_RE.test(entry))
75
42
  continue;
76
- const k = line.slice(0, colon).trim();
77
- const v = line.slice(colon + 1).trim();
78
- if (k !== '')
79
- fields[k] = v;
80
- }
81
- return { fields, body };
82
- }
83
- export function readArtifact(kind, name) {
84
- const filePath = artifactPath(kind, name);
85
- if (!pathExists(filePath)) {
86
- throw notFound(`${kind.slice(0, -1)} not found: ${name} (looked at ${filePath})`);
87
- }
88
- const raw = readText(filePath);
89
- const { fields, body } = parseArtifactFrontmatter(raw);
90
- const specField = fields['spec'];
91
- return {
92
- name,
93
- path: filePath,
94
- body,
95
- spec: specField !== undefined ? specField : null,
96
- };
97
- }
98
- export function listArtifacts(kind) {
99
- const root = artifactsRoot(kind);
100
- if (!pathExists(root))
101
- return [];
102
- const files = walkFiles(root, (n) => n.endsWith('.md'));
103
- const items = files.map((abs) => {
104
- const name = abs.substring(root.length + 1).replace(/\.md$/, '');
105
- let updated_at = '';
43
+ const source = join(dataHome, entry);
44
+ const target = join(workspaceHome, entry);
45
+ if (pathExists(target)) {
46
+ removePath(source);
47
+ continue;
48
+ }
106
49
  try {
107
- updated_at = statSync(abs).mtime.toISOString();
50
+ renameSync(source, target);
108
51
  }
109
52
  catch {
110
- updated_at = new Date(0).toISOString();
53
+ // Leave the source in place only if the rename truly failed.
54
+ // The next startup will retry the same hard-cut move.
111
55
  }
112
- return { name, path: abs, updated_at };
113
- });
114
- // Sort ascending by name (stable key for pagination cursor).
115
- items.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
116
- return items;
56
+ }
117
57
  }
@@ -4,6 +4,7 @@ import { findProjectScopeRoot, resetScopeCache, userScopeRoot } from './scope.js
4
4
  import { ensureDir, pathExists, removePath, nowIso } from './fs-utils.js';
5
5
  import { readConfig, readState, updateConfig, updateState, ensureScopeInitialized } from './config.js';
6
6
  import { clone } from './git.js';
7
+ import { migrateLegacyWorkspaceDirs } from './artifact.js';
7
8
  import { readMarketplaceManifest } from './manifest.js';
8
9
  import { CRTR_DIR_NAME } from '../types.js';
9
10
  export const OFFICIAL_MARKETPLACE_NAME = 'crouter-official-marketplace';
@@ -76,6 +77,7 @@ export function ensureProjectScope(argv) {
76
77
  return;
77
78
  if (shouldSkipForArgv(argv))
78
79
  return;
80
+ migrateLegacyWorkspaceDirs();
79
81
  // Already inside a project scope (here or in an ancestor) — nothing to do.
80
82
  if (findProjectScopeRoot() !== null)
81
83
  return;
@@ -1,6 +1,6 @@
1
1
  import { test } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
- import { buildTree, flatten, fuzzyMatch, tabPredicate, TABS, attentionTier, attentionMtime } from '../model.js';
3
+ import { buildTree, flatten, fuzzyMatch, pruneNode, tabPredicate, TABS, attentionTier, attentionMtime } from '../model.js';
4
4
  // ── Fixture canvas ───────────────────────────────────────────────────────────
5
5
  // root1 (active) ← rank 0
6
6
  // child-a (idle)
@@ -73,6 +73,40 @@ test('buildTree: unknown child ids are dropped (missing meta safe)', () => {
73
73
  assert.deepEqual(t.nodes.get('root1').childIds, ['child-a']);
74
74
  assert.equal(t.nodes.has('ghost'), false);
75
75
  });
76
+ // ── pruneNode ────────────────────────────────────────────────────────────────
77
+ // Regression: pressing `x` in canvas browse on an EMPTY active node looked like a
78
+ // no-op — closeNode hard-deletes (reaps) an empty node, but doClose never removed
79
+ // it from the in-memory tree, so the stale active row kept painting. pruneNode is
80
+ // the splice doClose now applies for a reaped (getNode===null) close.
81
+ test('pruneNode: reaping a leaf removes it from its parent and the node map', () => {
82
+ const t = tree();
83
+ pruneNode(t, 'grand-x'); // leaf under child-a
84
+ assert.equal(t.nodes.has('grand-x'), false);
85
+ assert.deepEqual(t.nodes.get('child-a').childIds, []);
86
+ });
87
+ test('pruneNode: reaping a mid-tree node re-homes its children to its parent', () => {
88
+ const t = tree();
89
+ pruneNode(t, 'child-a'); // child-a (under root1) owns grand-x
90
+ assert.equal(t.nodes.has('child-a'), false);
91
+ // grand-x lifts into child-a's slot in root1's child order (before child-b).
92
+ assert.deepEqual(t.nodes.get('root1').childIds, ['grand-x', 'child-b']);
93
+ assert.equal(t.nodes.get('grand-x').parentId, 'root1');
94
+ });
95
+ test('pruneNode: reaping a root lifts its children into the root list', () => {
96
+ const t = tree();
97
+ const rootIdx = t.roots.indexOf('root2');
98
+ pruneNode(t, 'root2'); // root2 owns child-c
99
+ assert.equal(t.nodes.has('root2'), false);
100
+ assert.equal(t.roots.includes('root2'), false);
101
+ assert.equal(t.roots[rootIdx], 'child-c'); // child-c took root2's slot
102
+ assert.equal(t.nodes.get('child-c').parentId, null);
103
+ });
104
+ test('pruneNode: absent id is a no-op', () => {
105
+ const t = tree();
106
+ const before = t.nodes.size;
107
+ pruneNode(t, 'ghost');
108
+ assert.equal(t.nodes.size, before);
109
+ });
76
110
  // ── tab predicates ───────────────────────────────────────────────────────────
77
111
  test('tabPredicate: All / Live / Dormant / Flagged', () => {
78
112
  assert.deepEqual([...TABS], ['All', 'Live', 'Dormant', 'Flagged']);
@@ -6,14 +6,14 @@
6
6
  //
7
7
  // Resume is one action: Enter routes the chosen node through `crtr node focus`,
8
8
  // which goes via reviveNode() — the ONLY sanctioned open. NEVER spawn
9
- // `pi --session` directly (see canvas-resume.ts header for the desync hazard).
9
+ // `pi --session` directly (see reviveNode() in revive.ts for the desync hazard).
10
10
  import { execFileSync } from 'node:child_process';
11
11
  import { resolve } from 'node:path';
12
12
  import { dashboardRowsAll, enrichRows, loadPreview, renderForest } from '../render.js';
13
13
  import { listNodes, subscriptionsOf, getNode } from '../canvas.js';
14
14
  import { closeNode } from '../../runtime/close.js';
15
15
  import { setupTerminal, restoreTerminal, getTerminalSize, parseKeypress, } from '../../tui/terminal.js';
16
- import { buildTree, flatten, TABS } from './model.js';
16
+ import { buildTree, flatten, pruneNode, TABS } from './model.js';
17
17
  // Sort cycle for the `s` key. Starts on the default `attention` ordering; one `s`
18
18
  // press restores the structural `tree` view, then relevance/recency, then back.
19
19
  const SORT_CYCLE = ['attention', 'tree', 'relevance', 'recency'];
@@ -34,7 +34,7 @@ export async function runBrowse(opts = {}) {
34
34
  }
35
35
  // Snapshot the canvas. Drop kind:'human' control-plane decks — they have no pi
36
36
  // session, so `node focus` refuses them; they are never a navigation/resume
37
- // target (mirrors canvas-resume.ts / the node focus guard).
37
+ // target (mirrors the node focus guard).
38
38
  const rows = dashboardRowsAll().filter((r) => r.kind !== 'human');
39
39
  const rootIds = listNodes()
40
40
  .filter((n) => n.parent === null && n.kind !== 'human')
@@ -80,6 +80,17 @@ export async function runBrowse(opts = {}) {
80
80
  // Safety net: an uncaught throw in the (un-unit-tested) keystroke path must
81
81
  // never strand the tty in raw + alt-screen + hidden-cursor.
82
82
  process.once('exit', cleanup);
83
+ /** On-demand kick of a hanging node: shell `canvas revive <id> --now` (which
84
+ * SIGTERMs the live broker — the daemon resumes it). Synchronous + quiet
85
+ * (stdio ignored) so it never takes over the TUI; best-effort. */
86
+ const reviveNow = (id) => {
87
+ try {
88
+ execFileSync('crtr', ['canvas', 'revive', id, '--now'], { stdio: 'ignore' });
89
+ }
90
+ catch {
91
+ /* best-effort — the SIGTERM is what matters; the daemon owns the resume */
92
+ }
93
+ };
83
94
  /** Open the chosen node — the ONLY sanctioned path (reviveNode via node focus). */
84
95
  const selectAndFocus = (id) => {
85
96
  cleanup();
@@ -188,16 +199,26 @@ export async function runBrowse(opts = {}) {
188
199
  return; // unknown/already-gone node — nothing to reflect
189
200
  }
190
201
  for (const cid of closed) {
202
+ const m = getNode(cid);
203
+ if (m === null) {
204
+ // Reaped, not parked: an EMPTY node (0k ctx, no assistant message) is
205
+ // hard-deleted by closeNode (row + dir gone). Splice it out of the tree so
206
+ // its row disappears — otherwise the stale active husk lingers on screen and
207
+ // `x` looks like a no-op (the row was the only thing that ever changed).
208
+ pruneNode(tree, cid);
209
+ continue;
210
+ }
191
211
  const n = tree.nodes.get(cid);
192
212
  if (n === undefined)
193
213
  continue;
194
- const m = getNode(cid);
195
- if (m !== null)
196
- n.row.status = m.status; // real post-close status (done / canceled)
214
+ n.row.status = m.status; // real post-close status (done / canceled)
197
215
  n.row.streaming = false;
198
216
  n.row.viewed = false;
199
217
  }
200
- recompute(id);
218
+ // Keep the cursor at the same INDEX (the row that shifts up into the closed
219
+ // slot), not pinned to the now-gone node — recompute() leaves state.cursor
220
+ // put and clamps it to the new bounds.
221
+ recompute();
201
222
  };
202
223
  const cycleTab = (dir) => {
203
224
  const i = TABS.indexOf(state.tab);
@@ -228,10 +249,29 @@ export async function runBrowse(opts = {}) {
228
249
  state.residentsOnly = !state.residentsOnly;
229
250
  recompute(keep);
230
251
  };
252
+ // Coalesced search repaint. The expensive work per keystroke is recompute()
253
+ // (a fuzzyMatch flatten over the whole corpus) + a full renderFrame. Doing it
254
+ // synchronously on every key blocks the event loop, so fast typing batches into
255
+ // the stdin reads — and batched chunks used to be dropped (see onKeySearch).
256
+ // Instead, each key only mutates state.query (cheap) and marks the view dirty;
257
+ // a single setImmediate drains AFTER every queued stdin 'data' callback (poll
258
+ // phase) has appended its chars, so N rapid keys collapse to ONE recompute+paint
259
+ // and no keystroke is ignored. The sort may still take a moment — that's fine.
260
+ let searchRepaintScheduled = false;
261
+ const scheduleSearchRepaint = () => {
262
+ if (searchRepaintScheduled)
263
+ return;
264
+ searchRepaintScheduled = true;
265
+ setImmediate(() => {
266
+ searchRepaintScheduled = false;
267
+ recompute();
268
+ flush();
269
+ });
270
+ };
231
271
  const onKeySearch = (input, key) => {
232
272
  if (key.escape) {
233
273
  // Cancel the search: drop the query AND the relevance ranking it switched
234
- // on, returning to the tree.
274
+ // on, returning to the tree. Synchronous: we're leaving search mode.
235
275
  state.search = false;
236
276
  state.query = '';
237
277
  state.sort = 'tree';
@@ -240,19 +280,20 @@ export async function runBrowse(opts = {}) {
240
280
  return;
241
281
  }
242
282
  if (key.return) {
243
- // Commit: keep the filter, drop search mode, land on the first match.
283
+ // Commit: keep the filter, drop search mode, land on the first match. Must
284
+ // recompute synchronously first so the match index reflects the final query
285
+ // even if a coalesced repaint is still pending.
244
286
  state.search = false;
287
+ recompute();
245
288
  const firstMatch = visible.findIndex((v) => v.matched);
246
289
  if (firstMatch >= 0)
247
290
  state.cursor = firstMatch;
248
- recompute();
249
291
  flush();
250
292
  return;
251
293
  }
252
294
  if (key.backspace) {
253
295
  state.query = state.query.slice(0, -1);
254
- recompute();
255
- flush();
296
+ scheduleSearchRepaint();
256
297
  return;
257
298
  }
258
299
  // Any ctrl-combo: Ctrl+C quits; everything else is swallowed (never typed).
@@ -261,11 +302,15 @@ export async function runBrowse(opts = {}) {
261
302
  quit();
262
303
  return;
263
304
  }
264
- // Printable single char append. Ignore multi-byte / control chunks.
265
- if (input.length === 1 && input >= ' ') {
266
- state.query += input;
267
- recompute();
268
- flush();
305
+ // Append every printable char in the chunk. Fast typing batches multiple
306
+ // chars into one stdin read, so we must take them all (not just a length-1
307
+ // chunk) or keystrokes get dropped. Filtering to >= ' ' strips control/escape
308
+ // bytes (e.g. stray \x1b from an unrecognised sequence) but keeps real input,
309
+ // including multi-byte UTF-8 code points.
310
+ const printable = [...input].filter((c) => c >= ' ').join('');
311
+ if (printable.length > 0) {
312
+ state.query += printable;
313
+ scheduleSearchRepaint();
269
314
  }
270
315
  };
271
316
  const onKeyNav = (input, key) => {
@@ -424,6 +469,20 @@ export async function runBrowse(opts = {}) {
424
469
  flush();
425
470
  return;
426
471
  }
472
+ // Revive now (on-demand kick): for a HANGING node, SIGTERM the live broker so
473
+ // the daemon's crash→grace→resume path brings it back in ~20s instead of
474
+ // waiting out the 5-min auto-revive grace. Shells `canvas revive <id> --now`
475
+ // (browse goes through commands, never touching the runtime directly). 'k' is
476
+ // vim-up here, so the kick is on SHIFT-K.
477
+ if (input === 'K') {
478
+ if (row !== undefined) {
479
+ const r = rowOf(row.id);
480
+ if (r?.hanging != null)
481
+ reviveNow(row.id);
482
+ }
483
+ flush();
484
+ return;
485
+ }
427
486
  // Resume.
428
487
  if (key.return) {
429
488
  if (row !== undefined)
@@ -445,6 +504,17 @@ export async function runBrowse(opts = {}) {
445
504
  state.cwdScope = null;
446
505
  recompute();
447
506
  }
507
+ // Open with the cursor on the CURRENT node (the one browse was launched from),
508
+ // not the top of the tree. Expand its ancestors so the row is actually visible,
509
+ // then home the cursor onto it. No-op when launched outside a node, or when the
510
+ // node is filtered out of the current tab/scope.
511
+ const selfId = process.env['CRTR_NODE_ID'];
512
+ if (selfId !== undefined && selfId !== '' && tree.nodes.has(selfId)) {
513
+ for (let p = tree.nodes.get(selfId)?.parentId ?? null; p !== null; p = tree.nodes.get(p)?.parentId ?? null) {
514
+ state.collapsed.delete(p);
515
+ }
516
+ recompute(selfId);
517
+ }
448
518
  setupTerminal();
449
519
  flush();
450
520
  // Background corpus warmer: after the instant first paint, progressively enrich
@@ -28,7 +28,7 @@ export interface Tree {
28
28
  nodes: Map<string, TreeNode>;
29
29
  }
30
30
  /** Sort rank for roots/stragglers — live first (active, then idle), dormant
31
- * after. Mirrors render.ts / canvas-resume.ts statusRank. */
31
+ * after. Mirrors render.ts statusRank. */
32
32
  export declare function statusRank(status: NodeStatus): number;
33
33
  /**
34
34
  * Build a spanning tree of the whole canvas.
@@ -43,6 +43,15 @@ export declare function statusRank(status: NodeStatus): number;
43
43
  * appended as a depth-0 straggler so "All" is genuinely the whole canvas.
44
44
  */
45
45
  export declare function buildTree(rows: DashboardRow[], rootIds: string[], childIdsOf: (id: string) => string[]): Tree;
46
+ /** Splice a node out of an already-built tree in place (mutates `tree`). Used when
47
+ * a close REAPED an empty node (row + dir hard-deleted) rather than parking it:
48
+ * the node is gone from the db, so it must vanish from the in-memory tree too or
49
+ * its stale row lingers on screen (the "press x, nothing happens" bug — an empty
50
+ * active husk reaped by closeNode but never dropped from `tree.nodes`, so the
51
+ * attention/flat view kept painting it). Surviving children are re-homed to the
52
+ * reaped node's parent so a non-leaf reap never orphans them out of view. No-op
53
+ * if the id is absent. */
54
+ export declare function pruneNode(tree: Tree, id: string): void;
46
55
  /** Case-insensitive subsequence match: every char of `query` appears in `text`
47
56
  * in order (gaps allowed). Empty query matches everything. Substrings are a
48
57
  * subsequence, so this subsumes substring matching too. */
@@ -20,7 +20,7 @@ export function tabPredicate(tab, row) {
20
20
  }
21
21
  }
22
22
  /** Sort rank for roots/stragglers — live first (active, then idle), dormant
23
- * after. Mirrors render.ts / canvas-resume.ts statusRank. */
23
+ * after. Mirrors render.ts statusRank. */
24
24
  export function statusRank(status) {
25
25
  switch (status) {
26
26
  case 'active': return 0;
@@ -85,6 +85,42 @@ export function buildTree(rows, rootIds, childIdsOf) {
85
85
  }
86
86
  return { roots: orderedRoots, nodes };
87
87
  }
88
+ /** Splice a node out of an already-built tree in place (mutates `tree`). Used when
89
+ * a close REAPED an empty node (row + dir hard-deleted) rather than parking it:
90
+ * the node is gone from the db, so it must vanish from the in-memory tree too or
91
+ * its stale row lingers on screen (the "press x, nothing happens" bug — an empty
92
+ * active husk reaped by closeNode but never dropped from `tree.nodes`, so the
93
+ * attention/flat view kept painting it). Surviving children are re-homed to the
94
+ * reaped node's parent so a non-leaf reap never orphans them out of view. No-op
95
+ * if the id is absent. */
96
+ export function pruneNode(tree, id) {
97
+ const node = tree.nodes.get(id);
98
+ if (node === undefined)
99
+ return;
100
+ const parentId = node.parentId;
101
+ // Children still present (their own reap, if any, already removed them) lift up
102
+ // to this node's parent, taking its slot in the child order.
103
+ const survivingKids = node.childIds.filter((c) => tree.nodes.has(c));
104
+ for (const c of survivingKids) {
105
+ const kid = tree.nodes.get(c);
106
+ if (kid !== undefined)
107
+ kid.parentId = parentId;
108
+ }
109
+ if (parentId !== null) {
110
+ const p = tree.nodes.get(parentId);
111
+ if (p !== undefined) {
112
+ const i = p.childIds.indexOf(id);
113
+ if (i >= 0)
114
+ p.childIds.splice(i, 1, ...survivingKids);
115
+ }
116
+ }
117
+ else {
118
+ const i = tree.roots.indexOf(id);
119
+ if (i >= 0)
120
+ tree.roots.splice(i, 1, ...survivingKids);
121
+ }
122
+ tree.nodes.delete(id);
123
+ }
88
124
  // ---------------------------------------------------------------------------
89
125
  // Fuzzy match
90
126
  // ---------------------------------------------------------------------------
@@ -11,6 +11,7 @@
11
11
  // Every hue (fg / bg color) is gated on `caps.color`; structural SGR (bold/dim/
12
12
  // reverse) is allowed always. See detectColorCaps() for the gate, and the
13
13
  // canvas-browse color spec for the rationale + palette.
14
+ import { STATUS_GLYPH, STATUS_COLOR, resolveNodeVisual, faultSummary } from '../status-glyph.js';
14
15
  import { TABS, matchIndices, promptText, previewSnippet } from './model.js';
15
16
  // Span/color primitives live in core/tui/draw.ts (one copy, shared with the
16
17
  // `crtr view` host). Re-export the color caps so browse's importers + tests keep
@@ -50,30 +51,17 @@ const DIM = `${ESC}2m`;
50
51
  const BOLD = `${ESC}1m`;
51
52
  const CURSOR_BG = `${ESC}48;5;236m`; // subtle dark-gray cursor-row bg (256-color)
52
53
  // Basic-16 ANSI fg codes used by the palette.
53
- const FG_GREEN = '32';
54
54
  const FG_YELLOW = '33';
55
55
  const FG_RED = '31';
56
56
  const FG_CYAN = '36';
57
57
  const FG_GRAY = '90'; // bright-black
58
58
  const FG_BRIGHT_YELLOW = '93';
59
- const FG_BRIGHT_GREEN = '92'; // streaming pulse (brighter than the active-status green)
60
59
  const FG_BRIGHT_CYAN = '96'; // query-match highlight (ties to the cyan search accent)
61
- const STATUS_GLYPH = {
62
- active: '●',
63
- idle: '○',
64
- done: '✓',
65
- dead: '✗',
66
- canceled: '⊘',
67
- };
68
- /** The load-bearing color: glyph hue per status. Single source of truth, mirrors
69
- * STATUS_GLYPH. Reinforces the glyph everywhere it appears (rows + summary). */
70
- const STATUS_COLOR = {
71
- active: FG_GREEN,
72
- idle: FG_YELLOW,
73
- done: FG_CYAN,
74
- dead: FG_RED,
75
- canceled: FG_GRAY,
76
- };
60
+ // STATUS_GLYPH + STATUS_COLOR are imported from ../status-glyph.js (the shared
61
+ // canvas-graph map). STATUS_COLOR there is NUMERIC SGR (e.g. 32) — wrap with
62
+ // `sgr()` where a string ANSI code is needed (Span fg / template literals accept
63
+ // the number directly).
64
+ const FG = (code) => String(code);
77
65
  function fmtCtx(tokens) {
78
66
  if (tokens <= 0)
79
67
  return '0k';
@@ -194,16 +182,17 @@ function spansWidth(spans) {
194
182
  * node is genuinely mid-turn; otherwise the lifecycle word. The word is what makes
195
183
  * state legible at a glance and survives NO_COLOR (the glyph is the second cue). */
196
184
  function statusRail(r) {
197
- const streaming = r.streaming === true;
198
- const word = streaming
199
- ? 'live'
200
- : ({ active: 'active', idle: 'idle', done: 'done', dead: 'dead', canceled: 'cancel' }[r.status] ?? r.status);
201
- const fg = streaming ? FG_BRIGHT_GREEN : STATUS_COLOR[r.status];
202
- const glyph = streaming ? '' : (STATUS_GLYPH[r.status] ?? '?');
203
- const text = `${glyph} ${word}`;
185
+ const v = resolveNodeVisual(r.status, { streaming: r.streaming === true, hanging: r.hanging ?? null });
186
+ // Plain status keeps browse's own status WORD; overlays (hanging/⟳)
187
+ // carry their own word from resolveNodeVisual. A hanging kind label can exceed
188
+ // STATUS_W acceptable: the rare, attention-demanding row gets a slightly
189
+ // wider rail (Math.max(1,…) keeps a gap), which draws the eye.
190
+ const word = v.word ?? ({ active: 'active', idle: 'idle', done: 'done', dead: 'dead', canceled: 'cancel' }[r.status] ?? r.status);
191
+ const fg = FG(v.color);
192
+ const text = `${v.glyph} ${word}`;
204
193
  const pad = Math.max(1, STATUS_W - [...text].length);
205
194
  return [
206
- { text, style: { fg, bold: streaming } },
195
+ { text, style: { fg, bold: v.bold === true } },
207
196
  { text: ' '.repeat(pad) },
208
197
  ];
209
198
  }
@@ -256,11 +245,15 @@ function rowLine(row, tree, width, isCursor, query, caps, showCwd, now) {
256
245
  const gutter = r.viewed === true
257
246
  ? { text: '◉ ', style: { fg: FG_BRIGHT_CYAN, bold: true } }
258
247
  : { text: ' ' };
259
- // Inline attention flag that travels with the name (streaming lives in the rail,
248
+ // Inline attention flag that travels with the name (fault summary lives in the rail,
260
249
  // attached lives in the gutter).
261
250
  const flags = [];
262
251
  if (r.asks > 0)
263
252
  flags.push({ text: ` ⚑${r.asks}`, style: { fg: FG_BRIGHT_YELLOW, bold: true } }); // pending asks
253
+ // Hanging: the fault summary travels with the name (the ⚠ + kind
254
+ // label live in the rail). Dim yellow so it reads as secondary detail.
255
+ if (r.hanging != null)
256
+ flags.push({ text: ` · ${faultSummary(r.hanging, now)}`, style: { fg: FG_YELLOW, dim: true } });
264
257
  const status = statusRail(r);
265
258
  const right = metaCluster(r, now, showCwd);
266
259
  const treeLead = `${indent}${collapse} `;
@@ -348,7 +341,7 @@ function snippetLine(ln, width, caps) {
348
341
  return assemble(spans, width, caps.color, '', false);
349
342
  }
350
343
  /** The bottom preview panel — exactly PREVIEW_HEIGHT lines: a separator, a meta
351
- * line (status · kind/mode · project · age · ctx · asks · streaming/viewing), then
344
+ * line (status · kind/mode · project · age · ctx · asks · fault/viewing), then
352
345
  * TWO blocks — the spawn prompt / query-match (PROMPT_LINES) and the node's LAST
353
346
  * assistant reply (REPLY_LINES, prefixed `↩`). Under a live query the prompt block
354
347
  * is WINDOWED to the matching prompt (anywhere in the conversation) with the match
@@ -362,14 +355,17 @@ function previewPanel(r, width, caps, now, query) {
362
355
  out.push('');
363
356
  return out;
364
357
  }
358
+ const v = resolveNodeVisual(r.status, { streaming: r.streaming === true, hanging: r.hanging ?? null });
365
359
  const glyph = caps.color
366
- ? `${ESC}${STATUS_COLOR[r.status]}m${STATUS_GLYPH[r.status]}${RESET}`
367
- : (STATUS_GLYPH[r.status] ?? '?');
360
+ ? `${ESC}${v.color}m${v.glyph}${RESET}`
361
+ : v.glyph;
368
362
  const metaPieces = [`${r.status} ${r.kind}/${r.mode}`, baseDir(r.cwd), relAge(r.created, now), `ctx ${fmtCtx(r.ctx_tokens)}`];
369
363
  if (r.asks > 0)
370
364
  metaPieces.push(`⚑${r.asks}`);
365
+ if (r.hanging != null)
366
+ metaPieces.push(faultSummary(r.hanging, now));
371
367
  if (r.streaming === true)
372
- metaPieces.push('⟳ streaming');
368
+ metaPieces.push('⟳');
373
369
  if (r.viewed === true)
374
370
  metaPieces.push('◉ viewing');
375
371
  const metaText = clip(metaPieces.filter((p) => p !== '').join(' · '), Math.max(0, width - 2));
@@ -471,13 +467,19 @@ export function renderFrame(state, size, caps = { color: false, color256: false
471
467
  if (state.pendingClose !== null && state.pendingClose !== undefined) {
472
468
  const node = state.tree.nodes.get(state.pendingClose);
473
469
  const who = node !== undefined ? node.row.name : state.pendingClose;
474
- const warn = `⚠ ${who} is actively streaming — close it (and its subtree) anyway? y / n`;
470
+ const warn = `⚠ ${who} is — close it (and its subtree) anyway? y / n`;
475
471
  lines.push(caps.color ? `${BOLD}${ESC}${FG_BRIGHT_YELLOW}m${clip(warn, width)}${RESET}` : `${REVERSE}${clip(warn, width)}${RESET}`);
476
472
  }
477
473
  else {
474
+ // When the cursor row has an active fault, lead the footer with the on-demand kick
475
+ // hint (K shells `canvas revive <id> --now` — immediate revive instead of
476
+ // waiting for the daemon's retry schedule).
477
+ const selRow = state.visible[state.cursor];
478
+ const cursorRow = selRow !== undefined ? state.tree.nodes.get(selRow.id)?.row : undefined;
479
+ const hangingHint = cursorRow?.hanging != null ? 'K revive now ' : '';
478
480
  const footer = state.search
479
481
  ? '⏎ commit Esc cancel ⌫ delete'
480
- : '↑↓ move →/← tree ⏎ resume x close Tab tabs / search s sort c cwd r residents p preview q quit';
482
+ : `${hangingHint}↑↓ move →/← tree ⏎ resume x close Tab tabs / search s sort c cwd r residents p preview q quit`;
481
483
  lines.push(`${DIM}${clip(footer, width)}${RESET}`);
482
484
  }
483
485
  // Assemble: home, each line cleared to EOL, then clear below.