@pellux/goodvibes-tui 2.0.20 → 2.1.0

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 (388) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/README.md +16 -16
  3. package/docs/README.md +4 -4
  4. package/docs/foundation-artifacts/operator-contract.json +59 -4
  5. package/package.json +16 -6
  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-registry.ts +77 -35
  52. package/src/input/commands/acp-runtime.ts +19 -9
  53. package/src/input/commands/channel-runtime.ts +2 -2
  54. package/src/input/commands/ci-runtime.ts +1 -1
  55. package/src/input/commands/codebase-runtime.ts +1 -1
  56. package/src/input/commands/config.ts +7 -1
  57. package/src/input/commands/context-window.ts +25 -19
  58. package/src/input/commands/control-room-runtime.ts +18 -18
  59. package/src/input/commands/conversation-runtime.ts +0 -1
  60. package/src/input/commands/cost-runtime.ts +29 -20
  61. package/src/input/commands/diff-runtime.ts +35 -181
  62. package/src/input/commands/eval.ts +1 -1
  63. package/src/input/commands/experience-runtime.ts +3 -7
  64. package/src/input/commands/fresh-conversation.ts +86 -0
  65. package/src/input/commands/git-runtime.ts +5 -22
  66. package/src/input/commands/graph-runtime.ts +1 -1
  67. package/src/input/commands/guidance-runtime.ts +1 -1
  68. package/src/input/commands/health-runtime.ts +8 -9
  69. package/src/input/commands/hooks-runtime.ts +2 -2
  70. package/src/input/commands/hosted-runtime.ts +3 -3
  71. package/src/input/commands/incident-runtime.ts +3 -3
  72. package/src/input/commands/intelligence-runtime.ts +6 -9
  73. package/src/input/commands/knowledge.ts +2 -2
  74. package/src/input/commands/legacy-panel-command.ts +57 -0
  75. package/src/input/commands/local-auth-runtime.ts +8 -9
  76. package/src/input/commands/local-provider-runtime.ts +7 -4
  77. package/src/input/commands/local-runtime.ts +12 -15
  78. package/src/input/commands/local-setup-review.ts +1 -1
  79. package/src/input/commands/local-setup.ts +13 -19
  80. package/src/input/commands/memory.ts +1 -1
  81. package/src/input/commands/notify-runtime.ts +9 -0
  82. package/src/input/commands/operator-runtime.ts +10 -12
  83. package/src/input/commands/permissions-provenance.ts +3 -3
  84. package/src/input/commands/planning-runtime.ts +7 -7
  85. package/src/input/commands/platform-sandbox-runtime.ts +2 -2
  86. package/src/input/commands/platform-services-runtime.ts +1 -1
  87. package/src/input/commands/policy-dispatch.ts +2 -2
  88. package/src/input/commands/policy.ts +3 -3
  89. package/src/input/commands/provider.ts +1 -1
  90. package/src/input/commands/remote-runtime.ts +1 -1
  91. package/src/input/commands/review-runtime.ts +39 -94
  92. package/src/input/commands/rewind-runtime.ts +81 -108
  93. package/src/input/commands/runtime-services.ts +15 -17
  94. package/src/input/commands/services-runtime.ts +2 -2
  95. package/src/input/commands/session-content.ts +21 -5
  96. package/src/input/commands/session-workflow.ts +13 -38
  97. package/src/input/commands/settings-sync-runtime.ts +2 -2
  98. package/src/input/commands/shell-core.ts +19 -4
  99. package/src/input/commands/skills-runtime.ts +2 -2
  100. package/src/input/commands/status-runtime.ts +13 -0
  101. package/src/input/commands/subscription-runtime.ts +1 -1
  102. package/src/input/commands/tasks-runtime.ts +2 -8
  103. package/src/input/commands/work-plan-runtime.ts +4 -4
  104. package/src/input/commands/workstream-attempts.ts +24 -47
  105. package/src/input/commands/workstream-runtime.ts +2 -2
  106. package/src/input/commands/worktree-runtime.ts +3 -3
  107. package/src/input/commands.ts +3 -1
  108. package/src/input/composer-editor.ts +44 -0
  109. package/src/input/concealed-input.ts +1 -1
  110. package/src/input/config-modal-types.ts +20 -8
  111. package/src/input/config-modal.ts +106 -105
  112. package/src/input/confirm-dialog-types.ts +28 -0
  113. package/src/input/confirm-dialog.ts +70 -0
  114. package/src/input/delete-key-policy.ts +3 -3
  115. package/src/input/feed-context-factory.ts +15 -16
  116. package/src/input/handler-command-route.ts +7 -42
  117. package/src/input/handler-content-actions.ts +16 -15
  118. package/src/input/handler-feed-routes.ts +26 -211
  119. package/src/input/handler-feed.ts +68 -95
  120. package/src/input/handler-interactions.ts +5 -3
  121. package/src/input/handler-modal-routes.ts +215 -216
  122. package/src/input/handler-modal-stack.ts +81 -11
  123. package/src/input/handler-modal-token-routes.ts +4 -0
  124. package/src/input/handler-onboarding.ts +0 -1
  125. package/src/input/handler-picker-routes.ts +65 -107
  126. package/src/input/handler-session-view-route.ts +149 -0
  127. package/src/input/handler-shortcuts.ts +16 -154
  128. package/src/input/handler-types.ts +4 -2
  129. package/src/input/handler-ui-state.ts +45 -15
  130. package/src/input/handler-work-tree-route.ts +123 -0
  131. package/src/input/handler.ts +25 -70
  132. package/src/input/keybindings.ts +16 -71
  133. package/src/input/masked-entry-modal.ts +117 -0
  134. package/src/input/mcp-workspace.ts +48 -17
  135. package/src/input/model-picker-filter.ts +8 -0
  136. package/src/input/model-picker-open.ts +99 -0
  137. package/src/input/model-picker-types.ts +1 -1
  138. package/src/input/model-picker.ts +92 -9
  139. package/src/input/notifications-modal.ts +123 -0
  140. package/src/input/overlay-filter.ts +26 -0
  141. package/src/input/profile-picker-modal.ts +37 -8
  142. package/src/input/search.ts +3 -3
  143. package/src/input/selection-modal-queue.ts +2 -0
  144. package/src/input/selection-modal.ts +28 -16
  145. package/src/input/session-picker-modal.ts +33 -8
  146. package/src/input/settings-modal-activation.ts +7 -2
  147. package/src/input/settings-modal-data.ts +11 -6
  148. package/src/input/settings-modal-types.ts +1 -1
  149. package/src/input/settings-modal.ts +20 -20
  150. package/src/input/submission-intent.ts +1 -1
  151. package/src/input/submission-router.ts +4 -9
  152. package/src/input/surface-modal-host.ts +110 -0
  153. package/src/input/theme-settings-actions.ts +82 -0
  154. package/src/input/usage-modal.ts +183 -0
  155. package/src/input/views.ts +97 -0
  156. package/src/main.ts +175 -165
  157. package/src/permissions/hunk-selection.ts +1 -1
  158. package/src/permissions/prompt-card.ts +511 -0
  159. package/src/permissions/prompt.ts +59 -622
  160. package/src/renderer/agents-modal.ts +310 -0
  161. package/src/renderer/autocomplete-overlay.ts +42 -220
  162. package/src/renderer/back-to-bottom.ts +80 -0
  163. package/src/renderer/block-actions-overlay.ts +30 -48
  164. package/src/renderer/bookmark-modal.ts +48 -79
  165. package/src/renderer/changes-modal.ts +325 -0
  166. package/src/renderer/code-block.ts +184 -106
  167. package/src/renderer/command-palette.ts +93 -0
  168. package/src/renderer/composer-fragments.ts +4 -4
  169. package/src/renderer/composer.ts +146 -0
  170. package/src/renderer/compositor.ts +26 -160
  171. package/src/renderer/config-modal.ts +185 -81
  172. package/src/renderer/confirm-dialog.ts +50 -0
  173. package/src/renderer/context-inspector.ts +116 -154
  174. package/src/renderer/context-status-hint.ts +4 -4
  175. package/src/renderer/conversation-layout.ts +20 -0
  176. package/src/renderer/conversation-overlays.ts +94 -84
  177. package/src/renderer/conversation-surface.ts +19 -38
  178. package/src/renderer/diff-tint.ts +98 -0
  179. package/src/renderer/file-picker-overlay.ts +63 -90
  180. package/src/renderer/footer-targets.ts +38 -0
  181. package/src/renderer/git-status.ts +3 -4
  182. package/src/renderer/header-line.ts +140 -117
  183. package/src/renderer/help-overlay.ts +133 -219
  184. package/src/renderer/history-search-overlay.ts +15 -12
  185. package/src/renderer/lane-graph/bead.ts +800 -0
  186. package/src/renderer/lane-graph/glyphs.ts +134 -0
  187. package/src/renderer/lane-graph/layout.ts +248 -0
  188. package/src/renderer/lane-graph/paint.ts +537 -0
  189. package/src/renderer/lane-graph/semantic-memo.ts +77 -0
  190. package/src/renderer/layout-engine.ts +2 -57
  191. package/src/renderer/layout.ts +7 -13
  192. package/src/renderer/markdown-inline.ts +2 -1
  193. package/src/renderer/markdown-table.ts +17 -12
  194. package/src/renderer/markdown.ts +92 -34
  195. package/src/renderer/masked-entry-modal.ts +78 -0
  196. package/src/renderer/mcp-workspace.ts +166 -203
  197. package/src/renderer/model-workspace.ts +381 -436
  198. package/src/renderer/notifications-modal.ts +116 -0
  199. package/src/renderer/onboarding/onboarding-wizard.ts +178 -588
  200. package/src/renderer/process-view.ts +192 -0
  201. package/src/renderer/profile-picker-modal.ts +58 -110
  202. package/src/renderer/search-overlay.ts +6 -3
  203. package/src/renderer/selection-modal-overlay.ts +87 -327
  204. package/src/renderer/semantic-diff.ts +2 -2
  205. package/src/renderer/session-chips.ts +102 -0
  206. package/src/renderer/session-picker-modal.ts +177 -297
  207. package/src/renderer/settings-modal-connections.ts +14 -23
  208. package/src/renderer/settings-modal-context.ts +367 -0
  209. package/src/renderer/settings-modal-helpers.ts +6 -11
  210. package/src/renderer/settings-modal.ts +322 -726
  211. package/src/renderer/shell-surface.ts +277 -175
  212. package/src/renderer/shortcuts-overlay.ts +217 -0
  213. package/src/renderer/status-line.ts +475 -0
  214. package/src/renderer/surface-compose.ts +174 -0
  215. package/src/renderer/surface-kit-extra.ts +303 -0
  216. package/src/renderer/surface-kit-list.ts +366 -0
  217. package/src/renderer/surface-kit-parts.ts +360 -0
  218. package/src/renderer/surface-kit.ts +568 -0
  219. package/src/renderer/syntax-highlighter.ts +280 -197
  220. package/src/renderer/syntax-theme.ts +51 -0
  221. package/src/renderer/system-message.ts +0 -1
  222. package/src/renderer/term-caps.ts +42 -1
  223. package/src/renderer/terminal-bg-probe.ts +116 -19
  224. package/src/renderer/terminal-palette-probe.ts +322 -0
  225. package/src/renderer/terminal-palette-reader.ts +183 -0
  226. package/src/renderer/terminal-palette.ts +58 -0
  227. package/src/renderer/theme-mode-config.ts +16 -1
  228. package/src/renderer/theme.ts +234 -207
  229. package/src/renderer/thinking.ts +0 -1
  230. package/src/renderer/throbber.ts +188 -0
  231. package/src/renderer/toast-center.ts +119 -0
  232. package/src/renderer/ui-factory.ts +39 -602
  233. package/src/renderer/ui-primitives.ts +8 -25
  234. package/src/renderer/usage-modal.ts +353 -0
  235. package/src/renderer/user-message.ts +58 -0
  236. package/src/renderer/voice-capture-chip.ts +53 -0
  237. package/src/runtime/bootstrap-command-context.ts +0 -5
  238. package/src/runtime/bootstrap-command-parts.ts +38 -89
  239. package/src/runtime/bootstrap-core.ts +13 -15
  240. package/src/runtime/bootstrap-hook-bridge.ts +12 -21
  241. package/src/runtime/bootstrap-notifier-sync.ts +17 -1
  242. package/src/runtime/bootstrap-shell.ts +16 -50
  243. package/src/runtime/bootstrap.ts +14 -14
  244. package/src/runtime/client/fleet-union.ts +5 -5
  245. package/src/runtime/client/hosted-exit.ts +2 -2
  246. package/src/runtime/client/hosted-sessions.ts +1 -1
  247. package/src/runtime/client/spine-adoption.ts +1 -1
  248. package/src/runtime/context-accounting-source.ts +5 -4
  249. package/src/runtime/index.ts +1 -0
  250. package/src/runtime/notification-dispatch.ts +102 -29
  251. package/src/runtime/recovery-offer-wiring.ts +4 -1
  252. package/src/runtime/recovery-prompt.ts +31 -15
  253. package/src/runtime/runtime-services-types.ts +10 -6
  254. package/src/runtime/services.ts +17 -12
  255. package/src/runtime/session-continuity-hints.ts +2 -5
  256. package/src/runtime/session-inbound-inputs.ts +1 -1
  257. package/src/runtime/session-resume-liveness-confirm.ts +14 -2
  258. package/src/runtime/startup-typeahead-gate.ts +103 -0
  259. package/src/runtime/store/domains/domain-read-matrix.ts +0 -1
  260. package/src/runtime/store/domains/index.ts +0 -8
  261. package/src/runtime/store/selectors/index.ts +0 -28
  262. package/src/runtime/store/state.ts +0 -8
  263. package/src/runtime/ui-services.ts +4 -6
  264. package/src/runtime/usage-tracker.ts +348 -0
  265. package/src/runtime/wrfc-persistence.ts +4 -4
  266. package/src/shell/blocking-input.ts +92 -2
  267. package/src/shell/recovery-input-helpers.ts +1 -1
  268. package/src/shell/session-views.ts +539 -0
  269. package/src/shell/status-report.ts +86 -0
  270. package/src/shell/transcript-scroll.ts +68 -0
  271. package/src/shell/ui-openers.ts +84 -172
  272. package/src/shell/view-openers.ts +205 -0
  273. package/src/tools/wrfc-agent-guard.ts +22 -2
  274. package/src/utils/format-duration.ts +4 -8
  275. package/src/utils/splash-lines.ts +9 -1
  276. package/src/utils/terminal-width.ts +56 -0
  277. package/src/verification/verification-ledger.ts +18 -16
  278. package/src/{panels → views}/agent-inspector-shared.ts +2 -2
  279. package/src/{panels → views}/builtin-modals.ts +22 -21
  280. package/src/views/builtin-views.ts +135 -0
  281. package/src/{panels → views}/confirm-state.ts +9 -9
  282. package/src/{panels → views}/diff-review-model.ts +10 -3
  283. package/src/{panels → views}/eval-registry.ts +1 -1
  284. package/src/{panels → views}/fleet-acts.ts +49 -85
  285. package/src/{panels/fleet-panel-format.ts → views/fleet-format.ts} +40 -40
  286. package/src/{panels → views}/fleet-gateway.ts +5 -5
  287. package/src/{panels → views}/fleet-observed-render.ts +16 -16
  288. package/src/{panels → views}/fleet-read-model.ts +9 -9
  289. package/src/{panels/fleet-tabs.ts → views/fleet-session-tabs.ts} +13 -13
  290. package/src/{panels → views}/fleet-spawn.ts +33 -49
  291. package/src/{panels → views}/fleet-steer.ts +12 -12
  292. package/src/{panels → views}/fleet-stop.ts +4 -4
  293. package/src/{panels → views}/fleet-transcript.ts +26 -27
  294. package/src/{panels/fleet-panel-worktree-detail.ts → views/fleet-worktree-detail.ts} +7 -7
  295. package/src/{panels → views}/hosted-session-feed.ts +6 -6
  296. package/src/views/modal-surface-registry.ts +41 -0
  297. package/src/{panels → views}/modals/devices-modal.ts +2 -2
  298. package/src/{panels → views}/modals/hooks-modal.ts +13 -13
  299. package/src/{panels → views}/modals/keybindings-modal.ts +19 -27
  300. package/src/{panels → views}/modals/knowledge-modal.ts +6 -5
  301. package/src/{panels → views}/modals/local-auth-modal.ts +22 -21
  302. package/src/{panels → views}/modals/marketplace-modal.ts +11 -11
  303. package/src/{panels → views}/modals/memory-modal.ts +8 -8
  304. package/src/{panels → views}/modals/modal-surface-helpers.ts +15 -5
  305. package/src/{panels → views}/modals/pairing-modal.ts +9 -9
  306. package/src/{panels → views}/modals/planning-modal.ts +15 -20
  307. package/src/{panels → views}/modals/plugins-modal.ts +6 -6
  308. package/src/{panels → views}/modals/policy-modal.ts +13 -5
  309. package/src/{panels → views}/modals/provider-health-modal.ts +4 -4
  310. package/src/{panels → views}/modals/remote-modal.ts +3 -3
  311. package/src/{panels → views}/modals/sandbox-modal.ts +4 -4
  312. package/src/{panels → views}/modals/security-modal.ts +22 -16
  313. package/src/{panels → views}/modals/services-modal.ts +3 -3
  314. package/src/{panels → views}/modals/settings-sync-modal.ts +4 -4
  315. package/src/{panels → views}/modals/skills-modal.ts +9 -9
  316. package/src/{panels → views}/modals/subscription-modal.ts +3 -3
  317. package/src/{panels → views}/modals/work-plan-modal.ts +10 -13
  318. package/src/views/notifications-feed.ts +314 -0
  319. package/src/{panels → views}/polish-core.ts +38 -36
  320. package/src/{panels → views}/polish-tables.ts +35 -26
  321. package/src/{panels → views}/polish.ts +101 -95
  322. package/src/{panels → views}/project-planning-answer-actions.ts +3 -3
  323. package/src/{panels → views}/provider-health-routes.ts +14 -14
  324. package/src/{panels → views}/search-focus.ts +8 -8
  325. package/src/{panels → views}/session-maintenance.ts +11 -11
  326. package/src/views/skills-discovery.ts +159 -0
  327. package/src/views/view-deps.ts +80 -0
  328. package/src/views/view-panel-adapter.ts +53 -0
  329. package/src/{panels → views}/workstream-graph-render.ts +1 -1
  330. package/src/core/conversation-fold.ts +0 -101
  331. package/src/core/conversation-turn-structure.ts +0 -464
  332. package/src/core/conversation-user-receipts.ts +0 -42
  333. package/src/input/commands/operator-panel-runtime.ts +0 -186
  334. package/src/input/panel-integration-actions.ts +0 -33
  335. package/src/input/panel-mouse-geometry.ts +0 -97
  336. package/src/input/panel-paste-flood-guard.ts +0 -86
  337. package/src/panels/base-panel.ts +0 -301
  338. package/src/panels/builtin/agent.ts +0 -27
  339. package/src/panels/builtin/development.ts +0 -92
  340. package/src/panels/builtin/knowledge.ts +0 -12
  341. package/src/panels/builtin/operations.ts +0 -177
  342. package/src/panels/builtin/session.ts +0 -81
  343. package/src/panels/builtin/shared.ts +0 -357
  344. package/src/panels/builtin-panels.ts +0 -28
  345. package/src/panels/cost-tracker-panel.ts +0 -746
  346. package/src/panels/diff-panel.ts +0 -797
  347. package/src/panels/diff-review-panel.ts +0 -395
  348. package/src/panels/expandable-list-panel.ts +0 -189
  349. package/src/panels/fleet-deep-link.ts +0 -31
  350. package/src/panels/fleet-panel.ts +0 -799
  351. package/src/panels/git-panel.ts +0 -800
  352. package/src/panels/hosted-session-panel.ts +0 -221
  353. package/src/panels/index.ts +0 -21
  354. package/src/panels/local-auth-panel.ts +0 -478
  355. package/src/panels/notifications-feed.ts +0 -127
  356. package/src/panels/notifications-panel.ts +0 -160
  357. package/src/panels/panel-confirm-overlay.ts +0 -72
  358. package/src/panels/panel-manager.ts +0 -791
  359. package/src/panels/plugins-panel.ts +0 -371
  360. package/src/panels/scrollable-list-panel.ts +0 -498
  361. package/src/panels/skills-panel.ts +0 -454
  362. package/src/panels/token-budget-panel.ts +0 -688
  363. package/src/panels/types.ts +0 -191
  364. package/src/panels/wrfc-panel-format.ts +0 -133
  365. package/src/renderer/diff-view.ts +0 -134
  366. package/src/renderer/fleet-tab-strip.ts +0 -56
  367. package/src/renderer/footer-tips.ts +0 -53
  368. package/src/renderer/fullscreen-primitives.ts +0 -146
  369. package/src/renderer/fullscreen-workspace.ts +0 -194
  370. package/src/renderer/hint-grammar.ts +0 -52
  371. package/src/renderer/modal-factory.ts +0 -480
  372. package/src/renderer/overlay-box.ts +0 -154
  373. package/src/renderer/overlay-viewport.ts +0 -104
  374. package/src/renderer/panel-composite.ts +0 -145
  375. package/src/renderer/panel-workspace-bar.ts +0 -54
  376. package/src/renderer/process-indicator.ts +0 -163
  377. package/src/renderer/tab-strip.ts +0 -222
  378. package/src/renderer/text-layout.ts +0 -31
  379. package/src/renderer/tool-call.ts +0 -276
  380. package/src/renderer/tool-result-expanded-lines.ts +0 -48
  381. package/src/renderer/tool-result-summary.ts +0 -219
  382. package/src/runtime/diagnostics/panels/index.ts +0 -21
  383. package/src/runtime/diagnostics/panels/ops.ts +0 -156
  384. package/src/runtime/perf/panel-contracts.ts +0 -32
  385. package/src/runtime/store/domains/panels.ts +0 -117
  386. /package/src/{panels → views}/memory-consolidation-gateway.ts +0 -0
  387. /package/src/{panels → views}/modals/modal-theme.ts +0 -0
  388. /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
+ }