@pellux/goodvibes-tui 2.0.21 → 2.1.1

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 (391) hide show
  1. package/CHANGELOG.md +65 -1
  2. package/README.md +30 -29
  3. package/docs/README.md +8 -5
  4. package/docs/foundation-artifacts/operator-contract.json +1 -1
  5. package/package.json +28 -19
  6. package/src/cli/management.ts +5 -2
  7. package/src/cli/tui-startup.ts +34 -39
  8. package/src/core/agent-view-render.ts +157 -0
  9. package/src/core/approval-alert.ts +49 -11
  10. package/src/core/bookmark-navigation.ts +7 -1
  11. package/src/core/budget-breach-notifier.ts +27 -8
  12. package/src/core/compaction-receipt.ts +1 -1
  13. package/src/core/context-usage.ts +1 -1
  14. package/src/core/conversation-compaction-render.ts +3 -3
  15. package/src/core/conversation-display-only.ts +86 -0
  16. package/src/core/conversation-event-navigation.ts +27 -0
  17. package/src/core/conversation-line-cache.ts +216 -447
  18. package/src/core/conversation-render-context.ts +20 -27
  19. package/src/core/conversation-rendering.ts +157 -575
  20. package/src/core/conversation-splash-state.ts +6 -1
  21. package/src/core/conversation-types.ts +35 -0
  22. package/src/core/conversation-usage.ts +12 -0
  23. package/src/core/conversation.ts +130 -131
  24. package/src/core/long-task-notifier.ts +100 -31
  25. package/src/core/notices.ts +75 -0
  26. package/src/core/pairing-tailscale-gateway.ts +1 -1
  27. package/src/core/process-output-log.ts +143 -0
  28. package/src/core/rewind-receipt.ts +1 -1
  29. package/src/core/session-focus.ts +190 -0
  30. package/src/core/session-resume-core.ts +23 -81
  31. package/src/core/stream-event-wiring.ts +31 -5
  32. package/src/core/system-message-noise.ts +2 -2
  33. package/src/core/system-message-router.ts +16 -18
  34. package/src/core/turn-event-wiring.ts +272 -33
  35. package/src/core/work-tree-focus.ts +290 -0
  36. package/src/core/work-tree-fold-store.ts +143 -0
  37. package/src/core/work-tree-model.ts +799 -0
  38. package/src/core/work-tree-render.ts +384 -0
  39. package/src/core/work-tree-sources.ts +184 -0
  40. package/src/core/work-tree-wiring.ts +176 -0
  41. package/src/core/workstream-notification.ts +36 -6
  42. package/src/core/wrfc-notice-titles.ts +37 -0
  43. package/src/input/agents-modal-text.ts +119 -0
  44. package/src/input/agents-modal.ts +699 -0
  45. package/src/input/bookmark-modal.ts +33 -9
  46. package/src/input/changes-git.ts +251 -0
  47. package/src/input/changes-modal.ts +629 -0
  48. package/src/input/changes-model.ts +125 -0
  49. package/src/input/command-palette-types.ts +41 -0
  50. package/src/input/command-palette.ts +272 -0
  51. package/src/input/command-reference.ts +2 -2
  52. package/src/input/command-registry.ts +77 -35
  53. package/src/input/commands/acp-runtime.ts +19 -9
  54. package/src/input/commands/channel-runtime.ts +2 -2
  55. package/src/input/commands/ci-runtime.ts +1 -1
  56. package/src/input/commands/codebase-runtime.ts +1 -1
  57. package/src/input/commands/config.ts +7 -1
  58. package/src/input/commands/context-window.ts +25 -19
  59. package/src/input/commands/control-room-runtime.ts +18 -18
  60. package/src/input/commands/conversation-runtime.ts +0 -1
  61. package/src/input/commands/cost-runtime.ts +29 -20
  62. package/src/input/commands/diff-runtime.ts +35 -181
  63. package/src/input/commands/eval.ts +1 -1
  64. package/src/input/commands/experience-runtime.ts +3 -7
  65. package/src/input/commands/fresh-conversation.ts +86 -0
  66. package/src/input/commands/git-runtime.ts +5 -22
  67. package/src/input/commands/graph-runtime.ts +1 -1
  68. package/src/input/commands/guidance-runtime.ts +1 -1
  69. package/src/input/commands/health-runtime.ts +8 -9
  70. package/src/input/commands/hooks-runtime.ts +2 -2
  71. package/src/input/commands/hosted-runtime.ts +3 -3
  72. package/src/input/commands/incident-runtime.ts +3 -3
  73. package/src/input/commands/intelligence-runtime.ts +6 -9
  74. package/src/input/commands/knowledge.ts +2 -2
  75. package/src/input/commands/legacy-panel-command.ts +57 -0
  76. package/src/input/commands/local-auth-runtime.ts +8 -9
  77. package/src/input/commands/local-provider-runtime.ts +7 -4
  78. package/src/input/commands/local-runtime.ts +12 -15
  79. package/src/input/commands/local-setup-review.ts +1 -1
  80. package/src/input/commands/local-setup.ts +13 -19
  81. package/src/input/commands/memory.ts +1 -1
  82. package/src/input/commands/notify-runtime.ts +9 -0
  83. package/src/input/commands/operator-runtime.ts +10 -12
  84. package/src/input/commands/permissions-provenance.ts +3 -3
  85. package/src/input/commands/planning-runtime.ts +7 -7
  86. package/src/input/commands/platform-sandbox-runtime.ts +2 -2
  87. package/src/input/commands/platform-services-runtime.ts +1 -1
  88. package/src/input/commands/policy-dispatch.ts +2 -2
  89. package/src/input/commands/policy.ts +3 -3
  90. package/src/input/commands/provider.ts +1 -1
  91. package/src/input/commands/remote-runtime.ts +1 -1
  92. package/src/input/commands/review-runtime.ts +39 -94
  93. package/src/input/commands/rewind-runtime.ts +81 -108
  94. package/src/input/commands/runtime-services.ts +15 -17
  95. package/src/input/commands/services-runtime.ts +2 -2
  96. package/src/input/commands/session-content.ts +21 -5
  97. package/src/input/commands/session-workflow.ts +13 -38
  98. package/src/input/commands/settings-sync-runtime.ts +2 -2
  99. package/src/input/commands/shell-core.ts +19 -4
  100. package/src/input/commands/skills-runtime.ts +2 -2
  101. package/src/input/commands/status-runtime.ts +13 -0
  102. package/src/input/commands/subscription-runtime.ts +1 -1
  103. package/src/input/commands/tasks-runtime.ts +2 -8
  104. package/src/input/commands/work-plan-runtime.ts +4 -4
  105. package/src/input/commands/workstream-attempts.ts +24 -47
  106. package/src/input/commands/workstream-runtime.ts +2 -2
  107. package/src/input/commands/worktree-runtime.ts +3 -3
  108. package/src/input/commands.ts +3 -1
  109. package/src/input/composer-editor.ts +44 -0
  110. package/src/input/concealed-input.ts +1 -1
  111. package/src/input/config-modal-types.ts +20 -8
  112. package/src/input/config-modal.ts +106 -105
  113. package/src/input/confirm-dialog-types.ts +28 -0
  114. package/src/input/confirm-dialog.ts +70 -0
  115. package/src/input/delete-key-policy.ts +3 -3
  116. package/src/input/feed-context-factory.ts +15 -16
  117. package/src/input/handler-command-route.ts +7 -42
  118. package/src/input/handler-content-actions.ts +16 -15
  119. package/src/input/handler-feed-routes.ts +26 -211
  120. package/src/input/handler-feed.ts +68 -95
  121. package/src/input/handler-interactions.ts +5 -3
  122. package/src/input/handler-modal-routes.ts +215 -216
  123. package/src/input/handler-modal-stack.ts +81 -11
  124. package/src/input/handler-modal-token-routes.ts +4 -0
  125. package/src/input/handler-onboarding.ts +0 -1
  126. package/src/input/handler-picker-routes.ts +65 -107
  127. package/src/input/handler-session-view-route.ts +149 -0
  128. package/src/input/handler-shortcuts.ts +16 -154
  129. package/src/input/handler-types.ts +4 -2
  130. package/src/input/handler-ui-state.ts +45 -15
  131. package/src/input/handler-work-tree-route.ts +123 -0
  132. package/src/input/handler.ts +25 -70
  133. package/src/input/keybindings.ts +16 -71
  134. package/src/input/masked-entry-modal.ts +117 -0
  135. package/src/input/mcp-workspace.ts +48 -17
  136. package/src/input/model-picker-filter.ts +8 -0
  137. package/src/input/model-picker-open.ts +99 -0
  138. package/src/input/model-picker-types.ts +1 -1
  139. package/src/input/model-picker.ts +92 -9
  140. package/src/input/notifications-modal.ts +123 -0
  141. package/src/input/overlay-filter.ts +26 -0
  142. package/src/input/profile-picker-modal.ts +37 -8
  143. package/src/input/search.ts +3 -3
  144. package/src/input/selection-modal-queue.ts +2 -0
  145. package/src/input/selection-modal.ts +28 -16
  146. package/src/input/session-picker-modal.ts +33 -8
  147. package/src/input/settings-modal-activation.ts +7 -2
  148. package/src/input/settings-modal-data.ts +11 -6
  149. package/src/input/settings-modal-types.ts +1 -1
  150. package/src/input/settings-modal.ts +20 -20
  151. package/src/input/submission-intent.ts +1 -1
  152. package/src/input/submission-router.ts +4 -9
  153. package/src/input/surface-modal-host.ts +110 -0
  154. package/src/input/theme-settings-actions.ts +82 -0
  155. package/src/input/usage-modal.ts +183 -0
  156. package/src/input/views.ts +97 -0
  157. package/src/main.ts +175 -165
  158. package/src/permissions/hunk-selection.ts +1 -1
  159. package/src/permissions/prompt-card.ts +511 -0
  160. package/src/permissions/prompt.ts +59 -622
  161. package/src/renderer/agents-modal.ts +310 -0
  162. package/src/renderer/autocomplete-overlay.ts +42 -220
  163. package/src/renderer/back-to-bottom.ts +80 -0
  164. package/src/renderer/block-actions-overlay.ts +30 -48
  165. package/src/renderer/bookmark-modal.ts +48 -79
  166. package/src/renderer/changes-modal.ts +325 -0
  167. package/src/renderer/code-block.ts +184 -106
  168. package/src/renderer/command-palette.ts +93 -0
  169. package/src/renderer/composer-fragments.ts +4 -4
  170. package/src/renderer/composer.ts +146 -0
  171. package/src/renderer/compositor.ts +26 -160
  172. package/src/renderer/config-modal.ts +185 -81
  173. package/src/renderer/confirm-dialog.ts +50 -0
  174. package/src/renderer/context-inspector.ts +116 -154
  175. package/src/renderer/context-status-hint.ts +4 -4
  176. package/src/renderer/conversation-layout.ts +20 -0
  177. package/src/renderer/conversation-overlays.ts +94 -84
  178. package/src/renderer/conversation-surface.ts +19 -38
  179. package/src/renderer/diff-tint.ts +98 -0
  180. package/src/renderer/file-picker-overlay.ts +63 -90
  181. package/src/renderer/footer-targets.ts +38 -0
  182. package/src/renderer/git-status.ts +3 -4
  183. package/src/renderer/header-line.ts +140 -117
  184. package/src/renderer/help-overlay.ts +133 -219
  185. package/src/renderer/history-search-overlay.ts +15 -12
  186. package/src/renderer/lane-graph/bead.ts +800 -0
  187. package/src/renderer/lane-graph/glyphs.ts +134 -0
  188. package/src/renderer/lane-graph/layout.ts +248 -0
  189. package/src/renderer/lane-graph/paint.ts +537 -0
  190. package/src/renderer/lane-graph/semantic-memo.ts +77 -0
  191. package/src/renderer/layout-engine.ts +2 -57
  192. package/src/renderer/layout.ts +7 -13
  193. package/src/renderer/markdown-inline.ts +2 -1
  194. package/src/renderer/markdown-table.ts +17 -12
  195. package/src/renderer/markdown.ts +92 -34
  196. package/src/renderer/masked-entry-modal.ts +78 -0
  197. package/src/renderer/mcp-workspace.ts +166 -203
  198. package/src/renderer/model-workspace.ts +381 -436
  199. package/src/renderer/notifications-modal.ts +116 -0
  200. package/src/renderer/onboarding/onboarding-wizard.ts +178 -588
  201. package/src/renderer/process-view.ts +192 -0
  202. package/src/renderer/profile-picker-modal.ts +58 -110
  203. package/src/renderer/search-overlay.ts +6 -3
  204. package/src/renderer/selection-modal-overlay.ts +87 -327
  205. package/src/renderer/semantic-diff.ts +2 -2
  206. package/src/renderer/session-chips.ts +102 -0
  207. package/src/renderer/session-picker-modal.ts +177 -297
  208. package/src/renderer/settings-modal-connections.ts +14 -23
  209. package/src/renderer/settings-modal-context.ts +367 -0
  210. package/src/renderer/settings-modal-helpers.ts +6 -11
  211. package/src/renderer/settings-modal.ts +322 -726
  212. package/src/renderer/shell-surface.ts +277 -175
  213. package/src/renderer/shortcuts-overlay.ts +217 -0
  214. package/src/renderer/status-line.ts +475 -0
  215. package/src/renderer/surface-compose.ts +174 -0
  216. package/src/renderer/surface-kit-extra.ts +303 -0
  217. package/src/renderer/surface-kit-list.ts +366 -0
  218. package/src/renderer/surface-kit-parts.ts +360 -0
  219. package/src/renderer/surface-kit.ts +568 -0
  220. package/src/renderer/syntax-highlighter.ts +280 -197
  221. package/src/renderer/syntax-theme.ts +51 -0
  222. package/src/renderer/system-message.ts +0 -1
  223. package/src/renderer/term-caps.ts +42 -1
  224. package/src/renderer/terminal-bg-probe.ts +116 -19
  225. package/src/renderer/terminal-palette-probe.ts +322 -0
  226. package/src/renderer/terminal-palette-reader.ts +183 -0
  227. package/src/renderer/terminal-palette.ts +58 -0
  228. package/src/renderer/theme-mode-config.ts +16 -1
  229. package/src/renderer/theme.ts +234 -207
  230. package/src/renderer/thinking.ts +0 -1
  231. package/src/renderer/throbber.ts +188 -0
  232. package/src/renderer/toast-center.ts +119 -0
  233. package/src/renderer/ui-factory.ts +39 -602
  234. package/src/renderer/ui-primitives.ts +8 -25
  235. package/src/renderer/usage-modal.ts +353 -0
  236. package/src/renderer/user-message.ts +58 -0
  237. package/src/renderer/voice-capture-chip.ts +53 -0
  238. package/src/runtime/bootstrap-command-context.ts +0 -5
  239. package/src/runtime/bootstrap-command-parts.ts +38 -89
  240. package/src/runtime/bootstrap-core.ts +13 -15
  241. package/src/runtime/bootstrap-hook-bridge.ts +12 -21
  242. package/src/runtime/bootstrap-notifier-sync.ts +17 -1
  243. package/src/runtime/bootstrap-shell.ts +16 -50
  244. package/src/runtime/bootstrap.ts +14 -14
  245. package/src/runtime/client/fleet-union.ts +5 -5
  246. package/src/runtime/client/hosted-exit.ts +2 -2
  247. package/src/runtime/client/hosted-sessions.ts +1 -1
  248. package/src/runtime/client/spine-adoption.ts +1 -1
  249. package/src/runtime/context-accounting-source.ts +5 -4
  250. package/src/runtime/index.ts +1 -0
  251. package/src/runtime/notification-dispatch.ts +102 -29
  252. package/src/runtime/recovery-offer-wiring.ts +4 -1
  253. package/src/runtime/recovery-prompt.ts +31 -15
  254. package/src/runtime/runtime-services-types.ts +10 -6
  255. package/src/runtime/services.ts +17 -12
  256. package/src/runtime/session-continuity-hints.ts +2 -5
  257. package/src/runtime/session-inbound-inputs.ts +1 -1
  258. package/src/runtime/session-resume-liveness-confirm.ts +14 -2
  259. package/src/runtime/startup-typeahead-gate.ts +103 -0
  260. package/src/runtime/store/domains/domain-read-matrix.ts +0 -1
  261. package/src/runtime/store/domains/index.ts +0 -8
  262. package/src/runtime/store/selectors/index.ts +0 -28
  263. package/src/runtime/store/state.ts +0 -8
  264. package/src/runtime/ui-services.ts +4 -6
  265. package/src/runtime/usage-tracker.ts +348 -0
  266. package/src/runtime/wrfc-persistence.ts +4 -4
  267. package/src/shell/blocking-input.ts +92 -2
  268. package/src/shell/recovery-input-helpers.ts +1 -1
  269. package/src/shell/session-views.ts +539 -0
  270. package/src/shell/status-report.ts +86 -0
  271. package/src/shell/transcript-scroll.ts +68 -0
  272. package/src/shell/ui-openers.ts +84 -172
  273. package/src/shell/view-openers.ts +205 -0
  274. package/src/tools/wrfc-agent-guard.ts +22 -2
  275. package/src/utils/format-duration.ts +4 -8
  276. package/src/utils/splash-lines.ts +9 -1
  277. package/src/utils/terminal-width.ts +56 -0
  278. package/src/verification/live-verifier.ts +0 -10
  279. package/src/version.ts +1 -1
  280. package/src/{panels → views}/agent-inspector-shared.ts +2 -2
  281. package/src/{panels → views}/builtin-modals.ts +22 -21
  282. package/src/views/builtin-views.ts +135 -0
  283. package/src/{panels → views}/confirm-state.ts +9 -9
  284. package/src/{panels → views}/diff-review-model.ts +10 -3
  285. package/src/{panels → views}/eval-registry.ts +1 -1
  286. package/src/{panels → views}/fleet-acts.ts +49 -85
  287. package/src/{panels/fleet-panel-format.ts → views/fleet-format.ts} +40 -40
  288. package/src/{panels → views}/fleet-gateway.ts +5 -5
  289. package/src/{panels → views}/fleet-observed-render.ts +16 -16
  290. package/src/{panels → views}/fleet-read-model.ts +9 -9
  291. package/src/{panels/fleet-tabs.ts → views/fleet-session-tabs.ts} +13 -13
  292. package/src/{panels → views}/fleet-spawn.ts +33 -49
  293. package/src/{panels → views}/fleet-steer.ts +12 -12
  294. package/src/{panels → views}/fleet-stop.ts +4 -4
  295. package/src/{panels → views}/fleet-transcript.ts +26 -27
  296. package/src/{panels/fleet-panel-worktree-detail.ts → views/fleet-worktree-detail.ts} +7 -7
  297. package/src/{panels → views}/hosted-session-feed.ts +6 -6
  298. package/src/views/modal-surface-registry.ts +41 -0
  299. package/src/{panels → views}/modals/devices-modal.ts +2 -2
  300. package/src/{panels → views}/modals/hooks-modal.ts +13 -13
  301. package/src/{panels → views}/modals/keybindings-modal.ts +19 -27
  302. package/src/{panels → views}/modals/knowledge-modal.ts +6 -5
  303. package/src/{panels → views}/modals/local-auth-modal.ts +22 -21
  304. package/src/{panels → views}/modals/marketplace-modal.ts +11 -11
  305. package/src/{panels → views}/modals/memory-modal.ts +8 -8
  306. package/src/{panels → views}/modals/modal-surface-helpers.ts +15 -5
  307. package/src/{panels → views}/modals/pairing-modal.ts +9 -9
  308. package/src/{panels → views}/modals/planning-modal.ts +15 -20
  309. package/src/{panels → views}/modals/plugins-modal.ts +6 -6
  310. package/src/{panels → views}/modals/policy-modal.ts +13 -5
  311. package/src/{panels → views}/modals/provider-health-modal.ts +4 -4
  312. package/src/{panels → views}/modals/remote-modal.ts +3 -3
  313. package/src/{panels → views}/modals/sandbox-modal.ts +4 -4
  314. package/src/{panels → views}/modals/security-modal.ts +22 -16
  315. package/src/{panels → views}/modals/services-modal.ts +3 -3
  316. package/src/{panels → views}/modals/settings-sync-modal.ts +4 -4
  317. package/src/{panels → views}/modals/skills-modal.ts +9 -9
  318. package/src/{panels → views}/modals/subscription-modal.ts +3 -3
  319. package/src/{panels → views}/modals/work-plan-modal.ts +10 -13
  320. package/src/views/notifications-feed.ts +314 -0
  321. package/src/{panels → views}/polish-core.ts +38 -36
  322. package/src/{panels → views}/polish-tables.ts +35 -26
  323. package/src/{panels → views}/polish.ts +101 -95
  324. package/src/{panels → views}/project-planning-answer-actions.ts +3 -3
  325. package/src/{panels → views}/provider-health-routes.ts +14 -14
  326. package/src/{panels → views}/search-focus.ts +8 -8
  327. package/src/{panels → views}/session-maintenance.ts +11 -11
  328. package/src/views/skills-discovery.ts +159 -0
  329. package/src/views/view-deps.ts +80 -0
  330. package/src/views/view-panel-adapter.ts +53 -0
  331. package/src/{panels → views}/workstream-graph-render.ts +1 -1
  332. package/src/core/conversation-fold.ts +0 -101
  333. package/src/core/conversation-turn-structure.ts +0 -464
  334. package/src/core/conversation-user-receipts.ts +0 -42
  335. package/src/input/commands/operator-panel-runtime.ts +0 -186
  336. package/src/input/panel-integration-actions.ts +0 -33
  337. package/src/input/panel-mouse-geometry.ts +0 -97
  338. package/src/input/panel-paste-flood-guard.ts +0 -86
  339. package/src/panels/base-panel.ts +0 -301
  340. package/src/panels/builtin/agent.ts +0 -27
  341. package/src/panels/builtin/development.ts +0 -92
  342. package/src/panels/builtin/knowledge.ts +0 -12
  343. package/src/panels/builtin/operations.ts +0 -177
  344. package/src/panels/builtin/session.ts +0 -81
  345. package/src/panels/builtin/shared.ts +0 -357
  346. package/src/panels/builtin-panels.ts +0 -28
  347. package/src/panels/cost-tracker-panel.ts +0 -746
  348. package/src/panels/diff-panel.ts +0 -797
  349. package/src/panels/diff-review-panel.ts +0 -395
  350. package/src/panels/expandable-list-panel.ts +0 -189
  351. package/src/panels/fleet-deep-link.ts +0 -31
  352. package/src/panels/fleet-panel.ts +0 -799
  353. package/src/panels/git-panel.ts +0 -800
  354. package/src/panels/hosted-session-panel.ts +0 -221
  355. package/src/panels/index.ts +0 -21
  356. package/src/panels/local-auth-panel.ts +0 -478
  357. package/src/panels/notifications-feed.ts +0 -127
  358. package/src/panels/notifications-panel.ts +0 -160
  359. package/src/panels/panel-confirm-overlay.ts +0 -72
  360. package/src/panels/panel-manager.ts +0 -791
  361. package/src/panels/plugins-panel.ts +0 -371
  362. package/src/panels/scrollable-list-panel.ts +0 -498
  363. package/src/panels/skills-panel.ts +0 -454
  364. package/src/panels/token-budget-panel.ts +0 -688
  365. package/src/panels/types.ts +0 -191
  366. package/src/panels/wrfc-panel-format.ts +0 -133
  367. package/src/renderer/diff-view.ts +0 -134
  368. package/src/renderer/fleet-tab-strip.ts +0 -56
  369. package/src/renderer/footer-tips.ts +0 -53
  370. package/src/renderer/fullscreen-primitives.ts +0 -146
  371. package/src/renderer/fullscreen-workspace.ts +0 -194
  372. package/src/renderer/hint-grammar.ts +0 -52
  373. package/src/renderer/modal-factory.ts +0 -480
  374. package/src/renderer/overlay-box.ts +0 -154
  375. package/src/renderer/overlay-viewport.ts +0 -104
  376. package/src/renderer/panel-composite.ts +0 -145
  377. package/src/renderer/panel-workspace-bar.ts +0 -54
  378. package/src/renderer/process-indicator.ts +0 -163
  379. package/src/renderer/tab-strip.ts +0 -222
  380. package/src/renderer/text-layout.ts +0 -31
  381. package/src/renderer/tool-call.ts +0 -276
  382. package/src/renderer/tool-result-expanded-lines.ts +0 -48
  383. package/src/renderer/tool-result-summary.ts +0 -219
  384. package/src/runtime/diagnostics/panels/index.ts +0 -21
  385. package/src/runtime/diagnostics/panels/ops.ts +0 -156
  386. package/src/runtime/perf/panel-contracts.ts +0 -32
  387. package/src/runtime/store/domains/panels.ts +0 -117
  388. package/src/verification/verification-ledger.ts +0 -707
  389. /package/src/{panels → views}/memory-consolidation-gateway.ts +0 -0
  390. /package/src/{panels → views}/modals/modal-theme.ts +0 -0
  391. /package/src/{panels → views}/provider-health-domains.ts +0 -0
