@crouton-kit/crouter 0.3.28 → 0.3.30

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 (324) hide show
  1. package/dist/build-root.d.ts +15 -7
  2. package/dist/build-root.js +48 -34
  3. package/dist/builtin-memory/crouter-development/marketplaces.md +7 -6
  4. package/dist/builtin-memory/crouter-development/personas/base-prompt.md +1 -1
  5. package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +1 -1
  6. package/dist/builtin-memory/crouter-development/personas.md +1 -1
  7. package/dist/builtin-memory/crouter-development/plugins.md +20 -18
  8. package/dist/builtin-memory/design.md +1 -1
  9. package/dist/builtin-memory/development.md +1 -1
  10. package/dist/builtin-memory/internal/INDEX.md +1 -1
  11. package/dist/builtin-memory/internal/examples/INDEX.md +1 -1
  12. package/dist/builtin-memory/internal/examples/imessage-assistant.md +2 -2
  13. package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
  14. package/dist/builtin-memory/internal/storage-tiers.md +1 -1
  15. package/dist/builtin-memory/planning.md +1 -1
  16. package/dist/builtin-memory/spec.md +6 -4
  17. package/dist/builtin-personas/orchestration-kernel.md +4 -5
  18. package/dist/builtin-personas/runtime-base.md +5 -4
  19. package/dist/builtin-personas/spec/PERSONA.md +8 -4
  20. package/dist/builtin-personas/spec/orchestrator.md +5 -3
  21. package/dist/builtin-personas/spec/requirements/PERSONA.md +10 -0
  22. package/dist/builtin-views/canvas/core.mjs +586 -0
  23. package/dist/builtin-views/canvas/text.mjs +58 -0
  24. package/dist/builtin-views/canvas/tui.mjs +165 -0
  25. package/dist/builtin-views/canvas/web.jsx +120 -0
  26. package/dist/builtin-views/git-pr/core.mjs +673 -0
  27. package/dist/builtin-views/git-pr/text.mjs +84 -0
  28. package/dist/builtin-views/git-pr/tui.mjs +302 -0
  29. package/dist/builtin-views/git-pr/web.jsx +216 -0
  30. package/dist/builtin-views/inbox/_lib/render.mjs +1 -3
  31. package/dist/builtin-views/inbox/core.mjs +1273 -0
  32. package/dist/builtin-views/inbox/text.mjs +73 -0
  33. package/dist/builtin-views/inbox/tui.mjs +312 -0
  34. package/dist/builtin-views/inbox/web.jsx +188 -0
  35. package/dist/builtin-views/linkedin/core.mjs +906 -0
  36. package/dist/builtin-views/linkedin/text.mjs +69 -0
  37. package/dist/builtin-views/linkedin/tui.mjs +427 -0
  38. package/dist/builtin-views/linkedin/web.jsx +206 -0
  39. package/dist/builtin-views/workspace-sidebar/core.mjs +621 -0
  40. package/dist/builtin-views/workspace-sidebar/text.mjs +53 -0
  41. package/dist/builtin-views/workspace-sidebar/tui.mjs +142 -0
  42. package/dist/builtin-views/workspace-sidebar/web.jsx +109 -0
  43. package/dist/cli.js +22 -17
  44. package/dist/clients/attach/__tests__/action-parity.test.js +48 -0
  45. package/dist/clients/attach/__tests__/clipboard-image.test.js +76 -0
  46. package/dist/clients/attach/__tests__/editor-newline.test.js +57 -0
  47. package/dist/clients/attach/__tests__/git-info.test.js +31 -0
  48. package/dist/clients/attach/__tests__/onrequest-wired.test.js +20 -0
  49. package/dist/clients/attach/__tests__/slash-quit-copy.test.js +42 -0
  50. package/dist/clients/attach/__tests__/titled-editor.test.js +39 -0
  51. package/dist/clients/attach/attach-cmd.js +282 -46
  52. package/dist/clients/attach/auth-pickers.d.ts +9 -0
  53. package/dist/clients/attach/auth-pickers.js +194 -0
  54. package/dist/clients/attach/canvas-panels.d.ts +4 -2
  55. package/dist/clients/attach/canvas-panels.js +45 -17
  56. package/dist/clients/attach/chat-view.d.ts +27 -2
  57. package/dist/clients/attach/chat-view.js +98 -9
  58. package/dist/clients/attach/clipboard-image.d.ts +47 -8
  59. package/dist/clients/attach/clipboard-image.js +150 -26
  60. package/dist/clients/attach/clipboard-text.d.ts +3 -0
  61. package/dist/clients/attach/clipboard-text.js +39 -0
  62. package/dist/clients/attach/config-load.d.ts +54 -7
  63. package/dist/clients/attach/config-load.js +99 -3
  64. package/dist/clients/attach/context-message.d.ts +12 -0
  65. package/dist/clients/attach/context-message.js +72 -0
  66. package/dist/clients/attach/git-info.d.ts +20 -0
  67. package/dist/clients/attach/git-info.js +44 -0
  68. package/dist/clients/attach/graph-overlay.d.ts +13 -4
  69. package/dist/clients/attach/graph-overlay.js +59 -19
  70. package/dist/clients/attach/input-controller.d.ts +75 -13
  71. package/dist/clients/attach/input-controller.js +305 -67
  72. package/dist/clients/attach/pickers.d.ts +58 -0
  73. package/dist/clients/attach/pickers.js +171 -0
  74. package/dist/clients/attach/slash-commands.d.ts +22 -0
  75. package/dist/clients/attach/slash-commands.js +91 -25
  76. package/dist/clients/attach/titled-editor.d.ts +33 -0
  77. package/dist/clients/attach/titled-editor.js +84 -0
  78. package/dist/clients/attach/view-socket.d.ts +18 -1
  79. package/dist/clients/attach/view-socket.js +70 -1
  80. package/dist/clients/web/dev-server.d.ts +7 -0
  81. package/dist/clients/web/dev-server.js +59 -0
  82. package/dist/clients/web/events.d.ts +14 -0
  83. package/dist/clients/web/events.js +151 -0
  84. package/dist/clients/web/server.d.ts +18 -0
  85. package/dist/clients/web/server.js +450 -0
  86. package/dist/clients/web/web-cmd.d.ts +2 -0
  87. package/dist/clients/web/web-cmd.js +120 -0
  88. package/dist/commands/canvas.js +1 -2
  89. package/dist/commands/chord.js +4 -25
  90. package/dist/commands/dashboard.js +5 -1
  91. package/dist/commands/memory/__tests__/lint-schema.test.js +6 -6
  92. package/dist/commands/memory/lint.js +14 -2
  93. package/dist/commands/memory/read.js +2 -2
  94. package/dist/commands/memory/shared.d.ts +7 -1
  95. package/dist/commands/memory/shared.js +18 -3
  96. package/dist/commands/memory/write.js +8 -4
  97. package/dist/commands/memory.js +4 -4
  98. package/dist/commands/node.d.ts +1 -1
  99. package/dist/commands/node.js +64 -65
  100. package/dist/commands/pkg.js +1 -2
  101. package/dist/commands/revive.js +11 -3
  102. package/dist/commands/sys/sync.d.ts +1 -0
  103. package/dist/commands/sys/sync.js +187 -0
  104. package/dist/commands/sys.js +3 -2
  105. package/dist/commands/view-cycle.js +2 -2
  106. package/dist/commands/view-list.js +8 -8
  107. package/dist/commands/view-new.js +21 -17
  108. package/dist/commands/view-pick.js +1 -1
  109. package/dist/commands/view-run.js +35 -14
  110. package/dist/commands/view.js +8 -6
  111. package/dist/commands/workspace.d.ts +2 -0
  112. package/dist/commands/workspace.js +161 -0
  113. package/dist/core/__tests__/broker-double-spawn.test.d.ts +1 -0
  114. package/dist/core/__tests__/broker-double-spawn.test.js +143 -0
  115. package/dist/core/__tests__/broker-fork-seam.test.d.ts +1 -0
  116. package/dist/core/__tests__/broker-fork-seam.test.js +102 -0
  117. package/dist/core/__tests__/broker-sdk-wiring.test.js +2 -0
  118. package/dist/core/__tests__/child-death-wake.test.js +56 -111
  119. package/dist/core/__tests__/close.test.js +14 -20
  120. package/dist/core/__tests__/context-intro.test.js +19 -19
  121. package/dist/core/__tests__/daemon-boot.test.js +30 -23
  122. package/dist/core/__tests__/draw-style.test.js +1 -1
  123. package/dist/core/__tests__/error-stall-recycle.test.d.ts +1 -0
  124. package/dist/core/__tests__/error-stall-recycle.test.js +141 -0
  125. package/dist/core/__tests__/fixtures/fake-engine.d.ts +17 -1
  126. package/dist/core/__tests__/fixtures/fake-engine.js +22 -2
  127. package/dist/core/__tests__/fixtures/fake-pi-host.js +2 -2
  128. package/dist/core/__tests__/focuses.test.js +25 -13
  129. package/dist/core/__tests__/fork.test.js +22 -5
  130. package/dist/core/__tests__/full/broker-control-preempt.test.d.ts +1 -0
  131. package/dist/core/__tests__/full/broker-control-preempt.test.js +61 -0
  132. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.d.ts +1 -0
  133. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.js +57 -0
  134. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.d.ts +1 -0
  135. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.js +80 -0
  136. package/dist/core/__tests__/full/broker-pane-resolution.test.js +90 -0
  137. package/dist/core/__tests__/full/cascade-close.test.js +22 -8
  138. package/dist/core/__tests__/full/detach-focus.test.js +66 -169
  139. package/dist/core/__tests__/helpers/harness.d.ts +6 -9
  140. package/dist/core/__tests__/helpers/harness.js +19 -36
  141. package/dist/core/__tests__/human-node-not-supervised.test.d.ts +1 -0
  142. package/dist/core/__tests__/human-node-not-supervised.test.js +83 -0
  143. package/dist/core/__tests__/lifecycle.test.js +1 -19
  144. package/dist/core/__tests__/listing-completeness.test.js +2 -2
  145. package/dist/core/__tests__/live-mutation-verbs.test.js +5 -5
  146. package/dist/core/__tests__/memory-resolver.test.js +1 -1
  147. package/dist/core/__tests__/on-read-dedup-resume.test.js +2 -2
  148. package/dist/core/__tests__/refresh-stall-recycle.test.d.ts +1 -0
  149. package/dist/core/__tests__/refresh-stall-recycle.test.js +88 -0
  150. package/dist/core/__tests__/relaunch-root.test.d.ts +1 -0
  151. package/dist/core/__tests__/relaunch-root.test.js +114 -0
  152. package/dist/core/__tests__/reset.test.js +19 -79
  153. package/dist/core/__tests__/steer-note.test.js +9 -7
  154. package/dist/core/__tests__/tmux-surface.test.js +13 -7
  155. package/dist/core/bootstrap.d.ts +0 -3
  156. package/dist/core/bootstrap.js +1 -143
  157. package/dist/core/canvas/browse/__tests__/model.test.js +86 -1
  158. package/dist/core/canvas/browse/__tests__/render.test.js +2 -1
  159. package/dist/core/canvas/browse/app.js +131 -8
  160. package/dist/core/canvas/browse/model.d.ts +23 -5
  161. package/dist/core/canvas/browse/model.js +68 -13
  162. package/dist/core/canvas/browse/render.d.ts +7 -1
  163. package/dist/core/canvas/browse/render.js +178 -47
  164. package/dist/core/canvas/canvas.js +1 -1
  165. package/dist/core/canvas/focuses.d.ts +4 -4
  166. package/dist/core/canvas/focuses.js +7 -7
  167. package/dist/core/canvas/nav-model.js +8 -7
  168. package/dist/core/canvas/render.d.ts +68 -9
  169. package/dist/core/canvas/render.js +195 -61
  170. package/dist/core/canvas/types.d.ts +16 -23
  171. package/dist/core/config.js +1 -4
  172. package/dist/core/memory-resolver.js +43 -5
  173. package/dist/core/runtime/bearings.d.ts +1 -1
  174. package/dist/core/runtime/bearings.js +8 -8
  175. package/dist/core/runtime/branded-host.d.ts +16 -0
  176. package/dist/core/runtime/branded-host.js +127 -0
  177. package/dist/core/runtime/broker-protocol.d.ts +212 -6
  178. package/dist/core/runtime/broker.d.ts +4 -1
  179. package/dist/core/runtime/broker.js +494 -58
  180. package/dist/core/runtime/close.d.ts +12 -2
  181. package/dist/core/runtime/close.js +37 -16
  182. package/dist/core/runtime/front-door.js +6 -14
  183. package/dist/core/runtime/host.d.ts +14 -34
  184. package/dist/core/runtime/host.js +14 -50
  185. package/dist/core/runtime/launch.d.ts +12 -9
  186. package/dist/core/runtime/launch.js +27 -15
  187. package/dist/core/runtime/lifecycle.d.ts +1 -1
  188. package/dist/core/runtime/lifecycle.js +15 -19
  189. package/dist/core/runtime/nodes.d.ts +0 -55
  190. package/dist/core/runtime/nodes.js +6 -74
  191. package/dist/core/runtime/placement.d.ts +91 -335
  192. package/dist/core/runtime/placement.js +291 -858
  193. package/dist/core/runtime/promote.d.ts +2 -0
  194. package/dist/core/runtime/promote.js +24 -6
  195. package/dist/core/runtime/recap.d.ts +8 -0
  196. package/dist/core/runtime/recap.js +107 -0
  197. package/dist/core/runtime/recycle.js +25 -61
  198. package/dist/core/runtime/reset.d.ts +43 -37
  199. package/dist/core/runtime/reset.js +131 -218
  200. package/dist/core/runtime/revive.d.ts +9 -29
  201. package/dist/core/runtime/revive.js +40 -139
  202. package/dist/core/runtime/spawn.d.ts +15 -16
  203. package/dist/core/runtime/spawn.js +165 -194
  204. package/dist/core/runtime/surface-bg.d.ts +11 -0
  205. package/dist/core/runtime/surface-bg.js +68 -0
  206. package/dist/core/runtime/tmux.d.ts +15 -52
  207. package/dist/core/runtime/tmux.js +35 -99
  208. package/dist/core/scope.d.ts +1 -2
  209. package/dist/core/scope.js +1 -5
  210. package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.d.ts +1 -0
  211. package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.js +57 -0
  212. package/dist/core/skill-sync/builtins.d.ts +42 -0
  213. package/dist/core/skill-sync/builtins.js +112 -0
  214. package/dist/core/skill-sync/claude-plugins.d.ts +23 -0
  215. package/dist/core/skill-sync/claude-plugins.js +71 -0
  216. package/dist/core/skill-sync/engine.d.ts +42 -0
  217. package/dist/core/skill-sync/engine.js +633 -0
  218. package/dist/core/skill-sync/export.d.ts +23 -0
  219. package/dist/core/skill-sync/export.js +86 -0
  220. package/dist/core/skill-sync/manifest.d.ts +64 -0
  221. package/dist/core/skill-sync/manifest.js +181 -0
  222. package/dist/core/skill-sync/profile.d.ts +76 -0
  223. package/dist/core/skill-sync/profile.js +173 -0
  224. package/dist/core/skill-sync/snapshot.d.ts +57 -0
  225. package/dist/core/skill-sync/snapshot.js +120 -0
  226. package/dist/core/substrate/index.d.ts +1 -1
  227. package/dist/core/substrate/index.js +1 -1
  228. package/dist/core/substrate/injected-store.js +3 -3
  229. package/dist/core/substrate/on-read.js +68 -6
  230. package/dist/core/substrate/render.d.ts +8 -11
  231. package/dist/core/substrate/render.js +29 -43
  232. package/dist/core/substrate/schema.d.ts +10 -3
  233. package/dist/core/substrate/schema.js +6 -3
  234. package/dist/core/tui/host.d.ts +12 -4
  235. package/dist/core/tui/host.js +280 -149
  236. package/dist/core/view/bridge.d.ts +10 -0
  237. package/dist/core/view/bridge.js +31 -0
  238. package/dist/core/view/chrome.d.ts +9 -0
  239. package/dist/core/view/chrome.js +22 -0
  240. package/dist/core/view/contract.d.ts +171 -0
  241. package/dist/core/view/contract.js +23 -0
  242. package/dist/core/view/loader.d.ts +31 -0
  243. package/dist/core/view/loader.js +188 -0
  244. package/dist/core/view/transport-local.d.ts +7 -0
  245. package/dist/core/view/transport-local.js +70 -0
  246. package/dist/core/view/transport.d.ts +4 -0
  247. package/dist/core/view/transport.js +15 -0
  248. package/dist/daemon/crtrd.d.ts +38 -27
  249. package/dist/daemon/crtrd.js +303 -376
  250. package/dist/daemon/manage.js +6 -1
  251. package/dist/index.js +1 -1
  252. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +28 -20
  253. package/dist/pi-extensions/canvas-context-intro.js +4 -4
  254. package/dist/pi-extensions/canvas-doc-substrate.js +16 -16
  255. package/dist/pi-extensions/canvas-nav.js +5 -0
  256. package/dist/pi-extensions/canvas-recap.d.ts +37 -0
  257. package/dist/pi-extensions/canvas-recap.js +236 -0
  258. package/dist/pi-extensions/canvas-resume.js +2 -1
  259. package/dist/pi-extensions/canvas-stophook.d.ts +14 -12
  260. package/dist/pi-extensions/canvas-stophook.js +87 -101
  261. package/dist/pi-extensions/canvas-view.js +2 -1
  262. package/dist/pi-extensions/widget-order-bus.d.ts +6 -0
  263. package/dist/pi-extensions/widget-order-bus.js +34 -0
  264. package/dist/prompts/view.d.ts +2 -2
  265. package/dist/prompts/view.js +145 -73
  266. package/dist/types.d.ts +1 -6
  267. package/dist/types.js +1 -3
  268. package/dist/web/ViewChrome.d.ts +7 -0
  269. package/dist/web/ViewChrome.js +28 -0
  270. package/dist/web/ViewPane.d.ts +39 -0
  271. package/dist/web/ViewPane.js +48 -0
  272. package/dist/web/index.d.ts +6 -0
  273. package/dist/web/index.js +16 -0
  274. package/dist/web/runtime.d.ts +39 -0
  275. package/dist/web/runtime.js +133 -0
  276. package/dist/web/states.d.ts +24 -0
  277. package/dist/web/states.js +24 -0
  278. package/dist/web/transport-http.d.ts +5 -0
  279. package/dist/web/transport-http.js +28 -0
  280. package/dist/web-client/assets/index-BUvQb4hR.css +2 -0
  281. package/dist/web-client/assets/index-ClLQXYAE.js +10 -0
  282. package/dist/web-client/index.html +13 -0
  283. package/package.json +20 -6
  284. package/dist/builtin-views/canvas/client.mjs +0 -303
  285. package/dist/builtin-views/canvas/view.mjs +0 -576
  286. package/dist/builtin-views/git-pr/client.mjs +0 -440
  287. package/dist/builtin-views/git-pr/view.mjs +0 -675
  288. package/dist/builtin-views/inbox/sources/gmail.mjs +0 -965
  289. package/dist/builtin-views/inbox/sources/linkedin.mjs +0 -427
  290. package/dist/builtin-views/inbox/view.mjs +0 -889
  291. package/dist/builtin-views/linkedin/client.mjs +0 -610
  292. package/dist/builtin-views/linkedin/view.mjs +0 -1171
  293. package/dist/commands/pkg/bridge.d.ts +0 -1
  294. package/dist/commands/pkg/bridge.js +0 -137
  295. package/dist/commands/skill/author.d.ts +0 -3
  296. package/dist/commands/skill/author.js +0 -140
  297. package/dist/commands/skill/shared.d.ts +0 -3
  298. package/dist/commands/skill/shared.js +0 -19
  299. package/dist/commands/skill.d.ts +0 -2
  300. package/dist/commands/skill.js +0 -21
  301. package/dist/commands/tmux-spread.d.ts +0 -2
  302. package/dist/commands/tmux-spread.js +0 -144
  303. package/dist/core/__tests__/full/daemon-liveness-pane.full.test.js +0 -292
  304. package/dist/core/__tests__/full/placement-focus.test.js +0 -309
  305. package/dist/core/__tests__/full/placement-reconcile.test.js +0 -212
  306. package/dist/core/__tests__/full/placement-revive.test.js +0 -238
  307. package/dist/core/__tests__/full/placement-teardown.test.js +0 -297
  308. package/dist/core/__tests__/home-session.test.js +0 -186
  309. package/dist/core/__tests__/relaunch.test.js +0 -335
  310. package/dist/core/bridge-map.d.ts +0 -19
  311. package/dist/core/bridge-map.js +0 -73
  312. package/dist/core/tui/contract.d.ts +0 -83
  313. package/dist/core/tui/contract.js +0 -8
  314. package/dist/core/tui/loader.d.ts +0 -16
  315. package/dist/core/tui/loader.js +0 -94
  316. package/dist/prompts/skill.d.ts +0 -2
  317. package/dist/prompts/skill.js +0 -650
  318. /package/dist/{core/__tests__/full/daemon-liveness-pane.full.test.d.ts → clients/attach/__tests__/action-parity.test.d.ts} +0 -0
  319. /package/dist/{core/__tests__/full/placement-focus.test.d.ts → clients/attach/__tests__/clipboard-image.test.d.ts} +0 -0
  320. /package/dist/{core/__tests__/full/placement-reconcile.test.d.ts → clients/attach/__tests__/editor-newline.test.d.ts} +0 -0
  321. /package/dist/{core/__tests__/full/placement-revive.test.d.ts → clients/attach/__tests__/git-info.test.d.ts} +0 -0
  322. /package/dist/{core/__tests__/full/placement-teardown.test.d.ts → clients/attach/__tests__/onrequest-wired.test.d.ts} +0 -0
  323. /package/dist/{core/__tests__/home-session.test.d.ts → clients/attach/__tests__/slash-quit-copy.test.d.ts} +0 -0
  324. /package/dist/{core/__tests__/relaunch.test.d.ts → clients/attach/__tests__/titled-editor.test.d.ts} +0 -0
