@crouton-kit/crouter 0.3.27 → 0.3.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (343) hide show
  1. package/dist/build-root.js +4 -2
  2. package/dist/builtin-memory/crouter-development/marketplaces.md +7 -6
  3. package/dist/builtin-memory/crouter-development/personas/base-prompt.md +1 -1
  4. package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +1 -1
  5. package/dist/builtin-memory/crouter-development/personas.md +1 -1
  6. package/dist/builtin-memory/crouter-development/plugins.md +20 -18
  7. package/dist/builtin-memory/design.md +1 -1
  8. package/dist/builtin-memory/development.md +1 -1
  9. package/dist/builtin-memory/internal/INDEX.md +1 -1
  10. package/dist/builtin-memory/internal/examples/INDEX.md +1 -1
  11. package/dist/builtin-memory/internal/examples/imessage-assistant.md +2 -2
  12. package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
  13. package/dist/builtin-memory/internal/storage-tiers.md +1 -1
  14. package/dist/builtin-memory/planning.md +1 -1
  15. package/dist/builtin-memory/spec.md +6 -4
  16. package/dist/builtin-personas/orchestration-kernel.md +4 -5
  17. package/dist/builtin-personas/runtime-base.md +8 -4
  18. package/dist/builtin-personas/spec/PERSONA.md +8 -4
  19. package/dist/builtin-personas/spec/orchestrator.md +5 -3
  20. package/dist/builtin-personas/spec/requirements/PERSONA.md +10 -0
  21. package/dist/builtin-views/canvas/core.mjs +586 -0
  22. package/dist/builtin-views/canvas/text.mjs +58 -0
  23. package/dist/builtin-views/canvas/tui.mjs +165 -0
  24. package/dist/builtin-views/canvas/web.jsx +120 -0
  25. package/dist/builtin-views/git-pr/core.mjs +673 -0
  26. package/dist/builtin-views/git-pr/text.mjs +84 -0
  27. package/dist/builtin-views/git-pr/tui.mjs +302 -0
  28. package/dist/builtin-views/git-pr/web.jsx +216 -0
  29. package/dist/builtin-views/inbox/_lib/render.mjs +1 -3
  30. package/dist/builtin-views/inbox/core.mjs +1273 -0
  31. package/dist/builtin-views/inbox/text.mjs +73 -0
  32. package/dist/builtin-views/inbox/tui.mjs +312 -0
  33. package/dist/builtin-views/inbox/web.jsx +188 -0
  34. package/dist/builtin-views/linkedin/core.mjs +906 -0
  35. package/dist/builtin-views/linkedin/text.mjs +69 -0
  36. package/dist/builtin-views/linkedin/tui.mjs +427 -0
  37. package/dist/builtin-views/linkedin/web.jsx +206 -0
  38. package/dist/builtin-views/workspace-sidebar/core.mjs +621 -0
  39. package/dist/builtin-views/workspace-sidebar/text.mjs +53 -0
  40. package/dist/builtin-views/workspace-sidebar/tui.mjs +142 -0
  41. package/dist/builtin-views/workspace-sidebar/web.jsx +109 -0
  42. package/dist/cli.js +3 -3
  43. package/dist/clients/attach/__tests__/action-parity.test.js +48 -0
  44. package/dist/clients/attach/__tests__/clipboard-image.test.js +76 -0
  45. package/dist/clients/attach/__tests__/editor-newline.test.js +57 -0
  46. package/dist/clients/attach/__tests__/git-info.test.js +31 -0
  47. package/dist/clients/attach/__tests__/onrequest-wired.test.js +20 -0
  48. package/dist/clients/attach/__tests__/reconnect-giveup.test.js +30 -0
  49. package/dist/clients/attach/__tests__/slash-quit-copy.test.js +42 -0
  50. package/dist/clients/attach/__tests__/titled-editor.test.d.ts +1 -0
  51. package/dist/clients/attach/__tests__/titled-editor.test.js +39 -0
  52. package/dist/clients/attach/attach-cmd.js +445 -41
  53. package/dist/clients/attach/auth-pickers.d.ts +9 -0
  54. package/dist/clients/attach/auth-pickers.js +194 -0
  55. package/dist/clients/attach/canvas-panels.d.ts +12 -0
  56. package/dist/clients/attach/canvas-panels.js +78 -0
  57. package/dist/clients/attach/chat-view.d.ts +27 -2
  58. package/dist/clients/attach/chat-view.js +98 -9
  59. package/dist/clients/attach/clipboard-image.d.ts +47 -8
  60. package/dist/clients/attach/clipboard-image.js +150 -26
  61. package/dist/clients/attach/clipboard-text.d.ts +3 -0
  62. package/dist/clients/attach/clipboard-text.js +39 -0
  63. package/dist/clients/attach/config-load.d.ts +54 -7
  64. package/dist/clients/attach/config-load.js +99 -3
  65. package/dist/clients/attach/context-message.d.ts +12 -0
  66. package/dist/clients/attach/context-message.js +72 -0
  67. package/dist/clients/attach/git-info.d.ts +20 -0
  68. package/dist/clients/attach/git-info.js +44 -0
  69. package/dist/clients/attach/graph-overlay.d.ts +43 -0
  70. package/dist/clients/attach/graph-overlay.js +306 -0
  71. package/dist/clients/attach/input-controller.d.ts +81 -13
  72. package/dist/clients/attach/input-controller.js +307 -67
  73. package/dist/clients/attach/pickers.d.ts +58 -0
  74. package/dist/clients/attach/pickers.js +171 -0
  75. package/dist/clients/attach/slash-commands.d.ts +44 -1
  76. package/dist/clients/attach/slash-commands.js +250 -27
  77. package/dist/clients/attach/titled-editor.d.ts +33 -0
  78. package/dist/clients/attach/titled-editor.js +84 -0
  79. package/dist/clients/attach/view-socket.d.ts +37 -2
  80. package/dist/clients/attach/view-socket.js +131 -7
  81. package/dist/clients/web/dev-server.d.ts +7 -0
  82. package/dist/clients/web/dev-server.js +59 -0
  83. package/dist/clients/web/events.d.ts +14 -0
  84. package/dist/clients/web/events.js +151 -0
  85. package/dist/clients/web/server.d.ts +18 -0
  86. package/dist/clients/web/server.js +450 -0
  87. package/dist/clients/web/web-cmd.d.ts +2 -0
  88. package/dist/clients/web/web-cmd.js +120 -0
  89. package/dist/commands/canvas.js +1 -2
  90. package/dist/commands/chord.js +1 -1
  91. package/dist/commands/dashboard.js +5 -1
  92. package/dist/commands/human/prompts.js +3 -3
  93. package/dist/commands/human/queue.d.ts +17 -0
  94. package/dist/commands/human/queue.js +111 -4
  95. package/dist/commands/memory/__tests__/lint-schema.test.js +27 -4
  96. package/dist/commands/memory/lint.d.ts +5 -4
  97. package/dist/commands/memory/lint.js +23 -7
  98. package/dist/commands/memory/read.js +2 -2
  99. package/dist/commands/memory/shared.d.ts +7 -1
  100. package/dist/commands/memory/shared.js +18 -3
  101. package/dist/commands/memory/write.js +20 -7
  102. package/dist/commands/memory.js +4 -4
  103. package/dist/commands/node.d.ts +1 -1
  104. package/dist/commands/node.js +64 -65
  105. package/dist/commands/pkg.js +1 -2
  106. package/dist/commands/revive.js +11 -3
  107. package/dist/commands/sys/feedback.d.ts +1 -0
  108. package/dist/commands/sys/feedback.js +163 -0
  109. package/dist/commands/sys/sync.d.ts +1 -0
  110. package/dist/commands/sys/sync.js +187 -0
  111. package/dist/commands/sys.js +4 -2
  112. package/dist/commands/view-cycle.js +2 -2
  113. package/dist/commands/view-list.js +8 -8
  114. package/dist/commands/view-new.js +21 -17
  115. package/dist/commands/view-pick.js +1 -1
  116. package/dist/commands/view-run.js +35 -14
  117. package/dist/commands/view.js +8 -6
  118. package/dist/commands/workspace.d.ts +2 -0
  119. package/dist/commands/workspace.js +161 -0
  120. package/dist/core/__tests__/broker-double-spawn.test.d.ts +1 -0
  121. package/dist/core/__tests__/broker-double-spawn.test.js +143 -0
  122. package/dist/core/__tests__/broker-fork-seam.test.d.ts +1 -0
  123. package/dist/core/__tests__/broker-fork-seam.test.js +102 -0
  124. package/dist/core/__tests__/broker-sdk-wiring.test.js +2 -0
  125. package/dist/core/__tests__/broker-snapshot-history.test.d.ts +1 -0
  126. package/dist/core/__tests__/broker-snapshot-history.test.js +105 -0
  127. package/dist/core/__tests__/child-death-wake.test.js +56 -111
  128. package/dist/core/__tests__/close.test.js +14 -20
  129. package/dist/core/__tests__/context-intro.test.js +19 -19
  130. package/dist/core/__tests__/daemon-boot.test.js +30 -23
  131. package/dist/core/__tests__/draw-style.test.js +1 -1
  132. package/dist/core/__tests__/error-stall-recycle.test.d.ts +1 -0
  133. package/dist/core/__tests__/error-stall-recycle.test.js +141 -0
  134. package/dist/core/__tests__/fixtures/fake-engine.d.ts +24 -1
  135. package/dist/core/__tests__/fixtures/fake-engine.js +32 -2
  136. package/dist/core/__tests__/fixtures/fake-pi-host.js +2 -2
  137. package/dist/core/__tests__/focuses.test.js +25 -13
  138. package/dist/core/__tests__/fork.test.js +22 -5
  139. package/dist/core/__tests__/full/broker-control-preempt.test.d.ts +1 -0
  140. package/dist/core/__tests__/full/broker-control-preempt.test.js +61 -0
  141. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.d.ts +1 -0
  142. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.js +57 -0
  143. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.d.ts +1 -0
  144. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.js +80 -0
  145. package/dist/core/__tests__/full/broker-pane-resolution.test.js +90 -0
  146. package/dist/core/__tests__/full/cascade-close.test.js +22 -8
  147. package/dist/core/__tests__/full/detach-focus.test.js +66 -169
  148. package/dist/core/__tests__/helpers/harness.d.ts +6 -9
  149. package/dist/core/__tests__/helpers/harness.js +19 -36
  150. package/dist/core/__tests__/human-node-not-supervised.test.d.ts +1 -0
  151. package/dist/core/__tests__/human-node-not-supervised.test.js +83 -0
  152. package/dist/core/__tests__/human-stranded-deliver.test.d.ts +1 -0
  153. package/dist/core/__tests__/human-stranded-deliver.test.js +108 -0
  154. package/dist/core/__tests__/lifecycle.test.js +1 -19
  155. package/dist/core/__tests__/live-mutation-verbs.test.js +5 -5
  156. package/dist/core/__tests__/memory-resolver.test.js +1 -1
  157. package/dist/core/__tests__/on-read-dedup-resume.test.d.ts +1 -0
  158. package/dist/core/__tests__/on-read-dedup-resume.test.js +81 -0
  159. package/dist/core/__tests__/refresh-stall-recycle.test.d.ts +1 -0
  160. package/dist/core/__tests__/refresh-stall-recycle.test.js +88 -0
  161. package/dist/core/__tests__/relaunch-root.test.d.ts +1 -0
  162. package/dist/core/__tests__/relaunch-root.test.js +114 -0
  163. package/dist/core/__tests__/reset.test.js +19 -79
  164. package/dist/core/__tests__/steer-note.test.js +9 -7
  165. package/dist/core/__tests__/tmux-surface.test.js +13 -7
  166. package/dist/core/bootstrap.d.ts +0 -3
  167. package/dist/core/bootstrap.js +1 -143
  168. package/dist/core/canvas/browse/__tests__/model.test.js +86 -1
  169. package/dist/core/canvas/browse/__tests__/render.test.js +2 -1
  170. package/dist/core/canvas/browse/app.js +131 -8
  171. package/dist/core/canvas/browse/model.d.ts +23 -5
  172. package/dist/core/canvas/browse/model.js +68 -13
  173. package/dist/core/canvas/browse/render.d.ts +7 -1
  174. package/dist/core/canvas/browse/render.js +178 -47
  175. package/dist/core/canvas/canvas.js +1 -1
  176. package/dist/core/canvas/focuses.d.ts +4 -4
  177. package/dist/core/canvas/focuses.js +7 -7
  178. package/dist/core/canvas/nav-model.d.ts +162 -0
  179. package/dist/core/canvas/nav-model.js +487 -0
  180. package/dist/core/canvas/paths.d.ts +7 -0
  181. package/dist/core/canvas/paths.js +9 -0
  182. package/dist/core/canvas/render.d.ts +68 -9
  183. package/dist/core/canvas/render.js +195 -61
  184. package/dist/core/canvas/types.d.ts +16 -23
  185. package/dist/core/config.js +1 -4
  186. package/dist/core/memory-resolver.js +43 -5
  187. package/dist/core/runtime/bearings.d.ts +1 -1
  188. package/dist/core/runtime/bearings.js +8 -8
  189. package/dist/core/runtime/branded-host.d.ts +16 -0
  190. package/dist/core/runtime/branded-host.js +127 -0
  191. package/dist/core/runtime/broker-protocol.d.ts +212 -6
  192. package/dist/core/runtime/broker-sdk.d.ts +0 -12
  193. package/dist/core/runtime/broker-sdk.js +77 -6
  194. package/dist/core/runtime/broker.d.ts +5 -1
  195. package/dist/core/runtime/broker.js +520 -59
  196. package/dist/core/runtime/close.d.ts +12 -2
  197. package/dist/core/runtime/close.js +37 -16
  198. package/dist/core/runtime/front-door.js +14 -7
  199. package/dist/core/runtime/host.d.ts +14 -34
  200. package/dist/core/runtime/host.js +14 -50
  201. package/dist/core/runtime/launch.d.ts +12 -9
  202. package/dist/core/runtime/launch.js +27 -15
  203. package/dist/core/runtime/lifecycle.d.ts +1 -1
  204. package/dist/core/runtime/lifecycle.js +15 -19
  205. package/dist/core/runtime/nodes.d.ts +0 -55
  206. package/dist/core/runtime/nodes.js +6 -74
  207. package/dist/core/runtime/placement.d.ts +91 -334
  208. package/dist/core/runtime/placement.js +262 -828
  209. package/dist/core/runtime/promote.d.ts +2 -0
  210. package/dist/core/runtime/promote.js +24 -6
  211. package/dist/core/runtime/recap.d.ts +8 -0
  212. package/dist/core/runtime/recap.js +107 -0
  213. package/dist/core/runtime/recycle.js +25 -61
  214. package/dist/core/runtime/reset.d.ts +43 -37
  215. package/dist/core/runtime/reset.js +131 -218
  216. package/dist/core/runtime/revive.d.ts +9 -29
  217. package/dist/core/runtime/revive.js +45 -135
  218. package/dist/core/runtime/spawn.d.ts +15 -11
  219. package/dist/core/runtime/spawn.js +168 -136
  220. package/dist/core/runtime/surface-bg.d.ts +11 -0
  221. package/dist/core/runtime/surface-bg.js +68 -0
  222. package/dist/core/runtime/tmux.d.ts +15 -43
  223. package/dist/core/runtime/tmux.js +38 -80
  224. package/dist/core/scope.d.ts +1 -2
  225. package/dist/core/scope.js +1 -5
  226. package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.d.ts +1 -0
  227. package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.js +57 -0
  228. package/dist/core/skill-sync/builtins.d.ts +42 -0
  229. package/dist/core/skill-sync/builtins.js +112 -0
  230. package/dist/core/skill-sync/claude-plugins.d.ts +23 -0
  231. package/dist/core/skill-sync/claude-plugins.js +71 -0
  232. package/dist/core/skill-sync/engine.d.ts +42 -0
  233. package/dist/core/skill-sync/engine.js +633 -0
  234. package/dist/core/skill-sync/export.d.ts +23 -0
  235. package/dist/core/skill-sync/export.js +86 -0
  236. package/dist/core/skill-sync/manifest.d.ts +64 -0
  237. package/dist/core/skill-sync/manifest.js +181 -0
  238. package/dist/core/skill-sync/profile.d.ts +76 -0
  239. package/dist/core/skill-sync/profile.js +173 -0
  240. package/dist/core/skill-sync/snapshot.d.ts +57 -0
  241. package/dist/core/skill-sync/snapshot.js +120 -0
  242. package/dist/core/spawn.d.ts +14 -0
  243. package/dist/core/spawn.js +29 -9
  244. package/dist/core/substrate/index.d.ts +2 -2
  245. package/dist/core/substrate/index.js +7 -7
  246. package/dist/core/substrate/injected-store.d.ts +10 -0
  247. package/dist/core/substrate/injected-store.js +55 -0
  248. package/dist/core/substrate/on-read.js +68 -6
  249. package/dist/core/substrate/render.d.ts +8 -11
  250. package/dist/core/substrate/render.js +29 -43
  251. package/dist/core/substrate/schema.d.ts +16 -11
  252. package/dist/core/substrate/schema.js +32 -31
  253. package/dist/core/tui/host.d.ts +12 -4
  254. package/dist/core/tui/host.js +280 -149
  255. package/dist/core/view/bridge.d.ts +10 -0
  256. package/dist/core/view/bridge.js +31 -0
  257. package/dist/core/view/chrome.d.ts +9 -0
  258. package/dist/core/view/chrome.js +22 -0
  259. package/dist/core/view/contract.d.ts +171 -0
  260. package/dist/core/view/contract.js +23 -0
  261. package/dist/core/view/loader.d.ts +31 -0
  262. package/dist/core/view/loader.js +188 -0
  263. package/dist/core/view/transport-local.d.ts +7 -0
  264. package/dist/core/view/transport-local.js +70 -0
  265. package/dist/core/view/transport.d.ts +4 -0
  266. package/dist/core/view/transport.js +15 -0
  267. package/dist/daemon/crtrd.d.ts +38 -27
  268. package/dist/daemon/crtrd.js +303 -376
  269. package/dist/daemon/manage.js +6 -1
  270. package/dist/index.js +1 -1
  271. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +28 -20
  272. package/dist/pi-extensions/canvas-context-intro.js +4 -4
  273. package/dist/pi-extensions/canvas-doc-substrate.js +29 -20
  274. package/dist/pi-extensions/canvas-nav.js +35 -385
  275. package/dist/pi-extensions/canvas-recap.d.ts +37 -0
  276. package/dist/pi-extensions/canvas-recap.js +236 -0
  277. package/dist/pi-extensions/canvas-resume.js +2 -1
  278. package/dist/pi-extensions/canvas-stophook.d.ts +15 -13
  279. package/dist/pi-extensions/canvas-stophook.js +119 -103
  280. package/dist/pi-extensions/canvas-view.js +2 -1
  281. package/dist/pi-extensions/widget-order-bus.d.ts +6 -0
  282. package/dist/pi-extensions/widget-order-bus.js +34 -0
  283. package/dist/prompts/view.d.ts +2 -2
  284. package/dist/prompts/view.js +145 -73
  285. package/dist/types.d.ts +1 -6
  286. package/dist/types.js +1 -3
  287. package/dist/web/ViewChrome.d.ts +7 -0
  288. package/dist/web/ViewChrome.js +28 -0
  289. package/dist/web/ViewPane.d.ts +39 -0
  290. package/dist/web/ViewPane.js +48 -0
  291. package/dist/web/index.d.ts +6 -0
  292. package/dist/web/index.js +16 -0
  293. package/dist/web/runtime.d.ts +39 -0
  294. package/dist/web/runtime.js +133 -0
  295. package/dist/web/states.d.ts +24 -0
  296. package/dist/web/states.js +24 -0
  297. package/dist/web/transport-http.d.ts +5 -0
  298. package/dist/web/transport-http.js +28 -0
  299. package/dist/web-client/assets/index-BUvQb4hR.css +2 -0
  300. package/dist/web-client/assets/index-ClLQXYAE.js +10 -0
  301. package/dist/web-client/index.html +13 -0
  302. package/package.json +20 -6
  303. package/dist/builtin-views/canvas/client.mjs +0 -303
  304. package/dist/builtin-views/canvas/view.mjs +0 -576
  305. package/dist/builtin-views/git-pr/client.mjs +0 -440
  306. package/dist/builtin-views/git-pr/view.mjs +0 -675
  307. package/dist/builtin-views/inbox/sources/gmail.mjs +0 -965
  308. package/dist/builtin-views/inbox/sources/linkedin.mjs +0 -427
  309. package/dist/builtin-views/inbox/view.mjs +0 -889
  310. package/dist/builtin-views/linkedin/client.mjs +0 -610
  311. package/dist/builtin-views/linkedin/view.mjs +0 -1171
  312. package/dist/commands/pkg/bridge.d.ts +0 -1
  313. package/dist/commands/pkg/bridge.js +0 -137
  314. package/dist/commands/skill/author.d.ts +0 -3
  315. package/dist/commands/skill/author.js +0 -140
  316. package/dist/commands/skill/shared.d.ts +0 -3
  317. package/dist/commands/skill/shared.js +0 -19
  318. package/dist/commands/skill.d.ts +0 -2
  319. package/dist/commands/skill.js +0 -21
  320. package/dist/commands/tmux-spread.d.ts +0 -2
  321. package/dist/commands/tmux-spread.js +0 -144
  322. package/dist/core/__tests__/full/daemon-liveness-pane.full.test.js +0 -292
  323. package/dist/core/__tests__/full/placement-focus.test.js +0 -309
  324. package/dist/core/__tests__/full/placement-reconcile.test.js +0 -212
  325. package/dist/core/__tests__/full/placement-revive.test.js +0 -238
  326. package/dist/core/__tests__/full/placement-teardown.test.js +0 -221
  327. package/dist/core/__tests__/home-session.test.js +0 -186
  328. package/dist/core/__tests__/relaunch.test.js +0 -335
  329. package/dist/core/bridge-map.d.ts +0 -19
  330. package/dist/core/bridge-map.js +0 -73
  331. package/dist/core/tui/contract.d.ts +0 -83
  332. package/dist/core/tui/contract.js +0 -8
  333. package/dist/core/tui/loader.d.ts +0 -16
  334. package/dist/core/tui/loader.js +0 -94
  335. package/dist/prompts/skill.d.ts +0 -2
  336. package/dist/prompts/skill.js +0 -650
  337. /package/dist/{core/__tests__/full/daemon-liveness-pane.full.test.d.ts → clients/attach/__tests__/action-parity.test.d.ts} +0 -0
  338. /package/dist/{core/__tests__/full/placement-focus.test.d.ts → clients/attach/__tests__/clipboard-image.test.d.ts} +0 -0
  339. /package/dist/{core/__tests__/full/placement-reconcile.test.d.ts → clients/attach/__tests__/editor-newline.test.d.ts} +0 -0
  340. /package/dist/{core/__tests__/full/placement-revive.test.d.ts → clients/attach/__tests__/git-info.test.d.ts} +0 -0
  341. /package/dist/{core/__tests__/full/placement-teardown.test.d.ts → clients/attach/__tests__/onrequest-wired.test.d.ts} +0 -0
  342. /package/dist/{core/__tests__/home-session.test.d.ts → clients/attach/__tests__/reconnect-giveup.test.d.ts} +0 -0
  343. /package/dist/{core/__tests__/relaunch.test.d.ts → clients/attach/__tests__/slash-quit-copy.test.d.ts} +0 -0
