@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,800 +0,0 @@
1
- import { BasePanel } from './base-panel.ts';
2
- import { createEmptyLine, createStyledCell, type Line } from '@pellux/goodvibes-sdk/platform/types';
3
- import { truncateDisplay, getDisplayWidth } from '../utils/terminal-width.ts';
4
- import { GitService } from '@pellux/goodvibes-sdk/platform/git';
5
- import { logger } from '@pellux/goodvibes-sdk/platform/utils';
6
- import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
7
- import { UI_TONES, DIFF_TONES } from '../renderer/ui-primitives.ts';
8
- import {
9
- buildEmptyState,
10
- buildKeyboardHints,
11
- buildPanelLine,
12
- buildPanelWorkspace,
13
- buildSearchInputLine,
14
- resolveScrollablePanelSection,
15
- buildSelectablePanelLine,
16
- buildStyledPanelLine,
17
- DEFAULT_PANEL_PALETTE,
18
- extendPalette,
19
- } from './polish.ts';
20
- import { type ConfirmState, handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
21
- import {
22
- isPanelSearchBackspace,
23
- isPanelSearchCancel,
24
- isPanelSearchCommit,
25
- isPanelSearchPrintable,
26
- } from './search-focus.ts';
27
-
28
- // Types
29
-
30
- interface GitFileEntry {
31
- path: string;
32
- staged: boolean;
33
- /** True when this file was touched during the current session (best-effort). */
34
- sessionChanged?: boolean;
35
- }
36
-
37
- interface CommitEntry {
38
- hash: string;
39
- message: string;
40
- author: string;
41
- date: string;
42
- }
43
-
44
- interface GitData {
45
- branch: string;
46
- ahead: number;
47
- behind: number;
48
- stagedFiles: GitFileEntry[];
49
- unstagedFiles: GitFileEntry[];
50
- recentCommits: CommitEntry[];
51
- }
52
-
53
- type ViewItem =
54
- | { kind: 'header' }
55
- | { kind: 'section'; label: string }
56
- | { kind: 'file'; entry: GitFileEntry }
57
- | { kind: 'commit'; entry: CommitEntry }
58
- | { kind: 'empty'; label: string };
59
-
60
- /** Subject carried by the panel's single ConfirmState, either a repo init or a commit. */
61
- type GitConfirmSubject =
62
- | { kind: 'init' }
63
- | { kind: 'commit'; message: string };
64
-
65
- // Constants
66
-
67
- /** Minimum number of diff lines kept visible when clamping scroll offset. */
68
- const MIN_VISIBLE_DIFF_LINES = 5;
69
-
70
- // Colors
71
-
72
- const C = extendPalette(DEFAULT_PANEL_PALETTE, {
73
- branch: '#00d7ff',
74
- sectionHeader: '244',
75
- commit: '250',
76
- commitHash: '238',
77
- commitAuthor: '244',
78
- selected: '#1c1c1c',
79
- selectedFg: '#ffffff',
80
- diffMeta: DIFF_TONES.hunk, // shared diff-hunk token, was a local literal
81
- diffNeutral: '250',
82
- // Reuses the existing workflow accent token rather than adding a new hex
83
- // literal (architecture gate ratchets the raw-hex-literal count).
84
- sessionChanged: UI_TONES.accent.workflow,
85
- });
86
-
87
- // GitPanel
88
-
89
- export class GitPanel extends BasePanel {
90
- private readonly workingDirectory: string;
91
- private readonly git: GitService;
92
- private readonly getChangedFiles?: () => readonly string[];
93
-
94
- private data: GitData = {
95
- branch: '...',
96
- ahead: 0,
97
- behind: 0,
98
- stagedFiles: [],
99
- unstagedFiles: [],
100
- recentCommits: [],
101
- };
102
-
103
- /** Flattened list of navigable rows (for arrow-key movement). */
104
- private items: ViewItem[] = [];
105
-
106
- /** Selected row index within `items`. */
107
- private selectedIndex = 0;
108
-
109
- /** When truthy, shows the diff for the selected file or commit. */
110
- private expandedDiff: string[] | null = null;
111
-
112
- /** Scroll offset for both main view and diff view. */
113
- private scrollOffset = 0;
114
-
115
- private refreshTimerId: ReturnType<typeof setInterval> | null = null;
116
- private loading = true;
117
- private error: string | null = null;
118
-
119
- /** True when the last refresh failed because `workingDirectory` isn't a git repo. */
120
- private notGitRepo = false;
121
-
122
- /** Single pending confirm, either "init this repo" or "commit with this message". */
123
- private confirm: ConfirmState<GitConfirmSubject> | null = null;
124
-
125
- /** Non-null while composing a commit message; null means the compose UI is closed. */
126
- private commitMessage: string | null = null;
127
-
128
- constructor(
129
- workingDirectory: string,
130
- private readonly requestRender: () => void = () => {},
131
- getChangedFiles?: () => readonly string[],
132
- ) {
133
- super('git', 'Git', 'G', 'development');
134
- this.workingDirectory = workingDirectory;
135
- // I2: reuse a single GitService instance across the panel's lifetime instead
136
- // of constructing a new one for every refresh/diff/stage/commit call.
137
- this.git = new GitService(workingDirectory);
138
- this.getChangedFiles = getChangedFiles;
139
- }
140
-
141
- private _markDirtyAndRender(): void {
142
- this.markDirty();
143
- this.requestRender();
144
- }
145
-
146
- // Lifecycle
147
-
148
- override onActivate(): void {
149
- super.onActivate();
150
- void this.refresh();
151
- this.refreshTimerId = this.registerTimer(setInterval(() => {
152
- void this.refresh();
153
- }, 5_000));
154
- }
155
-
156
- override onDeactivate(): void {
157
- if (this.refreshTimerId !== null) {
158
- this.clearTimer(this.refreshTimerId);
159
- this.refreshTimerId = null;
160
- }
161
- }
162
-
163
- override onDestroy(): void {
164
- this.onDeactivate();
165
- super.onDestroy();
166
- }
167
-
168
- // Data fetching
169
-
170
- private async refresh(): Promise<void> {
171
- try {
172
- const [statusResult, branchResult, logEntries] = await Promise.all([
173
- this.git.status(),
174
- this.git.branch(),
175
- this.git.log(10),
176
- ]);
177
-
178
- const changed = this.getChangedFiles ? new Set(this.getChangedFiles()) : null;
179
- const markChanged = (path: string): boolean | undefined => (changed ? changed.has(path) : undefined);
180
-
181
- // Classify from the raw per-file porcelain columns (index/working_dir), not simple-git's
182
- // modified/staged arrays: those double-count a staged-with-no-further-edits file (index 'M',
183
- // working_dir ' ') as both staged AND unstaged, landing 's'/'u' on a phantom row.
184
- const stagedFiles: GitFileEntry[] = [];
185
- const unstagedFiles: GitFileEntry[] = [];
186
- for (const f of statusResult.files) {
187
- const sessionChanged = markChanged(f.path);
188
- if (f.index === '?' && f.working_dir === '?') {
189
- unstagedFiles.push({ path: f.path, staged: false, sessionChanged }); // untracked
190
- continue;
191
- }
192
- if (f.index !== ' ') stagedFiles.push({ path: f.path, staged: true, sessionChanged });
193
- if (f.working_dir !== ' ') unstagedFiles.push({ path: f.path, staged: false, sessionChanged });
194
- }
195
-
196
- this.data = {
197
- branch: branchResult.current || 'HEAD',
198
- ahead: statusResult.ahead ?? 0,
199
- behind: statusResult.behind ?? 0,
200
- stagedFiles,
201
- unstagedFiles,
202
- recentCommits: logEntries.map((e) => ({
203
- hash: e.hash.slice(0, 7),
204
- message: e.message,
205
- author: e.author,
206
- date: e.date,
207
- })),
208
- };
209
-
210
- this.loading = false;
211
- this.error = null;
212
- this.notGitRepo = false;
213
- this.rebuildItems();
214
- // Do not clear expandedDiff during auto-refresh, only clear on explicit user action
215
- this._markDirtyAndRender();
216
- } catch (err) {
217
- const msg = summarizeError(err);
218
- if (/not a git\b/i.test(msg)) {
219
- // I4: no more auto `git init` side effect, surface the state and let
220
- // the user explicitly confirm initialisation with 'i'.
221
- this.notGitRepo = true;
222
- this.error = 'Not a git repository here. Press i to initialize one (explicit confirm).';
223
- } else {
224
- this.notGitRepo = false;
225
- this.error = msg;
226
- }
227
- this.loading = false;
228
- logger.debug('GitPanel: refresh failed', { error: this.error });
229
- this._markDirtyAndRender();
230
- }
231
- }
232
-
233
- /** Push a labeled section (with count) plus its rows, or an empty-row placeholder. */
234
- private pushSection<T>(items: ViewItem[], label: string, entries: readonly T[], emptyLabel: string, toItem: (entry: T) => ViewItem): void {
235
- items.push({ kind: 'section', label: `${label} (${entries.length})` });
236
- if (entries.length === 0) items.push({ kind: 'empty', label: emptyLabel });
237
- else for (const entry of entries) items.push(toItem(entry));
238
- }
239
-
240
- /** Rebuild the flat navigable item list from current data. */
241
- private rebuildItems(): void {
242
- const items: ViewItem[] = [{ kind: 'header' }];
243
- this.pushSection(items, 'Staged', this.data.stagedFiles, ' (no staged files)', (entry) => ({ kind: 'file', entry }));
244
- this.pushSection(items, 'Unstaged', this.data.unstagedFiles, ' (no unstaged files)', (entry) => ({ kind: 'file', entry }));
245
- this.pushSection(items, 'Recent Commits', this.data.recentCommits, ' (no commits)', (entry) => ({ kind: 'commit', entry }));
246
- this.items = items;
247
-
248
- // Keep selection in bounds and off header/section/empty filler rows (I5).
249
- if (this.selectedIndex >= this.items.length) {
250
- this.selectedIndex = Math.max(0, this.items.length - 1);
251
- }
252
- if (!this.isSelectable(this.items.at(this.selectedIndex))) {
253
- const firstSelectable = this.items.findIndex((it) => this.isSelectable(it));
254
- if (firstSelectable >= 0) this.selectedIndex = firstSelectable;
255
- }
256
- }
257
-
258
- /** True for rows the cursor is allowed to rest on (file or commit rows). */
259
- private isSelectable(item: ViewItem | undefined): boolean {
260
- return item?.kind === 'file' || item?.kind === 'commit';
261
- }
262
-
263
- /** Move the selection to the next/previous selectable row, skipping filler rows. */
264
- private moveSelection(direction: 1 | -1): void {
265
- let idx = this.selectedIndex;
266
- for (let i = 0; i < this.items.length; i++) {
267
- idx += direction;
268
- if (idx < 0 || idx >= this.items.length) return;
269
- if (this.isSelectable(this.items[idx])) {
270
- this.selectedIndex = idx;
271
- this.markDirty();
272
- return;
273
- }
274
- }
275
- }
276
-
277
- // Input handling
278
-
279
- /** Commit-message entry wants every char of a burst delivered one at a time, see Panel.isCapturingTextBurst. */
280
- isCapturingTextBurst(): boolean {
281
- return this.commitMessage !== null;
282
- }
283
-
284
- handleInput(key: string): boolean {
285
- // Confirm (init repo / commit) takes priority over everything else.
286
- const confirmResult = handleConfirmInput(this.confirm, key);
287
- if (confirmResult === 'confirmed') {
288
- const subject = this.confirm!.subject;
289
- this.confirm = null;
290
- if (subject.kind === 'init') {
291
- void this.performInit();
292
- } else {
293
- void this.performCommit(subject.message);
294
- }
295
- this.markDirty();
296
- return true;
297
- }
298
- if (confirmResult === 'cancelled') {
299
- this.confirm = null;
300
- this.markDirty();
301
- return true;
302
- }
303
- if (confirmResult === 'absorbed') return true;
304
-
305
- if (this.commitMessage !== null) {
306
- return this.handleCommitMessageInput(key);
307
- }
308
-
309
- if (this.expandedDiff !== null) {
310
- return this.handleDiffInput(key);
311
- }
312
- return this.handleListInput(key);
313
- }
314
-
315
- private handleListInput(key: string): boolean {
316
- switch (key) {
317
- case 'up':
318
- case 'k': {
319
- this.moveSelection(-1);
320
- return true;
321
- }
322
- case 'down':
323
- case 'j': {
324
- this.moveSelection(1);
325
- return true;
326
- }
327
- case 'return': {
328
- void this.openDiff();
329
- return true;
330
- }
331
- case 'r': {
332
- void this.refresh();
333
- return true;
334
- }
335
- case 's': {
336
- void this.setStaged(true);
337
- return true;
338
- }
339
- case 'u': {
340
- void this.setStaged(false);
341
- return true;
342
- }
343
- case 'c': {
344
- this.commitMessage = '';
345
- this.markDirty();
346
- return true;
347
- }
348
- case 'i': {
349
- // Only meaningful when no repo is loaded, otherwise leave the key
350
- // unconsumed instead of absorbing it as a silent no-op.
351
- if (!this.notGitRepo) return false;
352
- this.confirm = { subject: { kind: 'init' }, label: this.workingDirectory, verb: 'Init' };
353
- this.markDirty();
354
- return true;
355
- }
356
- default:
357
- return false;
358
- }
359
- }
360
-
361
- private handleCommitMessageInput(key: string): boolean {
362
- if (isPanelSearchCancel(key)) {
363
- this.commitMessage = null;
364
- this.markDirty();
365
- return true;
366
- }
367
- if (isPanelSearchCommit(key)) {
368
- const message = (this.commitMessage ?? '').trim();
369
- if (!message) return true; // absorb, require a non-empty message before confirming
370
- this.confirm = { subject: { kind: 'commit', message }, label: message, verb: 'Commit' };
371
- this.commitMessage = null;
372
- this.markDirty();
373
- return true;
374
- }
375
- if (isPanelSearchBackspace(key)) {
376
- this.commitMessage = (this.commitMessage ?? '').slice(0, -1);
377
- this.markDirty();
378
- return true;
379
- }
380
- if (isPanelSearchPrintable(key)) {
381
- this.commitMessage = (this.commitMessage ?? '') + key;
382
- this.markDirty();
383
- return true;
384
- }
385
- return true; // absorb every other key while composing
386
- }
387
-
388
- private handleDiffInput(key: string): boolean {
389
- switch (key) {
390
- case 'up':
391
- case 'k': {
392
- if (this.scrollOffset > 0) {
393
- this.scrollOffset--;
394
- this.markDirty();
395
- }
396
- return true;
397
- }
398
- case 'down':
399
- case 'j': {
400
- const diffLen = this.expandedDiff?.length ?? 0;
401
- this.scrollOffset = Math.min(this.scrollOffset + 1, Math.max(0, diffLen - MIN_VISIBLE_DIFF_LINES));
402
- this.markDirty();
403
- return true;
404
- }
405
- case 'escape':
406
- case 'q': {
407
- this.expandedDiff = null;
408
- this.scrollOffset = 0;
409
- this.markDirty();
410
- return true;
411
- }
412
- default:
413
- return false;
414
- }
415
- }
416
-
417
- /** Commit the given lines as the new expanded diff and reset scroll/dirty state. */
418
- private finishDiff(lines: string[]): void {
419
- this.expandedDiff = lines;
420
- this.scrollOffset = 0;
421
- this.markDirty();
422
- }
423
-
424
- // I3: withLoading guarantees the spinner is cleared even if the fetch throws.
425
- private async openDiff(): Promise<void> {
426
- const item = this.items.at(this.selectedIndex);
427
- if (!item) return;
428
-
429
- if (item.kind === 'file') {
430
- try {
431
- const raw = await this.withLoading('Loading diff…', async () => this.git.diffFile(item.entry.path, item.entry.staged));
432
- this.finishDiff(raw ? raw.split('\n') : ['(no diff available)']);
433
- } catch (err) {
434
- this.finishDiff([`Error: ${summarizeError(err)}`]);
435
- }
436
- return;
437
- }
438
-
439
- if (item.kind === 'commit') {
440
- // Root commits have no parent (`<hash>^` is invalid), the catch below
441
- // surfaces that instead of pretending a diff exists.
442
- const before = `${item.entry.hash}^`;
443
- const after = item.entry.hash;
444
- try {
445
- const [patch, stat] = await this.withLoading('Loading commit…', async () => Promise.all([
446
- this.git.diffBetween(before, after),
447
- this.git.diffStat(before, after),
448
- ]));
449
- const statLines = stat.split('\n').filter((l) => l.trim().length > 0);
450
- const patchLines = patch ? patch.split('\n') : [];
451
- this.finishDiff(statLines.length > 0 ? [...statLines, '', ...patchLines] : (patchLines.length > 0 ? patchLines : ['(no diff available)']));
452
- } catch (err) {
453
- this.finishDiff([`Error: ${summarizeError(err)}`]);
454
- }
455
- }
456
- }
457
-
458
- // -- Mutating actions: stage / unstage / commit / init ----------------------
459
-
460
- /** Stage (target=true) or unstage (target=false) the selected file row. */
461
- private async setStaged(target: boolean): Promise<void> {
462
- const item = this.items.at(this.selectedIndex);
463
- if (!item || item.kind !== 'file' || item.entry.staged === target) return;
464
- try {
465
- if (target) await this.git.add(item.entry.path);
466
- else await this.git.reset(item.entry.path);
467
- await this.refresh();
468
- } catch (err) {
469
- this.error = `${target ? 'Stage' : 'Unstage'} failed: ${summarizeError(err)}`;
470
- this._markDirtyAndRender();
471
- }
472
- }
473
-
474
- private async performCommit(message: string): Promise<void> {
475
- try {
476
- await this.withLoading('Committing…', async () => this.git.commit(message));
477
- await this.refresh();
478
- } catch (err) {
479
- this.error = `Commit failed: ${summarizeError(err)}`;
480
- this._markDirtyAndRender();
481
- }
482
- }
483
-
484
- private async performInit(): Promise<void> {
485
- const result = GitService.initRepo(this.workingDirectory);
486
- if (result.success) {
487
- logger.debug('GitPanel: repo initialised via explicit confirm', { cwd: this.workingDirectory });
488
- this.notGitRepo = false;
489
- this.error = null;
490
- await this.refresh();
491
- } else {
492
- this.error = `Git init failed: ${result.error ?? 'unknown error'}`;
493
- this._markDirtyAndRender();
494
- }
495
- }
496
-
497
- // Rendering
498
-
499
- override render(width: number, height: number): Line[] {
500
- if (this.confirm) {
501
- return buildPanelWorkspace(width, height, {
502
- title: ' Git',
503
- sections: [{ title: 'Confirmation', lines: renderConfirmLines(width, this.confirm) }],
504
- palette: DEFAULT_PANEL_PALETTE,
505
- });
506
- }
507
- if (this.commitMessage !== null) {
508
- return this.renderCommitCompose(width, height);
509
- }
510
- if (this.loading) {
511
- return this.renderMessage(width, height, 'Loading git status...', C.branch);
512
- }
513
- if (this.error) {
514
- return this.renderMessage(width, height, this.error, this.notGitRepo ? C.warn : C.bad);
515
- }
516
- // I3: spinner during openDiff()/performCommit() async fetch
517
- if (this.loadingState === 'loading') {
518
- return this.renderMessage(width, height, 'Loading diff...', C.branch);
519
- }
520
- if (this.expandedDiff !== null) {
521
- return this.renderDiff(width, height);
522
- }
523
- return this.renderList(width, height);
524
- }
525
-
526
- // -- Helpers -----------------------------------------------------------------
527
-
528
- private renderMessage(width: number, height: number, msg: string, fg: string): Line[] {
529
- const lines: Line[] = [buildStyledPanelLine(width, [{ text: msg, fg }])];
530
- while (lines.length < height) lines.push(createEmptyLine(width));
531
- return lines;
532
- }
533
-
534
- private renderCommitCompose(width: number, height: number): Line[] {
535
- const inputLine = buildSearchInputLine(
536
- width,
537
- 'Commit message: ',
538
- `${this.commitMessage ?? ''}_`,
539
- DEFAULT_PANEL_PALETTE,
540
- { active: true, bg: DEFAULT_PANEL_PALETTE.inputBg, valueColor: DEFAULT_PANEL_PALETTE.info },
541
- );
542
- const footerLines = [
543
- buildKeyboardHints(width, [{ keys: 'Enter', label: 'review & confirm' }, { keys: 'Esc', label: 'cancel' }], DEFAULT_PANEL_PALETTE),
544
- ];
545
- return buildPanelWorkspace(width, height, {
546
- title: ' Git: Commit',
547
- sections: [{ title: 'Message', lines: [inputLine] }],
548
- footerLines,
549
- palette: DEFAULT_PANEL_PALETTE,
550
- });
551
- }
552
-
553
- /** Paint a single text string into a new Line at x=startX. */
554
- private paintText(
555
- line: Line,
556
- text: string,
557
- startX: number,
558
- width: number,
559
- fg: string,
560
- opts: { bold?: boolean; dim?: boolean } = {},
561
- ): number {
562
- let x = startX;
563
- for (const ch of text) {
564
- if (x >= width) break;
565
- line[x++] = createStyledCell(ch, { fg, bold: opts.bold, dim: opts.dim });
566
- }
567
- return x;
568
- }
569
-
570
- private renderBranchLine(width: number): Line {
571
- const isDirty = this.data.stagedFiles.length > 0 || this.data.unstagedFiles.length > 0;
572
- const segments: Array<{ text: string; fg: string; bold?: boolean }> = [
573
- { text: ' ⎇ ', fg: C.sectionHeader },
574
- { text: this.data.branch, fg: C.branch, bold: true },
575
- ];
576
- if (this.data.ahead > 0) segments.push({ text: ` ↑${this.data.ahead}`, fg: C.good });
577
- if (this.data.behind > 0) segments.push({ text: ` ↓${this.data.behind}`, fg: C.bad });
578
- if (this.data.ahead === 0 && this.data.behind === 0) {
579
- segments.push({ text: ' ≡ up to date', fg: C.dim });
580
- }
581
- segments.push({
582
- text: isDirty ? ' ● dirty' : ' ✓ clean',
583
- fg: isDirty ? C.warn : C.good,
584
- bold: true,
585
- });
586
- segments.push({ text: ` ${this.data.stagedFiles.length} staged`, fg: this.data.stagedFiles.length > 0 ? C.good : C.dim });
587
- segments.push({ text: ` · ${this.data.unstagedFiles.length} unstaged`, fg: this.data.unstagedFiles.length > 0 ? C.warn : C.dim });
588
- return buildStyledPanelLine(width, segments);
589
- }
590
-
591
- private renderSectionHeader(label: string, width: number): Line {
592
- return buildStyledPanelLine(width, [{ text: ` ◆ ${label}`, fg: C.sectionHeader, bold: true }]);
593
- }
594
-
595
- private renderFileRow(entry: GitFileEntry, selected: boolean, width: number): Line {
596
- const fg = entry.staged ? C.good : C.bad;
597
- // Single-char status marker: + staged (ready to commit), M modified/unstaged.
598
- const statusGlyph = entry.staged ? '+' : 'M';
599
- const sessionMarker = entry.sessionChanged ? '● ' : ' ';
600
- const path = truncateDisplay(entry.path, Math.max(0, width - 8));
601
- return buildSelectablePanelLine(width, [
602
- { text: ` ${statusGlyph} `, fg, bg: selected ? C.selected : undefined, bold: true },
603
- { text: sessionMarker, fg: selected ? C.selectedFg : C.sessionChanged, bg: selected ? C.selected : undefined },
604
- { text: path, fg: selected ? C.selectedFg : fg, bg: selected ? C.selected : undefined, bold: selected },
605
- ], { selected, selectedBg: C.selected, fillFg: selected ? C.selectedFg : '', leadingMarker: '▸' });
606
- }
607
-
608
- private renderCommitRow(entry: CommitEntry, selected: boolean, width: number): Line {
609
- const hashPart = ` ${entry.hash} `;
610
- const msgBudget = Math.max(0, width - getDisplayWidth(hashPart) - 3);
611
- const msgPart = truncateDisplay(entry.message, msgBudget);
612
- return buildSelectablePanelLine(width, [
613
- { text: hashPart, fg: selected ? C.selectedFg : C.commitHash, bg: selected ? C.selected : undefined },
614
- { text: msgPart, fg: selected ? C.selectedFg : C.commit, bg: selected ? C.selected : undefined, bold: selected },
615
- ], { selected, selectedBg: C.selected, fillFg: selected ? C.selectedFg : '', leadingMarker: '▸' });
616
- }
617
-
618
- private renderList(width: number, height: number): Line[] {
619
- const intro = 'Review branch status, staged and unstaged files, and recent commits. Open a file or commit row to inspect its diff.';
620
- const footerHints = [
621
- buildKeyboardHints(width, [
622
- { keys: '↑/↓', label: 'navigate' },
623
- { keys: 'Enter', label: 'open diff →' },
624
- { keys: 's/u', label: 'stage/unstage' },
625
- { keys: 'c', label: 'commit' },
626
- { keys: 'r', label: 'refresh' },
627
- ], DEFAULT_PANEL_PALETTE),
628
- ];
629
- const emptyStateLines = buildEmptyState(width, ' No git rows', 'This repository has no staged or unstaged changes and no commits to display yet.', [{ command: 'r', summary: 'refresh working-tree status in this panel, or stage changes to populate this view' }], DEFAULT_PANEL_PALETTE); // was '/git status' (chat-only, never touches this panel)
630
- const rows: Line[] = [];
631
- for (let i = 0; i < this.items.length; i++) {
632
- const item = this.items[i];
633
- const selected = i === this.selectedIndex;
634
- if (!item) continue;
635
-
636
- switch (item.kind) {
637
- case 'header':
638
- rows.push(this.renderBranchLine(width));
639
- rows.push(createEmptyLine(width)); // spacer
640
- break;
641
- case 'section':
642
- rows.push(this.renderSectionHeader(item.label, width));
643
- break;
644
- case 'file':
645
- rows.push(this.renderFileRow(item.entry, selected, width));
646
- break;
647
- case 'commit':
648
- rows.push(this.renderCommitRow(item.entry, selected, width));
649
- break;
650
- case 'empty': {
651
- rows.push(buildStyledPanelLine(width, [{ text: item.label, fg: C.sectionHeader, dim: true }]));
652
- break;
653
- }
654
- }
655
- }
656
-
657
- const selectedRowIndex = this.getRowIndexForItem(this.selectedIndex);
658
- if (selectedRowIndex >= 0) {
659
- const isDirty = this.data.stagedFiles.length > 0 || this.data.unstagedFiles.length > 0;
660
- const selectedItem = this.items.at(this.selectedIndex);
661
- const selectedLines: Line[] = [];
662
- if (selectedItem?.kind === 'file') {
663
- selectedLines.push(buildPanelLine(width, [
664
- [' File ', DEFAULT_PANEL_PALETTE.label],
665
- [selectedItem.entry.path, DEFAULT_PANEL_PALETTE.value],
666
- [' State ', DEFAULT_PANEL_PALETTE.label],
667
- [selectedItem.entry.staged ? 'staged' : 'unstaged', selectedItem.entry.staged ? DEFAULT_PANEL_PALETTE.good : DEFAULT_PANEL_PALETTE.warn],
668
- ]));
669
- } else if (selectedItem?.kind === 'commit') {
670
- selectedLines.push(buildPanelLine(width, [
671
- [' Commit ', DEFAULT_PANEL_PALETTE.label],
672
- [selectedItem.entry.hash, DEFAULT_PANEL_PALETTE.info],
673
- [' Author ', DEFAULT_PANEL_PALETTE.label],
674
- [selectedItem.entry.author, DEFAULT_PANEL_PALETTE.value],
675
- ]));
676
- selectedLines.push(buildPanelLine(width, [
677
- [' Message ', DEFAULT_PANEL_PALETTE.label],
678
- [selectedItem.entry.message, DEFAULT_PANEL_PALETTE.value],
679
- ]));
680
- }
681
-
682
- const summarySection = {
683
- title: 'Summary',
684
- lines: [
685
- buildPanelLine(width, [
686
- [' Branch ', DEFAULT_PANEL_PALETTE.label],
687
- [this.data.branch, DEFAULT_PANEL_PALETTE.info],
688
- [' Ahead ', DEFAULT_PANEL_PALETTE.label],
689
- [String(this.data.ahead), this.data.ahead > 0 ? DEFAULT_PANEL_PALETTE.good : DEFAULT_PANEL_PALETTE.dim],
690
- [' Behind ', DEFAULT_PANEL_PALETTE.label],
691
- [String(this.data.behind), this.data.behind > 0 ? DEFAULT_PANEL_PALETTE.bad : DEFAULT_PANEL_PALETTE.dim],
692
- [' Status ', DEFAULT_PANEL_PALETTE.label],
693
- [isDirty ? 'dirty' : 'clean', isDirty ? DEFAULT_PANEL_PALETTE.warn : DEFAULT_PANEL_PALETTE.good],
694
- ]),
695
- ],
696
- } as const;
697
- const selectedSection = { title: 'Selected', lines: selectedLines } as const;
698
- const workspaceSection = resolveScrollablePanelSection(width, height, {
699
- intro,
700
- footerLines: footerHints,
701
- palette: DEFAULT_PANEL_PALETTE,
702
- beforeSections: [summarySection],
703
- section: {
704
- title: 'Workspace',
705
- scrollableLines: rows,
706
- selectedIndex: selectedRowIndex,
707
- scrollOffset: this.scrollOffset,
708
- minRows: 8,
709
- },
710
- afterSections: [selectedSection],
711
- });
712
- this.scrollOffset = workspaceSection.scrollOffset;
713
-
714
- return buildPanelWorkspace(width, height, {
715
- title: ' Git',
716
- intro,
717
- sections: [
718
- summarySection,
719
- workspaceSection.section.lines.length > 0 ? workspaceSection.section : { title: 'Workspace', lines: emptyStateLines },
720
- selectedSection,
721
- ],
722
- footerLines: footerHints,
723
- palette: DEFAULT_PANEL_PALETTE,
724
- });
725
- }
726
- return buildPanelWorkspace(width, height, {
727
- title: ' Git',
728
- intro,
729
- sections: [{ lines: emptyStateLines }],
730
- palette: DEFAULT_PANEL_PALETTE,
731
- });
732
- }
733
-
734
- /** Map an item index in `this.items` to its rendered row index (header items expand to 2 rows: branch + spacer). */
735
- private getRowIndexForItem(itemIndex: number): number {
736
- let row = 0;
737
- for (let i = 0; i < itemIndex && i < this.items.length; i++) {
738
- const it = this.items[i];
739
- if (it?.kind === 'header') {
740
- row += 2;
741
- } else {
742
- row += 1;
743
- }
744
- }
745
- return row;
746
- }
747
-
748
- private renderDiff(width: number, height: number): Line[] {
749
- const item = this.items.at(this.selectedIndex);
750
- const diffLines = this.expandedDiff ?? [];
751
- let added = 0;
752
- let removed = 0;
753
- for (const l of diffLines) {
754
- if (l.startsWith('+') && !l.startsWith('+++')) added++;
755
- else if (l.startsWith('-') && !l.startsWith('---')) removed++;
756
- }
757
- const title =
758
- item?.kind === 'file'
759
- ? `Diff: ${item.entry.path} +${added} -${removed}`
760
- : item?.kind === 'commit'
761
- ? `Commit ${item.entry.hash}: ${item.entry.message} +${added} -${removed}`
762
- : 'Diff';
763
- const renderedLines = diffLines.map((rawLine) => {
764
- const dLine = createEmptyLine(width);
765
- let fg: string;
766
- if (rawLine.startsWith('+') && !rawLine.startsWith('+++')) {
767
- fg = C.good;
768
- } else if (rawLine.startsWith('-') && !rawLine.startsWith('---')) {
769
- fg = C.bad;
770
- } else if (rawLine.startsWith('@@') || rawLine.startsWith('diff') || rawLine.startsWith('index')) {
771
- fg = C.diffMeta;
772
- } else {
773
- fg = C.diffNeutral;
774
- }
775
- this.paintText(dLine, rawLine, 0, width, fg);
776
- return dLine;
777
- });
778
- const footerLines = [
779
- buildKeyboardHints(width, [{ keys: '↑/↓', label: 'scroll' }, { keys: 'Esc/q', label: 'back to files' }], DEFAULT_PANEL_PALETTE),
780
- ];
781
- const diffSection = resolveScrollablePanelSection(width, height, {
782
- palette: DEFAULT_PANEL_PALETTE,
783
- footerLines,
784
- section: {
785
- title: 'Patch',
786
- scrollableLines: renderedLines,
787
- scrollOffset: this.scrollOffset,
788
- minRows: 1,
789
- },
790
- });
791
- this.scrollOffset = diffSection.scrollOffset;
792
-
793
- return buildPanelWorkspace(width, height, {
794
- title: ` ${title}`,
795
- sections: [diffSection.section],
796
- footerLines,
797
- palette: DEFAULT_PANEL_PALETTE,
798
- });
799
- }
800
- }