@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,797 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // DiffPanel, unified diff view of agent file changes
3
- // ---------------------------------------------------------------------------
4
-
5
- import type { Line } from '@pellux/goodvibes-sdk/platform/types';
6
- import { createStyledCell, createEmptyLine } from '@pellux/goodvibes-sdk/platform/types';
7
- import { truncateDisplay, getDisplayWidth } from '../utils/terminal-width.ts';
8
- import { GitService, type StructuredDiff, type StructuredDiffFile } from '@pellux/goodvibes-sdk/platform/git';
9
- import { BasePanel } from './base-panel.ts';
10
- import { UI_TONES, DIFF_TONES } from '../renderer/ui-primitives.ts';
11
- import { logger } from '@pellux/goodvibes-sdk/platform/utils';
12
- import { PanelConfirmOverlay } from './panel-confirm-overlay.ts';
13
- import {
14
- buildBodyText,
15
- buildEmptyState,
16
- buildPanelWorkspace,
17
- resolveScrollablePanelSection,
18
- buildStyledPanelLine,
19
- type PanelWorkspaceSection,
20
- DEFAULT_PANEL_PALETTE,
21
- extendPalette,
22
- } from './polish.ts';
23
-
24
- // ---------------------------------------------------------------------------
25
- // Colour palette, dedicated diff-viewer scheme (like a mini syntax
26
- // highlighter). Each hex literal is named once and reused for every role
27
- // that shares it, including the workspace-chrome aliases (info/dim/value/
28
- // empty) so the raw-hex count never grows. The title band itself is NOT
29
- // overridden, buildPanelWorkspace always falls back to the canonical
30
- // DEFAULT_PANEL_PALETTE.headerBg (one title band everywhere).
31
- // ---------------------------------------------------------------------------
32
-
33
- // Hunk blue is the shared DIFF_TONES token, diff-view.ts (conversation)
34
- // and git-panel.ts's inline diff converge onto this file's pre-existing value.
35
- const HUNK_BLUE: string = DIFF_TONES.hunk;
36
- // Context rows and line-number gutter use the shared theme's muted/dim
37
- // foreground tones rather than dedicated gray hex literals.
38
- const CONTEXT_GRAY = UI_TONES.fg.muted;
39
- const FILENAME_WHITE = '#ffffff';
40
- // Add/del text colors are the shared DIFF_TONES tokens, whose values ARE this
41
- // panel's shipped colors, this panel is the reference diff look (diff-view
42
- // was unwired dead code until, so "majority of surfaces" was a mirage);
43
- // the conversation surface converges onto these, not the reverse.
44
- const ADD_GREEN: string = DIFF_TONES.add;
45
- const ADD_BG = '#001a0d';
46
- const DEL_RED: string = DIFF_TONES.del;
47
- const DEL_BG = '#1a0000';
48
- const HUNK_BG = '#0a0a1a';
49
- const MARKER_GRAY = '#aaaaaa';
50
- const LINE_NUM_GRAY = UI_TONES.fg.dim;
51
- const LINE_NUM_ADD = '#00aa55';
52
- const LINE_NUM_DEL = '#aa2222';
53
- const TAB_ACTIVE_BG = '#333333';
54
- const TAB_INACTIVE_GRAY = '#666666';
55
- const TAB_BG = '#222222';
56
- const STATUS_BAR_BG = '#444444';
57
-
58
- const COLOR = extendPalette(DEFAULT_PANEL_PALETTE, {
59
- // Workspace-chrome aliases (title band excluded, no headerBg override)
60
- info: HUNK_BLUE,
61
- dim: CONTEXT_GRAY,
62
- value: FILENAME_WHITE,
63
- empty: CONTEXT_GRAY,
64
-
65
- // Domain accents
66
- addition: ADD_GREEN,
67
- additionBg: ADD_BG,
68
- deletion: DEL_RED,
69
- deletionBg: DEL_BG,
70
- hunk: HUNK_BLUE,
71
- hunkBg: HUNK_BG,
72
- markerText: MARKER_GRAY,
73
- lineNum: LINE_NUM_GRAY,
74
- lineNumAdd: LINE_NUM_ADD,
75
- lineNumDel: LINE_NUM_DEL,
76
- filename: FILENAME_WHITE,
77
- tabActive: FILENAME_WHITE,
78
- tabActiveBg: TAB_ACTIVE_BG,
79
- tabInactive: TAB_INACTIVE_GRAY,
80
- tabBg: TAB_BG,
81
- context: CONTEXT_GRAY,
82
- statusBar: STATUS_BAR_BG,
83
- } as const);
84
-
85
- // ---------------------------------------------------------------------------
86
- // Types
87
- // ---------------------------------------------------------------------------
88
-
89
- interface DiffEntry {
90
- filePath: string;
91
- raw: string; // raw unified diff text
92
- lines: ParsedLine[];
93
- /** One-line semantic summary from computeSemanticDiff, if available. */
94
- semanticSummary?: string;
95
- }
96
-
97
- /** +added / -removed line counts for a parsed diff entry. */
98
- function diffStat(entry: DiffEntry): { added: number; removed: number } {
99
- let added = 0;
100
- let removed = 0;
101
- for (const line of entry.lines) {
102
- if (line.kind === 'addition') added++;
103
- else if (line.kind === 'deletion') removed++;
104
- }
105
- return { added, removed };
106
- }
107
-
108
- type LineKind = 'addition' | 'deletion' | 'context' | 'hunk' | 'header';
109
-
110
- interface ParsedLine {
111
- kind: LineKind;
112
- text: string;
113
- beforeNum: number | null; // line number in original file
114
- afterNum: number | null; // line number in new file
115
- }
116
-
117
- // ---------------------------------------------------------------------------
118
- // Diff parser
119
- // ---------------------------------------------------------------------------
120
-
121
- function parseDiff(raw: string): ParsedLine[] {
122
- const result: ParsedLine[] = [];
123
- let before = 0;
124
- let after = 0;
125
-
126
- for (const line of raw.split('\n')) {
127
- // Hunk header: @@ -a,b +c,d @@
128
- const hunkMatch = line.match(/^@@\s+-([0-9]+)(?:,[0-9]+)?\s+\+([0-9]+)(?:,[0-9]+)?\s+@@/);
129
- if (hunkMatch) {
130
- before = parseInt(hunkMatch[1]!, 10);
131
- after = parseInt(hunkMatch[2]!, 10);
132
- result.push({ kind: 'hunk', text: line, beforeNum: null, afterNum: null });
133
- continue;
134
- }
135
-
136
- if (line.startsWith('+++') || line.startsWith('---') ||
137
- line.startsWith('diff ') || line.startsWith('index ') ||
138
- line.startsWith('new file') || line.startsWith('old file') ||
139
- line.startsWith('Binary')) {
140
- result.push({ kind: 'header', text: line, beforeNum: null, afterNum: null });
141
- continue;
142
- }
143
-
144
- if (line.startsWith('+')) {
145
- result.push({ kind: 'addition', text: line.slice(1), beforeNum: null, afterNum: after });
146
- after++;
147
- } else if (line.startsWith('-')) {
148
- result.push({ kind: 'deletion', text: line.slice(1), beforeNum: before, afterNum: null });
149
- before++;
150
- } else if (line.startsWith('\\')) {
151
- // "No newline at end of file" note, treat as header
152
- result.push({ kind: 'header', text: line, beforeNum: null, afterNum: null });
153
- } else {
154
- // context line (starts with space, or empty for blank context)
155
- const text = line.startsWith(' ') ? line.slice(1) : line;
156
- result.push({ kind: 'context', text, beforeNum: before, afterNum: after });
157
- before++;
158
- after++;
159
- }
160
- }
161
-
162
- return result;
163
- }
164
-
165
- // ---------------------------------------------------------------------------
166
- // Split a full `git diff` output into per-file entries
167
- // ---------------------------------------------------------------------------
168
-
169
- /**
170
- * Synthetic placeholder entries ('(error)', '(no changes)', '(no staged
171
- * changes)', '(not a git repo)') are not real diffs, parenthesized so they
172
- * can be styled distinctly in the tab/status bar instead of reading as a fake
173
- * diff of a real file.
174
- */
175
- function isPlaceholderPath(filePath: string): boolean {
176
- return filePath.startsWith('(') && filePath.endsWith(')');
177
- }
178
-
179
- /**
180
- * Extract the file path from one `diff --git`-delimited chunk. Tries, in
181
- * order: the standard two-sided header (`diff --git a/x b/y`, bare or
182
- * quoted), the combined/merge-conflict header (`diff --cc <path>` /
183
- * `diff --combined <path>`, which carries no a/b prefixes), then falls back
184
- * to the `+++`/`---` file lines (present across every variant, uniformly),
185
- * skipping a `/dev/null` side (deleted/new file). 'unknown' is reserved for
186
- * genuinely unparseable input.
187
- */
188
- function extractDiffFilePath(trimmed: string): string {
189
- const quotedGit = trimmed.match(/^diff --git "a\/(.+)" "b\/(.+)"$/m);
190
- if (quotedGit) return quotedGit[2]!;
191
- const plainGit = trimmed.match(/^diff --git a\/.+? b\/(.+)$/m);
192
- if (plainGit) return plainGit[1]!;
193
- const combined = trimmed.match(/^diff --(?:cc|combined) "?([^"\n]+?)"?$/m);
194
- if (combined) return combined[1]!;
195
- const plus = trimmed.match(/^\+\+\+ (?:b\/)?"?([^"\n]+?)"?\s*$/m);
196
- if (plus && plus[1] !== '/dev/null') return plus[1]!;
197
- const minus = trimmed.match(/^--- (?:a\/)?"?([^"\n]+?)"?\s*$/m);
198
- if (minus && minus[1] !== '/dev/null') return minus[1]!;
199
- return 'unknown';
200
- }
201
-
202
- /** Reconstruct one file's unified-diff text from a StructuredDiffFile (header + hunks, prefixes re-attached; no size cap). */
203
- function reconstructStructuredFile(file: StructuredDiffFile): string {
204
- const out: string[] = [...file.headerLines];
205
- for (const hunk of file.hunks) {
206
- out.push(hunk.header);
207
- for (const line of hunk.lines) {
208
- const prefix = line.kind === 'add' ? '+' : line.kind === 'del' ? '-' : ' ';
209
- out.push(`${prefix}${line.text}`);
210
- }
211
- }
212
- return out.join('\n');
213
- }
214
-
215
- function splitIntoDiffEntries(raw: string): DiffEntry[] {
216
- const entries: DiffEntry[] = [];
217
- // Split on "diff --git" / "diff --cc" / "diff --combined" lines
218
- const chunks = raw.split(/(?=^diff --git |^diff --cc |^diff --combined )/m);
219
- for (const chunk of chunks) {
220
- const trimmed = chunk.trim();
221
- if (!trimmed) continue;
222
-
223
- entries.push({
224
- filePath: extractDiffFilePath(trimmed),
225
- raw: chunk,
226
- lines: parseDiff(chunk),
227
- });
228
- }
229
- return entries;
230
- }
231
-
232
- // ---------------------------------------------------------------------------
233
- // Rendering helpers
234
- // ---------------------------------------------------------------------------
235
-
236
- function makeLine(
237
- width: number,
238
- leftNum: string,
239
- rightNum: string,
240
- content: string,
241
- fg: string,
242
- bg: string,
243
- numFg: string,
244
- bold: boolean = false,
245
- ): Line {
246
- // Left line number (5 chars + space)
247
- const LEFT_W = 5;
248
- const usedForNums = LEFT_W + 1 + LEFT_W + 1 + 2; // 14
249
- const contentWidth = Math.max(0, width - usedForNums);
250
- const truncated = truncateDisplay(content, contentWidth);
251
- return buildStyledPanelLine(width, [
252
- { text: leftNum.padStart(LEFT_W), fg: numFg, bg, dim: true },
253
- { text: ' ', fg: '', bg },
254
- { text: rightNum.padStart(LEFT_W), fg: numFg, bg, dim: true },
255
- { text: ' ', fg: '', bg },
256
- { text: '| ', fg: COLOR.lineNum, bg },
257
- { text: truncated, fg, bg, bold },
258
- ]);
259
- }
260
-
261
- function renderText(width: number, text: string, fg: string, bg: string, bold = false): Line {
262
- return buildStyledPanelLine(width, [{ text: truncateDisplay(text, width), fg, bg, bold }]);
263
- }
264
-
265
- // ---------------------------------------------------------------------------
266
- // DiffPanel
267
- // ---------------------------------------------------------------------------
268
-
269
- export class DiffPanel extends BasePanel {
270
- public override isTransient = true;
271
-
272
- private readonly workingDirectory: string;
273
- private entries: DiffEntry[] = [];
274
- private selectedFile = 0;
275
- private scrollOffset = 0;
276
-
277
- /** One-line confirmation for the 'w'/'h'/'s' self-load hotkeys, otherwise a
278
- * reload that produces identical content is visually indistinguishable
279
- * from the keypress doing nothing at all. */
280
- private hotkeyStatus: string | null = null;
281
-
282
- public readonly confirmOverlay = new PanelConfirmOverlay(() => this.markDirty()); // armed by a command handler (e.g. /rewind); see panel-confirm-overlay.ts
283
- constructor(
284
- workingDirectory: string,
285
- private readonly requestRender: () => void = () => {},
286
- ) {
287
- super('diff', 'Diff', 'D', 'development');
288
- this.workingDirectory = workingDirectory;
289
- }
290
-
291
- // -------------------------------------------------------------------------
292
- // Public API
293
- // -------------------------------------------------------------------------
294
-
295
- /** Show a unified diff for a specific file. Adds or replaces the entry. */
296
- showDiff(filePath: string, diff: string): void {
297
- const idx = this.entries.findIndex(e => e.filePath === filePath);
298
- const entry: DiffEntry = { filePath, raw: diff, lines: parseDiff(diff), semanticSummary: this.entries[idx]?.semanticSummary };
299
- if (idx >= 0) {
300
- this.entries[idx] = entry;
301
- // Stay on this file if it was already selected
302
- if (this.selectedFile !== idx) {
303
- this.selectedFile = idx;
304
- this.scrollOffset = 0;
305
- }
306
- } else {
307
- this.entries.push(entry);
308
- this.selectedFile = this.entries.length - 1;
309
- this.scrollOffset = 0;
310
- }
311
- this.markDirty();
312
- }
313
-
314
- /** Load a raw multi-file unified diff string directly. */
315
- loadRawDiff(raw: string): void {
316
- this.entries = splitIntoDiffEntries(raw);
317
- this.selectedFile = 0;
318
- this.scrollOffset = 0;
319
- this.markDirty();
320
- }
321
-
322
- /**
323
- * Ingest a StructuredDiff (GitService.diffStructured, the FULL, uncapped
324
- * working-tree diff). Each file becomes one DiffEntry (unified text
325
- * reconstructed) so the existing tabs/renderer work unchanged; the old
326
- * 4,000-char slice is gone and a diff of any length renders complete.
327
- */
328
- loadStructuredDiff(diff: StructuredDiff): void {
329
- this.entries = diff.files.map((file) => {
330
- const raw = reconstructStructuredFile(file);
331
- return {
332
- filePath: file.newPath ?? file.oldPath ?? 'unknown',
333
- raw,
334
- lines: parseDiff(raw),
335
- };
336
- });
337
- this.selectedFile = 0;
338
- this.scrollOffset = 0;
339
- this.markDirty();
340
- }
341
-
342
- /**
343
- * Defensive gate mirroring GitPanel's own notGitRepo messaging, short-
344
- * circuits before any of this panel's self-load git spawns run, instead of
345
- * letting git fail per-subcommand with an inconsistent error shape.
346
- */
347
- private showNotGitRepo(): void {
348
- this.showDiff('(not a git repo)', '@@ -0,0 +0,0 @@\n Not a git repository here.');
349
- }
350
-
351
- /** Run `git diff` against specific files and populate entries. */
352
- async showFileDiffs(files: string[], ref?: string): Promise<void> {
353
- if (!GitService.isGitRepo(this.workingDirectory)) {
354
- this.showNotGitRepo();
355
- return;
356
- }
357
- const args = ['diff', ...(ref ? [ref] : []), '--', ...files];
358
- const proc = Bun.spawn(['git', ...args], { stdout: 'pipe', stderr: 'pipe', cwd: this.workingDirectory });
359
- const [raw, errText] = await Promise.all([
360
- new Response(proc.stdout).text(),
361
- new Response(proc.stderr).text(),
362
- ]);
363
- const exitCode = await proc.exited;
364
- if (exitCode !== 0) {
365
- const errorText = errText.trim() || 'git diff failed';
366
- this.showDiff('(error)', `--- error\n+++ error\n@@ -0,0 +1,1 @@\n+${errorText}`);
367
- return;
368
- }
369
- this.loadRawDiff(raw);
370
- this.enrichSemanticDiffs(files, ref ?? 'HEAD').catch((err) => { logger.debug('DiffPanel: semantic diff enrichment failed', { err }); });
371
- }
372
-
373
- /**
374
- * Run `git diff` and populate all changed files. Self-load entry point for
375
- * the 'w' (working tree, no ref) and 'h' (vs HEAD) keys. Automatically
376
- * enriches every loaded file with a semantic-diff summary in the background.
377
- */
378
- async showGitDiff(ref?: string): Promise<void> {
379
- if (!GitService.isGitRepo(this.workingDirectory)) {
380
- this.showNotGitRepo();
381
- return;
382
- }
383
- const args = ['diff', ...(ref ? [ref] : [])];
384
- const proc = Bun.spawn(['git', ...args], { stdout: 'pipe', stderr: 'pipe', cwd: this.workingDirectory });
385
- const [raw, errText] = await Promise.all([
386
- new Response(proc.stdout).text(),
387
- new Response(proc.stderr).text(),
388
- ]);
389
- const exitCode = await proc.exited;
390
- if (exitCode !== 0) {
391
- const errorText = errText.trim() || 'git diff failed';
392
- this.showDiff('(error)', `--- error\n+++ error\n@@ -0,0 +1,1 @@\n+${errorText}`);
393
- return;
394
- }
395
- if (!raw.trim()) {
396
- this.showDiff('(no changes)', '@@ -0,0 +0,0 @@\n No changes in working tree.');
397
- return;
398
- }
399
- const newEntries = splitIntoDiffEntries(raw);
400
- // Merge: update existing, append new
401
- for (const entry of newEntries) {
402
- const idx = this.entries.findIndex(e => e.filePath === entry.filePath);
403
- if (idx >= 0) {
404
- this.entries[idx] = { ...entry, semanticSummary: this.entries[idx]!.semanticSummary };
405
- } else {
406
- this.entries.push(entry);
407
- }
408
- }
409
- this.selectedFile = Math.min(this.selectedFile, Math.max(0, this.entries.length - 1));
410
- this.scrollOffset = 0;
411
- this.markDirty();
412
- this.enrichSemanticDiffs(newEntries.map((e) => e.filePath), 'HEAD').catch((err) => { logger.debug('DiffPanel: semantic diff enrichment failed', { err }); });
413
- }
414
-
415
- /**
416
- * Run `git diff --cached` and populate the staged changes. Self-load entry
417
- * point for the 's' key, its own diff plumbing, independent of the
418
- * `/diff staged` command handler.
419
- */
420
- async showStagedDiff(): Promise<void> {
421
- if (!GitService.isGitRepo(this.workingDirectory)) {
422
- this.showNotGitRepo();
423
- return;
424
- }
425
- const proc = Bun.spawn(['/bin/sh', '-c', 'git diff --cached'], { stdout: 'pipe', stderr: 'pipe', cwd: this.workingDirectory });
426
- const [raw, errText] = await Promise.all([
427
- new Response(proc.stdout).text(),
428
- new Response(proc.stderr).text(),
429
- ]);
430
- const exitCode = await proc.exited;
431
- if (exitCode !== 0) {
432
- const errorText = errText.trim() || 'git diff --cached failed';
433
- this.showDiff('(error)', `--- error\n+++ error\n@@ -0,0 +1,1 @@\n+${errorText}`);
434
- return;
435
- }
436
- if (!raw.trim()) {
437
- this.showDiff('(no staged changes)', '@@ -0,0 +0,0 @@\n No staged changes.');
438
- return;
439
- }
440
- this.loadRawDiff(raw);
441
- this.enrichSemanticDiffs(this.entries.map((e) => e.filePath), 'HEAD').catch((err) => { logger.debug('DiffPanel: semantic diff enrichment failed', { err }); });
442
- }
443
-
444
- /**
445
- * Best-effort semantic-diff enrichment for a set of files, run against the
446
- * before-content at `ref` and the current on-disk after-content. Self-contained
447
- * diff plumbing, does not depend on the `/diff` command handler.
448
- */
449
- private async enrichSemanticDiffs(files: string[], ref: string): Promise<void> {
450
- if (files.length === 0) return;
451
- const { computeSemanticDiff, formatSemanticDiffSummary } = await import('../renderer/semantic-diff.ts');
452
- const { join, relative: pathRelative } = await import('path');
453
- const repoRootProc = Bun.spawn(['git', 'rev-parse', '--show-toplevel'], { stdout: 'pipe', stderr: 'pipe', cwd: this.workingDirectory });
454
- await repoRootProc.exited;
455
- const repoRoot = (await new Response(repoRootProc.stdout).text()).trim() || this.workingDirectory;
456
- await Promise.allSettled(
457
- files.map(async (filePath) => {
458
- try {
459
- const absPath = filePath.startsWith('/') ? filePath : join(this.workingDirectory, filePath);
460
- const repoRelPath = filePath.startsWith('/') ? pathRelative(repoRoot, filePath) : filePath;
461
- const [beforeResult, afterResult] = await Promise.allSettled([
462
- (async () => {
463
- const proc = Bun.spawn(
464
- ['git', 'show', `${ref}:${repoRelPath}`],
465
- { stdout: 'pipe', stderr: 'pipe', cwd: repoRoot },
466
- );
467
- const [text, exitCode] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
468
- if (exitCode !== 0) throw new Error(`git show failed for ${repoRelPath}`);
469
- return text;
470
- })(),
471
- Bun.file(absPath).text(),
472
- ]);
473
- if (beforeResult.status !== 'fulfilled' || afterResult.status !== 'fulfilled') return;
474
- const semanticDiff = await computeSemanticDiff(filePath, beforeResult.value, afterResult.value);
475
- if (!semanticDiff) return;
476
- const summary = formatSemanticDiffSummary(semanticDiff);
477
- if (summary) this.setSemanticSummary(filePath, summary);
478
- } catch {
479
- // best-effort only
480
- }
481
- }),
482
- );
483
- }
484
-
485
- /**
486
- * Attach or update the semantic diff summary for a file entry.
487
- * No-op if the file isn't currently loaded. Safe to call from an async
488
- * callback after the entry has already been replaced.
489
- */
490
- setSemanticSummary(filePath: string, summary: string): void {
491
- const entry = this.entries.find(e => e.filePath === filePath);
492
- if (entry) {
493
- entry.semanticSummary = summary;
494
- this.markDirty();
495
- }
496
- }
497
-
498
- /** Clear all diff entries. */
499
- clear(): void {
500
- this.entries = [];
501
- this.selectedFile = 0;
502
- this.scrollOffset = 0;
503
- this.markDirty();
504
- }
505
-
506
- // -------------------------------------------------------------------------
507
- // Lifecycle
508
- // -------------------------------------------------------------------------
509
-
510
- override onActivate(): void {
511
- this.needsRender = true;
512
- }
513
-
514
- // -------------------------------------------------------------------------
515
- // Input
516
- // -------------------------------------------------------------------------
517
-
518
- /**
519
- * Wraps a self-load hotkey ('w'/'h'/'s') with visible before/after status
520
- * text in the status bar, otherwise a reload whose content is unchanged
521
- * from what's already shown is indistinguishable from the keypress having
522
- * done nothing at all (contrast with `/diff working`'s ctx.print() calls).
523
- */
524
- private runHotkeyReload(label: string, load: () => Promise<void>): void {
525
- this.hotkeyStatus = `Loading ${label} diff…`;
526
- this.markDirty();
527
- void load().then(() => {
528
- this.hotkeyStatus = `Reloaded ${label} diff.`;
529
- this.markDirty();
530
- this.requestRender();
531
- });
532
- }
533
-
534
- handleInput(key: string): boolean {
535
- if (this.confirmOverlay.handleInput(key)) return true; // confirm takes priority (git-panel.ts:284-303 precedent)
536
- switch (key) {
537
- case 'up': this.scrollUp(); return true;
538
- case 'down': this.scrollDown(); return true;
539
- case 'tab': this.nextFile(); return true;
540
- // Shift+Tab: most terminals send the bare CSI-Z ("backtab") escape
541
- // sequence rather than a Tab keypress with a shift modifier, and the
542
- // input tokenizer passes that sequence through unchanged as the
543
- // logical key name, so it (and the friendlier aliases some terminals/
544
- // future tokenizer versions may emit) are matched explicitly here.
545
- case '\x1b[Z':
546
- case 'shift-tab':
547
- case 'backtab': this.prevFile(); return true;
548
- case 'pageup': this.scrollPageUp(); return true;
549
- case 'pagedown': this.scrollPageDown(); return true;
550
- case 'w': this.runHotkeyReload('working tree', () => this.showGitDiff()); return true;
551
- case 'h': this.runHotkeyReload('HEAD', () => this.showGitDiff('HEAD')); return true;
552
- case 's': this.runHotkeyReload('staged', () => this.showStagedDiff()); return true;
553
- default: return false;
554
- }
555
- }
556
-
557
- // (the purge): 'o' used to open the currently selected file in the
558
- // preview panel via a staged pendingOpenPreview flag + a
559
- // handlePanelIntegrationAction cross-panel hook (the same bridge
560
- // FileExplorerPanel used). 'preview' is DELETE-disposition with no
561
- // successor surface (no file-picker-overlay preview to repoint to
562
- // either, verified), so the key and the hook were removed rather than
563
- // repointed; diff no longer has an 'o' action.
564
-
565
- private scrollUp(): void {
566
- if (this.scrollOffset > 0) {
567
- this.scrollOffset--;
568
- this.markDirty();
569
- }
570
- }
571
-
572
- private scrollDown(): void {
573
- const entry = this.currentEntry();
574
- if (!entry) return;
575
- const max = Math.max(0, entry.lines.length - 1);
576
- if (this.scrollOffset < max) {
577
- this.scrollOffset++;
578
- this.markDirty();
579
- }
580
- }
581
-
582
- private scrollPageUp(): void {
583
- this.scrollOffset = Math.max(0, this.scrollOffset - 20);
584
- this.markDirty();
585
- }
586
-
587
- private scrollPageDown(): void {
588
- const entry = this.currentEntry();
589
- if (!entry) return;
590
- const max = Math.max(0, entry.lines.length - 1);
591
- this.scrollOffset = Math.min(max, this.scrollOffset + 20);
592
- this.markDirty();
593
- }
594
-
595
- private nextFile(): void {
596
- if (this.entries.length === 0) return;
597
- this.selectedFile = (this.selectedFile + 1) % this.entries.length;
598
- this.scrollOffset = 0;
599
- this.markDirty();
600
- }
601
-
602
- private prevFile(): void {
603
- if (this.entries.length === 0) return;
604
- this.selectedFile = (this.selectedFile - 1 + this.entries.length) % this.entries.length;
605
- this.scrollOffset = 0;
606
- this.markDirty();
607
- }
608
-
609
- private currentEntry(): DiffEntry | null {
610
- return this.entries[this.selectedFile] ?? null;
611
- }
612
-
613
- // -------------------------------------------------------------------------
614
- // Render
615
- // -------------------------------------------------------------------------
616
-
617
- render(width: number, height: number): Line[] {
618
- return this.trackedRender(() => {
619
- if (height <= 0 || width <= 0) return [];
620
-
621
- const confirmLines = this.confirmOverlay.renderLines(width);
622
- if (confirmLines) return buildPanelWorkspace(width, height, { title: 'Diff Workspace', palette: COLOR, sections: [{ title: 'Confirmation', lines: confirmLines }] });
623
- if (this.entries.length === 0) {
624
- return buildPanelWorkspace(width, height, {
625
- title: 'Diff Workspace',
626
- palette: COLOR,
627
- sections: [{
628
- title: 'Diff',
629
- lines: buildEmptyState(
630
- width,
631
- ' No diff to display.',
632
- 'Load a git diff or select a changed file to populate the workspace. w=working h=HEAD s=staged self-load right here.',
633
- // corrected, bare /diff loads files changed *this
634
- // session* (falling back to the full HEAD diff when none are
635
- // tracked yet), not the working-tree diff the w key already
636
- // loads in-panel; the old summary text described the wrong
637
- // command.
638
- [{ command: '/diff', summary: 'load files changed this session into the diff workspace (falls back to the HEAD diff if none yet)' }],
639
- COLOR,
640
- ),
641
- }],
642
- });
643
- }
644
-
645
- const entry = this.currentEntry();
646
- if (!entry) {
647
- return Array.from({ length: height }, () => createEmptyLine(width));
648
- }
649
-
650
- const compact = height <= 12;
651
- const summaryLines = entry.semanticSummary
652
- ? buildBodyText(width, `Semantic summary: ${entry.semanticSummary}`, COLOR, COLOR.context)
653
- : [];
654
- const previewSection = resolveScrollablePanelSection(width, height, {
655
- palette: COLOR,
656
- footerLines: [this.renderStatusBar(width, entry)],
657
- beforeSections: [
658
- {
659
- title: compact ? undefined : 'Files',
660
- lines: [
661
- this.renderTabBar(width),
662
- ...summaryLines,
663
- ],
664
- },
665
- ],
666
- section: {
667
- title: compact ? undefined : 'Changes',
668
- scrollableLines: entry.lines.map((pl) => this.renderParsedLine(pl, width)),
669
- scrollOffset: this.scrollOffset,
670
- minRows: 1,
671
- },
672
- });
673
- this.scrollOffset = previewSection.scrollOffset;
674
-
675
- const sections: PanelWorkspaceSection[] = [
676
- {
677
- title: compact ? undefined : 'Files',
678
- lines: [
679
- this.renderTabBar(width),
680
- ...summaryLines,
681
- ],
682
- },
683
- {
684
- title: previewSection.section.title,
685
- lines: previewSection.section.lines,
686
- },
687
- ];
688
- return buildPanelWorkspace(width, height, {
689
- title: 'Diff Workspace',
690
- palette: COLOR,
691
- sections,
692
- footerLines: [this.renderStatusBar(width, entry)],
693
- });
694
- });
695
- }
696
-
697
- // ── Tab bar ──────────────────────────────────────────────────────────────
698
-
699
- private renderTabBar(width: number): Line {
700
- const cells: Line = [];
701
- const push = (ch: string, fg: string, bg: string, bold = false): void => {
702
- const cw = getDisplayWidth(ch);
703
- if (cells.length + cw > width) return;
704
- cells.push(createStyledCell(ch, { fg, bg, bold }));
705
- if (cw === 2 && cells.length < width) cells.push(createStyledCell('', { fg, bg }));
706
- };
707
-
708
- for (let i = 0; i < this.entries.length; i++) {
709
- const entry = this.entries[i]!;
710
- const active = i === this.selectedFile;
711
- const placeholder = isPlaceholderPath(entry.filePath);
712
- const stat = diffStat(entry);
713
- // Placeholder entries ('(error)', '(no changes)', ...) get a distinct
714
- // warn color regardless of active/inactive, they are not a real diff
715
- // of a real file and must not read as one.
716
- const fg = placeholder ? COLOR.warn : (active ? COLOR.tabActive : COLOR.tabInactive);
717
- const bg = active ? COLOR.tabActiveBg : COLOR.tabBg;
718
- // Active file gets a leading marker; every tab shows +adds/-dels at a glance.
719
- const marker = active ? '▸ ' : (placeholder ? '⚠ ' : ' ');
720
- const label = `${marker}${basename(entry.filePath)} `;
721
- for (const ch of label) push(ch, fg, bg, active);
722
- if (stat.added > 0) for (const ch of `+${stat.added}`) push(ch, COLOR.addition, bg, active);
723
- if (stat.added > 0 && stat.removed > 0) push(' ', fg, bg);
724
- if (stat.removed > 0) for (const ch of `-${stat.removed}`) push(ch, COLOR.deletion, bg, active);
725
- push(' ', fg, bg);
726
- push('│', COLOR.lineNum, COLOR.tabBg);
727
- }
728
-
729
- while (cells.length < width) {
730
- cells.push(createStyledCell(' ', { fg: '', bg: COLOR.tabBg }));
731
- }
732
- return cells.slice(0, width);
733
- }
734
-
735
- // ── Status bar ───────────────────────────────────────────────────────────
736
-
737
- private renderStatusBar(width: number, entry: DiffEntry | null): Line {
738
- if (!entry) {
739
- return buildStyledPanelLine(width, [{ text: ' No file', fg: COLOR.tabInactive, bg: COLOR.statusBar }], { fillBg: COLOR.statusBar });
740
- }
741
- const stat = diffStat(entry);
742
- const placeholder = isPlaceholderPath(entry.filePath);
743
- // Keep the file path display-width-aware so a long/wide path can't overflow.
744
- const pathBudget = Math.max(8, Math.floor(width / 2));
745
- const fileInfo = truncateDisplay(entry.filePath, pathBudget);
746
- const segments: Array<{ text: string; fg: string; bg?: string; bold?: boolean }> = [
747
- // Placeholder states ('(error)', '(no changes)', ...) are not a real
748
- // diff of a real file, a distinct warn color keeps them from being
749
- // mistaken for one.
750
- { text: ` ${fileInfo} `, fg: placeholder ? COLOR.warn : COLOR.filename, bg: COLOR.statusBar, bold: true },
751
- { text: `[${this.selectedFile + 1}/${this.entries.length}]`, fg: COLOR.tabInactive, bg: COLOR.statusBar },
752
- { text: ' +', fg: COLOR.tabInactive, bg: COLOR.statusBar },
753
- { text: String(stat.added), fg: COLOR.addition, bg: COLOR.statusBar },
754
- { text: ' -', fg: COLOR.tabInactive, bg: COLOR.statusBar },
755
- { text: String(stat.removed), fg: COLOR.deletion, bg: COLOR.statusBar },
756
- { text: ` L${this.scrollOffset + 1}/${entry.lines.length}`, fg: COLOR.tabInactive, bg: COLOR.statusBar },
757
- { text: ' Tab/S-Tab file o open w/h/s load ↑/↓ scroll', fg: COLOR.context, bg: COLOR.statusBar },
758
- ];
759
- if (entry.semanticSummary) {
760
- segments.push({ text: ` ◈ ${entry.semanticSummary}`, fg: COLOR.hunk, bg: COLOR.statusBar });
761
- }
762
- if (this.hotkeyStatus) {
763
- segments.push({ text: ` ${this.hotkeyStatus}`, fg: COLOR.info, bg: COLOR.statusBar, bold: true });
764
- }
765
- return buildStyledPanelLine(width, segments, { fillBg: COLOR.statusBar });
766
- }
767
-
768
- // ── Parsed line ──────────────────────────────────────────────────────────
769
-
770
- private renderParsedLine(pl: ParsedLine, width: number): Line {
771
- const left = pl.beforeNum !== null ? String(pl.beforeNum) : '';
772
- const right = pl.afterNum !== null ? String(pl.afterNum) : '';
773
-
774
- switch (pl.kind) {
775
- case 'addition':
776
- return makeLine(width, left, right, `+ ${pl.text}`, COLOR.addition, COLOR.additionBg, COLOR.lineNumAdd, true);
777
- case 'deletion':
778
- return makeLine(width, left, right, `- ${pl.text}`, COLOR.deletion, COLOR.deletionBg, COLOR.lineNumDel, false);
779
- case 'hunk':
780
- return renderText(width, pl.text, COLOR.hunk, COLOR.hunkBg, false);
781
- case 'header':
782
- return renderText(width, pl.text, COLOR.markerText, '', false);
783
- case 'context':
784
- default:
785
- return makeLine(width, left, right, ` ${pl.text}`, COLOR.context, '', COLOR.lineNum, false);
786
- }
787
- }
788
- }
789
-
790
- // ---------------------------------------------------------------------------
791
- // Helpers
792
- // ---------------------------------------------------------------------------
793
-
794
- function basename(p: string): string {
795
- const parts = p.replace(/\\/g, '/').split('/');
796
- return parts[parts.length - 1] ?? p;
797
- }