@@ -0,0 +1,69 @@
1
+ // @ts-check
2
+ /**
3
+ * LinkedIn Messages — the text presenter (`dump`) for the non-TTY / piped path
4
+ * (`crtr view run linkedin | cat`). Node-only, no ANSI. A plain snapshot of the
5
+ * current state; the host threads its current banner via `ctx`. Surfaces the
6
+ * guided recovery panel (its typed `display`) + the banner so the static path
7
+ * shows guidance, not a blank screen.
8
+ *
9
+ * @module linkedin/text
10
+ */
11
+
12
+ import { relTimestamp, truncate, padEnd, sortConvos } from './core.mjs';
13
+
14
+ /** @typedef {import('./core.mjs').LiState} LiState */
15
+
16
+ /** @param {string|string[]|null|undefined} v @returns {string[]} */
17
+ function toLinesArr(v) {
18
+ if (v == null) return [];
19
+ if (Array.isArray(v)) return v.map((s) => String(s == null ? '' : s));
20
+ return [String(v)];
21
+ }
22
+
23
+ /**
24
+ * Plain-text snapshot for the non-TTY / piped path.
25
+ * @param {LiState} state
26
+ * @param {import('../../core/view/contract.js').DumpContext} [ctx]
27
+ * @returns {string}
28
+ */
29
+ export function dump(state, ctx) {
30
+ const banner = ctx && ctx.banner ? ctx.banner : null;
31
+ const sigil = (lvl) => (lvl === 'error' ? '✗' : lvl === 'action' ? '▸' : 'ℹ');
32
+ /** @type {string[]} */
33
+ const lines = [];
34
+ let n = 0;
35
+ for (const c of state.convos) if (c.unread) n++;
36
+ lines.push('LinkedIn Messages' + (n ? ` · ${n} unread` : ''), '');
37
+
38
+ if (state.recovery) {
39
+ const d = state.recovery.display;
40
+ lines.push(d.headline);
41
+ for (const e of toLinesArr(d.explanation)) if (e) lines.push(' ' + e);
42
+ if (d.nextStep) lines.push(' → ' + d.nextStep);
43
+ if (banner) lines.push(' ' + sigil(banner.level) + ' ' + banner.msg);
44
+ return lines.join('\n');
45
+ }
46
+
47
+ if (state.convos.length === 0) {
48
+ if (banner) lines.push(sigil(banner.level) + ' ' + banner.msg);
49
+ else lines.push(state.lastFetch === 0 ? '(loading…)' : '✓ All caught up — no conversations.');
50
+ } else {
51
+ for (const c of sortConvos(state.convos)) {
52
+ const badge = c.unread ? '●' : ' ';
53
+ const snip = truncate((c.lastMessage || '').replace(/\s+/g, ' ').trim(), 56);
54
+ lines.push(`[${badge}] ${padEnd(c.name || 'Unknown', 20)} ${padEnd(snip, 56)} ${relTimestamp(c.ts)}`);
55
+ }
56
+ }
57
+
58
+ if (state.openUrn && state.thread.length) {
59
+ const convo = state.convos.find((c) => c.urn === state.openUrn);
60
+ lines.push('', `— ${convo ? convo.name : state.openUrn} —`);
61
+ for (const m of state.thread) {
62
+ const who = m.fromMe ? 'You' : m.sender || 'Them';
63
+ lines.push(`${who}: ${(m.text || '').replace(/\s+/g, ' ').trim()}`);
64
+ }
65
+ }
66
+
67
+ if (banner && state.convos.length) lines.push('', sigil(banner.level) + ' ' + banner.msg);
68
+ return lines.join('\n');
69
+ }
@@ -0,0 +1,427 @@
1
+ // @ts-check
2
+ /**
3
+ * LinkedIn Messages — the TUI presenter (`render` + `keymap`) for the `linkedin`
4
+ * view. Node-only (it uses the host's `Draw` API + the `_lib/states.mjs` draw
5
+ * helpers). All state + behavior live in `core.mjs`; this is a pure read of
6
+ * state. Keystrokes map to named intents via `keymap`.
7
+ *
8
+ * The recovery / degraded panels render the typed `SourceError.display` VERBATIM
9
+ * (the contract display/kind split): the presenter maps only `display.level` →
10
+ * glyph + hue, never branching on `kind` and never hardcoding error copy.
11
+ *
12
+ * SGR discipline (§2): all hue is NUMERIC SGR codes (36 cyan, 33 yellow, 32
13
+ * green, 31 red, 90 grey, bg 236); every colored element pairs hue with a glyph
14
+ * or weight so it survives NO_COLOR / dumb terminals.
15
+ *
16
+ * @module linkedin/tui
17
+ */
18
+
19
+ import { relTimestamp, EMOJIS } from './core.mjs';
20
+ import { loadingState, emptyState, notReadyState } from '../_lib/states.mjs';
21
+
22
+ /** @typedef {import('./core.mjs').LiState} LiState */
23
+ /** @typedef {import('./core.mjs').Conversation} Conversation */
24
+ /** @typedef {import('./core.mjs').Message} Message */
25
+ /** @typedef {import('../../core/tui/draw.js').Draw} Draw */
26
+ /** @typedef {import('../../core/tui/draw.js').Rect} Rect */
27
+ /** @typedef {import('../../core/tui/draw.js').Span} Span */
28
+ /** @typedef {import('../../core/tui/draw.js').ListItemRow} ListItemRow */
29
+
30
+ /** Spinner frames for the auto-progress panel glyph (animates via the busy-tick). */
31
+ const SPINNER = ['⟳', '⟲'];
32
+
33
+ const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
34
+
35
+ // ── Helpers (presentation-only) ───────────────────────────────────────────────
36
+
37
+ /** @param {string|string[]|null|undefined} v @returns {string[]} */
38
+ function toLinesArr(v) {
39
+ if (v == null) return [];
40
+ if (Array.isArray(v)) return v.map((s) => String(s == null ? '' : s));
41
+ return [String(v)];
42
+ }
43
+
44
+ /** Visible (column) width of a span group. @param {Span[]} spans */
45
+ function spanWidth(spans) {
46
+ let n = 0;
47
+ for (const s of spans) n += Array.from(s.text).length;
48
+ return n;
49
+ }
50
+
51
+ /** Calendar-day key for day-divider grouping. @param {number} ts @returns {string} */
52
+ function dayKey(ts) {
53
+ if (!ts) return '';
54
+ const d = new Date(ts);
55
+ return `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
56
+ }
57
+
58
+ /** Human day label for the divider: Today / Yesterday / Mon D / Mon D, YYYY. */
59
+ function dayLabel(ts, now = Date.now()) {
60
+ const d = new Date(ts);
61
+ const n = new Date(now);
62
+ const same = (a, b) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
63
+ if (same(d, n)) return 'Today';
64
+ if (same(d, new Date(now - 86400000))) return 'Yesterday';
65
+ const mon = MONTHS[d.getMonth()] || '';
66
+ if (d.getFullYear() === n.getFullYear()) return `${mon} ${d.getDate()}`;
67
+ return `${mon} ${d.getDate()}, ${d.getFullYear()}`;
68
+ }
69
+
70
+ /**
71
+ * Word-wrap text to a width (also hard-splits over-long words). Preserves
72
+ * explicit newlines as paragraph breaks.
73
+ * @param {string} text @param {number} width @returns {string[]}
74
+ */
75
+ function wrapText(text, width) {
76
+ const w = Math.max(1, width | 0);
77
+ /** @type {string[]} */
78
+ const out = [];
79
+ const paragraphs = String(text == null ? '' : text).split('\n');
80
+ for (const para of paragraphs) {
81
+ if (para.trim() === '') {
82
+ out.push('');
83
+ continue;
84
+ }
85
+ let line = '';
86
+ for (const word of para.split(/\s+/).filter(Boolean)) {
87
+ let wd = word;
88
+ while (wd.length > w) {
89
+ if (line) {
90
+ out.push(line);
91
+ line = '';
92
+ }
93
+ out.push(wd.slice(0, w));
94
+ wd = wd.slice(w);
95
+ }
96
+ if (line === '') line = wd;
97
+ else if (line.length + 1 + wd.length <= w) line += ' ' + wd;
98
+ else {
99
+ out.push(line);
100
+ line = wd;
101
+ }
102
+ }
103
+ if (line !== '') out.push(line);
104
+ }
105
+ return out;
106
+ }
107
+
108
+ /**
109
+ * Place a vertically + horizontally centered stack of span-lines in `rect`.
110
+ * @param {Draw} draw @param {Rect} rect @param {Span[][]} lines
111
+ */
112
+ function centeredStack(draw, rect, lines) {
113
+ if (!rect || rect.width <= 0 || rect.height <= 0 || lines.length === 0) return;
114
+ const start = rect.row + Math.max(0, Math.floor((rect.height - lines.length) / 2));
115
+ lines.forEach((spans, i) => {
116
+ const row = start + i;
117
+ if (row < rect.row || row >= rect.row + rect.height) return;
118
+ const w = spanWidth(spans);
119
+ const col = rect.col + Math.max(0, Math.floor((rect.width - w) / 2));
120
+ draw.spans(row, col, spans, rect.col + rect.width - col);
121
+ });
122
+ }
123
+
124
+ /**
125
+ * Split the content rect into the 1:2 two-pane layout, drawing the `vline` rule.
126
+ * @param {Draw} draw @param {Rect} content
127
+ * @returns {{left: Rect, right: Rect}}
128
+ */
129
+ function splitPanes(draw, content) {
130
+ const cols = draw.columns(content, [1, 2]);
131
+ const l = cols[0];
132
+ const r = cols[1];
133
+ const vcol = r.col;
134
+ draw.vline(vcol, content.row, content.row + content.height);
135
+ const left = { row: content.row, col: l.col, width: Math.max(0, vcol - l.col - 1), height: content.height };
136
+ const right = { row: content.row, col: vcol + 2, width: Math.max(0, r.col + r.width - (vcol + 2)), height: content.height };
137
+ return { left, right };
138
+ }
139
+
140
+ // ── Recovery panel (renders display.* VERBATIM; level → glyph/hue) ────────────
141
+
142
+ /**
143
+ * Map a SourceError display level to a panel glyph + numeric SGR hue.
144
+ * @param {import('../../core/view/contract.js').BannerLevel} level
145
+ */
146
+ function levelGlyph(level) {
147
+ if (level === 'action') return { glyph: '⚠', fg: '33' }; // yellow attention
148
+ if (level === 'info') return { glyph: 'ℹ', fg: '36' }; // cyan notice
149
+ return { glyph: '⚠', fg: '31' }; // red error
150
+ }
151
+
152
+ /**
153
+ * The guided-recovery full-content takeover. The copy comes from the typed
154
+ * SourceError display VERBATIM; only the glyph/hue are mapped off `display.level`
155
+ * (or the live spinner frame on an auto-progress branch).
156
+ * @param {LiState} state @param {Draw} draw @param {Rect} content
157
+ */
158
+ function renderRecovery(state, draw, content) {
159
+ const rec = state.recovery;
160
+ if (!rec) return;
161
+ const d = rec.display;
162
+ let glyph;
163
+ let glyphFg;
164
+ let explanation = d.explanation;
165
+ if (rec.spinner) {
166
+ glyph = SPINNER[Math.floor(Date.now() / 240) % SPINNER.length] || '⟳';
167
+ glyphFg = '36'; // cyan working
168
+ const secs = Math.max(0, Math.floor((Date.now() - (rec.startedAt || Date.now())) / 1000));
169
+ explanation = toLinesArr(d.explanation).concat([`(${secs}s)`]);
170
+ } else {
171
+ const g = levelGlyph(d.level);
172
+ glyph = g.glyph;
173
+ glyphFg = g.fg;
174
+ }
175
+ notReadyState(draw, content, {
176
+ glyph,
177
+ glyphFg,
178
+ headline: d.headline,
179
+ explanation,
180
+ nextStep: d.nextStep || undefined,
181
+ });
182
+ }
183
+
184
+ /** Two-pane loading skeleton. @param {LiState} state @param {Draw} draw @param {Rect} content */
185
+ function renderLoadingSkeleton(state, draw, content) {
186
+ const { left, right } = splitPanes(draw, content);
187
+ loadingState(draw, left, { rows: Math.min(5, Math.max(1, left.height)) });
188
+ centeredStack(draw, right, [[{ text: 'Loading conversations…', style: { dim: true } }]]);
189
+ }
190
+
191
+ /** Left pane — the conversation list. @param {LiState} state @param {Draw} draw @param {Rect} left */
192
+ function renderConvoList(state, draw, left) {
193
+ if (left.width <= 0 || left.height <= 0) return;
194
+ /** @type {ListItemRow[]} */
195
+ const items = state.convos.map((c, i) => {
196
+ const isCursor = i === state.convCursor;
197
+ /** @type {Span[]} */
198
+ const spans = [
199
+ { text: isCursor ? '▸' : ' ', style: isCursor ? { fg: '36', bold: true } : undefined },
200
+ { text: c.unread ? '●' : ' ', style: c.unread ? { fg: '36', bold: true } : undefined },
201
+ { text: ' ' + (c.name || 'Unknown'), style: c.unread ? { bold: true } : undefined },
202
+ ];
203
+ const snippet = (c.lastMessage || '').replace(/\s+/g, ' ').trim();
204
+ if (snippet) spans.push({ text: ' ' + snippet, style: { dim: true } });
205
+ const ts = relTimestamp(c.ts);
206
+ /** @type {ListItemRow} */
207
+ const item = { spans };
208
+ if (ts) item.right = [{ text: ts, style: { dim: true } }];
209
+ return item;
210
+ });
211
+ const res = draw.list(left, items, state.convCursor, state.convScroll);
212
+ state.convScroll = res.scroll;
213
+ }
214
+
215
+ /**
216
+ * Build the thread's flat visual lines with you-vs-them grouping. Day dividers
217
+ * between date changes; 1 blank spacer between groups. The react target's header
218
+ * gets a cyan ▸ tick.
219
+ * @param {LiState} state @param {number} width @param {number} reactTarget
220
+ * @returns {ListItemRow[]}
221
+ */
222
+ function buildThreadLines(state, width, reactTarget) {
223
+ /** @type {ListItemRow[]} */
224
+ const lines = [];
225
+ let prevDay = null;
226
+ state.thread.forEach((m, idx) => {
227
+ const day = dayKey(m.ts);
228
+ if (m.ts && day !== prevDay) {
229
+ const txt = `── ${dayLabel(m.ts)} ──`;
230
+ const pad = Math.max(0, Math.floor((width - Array.from(txt).length) / 2));
231
+ lines.push({ spans: [{ text: ' '.repeat(pad) + txt, style: { dim: true } }] });
232
+ prevDay = day;
233
+ }
234
+ const tick = idx === reactTarget ? [{ text: '▸ ', style: { fg: '36', bold: true } }] : [];
235
+ const ts = relTimestamp(m.ts);
236
+ const right = ts ? [{ text: ts, style: { dim: true } }] : undefined;
237
+ if (m.fromMe) {
238
+ lines.push({
239
+ spans: [...tick, { text: '▎ ', style: { fg: '32', bold: true } }, { text: 'You', style: { fg: '32', bold: true } }],
240
+ right,
241
+ });
242
+ for (const bl of wrapText(m.text || '', Math.max(1, width - 2))) {
243
+ lines.push({ spans: [{ text: '▎ ', style: { fg: '32' } }, { text: bl }] });
244
+ }
245
+ } else {
246
+ lines.push({ spans: [...tick, { text: m.sender || 'Them', style: { fg: '36', bold: true } }], right });
247
+ for (const bl of wrapText(m.text || '', width)) {
248
+ lines.push({ spans: [{ text: bl }] });
249
+ }
250
+ }
251
+ if (idx < state.thread.length - 1) lines.push({ spans: [{ text: '' }] });
252
+ });
253
+ return lines;
254
+ }
255
+
256
+ /** Paint the thread body (tail-windowed). @param {LiState} state @param {Draw} draw @param {Rect} rect @param {number} reactTarget */
257
+ function renderThreadBody(state, draw, rect, reactTarget) {
258
+ if (rect.width <= 0 || rect.height <= 0) return;
259
+ const lines = buildThreadLines(state, rect.width, reactTarget);
260
+ const start = Math.max(0, lines.length - rect.height);
261
+ state.threadScroll = start;
262
+ let r = rect.row;
263
+ for (let i = start; i < lines.length && r < rect.row + rect.height; i++, r++) {
264
+ const ln = lines[i];
265
+ if (ln.right && ln.right.length) {
266
+ const rw = spanWidth(ln.right);
267
+ draw.spans(r, rect.col, ln.spans, Math.max(0, rect.width - rw - 1));
268
+ draw.spansRight(r, rect.col + rect.width, ln.right, rw);
269
+ } else {
270
+ draw.spans(r, rect.col, ln.spans, rect.width);
271
+ }
272
+ }
273
+ }
274
+
275
+ /** The compose bar (reply mode). @param {LiState} state @param {Draw} draw @param {Rect} right */
276
+ function renderComposer(state, draw, right) {
277
+ const hairRow = right.row + right.height - 3;
278
+ const barRow = right.row + right.height - 2;
279
+ const hintRow = right.row + right.height - 1;
280
+ draw.hline(hairRow, right.col, right.col + right.width);
281
+ const label = '✎ Reply ';
282
+ const labelW = Array.from(label).length;
283
+ const avail = Math.max(1, right.width - labelW - 1);
284
+ let shown = state.draft;
285
+ const arr = Array.from(shown);
286
+ if (arr.length > avail - 1) shown = arr.slice(arr.length - (avail - 1)).join('');
287
+ draw.spans(barRow, right.col, [
288
+ { text: label, style: { fg: '33', bold: true } },
289
+ { text: shown },
290
+ { text: '█', style: { fg: '33' } },
291
+ ], right.width);
292
+ draw.spans(hintRow, right.col, [
293
+ { text: 'enter', style: { bold: true } }, { text: ' send', style: { dim: true } },
294
+ { text: ' · ', style: { dim: true } },
295
+ { text: 'esc', style: { bold: true } }, { text: ' cancel', style: { dim: true } },
296
+ ], right.width);
297
+ }
298
+
299
+ /** The react picker bar (react mode). @param {LiState} state @param {Draw} draw @param {Rect} right */
300
+ function renderReactBar(state, draw, right) {
301
+ const hairRow = right.row + right.height - 3;
302
+ const barRow = right.row + right.height - 2;
303
+ const hintRow = right.row + right.height - 1;
304
+ draw.hline(hairRow, right.col, right.col + right.width);
305
+ /** @type {Span[]} */
306
+ const spans = [{ text: '☺ React ', style: { fg: '33', bold: true } }];
307
+ EMOJIS.forEach((e, i) => {
308
+ if (i === state.reactCursor) {
309
+ spans.push({ text: ' ' });
310
+ spans.push({ text: '[' + e + ']', style: { bg: '236', reverse: true } });
311
+ } else {
312
+ spans.push({ text: ' ' + e + ' ' });
313
+ }
314
+ });
315
+ draw.spans(barRow, right.col, spans, right.width);
316
+ draw.spans(hintRow, right.col, [
317
+ { text: '←/→', style: { bold: true } }, { text: ' pick', style: { dim: true } },
318
+ { text: ' · ', style: { dim: true } },
319
+ { text: 'enter', style: { bold: true } }, { text: ' react', style: { dim: true } },
320
+ { text: ' · ', style: { dim: true } },
321
+ { text: 'esc', style: { bold: true } }, { text: ' cancel', style: { dim: true } },
322
+ ], right.width);
323
+ }
324
+
325
+ /** Right pane — the open thread + the compose/react bar when in a mode. @param {LiState} state @param {Draw} draw @param {Rect} right */
326
+ function renderThread(state, draw, right) {
327
+ if (right.width <= 0 || right.height <= 0) return;
328
+ const openConvo = state.openUrn ? state.convos.find((c) => c.urn === state.openUrn) : null;
329
+
330
+ if (!state.openUrn) {
331
+ centeredStack(draw, right, [
332
+ [{ text: '✉ ', style: { dim: true } }, { text: 'No conversation open', style: { dim: true } }],
333
+ [{ text: '' }],
334
+ [{ text: 'Press ' }, { text: 'Enter', style: { bold: true } }, { text: ' to open a conversation' }],
335
+ ]);
336
+ return;
337
+ }
338
+
339
+ const headerName = openConvo ? openConvo.name : 'Conversation';
340
+ const convTs = openConvo ? relTimestamp(openConvo.ts) : '';
341
+ const rw = convTs ? Array.from(convTs).length : 0;
342
+ draw.spans(right.row, right.col, [{ text: headerName, style: { bold: true } }], Math.max(0, right.width - rw - 1));
343
+ if (convTs) draw.spansRight(right.row, right.col + right.width, [{ text: convTs, style: { dim: true } }], rw);
344
+ draw.hline(right.row + 1, right.col, right.col + right.width);
345
+
346
+ const composing = state.mode === 'reply';
347
+ const reacting = state.mode === 'react';
348
+ const composerRows = composing || reacting ? 3 : 0;
349
+ const bodyTop = right.row + 2;
350
+ const bodyBottom = right.row + right.height - 1 - composerRows;
351
+ const bodyRect = { row: bodyTop, col: right.col, width: right.width, height: Math.max(0, bodyBottom - bodyTop + 1) };
352
+
353
+ if (state.thread.length === 0) {
354
+ centeredStack(draw, bodyRect, [[{ text: 'Loading messages…', style: { dim: true } }]]);
355
+ } else {
356
+ renderThreadBody(state, draw, bodyRect, reacting ? state.thread.length - 1 : -1);
357
+ }
358
+
359
+ if (composing) renderComposer(state, draw, right);
360
+ else if (reacting) renderReactBar(state, draw, right);
361
+ }
362
+
363
+ // ── render ─────────────────────────────────────────────────────────────────────
364
+
365
+ /**
366
+ * Paint the view. Precedence: recovery takeover → loading skeleton → empty
367
+ * reward → the two-pane inbox.
368
+ * @param {LiState} state @param {Draw} draw @param {Rect} content
369
+ */
370
+ export function render(state, draw, content) {
371
+ if (content.width <= 0 || content.height <= 0) return;
372
+ if (state.recovery) {
373
+ renderRecovery(state, draw, content);
374
+ return;
375
+ }
376
+ if (state.convos.length === 0) {
377
+ if (state.lastFetch === 0) {
378
+ renderLoadingSkeleton(state, draw, content);
379
+ return;
380
+ }
381
+ emptyState(draw, content, {
382
+ headline: 'All caught up',
383
+ secondary: ['No conversations in your inbox.', 'Press g to refresh.'],
384
+ });
385
+ return;
386
+ }
387
+ const { left, right } = splitPanes(draw, content);
388
+ renderConvoList(state, draw, left);
389
+ renderThread(state, draw, right);
390
+ }
391
+
392
+ // ── keymap (mode-gated bindings → named intents) ──────────────────────────────
393
+
394
+ /** @param {LiState} s */
395
+ const isList = (s) => s.mode === 'list';
396
+ /** @param {LiState} s */
397
+ const isReply = (s) => s.mode === 'reply';
398
+ /** @param {LiState} s */
399
+ const isReact = (s) => s.mode === 'react';
400
+ /** @param {LiState} s */
401
+ const isComposeOrReact = (s) => s.mode === 'reply' || s.mode === 'react';
402
+
403
+ /**
404
+ * Input→intent map. List-mode navigation + entry into compose/react; the compose
405
+ * bar uses a `capture` binding (the host's line-editor dispatches setDraft with
406
+ * the next draft); react uses h/l + arrows. Footer hints come from these `hint`
407
+ * fields (single source of truth).
408
+ * @type {import('../../core/view/contract.js').KeyBinding<LiState>[]}
409
+ */
410
+ export const keymap = [
411
+ // ── List mode ──
412
+ { keys: ['j', 'down'], intent: 'cursorDown', when: isList, hint: { keys: 'j/k', label: 'move' } },
413
+ { keys: ['k', 'up'], intent: 'cursorUp', when: isList },
414
+ { keys: ['enter', 'return'], intent: 'openThread', payload: (s) => s.convCursor, when: isList, hint: { keys: 'enter', label: 'open' } },
415
+ { keys: ['r'], intent: 'startReply', when: isList, hint: { keys: 'r', label: 'reply' } },
416
+ { keys: ['e'], intent: 'startReact', when: isList, hint: { keys: 'e', label: 'react' } },
417
+ { keys: ['g'], intent: 'refresh', when: isList, hint: { keys: 'g', label: 'refresh' } },
418
+ { keys: ['q'], intent: 'quit', when: isList, hint: { keys: 'q', label: 'quit' } },
419
+ // ── Reply mode ── (capture → setDraft; enter sends; esc cancels)
420
+ { capture: 'setDraft', when: isReply },
421
+ { keys: ['enter', 'return'], intent: 'submitReply', when: isReply },
422
+ { keys: ['escape', 'esc'], intent: 'cancelCompose', when: isComposeOrReact },
423
+ // ── React mode ──
424
+ { keys: ['left', 'h'], intent: 'reactPrev', when: isReact },
425
+ { keys: ['right', 'l'], intent: 'reactNext', when: isReact },
426
+ { keys: ['enter', 'return'], intent: 'submitReact', when: isReact },
427
+ ];