@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,84 @@
1
+ // @ts-check
2
+ /**
3
+ * Git / PR board — the text presenter (`dump`) for the non-TTY / piped path of
4
+ * the `git-pr` view (`crtr view run git-pr | cat`). Node-only, no ANSI. A plain
5
+ * snapshot of the current state; the host threads its current banner via `ctx`.
6
+ *
7
+ * The pure helpers `dump` shares with the TUI (relAge, treePhrase) are imported
8
+ * from `core.mjs`.
9
+ *
10
+ * @module git-pr/text
11
+ */
12
+
13
+ import { relAge, treePhrase } from './core.mjs';
14
+
15
+ /** @typedef {import('./core.mjs').GitPrState} GitPrState */
16
+
17
+ /**
18
+ * Plain-text snapshot for the non-TTY / piped path.
19
+ * @param {GitPrState} state
20
+ * @param {import('../../core/view/contract.js').DumpContext} [ctx]
21
+ * @returns {string}
22
+ */
23
+ export function dump(state, ctx) {
24
+ /** @type {string[]} */
25
+ const lines = ['Git / PR board'];
26
+ const banner = ctx && ctx.banner ? ctx.banner : null;
27
+ if (banner) lines.push('', `[${banner.level}] ${banner.msg}`);
28
+ else if (state.gitErr) lines.push('', `[${state.gitErr.display.level}] ${state.gitErr.display.explanation}`);
29
+
30
+ const g = state.git;
31
+ if (!g) {
32
+ lines.push('', state.lastFetch === 0 ? '(reading git…)' : '(git unavailable)');
33
+ return lines.join('\n');
34
+ }
35
+
36
+ const now = Date.now();
37
+
38
+ // Header.
39
+ let head = `⎇ ${g.branch}`;
40
+ if (g.upstream) {
41
+ head += ` → ${g.upstream}`;
42
+ if (g.ahead) head += ` ↑${g.ahead}`;
43
+ if (g.behind) head += ` ↓${g.behind}`;
44
+ if (!g.ahead && !g.behind) head += ' (up to date)';
45
+ } else {
46
+ head += ' (no upstream)';
47
+ }
48
+ head += ` · ${treePhrase(g)}`;
49
+ lines.push('', head);
50
+ if (g.lastCommit) {
51
+ lines.push(`⊙ ${g.lastCommit.sha} ${g.lastCommit.subject} (${relAge(g.lastCommit.when, now)})`);
52
+ } else {
53
+ lines.push('⊙ no commits yet');
54
+ }
55
+
56
+ // Working tree.
57
+ lines.push('', 'Working tree');
58
+ if (g.files.length === 0) {
59
+ lines.push(' ✓ nothing to commit, working tree clean');
60
+ } else {
61
+ for (const f of g.files) {
62
+ const churn = f.add || f.del ? ` +${f.add} −${f.del}` : '';
63
+ lines.push(` ${f.xy.replace(/ /g, '·')} ${f.path}${churn} [${f.cls}]`);
64
+ }
65
+ }
66
+
67
+ // Pull requests.
68
+ lines.push('', 'Pull requests');
69
+ if (state.prNote) {
70
+ lines.push(` · ${state.prNote}`);
71
+ } else if (state.prs.length === 0) {
72
+ lines.push(' · No open pull requests.');
73
+ } else {
74
+ for (const pr of state.prs) {
75
+ const mark = pr.current ? '*' : ' ';
76
+ const draft = pr.isDraft ? ' (draft)' : '';
77
+ lines.push(
78
+ ` ${mark}#${pr.number} ${pr.title}${draft} [review:${pr.review} ci:${pr.ci}] (${relAge(pr.updatedAt, now)})`
79
+ );
80
+ }
81
+ }
82
+
83
+ return lines.join('\n');
84
+ }
@@ -0,0 +1,302 @@
1
+ // @ts-check
2
+ /**
3
+ * Git / PR board — the TUI presenter (`render` + `keymap`) for the `git-pr` view.
4
+ * Node-only (it uses the host's `Draw` API + the `_lib/states.mjs` draw helpers).
5
+ *
6
+ * `render` is a pure read of state; keystrokes map to named intents through
7
+ * `keymap`. All state + data logic lives in `core.mjs`.
8
+ *
9
+ * VISUAL LANGUAGE (crtr-views-visual-design §2/§3/§4): hierarchy by weight + hue
10
+ * + position, never boxes. Hues are NUMERIC SGR only — green=clean/passing/
11
+ * staged/ahead (32), yellow=attention/pending/modified/behind (33), red=failing/
12
+ * conflicted (31), cyan=identity/branch/PR# (36), grey=metadata (90). Every hue
13
+ * is paired with a glyph + the git-native XY code so it survives NO_COLOR.
14
+ *
15
+ * @module git-pr/tui
16
+ */
17
+
18
+ import { relAge } from './core.mjs';
19
+ import { loadingState, emptyState, notReadyState } from '../_lib/states.mjs';
20
+
21
+ /** @typedef {import('./core.mjs').GitState} GitState */
22
+ /** @typedef {import('./core.mjs').ChangedFile} ChangedFile */
23
+ /** @typedef {import('./core.mjs').Pr} Pr */
24
+ /** @typedef {import('./core.mjs').BoardRow} BoardRow */
25
+ /** @typedef {import('./core.mjs').GitPrState} GitPrState */
26
+
27
+ // ── Status vocabulary (triple-coded: hue + glyph + the git-native XY code) ─────
28
+
29
+ /** @type {Record<string,{glyph:string, fg:string}>} */
30
+ const FILE_GLYPH = {
31
+ staged: { glyph: '●', fg: '32' }, // green — in the index, ready
32
+ modified: { glyph: '○', fg: '33' }, // yellow — unstaged worktree edit
33
+ untracked: { glyph: '?', fg: '90' }, // grey — not tracked
34
+ conflict: { glyph: '✗', fg: '31' }, // red — merge conflict
35
+ };
36
+
37
+ /** PR review decision → glyph + hue. Pending review is neutral (grey), not red. */
38
+ const REVIEW = {
39
+ approved: { glyph: '✓', fg: '32' }, // green
40
+ changes: { glyph: '✗', fg: '31' }, // red — changes requested (attention)
41
+ review: { glyph: '◌', fg: '90' }, // grey — review required / pending
42
+ };
43
+
44
+ /** PR CI rollup → glyph + hue. */
45
+ const CI = {
46
+ pass: { glyph: '✓', fg: '32' }, // green
47
+ fail: { glyph: '✗', fg: '31' }, // red
48
+ pending: { glyph: '⟳', fg: '33' }, // yellow
49
+ none: { glyph: '·', fg: '90' }, // grey — no checks
50
+ };
51
+
52
+ // ── Row → ListItemRow (left spans + right-flushed metadata) ────────────────────
53
+
54
+ /**
55
+ * Build one board list row. A 1-cell left gutter rides the cursor bg (§2).
56
+ * @param {BoardRow} row @param {number} now
57
+ * @returns {import('../../core/tui/draw.js').ListItemRow}
58
+ */
59
+ function rowToItem(row, now) {
60
+ /** @type {import('../../core/tui/draw.js').Span[]} */
61
+ const spans = [{ text: ' ', style: undefined }];
62
+
63
+ if (row.kind === 'label') {
64
+ spans.push({ text: row.text, style: { fg: '90', dim: true } });
65
+ return { spans };
66
+ }
67
+ if (row.kind === 'spacer') {
68
+ return { spans: [{ text: '', style: undefined }] };
69
+ }
70
+ if (row.kind === 'clean') {
71
+ spans.push({ text: '✓', style: { fg: '32' } });
72
+ spans.push({ text: ' nothing to commit, working tree clean', style: { dim: true } });
73
+ return { spans };
74
+ }
75
+ if (row.kind === 'note') {
76
+ spans.push({ text: '·', style: { fg: '90', dim: true } });
77
+ spans.push({ text: ' ' + row.text, style: { dim: true } });
78
+ return { spans };
79
+ }
80
+ if (row.kind === 'file') {
81
+ const f = row.file;
82
+ const g = FILE_GLYPH[f.cls] || FILE_GLYPH.modified;
83
+ spans.push({ text: g.glyph, style: { fg: g.fg } }); // hue + shape (mono carrier)
84
+ spans.push({ text: ' ', style: undefined });
85
+ spans.push({ text: f.xy.replace(/ /g, '·'), style: { fg: '90', dim: true } }); // git-native XY code
86
+ spans.push({ text: ' ', style: undefined });
87
+ spans.push({ text: f.path, style: f.cls === 'conflict' ? { fg: '31' } : undefined });
88
+ if (f.add || f.del) {
89
+ /** @type {import('../../core/tui/draw.js').Span[]} */
90
+ const right = [
91
+ { text: `+${f.add}`, style: { fg: '90', dim: true } },
92
+ { text: ' ', style: undefined },
93
+ { text: `−${f.del}`, style: { fg: '90', dim: true } },
94
+ ];
95
+ return { spans, right };
96
+ }
97
+ return { spans };
98
+ }
99
+ // row.kind === 'pr'
100
+ const pr = row.pr;
101
+ const rv = REVIEW[pr.review] || REVIEW.review;
102
+ const ci = CI[pr.ci] || CI.none;
103
+ spans.push({ text: `#${pr.number}`, style: { fg: '36', bold: pr.current } }); // cyan identity; current branch bold
104
+ spans.push({ text: ' ', style: undefined });
105
+ spans.push({ text: pr.title, style: pr.isDraft ? { dim: true } : undefined });
106
+ if (pr.isDraft) spans.push({ text: ' (draft)', style: { fg: '90', dim: true } });
107
+
108
+ const age = relAge(pr.updatedAt, now);
109
+ /** @type {import('../../core/tui/draw.js').Span[]} */
110
+ const right = [
111
+ { text: rv.glyph, style: { fg: rv.fg } }, // review (color + glyph)
112
+ { text: ' ', style: undefined },
113
+ { text: ci.glyph, style: { fg: ci.fg } }, // CI rollup (color + glyph)
114
+ ];
115
+ if (age) {
116
+ right.push({ text: ' ', style: undefined });
117
+ right.push({ text: age, style: { fg: '90', dim: true } });
118
+ }
119
+ return { spans, right };
120
+ }
121
+
122
+ // ── Header zone (drawn manually above the board list) ──────────────────────────
123
+
124
+ /**
125
+ * Visible (column) width of a span group — ANSI-free cell count. Mirrors
126
+ * draw.ts's internal `spanWidth` so the header can reserve room for a
127
+ * right-flushed group before clipping the left line.
128
+ * @param {import('../../core/tui/draw.js').Span[]} spans @returns {number}
129
+ */
130
+ function spanWidth(spans) {
131
+ let n = 0;
132
+ for (const s of spans) n += Array.from(s.text).length;
133
+ return n;
134
+ }
135
+
136
+ /**
137
+ * Paint the always-on header gauges into the top of `content`: branch line then
138
+ * commit line, with right-flushed metadata. Returns the number of rows consumed
139
+ * (incl. the trailing hairline) so render can place the board below it.
140
+ * @param {GitState} g
141
+ * @param {import('../../core/tui/draw.js').Draw} draw
142
+ * @param {import('../../core/tui/draw.js').Rect} content
143
+ * @param {number} now
144
+ * @returns {number} rows consumed
145
+ */
146
+ function drawHeader(g, draw, content, now) {
147
+ const right = content.col + content.width; // exclusive right edge for spansRight
148
+ let r = content.row;
149
+
150
+ // ── Branch line: ⎇ branch → upstream ↑N ↓M ····· [tree chip right-flush]
151
+ /** @type {import('../../core/tui/draw.js').Span[]} */
152
+ const left = [
153
+ { text: ' ', style: undefined },
154
+ { text: '⎇ ', style: { fg: '36' } },
155
+ { text: g.branch, style: { fg: '36', bold: true } }, // cyan identity
156
+ ];
157
+ if (g.upstream) {
158
+ left.push({ text: ` → ${g.upstream}`, style: { fg: '90', dim: true } });
159
+ if (g.ahead) left.push({ text: ` ↑${g.ahead}`, style: { fg: '32' } }); // green ahead-ok
160
+ if (g.behind) left.push({ text: ` ↓${g.behind}`, style: { fg: '33' } }); // yellow attention
161
+ if (!g.ahead && !g.behind) left.push({ text: ' ✓ up to date', style: { fg: '32' } });
162
+ } else {
163
+ left.push({ text: ' · no upstream', style: { fg: '90', dim: true } });
164
+ }
165
+ // Tree chip, right-flushed on the same row (clean = green ✓, else colored counts).
166
+ /** @type {import('../../core/tui/draw.js').Span[]} */
167
+ const chip = [];
168
+ if (g.files.length === 0) {
169
+ chip.push({ text: '✓ clean', style: { fg: '32' } });
170
+ } else {
171
+ const c = g.counts;
172
+ /** @param {number} n @param {string} word @param {string} glyph @param {string} fg */
173
+ const seg = (n, word, glyph, fg) => {
174
+ if (!n) return;
175
+ if (chip.length) chip.push({ text: ' ', style: undefined });
176
+ chip.push({ text: `${glyph} ${n} ${word}`, style: { fg } });
177
+ };
178
+ seg(c.conflict, 'conflict', '✗', '31');
179
+ seg(c.staged, 'staged', '●', '32');
180
+ seg(c.modified, 'modified', '○', '33');
181
+ seg(c.untracked, 'untracked', '?', '90');
182
+ }
183
+ // Clip the left line so the right-flushed chip never overpaints its tail
184
+ // (mirror draw.list's leftLimit = width − rightWidth − 1).
185
+ const chipW = spanWidth(chip);
186
+ draw.spans(r, content.col, left, chipW ? Math.max(0, content.width - chipW - 1) : content.width);
187
+ draw.spansRight(r, right, chip);
188
+ r++;
189
+
190
+ // ── Commit line: ⊙ sha subject ............................ age (right-flush)
191
+ if (g.lastCommit) {
192
+ /** @type {import('../../core/tui/draw.js').Span[]} */
193
+ const commit = [
194
+ { text: ' ', style: undefined },
195
+ { text: '⊙ ', style: { fg: '90', dim: true } },
196
+ { text: g.lastCommit.sha, style: { fg: '33' } }, // yellow sha (git-native)
197
+ { text: ' ', style: undefined },
198
+ { text: g.lastCommit.subject, style: undefined },
199
+ ];
200
+ // Build the right-flushed age first, then clip the subject so the age never
201
+ // overpaints it (mirror draw.list's leftLimit).
202
+ const age = relAge(g.lastCommit.when, now);
203
+ /** @type {import('../../core/tui/draw.js').Span[]} */
204
+ const ageSpans = age ? [{ text: age, style: { fg: '90', dim: true } }] : [];
205
+ const ageW = spanWidth(ageSpans);
206
+ draw.spans(r, content.col, commit, ageW ? Math.max(0, content.width - ageW - 1) : content.width);
207
+ if (age) draw.spansRight(r, right, ageSpans);
208
+ } else {
209
+ draw.spans(r, content.col, [
210
+ { text: ' ', style: undefined },
211
+ { text: '⊙ ', style: { fg: '90', dim: true } },
212
+ { text: 'no commits yet', style: { dim: true } },
213
+ ]);
214
+ }
215
+ r++;
216
+
217
+ // Hairline separating the gauges from the board (figure-ground, §3).
218
+ draw.hline(r, content.col, content.col + content.width);
219
+ r++;
220
+
221
+ return r - content.row;
222
+ }
223
+
224
+ // ── render ─────────────────────────────────────────────────────────────────────
225
+
226
+ /**
227
+ * Paint the header + board, or one of the four standard states. Pure (reads
228
+ * state, calls draw.*); the only write is storing draw.list's adjusted scroll.
229
+ * @param {GitPrState} state
230
+ * @param {import('../../core/tui/draw.js').Draw} draw
231
+ * @param {import('../../core/tui/draw.js').Rect} content
232
+ */
233
+ export function render(state, draw, content) {
234
+ if (content.width <= 0 || content.height <= 0) return;
235
+
236
+ // ── Whole-screen takeovers (no git data to anchor a header) ──
237
+ // The copy comes from the typed SourceError's `display` VERBATIM (the contract
238
+ // display/kind split — we never branch on `kind`); only the glyph + hue are a
239
+ // presentation map off `display.level` (action → ⊙ yellow, error → ⚠ red).
240
+ if (!state.git) {
241
+ if (state.gitErr) {
242
+ const d = state.gitErr.display;
243
+ const g = d.level === 'action' ? { glyph: '⊙', fg: '33' } : { glyph: '⚠', fg: '31' };
244
+ notReadyState(draw, content, {
245
+ glyph: g.glyph,
246
+ glyphFg: g.fg,
247
+ headline: d.headline,
248
+ explanation: d.explanation,
249
+ nextStep: d.nextStep,
250
+ });
251
+ return;
252
+ }
253
+ // No error yet → first-load loading skeleton.
254
+ loadingState(draw, content, { rows: Math.min(5, content.height), label: 'Reading git…' });
255
+ return;
256
+ }
257
+
258
+ const now = Date.now();
259
+ const g = state.git;
260
+
261
+ // ── Header gauges (always on when we have git data) ──
262
+ // Need ≥3 rows for the 2-row header + hairline; below that, skip straight to
263
+ // the board so a tiny pane still shows the changes.
264
+ let bodyRow = content.row;
265
+ let bodyHeight = content.height;
266
+ if (content.height >= 4) {
267
+ const used = drawHeader(g, draw, content, now);
268
+ bodyRow = content.row + used + (content.height - used > 2 ? 1 : 0); // 1-row section gap when there's room
269
+ bodyHeight = content.row + content.height - bodyRow;
270
+ }
271
+ if (bodyHeight <= 0) return;
272
+ const bodyRect = { row: bodyRow, col: content.col, width: content.width, height: bodyHeight };
273
+
274
+ // ── Empty reward: clean tree + no open PRs + gh healthy → "All clear" ──
275
+ if (g.files.length === 0 && state.prs.length === 0 && !state.prNote) {
276
+ emptyState(draw, bodyRect, {
277
+ headline: 'All clear',
278
+ secondary: [`${g.branch} — clean working tree, no open PRs.`, 'Press g to refresh.'],
279
+ });
280
+ return;
281
+ }
282
+
283
+ // ── The board ──
284
+ const items = state.board.map((row) => rowToItem(row, now));
285
+ const res = draw.list(bodyRect, items, state.cursor, state.scroll);
286
+ state.scroll = res.scroll; // store adjusted scroll back (Draw.list contract)
287
+ }
288
+
289
+ // ── keymap ───────────────────────────────────────────────────────────────
290
+
291
+ /**
292
+ * Read-only navigation: j/k move the cursor, g refreshes, q quits. No async
293
+ * actions from input — this is a monitor, not a controller. Footer hints come
294
+ * from these bindings' `hint` fields (the single source of truth).
295
+ * @type {import('../../core/view/contract.js').KeyBinding<GitPrState>[]}
296
+ */
297
+ export const keymap = [
298
+ { keys: ['j', 'down'], intent: 'cursorDown', hint: { keys: 'j/k', label: 'move' } },
299
+ { keys: ['k', 'up'], intent: 'cursorUp' },
300
+ { keys: ['g'], intent: 'refresh', hint: { keys: 'g', label: 'refresh' } },
301
+ { keys: ['q'], intent: 'quit', hint: { keys: 'q', label: 'quit' } },
302
+ ];
@@ -0,0 +1,216 @@
1
+ // @ts-check
2
+ /**
3
+ * Git / PR board — the WEB presenter for the `git-pr` view (React + Tailwind).
4
+ * Browser-only: consumed solely by the web serve path (Vite owns JSX +
5
+ * Tailwind); NEVER Node-imported. The default export is a pure function of
6
+ * `state`; DOM events call `dispatch(intentName, payload?)`.
7
+ *
8
+ * Same logical model as the TUI presenter (`state.board` + `state.cursor`) read
9
+ * from the SAME portable `core.mjs` — zero shared rendering code with `tui.mjs`
10
+ * (the contract's accepted hard fork: `draw.*` has no web analog). The outer
11
+ * chrome (title / status / banner / state chip) is rendered by `<ViewChrome>`,
12
+ * which wraps this component — so do NOT render it here.
13
+ *
14
+ * @module git-pr/web
15
+ */
16
+
17
+ import { relAge } from './core.mjs';
18
+ import { Loading, NotReady, ErrorState, Empty } from '@crouton-kit/crouter/web';
19
+
20
+ /** @typedef {import('./core.mjs').GitPrState} GitPrState */
21
+
22
+ // ── Status vocabulary (the web analog of tui.mjs's NUMERIC-SGR maps) ──────────
23
+
24
+ const FILE_GLYPH = {
25
+ staged: { glyph: '●', cls: 'text-green-600' },
26
+ modified: { glyph: '○', cls: 'text-amber-600' },
27
+ untracked: { glyph: '?', cls: 'text-slate-400' },
28
+ conflict: { glyph: '✗', cls: 'text-red-600' },
29
+ };
30
+ const REVIEW = {
31
+ approved: { glyph: '✓', cls: 'text-green-600' },
32
+ changes: { glyph: '✗', cls: 'text-red-600' },
33
+ review: { glyph: '◌', cls: 'text-slate-400' },
34
+ };
35
+ const CI = {
36
+ pass: { glyph: '✓', cls: 'text-green-600' },
37
+ fail: { glyph: '✗', cls: 'text-red-600' },
38
+ pending: { glyph: '⟳', cls: 'text-amber-600' },
39
+ none: { glyph: '·', cls: 'text-slate-400' },
40
+ };
41
+
42
+ // ── Header gauges (the web analog of tui.mjs's drawHeader) ─────────────────────
43
+
44
+ /** @param {{ g: import('./core.mjs').GitState }} props */
45
+ function Header({ g }) {
46
+ const now = Date.now();
47
+ return (
48
+ <div className="border-b border-slate-200 pb-2">
49
+ <div className="flex items-baseline gap-2">
50
+ <span className="font-semibold text-cyan-700">⎇ {g.branch}</span>
51
+ {g.upstream ? (
52
+ <span className="text-slate-400">
53
+ → {g.upstream}
54
+ {g.ahead ? <span className="ml-1 text-green-600">↑{g.ahead}</span> : null}
55
+ {g.behind ? <span className="ml-1 text-amber-600">↓{g.behind}</span> : null}
56
+ {!g.ahead && !g.behind ? <span className="ml-1 text-green-600">✓ up to date</span> : null}
57
+ </span>
58
+ ) : (
59
+ <span className="text-slate-400">· no upstream</span>
60
+ )}
61
+ <span className="ml-auto">{treeChip(g)}</span>
62
+ </div>
63
+ <div className="mt-0.5 text-slate-600">
64
+ {g.lastCommit ? (
65
+ <span className="flex items-baseline gap-2">
66
+ <span className="text-slate-400">⊙</span>
67
+ <span className="text-amber-600">{g.lastCommit.sha}</span>
68
+ <span className="truncate">{g.lastCommit.subject}</span>
69
+ <span className="ml-auto shrink-0 text-slate-400">{relAge(g.lastCommit.when, now)}</span>
70
+ </span>
71
+ ) : (
72
+ <span className="text-slate-400">⊙ no commits yet</span>
73
+ )}
74
+ </div>
75
+ </div>
76
+ );
77
+ }
78
+
79
+ /** @param {import('./core.mjs').GitState} g */
80
+ function treeChip(g) {
81
+ if (g.files.length === 0) return <span className="text-green-600">✓ clean</span>;
82
+ const c = g.counts;
83
+ /** @type {Array<{n:number, word:string, glyph:string, cls:string}>} */
84
+ const segs = [
85
+ { n: c.conflict, word: 'conflict', glyph: '✗', cls: 'text-red-600' },
86
+ { n: c.staged, word: 'staged', glyph: '●', cls: 'text-green-600' },
87
+ { n: c.modified, word: 'modified', glyph: '○', cls: 'text-amber-600' },
88
+ { n: c.untracked, word: 'untracked', glyph: '?', cls: 'text-slate-400' },
89
+ ].filter((s) => s.n);
90
+ return (
91
+ <span className="flex gap-3">
92
+ {segs.map((s, i) => (
93
+ <span key={i} className={s.cls}>{s.glyph} {s.n} {s.word}</span>
94
+ ))}
95
+ </span>
96
+ );
97
+ }
98
+
99
+ // ── Board rows (the web analog of tui.mjs's rowToItem) ─────────────────────────
100
+
101
+ /** @param {{ row: import('./core.mjs').BoardRow, selected: boolean, onClick: () => void }} props */
102
+ function BoardRow({ row, selected, onClick }) {
103
+ const now = Date.now();
104
+ const base = `flex items-baseline gap-1.5 rounded px-2 py-0.5 ${selected ? 'bg-slate-200' : ''}`;
105
+
106
+ if (row.kind === 'label') {
107
+ return <li className="px-2 pt-2 pb-0.5 text-xs uppercase tracking-wide text-slate-400">{row.text}</li>;
108
+ }
109
+ if (row.kind === 'spacer') {
110
+ return <li className="h-2" aria-hidden />;
111
+ }
112
+ if (row.kind === 'clean') {
113
+ return (
114
+ <li className={base} onClick={onClick}>
115
+ <span className="text-green-600">✓</span>
116
+ <span className="text-slate-400">nothing to commit, working tree clean</span>
117
+ </li>
118
+ );
119
+ }
120
+ if (row.kind === 'note') {
121
+ return (
122
+ <li className={base} onClick={onClick}>
123
+ <span className="text-slate-400">·</span>
124
+ <span className="text-slate-400">{row.text}</span>
125
+ </li>
126
+ );
127
+ }
128
+ if (row.kind === 'file') {
129
+ const f = row.file;
130
+ const g = FILE_GLYPH[f.cls] || FILE_GLYPH.modified;
131
+ return (
132
+ <li className={base} onClick={onClick}>
133
+ <span className={g.cls}>{g.glyph}</span>
134
+ <span className="text-slate-400">{f.xy.replace(/ /g, '·')}</span>
135
+ <span className={f.cls === 'conflict' ? 'text-red-600' : ''}>{f.path}</span>
136
+ {f.add || f.del ? (
137
+ <span className="ml-auto shrink-0 text-slate-400">+{f.add} −{f.del}</span>
138
+ ) : null}
139
+ </li>
140
+ );
141
+ }
142
+ // row.kind === 'pr'
143
+ const pr = row.pr;
144
+ const rv = REVIEW[pr.review] || REVIEW.review;
145
+ const ci = CI[pr.ci] || CI.none;
146
+ const age = relAge(pr.updatedAt, now);
147
+ return (
148
+ <li className={base} onClick={onClick}>
149
+ <span className={`text-cyan-700 ${pr.current ? 'font-bold' : ''}`}>#{pr.number}</span>
150
+ <span className={pr.isDraft ? 'text-slate-400' : ''}>{pr.title}</span>
151
+ {pr.isDraft ? <span className="text-slate-400">(draft)</span> : null}
152
+ <span className="ml-auto flex shrink-0 items-baseline gap-2">
153
+ <span className={rv.cls}>{rv.glyph}</span>
154
+ <span className={ci.cls}>{ci.glyph}</span>
155
+ {age ? <span className="text-slate-400">{age}</span> : null}
156
+ </span>
157
+ </li>
158
+ );
159
+ }
160
+
161
+ // ── The view ───────────────────────────────────────────────────────────────────
162
+
163
+ /**
164
+ * @param {import('../../core/view/contract.js').ViewProps<GitPrState>} props
165
+ */
166
+ export default function GitPr({ state, dispatch }) {
167
+ // ── Whole-view takeovers (no git data to anchor a header). The copy comes
168
+ // from the typed SourceError's `display` VERBATIM (the contract display/kind
169
+ // split — we never branch on `kind`); only the four-state component is a
170
+ // presentation map off `display.level` so the hue matches the TUI (error →
171
+ // red ErrorState, action → amber NotReady). ──
172
+ if (!state.git) {
173
+ if (state.gitErr) {
174
+ const d = state.gitErr.display;
175
+ const Takeover = d.level === 'error' ? ErrorState : NotReady;
176
+ return (
177
+ <Takeover
178
+ headline={d.headline}
179
+ explanation={d.explanation}
180
+ nextStep={d.nextStep}
181
+ onRetry={() => dispatch('refresh')}
182
+ />
183
+ );
184
+ }
185
+ // No error yet → first-load loading state.
186
+ return <Loading label="Reading git…" />;
187
+ }
188
+
189
+ const g = state.git;
190
+
191
+ // ── Empty reward: clean tree + no open PRs + gh healthy → "All clear". ──
192
+ const allClear = g.files.length === 0 && state.prs.length === 0 && !state.prNote;
193
+
194
+ return (
195
+ <div
196
+ className="font-mono text-sm outline-none"
197
+ tabIndex={0}
198
+ onKeyDown={(e) => {
199
+ if (e.key === 'j' || e.key === 'ArrowDown') { dispatch('cursorDown'); e.preventDefault(); }
200
+ else if (e.key === 'k' || e.key === 'ArrowUp') { dispatch('cursorUp'); e.preventDefault(); }
201
+ else if (e.key === 'g') { dispatch('refresh'); }
202
+ }}
203
+ >
204
+ <Header g={g} />
205
+ {allClear ? (
206
+ <Empty label={`${g.branch} — clean working tree, no open PRs.`} />
207
+ ) : (
208
+ <ul className="mt-1">
209
+ {state.board.map((row, i) => (
210
+ <BoardRow key={i} row={row} selected={i === state.cursor} onClick={() => dispatch('select', i)} />
211
+ ))}
212
+ </ul>
213
+ )}
214
+ </div>
215
+ );
216
+ }
@@ -2,9 +2,7 @@
2
2
  /**
3
3
  * Shared render helpers for the combined `inbox` view.
4
4
  *
5
- * These are COPIED VERBATIM-IN-BEHAVIOR from `../../linkedin/view.mjs` so the
6
- * standalone `linkedin` view stays untouched (the contract forbids refactoring
7
- * it). Self-contained ESM, Node-builtins only, imports NOTHING from crtr — the
5
+ * Self-contained ESM, Node-builtins only, imports NOTHING from crtr the
8
6
  * `inbox` view + its source adapters import these RELATIVELY.
9
7
  *
10
8
  * Discipline (design §2): all hue is NUMERIC SGR codes; color never carries