@@ -1,59 +1,41 @@
1
1
  /**
2
- * renderBlockActionsMenu, renders the BlockActionsMenu (src/renderer/block-actions.ts)
3
- * as a docked overlay, so opening it via Enter-on-an-empty-composer actually
4
- * shows something. Follows the established docked-overlay pattern (see
5
- * selection-modal-overlay.ts / bookmark-modal.ts): ModalFactory sizes the box
6
- * to its content, and every text section wraps rather than clips, so the
7
- * block summary and action list are always shown in full.
2
+ * renderBlockActionsMenu, the block-actions menu (Enter on an empty composer)
3
+ * as a small centered kit dialog: the target block's summary wrapped in full,
4
+ * then one kit row per action with its key right-aligned, the selected action
5
+ * as the gradient row, and keycap hints. Each action's key fires it directly.
8
6
  */
9
7
 
10
- import { type Line } from '@pellux/goodvibes-sdk/platform/types';
11
- import { ModalFactory, type ModalSection } from './modal-factory.ts';
12
8
  import type { BlockActionsMenu } from './block-actions.ts';
13
9
  import { describeBlockForReceipt } from '../input/handler-content-actions.ts';
14
- import { getOverlaySurfaceMetrics } from './overlay-viewport.ts';
15
- import { formatHints } from './hint-grammar.ts';
10
+ import { activeTokens } from './theme.ts';
11
+ import { beginModal, finishModal, type KitHint, type SurfaceLayer } from './surface-kit.ts';
12
+ import { drawList, type KitRow } from './surface-kit-list.ts';
13
+ import { drawTextBlock, listHeight, modalHeightFor, modalTextWidth, textBlockHeight, type TextLine } from './surface-kit-extra.ts';
14
+
15
+ const HINTS: readonly KitHint[] = [['↑↓', 'move'], ['⏎', 'select']];
16
+ /** Preferred width: a menu, not a workspace. */
17
+ const MENU_WIDTH = 72;
16
18
 
