@north-light/crouter 0.3.197 → 0.3.199

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 (339) hide show
  1. package/dist/api/client.d.ts +19 -19
  2. package/dist/api/client.js +29 -37
  3. package/dist/api/dto/broker-ops.d.ts +3 -1
  4. package/dist/api/dto/broker.d.ts +13 -2
  5. package/dist/api/dto/broker.js +4 -4
  6. package/dist/api/dto/human.d.ts +6 -5
  7. package/dist/api/dto/inbox.d.ts +126 -33
  8. package/dist/api/dto/nodes.d.ts +8 -2
  9. package/dist/api/dto/profiles.d.ts +23 -0
  10. package/dist/api/dto/profiles.js +2 -2
  11. package/dist/api/dto/worktree.d.ts +3 -5
  12. package/dist/api/routes.d.ts +3 -5
  13. package/dist/api/routes.js +4 -6
  14. package/dist/builtin-memory/00-runtime-base.md +8 -8
  15. package/dist/builtin-memory/01-spine/01-no-manager.md +1 -1
  16. package/dist/builtin-memory/02-lifecycle/00-terminal.md +3 -3
  17. package/dist/builtin-memory/04-orchestration-kernel.md +6 -6
  18. package/dist/builtin-memory/05-kinds/design/00-base.md +1 -1
  19. package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +2 -2
  20. package/dist/builtin-memory/05-kinds/review/00-base.md +2 -2
  21. package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +1 -1
  22. package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +1 -1
  23. package/dist/builtin-memory/insights/capture.md +2 -2
  24. package/dist/builtin-memory/insights/init.md +3 -3
  25. package/dist/builtin-memory/internal/INDEX.md +1 -1
  26. package/dist/builtin-memory/internal/memory-loading.md +1 -1
  27. package/dist/builtin-memory/internal/nodes-and-canvas.md +3 -3
  28. package/dist/builtin-memory/internal/plugins.md +59 -2
  29. package/dist/builtin-memory/internal/storage-tiers.md +3 -1
  30. package/dist/builtin-memory/spec/roadmap.md +2 -2
  31. package/dist/builtin-memory/testing.md +1 -1
  32. package/dist/builtin-memory/wedged-child-on-runaway-bash.md +1 -1
  33. package/dist/clients/attach/chrome/bash-jobs.js +3 -3
  34. package/dist/clients/attach/render/chat-view.d.ts +29 -9
  35. package/dist/clients/attach/render/chat-view.js +64 -35
  36. package/dist/clients/attach/render/markdown-source.js +0 -6
  37. package/dist/clients/attach/render/page-block.d.ts +34 -0
  38. package/dist/clients/attach/render/page-block.js +226 -0
  39. package/dist/clients/attach/render/tool-calls.js +16 -2
  40. package/dist/clients/attach/session/whip.d.ts +2 -0
  41. package/dist/clients/attach/session/whip.js +2 -0
  42. package/dist/clients/attach/viewer.js +1419 -982
  43. package/dist/clients/conversation/projection.js +18 -3
  44. package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +9 -6
  45. package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +13 -14
  46. package/dist/clients/inbox/controller.js +10 -16
  47. package/dist/clients/inbox/page-adapter.d.ts +3 -4
  48. package/dist/clients/inbox/page-adapter.js +3 -4
  49. package/dist/clients/inbox/resolve.d.ts +1 -1
  50. package/dist/clients/inbox/resolve.js +3 -3
  51. package/dist/clients/inbox/review/document-surface.js +3 -1
  52. package/dist/clients/inbox/review/state.d.ts +2 -1
  53. package/dist/clients/inbox/review/state.js +5 -16
  54. package/dist/clients/inbox/tui/input.js +37 -19
  55. package/dist/clients/inbox/tui/panel.js +5 -31
  56. package/dist/clients/inbox/tui/render.d.ts +11 -0
  57. package/dist/clients/inbox/tui/render.js +101 -36
  58. package/dist/clients/inbox/tui/slots.d.ts +4 -4
  59. package/dist/clients/inbox/tui/slots.js +19 -44
  60. package/dist/clients/inbox/tui/types.d.ts +6 -10
  61. package/dist/clients/inbox/tui.js +2 -2
  62. package/dist/commands/__tests__/human.test.js +118 -24
  63. package/dist/commands/__tests__/node-message.test.js +14 -0
  64. package/dist/commands/attention.js +4 -4
  65. package/dist/commands/canvas-browse.js +1 -1
  66. package/dist/commands/canvas-snapshot.js +1 -1
  67. package/dist/commands/canvas.js +1 -1
  68. package/dist/commands/dashboard.js +1 -1
  69. package/dist/commands/human/{doc.d.ts → components.d.ts} +1 -1
  70. package/dist/commands/human/components.js +77 -0
  71. package/dist/commands/human/feedback.d.ts +2 -0
  72. package/dist/commands/human/feedback.js +108 -0
  73. package/dist/commands/human/prompts.d.ts +17 -8
  74. package/dist/commands/human/prompts.js +217 -226
  75. package/dist/commands/human/queue.js +57 -88
  76. package/dist/commands/human/shared.d.ts +4 -15
  77. package/dist/commands/human/shared.js +19 -61
  78. package/dist/commands/human.js +32 -19
  79. package/dist/commands/memory/origin.js +2 -2
  80. package/dist/commands/node/bash.js +4 -4
  81. package/dist/commands/node/inspect.js +3 -3
  82. package/dist/commands/node/lifecycle.js +1 -1
  83. package/dist/commands/node/message.js +2 -2
  84. package/dist/commands/node-worktree.js +8 -8
  85. package/dist/commands/pkg/browse.js +1 -1
  86. package/dist/commands/pkg/plugin-manage.js +129 -10
  87. package/dist/commands/profile/delete.js +41 -15
  88. package/dist/commands/profile.js +1 -1
  89. package/dist/commands/push.js +1 -1
  90. package/dist/commands/surface/node/focus.js +29 -14
  91. package/dist/commands/surface-edit.js +3 -3
  92. package/dist/commands/surface-inbox.d.ts +2 -0
  93. package/dist/commands/{human/inbox.js → surface-inbox.js} +14 -18
  94. package/dist/commands/surface.js +2 -1
  95. package/dist/commands/sys/doctor.js +60 -3
  96. package/dist/commands/sys/feedback.js +79 -25
  97. package/dist/commands/sys/settings.js +1 -1
  98. package/dist/core/__tests__/cron-broker-capacity.test.js +1 -1
  99. package/dist/core/__tests__/daemon-boot.test.js +7 -4
  100. package/dist/core/__tests__/dead-node-policy-table.test.js +20 -36
  101. package/dist/core/__tests__/extension-abort.test.js +5 -2
  102. package/dist/core/__tests__/fixtures/fake-engine.d.ts +13 -2
  103. package/dist/core/__tests__/fixtures/fake-engine.js +47 -3
  104. package/dist/core/__tests__/human-cancel-guard.test.js +31 -10
  105. package/dist/core/__tests__/human-deliver.test.js +41 -45
  106. package/dist/core/__tests__/human-node-not-supervised.test.js +1 -1
  107. package/dist/core/__tests__/plugin-page-components.test.js +157 -0
  108. package/dist/core/__tests__/seam/broker-crash-teardown.test.js +13 -11
  109. package/dist/core/__tests__/seam/dormancy-release.test.js +103 -4
  110. package/dist/core/__tests__/serial/flagship-lifecycle.test.js +2 -2
  111. package/dist/core/__tests__/serial/human-deliver-e2e.test.js +12 -16
  112. package/dist/core/__tests__/serial/live-mutation.test.js +1 -1
  113. package/dist/core/__tests__/serial/tmux-surface.test.js +4 -2
  114. package/dist/core/__tests__/serial/worktree.test.js +24 -198
  115. package/dist/core/__tests__/session-cycles.test.js +22 -0
  116. package/dist/core/__tests__/stop-guard.test.js +4 -4
  117. package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +19 -4
  118. package/dist/core/bash-jobs.d.ts +6 -0
  119. package/dist/core/bash-jobs.js +10 -0
  120. package/dist/core/canvas/__tests__/attention.test.js +8 -5
  121. package/dist/core/canvas/__tests__/render-remote.test.js +5 -5
  122. package/dist/core/canvas/attention.js +8 -6
  123. package/dist/core/canvas/browse/app.d.ts +1 -1
  124. package/dist/core/canvas/browse/app.js +21 -3
  125. package/dist/core/canvas/browse/model.d.ts +8 -2
  126. package/dist/core/canvas/browse/model.js +11 -4
  127. package/dist/core/canvas/browse/render.d.ts +4 -0
  128. package/dist/core/canvas/browse/render.js +5 -1
  129. package/dist/core/canvas/canvas.d.ts +33 -0
  130. package/dist/core/canvas/canvas.js +92 -0
  131. package/dist/core/canvas/crons.d.ts +9 -0
  132. package/dist/core/canvas/crons.js +51 -0
  133. package/dist/core/canvas/node-order.d.ts +22 -1
  134. package/dist/core/canvas/node-order.js +26 -1
  135. package/dist/core/canvas/render-source.d.ts +5 -0
  136. package/dist/core/canvas/render-source.js +1 -0
  137. package/dist/core/canvas/types.d.ts +9 -3
  138. package/dist/core/command-plugins/bundle.d.ts +6 -1
  139. package/dist/core/command-plugins/bundle.js +26 -13
  140. package/dist/core/command.js +8 -12
  141. package/dist/core/config.d.ts +47 -1
  142. package/dist/core/config.js +138 -4
  143. package/dist/core/help.d.ts +3 -0
  144. package/dist/core/help.js +1 -1
  145. package/dist/core/human/__tests__/page-catalog.test.d.ts +1 -0
  146. package/dist/core/human/__tests__/page-catalog.test.js +20 -0
  147. package/dist/core/human/__tests__/page-tickets.test.js +68 -34
  148. package/dist/core/human/__tests__/page.test.js +60 -119
  149. package/dist/core/human/__tests__/serial/inbox-core.test.js +6 -16
  150. package/dist/core/human/answer-text.d.ts +3 -0
  151. package/dist/core/human/answer-text.js +98 -0
  152. package/dist/core/human/answer.d.ts +53 -0
  153. package/dist/core/human/answer.js +119 -0
  154. package/dist/core/human/component-docs.d.ts +18 -0
  155. package/dist/core/human/component-docs.js +434 -0
  156. package/dist/core/human/convention.d.ts +17 -3
  157. package/dist/core/human/convention.js +36 -5
  158. package/dist/core/human/feedback-companion.d.ts +20 -0
  159. package/dist/core/human/feedback-companion.js +98 -0
  160. package/dist/core/human/feedback.d.ts +65 -0
  161. package/dist/core/human/feedback.js +116 -0
  162. package/dist/core/human/page-catalog.d.ts +56 -5
  163. package/dist/core/human/page-catalog.js +151 -22
  164. package/dist/core/human/page-errors.d.ts +4 -0
  165. package/dist/core/human/page-errors.js +7 -0
  166. package/dist/core/human/page-eval.d.ts +20 -0
  167. package/dist/core/human/page-eval.js +123 -0
  168. package/dist/core/human/page-schema.d.ts +48 -116
  169. package/dist/core/human/page-schema.js +100 -72
  170. package/dist/core/human/page-synth.d.ts +2 -4
  171. package/dist/core/human/page-synth.js +20 -7
  172. package/dist/core/human/page.d.ts +13 -21
  173. package/dist/core/human/page.js +147 -391
  174. package/dist/core/human/review-schema.d.ts +1 -0
  175. package/dist/core/human/review-schema.js +1 -1
  176. package/dist/core/human/root.d.ts +6 -2
  177. package/dist/core/human/root.js +12 -4
  178. package/dist/core/human/scan.d.ts +3 -3
  179. package/dist/core/human/scan.js +21 -21
  180. package/dist/core/human/summary.d.ts +2 -6
  181. package/dist/core/human/summary.js +3 -59
  182. package/dist/core/human/tickets.d.ts +22 -15
  183. package/dist/core/human/tickets.js +90 -28
  184. package/dist/core/human/types.d.ts +14 -2
  185. package/dist/core/keybindings/catalog.d.ts +2 -2
  186. package/dist/core/keybindings/catalog.js +1 -0
  187. package/dist/core/preview-registry.js +8 -10
  188. package/dist/core/profiles/default-binding.d.ts +4 -0
  189. package/dist/core/profiles/default-binding.js +24 -0
  190. package/dist/core/profiles/deletion-reservation.d.ts +8 -0
  191. package/dist/core/profiles/deletion-reservation.js +38 -0
  192. package/dist/core/profiles/manifest.d.ts +3 -0
  193. package/dist/core/profiles/manifest.js +12 -0
  194. package/dist/core/profiles/select.js +2 -2
  195. package/dist/core/review/realize.js +12 -4
  196. package/dist/core/review/store.d.ts +6 -0
  197. package/dist/core/review/store.js +31 -0
  198. package/dist/core/runtime/bearings.d.ts +0 -4
  199. package/dist/core/runtime/bearings.js +4 -62
  200. package/dist/core/runtime/bin-contributions.d.ts +44 -0
  201. package/dist/core/runtime/bin-contributions.js +186 -0
  202. package/dist/core/runtime/boot-root.js +6 -1
  203. package/dist/core/runtime/broker/client-registry.d.ts +4 -0
  204. package/dist/core/runtime/broker/client-registry.js +13 -1
  205. package/dist/core/runtime/broker/extension-abort.d.ts +1 -1
  206. package/dist/core/runtime/broker/extension-abort.js +4 -2
  207. package/dist/core/runtime/broker/fault-retry.js +4 -0
  208. package/dist/core/runtime/broker/frame-dispatch.d.ts +3 -1
  209. package/dist/core/runtime/broker/frame-dispatch.js +5 -6
  210. package/dist/core/runtime/broker/read-ops.d.ts +1 -1
  211. package/dist/core/runtime/broker/read-ops.js +2 -2
  212. package/dist/core/runtime/broker/rebind.d.ts +1 -0
  213. package/dist/core/runtime/broker/rebind.js +6 -1
  214. package/dist/core/runtime/broker-extension-render.js +19 -0
  215. package/dist/core/runtime/broker-protocol.d.ts +14 -0
  216. package/dist/core/runtime/broker.d.ts +1 -1
  217. package/dist/core/runtime/broker.js +25 -12
  218. package/dist/core/runtime/busy.d.ts +4 -3
  219. package/dist/core/runtime/busy.js +4 -3
  220. package/dist/core/runtime/front-door-env.d.ts +5 -0
  221. package/dist/core/runtime/front-door-env.js +16 -0
  222. package/dist/core/runtime/front-door.d.ts +1 -5
  223. package/dist/core/runtime/front-door.js +2 -5
  224. package/dist/core/runtime/host.d.ts +1 -1
  225. package/dist/core/runtime/invocation.d.ts +6 -0
  226. package/dist/core/runtime/invocation.js +10 -0
  227. package/dist/core/runtime/kickoff.js +34 -0
  228. package/dist/core/runtime/launch.d.ts +2 -6
  229. package/dist/core/runtime/node-read.js +4 -1
  230. package/dist/core/runtime/nodes.js +2 -0
  231. package/dist/core/runtime/promote.d.ts +1 -0
  232. package/dist/core/runtime/promote.js +3 -1
  233. package/dist/core/runtime/session-cycles.d.ts +6 -0
  234. package/dist/core/runtime/session-cycles.js +15 -1
  235. package/dist/core/runtime/session-visibility.d.ts +17 -8
  236. package/dist/core/runtime/session-visibility.js +19 -10
  237. package/dist/core/runtime/spawn-env.d.ts +9 -1
  238. package/dist/core/runtime/spawn-env.js +62 -2
  239. package/dist/core/runtime/spawn.js +4 -4
  240. package/dist/core/runtime/stop-guard.d.ts +2 -2
  241. package/dist/core/runtime/stop-guard.js +2 -2
  242. package/dist/core/runtime/stop-signals.d.ts +1 -1
  243. package/dist/core/runtime/stop-signals.js +2 -2
  244. package/dist/core/runtime/tmux-bindings.js +1 -1
  245. package/dist/core/session-model/session-state.d.ts +4 -3
  246. package/dist/core/session-model/session-state.js +14 -1
  247. package/dist/core/user-settings.d.ts +36 -2
  248. package/dist/core/user-settings.js +56 -14
  249. package/dist/core/worktree.d.ts +15 -45
  250. package/dist/core/worktree.js +59 -261
  251. package/dist/daemon/api/__tests__/seam/profile-delete.test.d.ts +1 -0
  252. package/dist/daemon/api/__tests__/seam/profile-delete.test.js +386 -0
  253. package/dist/daemon/api/handlers/bash-jobs.js +1 -1
  254. package/dist/daemon/api/handlers/broker-ops.js +9 -5
  255. package/dist/daemon/api/handlers/feedback-comments.d.ts +2 -0
  256. package/dist/daemon/api/handlers/feedback-comments.js +207 -0
  257. package/dist/daemon/api/handlers/human.js +24 -20
  258. package/dist/daemon/api/handlers/inbox.d.ts +10 -0
  259. package/dist/daemon/api/handlers/inbox.js +48 -109
  260. package/dist/daemon/api/handlers/profiles.js +11 -29
  261. package/dist/daemon/api/map.d.ts +6 -1
  262. package/dist/daemon/api/map.js +23 -0
  263. package/dist/daemon/api/server.js +3 -1
  264. package/dist/daemon/companion-retire.d.ts +2 -0
  265. package/dist/daemon/companion-retire.js +33 -0
  266. package/dist/daemon/cron-run.js +29 -17
  267. package/dist/daemon/crtrd.js +1 -1
  268. package/dist/daemon/fleet.d.ts +22 -10
  269. package/dist/daemon/fleet.js +43 -24
  270. package/dist/daemon/human/finish.d.ts +12 -6
  271. package/dist/daemon/human/finish.js +134 -54
  272. package/dist/daemon/human/sweep.js +9 -3
  273. package/dist/daemon/profile-delete.d.ts +7 -0
  274. package/dist/daemon/profile-delete.js +306 -0
  275. package/dist/daemon/reconcilers/broker-supervision.js +14 -9
  276. package/dist/daemon/reconcilers/dormant-inbox.js +11 -6
  277. package/dist/daemon/reconcilers/live-obligation.d.ts +21 -1
  278. package/dist/daemon/reconcilers/live-obligation.js +30 -2
  279. package/dist/daemon/reconcilers/storage-maintenance.d.ts +5 -0
  280. package/dist/daemon/reconcilers/storage-maintenance.js +28 -1
  281. package/dist/daemon/review/deliver.js +3 -3
  282. package/dist/daemon/review/finish.d.ts +0 -2
  283. package/dist/daemon/review/finish.js +1 -29
  284. package/dist/daemon/review/sweep.js +12 -1
  285. package/dist/pi-extensions/__tests__/canvas-bash-valve.test.js +56 -1
  286. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +4 -4
  287. package/dist/pi-extensions/canvas-bash-valve.d.ts +9 -1
  288. package/dist/pi-extensions/canvas-bash-valve.js +31 -13
  289. package/dist/pi-extensions/canvas-inbox-watcher.js +1 -1
  290. package/dist/pi-extensions/canvas-review-boundary.d.ts +4 -0
  291. package/dist/pi-extensions/canvas-review-boundary.js +18 -9
  292. package/dist/pi-extensions/canvas-stophook.js +4 -4
  293. package/dist/shared/generated-context.js +2 -2
  294. package/dist/types.d.ts +61 -5
  295. package/dist/types.js +8 -2
  296. package/package.json +4 -4
  297. package/runtime.lock.json +5 -33
  298. package/scripts/install-runtime.mjs +19 -24
  299. package/dist/builtin-memory/init.md +0 -38
  300. package/dist/builtin-memory/plan.md +0 -19
  301. package/dist/builtin-memory/spec.md +0 -19
  302. package/dist/clients/attach/render/html-markdown.d.ts +0 -13
  303. package/dist/clients/attach/render/html-markdown.js +0 -206
  304. package/dist/commands/human/doc.js +0 -144
  305. package/dist/commands/human/inbox.d.ts +0 -2
  306. package/dist/core/human/__tests__/page-render.test.js +0 -49
  307. package/dist/core/human/markdown-html.d.ts +0 -8
  308. package/dist/core/human/markdown-html.js +0 -581
  309. package/dist/core/human/page-render.d.ts +0 -3
  310. package/dist/core/human/page-render.js +0 -131
  311. package/dist/pages/bundle.css +0 -1
  312. package/dist/pages/bundle.js +0 -1347
  313. package/dist/pages/comments.d.ts +0 -43
  314. package/dist/pages/comments.js +0 -125
  315. package/dist/pages/elements/cards.d.ts +0 -17
  316. package/dist/pages/elements/cards.js +0 -802
  317. package/dist/pages/elements/chart.js +0 -648
  318. package/dist/pages/elements/options.d.ts +0 -21
  319. package/dist/pages/elements/options.js +0 -629
  320. package/dist/pages/elements/pages.d.ts +0 -24
  321. package/dist/pages/elements/pages.js +0 -355
  322. package/dist/pages/elements/slot.d.ts +0 -21
  323. package/dist/pages/elements/slot.js +0 -105
  324. package/dist/pages/elements/table.d.ts +0 -19
  325. package/dist/pages/elements/table.js +0 -935
  326. package/dist/pages/elements/text.d.ts +0 -25
  327. package/dist/pages/elements/text.js +0 -968
  328. package/dist/pages/entry.d.ts +0 -8
  329. package/dist/pages/entry.js +0 -8
  330. package/dist/pages/host.d.ts +0 -158
  331. package/dist/pages/host.js +0 -182
  332. package/dist/pages/register.d.ts +0 -2
  333. package/dist/pages/register.js +0 -3
  334. package/dist/pages/slot-config.d.ts +0 -36
  335. package/dist/pages/slot-config.js +0 -50
  336. package/dist/pages/types.d.ts +0 -98
  337. package/dist/pages/types.js +0 -23
  338. /package/dist/{core/human/__tests__/page-render.test.d.ts → commands/__tests__/node-message.test.d.ts} +0 -0
  339. /package/dist/{pages/elements/chart.d.ts → core/__tests__/plugin-page-components.test.d.ts} +0 -0