@@ -0,0 +1,73 @@
1
+ // @ts-check
2
+ /**
3
+ * Combined `inbox` view — the text presenter (`dump`) for the non-TTY / piped
4
+ * path (`crtr view run inbox | cat`). Node-only, no ANSI. A plain snapshot of
5
+ * the merged list + each down source's banner + the host's current banner.
6
+ *
7
+ * The pure helpers `dump` shares with the TUI (badgeFor, unreadCount, labeled,
8
+ * relTimestamp, …) are imported from `core.mjs`.
9
+ *
10
+ * @module inbox/text
11
+ */
12
+
13
+ import { badgeFor, unreadCount, labeled, relTimestamp, truncate, padEnd, SOURCES_META } from './core.mjs';
14
+
15
+ /** @typedef {import('./core.mjs').InboxState} InboxState */
16
+
17
+ const SOURCE_BY_ID = {};
18
+ for (const m of SOURCES_META) SOURCE_BY_ID[m.id] = m;
19
+
20
+ /**
21
+ * Plain-text snapshot for the non-TTY / piped path.
22
+ * @param {InboxState} state
23
+ * @param {import('../../core/view/contract.js').DumpContext} [ctx]
24
+ * @returns {string}
25
+ */
26
+ export function dump(state, ctx) {
27
+ const banner = ctx && ctx.banner ? ctx.banner : null;
28
+ const sigil = (lvl) => (lvl === 'error' ? '✗' : lvl === 'action' ? '▸' : 'ℹ');
29
+ /** @type {string[]} */
30
+ const lines = [];
31
+ const n = unreadCount(state);
32
+ let head = 'Inbox';
33
+ if (n) head += ` · ${n} unread`;
34
+ if (state.filter !== 'all') {
35
+ const s = SOURCE_BY_ID[state.filter];
36
+ head += ` · ${s ? s.label : state.filter} only`;
37
+ }
38
+ lines.push(head, '');
39
+
40
+ let anyDown = false;
41
+ for (const m of SOURCES_META) {
42
+ const e = state.banners[m.id];
43
+ if (e && e.display) {
44
+ anyDown = true;
45
+ lines.push(labeled(m.label, e.display.banner));
46
+ }
47
+ }
48
+ if (anyDown) lines.push('');
49
+
50
+ if (state.rows.length === 0) {
51
+ if (state.lastFetch === 0) lines.push('(loading…)');
52
+ else if (!anyDown) lines.push('✓ All caught up — no messages.');
53
+ } else {
54
+ for (const row of state.rows) {
55
+ const badge = padEnd(badgeFor(row.sourceId).glyph, 2);
56
+ const dot = row.unread ? '●' : ' ';
57
+ const snip = truncate((row.snippet || '').replace(/\s+/g, ' ').trim(), 48);
58
+ lines.push(`[${dot}] ${badge} ${padEnd(row.name || 'Unknown', 20)} ${padEnd(snip, 48)} ${relTimestamp(row.ts)}`);
59
+ }
60
+ }
61
+
62
+ if (state.openKey && state.thread) {
63
+ lines.push('', `— ${state.thread.title} —`);
64
+ if (state.thread.subtitle) lines.push(state.thread.subtitle);
65
+ for (const m of state.thread.messages || []) {
66
+ const who = m.fromMe ? 'You' : m.sender || 'Them';
67
+ lines.push(`${who}: ${(m.text || '').replace(/\s+/g, ' ').trim()}`);
68
+ }
69
+ }
70
+
71
+ if (banner) lines.push('', sigil(banner.level) + ' ' + banner.msg);
72
+ return lines.join('\n');
73
+ }
@@ -0,0 +1,312 @@
1
+ // @ts-check
2
+ /**
3
+ * Combined `inbox` view — the TUI presenter (`render` + `keymap`). Node-only (it
4
+ * uses the host's `Draw` API + the `_lib` draw helpers).
5
+ *
6
+ * `render` is a pure read of state; keystrokes map to named intents through
7
+ * `keymap`. The compose/react bars READ `state.draft`/`state.reactCursor`/
8
+ * `state.mode` (input flows through the `setDraft` capture intent, not a buffer
9
+ * owned here). All state + data logic lives in `core.mjs`.
10
+ *
11
+ * SGR discipline (§2): all hue is NUMERIC SGR codes; every colored element pairs
12
+ * hue with a glyph or weight so it survives NO_COLOR / dumb terminals.
13
+ *
14
+ * @module inbox/tui
15
+ */
16
+
17
+ import { loadingState, emptyState, errorState, notReadyState } from '../_lib/states.mjs';
18
+ import { spanWidth, centeredStack, splitPanes, wrapText } from './_lib/render.mjs';
19
+ import { badgeFor, pickGuided, EMOJIS, relTimestamp, dayKey, dayLabel } from './core.mjs';
20
+
21
+ /** @typedef {import('./core.mjs').InboxState} InboxState */
22
+
23
+ // ── Render ───────────────────────────────────────────────────────────────────
24
+
25
+ /**
26
+ * The no-rows guided full-content takeover, built from a down source's display.
27
+ * @param {object} draw @param {object} content @param {{label:string, d:any}} guide
28
+ */
29
+ function renderGuided(draw, content, guide) {
30
+ const d = guide.d;
31
+ const headline = `${guide.label}: ${d.headline}`;
32
+ if (d.level === 'error') {
33
+ errorState(draw, content, {
34
+ headline,
35
+ cause: d.explanation,
36
+ hint: d.nextStep || 'Press g to retry.',
37
+ });
38
+ return;
39
+ }
40
+ notReadyState(draw, content, {
41
+ glyph: d.level === 'action' ? '⚠' : '⊙',
42
+ glyphFg: d.level === 'action' ? '33' : '36',
43
+ headline,
44
+ explanation: d.explanation,
45
+ nextStep: d.nextStep || undefined,
46
+ });
47
+ }
48
+
49
+ /** Two-pane loading skeleton: dim placeholder rows left, a dim caption right. */
50
+ function renderLoadingSkeleton(draw, content) {
51
+ const { left, right } = splitPanes(draw, content);
52
+ loadingState(draw, left, { rows: Math.min(5, Math.max(1, left.height)) });
53
+ centeredStack(draw, right, [[{ text: 'Loading inbox…', style: { dim: true } }]]);
54
+ }
55
+
56
+ /**
57
+ * Left pane — the merged row list: cursor ▸ · unread ● · source badge · name ·
58
+ * dim snippet · right-flush relative ts.
59
+ * @param {InboxState} state @param {object} draw @param {object} left
60
+ */
61
+ function renderRowList(state, draw, left) {
62
+ if (left.width <= 0 || left.height <= 0) return;
63
+ const items = state.rows.map((row, i) => {
64
+ const isCursor = i === state.cursor;
65
+ const b = badgeFor(row.sourceId);
66
+ const glyph = padEnd2(b.glyph || '?'); // 'in' / '@ ' — align names
67
+ /** @type {object[]} */
68
+ const spans = [
69
+ { text: isCursor ? '▸' : ' ', style: isCursor ? { fg: '36', bold: true } : undefined },
70
+ { text: row.unread ? '●' : ' ', style: row.unread ? { fg: '36', bold: true } : undefined },
71
+ { text: ' ' },
72
+ { text: glyph, style: { fg: b.fg || '37', bold: true } }, // badge: hue + glyph (mono-safe)
73
+ { text: ' ' + (row.name || 'Unknown'), style: row.unread ? { bold: true } : undefined },
74
+ ];
75
+ const snip = (row.snippet || '').replace(/\s+/g, ' ').trim();
76
+ if (snip) spans.push({ text: ' ' + snip, style: { dim: true } });
77
+ /** @type {any} */
78
+ const item = { spans };
79
+ const ts = relTimestamp(row.ts);
80
+ if (ts) item.right = [{ text: ts, style: { dim: true } }];
81
+ return item;
82
+ });
83
+ const res = draw.list(left, items, state.cursor, state.scroll);
84
+ state.scroll = res.scroll;
85
+ }
86
+
87
+ /** Pad a badge glyph to 2 cols. */
88
+ function padEnd2(s) {
89
+ const str = String(s == null ? '' : s);
90
+ return str.length >= 2 ? str.slice(0, 2) : str + ' '.repeat(2 - str.length);
91
+ }
92
+
93
+ /**
94
+ * Build the thread's flat visual lines with you-vs-them grouping.
95
+ * @param {object[]} messages @param {number} width @param {number} reactTarget index, or -1
96
+ * @returns {object[]}
97
+ */
98
+ function buildThreadLines(messages, width, reactTarget) {
99
+ /** @type {object[]} */
100
+ const lines = [];
101
+ let prevDay = null;
102
+ messages.forEach((m, idx) => {
103
+ const day = dayKey(m.ts);
104
+ if (m.ts && day !== prevDay) {
105
+ const txt = `── ${dayLabel(m.ts)} ──`;
106
+ const pad = Math.max(0, Math.floor((width - Array.from(txt).length) / 2));
107
+ lines.push({ spans: [{ text: ' '.repeat(pad) + txt, style: { dim: true } }] });
108
+ prevDay = day;
109
+ }
110
+ const tick = idx === reactTarget ? [{ text: '▸ ', style: { fg: '36', bold: true } }] : [];
111
+ const ts = relTimestamp(m.ts);
112
+ const right = ts ? [{ text: ts, style: { dim: true } }] : undefined;
113
+ if (m.fromMe) {
114
+ lines.push({
115
+ spans: [...tick, { text: '▎ ', style: { fg: '32', bold: true } }, { text: 'You', style: { fg: '32', bold: true } }],
116
+ right,
117
+ });
118
+ for (const bl of wrapText(m.text || '', Math.max(1, width - 2))) {
119
+ lines.push({ spans: [{ text: '▎ ', style: { fg: '32' } }, { text: bl }] });
120
+ }
121
+ } else {
122
+ lines.push({ spans: [...tick, { text: m.sender || 'Them', style: { fg: '36', bold: true } }], right });
123
+ for (const bl of wrapText(m.text || '', width)) {
124
+ lines.push({ spans: [{ text: bl }] });
125
+ }
126
+ }
127
+ if (idx < messages.length - 1) lines.push({ spans: [{ text: '' }] }); // spacer BETWEEN groups
128
+ });
129
+ return lines;
130
+ }
131
+
132
+ /** Paint the thread body (tail-windowed) into `rect`. */
133
+ function renderThreadBody(state, draw, rect, reactTarget) {
134
+ if (rect.width <= 0 || rect.height <= 0) return;
135
+ const messages = (state.thread && state.thread.messages) || [];
136
+ const lines = buildThreadLines(messages, rect.width, reactTarget);
137
+ const start = Math.max(0, lines.length - rect.height);
138
+ state.threadScroll = start;
139
+ let r = rect.row;
140
+ for (let i = start; i < lines.length && r < rect.row + rect.height; i++, r++) {
141
+ const ln = lines[i];
142
+ if (ln.right && ln.right.length) {
143
+ const rw = spanWidth(ln.right);
144
+ draw.spans(r, rect.col, ln.spans, Math.max(0, rect.width - rw - 1));
145
+ draw.spansRight(r, rect.col + rect.width, ln.right, rw);
146
+ } else {
147
+ draw.spans(r, rect.col, ln.spans, rect.width);
148
+ }
149
+ }
150
+ }
151
+
152
+ /** The compose bar (reply mode): hairline + label + draft + cursor + hint. */
153
+ function renderComposer(state, draw, right) {
154
+ const hairRow = right.row + right.height - 3;
155
+ const barRow = right.row + right.height - 2;
156
+ const hintRow = right.row + right.height - 1;
157
+ draw.hline(hairRow, right.col, right.col + right.width);
158
+ const label = '✎ Reply ';
159
+ const labelW = Array.from(label).length;
160
+ const avail = Math.max(1, right.width - labelW - 1); // 1 cell reserved for the cursor
161
+ let shown = state.draft;
162
+ const arr = Array.from(shown);
163
+ if (arr.length > avail - 1) shown = arr.slice(arr.length - (avail - 1)).join(''); // keep the tail/cursor visible
164
+ draw.spans(barRow, right.col, [
165
+ { text: label, style: { fg: '33', bold: true } },
166
+ { text: shown },
167
+ { text: '█', style: { fg: '33' } },
168
+ ], right.width);
169
+ draw.spans(hintRow, right.col, [
170
+ { text: 'enter', style: { bold: true } }, { text: ' send', style: { dim: true } },
171
+ { text: ' · ', style: { dim: true } },
172
+ { text: 'esc', style: { bold: true } }, { text: ' cancel', style: { dim: true } },
173
+ ], right.width);
174
+ }
175
+
176
+ /** The react picker bar (react mode): hairline + emoji chip row + hint. */
177
+ function renderReactBar(state, draw, right) {
178
+ const hairRow = right.row + right.height - 3;
179
+ const barRow = right.row + right.height - 2;
180
+ const hintRow = right.row + right.height - 1;
181
+ draw.hline(hairRow, right.col, right.col + right.width);
182
+ /** @type {object[]} */
183
+ const spans = [{ text: '☺ React ', style: { fg: '33', bold: true } }];
184
+ EMOJIS.forEach((e, i) => {
185
+ if (i === state.reactCursor) {
186
+ spans.push({ text: ' ' });
187
+ spans.push({ text: '[' + e + ']', style: { bg: '236', reverse: true } }); // accent-bg + brackets/reverse (mono carrier)
188
+ } else {
189
+ spans.push({ text: ' ' + e + ' ' });
190
+ }
191
+ });
192
+ draw.spans(barRow, right.col, spans, right.width);
193
+ draw.spans(hintRow, right.col, [
194
+ { text: '←/→', style: { bold: true } }, { text: ' pick', style: { dim: true } },
195
+ { text: ' · ', style: { dim: true } },
196
+ { text: 'enter', style: { bold: true } }, { text: ' react', style: { dim: true } },
197
+ { text: ' · ', style: { dim: true } },
198
+ { text: 'esc', style: { bold: true } }, { text: ' cancel', style: { dim: true } },
199
+ ], right.width);
200
+ }
201
+
202
+ /**
203
+ * Right pane — header (+ optional subtitle) + hairline + grouped body, plus the
204
+ * compose/react bar when in a mode.
205
+ * @param {InboxState} state @param {object} draw @param {object} right
206
+ */
207
+ function renderDetail(state, draw, right) {
208
+ if (right.width <= 0 || right.height <= 0) return;
209
+ if (!state.openKey || !state.thread) {
210
+ centeredStack(draw, right, [
211
+ [{ text: '✉ ', style: { dim: true } }, { text: 'No conversation open', style: { dim: true } }],
212
+ [{ text: '' }],
213
+ [{ text: 'Press ' }, { text: 'Enter', style: { bold: true } }, { text: ' to open a conversation' }],
214
+ ]);
215
+ return;
216
+ }
217
+ const thread = state.thread;
218
+ const messages = thread.messages || [];
219
+ const hasSub = !!thread.subtitle;
220
+
221
+ // Header: bold title + right-flush relative time of the latest message.
222
+ const lastTs = messages.length ? messages[messages.length - 1].ts : (state.openRow ? state.openRow.ts : 0);
223
+ const ts = relTimestamp(lastTs);
224
+ const rw = ts ? Array.from(ts).length : 0;
225
+ draw.spans(right.row, right.col, [{ text: thread.title || 'Conversation', style: { bold: true } }], Math.max(0, right.width - rw - 1));
226
+ if (ts) draw.spansRight(right.row, right.col + right.width, [{ text: ts, style: { dim: true } }], rw);
227
+ let headerRows = 1;
228
+ if (hasSub) {
229
+ draw.spans(right.row + 1, right.col, [{ text: thread.subtitle, style: { dim: true } }], right.width);
230
+ headerRows = 2;
231
+ }
232
+ draw.hline(right.row + headerRows, right.col, right.col + right.width);
233
+
234
+ const composing = state.mode === 'reply';
235
+ const reacting = state.mode === 'react';
236
+ const composerRows = composing || reacting ? 3 : 0;
237
+ const bodyTop = right.row + headerRows + 1;
238
+ const bodyBottom = right.row + right.height - 1 - composerRows;
239
+ const bodyRect = { row: bodyTop, col: right.col, width: right.width, height: Math.max(0, bodyBottom - bodyTop + 1) };
240
+
241
+ if (messages.length === 0) {
242
+ centeredStack(draw, bodyRect, [[{ text: 'Loading messages…', style: { dim: true } }]]);
243
+ } else {
244
+ renderThreadBody(state, draw, bodyRect, reacting ? messages.length - 1 : -1);
245
+ }
246
+
247
+ if (composing) renderComposer(state, draw, right);
248
+ else if (reacting) renderReactBar(state, draw, right);
249
+ }
250
+
251
+ /**
252
+ * Paint. Precedence: no rows → loading skeleton (first paint) / guided panel (a
253
+ * down source) / empty reward; else the two-pane merged inbox.
254
+ * @param {InboxState} state
255
+ * @param {import('../../core/tui/draw.js').Draw} draw
256
+ * @param {import('../../core/tui/draw.js').Rect} content
257
+ */
258
+ export function render(state, draw, content) {
259
+ if (content.width <= 0 || content.height <= 0) return;
260
+ if (state.rows.length === 0) {
261
+ if (state.lastFetch === 0) {
262
+ renderLoadingSkeleton(draw, content);
263
+ return;
264
+ }
265
+ const guide = pickGuided(state);
266
+ if (guide) {
267
+ renderGuided(draw, content, guide);
268
+ return;
269
+ }
270
+ emptyState(draw, content, {
271
+ headline: 'All caught up',
272
+ secondary: ['No messages in your inbox.', 'Press g to refresh.'],
273
+ });
274
+ return;
275
+ }
276
+ const { left, right } = splitPanes(draw, content);
277
+ renderRowList(state, draw, left);
278
+ renderDetail(state, draw, right);
279
+ }
280
+
281
+ // ── keymap ───────────────────────────────────────────────────────────────
282
+
283
+ /** @param {InboxState} s */
284
+ const listMode = (s) => s.mode === 'list';
285
+ /** @param {InboxState} s */
286
+ const replyMode = (s) => s.mode === 'reply';
287
+ /** @param {InboxState} s */
288
+ const reactMode = (s) => s.mode === 'react';
289
+
290
+ /** @type {import('../../core/view/contract.js').KeyBinding<InboxState>[]} */
291
+ export const keymap = [
292
+ // list
293
+ { keys: ['j', 'down'], intent: 'cursorDown', when: listMode, hint: { keys: 'j/k', label: 'move' } },
294
+ { keys: ['k', 'up'], intent: 'cursorUp', when: listMode },
295
+ { keys: ['return'], intent: 'open', payload: (s) => s.cursor, when: listMode, hint: { keys: 'enter', label: 'open' } },
296
+ { keys: ['r'], intent: 'startReply', when: listMode, hint: { keys: 'r', label: 'reply' } },
297
+ { keys: ['e'], intent: 'startReact', when: listMode, hint: { keys: 'e', label: 'react' } },
298
+ { keys: ['f'], intent: 'cycleFilter', when: listMode, hint: { keys: 'f', label: 'filter' } },
299
+ { keys: ['g'], intent: 'refresh', when: listMode, hint: { keys: 'g', label: 'refresh' } },
300
+ { keys: ['L'], intent: 'connectLinkedin', when: listMode },
301
+ { keys: ['G'], intent: 'connectGmail', when: listMode, hint: { keys: 'G', label: 'connect' } },
302
+ { keys: ['q'], intent: 'quit', when: listMode, hint: { keys: 'q', label: 'quit' } },
303
+ // reply
304
+ { capture: 'setDraft', when: replyMode, hint: { keys: 'type', label: 'reply' } },
305
+ { keys: ['return'], intent: 'submitReply', when: replyMode },
306
+ { keys: ['escape'], intent: 'cancelCompose', when: replyMode },
307
+ // react
308
+ { keys: ['h', 'left'], intent: 'reactPrev', when: reactMode },
309
+ { keys: ['l', 'right'], intent: 'reactNext', when: reactMode },
310
+ { keys: ['return'], intent: 'submitReact', when: reactMode },
311
+ { keys: ['escape'], intent: 'cancelCompose', when: reactMode },
312
+ ];
@@ -0,0 +1,188 @@
1
+ // @ts-check
2
+ /**
3
+ * Combined `inbox` view — the WEB presenter (React + Tailwind). Browser-only:
4
+ * consumed solely by the web serve path (Vite owns JSX + Tailwind); NEVER
5
+ * Node-imported. The default export is a pure function of `state`; DOM events
6
+ * call `dispatch(intentName, payload?)`.
7
+ *
8
+ * Same logical model as the TUI presenter (merged `state.rows` + `state.cursor`
9
+ * + `state.thread`) read from the SAME portable `core.mjs` — zero shared
10
+ * rendering code with `tui.mjs`. The outer chrome (title / status / banner) is
11
+ * rendered by `<ViewChrome>`, which wraps this component — so do NOT render it.
12
+ *
13
+ * @module inbox/web
14
+ */
15
+
16
+ import { Loading, Empty, ErrorState, NotReady } from '@crouton-kit/crouter/web';
17
+ import { badgeFor, pickGuided, EMOJIS, relTimestamp, dayKey, dayLabel } from './core.mjs';
18
+
19
+ /** @typedef {import('./core.mjs').InboxState} InboxState */
20
+
21
+ // ── Source badge hues (the web analog of tui.mjs's NUMERIC-SGR badge map) ─────
22
+
23
+ const BADGE_CLS = { linkedin: 'text-cyan-600', gmail: 'text-red-600' };
24
+
25
+ /** @param {{ row: import('./core.mjs').UnifiedRow, selected: boolean, onClick: () => void }} props */
26
+ function Row({ row, selected, onClick }) {
27
+ const cls = BADGE_CLS[row.sourceId] || 'text-slate-400';
28
+ const snip = (row.snippet || '').replace(/\s+/g, ' ').trim();
29
+ const ts = relTimestamp(row.ts);
30
+ return (
31
+ <li
32
+ onClick={onClick}
33
+ className={`flex items-baseline gap-1.5 rounded px-2 py-1 cursor-pointer ${selected ? 'bg-slate-200' : 'hover:bg-slate-100'}`}
34
+ >
35
+ <span className={`shrink-0 ${row.unread ? 'text-cyan-600' : 'text-transparent'}`}>●</span>
36
+ <span className={`shrink-0 font-bold ${cls}`}>{badgeFor(row.sourceId).glyph}</span>
37
+ <span className={`shrink-0 ${row.unread ? 'font-semibold' : ''}`}>{row.name || 'Unknown'}</span>
38
+ {snip ? <span className="truncate text-slate-400">{snip}</span> : null}
39
+ {ts ? <span className="ml-auto shrink-0 text-slate-400">{ts}</span> : null}
40
+ </li>
41
+ );
42
+ }
43
+
44
+ // ── Thread detail (the web analog of tui.mjs's renderDetail) ──────────────────
45
+
46
+ /** @param {{ state: InboxState, dispatch: (i:string,p?:unknown)=>void }} props */
47
+ function Detail({ state, dispatch }) {
48
+ if (!state.openKey || !state.thread) {
49
+ return (
50
+ <div className="flex h-full flex-col items-center justify-center gap-1 text-slate-400">
51
+ <div>✉ No conversation open</div>
52
+ <div className="text-slate-500">Click a conversation to open it.</div>
53
+ </div>
54
+ );
55
+ }
56
+ const thread = state.thread;
57
+ const messages = thread.messages || [];
58
+ let prevDay = null;
59
+
60
+ return (
61
+ <div className="flex h-full flex-col">
62
+ <div className="border-b border-slate-200 pb-1">
63
+ <div className="font-semibold">{thread.title || 'Conversation'}</div>
64
+ {thread.subtitle ? <div className="text-slate-400">{thread.subtitle}</div> : null}
65
+ </div>
66
+
67
+ <div className="flex-1 overflow-y-auto py-2">
68
+ {messages.length === 0 ? (
69
+ <div className="text-slate-400">Loading messages…</div>
70
+ ) : (
71
+ messages.map((m, i) => {
72
+ const day = dayKey(m.ts);
73
+ const divider = m.ts && day !== prevDay ? dayLabel(m.ts) : null;
74
+ if (m.ts) prevDay = day;
75
+ return (
76
+ <div key={i}>
77
+ {divider ? <div className="my-1 text-center text-xs text-slate-400">── {divider} ──</div> : null}
78
+ {m.fromMe ? (
79
+ <div className="border-l-2 border-green-600 pl-2">
80
+ <div className="font-semibold text-green-700">You</div>
81
+ <div className="whitespace-pre-wrap">{m.text}</div>
82
+ </div>
83
+ ) : (
84
+ <div className="mb-2">
85
+ <div className="font-semibold text-cyan-700">{m.sender || 'Them'}</div>
86
+ <div className="whitespace-pre-wrap">{m.text}</div>
87
+ </div>
88
+ )}
89
+ </div>
90
+ );
91
+ })
92
+ )}
93
+ </div>
94
+
95
+ {state.mode === 'reply' ? (
96
+ <div className="border-t border-slate-200 pt-2">
97
+ <textarea
98
+ autoFocus
99
+ value={state.draft}
100
+ onChange={(e) => dispatch('setDraft', e.target.value)}
101
+ onKeyDown={(e) => {
102
+ if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); dispatch('submitReply'); }
103
+ else if (e.key === 'Escape') { e.preventDefault(); dispatch('cancelCompose'); }
104
+ }}
105
+ className="w-full rounded border border-slate-300 p-2 font-mono text-sm outline-none focus:border-slate-500"
106
+ rows={3}
107
+ placeholder="Type a reply…"
108
+ />
109
+ <div className="mt-1 flex gap-2">
110
+ <button type="button" onClick={() => dispatch('submitReply')} className="rounded bg-slate-700 px-3 py-1 text-white hover:bg-slate-800">Send</button>
111
+ <button type="button" onClick={() => dispatch('cancelCompose')} className="rounded border border-slate-300 px-3 py-1 text-slate-700 hover:bg-slate-100">Cancel</button>
112
+ </div>
113
+ </div>
114
+ ) : null}
115
+
116
+ {state.mode === 'react' ? (
117
+ <div className="border-t border-slate-200 pt-2">
118
+ <div className="flex items-center gap-2">
119
+ <span className="text-amber-600">☺ React</span>
120
+ {EMOJIS.map((emoji, i) => (
121
+ <button
122
+ type="button"
123
+ key={emoji}
124
+ onClick={() => dispatch('submitReact', i)}
125
+ className={`rounded px-2 py-1 text-lg ${i === state.reactCursor ? 'bg-slate-200' : 'hover:bg-slate-100'}`}
126
+ >
127
+ {emoji}
128
+ </button>
129
+ ))}
130
+ <button type="button" onClick={() => dispatch('cancelCompose')} className="ml-auto rounded border border-slate-300 px-3 py-1 text-slate-700 hover:bg-slate-100">Cancel</button>
131
+ </div>
132
+ </div>
133
+ ) : null}
134
+ </div>
135
+ );
136
+ }
137
+
138
+ // ── The view ───────────────────────────────────────────────────────────────────
139
+
140
+ /**
141
+ * @param {import('../../core/view/contract.js').ViewProps<InboxState>} props
142
+ */
143
+ export default function Inbox({ state, dispatch }) {
144
+ // ── Whole-view states (no rows). ──
145
+ if (state.rows.length === 0) {
146
+ if (state.lastFetch === 0) return <Loading label="Loading inbox…" />;
147
+ const guide = pickGuided(state);
148
+ if (guide) {
149
+ const d = guide.d;
150
+ const Takeover = d.level === 'error' ? ErrorState : NotReady;
151
+ return (
152
+ <Takeover
153
+ headline={`${guide.label}: ${d.headline}`}
154
+ explanation={d.explanation}
155
+ nextStep={d.nextStep || undefined}
156
+ onRetry={() => dispatch('refresh')}
157
+ />
158
+ );
159
+ }
160
+ return <Empty label="All caught up — no messages." />;
161
+ }
162
+
163
+ return (
164
+ <div
165
+ className="flex h-full gap-3 font-mono text-sm outline-none"
166
+ tabIndex={0}
167
+ onKeyDown={(e) => {
168
+ if (state.mode !== 'list') return;
169
+ if (e.key === 'j' || e.key === 'ArrowDown') { dispatch('cursorDown'); e.preventDefault(); }
170
+ else if (e.key === 'k' || e.key === 'ArrowUp') { dispatch('cursorUp'); e.preventDefault(); }
171
+ else if (e.key === 'Enter') { dispatch('open', state.cursor); e.preventDefault(); }
172
+ else if (e.key === 'g') { dispatch('refresh'); }
173
+ else if (e.key === 'r') { dispatch('startReply'); }
174
+ else if (e.key === 'e') { dispatch('startReact'); }
175
+ else if (e.key === 'f') { dispatch('cycleFilter'); }
176
+ }}
177
+ >
178
+ <ul className="w-2/5 shrink-0 overflow-y-auto border-r border-slate-200 pr-2">
179
+ {state.rows.map((row, i) => (
180
+ <Row key={row.key} row={row} selected={i === state.cursor} onClick={() => dispatch('open', i)} />
181
+ ))}
182
+ </ul>
183
+ <div className="min-w-0 flex-1">
184
+ <Detail state={state} dispatch={dispatch} />
185
+ </div>
186
+ </div>
187
+ );
188
+ }