17
19
  export function renderBlockActionsMenu(
18
20
  menu: BlockActionsMenu,
19
- width: number,
20
- viewportHeight = 24,
21
- ): Line[] {
22
- if (!menu.active || !menu.block) return [];
23
-
24
- const summary = describeBlockForReceipt(menu.block);
25
- const metrics = getOverlaySurfaceMetrics(width, viewportHeight, {
26
- margin: 4,
27
- maxWidth: 64,
28
- chromeRows: 5,
29
- minContentRows: 3,
30
- maxContentRows: Math.max(6, viewportHeight - 8),
31
- });
21
+ screenWidth: number,
22
+ screenHeight = 24,
23
+ ): SurfaceLayer | null {
24
+ if (!menu.active || !menu.block) return null;
25
+ const t = activeTokens();
32
26
 
33
- const sections: ModalSection[] = [
34
- { type: 'text', content: `Target: ${summary}`, style: { dim: true } },
35
- { type: 'separator' },
36
- {
37
- type: 'list',
38
- items: menu.actions.map((action, i) => ({
39
- label: `[${action.key}] ${action.label}`,
40
- selected: i === menu.selectedIndex,
41
- })),
42
- },
43
- ];
27
+ const target: TextLine[] = [{ text: `Target: ${describeBlockForReceipt(menu.block)}`, style: { fg: t.textMuted } }];
28
+ const rows: KitRow[] = menu.actions.map((action, i) => ({
29
+ label: action.label,
30
+ right: action.key === 'Tab' ? 'tab' : action.key,
31
+ selected: i === menu.selectedIndex,
32
+ }));
33
+ const width = modalTextWidth(screenWidth, screenHeight, MENU_WIDTH);
34
+ const body = textBlockHeight(target, width) + 1 + listHeight(rows, 0, width - 1);
35
+ const height = modalHeightFor(screenWidth, screenHeight, { width: MENU_WIDTH, hints: HINTS }, body);
44
36
 
45
- return ModalFactory.createModal(
46
- {
47
- title: 'Block Actions',
48
- width: metrics.boxWidth,
49
- margin: metrics.margin,
50
- sections,
51
- hints: [formatHints([
52
- { key: 'Up/Down', verb: 'Navigate' },
53
- { key: 'Enter', verb: 'Select' },
54
- { key: 'Esc', verb: 'Close' },
55
- ])],
56
- },
57
- width,
58
- );
37
+ const f = beginModal(screenWidth, screenHeight, { title: 'Block actions', hints: HINTS, width: MENU_WIDTH, height, center: true });
38
+ const y = drawTextBlock(f.canvas, f.l, f.top, f.r - f.l + 1, target, f.bottom) + 1;
39
+ drawList(f.canvas, { rows, top: y, bottom: f.bottom, x0: f.l, x1: f.r });
40
+ return finishModal(f);
59
41
  }