@@ -1,24 +1,42 @@
1
1
  import stringWidth from 'string-width';
2
2
  import { isResponseBearingSlot } from '../../../core/human/page-schema.js';
3
+ import { answerCommentAnchorLabel, describePageAnswer } from '../../../core/human/answer.js';
3
4
  import { renderMarkdownBlockAwareLines } from '../../../core/termrender/termrender.js';
4
- import { anchorLabel, describeSlot, pageComplete, slotActions, slotAnswered, summarizeSlotResponse, tableMarkdown, terminalAnswerable } from './slots.js';
5
- import { RESET, BOLD, DIM, GREEN, YELLOW, CYAN, REVERSE, sanitize, singleLine, truncate, hline, hardWrap, centerHorizontal, clipLine, panLine, visibleWidth } from './ansi.js';
5
+ import { commentAnchors, describeSlot, pageComplete, slotActions, slotAnswered, tableMarkdown, terminalAnswerable } from './slots.js';
6
+ import { RESET, BOLD, DIM, GREEN, YELLOW, CYAN, REVERSE, sanitize, singleLine, truncate, hline, wrap, centerHorizontal, clipLine, panLine, visibleWidth } from './ansi.js';
6
7
  const PAN_STEP = 8;
7
8
  const mdLines = (md, maxW, paneW) => renderMarkdownBlockAwareLines(md, maxW, paneW);
