@crouton-kit/crouter 0.3.28 → 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 (322) 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 +5 -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__/slash-quit-copy.test.js +42 -0
  49. package/dist/clients/attach/__tests__/titled-editor.test.js +39 -0
  50. package/dist/clients/attach/attach-cmd.js +282 -46
  51. package/dist/clients/attach/auth-pickers.d.ts +9 -0
  52. package/dist/clients/attach/auth-pickers.js +194 -0
  53. package/dist/clients/attach/canvas-panels.d.ts +4 -2
  54. package/dist/clients/attach/canvas-panels.js +45 -17
  55. package/dist/clients/attach/chat-view.d.ts +27 -2
  56. package/dist/clients/attach/chat-view.js +98 -9
  57. package/dist/clients/attach/clipboard-image.d.ts +47 -8
  58. package/dist/clients/attach/clipboard-image.js +150 -26
  59. package/dist/clients/attach/clipboard-text.d.ts +3 -0
  60. package/dist/clients/attach/clipboard-text.js +39 -0
  61. package/dist/clients/attach/config-load.d.ts +54 -7
  62. package/dist/clients/attach/config-load.js +99 -3
  63. package/dist/clients/attach/context-message.d.ts +12 -0
  64. package/dist/clients/attach/context-message.js +72 -0
  65. package/dist/clients/attach/git-info.d.ts +20 -0
  66. package/dist/clients/attach/git-info.js +44 -0
  67. package/dist/clients/attach/graph-overlay.d.ts +13 -4
  68. package/dist/clients/attach/graph-overlay.js +59 -19
  69. package/dist/clients/attach/input-controller.d.ts +75 -13
  70. package/dist/clients/attach/input-controller.js +305 -67
  71. package/dist/clients/attach/pickers.d.ts +58 -0
  72. package/dist/clients/attach/pickers.js +171 -0
  73. package/dist/clients/attach/slash-commands.d.ts +22 -0
  74. package/dist/clients/attach/slash-commands.js +91 -25
  75. package/dist/clients/attach/titled-editor.d.ts +33 -0
  76. package/dist/clients/attach/titled-editor.js +84 -0
  77. package/dist/clients/attach/view-socket.d.ts +18 -1
  78. package/dist/clients/attach/view-socket.js +70 -1
  79. package/dist/clients/web/dev-server.d.ts +7 -0
  80. package/dist/clients/web/dev-server.js +59 -0
  81. package/dist/clients/web/events.d.ts +14 -0
  82. package/dist/clients/web/events.js +151 -0
  83. package/dist/clients/web/server.d.ts +18 -0
  84. package/dist/clients/web/server.js +450 -0
  85. package/dist/clients/web/web-cmd.d.ts +2 -0
  86. package/dist/clients/web/web-cmd.js +120 -0
  87. package/dist/commands/canvas.js +1 -2
  88. package/dist/commands/chord.js +1 -1
  89. package/dist/commands/dashboard.js +5 -1
  90. package/dist/commands/memory/__tests__/lint-schema.test.js +6 -6
  91. package/dist/commands/memory/lint.js +14 -2
  92. package/dist/commands/memory/read.js +2 -2
  93. package/dist/commands/memory/shared.d.ts +7 -1
  94. package/dist/commands/memory/shared.js +18 -3
  95. package/dist/commands/memory/write.js +8 -4
  96. package/dist/commands/memory.js +4 -4
  97. package/dist/commands/node.d.ts +1 -1
  98. package/dist/commands/node.js +64 -65
  99. package/dist/commands/pkg.js +1 -2
  100. package/dist/commands/revive.js +11 -3
  101. package/dist/commands/sys/sync.d.ts +1 -0
  102. package/dist/commands/sys/sync.js +187 -0
  103. package/dist/commands/sys.js +3 -2
  104. package/dist/commands/view-cycle.js +2 -2
  105. package/dist/commands/view-list.js +8 -8
  106. package/dist/commands/view-new.js +21 -17
  107. package/dist/commands/view-pick.js +1 -1
  108. package/dist/commands/view-run.js +35 -14
  109. package/dist/commands/view.js +8 -6
  110. package/dist/commands/workspace.d.ts +2 -0
  111. package/dist/commands/workspace.js +161 -0
  112. package/dist/core/__tests__/broker-double-spawn.test.d.ts +1 -0
  113. package/dist/core/__tests__/broker-double-spawn.test.js +143 -0
  114. package/dist/core/__tests__/broker-fork-seam.test.d.ts +1 -0
  115. package/dist/core/__tests__/broker-fork-seam.test.js +102 -0
  116. package/dist/core/__tests__/broker-sdk-wiring.test.js +2 -0
  117. package/dist/core/__tests__/child-death-wake.test.js +56 -111
  118. package/dist/core/__tests__/close.test.js +14 -20
  119. package/dist/core/__tests__/context-intro.test.js +19 -19
  120. package/dist/core/__tests__/daemon-boot.test.js +30 -23
  121. package/dist/core/__tests__/draw-style.test.js +1 -1
  122. package/dist/core/__tests__/error-stall-recycle.test.d.ts +1 -0
  123. package/dist/core/__tests__/error-stall-recycle.test.js +141 -0
  124. package/dist/core/__tests__/fixtures/fake-engine.d.ts +17 -1
  125. package/dist/core/__tests__/fixtures/fake-engine.js +22 -2
  126. package/dist/core/__tests__/fixtures/fake-pi-host.js +2 -2
  127. package/dist/core/__tests__/focuses.test.js +25 -13
  128. package/dist/core/__tests__/fork.test.js +22 -5
  129. package/dist/core/__tests__/full/broker-control-preempt.test.d.ts +1 -0
  130. package/dist/core/__tests__/full/broker-control-preempt.test.js +61 -0
  131. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.d.ts +1 -0
  132. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.js +57 -0
  133. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.d.ts +1 -0
  134. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.js +80 -0
  135. package/dist/core/__tests__/full/broker-pane-resolution.test.js +90 -0
  136. package/dist/core/__tests__/full/cascade-close.test.js +22 -8
  137. package/dist/core/__tests__/full/detach-focus.test.js +66 -169
  138. package/dist/core/__tests__/helpers/harness.d.ts +6 -9
  139. package/dist/core/__tests__/helpers/harness.js +19 -36
  140. package/dist/core/__tests__/human-node-not-supervised.test.d.ts +1 -0
  141. package/dist/core/__tests__/human-node-not-supervised.test.js +83 -0
  142. package/dist/core/__tests__/lifecycle.test.js +1 -19
  143. package/dist/core/__tests__/live-mutation-verbs.test.js +5 -5
  144. package/dist/core/__tests__/memory-resolver.test.js +1 -1
  145. package/dist/core/__tests__/on-read-dedup-resume.test.js +2 -2
  146. package/dist/core/__tests__/refresh-stall-recycle.test.d.ts +1 -0
  147. package/dist/core/__tests__/refresh-stall-recycle.test.js +88 -0
  148. package/dist/core/__tests__/relaunch-root.test.d.ts +1 -0
  149. package/dist/core/__tests__/relaunch-root.test.js +114 -0
  150. package/dist/core/__tests__/reset.test.js +19 -79
  151. package/dist/core/__tests__/steer-note.test.js +9 -7
  152. package/dist/core/__tests__/tmux-surface.test.js +13 -7
  153. package/dist/core/bootstrap.d.ts +0 -3
  154. package/dist/core/bootstrap.js +1 -143
  155. package/dist/core/canvas/browse/__tests__/model.test.js +86 -1
  156. package/dist/core/canvas/browse/__tests__/render.test.js +2 -1
  157. package/dist/core/canvas/browse/app.js +131 -8
  158. package/dist/core/canvas/browse/model.d.ts +23 -5
  159. package/dist/core/canvas/browse/model.js +68 -13
  160. package/dist/core/canvas/browse/render.d.ts +7 -1
  161. package/dist/core/canvas/browse/render.js +178 -47
  162. package/dist/core/canvas/canvas.js +1 -1
  163. package/dist/core/canvas/focuses.d.ts +4 -4
  164. package/dist/core/canvas/focuses.js +7 -7
  165. package/dist/core/canvas/nav-model.js +8 -7
  166. package/dist/core/canvas/render.d.ts +68 -9
  167. package/dist/core/canvas/render.js +195 -61
  168. package/dist/core/canvas/types.d.ts +16 -23
  169. package/dist/core/config.js +1 -4
  170. package/dist/core/memory-resolver.js +43 -5
  171. package/dist/core/runtime/bearings.d.ts +1 -1
  172. package/dist/core/runtime/bearings.js +8 -8
  173. package/dist/core/runtime/branded-host.d.ts +16 -0
  174. package/dist/core/runtime/branded-host.js +127 -0
  175. package/dist/core/runtime/broker-protocol.d.ts +212 -6
  176. package/dist/core/runtime/broker.d.ts +4 -1
  177. package/dist/core/runtime/broker.js +494 -58
  178. package/dist/core/runtime/close.d.ts +12 -2
  179. package/dist/core/runtime/close.js +37 -16
  180. package/dist/core/runtime/front-door.js +6 -14
  181. package/dist/core/runtime/host.d.ts +14 -34
  182. package/dist/core/runtime/host.js +14 -50
  183. package/dist/core/runtime/launch.d.ts +12 -9
  184. package/dist/core/runtime/launch.js +27 -15
  185. package/dist/core/runtime/lifecycle.d.ts +1 -1
  186. package/dist/core/runtime/lifecycle.js +15 -19
  187. package/dist/core/runtime/nodes.d.ts +0 -55
  188. package/dist/core/runtime/nodes.js +6 -74
  189. package/dist/core/runtime/placement.d.ts +91 -335
  190. package/dist/core/runtime/placement.js +262 -840
  191. package/dist/core/runtime/promote.d.ts +2 -0
  192. package/dist/core/runtime/promote.js +24 -6
  193. package/dist/core/runtime/recap.d.ts +8 -0
  194. package/dist/core/runtime/recap.js +107 -0
  195. package/dist/core/runtime/recycle.js +25 -61
  196. package/dist/core/runtime/reset.d.ts +43 -37
  197. package/dist/core/runtime/reset.js +131 -218
  198. package/dist/core/runtime/revive.d.ts +9 -29
  199. package/dist/core/runtime/revive.js +40 -139
  200. package/dist/core/runtime/spawn.d.ts +15 -16
  201. package/dist/core/runtime/spawn.js +165 -194
  202. package/dist/core/runtime/surface-bg.d.ts +11 -0
  203. package/dist/core/runtime/surface-bg.js +68 -0
  204. package/dist/core/runtime/tmux.d.ts +13 -50
  205. package/dist/core/runtime/tmux.js +33 -87
  206. package/dist/core/scope.d.ts +1 -2
  207. package/dist/core/scope.js +1 -5
  208. package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.d.ts +1 -0
  209. package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.js +57 -0
  210. package/dist/core/skill-sync/builtins.d.ts +42 -0
  211. package/dist/core/skill-sync/builtins.js +112 -0
  212. package/dist/core/skill-sync/claude-plugins.d.ts +23 -0
  213. package/dist/core/skill-sync/claude-plugins.js +71 -0
  214. package/dist/core/skill-sync/engine.d.ts +42 -0
  215. package/dist/core/skill-sync/engine.js +633 -0
  216. package/dist/core/skill-sync/export.d.ts +23 -0
  217. package/dist/core/skill-sync/export.js +86 -0
  218. package/dist/core/skill-sync/manifest.d.ts +64 -0
  219. package/dist/core/skill-sync/manifest.js +181 -0
  220. package/dist/core/skill-sync/profile.d.ts +76 -0
  221. package/dist/core/skill-sync/profile.js +173 -0
  222. package/dist/core/skill-sync/snapshot.d.ts +57 -0
  223. package/dist/core/skill-sync/snapshot.js +120 -0
  224. package/dist/core/substrate/index.d.ts +1 -1
  225. package/dist/core/substrate/index.js +1 -1
  226. package/dist/core/substrate/injected-store.js +3 -3
  227. package/dist/core/substrate/on-read.js +68 -6
  228. package/dist/core/substrate/render.d.ts +8 -11
  229. package/dist/core/substrate/render.js +29 -43
  230. package/dist/core/substrate/schema.d.ts +10 -3
  231. package/dist/core/substrate/schema.js +6 -3
  232. package/dist/core/tui/host.d.ts +12 -4
  233. package/dist/core/tui/host.js +280 -149
  234. package/dist/core/view/bridge.d.ts +10 -0
  235. package/dist/core/view/bridge.js +31 -0
  236. package/dist/core/view/chrome.d.ts +9 -0
  237. package/dist/core/view/chrome.js +22 -0
  238. package/dist/core/view/contract.d.ts +171 -0
  239. package/dist/core/view/contract.js +23 -0
  240. package/dist/core/view/loader.d.ts +31 -0
  241. package/dist/core/view/loader.js +188 -0
  242. package/dist/core/view/transport-local.d.ts +7 -0
  243. package/dist/core/view/transport-local.js +70 -0
  244. package/dist/core/view/transport.d.ts +4 -0
  245. package/dist/core/view/transport.js +15 -0
  246. package/dist/daemon/crtrd.d.ts +38 -27
  247. package/dist/daemon/crtrd.js +303 -376
  248. package/dist/daemon/manage.js +6 -1
  249. package/dist/index.js +1 -1
  250. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +28 -20
  251. package/dist/pi-extensions/canvas-context-intro.js +4 -4
  252. package/dist/pi-extensions/canvas-doc-substrate.js +16 -16
  253. package/dist/pi-extensions/canvas-nav.js +5 -0
  254. package/dist/pi-extensions/canvas-recap.d.ts +37 -0
  255. package/dist/pi-extensions/canvas-recap.js +236 -0
  256. package/dist/pi-extensions/canvas-resume.js +2 -1
  257. package/dist/pi-extensions/canvas-stophook.d.ts +14 -12
  258. package/dist/pi-extensions/canvas-stophook.js +87 -101
  259. package/dist/pi-extensions/canvas-view.js +2 -1
  260. package/dist/pi-extensions/widget-order-bus.d.ts +6 -0
  261. package/dist/pi-extensions/widget-order-bus.js +34 -0
  262. package/dist/prompts/view.d.ts +2 -2
  263. package/dist/prompts/view.js +145 -73
  264. package/dist/types.d.ts +1 -6
  265. package/dist/types.js +1 -3
  266. package/dist/web/ViewChrome.d.ts +7 -0
  267. package/dist/web/ViewChrome.js +28 -0
  268. package/dist/web/ViewPane.d.ts +39 -0
  269. package/dist/web/ViewPane.js +48 -0
  270. package/dist/web/index.d.ts +6 -0
  271. package/dist/web/index.js +16 -0
  272. package/dist/web/runtime.d.ts +39 -0
  273. package/dist/web/runtime.js +133 -0
  274. package/dist/web/states.d.ts +24 -0
  275. package/dist/web/states.js +24 -0
  276. package/dist/web/transport-http.d.ts +5 -0
  277. package/dist/web/transport-http.js +28 -0
  278. package/dist/web-client/assets/index-BUvQb4hR.css +2 -0
  279. package/dist/web-client/assets/index-ClLQXYAE.js +10 -0
  280. package/dist/web-client/index.html +13 -0
  281. package/package.json +20 -6
  282. package/dist/builtin-views/canvas/client.mjs +0 -303
  283. package/dist/builtin-views/canvas/view.mjs +0 -576
  284. package/dist/builtin-views/git-pr/client.mjs +0 -440
  285. package/dist/builtin-views/git-pr/view.mjs +0 -675
  286. package/dist/builtin-views/inbox/sources/gmail.mjs +0 -965
  287. package/dist/builtin-views/inbox/sources/linkedin.mjs +0 -427
  288. package/dist/builtin-views/inbox/view.mjs +0 -889
  289. package/dist/builtin-views/linkedin/client.mjs +0 -610
  290. package/dist/builtin-views/linkedin/view.mjs +0 -1171
  291. package/dist/commands/pkg/bridge.d.ts +0 -1
  292. package/dist/commands/pkg/bridge.js +0 -137
  293. package/dist/commands/skill/author.d.ts +0 -3
  294. package/dist/commands/skill/author.js +0 -140
  295. package/dist/commands/skill/shared.d.ts +0 -3
  296. package/dist/commands/skill/shared.js +0 -19
  297. package/dist/commands/skill.d.ts +0 -2
  298. package/dist/commands/skill.js +0 -21
  299. package/dist/commands/tmux-spread.d.ts +0 -2
  300. package/dist/commands/tmux-spread.js +0 -144
  301. package/dist/core/__tests__/full/daemon-liveness-pane.full.test.js +0 -292
  302. package/dist/core/__tests__/full/placement-focus.test.js +0 -309
  303. package/dist/core/__tests__/full/placement-reconcile.test.js +0 -212
  304. package/dist/core/__tests__/full/placement-revive.test.js +0 -238
  305. package/dist/core/__tests__/full/placement-teardown.test.js +0 -297
  306. package/dist/core/__tests__/home-session.test.js +0 -186
  307. package/dist/core/__tests__/relaunch.test.js +0 -335
  308. package/dist/core/bridge-map.d.ts +0 -19
  309. package/dist/core/bridge-map.js +0 -73
  310. package/dist/core/tui/contract.d.ts +0 -83
  311. package/dist/core/tui/contract.js +0 -8
  312. package/dist/core/tui/loader.d.ts +0 -16
  313. package/dist/core/tui/loader.js +0 -94
  314. package/dist/prompts/skill.d.ts +0 -2
  315. package/dist/prompts/skill.js +0 -650
  316. /package/dist/{core/__tests__/full/daemon-liveness-pane.full.test.d.ts → clients/attach/__tests__/action-parity.test.d.ts} +0 -0
  317. /package/dist/{core/__tests__/full/placement-focus.test.d.ts → clients/attach/__tests__/clipboard-image.test.d.ts} +0 -0
  318. /package/dist/{core/__tests__/full/placement-reconcile.test.d.ts → clients/attach/__tests__/editor-newline.test.d.ts} +0 -0
  319. /package/dist/{core/__tests__/full/placement-revive.test.d.ts → clients/attach/__tests__/git-info.test.d.ts} +0 -0
  320. /package/dist/{core/__tests__/full/placement-teardown.test.d.ts → clients/attach/__tests__/onrequest-wired.test.d.ts} +0 -0
  321. /package/dist/{core/__tests__/home-session.test.d.ts → clients/attach/__tests__/slash-quit-copy.test.d.ts} +0 -0
  322. /package/dist/{core/__tests__/relaunch.test.d.ts → clients/attach/__tests__/titled-editor.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
+ ];