@@ -1,101 +1,70 @@
1
1
  /**
2
- * renderBookmarkModal, renders the /bookmarks modal as Line[] using ModalFactory.
2
+ * renderBookmarkModal, the /bookmarks modal drawn with the modal surface kit.
3
3
  *
4
- * Shows a list of bookmarked blocks with:
5
- * - label (block type + excerpt)
6
- * - timestamp (human-readable time)
7
- * Footer hints: [Up/Down] Navigate [Enter] Jump [o] Open File [d] Remove [Esc] Close
4
+ * Title with the selected position, the always-live search row (filters by
5
+ * key and label), one kit row per bookmark (label, the block key muted after
6
+ * it, the time right-aligned), a muted scroll count and keycap hints:
7
+ * ↑↓ move · ⏎ jump · o open file · d remove.
8
8
  */
9
9
 
10
- import { type Line } from '@pellux/goodvibes-sdk/platform/types';
11
- import { ModalFactory } from './modal-factory.ts';
12
10
  import { BookmarkModal } from '../input/bookmark-modal.ts';
13
11
  import type { BookmarkEntry } from '@pellux/goodvibes-sdk/platform/bookmarks';
14
- import { getOverlayContentBudget, getStableOverlayContentRows } from './overlay-viewport.ts';
15
-
16
- // ---------------------------------------------------------------------------
17
- // Helpers
18
- // ---------------------------------------------------------------------------
12
+ import { activeTokens } from './theme.ts';
13
+ import {
14
+ beginModal,
15
+ drawWrapped,
16
+ finishModal,
17
+ searchRow,
18
+ scrollCountText,
19
+ type KitHint,
20
+ type SurfaceLayer,
21
+ } from './surface-kit.ts';
22
+ import { drawList, type KitRow } from './surface-kit-list.ts';
23
+ import { listHeight, modalHeightFor, modalTextWidth } from './surface-kit-extra.ts';
19
24
 