8
- function proseLines(blocks, maxW, paneW) {
9
- return blocks.flatMap((block, index) => [...(index > 0 ? [''] : []), ...mdLines(block, maxW, paneW)]);
9
+ function pageAnswer(state) {
10
+ return describePageAnswer(state.manifest, Object.fromEntries(state.responses), { completedAt: '' });
11
+ }
12
+ function answerSlot(answer, slot) {
13
+ return slot.id === undefined ? undefined : answer.slots.find((candidate) => candidate.id === slot.id);
14
+ }
15
+ function answerSummary(slot) {
16
+ if (slot === undefined || slot.state === 'unanswered')
17
+ return 'unanswered';
18
+ if (slot.raw !== undefined)
19
+ return 'custom response';
20
+ const groups = slot.text !== undefined ? [slot.text.edited ? 'edited' : 'accepted'] : slot.groups.map((group) => group.picked.length === 0 ? 'none' : group.picked.map((item) => item.label).join(', '));
21
+ if (slot.freetext !== undefined)
22
+ groups.push(slot.freetext);
23
+ if (slot.comments.length > 0)
24
+ groups.push(`${slot.comments.length} comment${slot.comments.length === 1 ? '' : 's'}`);
25
+ return groups.join('; ') || 'answered';
26
+ }
27
+ function commentLabel(slot, anchor) {
28
+ return answerCommentAnchorLabel(slot, anchor);
10
29
  }
11
30
  export function renderOverview(state, cols, rows) {
12
31
  const maxW = Math.min(Math.max(20, cols - 4), 120);
13
32
  const lines = ['', ` ${BOLD}${CYAN}${sanitize(state.manifest.title)}${RESET}`];
14
33
  if (state.manifest.subtitle)
15
- lines.push(` ${DIM}${sanitize(state.manifest.subtitle)}${RESET}`);
16
- lines.push(` ${DIM}${hline(maxW)}${RESET}`, '');
17
- if (state.manifest.dialect === 'html')
18
- lines.push(` ${DIM}rich page — open in Northlight${RESET}`, '');
19
- else
20
- for (const line of proseLines(state.overviewProse, maxW, Math.max(20, cols - 4)))
21
- lines.push(` ${line}`);
34
+ for (const line of wrap(sanitize(state.manifest.subtitle), maxW))
35
+ lines.push(` ${DIM}${line}${RESET}`);
36
+ const hasBody = state.slots.length > 0;
37
+ if (hasBody)
38
+ lines.push(` ${DIM}${hline(maxW)}${RESET}`, '');
39
+ const answer = pageAnswer(state);
22
40
  let priorStep = -1;
23
41
  for (let i = 0; i < state.slots.length; i++) {
24
42
  const slot = state.slots[i];
@@ -33,20 +51,66 @@ export function renderOverview(state, cols, rows) {
33
51
  const cursor = i === state.currentIndex ? `${CYAN}▸${RESET} ` : ' ';
34
52
  lines.push(` ${cursor}${icon} ${truncate(singleLine(describeSlot(slot)), Math.max(10, cols - 16))}`);
35
53
  if (response !== undefined)
36
- lines.push(` ${DIM}${truncate(singleLine(summarizeSlotResponse(slot, response)), Math.max(10, cols - 10))}${RESET}`);
54
+ lines.push(` ${DIM}${truncate(singleLine(answerSummary(answerSlot(answer, slot))), Math.max(10, cols - 10))}${RESET}`);
37
55
  }
38
- if (state.slots.length === 0)
39
- lines.push(` ${DIM}No interactive slots${RESET}`);
40
- lines.push('', ` ${DIM}${hline(maxW)}${RESET}`);
56
+ if (hasBody)
57
+ lines.push('');
58
+ lines.push(` ${DIM}${hline(maxW)}${RESET}`);
41
59
  const display = !state.manifest.slots.some(isResponseBearingSlot);
42
60
  lines.push(display ? ` ${DIM}q${RESET} dismiss` : ` ${DIM}enter${RESET} review ${DIM}j/k${RESET} navigate ${DIM}q${RESET} close`);
43
61
  return centeredWindow(lines, state, cols, rows, maxW + 2);
44
62
  }
63
+ /** Wrap an input buffer and paint the cursor the way a terminal does: the
64
+ * character under it is drawn in reverse video, occupying its own cell rather
65
+ * than pushing the rest of the line across by one. Only past the end of a row
66
+ * does the cursor need a cell of its own, and there it is a reversed space.
67
+ * Wrapping runs over the plain buffer, so the rows here are exactly the rows
68
+ * `verticalCursor` moves between. */
45
69
  export function renderInputBuffer(buffer, cursor, maxWidth) {
46
70
  const chars = [...buffer];
47
71
  const at = Math.max(0, Math.min(cursor, chars.length));
48
- const withCaret = [...chars.slice(0, at), '\uE000', ...chars.slice(at)].join('');
49
- return hardWrap(withCaret, maxWidth).map((line) => line.includes('\uE000') ? line.replace('\uE000', `${REVERSE} ${RESET}`) : line);
72
+ const rows = wrappedRowBounds(buffer, maxWidth);
73
+ let cursorRow = 0;
74
+ for (let i = 0; i < rows.length; i++)
75
+ if (rows[i].start <= at)
76
+ cursorRow = i;
77
+ const lines = rows.map((row, index) => {
78
+ if (index !== cursorRow)
79
+ return chars.slice(row.start, row.end).join('');
80
+ const head = chars.slice(row.start, at).join('');
81
+ const under = at < row.end ? chars[at] : ' ';
82
+ const tail = at < row.end ? chars.slice(at + 1, row.end).join('') : '';
83
+ return `${head}${REVERSE}${under}${RESET}${tail}`;
84
+ });
85
+ // A cursor past the end of an already-full row has no cell left on it, so it
86
+ // takes the first cell of the next row instead of overflowing the wrap width.
87
+ const last = rows[cursorRow];
88
+ if (at === last.end && stringWidth(chars.slice(last.start, last.end).join('')) >= maxWidth) {
89
+ lines[cursorRow] = chars.slice(last.start, last.end).join('');
90
+ lines.splice(cursorRow + 1, 0, `${REVERSE} ${RESET}`);
91
+ }
92
+ return lines;
93
+ }
94
+ /** Start of the word before the cursor (alt+left / readline alt-b). The one
95
+ * implementation of word motion for every inbox input buffer. */
96
+ export function wordLeftIndex(buffer, cursor) {
97
+ const chars = [...buffer];
98
+ let i = Math.max(0, Math.min(cursor, chars.length));
99
+ while (i > 0 && /\s/.test(chars[i - 1]))
100
+ i--;
101
+ while (i > 0 && !/\s/.test(chars[i - 1]))
102
+ i--;
103
+ return i;
104
+ }
105
+ /** Just past the end of the word after the cursor (alt+right / readline alt-f). */
106
+ export function wordRightIndex(buffer, cursor) {
107
+ const chars = [...buffer];
108
+ let i = Math.max(0, Math.min(cursor, chars.length));
109
+ while (i < chars.length && /\s/.test(chars[i]))
110
+ i++;
111
+ while (i < chars.length && !/\s/.test(chars[i]))
112
+ i++;
113
+ return i;
50
114
  }
51
115
  export function wrappedRowBounds(buffer, width) {
52
116
  const chars = [...buffer];
@@ -107,13 +171,12 @@ function itemLines(state, cols) {
107
171
  const maxW = Math.min(Math.max(20, cols - 4), 120);
108
172
  const paneW = Math.max(20, cols - 4);
109
173
  const lines = ['', ` ${BOLD}${CYAN}[${state.currentIndex + 1}/${state.slots.length}]${RESET} ${DIM}${sanitize(state.manifest.title)}${RESET}`, ` ${DIM}${hline(maxW)}${RESET}`, '', ` ${BOLD}${sanitize(describeSlot(slot))}${RESET}`];
110
- // A single-slot page opens straight into item-review, so its leading prose has nowhere else to render.
111
- const prose = state.slots.length === 1 ? [...state.overviewProse, ...(state.slotProse.get(0) ?? [])] : state.slotProse.get(state.currentIndex) ?? [];
112
- if (state.manifest.dialect === 'html')
113
- lines.push('', ` ${DIM}rich page — open in Northlight${RESET}`);
114
- else
115
- for (const line of proseLines(prose, maxW, paneW))
174
+ const body = slot.config.body;
175
+ if (typeof body === 'string' && body.length > 0) {
176
+ lines.push('');
177
+ for (const line of mdLines(body, maxW, paneW))
116
178
  lines.push(` ${line}`);
179
+ }
117
180
  if (!terminalAnswerable(slot))
118
181
  lines.push('', ` ${DIM}${sanitize(slot.kind)} — cannot be answered in the terminal; open in Northlight${RESET}`);
119
182
  else if (slot.kind === 'chart') {
@@ -125,9 +188,9 @@ function itemLines(state, cols) {
125
188
  lines.push('');
126
189
  for (const line of mdLines(tableMarkdown(slot.config), maxW, paneW))
127
190
  lines.push(` ${line}`);
128
- const caption = slot.config.caption;
129
- if (caption)
130
- lines.push(` ${DIM}${sanitize(caption)}${RESET}`);
191
+ const label = slot.config.label;
192
+ if (label)
193
+ lines.push(` ${DIM}${sanitize(label)}${RESET}`);
131
194
  }
132
195
  else if (slot.kind === 'text') {
133
196
  const response = slot.id === undefined ? undefined : state.responses.get(slot.id);
@@ -138,7 +201,7 @@ function itemLines(state, cols) {
138
201
  }
139
202
  lines.push('', ` ${DIM}${hline(maxW)}${RESET}`);
140
203
  if (state.inputMode !== null) {
141
- const label = state.inputMode.kind === 'comment' ? `Comment on ${anchorLabel(slot, state.inputMode.anchor ?? { kind: 'whole' })}` : state.inputMode.kind === 'text' ? 'Edit text' : 'Other response';
204
+ const label = state.inputMode.kind === 'comment' ? `Comment on ${commentLabel(slot, state.inputMode.anchor)}` : state.inputMode.kind === 'text' ? 'Edit text' : 'Other response';
142
205
  lines.push(` ${BOLD}${label}${RESET}`);
143
206
  for (const line of renderInputBuffer(state.inputMode.buffer, state.inputMode.cursor, Math.max(10, maxW - 4)))
144
207
  lines.push(` ${line}`);
@@ -152,7 +215,8 @@ function itemLines(state, cols) {
152
215
  const mark = selected(state, slot, action.group, action.id) ? `${GREEN}✓${RESET}` : ' ';
153
216
  lines.push(` ${cursor} [${mark}] ${action.shortcut ? `[${action.shortcut}] ` : ''}${sanitize(action.label)}`);
154
217
  if (action.description)
155
- lines.push(` ${DIM}${sanitize(action.description)}${RESET}`);
218
+ for (const line of wrap(sanitize(action.description), Math.max(10, maxW - 6)))
219
+ lines.push(` ${DIM}${line}${RESET}`);
156
220
  }
157
221
  else {
158
222
  const key = action.kind === 'freetext' ? 'r' : action.kind === 'edit' ? 'e' : 'a';
@@ -160,10 +224,11 @@ function itemLines(state, cols) {
160
224
  }
161
225
  });
162
226
  for (const comment of responseComments(state, slot))
163
- lines.push(` ${DIM}attached: ${anchorLabel(slot, comment.anchor)} — ${sanitize(comment.text)}${RESET}`);
227
+ for (const line of wrap(`attached: ${commentLabel(slot, comment.anchor)} — ${sanitize(comment.text)}`, Math.max(10, maxW - 2)))
228
+ lines.push(` ${DIM}${line}${RESET}`);
164
229
  if (state.hint)
165
230
  lines.push(` ${YELLOW}${sanitize(state.hint)}${RESET}`);
166
- lines.push(` ${DIM}n/p next/previous j/k actions c comment u/d scroll h/l pan q close${RESET}`);
231
+ lines.push(` ${DIM}n/p next/previous j/k actions${commentAnchors(slot).length > 0 ? ' c comment' : ''} u/d scroll h/l pan q close${RESET}`);
167
232
  }
168
233
  const maxLine = Math.max(0, ...lines.map(visibleWidth));
169
234
  return { lines, maxW, maxH: Math.max(0, maxLine - Math.max(1, cols)) };
@@ -195,13 +260,13 @@ export function renderItemReview(state, cols, rows) {
195
260
  export function renderFinal(state, cols, rows) {
196
261
  const maxW = Math.min(Math.max(20, cols - 4), 120);
197
262
  const lines = ['', ` ${BOLD}${CYAN}${sanitize(state.manifest.title)} — Summary${RESET}`, ` ${DIM}${hline(maxW)}${RESET}`, ''];
198
- for (const slot of state.slots) {
199
- const response = slot.id === undefined ? undefined : state.responses.get(slot.id);
200
- lines.push(` ${slotAnswered(state, slot) ? `${GREEN}✓${RESET}` : isResponseBearingSlot(slot) ? `${YELLOW}○${RESET}` : `${DIM}·${RESET}`} ${BOLD}${sanitize(describeSlot(slot))}${RESET} ${DIM}${sanitize(summarizeSlotResponse(slot, response))}${RESET}`);
263
+ const answer = pageAnswer(state);
264
+ for (const slot of answer.slots) {
265
+ const manifestSlot = state.manifest.slots.find((candidate) => candidate.id === slot.id);
266
+ const answered = manifestSlot !== undefined && slotAnswered(state, manifestSlot);
267
+ const mark = answered ? `${GREEN}✓${RESET}` : `${YELLOW}○${RESET}`;
268
+ lines.push(` ${mark} ${BOLD}${sanitize(manifestSlot === undefined ? slot.kind : describeSlot(manifestSlot))}${RESET} ${DIM}${sanitize(answerSummary(slot))}${RESET}`);
201
269
  }
202
- if (state.manifest.dialect !== 'html')
203
- for (const line of proseLines(state.finalProse, maxW, Math.max(20, cols - 4)))
204
- lines.push(` ${line}`);
205
270
  const complete = pageComplete(state);
206
271
  if (!complete) {
207
272
  lines.push('', ` ${YELLOW}Cannot submit yet.${RESET}`);
@@ -1,4 +1,4 @@
1
- import type { CommentAnchor, PageManifest, PageSlot, SlotResponse, TableConfig } from '../../../core/human/page-schema.js';
1
+ import type { CommentAnchor, PageManifest, PageSlot, TableConfig } from '../../../core/human/page-schema.js';
2
2
  import type { TuiState } from './types.js';
3
3
  export type SelectGroup = 'option' | 'card' | 'row' | 'column';
4
4
  export type ActionRow = {
@@ -20,13 +20,13 @@ export type ActionRow = {
20
20
  label: string;
21
21
  };
22
22
  export declare function describeSlot(slot: PageSlot): string;
23
- /** False when the slot's response cannot be authored here: an unvalidated product kind, or a source-backed cards slot whose cards the terminal never sees. */
23
+ /** False when this component's response cannot be authored here: an unvalidated product kind, or a source-backed cards component whose cards the terminal never sees. */
24
24
  export declare function terminalAnswerable(slot: PageSlot): boolean;
25
25
  export declare function slotActions(slot: PageSlot): ActionRow[];
26
26
  export declare function tableMarkdown(config: TableConfig): string;
27
- export declare function summarizeSlotResponse(slot: PageSlot, response: SlotResponse | undefined): string;
28
27
  export declare function slotAnswered(state: TuiState, slot: PageSlot): boolean;
29
28
  export declare function pageComplete(state: Pick<TuiState, 'manifest' | 'responses'>): boolean;
30
29
  export declare function commentIdFor(anchor: CommentAnchor): string;
31
- export declare function anchorLabel(slot: PageSlot, anchor: CommentAnchor): string;
30
+ /** The response-comment anchors this component may author: options take per-option, tables take rows only. */
31
+ export declare function commentAnchors(slot: PageSlot): CommentAnchor[];
32
32
  export declare function awaitsResponse(manifest: PageManifest): boolean;
@@ -1,6 +1,9 @@
1
1
  import { isResponseBearingSlot, validatePageResponses, validatePartialPageResponses } from '../../../core/human/page-schema.js';
2
2
  const RESERVED = new Set(['c', 'r', 'e', 'a', 'n', 'p', 'q', 'j', 'k', 'u', 'd', ' ']);
3
3
  export function describeSlot(slot) {
4
+ const label = slot.config.label;
5
+ if (typeof label === 'string' && label !== '')
6
+ return label;
4
7
  if (slot.unvalidated === true)
5
8
  return slot.kind;
6
9
  if (slot.kind === 'options')
@@ -8,14 +11,14 @@ export function describeSlot(slot) {
8
11
  if (slot.kind === 'text')
9
12
  return 'Text';
10
13
  if (slot.kind === 'table')
11
- return slot.config.caption ?? 'Table';
14
+ return 'Table';
12
15
  if (slot.kind === 'cards')
13
16
  return 'Cards';
14
17
  if (slot.kind === 'chart')
15
18
  return 'Chart';
16
19
  return slot.kind;
17
20
  }
18
- /** False when the slot's response cannot be authored here: an unvalidated product kind, or a source-backed cards slot whose cards the terminal never sees. */
21
+ /** False when this component's response cannot be authored here: an unvalidated product kind, or a source-backed cards component whose cards the terminal never sees. */
19
22
  export function terminalAnswerable(slot) {
20
23
  if (slot.unvalidated === true)
21
24
  return false;
@@ -31,15 +34,16 @@ export function slotActions(slot) {
31
34
  const config = slot.config;
32
35
  rows = config.options.map((option) => ({ kind: 'select', group: 'option', id: option.id, label: option.label, ...(option.description === undefined ? {} : { description: option.description }), mode: config.mode }));
33
36
  if (config.allowFreetext === true)
34
- rows.push({ kind: 'freetext', label: config.freetextLabel ?? 'Other' });
37
+ rows.push({ kind: 'freetext', label: config.freetextLabel ?? 'Something else' });
35
38
  }
36
39
  else if (slot.kind === 'cards') {
37
40
  const config = slot.config;
38
41
  rows = (config.cards ?? []).map((card) => ({ kind: 'select', group: 'card', id: card.id, label: card.subtitle === undefined ? card.title : `${card.title} — ${card.subtitle}`, ...(card.body === undefined ? {} : { description: card.body }), mode: config.mode }));
39
42
  }
40
43
  else if (slot.kind === 'text') {
41
- if (slot.config.editable === true)
42
- rows.push({ kind: 'edit', label: 'Edit' });
44
+ // Text is always a writing surface — the same contract the page kit presents. Accepting
45
+ // the draft as written is the other half of that answer, not a separate read-only mode.
46
+ rows.push({ kind: 'edit', label: 'Edit' });
43
47
  rows.push({ kind: 'accept', label: 'Accept as-is' });
44
48
  }
45
49
  else if (slot.kind === 'table') {
@@ -84,28 +88,6 @@ export function tableMarkdown(config) {
84
88
  const divider = `| ${config.columns.map((column) => column.align === 'right' ? '---:' : '---').join(' | ')} |`;
85
89
  return [header, divider, ...(config.rows ?? []).map((row) => `| ${config.columns.map((column) => escapeCell(row.cells[column.id], column.mono === true)).join(' | ')} |`)].join('\n');
86
90
  }
87
- export function summarizeSlotResponse(slot, response) {
88
- if (response === undefined)
89
- return isResponseBearingSlot(slot) ? 'unanswered' : 'display';
90
- if (slot.kind === 'options') {
91
- const r = response;
92
- const labels = slot.config.options.filter((o) => r.selectedOptionIds.includes(o.id)).map((o) => o.label);
93
- if ((r.freetext?.trim() ?? '') !== '')
94
- labels.push(r.freetext.trim());
95
- return labels.join(', ') || (r.comments.length > 0 ? `${r.comments.length} comment${r.comments.length === 1 ? '' : 's'}` : 'answered');
96
- }
97
- if (slot.kind === 'cards') {
98
- const r = response;
99
- return slot.config.cards?.filter((c) => r.selectedCardIds.includes(c.id)).map((c) => c.title).join(', ') || 'answered';
100
- }
101
- if (slot.kind === 'table') {
102
- const r = response;
103
- return `${r.selectedRowIds.length} row${r.selectedRowIds.length === 1 ? '' : 's'}, ${r.selectedColumnIds.length} column${r.selectedColumnIds.length === 1 ? '' : 's'}`;
104
- }
105
- if (slot.kind === 'text')
106
- return response.edited ? 'edited' : 'accepted';
107
- return 'answered';
108
- }
109
91
  export function slotAnswered(state, slot) {
110
92
  if (!terminalAnswerable(slot) || slot.id === undefined || !state.responses.has(slot.id))
111
93
  return false;
@@ -130,27 +112,20 @@ export function pageComplete(state) {
130
112
  }
131
113
  export function commentIdFor(anchor) {
132
114
  switch (anchor.kind) {
133
- case 'whole': return 'whole';
134
115
  case 'option': return `option:${anchor.optionId}`;
135
- case 'card': return `card:${anchor.cardId}`;
136
116
  case 'row': return `row:${anchor.rowId}`;
137
- case 'column': return `column:${anchor.columnId}`;
138
- case 'range': return `range:${anchor.start}:${anchor.end}`;
139
117
  }
140
118
  }
141
- export function anchorLabel(slot, anchor) {
142
- if (anchor.kind === 'whole')
143
- return describeSlot(slot);
144
- if (anchor.kind === 'option')
145
- return slot.config.options.find((x) => x.id === anchor.optionId)?.label ?? anchor.optionId;
146
- if (anchor.kind === 'card')
147
- return slot.config.cards?.find((x) => x.id === anchor.cardId)?.title ?? anchor.cardId;
148
- if (anchor.kind === 'row')
149
- return slot.config.rows?.find((x) => x.id === anchor.rowId)?.id ?? anchor.rowId;
150
- if (anchor.kind === 'column')
151
- return slot.config.columns.find((x) => x.id === anchor.columnId)?.label ?? anchor.columnId;
152
- return anchor.quote;
119
+ /** The response-comment anchors this component may author: options take per-option, tables take rows only. */
120
+ export function commentAnchors(slot) {
121
+ if (!terminalAnswerable(slot))
122
+ return [];
123
+ if (slot.kind === 'options')
124
+ return slot.config.options.map((option) => ({ kind: 'option', optionId: option.id }));
125
+ if (slot.kind === 'table')
126
+ return (slot.config.rows ?? []).map((row) => ({ kind: 'row', rowId: row.id }));
127
+ return [];
153
128
  }
154
129
  export function awaitsResponse(manifest) {
155
- return manifest.slots.some(isResponseBearingSlot);
130
+ return manifest.delivery.reply;
156
131
  }
@@ -1,12 +1,12 @@
1
1
  import type { Key } from './keys.js';
2
+ import type { ProductPageComponents } from '../../../types.js';
2
3
  import type { CommentAnchor, PageManifest, PageResponses, PageSlot, SlotResponse } from '../../../core/human/page-schema.js';
3
- import type { PageSourceProjection } from '../../../core/human/page.js';
4
4
  export type Phase = 'overview' | 'item-review' | 'final';
5
5
  export type InputMode = null | {
6
6
  kind: 'comment';
7
7
  buffer: string;
8
8
  cursor: number;
9
- anchor?: CommentAnchor;
9
+ anchor: CommentAnchor;
10
10
  } | {
11
11
  kind: 'freetext';
12
12
  buffer: string;
@@ -21,11 +21,8 @@ export interface TuiState {
21
21
  currentIndex: number;
22
22
  manifest: PageManifest;
23
23
  slots: PageSlot[];
24
- overviewProse: string[];
25
- slotProse: Map<number, string[]>;
26
- finalProse: string[];
27
24
  responses: Map<string, SlotResponse>;
28
- productKinds: string[];
25
+ productKinds: ProductPageComponents;
29
26
  inputMode: InputMode;
30
27
  selectedAction: number;
31
28
  bodyMode: 'question';
@@ -42,8 +39,7 @@ export interface TuiState {
42
39
  }
43
40
  export interface MountedPanelOpts {
44
41
  manifest: PageManifest;
45
- projection: PageSourceProjection;
46
- productKinds?: string[];
42
+ productKinds?: ProductPageComponents;
47
43
  initialResponses?: PageResponses | null;
48
44
  cols: number;
49
45
  rows: number;
@@ -58,9 +54,9 @@ export interface MountedPanel {
58
54
  render(): string[];
59
55
  handleResize(cols: number, rows: number): string[];
60
56
  unmount(): void;
61
- loadPage(manifest: PageManifest, projection: PageSourceProjection, opts?: {
57
+ loadPage(manifest: PageManifest, opts?: {
62
58
  initialResponses?: PageResponses | null;
63
- productKinds?: string[];
59
+ productKinds?: ProductPageComponents;
64
60
  }): void;
65
61
  canAcceptHostKeys(): boolean;
66
62
  atPageTop(): boolean;
@@ -36,12 +36,12 @@ export function buildInboxLines(items, width, selectedIndex, height = Number.MAX
36
36
  }
37
37
  function buildItemLines(item, index, selectedIndex, contentWidth) {
38
38
  const kind = item.kind === 'page'
39
- ? item.awaitsResponse === false ? 'display' : item.dialect === 'html' ? 'rich' : 'interactive'
39
+ ? item.awaitsResponse === false ? 'display' : 'rich'
40
40
  : item.kind;
41
41
  const icon = KIND_ICON[kind] ?? '·';
42
42
  // The inbox is a title-first triage surface. Origin details remain available
43
43
  // after opening a ticket, rather than consuming the list's scarce row width.
44
- const age = formatTimeAgo(item.blockedSince);
44
+ const age = formatTimeAgo(item.kind === 'page' ? item.emittedAt : item.blockedSince);
45
45
  const cursor = index === selectedIndex ? `${CYAN}▸${RESET} ` : ' ';
46
46
  const titleWidth = Math.max(1, contentWidth - age.length - 6);
47
47
  const row = `${cursor}${ansiColor(icon, KIND_COLOR[kind] ?? 'cyan')} ${BOLD}${truncateRow(item.title || `(${item.id.slice(0, 8)})`, titleWidth)}${RESET} ${DIM}${age}${RESET}`;
@@ -1,16 +1,24 @@
1
- // Regression: a caller passing BOTH a positional AND piped stdin (e.g.
2
- // `crtr node new --kind x "title" < prompt.md`, or `crtr human ask -
3
- // <<EOF`) used to have the positional silently win, with the entire piped
4
- // body dropped on the floor. `-` must always mean "read stdin", and any
5
- // OTHER positional alongside genuinely piped, non-empty stdin must error
6
- // loudly rather than silently pick one.
7
- //
8
1
  // Run with: node --import tsx/esm --test 'src/commands/__tests__/human.test.ts'
9
2
  import { test, describe } from 'node:test';
10
3
  import assert from 'node:assert/strict';
4
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
5
+ import { tmpdir } from 'node:os';
6
+ import { join } from 'node:path';
11
7
  import { Readable } from 'node:stream';
12
8
  import { parseArgv } from '../../core/command.js';
13
- import { humanAsk } from '../human/prompts.js';
9
+ import { renderLeafArgv } from '../../core/help.js';
10
+ import { componentDoc, renderComponentCatalog, renderComponentContract } from '../../core/human/component-docs.js';
11
+ import { humanComponents } from '../human/components.js';
12
+ import { humanSend } from '../human/prompts.js';
13
+ import { registerHuman } from '../human.js';
14
+ import { pageHelpText } from '../human/shared.js';
15
+ const nlThreadRegistration = {
16
+ kind: 'nl-thread',
17
+ description: 'message conversation with participants and metadata',
18
+ useWhen: 'use when the human needs to read a conversation - an email chain, a LinkedIn DM, an iMessage exchange - in message order, usually above whatever the page asks them to decide',
19
+ display: true,
20
+ doc: '`nl-thread` renders a conversation as bubbles down one column. Display only: it contributes nothing to the response.',
21
+ };
14
22
  async function withPipedStdin(body, fn) {
15
23
  const original = process.stdin;
16
24
  const stdin = Readable.from([Buffer.from(body)]);
@@ -23,25 +31,111 @@ async function withPipedStdin(body, fn) {
23
31
  Object.defineProperty(process, 'stdin', { value: original, configurable: true });
24
32
  }
25
33
  }
26
- describe('human ask: positional vs piped stdin', () => {
27
- test('`-` positional reads stdin instead of taking the literal value', async () => {
28
- const body = 'Should we ship today?\nmulti-line brief\n';
29
- const result = await withPipedStdin(body, () => parseArgv(humanAsk.help.params ?? [], ['-']));
30
- assert.equal(result['question'], body);
31
- });
32
- test('positional + piped non-empty stdin → loud bad_invocation naming both sources', async () => {
33
- await assert.rejects(() => withPipedStdin('piped body', () => parseArgv(humanAsk.help.params ?? [], ['Should we ship today?'])), (err) => {
34
+ function withPageSurface(pageSurface, run) {
35
+ const home = mkdtempSync(join(tmpdir(), 'crtr-human-help-'));
36
+ const originalHome = process.env['HOME'];
37
+ process.env['HOME'] = home;
38
+ mkdirSync(join(home, '.crouter'), { recursive: true });
39
+ writeFileSync(join(home, '.crouter', 'config.json'), JSON.stringify({ page_surface: pageSurface }));
40
+ try {
41
+ return run();
42
+ }
43
+ finally {
44
+ if (originalHome === undefined)
45
+ delete process.env['HOME'];
46
+ else
47
+ process.env['HOME'] = originalHome;
48
+ rmSync(home, { recursive: true, force: true });
49
+ }
50
+ }
51
+ describe('human page input', () => {
52
+ test('stdin supplies the authored page document', async () => {
53
+ const document = 'export default () => <Page title="Question" />;\n';
54
+ const result = await withPipedStdin(document, () => parseArgv(humanSend.help.params ?? [], []));
55
+ assert.equal(result['document'], document);
56
+ });
57
+ test('empty stdin remains empty alongside --page', async () => {
58
+ const result = await withPipedStdin('', () => parseArgv(humanSend.help.params ?? [], ['--page', 'page.tsx']));
59
+ assert.equal(result['page'], 'page.tsx');
60
+ assert.equal(result['document'], '');
61
+ });
62
+ test('a positional document is rejected', async () => {
63
+ await assert.rejects(() => parseArgv(humanSend.help.params ?? [], ['page.tsx']), (err) => {
34
64
  assert.equal(err.code, 'bad_invocation');
35
- assert.match(err.message, /positional/);
36
- assert.match(err.message, /stdin/);
37
- assert.match(err.message, /Should we ship today\?/);
38
- assert.equal(err.details?.field, 'question');
39
- assert.match(err.details?.next ?? '', /exactly one/);
65
+ assert.match(err.message, /takes no positional arguments/);
66
+ assert.match(err.details?.next ?? '', /schema/);
40
67
  return true;
41
68
  });
42
69
  });
43
- test('positional with empty piped stdin (no TTY, no bytes) still uses the positional', async () => {
44
- const result = await withPipedStdin('', () => parseArgv(humanAsk.help.params ?? [], ['Should we ship today?']));
45
- assert.equal(result['question'], 'Should we ship today?');
70
+ test('send help keeps page authoring at the human branch and makes reply routing explicit', () => {
71
+ const rendered = renderLeafArgv(humanSend.help);
72
+ assert.match(rendered, /^human send: send a durable TSX or HTML page\./);
73
+ assert.match(rendered, /Page authoring: `crtr human -h`\./);
74
+ assert.doesNotMatch(rendered, /--no-reply|--reply/);
75
+ assert.match(rendered, /page_id/);
76
+ assert.match(rendered, /reply/);
77
+ assert.doesNotMatch(rendered, /human notify|run\.json|job_id/);
78
+ });
79
+ });
80
+ test('human help carries the page authoring model and follows the page-surface setting', () => {
81
+ withPageSurface(false, () => {
82
+ const human = registerHuman();
83
+ const help = pageHelpText(false);
84
+ assert.equal(human.rootEntry?.useWhen, help.rootUseWhen);
85
+ assert.ok(human.help.model?.includes(help.authoring));
86
+ assert.ok(human.help.model?.includes('`crtr human components`'));
87
+ assert.doesNotMatch(human.help.model ?? '', /doc check/);
88
+ assert.match(human.help.model ?? '', /\.html/);
89
+ });
90
+ withPageSurface(true, () => {
91
+ const human = registerHuman();
92
+ const help = pageHelpText(true);
93
+ assert.notEqual(help.authoring, pageHelpText(false).authoring);
94
+ assert.equal(human.rootEntry?.useWhen, help.rootUseWhen);
95
+ assert.ok(human.help.model?.includes(help.authoring));
96
+ });
97
+ });
98
+ // The gate itself, not its prose: a page-surface host must not carry the two
99
+ // terminal-only surfaces, because a review ticket filed there reaches a person
100
+ // who has no editor to open it in.
101
+ test('page_surface removes the terminal-only human surfaces from the tree', () => {
102
+ const names = () => registerHuman().children.map((child) => child.name);
103
+ withPageSurface(false, () => {
104
+ assert.deepEqual(names(), ['components', 'send', 'review', 'feedback', 'show', 'cancel', 'list', 'resolve']);
105
+ });
106
+ withPageSurface(true, () => {
107
+ const children = names();
108
+ assert.deepEqual(children, ['components', 'send', 'feedback', 'cancel', 'list', 'resolve']);
109
+ const human = registerHuman();
110
+ assert.doesNotMatch(human.help.model ?? '', /`review`|`show`/);
111
+ assert.doesNotMatch(human.rootEntry?.useWhen ?? '', /review a document|live display/);
112
+ });
113
+ });
114
+ test('human components catalog and contracts teach product registrations', () => {
115
+ const legacyRegistration = { kind: 'product-card' };
116
+ const catalog = renderComponentCatalog([nlThreadRegistration, legacyRegistration]);
117
+ assert.match(catalog, /^<components count="\d+">\nStructural — /);
118
+ assert.match(catalog, /\n`Page` — /);
119
+ assert.match(catalog, /\n`NlThread` \(kind `nl-thread`\) — message conversation with participants and metadata\. Use when the human needs to read a conversation - an email chain, a LinkedIn DM, an iMessage exchange - in message order, usually above whatever the page asks them to decide\. Display-only\n/);
120
+ assert.match(catalog, /\n`ProductCard` \(kind `product-card`\) — product-registered\n<\/components>$/);
121
+ assert.doesNotMatch(catalog, /Props\n|Response rules/);
122
+ // A product component is addressed by the JSX tag page-catalog derives from its registered
123
+ // kind; the contract heading names the wire kind that tag maps to.
124
+ assert.equal(renderComponentContract('nl-thread', [nlThreadRegistration]), `\`<NlThread>\` — product-registered, wire kind \`nl-thread\`\n\n${nlThreadRegistration.doc}`);
125
+ assert.equal(renderComponentContract('ProductCard', [legacyRegistration]), "`<ProductCard>` — product-registered, wire kind `product-card`\n\nproduct-registered; config is passed through unvalidated (unvalidated: true) — see the product's own docs");
126
+ });
127
+ test('human components show resolves every registry name, families included', async () => {
128
+ const show = humanComponents.children.find((child) => child.name === 'show');
129
+ assert.ok(show !== undefined && show.kind === 'leaf');
130
+ const question = await show.run({ component: 'UserQuestion' });
131
+ assert.ok(question !== undefined);
132
+ assert.equal(question['contract'], componentDoc('UserQuestion')?.contract);
133
+ assert.match(String(question['contract']), /wire kind `options`/);
134
+ // A shadcn family member resolves to the family contract that documents it.
135
+ const cardHeader = await show.run({ component: 'CardHeader' });
136
+ assert.equal(cardHeader?.['contract'], componentDoc('Card')?.contract);
137
+ await assert.rejects(() => show.run({ component: 'crtr-options' }), (err) => {
138
+ assert.equal(err.code, 'unknown_component');
139
+ return true;
46
140
  });
47
141
  });
@@ -0,0 +1,14 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { renderLeafArgv } from '../../core/help.js';
4
+ import { nodeMessage } from '../node/message.js';
5
+ test('node message send help documents the finalized-node re-task gate', () => {
6
+ const send = nodeMessage.children.find((child) => child.kind === 'leaf' && child.name === 'send');
7
+ if (send === undefined || send.kind !== 'leaf')
8
+ assert.fail('node message send leaf is missing');
9
+ const rendered = renderLeafArgv(send.help);
10
+ assert.match(rendered, /--reopen/);
11
+ assert.match(rendered, /Required to re-task a finalized target/);
12
+ assert.match(rendered, /clears a finalized target's finalization latch before delivery/);
13
+ assert.match(rendered, /Without it, a finalized target rejects delivery before an inbox entry is appended or a revive is attempted/);
14
+ });
@@ -1,6 +1,6 @@
1
1
  // `crtr canvas attention` — aggregate pending human tickets across the canvas.
2
2
  //
3
- // A pending "ticket" is any humanloop interaction awaiting a human: a page or
3
+ // A pending "ticket" is any humanloop interaction awaiting the user: a page or
4
4
  // an anchored review. Each lives in its human bridge node
5
5
  // directory and is attributed through that bridge's parent relation. This
6
6
  // subtree surfaces a direct node projection; cwd is display metadata only.
@@ -93,7 +93,7 @@ const attentionCount = defineLeaf({
93
93
  const attentionList = defineLeaf({
94
94
  name: 'list',
95
95
  description: 'itemised list of cwds with pending human tickets',
96
- whenToUse: 'finding which agents are blocked waiting on a human — an itemised list of the cwds with pending tickets (asks, reviews, notifications), oldest first, so you know where to go answer. Scope it to a sub-DAG or list canvas-wide. Use `canvas attention count` instead when you just need the number.',
96
+ whenToUse: 'finding which agents are blocked waiting on the user — an itemised list of the cwds with pending tickets (asks, reviews, notifications), oldest first, so you know where to go answer. Scope it to a sub-DAG or list canvas-wide. Use `canvas attention count` instead when you just need the number.',
97
97
  help: {
98
98
  name: 'canvas attention list',
99
99
  summary: 'list nodes that asked pending human tickets',
@@ -213,11 +213,11 @@ const attentionMap = defineLeaf({
213
213
  export const attentionBranch = defineBranch({
214
214
  name: 'attention',
215
215
  description: 'count/list pending human tickets across the graph',
216
- whenToUse: 'checking whether any agent on the canvas is blocked waiting on a human, and where: count the pending tickets (asks, reviews, notifications) or list the cwds that have them. Scope it to a node or a sub-DAG, or go canvas-wide. Use `canvas dashboard` instead for the graph SHAPE, or `node inspect list` for a plain node roster',
216
+ whenToUse: 'checking whether any agent on the canvas is blocked waiting on the user, and where: count the pending tickets (asks, reviews, notifications) or list the cwds that have them. Scope it to a node or a sub-DAG, or go canvas-wide. Use `canvas dashboard` instead for the graph SHAPE, or `node inspect list` for a plain node roster',
217
217
  help: {
218
218
  name: 'canvas attention',
219
219
  summary: 'aggregate pending human tickets across the canvas',
220
- model: '`count` returns a single integer (stdout.count is machine-parseable); `list` returns itemised asking-node entries. Scope with --node (one node) or --view (sub-DAG) — default is canvas-wide. A ticket is any humanloop interaction awaiting a human: an ask, a review, or a notification.',
220
+ model: '`count` returns a single integer (stdout.count is machine-parseable); `list` returns itemised asking-node entries. Scope with --node (one node) or --view (sub-DAG) — default is canvas-wide. A ticket is any humanloop interaction awaiting the user: an ask, a review, or a notification.',
221
221
  },
222
222
  children: [attentionCount, attentionList, attentionMap],
223
223
  });