@pellux/goodvibes-tui 2.0.20 → 2.1.0

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