20
25
  function formatTime(ts: number): string {
21
26
  const d = new Date(ts);
22
27
  return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
23
28
  }
24
29
 
25
- function entryLabel(entry: BookmarkEntry): string {
26
- const time = formatTime(entry.timestamp);
27
- // Pad the key to a consistent width (truncated to 28 chars)
28
- const keyPart = entry.key.length > 28
29
- ? entry.key.slice(0, 25) + '...'
30
- : entry.key.padEnd(28);
31
- const labelPart = entry.label.length > 30
32
- ? entry.label.slice(0, 27) + '...'
33
- : entry.label;
34
- return `${keyPart} ${labelPart} ${time}`;
30
+ function rowFor(entry: BookmarkEntry, selected: boolean): KitRow {
31
+ return { label: entry.label, desc: entry.key, right: formatTime(entry.timestamp), selected };
35
32
  }
36
33
 
37
- // ---------------------------------------------------------------------------
38
- // Renderer
39
- // ---------------------------------------------------------------------------
34
+ const HINTS: readonly KitHint[] = [['↑↓', 'move'], ['⏎', 'jump'], ['o', 'open file'], ['d', 'remove']];
40
35
 
41
36
  /**
42
- * Render the bookmark modal as Line[] for overlay in the viewport.
43
- *
44
- * @param modal BookmarkModal state object.
45
- * @param width Terminal width.
37
+ * Render the bookmark modal as a SurfaceLayer in screen coordinates.
46
38
  */
47
39
  export function renderBookmarkModal(
48
40
  modal: BookmarkModal,
49
- width: number,
50
- viewportHeight = 24,
51
- ): Line[] {
52
- const visRows = getOverlayContentBudget(viewportHeight, {
53
- chromeRows: 5,
54
- minContentRows: 5,
55
- maxContentRows: 9,
56
- });
57
- const targetContentRows = getStableOverlayContentRows(visRows, 8);
58
- modal.setVisibleRows(visRows);
59
- const visible = modal.entries.slice(modal.scrollOffset, modal.scrollOffset + visRows);
60
- const relSelected = Math.max(0, modal.selectedIndex - modal.scrollOffset);
41
+ screenWidth: number,
42
+ screenHeight = 24,
43
+ ): SurfaceLayer {
44
+ const t = activeTokens();
45
+ const visible = modal.visibleEntries;
46
+ const rows = visible.map((entry, i) => rowFor(entry, i === modal.selectedIndex));
47
+ // Size to every bookmark (not the filtered set) so typing never resizes the modal.
48
+ const width = modalTextWidth(screenWidth, screenHeight);
49
+ const allRows = modal.entries.map((entry) => rowFor(entry, false));
50
+ const body = 2 + Math.max(1, listHeight(allRows, 0, width - 1));
51
+ const height = modalHeightFor(screenWidth, screenHeight, { hints: HINTS }, body);
61
52
 
62
- const items = visible.length === 0
63
- ? [{ label: 'No bookmarks - use Ctrl+B to bookmark a block', selected: false }]
64
- : visible.map((entry, i) => ({
65
- label: entryLabel(entry),
66
- selected: i === relSelected,
67
- }));
68
- const sections: import('./modal-factory.ts').ModalSection[] = [
69
- {
70
- type: 'text',
71
- content: ' Key Label Time',
72
- style: { dim: true },
73
- },
74
- { type: 'separator' },
75
- { type: 'list', items },
76
- ];
77
- if (modal.entries.length > visRows) {
78
- sections.push({ type: 'separator' });
79
- sections.push({
80
- type: 'text',
81
- content: `[${modal.scrollOffset + 1}-${Math.min(modal.entries.length, modal.scrollOffset + visRows)} of ${modal.entries.length}]`,
82
- style: { fg: '244', dim: true },
83
- });
84
- }
53
+ const position = visible.length > 0 ? `${Math.min(modal.selectedIndex + 1, visible.length)}/${visible.length}` : '';
54
+ const f = beginModal(screenWidth, screenHeight, { title: 'Bookmarks', sub: position, hints: HINTS, height });
55
+ const total = modal.entries.length;
56
+ const count = modal.query ? `${visible.length} of ${total}` : `${total} saved`;
57
+ searchRow(f, f.top, modal.query, 'Filter bookmarks', count);
85
58
 
86
- // Scroll indicator in title
87
- const totalStr = modal.entries.length > 0
88
- ? `${modal.selectedIndex + 1}/${modal.entries.length}`
89
- : '0';
90
-
91
- return ModalFactory.createModal(
92
- {
93
- title: `Bookmarks ${totalStr}`,
94
- width: 80,
95
- targetContentRows,
96
- sections,
97
- hints: ['Up/Down Navigate', 'Enter Jump', 'o Open file', 'd Remove', 'Esc Close'],
98
- },
99
- width,
100
- );
59
+ const top = f.top + 2;
60
+ if (rows.length === 0) {
61
+ const message = total === 0
62
+ ? 'No bookmarks yet. Press Ctrl+B on a block to bookmark it.'
63
+ : `No bookmarks match "${modal.query}".`;
64
+ drawWrapped(f.canvas, f.l, top, f.r - f.l + 1, message, { fg: t.textMuted }, f.bottom);
65
+ return finishModal(f);
66
+ }
67
+ const res = drawList(f.canvas, { rows, top, bottom: f.bottom, x0: f.l, x1: f.r });
68
+ f.hintRight = scrollCountText(res.above, res.below);
69
+ return finishModal(f);
101
70
  }
@@ -0,0 +1,325 @@
1
+ /**
2
+ * renderChangesModal, files, a tinted diff and review actions (replaces the
3
+ * Git, Diff and Review views).
4
+ *
5
+ * ✦ Changes main · 2 files · +45 −1 · this session esc
6
+ *
7
+ * ✦ files 2 ◈ 2 semantic changes ~ fn withRetry + const backoff
8
+ * src/net/retry.ts +3 −1
9
+ * ✓ test/retry.test.ts +42 ┌ element inset ──────────────────────────────┐
10
+ * ⋯ line 9 hunk 1 of 2
11
+ * ✦ recent commits 9 try {
12
+ * a41c09e init 13 - await sleep(opts.baseDelayMs);
13
+ * 13 + if (i === opts.attempts - 1) break;
14
+ *
15
+ * ] [ next / prev hunk space stage hunk c comment m mark reviewed o open in editor
16
+ *
17
+ * Changed rows keep their syntax colors; the change shows as a background tint
18
+ * across the row and a separate tint on the line-number gutter (the theme's
19
+ * diff tokens, pushed further from the inset fill when a theme's tint sits too
20
+ * close to it; see diff-tint.ts). Long lines wrap under their own gutter. The semantic summary
21
+ * (tree-sitter) leads the diff side. A preview (a fleet candidate, a rewind or an
22
+ * attempt) can carry a question with its two buttons on the first body row.
23
+ */
24
+
25
+ import { activeDiffTones, activeTokens } from './theme.ts';
26
+ import { beginModal, finishModal, clipText, scrollCountText, wrapLines, type KitHint, type ModalFrame, type SurfaceCanvas, type SurfaceLayer } from './surface-kit.ts';
27
+ import { drawList, type KitRow } from './surface-kit-list.ts';
28
+ import { button, buttonWidth, inset, type KitInset } from './surface-kit-parts.ts';
29
+ import { drawTextBlock, splitListDetail } from './surface-kit-extra.ts';
30
+ import { highlightCodeLines, syntaxHighlightGeneration, syntaxHighlightMisses } from './code-block.ts';
31
+ import type { SemanticDiff } from './semantic-diff.ts';
32
+ import { diffRowTints } from './diff-tint.ts';
33
+ import { getDisplayWidth } from '../utils/terminal-width.ts';
34
+ import { languageForPath, type ChangeFile, type ChangeHunk, type ChangeLine } from '../input/changes-model.ts';
35
+
36
+ /** A question asked over a preview (pick this candidate? restore this checkpoint?). */
37
+ export interface ChangesQuestion {
38
+ readonly text: string;
39
+ readonly confirmLabel: string;
40
+ readonly cancelLabel: string;
41
+ readonly tone: 'danger' | 'warning' | 'primary';
42
+ /** 0 = cancel (left), 1 = confirm (right). */
43
+ readonly chosen: 0 | 1;
44
+ }
45
+
46
+ /** One line of text under the diff (a comment, a status, the comment being typed). */
47
+ export interface ChangesFootLine {
48
+ readonly text: string;
49
+ readonly tone: 'muted' | 'faint' | 'text' | 'error' | 'success';
50
+ /** Draw a cursor after the text (an input being typed). */
51
+ readonly cursor?: boolean;
52
+ }
53
+
54
+ /** What the renderer reads from the modal. */
55
+ export interface ChangesModalView {
56
+ readonly title: string;
57
+ readonly crumbs: readonly string[];
58
+ readonly sub: string;
59
+ /** The left list: files, then recent commits (built by the modal, selection included). */
60
+ readonly listRows: readonly KitRow[];
61
+ /** Files shown in the diff side (one file, or every file of a commit or preview). */
62
+ readonly diffFiles: readonly ChangeFile[];
63
+ /** Draw a path row above each file's hunks (more than one file in the diff side). */
64
+ readonly fileHeaders: boolean;
65
+ /** The selected hunk, counted across diffFiles. */
66
+ readonly hunkIndex: number;
67
+ /** Lines scrolled from the selected hunk's first row (pgup / pgdn). */
68
+ readonly scrollDelta: number;
69
+ /** The semantic summary for the shown file: undefined while loading, null when there is none. */
70
+ readonly semantic: SemanticDiff | null | undefined;
71
+ /** Shown instead of a diff (a preview that is text, an empty state, an error). */
72
+ readonly message: string | null;
73
+ readonly foot: readonly ChangesFootLine[];
74
+ readonly hints: readonly KitHint[];
75
+ readonly question: ChangesQuestion | null;
76
+ /** Owner for remembered list scroll positions. */
77
+ readonly scrollOwner: object;
78
+ }
79
+
80
+ /** Width of the line-number gutter inside the diff inset. */
81
+ const DIFF_GUTTER = 5;
82
+ const GUTTER = DIFF_GUTTER;
83
+
84
+ /** One drawn row of a diff: a file header, a hunk separator, or a (wrapped) code line. */
85
+ export type DiffRow =
86
+ | { readonly kind: 'file'; readonly text: string }
87
+ | { readonly kind: 'sep'; readonly hunk: number; readonly text: string }
88
+ | { readonly kind: 'line'; readonly hunk: number; readonly line: ChangeLine; readonly first: boolean; readonly text: string; readonly tokens: ReadonlyArray<{ text: string; fg: string; bold?: boolean; italic?: boolean }> | null };
89
+
90
+ /** Syntax tokens per hunk, remembered until the theme changes. */
91
+ /** A hunk's tokens; `generation` is set when they were the regex placeholder (a parse was on its way). */
92
+ const tokenMemo = new WeakMap<ChangeHunk, { theme: object; lines: ReturnType<typeof highlightCodeLines>; generation: number | undefined }>();
93
+
94
+ function hunkTokens(file: ChangeFile, hunk: ChangeHunk): ReturnType<typeof highlightCodeLines> {
95
+ const theme = activeTokens();
96
+ const memo = tokenMemo.get(hunk);
97
+ // Placeholder tokens are kept only until the hunk's parse lands.
98
+ if (memo && memo.theme === theme && (memo.generation === undefined || memo.generation === syntaxHighlightGeneration())) return memo.lines;
99
+ const missesBefore = syntaxHighlightMisses();
100
+ const lines = highlightCodeLines(hunk.lines.map((line) => line.text), languageForPath(file.path));
101
+ tokenMemo.set(hunk, { theme, lines, generation: syntaxHighlightMisses() !== missesBefore ? syntaxHighlightGeneration() : undefined });
102
+ return lines;
103
+ }
104
+
105
+ /** Split syntax tokens into rows of at most `width` cells. */
106
+ function wrapTokens(tokens: ReadonlyArray<{ text: string; fg: string; bold?: boolean; italic?: boolean }>, width: number): Array<Array<{ text: string; fg: string; bold?: boolean; italic?: boolean }>> {
107
+ const rows: Array<Array<{ text: string; fg: string; bold?: boolean; italic?: boolean }>> = [[]];
108
+ let used = 0;
109
+ for (const token of tokens) {
110
+ let piece = '';
111
+ for (const ch of token.text.replace(/\t/g, ' ')) {
112
+ const w = getDisplayWidth(ch);
113
+ if (w <= 0) continue;
114
+ if (used + w > width) {
115
+ if (piece) rows[rows.length - 1]!.push({ ...token, text: piece });
116
+ rows.push([]);
117
+ piece = '';
118
+ used = 0;
119
+ }
120
+ piece += ch;
121
+ used += w;
122
+ }
123
+ if (piece) rows[rows.length - 1]!.push({ ...token, text: piece });
124
+ }
125
+ return rows;
126
+ }
127
+
128
+ function buildRows(view: ChangesModalView, codeWidth: number): { rows: DiffRow[]; hunkStarts: number[] } {
129
+ return buildDiffRows(view.diffFiles, codeWidth, view.fileHeaders);
130
+ }
131
+
132
+ /**
133
+ * The rows a diff draws as, code wrapped to `codeWidth` under its own gutter.
134
+ * Shared with the conversation work tree, which draws an opened edit with the
135
+ * same rows (lane-graph/paint.ts).
136
+ */
137
+ export function buildDiffRows(diffFiles: readonly ChangeFile[], codeWidth: number, fileHeaders: boolean): { rows: DiffRow[]; hunkStarts: number[] } {
138
+ const rows: DiffRow[] = [];
139
+ const hunkStarts: number[] = [];
140
+ let hunkNo = 0;
141
+ const total = diffFiles.reduce((n, f) => n + f.hunks.length, 0);
142
+ for (const file of diffFiles) {
143
+ if (fileHeaders) rows.push({ kind: 'file', text: `${file.path} +${file.added} −${file.removed}` });
144
+ if (file.headerOnly) {
145
+ const why = file.note ?? (file.headerLines.some((l) => l.startsWith('Binary')) ? 'binary file, no line changes to show' : 'no line changes (mode or rename only)');
146
+ rows.push({ kind: 'file', text: why });
147
+ continue;
148
+ }
149
+ for (const hunk of file.hunks) {
150
+ hunkStarts.push(rows.length);
151
+ rows.push({ kind: 'sep', hunk: hunkNo, text: `⋯ line ${hunk.review.newStart}` + (total > 1 ? ` · hunk ${hunkNo + 1} of ${total}` : '') });
152
+ const tokens = hunkTokens(file, hunk);
153
+ hunk.lines.forEach((line, i) => {
154
+ if (line.kind === 'note') {
155
+ rows.push({ kind: 'line', hunk: hunkNo, line, first: true, text: line.text, tokens: null });
156
+ return;
157
+ }
158
+ const wrapped = wrapTokens(tokens[i] ?? [{ text: line.text, fg: activeTokens().text }], codeWidth);
159
+ wrapped.forEach((part, k) => rows.push({ kind: 'line', hunk: hunkNo, line, first: k === 0, text: '', tokens: part }));
160
+ });
161
+ hunkNo++;
162
+ }
163
+ }
164
+ return { rows, hunkStarts };
165
+ }
166
+
167
+ /**
168
+ * Draw one diff row inside inset `p`: the change tint across the row, a
169
+ * separate tint on the line-number gutter, the number, the sign and the
170
+ * syntax-colored code. `lineNumbers: false` leaves the numbers out (a diff
171
+ * whose line numbers are not known).
172
+ */
173
+ export function drawDiffRow(canvas: SurfaceCanvas, p: KitInset, y: number, row: DiffRow, selectedHunk: number, options: { readonly lineNumbers?: boolean } = {}): void {
174
+ const t = activeTokens();
175
+ if (row.kind === 'file') {
176
+ canvas.put(p.l, y, clipText(row.text, p.r - p.l + 1), { fg: t.text, bold: true, bg: p.bg });
177
+ return;
178
+ }
179
+ const tones = activeDiffTones();
180
+ if (row.kind === 'sep') {
181
+ const on = row.hunk === selectedHunk;
182
+ canvas.put(p.l, y, clipText(row.text, p.r - p.l + 1), { fg: on ? t.brand : tones.hunk, bold: on, bg: p.bg });
183
+ return;
184
+ }
185
+ const line = row.line;
186
+ const gutterEnd = p.l + GUTTER - 1;
187
+ if (line.kind === 'add' || line.kind === 'del') {
188
+ const tints = diffRowTints(p.bg);
189
+ canvas.tint(p.x, y, p.w, line.kind === 'add' ? tints.addedRow : tints.removedRow);
190
+ canvas.tint(p.x, y, gutterEnd - p.x + 2, line.kind === 'add' ? tints.addedGutter : tints.removedGutter);
191
+ }
192
+ if (line.kind === 'note') {
193
+ canvas.put(p.l + GUTTER + 3, y, clipText(row.text, p.r - p.l - GUTTER - 2), { fg: t.textFaint });
194
+ return;
195
+ }
196
+ if (row.first) {
197
+ const n = line.kind === 'del' ? line.oldNo : line.newNo;
198
+ if (n !== null && options.lineNumbers !== false) canvas.right(gutterEnd, y, String(n).slice(-GUTTER), { fg: t.diffLineNumber });
199
+ const sign = line.kind === 'add' ? '+' : line.kind === 'del' ? '-' : ' ';
200
+ canvas.put(gutterEnd + 2, y, sign, { fg: line.kind === 'add' ? tones.add : line.kind === 'del' ? tones.del : t.textFaint });
201
+ }
202
+ let x = gutterEnd + 4;
203
+ for (const token of row.tokens ?? []) x = canvas.put(x, y, token.text, { fg: token.fg, bold: token.bold, italic: token.italic });
204
+ }
205
+
206
+ /** The ◈ summary's chips ("~ fn withRetry", "+ import x"), colored by kind. */
207
+ export function semanticChips(diff: SemanticDiff): Array<{ text: string; fg: string }> {
208
+ const t = activeTokens();
209
+ const chips: Array<{ text: string; fg: string }> = [];
210
+ for (const s of diff.symbols) {
211
+ const glyph = s.kind === 'added' ? '+' : s.kind === 'removed' ? '-' : '~';
212
+ chips.push({ text: `${glyph} ${s.symbolKind === 'function' ? 'fn' : s.symbolKind} ${s.name}`, fg: s.kind === 'added' ? t.success : s.kind === 'removed' ? t.error : t.warning });
213
+ }
214
+ for (const i of diff.imports) {
215
+ const glyph = i.kind === 'added' ? '+' : i.kind === 'removed' ? '-' : '~';
216
+ chips.push({ text: `${glyph} import ${i.specifier}`, fg: i.kind === 'added' ? t.success : i.kind === 'removed' ? t.error : t.warning });
217
+ }
218
+ return chips;
219
+ }
220
+
221
+ /** The ◈ summary row(s); returns the row after them. */
222
+ function drawSemantic(f: ModalFrame, view: ChangesModalView, x: number, xr: number, y: number): number {
223
+ const t = activeTokens();
224
+ const { canvas } = f;
225
+ if (view.semantic === undefined) {
226
+ canvas.put(x, y, clipText('◈ reading the structure of this file…', xr - x + 1), { fg: t.textFaint });
227
+ return y + 2;
228
+ }
229
+ if (view.semantic === null || view.semantic.totalChanges === 0) return y;
230
+ canvas.put(x, y, '◈', { fg: t.brandEnd });
231
+ const head = `${view.semantic.totalChanges} semantic change${view.semantic.totalChanges === 1 ? '' : 's'}`;
232
+ let cx = canvas.put(x + 2, y, clipText(head, xr - x - 1), { fg: t.text, bold: true }) + 2;
233
+ let yy = y;
234
+ const chips = semanticChips(view.semantic);
235
+ chips.forEach((chip, k) => {
236
+ const w = getDisplayWidth(chip.text);
237
+ const rest = chips.length - k;
238
+ if (cx + w > xr) {
239
+ // Wrap chips onto one more row, then fold the rest into a count.
240
+ if (yy > y) return;
241
+ yy++;
242
+ cx = x + 2;
243
+ }
244
+ if (yy > y && cx + w > xr - 8 && rest > 1) {
245
+ canvas.put(cx, yy, `+${rest} more`, { fg: t.textFaint });
246
+ cx = xr + 1;
247
+ return;
248
+ }
249
+ if (cx + w > xr) return;
250
+ cx = canvas.put(cx, yy, chip.text, { fg: chip.fg }) + 2;
251
+ });
252
+ return yy + 2;
253
+ }
254
+
255
+ function drawQuestion(f: ModalFrame, q: ChangesQuestion, y: number): number {
256
+ const t = activeTokens();
257
+ const width = f.r - f.l + 1;
258
+ const buttonsW = buttonWidth(q.cancelLabel) + 2 + buttonWidth(q.confirmLabel);
259
+ const textW = width >= 70 ? width - buttonsW - 3 : width;
260
+ const lines = wrapLines(q.text, textW);
261
+ lines.forEach((line, k) => f.canvas.put(f.l, y + k, line, { fg: t.text, bold: k === 0 }));
262
+ const by = width >= 70 ? y : y + lines.length;
263
+ let bx = f.r - buttonsW + 1;
264
+ bx = button(f.canvas, bx, by, q.cancelLabel, q.chosen === 0) + 2;
265
+ button(f.canvas, bx, by, q.confirmLabel, q.chosen === 1, q.tone);
266
+ return Math.max(y + lines.length, by + 1) + 1;
267
+ }
268
+
269
+ function drawDiffColumn(f: ModalFrame, view: ChangesModalView, x: number, xr: number, top: number, bottom: number): void {
270
+ const t = activeTokens();
271
+ const { canvas } = f;
272
+ let y = top;
273
+ if (view.message !== null) {
274
+ drawTextBlock(canvas, x, y, xr - x + 1, view.message.split('\n').map((text) => ({ text, style: { fg: t.textMuted } })), bottom);
275
+ return;
276
+ }
277
+ y = drawSemantic(f, view, x, xr, y);
278
+ // Foot lines under the diff (comment, status), each wrapped in full.
279
+ const footRows = view.foot.reduce((n, line) => n + wrapLines(line.text, xr - x + 1).length, 0);
280
+ const insetBottom = bottom - (footRows > 0 ? footRows + 1 : 0);
281
+ if (insetBottom - y + 1 >= 3) {
282
+ const p = inset(canvas, x - 2, y, xr - x + 5, insetBottom - y + 1);
283
+ const codeWidth = Math.max(8, p.r - (p.l + GUTTER + 3) + 1);
284
+ const { rows, hunkStarts } = buildRows(view, codeWidth);
285
+ const capacity = p.bottom - p.top + 1;
286
+ const anchor = hunkStarts[Math.max(0, Math.min(view.hunkIndex, hunkStarts.length - 1))] ?? 0;
287
+ const start = Math.max(0, Math.min(anchor + view.scrollDelta, Math.max(0, rows.length - capacity)));
288
+ for (let k = 0; k < capacity && start + k < rows.length; k++) drawDiffRow(canvas, p, p.top + k, rows[start + k]!, view.hunkIndex);
289
+ const hidden = scrollCountText(start, Math.max(0, rows.length - start - capacity)).replace(/more/g, 'lines');
290
+ if (hidden) f.hintRight = hidden;
291
+ }
292
+ let fy = insetBottom + 2;
293
+ for (const line of view.foot) {
294
+ const fg = line.tone === 'error' ? t.error : line.tone === 'success' ? t.success : line.tone === 'faint' ? t.textFaint : line.tone === 'text' ? t.text : t.textMuted;
295
+ const wrapped = wrapLines(line.text, xr - x + 1);
296
+ wrapped.forEach((part, k) => {
297
+ if (fy > bottom) return;
298
+ const end = canvas.put(x, fy, part, { fg });
299
+ if (line.cursor && k === wrapped.length - 1) canvas.put(end, fy, '▏', { fg: t.brand });
300
+ fy++;
301
+ });
302
+ }
303
+ }
304
+
305
+ export function renderChangesModal(view: ChangesModalView, screenWidth: number, screenHeight: number): SurfaceLayer {
306
+ const f = beginModal(screenWidth, screenHeight, {
307
+ title: view.title,
308
+ crumbs: view.crumbs,
309
+ sub: view.sub,
310
+ hints: view.hints,
311
+ cap: view.question ? (view.question.tone === 'danger' ? 'danger' : 'warning') : 'brand',
312
+ });
313
+ let top = f.top;
314
+ if (view.question) top = drawQuestion(f, view.question, top);
315
+ if (view.listRows.length === 0) {
316
+ drawDiffColumn(f, view, f.l, f.r, top, f.bottom);
317
+ return finishModal(f);
318
+ }
319
+ const split = splitListDetail(f.l, f.r, top, f.bottom, 0.3, 4);
320
+ drawList(f.canvas, { rows: view.listRows, top: split.top, bottom: split.bottom, x0: split.x0, x1: split.x1, scrollKey: { owner: view.scrollOwner, name: 'files' } });
321
+ const paneX = split.stacked ? f.l : split.detailX + 2;
322
+ const paneTop = split.stacked ? split.detailY : split.top;
323
+ drawDiffColumn(f, view, paneX, f.r, paneTop, f.bottom);
324
+ return finishModal(f);
325
+ }