@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,95 +1,199 @@
1
- import type { Line } from '@pellux/goodvibes-sdk/platform/types';
2
- import { ModalFactory, type ModalSection, type ModalTab } from './modal-factory.ts';
3
- import { getOverlayMaxWidth } from './overlay-viewport.ts';
4
- import { activeUiTones } from './theme.ts';
5
- import type { ConfigModal, ConfigModalRenderModel } from '../input/config-modal.ts';
6
-
7
- // Matches ModalFactory.createModal's own box-width computation (margin 4,
8
- // requested max width 76, see renderConfigModalModel below) so the
9
- // wrap-clamp in ConfigModal.getRenderModel() measures against the SAME width
10
- // the list section will actually wrap at, at the current terminal size.
11
- const MODAL_MARGIN = 4;
12
- const MODAL_MAX_WIDTH = 76;
13
1
  /**
14
- * renderConfigModal, the single render path for every ConfigModalSurface.
15
- * Reads the host's frozen-structure-plus-live-values render model and maps it
16
- * onto ModalFactory.createModal (the same renderer settings-modal uses), so the
17
- * chrome, tab strip, list, and footer are one dialect across all migrated
18
- * surfaces. Pure: takes the modal's render model, returns Line[].
2
+ * renderConfigModal, the single render path for every ConfigModalSurface
3
+ * (hooks, plugins, marketplace, skills, security, policy, knowledge, memory,
4
+ * work plan, keybindings, pairing, planning, local auth, services, providers,
5
+ * remote, sandbox, settings sync, devices, subscription, provider health).
6
+ *
7
+ * Drawn with the modal surface kit: soft caps, ✦ title with the esc keycap,
8
+ * tabs (the active one carries the gradient), the always-live search row,
9
+ * the surface's posture lines, kit rows (group headers where the surface has
10
+ * sections, the selected row as the gradient), then status and notes, and the
11
+ * keycap hint row. Reads the host's frozen-structure-plus-live-values render
12
+ * model, so live ticks repaint values without moving rows.
19
13
  */
20
- export function renderConfigModal(modal: ConfigModal, width: number, height: number): Line[] {
21
- // Size the visible row window to the available height so the list scrolls
22
- // rather than overflowing (posture header + separator + chrome ≈ 8 rows).
23
- const visible = Math.max(3, Math.min(16, height - 8));
24
- modal.setViewportRows(visible);
25
- // item 2: the list section wraps a row label at (contentW - 2),
26
- // where contentW = boxW - 4 (see ModalFactory._renderListSection). Compute
27
- // that exact column here so getRenderModel's wrap-clamp measures against
28
- // reality instead of a fixed guess.
29
- const boxW = Math.max(24, getOverlayMaxWidth(width, MODAL_MARGIN, MODAL_MAX_WIDTH));
30
- const labelWrapWidth = Math.max(8, boxW - 4 - 2);
31
- const model = modal.getRenderModel(labelWrapWidth);
32
- return renderConfigModalModel(model, width);
14
+
15
+ import { activeTokens } from './theme.ts';
16
+ import {
17
+ beginModal,
18
+ clipText,
19
+ finishModal,
20
+ modalGeometry,
21
+ searchRow,
22
+ scrollCountText,
23
+ MODAL_PAD_X,
24
+ type KitHint,
25
+ type ModalFrame,
26
+ type SurfaceLayer,
27
+ } from './surface-kit.ts';
28
+ import { drawRow, measureRow, type KitRow } from './surface-kit-list.ts';
29
+ import {
30
+ drawTabRows,
31
+ drawTextBlock,
32
+ kitHintsFromStrings,
33
+ tabRowCount,
34
+ textBlockHeight,
35
+ type TextLine,
36
+ } from './surface-kit-extra.ts';
37
+ import type { ConfigModal, ConfigModalRenderModel, ConfigModalRenderRow } from '../input/config-modal.ts';
38
+
39
+ function kitRowFor(row: ConfigModalRenderRow): KitRow {
40
+ const label = row.label.replace(/\n/g, ' ');
41
+ if (row.header) return { header: label };
42
+ return {
43
+ label,
44
+ selected: row.selected,
45
+ labelFg: row.style?.fg,
46
+ bold: row.style?.bold === true,
47
+ muted: row.stale,
48
+ };
49
+ }
50
+
51
+ interface Layout {
52
+ readonly hints: KitHint[];
53
+ readonly topLines: TextLine[];
54
+ readonly bottomLines: TextLine[];
55
+ }
56
+
57
+ function layoutFor(model: ConfigModalRenderModel): Layout {
58
+ const t = activeTokens();
59
+ const base: KitHint[] = [['↑↓', 'move']];
60
+ if (model.tabs.length > 1) base.push(['←→', 'tab']);
61
+ const { hints, notes } = kitHintsFromStrings(model.hints, base);
62
+ const topLines: TextLine[] = [];
63
+ if (model.degraded) topLines.push({ text: `⚠ ${model.degraded}`, style: { fg: t.warning, bold: true } });
64
+ for (const line of model.header) topLines.push({ text: line, style: { fg: t.textMuted } });
65
+ const bottomLines: TextLine[] = notes.map((text) => ({ text, style: { fg: t.textFaint } }));
66
+ if (model.status) bottomLines.push({ text: model.status, style: { fg: model.confirmPending ? t.warning : t.accent, bold: model.confirmPending } });
67
+ return { hints, topLines, bottomLines };
33
68
  }
34
69
 
35
- /** Pure model→Line[] mapping, the shared inner step of renderConfigModal. */
36
- function renderConfigModalModel(model: ConfigModalRenderModel, width: number): Line[] {
37
- // Read the active-mode chrome tones per call (this render path is not on a
38
- // hot loop; the modal renders once per keystroke).
39
- const TONES = activeUiTones();
40
- const sections: ModalSection[] = [];
70
+ /**
71
+ * Render the open config modal as a SurfaceLayer in screen coordinates.
72
+ * Also tells the host how many list rows fit (setViewportRows) so its
73
+ * row windowing matches what is drawn.
74
+ */
75
+ export function renderConfigModal(modal: ConfigModal, screenWidth: number, screenHeight: number): SurfaceLayer {
76
+ const t = activeTokens();
77
+ const geometry = modalGeometry(screenWidth, screenHeight);
78
+ const textWidth = Math.max(1, geometry.w - 2 * MODAL_PAD_X);
79
+
80
+ // First pass: header, status and hints decide how many rows the list gets.
81
+ const first = modal.getRenderModel(textWidth);
82
+ const firstLayout = layoutFor(first);
83
+ const f0 = beginModal(screenWidth, screenHeight, { title: first.title || 'Settings', hints: firstLayout.hints });
84
+ const firstCompact = compactionFor(first, firstLayout, f0);
85
+ modal.setViewportRows(Math.max(3, listCapacity(first, firstLayout, f0, firstCompact)));
41
86
 
42
- if (model.degraded) {
43
- sections.push({ type: 'text', content: `⚠ ${model.degraded}`, style: { fg: TONES.state.warn, bold: true } });
44
- sections.push({ type: 'separator' });
87
+ const model = modal.getRenderModel(textWidth);
88
+ const layout = layoutFor(model);
89
+ const f = beginModal(screenWidth, screenHeight, { title: model.title || 'Settings', hints: layout.hints });
90
+ const compact = compactionFor(model, layout, f);
91
+ const { canvas, l, r } = f;
92
+ const gap = compact.tight ? 0 : 1;
93
+
94
+ let y = f.top;
95
+ if (model.tabs.length > 1) {
96
+ const active = Math.max(0, model.tabs.findIndex((tab) => tab.active));
97
+ y += drawTabRows(canvas, l, y, model.tabs.map((tab) => tab.label), active, r) + gap;
45
98
  }
99
+ const activeTab = model.tabs.find((tab) => tab.active);
100
+ const placeholder = `Filter ${(activeTab?.label ?? 'rows').toLowerCase()}`;
101
+ const count = model.search.total === 0
102
+ ? ''
103
+ : model.search.query.length > 0
104
+ ? `${model.search.matched} of ${model.search.total}`
105
+ : `${model.search.total} ${model.search.total === 1 ? 'item' : 'items'}`;
106
+ searchRow(f, y, model.search.query, placeholder, count || undefined);
107
+ y += 1 + gap;
46
108
 
47
- for (const line of model.header) {
48
- sections.push({ type: 'text', content: line, style: { fg: TONES.fg.secondary } });
109
+ if (layout.topLines.length > 0 && !compact.dropTop) {
110
+ y = drawTextBlock(canvas, l, y, r - l + 1, layout.topLines, f.bottom) + 1;
49
111
  }
50
- if (model.header.length > 0) sections.push({ type: 'separator' });
51
112
 
113
+ const bottomRows = bottomRowsFor(layout, r - l + 1);
114
+ const listBottom = f.bottom - bottomRows;
115
+ if (bottomRows > 0) drawTextBlock(canvas, l, listBottom + 2, r - l + 1, layout.bottomLines, f.bottom);
116
+
117
+ let cutLines = 0;
52
118
  if (model.rows.length === 0) {
53
- sections.push({ type: 'text', content: model.emptyText ?? 'Nothing to show.', style: { fg: TONES.fg.muted, dim: true } });
119
+ drawTextBlock(canvas, l, y, r - l + 1, [{ text: model.emptyText ?? 'Nothing to show.', style: { fg: t.textMuted } }], listBottom);
54
120
  } else {
55
- sections.push({
56
- type: 'list',
57
- items: model.rows.map((row) => ({
58
- label: row.label,
59
- selected: row.selected,
60
- style: row.stale
61
- ? { fg: TONES.fg.muted, dim: true }
62
- : row.style,
63
- })),
64
- });
121
+ cutLines = drawRows(model, canvas, y, listBottom, l, r);
122
+ }
123
+
124
+ const above = model.scroll.offset;
125
+ const below = Math.max(0, model.scroll.total - model.scroll.offset - model.rows.length);
126
+ // A single row taller than the list is cut at its bottom; say so honestly.
127
+ const cut = cutLines > 0 ? `${cutLines} more ${cutLines === 1 ? 'line' : 'lines'} ↓` : '';
128
+ f.hintRight = [scrollCountText(above, below), cut].filter(Boolean).join(' · ');
129
+ return finishModal(f);
130
+ }
131
+
132
+ /** How the body gives way on short screens: posture lines go first, then the blank rows under tabs and search. */
133
+ interface Compaction {
134
+ readonly dropTop: boolean;
135
+ readonly tight: boolean;
136
+ }
137
+
138
+ const MIN_LIST_ROWS = 3;
139
+
140
+ function compactionFor(model: ConfigModalRenderModel, layout: Layout, f: ModalFrame): Compaction {
141
+ const steps: Compaction[] = [
142
+ { dropTop: false, tight: false },
143
+ { dropTop: true, tight: false },
144
+ { dropTop: true, tight: true },
145
+ ];
146
+ for (const step of steps) {
147
+ if (listCapacity(model, layout, f, step) >= MIN_LIST_ROWS) return step;
65
148
  }
149
+ return steps[steps.length - 1]!;
150
+ }
151
+
152
+ function listCapacity(model: ConfigModalRenderModel, layout: Layout, f: ModalFrame, compact: Compaction): number {
153
+ const width = f.r - f.l + 1;
154
+ const gap = compact.tight ? 0 : 1;
155
+ let y = f.top;
156
+ if (model.tabs.length > 1) y += tabRowCount(model.tabs.map((tab) => tab.label), f.l, f.r) + gap;
157
+ y += 1 + gap;
158
+ if (layout.topLines.length > 0 && !compact.dropTop) y += textBlockHeight(layout.topLines, width) + 1;
159
+ return f.bottom - y + 1 - bottomRowsFor(layout, width);
160
+ }
66
161
 
67
- const tabs: ModalTab[] | undefined = model.tabs.length > 1
68
- ? model.tabs.map((t) => ({ label: t.label, active: t.active }))
69
- : undefined;
70
-
71
- // Keep the content region a stable height so live-value ticks never change
72
- // the box size: frozen header + separator + the fixed visible-row window.
73
- const targetContentRows =
74
- (model.degraded ? 2 : 0) +
75
- model.header.length +
76
- (model.header.length > 0 ? 1 : 0) +
77
- model.scroll.visible;
78
-
79
- const helpers = model.status
80
- ? [{ content: model.status, accent: true }]
81
- : undefined;
82
-
83
- return ModalFactory.createModal(
84
- {
85
- title: model.title,
86
- width: MODAL_MAX_WIDTH,
87
- tabs,
88
- sections,
89
- targetContentRows,
90
- helpers,
91
- hints: [...model.hints],
92
- },
93
- width,
94
- );
162
+ /** Rows kept under the list for notes and status (plus the blank row above them). */
163
+ function bottomRowsFor(layout: Layout, width: number): number {
164
+ return layout.bottomLines.length > 0 ? textBlockHeight(layout.bottomLines, width) + 1 : 0;
95
165
  }
166
+
167
+ function drawRows(
168
+ model: ConfigModalRenderModel,
169
+ canvas: ReturnType<typeof beginModal>['canvas'],
170
+ top: number,
171
+ bottom: number,
172
+ l: number,
173
+ r: number,
174
+ ): number {
175
+ const rows = model.rows.map(kitRowFor);
176
+ let cut = 0;
177
+ const capacity = Math.max(1, bottom - top + 1);
178
+ const needed = rows.reduce((n, row, k) => n + measureRow(row, l, r) + (k > 0 && row.header !== undefined ? 1 : 0), 0);
179
+ const gaps = needed <= capacity;
180
+ let y = top;
181
+ model.rows.forEach((source, k) => {
182
+ if (y > bottom) return;
183
+ const row = rows[k]!;
184
+ if (gaps && k > 0 && row.header !== undefined) y++;
185
+ if (y > bottom) return;
186
+ if (source.style?.bg) {
187
+ // Preformatted rows with their own background (the pairing QR code)
188
+ // are drawn cell for cell: wrapping would break the pattern.
189
+ canvas.put(l, y, clipText(source.label, r - l + 1), { fg: source.style.fg, bg: source.style.bg });
190
+ y++;
191
+ return;
192
+ }
193
+ const used = drawRow(canvas, y, row, l, r, bottom);
194
+ cut += measureRow(row, l, r) - used;
195
+ y += used;
196
+ });
197
+ return cut;
198
+ }
199
+
@@ -0,0 +1,50 @@
1
+ /**
2
+ * renderConfirmDialog, a small centered dialog on the kit surface:
3
+ *
4
+ * ✦ Delete session? esc
5
+ *
6
+ * This removes "test" and its 3 messages from this project.
7
+ * It cannot be undone.
8
+ *
9
+ * Cancel Delete
10
+ *
11
+ * Content-sized and centered, over the dimmed screen. The confirming button
12
+ * carries the dialog's tone (red for destructive actions); the safe choice is
13
+ * on the left. The chosen button is filled, the other sits on the element fill.
14
+ */
15
+
16
+ import type { ConfirmView } from '../input/confirm-dialog-types.ts';
17
+ import { activeTokens } from './theme.ts';
18
+ import { beginModal, finishModal, modalInnerWidth, standardModalWidth, wrapLines, type SurfaceLayer } from './surface-kit.ts';
19
+ import { buttonWidth } from './surface-kit-parts.ts';
20
+
21
+ /** Preferred width of a confirm dialog. */
22
+ const CONFIRM_WIDTH = 64;
23
+
24
+ export function renderConfirmDialog(dialog: ConfirmView, screenWidth: number, screenHeight: number): SurfaceLayer {
25
+ const t = activeTokens();
26
+ const { title, body, confirmLabel, tone } = dialog.options;
27
+ const cancelLabel = dialog.options.cancelLabel ?? 'Cancel';
28
+ const width = Math.min(CONFIRM_WIDTH, standardModalWidth(screenWidth));
29
+ const lines = body.split('\n').flatMap((para) => (para.trim() === '' ? [''] : wrapLines(para, modalInnerWidth(width))));
30
+ // Fill rows: padding, title, blank, body, blank, buttons, empty last row.
31
+ const f = beginModal(screenWidth, screenHeight, { title, width, height: lines.length + 6, center: true });
32
+ let y = f.top;
33
+ for (const line of lines) {
34
+ if (y > f.bottom - 2) break;
35
+ f.canvas.put(f.l, y++, line, { fg: t.textMuted });
36
+ }
37
+ const toneBg = tone === 'danger' ? t.error : tone === 'warning' ? t.warning : t.brand;
38
+ const confirmW = buttonWidth(confirmLabel);
39
+ const cancelW = buttonWidth(cancelLabel);
40
+ const by = f.bottom;
41
+ const confirmX = f.r - confirmW + 1;
42
+ const cancelX = confirmX - 2 - cancelW;
43
+ f.canvas.put(cancelX, by, ` ${cancelLabel} `, dialog.chosen === 0
44
+ ? { fg: t.selectedListItemText, bg: t.textMuted, bold: true }
45
+ : { fg: t.textMuted, bg: t.border });
46
+ f.canvas.put(confirmX, by, ` ${confirmLabel} `, dialog.chosen === 1
47
+ ? { fg: t.selectedListItemText, bg: toneBg, bold: true }
48
+ : { fg: toneBg, bg: t.border, bold: true });
49
+ return finishModal(f);
50
+ }
@@ -1,24 +1,42 @@
1
- import { type Line } from '@pellux/goodvibes-sdk/platform/types';
2
- import { ModalFactory } from './modal-factory.ts';
3
1
  import type { ConversationManager } from '../core/conversation';
4
- import { getOverlayContentBudget, getOverlaySurfaceMetrics, getStableOverlayContentRows } from './overlay-viewport.ts';
5
2
  import { estimateTokens } from '@pellux/goodvibes-sdk/platform/core';
6
- import { UI_TONES } from './ui-primitives.ts';
3
+ import { activeTokens } from './theme.ts';
4
+ import {
5
+ beginModal,
6
+ finishModal,
7
+ scrollCountText,
8
+ type KitHint,
9
+ type SurfaceLayer,
10
+ } from './surface-kit.ts';
11
+ import { drawList, measureRow, type KitRow } from './surface-kit-list.ts';
12
+ import { drawTextBlock, modalHeightFor, modalTextWidth, textBlockHeight, type TextLine } from './surface-kit-extra.ts';
7
13
 
8
14
  // ─── ContextInspectorModal ────────────────────────────────────────────────────
9
15
 
10
16
  /**
11
- * ContextInspectorModal, state for the context inspector overlay.
17
+ * ContextInspectorModal, state for the context inspector overlay. The list
18
+ * opens on the newest messages; ↑ scrolls back toward older ones.
12
19
  */
13
20
  export class ContextInspectorModal {
14
21
  public active = false;
22
+ /** Rows scrolled back from the newest message (0 = the newest are in view). */
23
+ public scrollBack = 0;
24
+ /** The furthest the list can scroll back, recorded by the renderer. */
25
+ public maxScrollBack = 0;
15
26
 
16
27
  open(): void {
17
28
  this.active = true;
29
+ this.scrollBack = 0;
18
30
  }
19
31
 
20
32
  close(): void {
21
33
  this.active = false;
34
+ this.scrollBack = 0;
35
+ }
36
+
37
+ /** Positive scrolls back toward older messages, negative toward the newest. */
38
+ scrollBy(delta: number): void {
39
+ this.scrollBack = Math.max(0, Math.min(this.maxScrollBack, this.scrollBack + delta));
22
40
  }
23
41
  }
24
42
 
@@ -34,179 +52,123 @@ function fmtPct(ratio: number): string {
34
52
  return `${(ratio * 100).toFixed(1)}%`;
35
53
  }
36
54
 
37
- /**
38
- * Render the context inspector as Line[] for overlay in the viewport.
39
- *
40
- * Lists each message with role, estimated token count, and percentage of total
41
- * context. Highlights large consumers (>10%). Shows total vs context window
42
- * capacity and suggests compaction targets.
43
- *
44
- * @param conversation The conversation manager to inspect.
45
- * @param width Terminal width.
46
- * @param _height Terminal height (reserved for future scrolling).
47
- * @param contextWindow Optional context window size for capacity display.
48
- */
49
- export function renderContextInspector(
50
- conversation: ConversationManager,
51
- width: number,
52
- viewportHeight = 24,
53
- contextWindow = 0,
54
- ): Line[] {
55
- const messages = conversation.getMessagesForLLM();
56
- const metrics = getOverlaySurfaceMetrics(width, viewportHeight, {
57
- margin: 1,
58
- maxWidth: 78,
59
- chromeRows: 7,
60
- minContentRows: 6,
61
- maxContentRows: 10,
62
- });
63
- const targetContentRows = getStableOverlayContentRows(metrics.contentRows, 8);
64
-
65
- if (messages.length === 0) {
66
- return ModalFactory.createModal({
67
- title: 'Context Inspector',
68
- width: metrics.boxWidth,
69
- margin: metrics.margin,
70
- targetContentRows,
71
- sections: [
72
- { type: 'text', content: 'No messages in conversation yet.' },
73
- ],
74
- hints: ['[Esc] Close'],
75
- }, width);
76
- }
77
-
78
- // ── Token accounting ──────────────────────────────────────────────────────
79
-
80
- type MsgEntry = {
81
- role: string;
82
- tokens: number;
83
- label: string;
84
- };
55
+ interface MsgEntry {
56
+ readonly tokens: number;
57
+ readonly label: string;
58
+ }
85
59
 
60
+ function messageEntries(conversation: ConversationManager): { entries: MsgEntry[]; total: number } {
86
61
  const entries: MsgEntry[] = [];
87
- let totalTokens = 0;
88
-
89
- for (const msg of messages) {
62
+ let total = 0;
63
+ for (const msg of conversation.getMessagesForLLM()) {
90
64
  const role = msg.role;
91
65
  let text = '';
92
66
  if (typeof msg.content === 'string') {
93
67
  text = msg.content;
94
68
  } else if (Array.isArray(msg.content)) {
95
- // ContentPart[]
96
69
  text = (msg.content as Array<{ type: string; text?: string }>)
97
70
  .filter((p) => p.type === 'text')
98
71
  .map((p) => p.text ?? '')
99
72
  .join('');
100
73
  }
101
74
  // Include tool call text for assistant messages
102
- if (role === 'assistant' && (msg as { toolCalls?: Array<{ name: string; arguments: unknown }> }).toolCalls) {
103
- const tcs = (msg as { toolCalls?: Array<{ name: string; arguments: unknown }> }).toolCalls!;
104
- for (const tc of tcs) {
105
- text += tc.name + JSON.stringify(tc.arguments);
106
- }
75
+ const toolCalls = (msg as { toolCalls?: Array<{ name: string; arguments: unknown }> }).toolCalls;
76
+ if (role === 'assistant' && toolCalls) {
77
+ for (const tc of toolCalls) text += tc.name + JSON.stringify(tc.arguments);
107
78
  }
108
79
  const tokens = estimateTokens(text);
109
- totalTokens += tokens;
110
-
80
+ total += tokens;
81
+ // A one-line excerpt identifies the message; the inspector is about its
82
+ // token cost, not its content.
83
+ const excerpt = (limit: number): string => `${text.slice(0, limit).replace(/\s+/g, ' ')}${text.length > limit ? '…' : ''}`;
111
84
  let label: string;
112
- if (role === 'user') {
113
- label = `user: ${text.slice(0, 40).replace(/\n/g, ' ')}${text.length > 40 ? '...' : ''}`;
114
- } else if (role === 'assistant') {
115
- label = `assistant: ${text.slice(0, 36).replace(/\n/g, ' ')}${text.length > 36 ? '...' : ''}`;
116
- } else if (role === 'tool') {
117
- const toolMsg = msg as { callId?: string };
118
- label = `tool-result (${(toolMsg.callId ?? '').slice(0, 12)})`;
119
- } else {
120
- label = role;
121
- }
122
-
123
- entries.push({ role, tokens, label });
85
+ if (role === 'user') label = `user: ${excerpt(60)}`;
86
+ else if (role === 'assistant') label = `assistant: ${excerpt(56)}`;
87
+ else if (role === 'tool') label = `tool result ${((msg as { callId?: string }).callId ?? '').slice(0, 12)}`;
88
+ else label = role;
89
+ entries.push({ tokens, label });
124
90
  }
91
+ return { entries, total };
92
+ }
125
93
 
126
- // ── Identify large consumers (>10%) ───────────────────────────────────────
127
-
128
- const largeThreshold = totalTokens * 0.10;
129
-
130
- // ── Build sections ────────────────────────────────────────────────────────
131
-
132
- const sections: import('./modal-factory.ts').ModalSection[] = [];
133
-
134
- // Summary header
135
- const capacityStr = contextWindow > 0
136
- ? ` | Capacity: ${fmtN(totalTokens)} / ${fmtN(contextWindow)} (${fmtPct(totalTokens / contextWindow)})`
137
- : '';
138
- sections.push({
139
- type: 'text',
140
- content: `Total: ~${fmtN(totalTokens)} tokens (${messages.length} messages)${capacityStr}`,
141
- style: { bold: true },
142
- });
94
+ /**
95
+ * Render the context inspector as a SurfaceLayer in screen coordinates.
96
+ *
97
+ * Lists each message with its estimated token count and share of the total
98
+ * (right-aligned), marks large consumers (>10%) with an amber ●, shows the
99
+ * total against the context window, and suggests compaction targets.
100
+ */
101
+ export function renderContextInspector(
102
+ conversation: ConversationManager,
103
+ screenWidth: number,
104
+ screenHeight = 24,
105
+ contextWindow = 0,
106
+ modal?: ContextInspectorModal,
107
+ ): SurfaceLayer {
108
+ const t = activeTokens();
109
+ const { entries, total } = messageEntries(conversation);
110
+
111
+ if (entries.length === 0) {
112
+ const lines: TextLine[] = [{ text: 'No messages in conversation yet.', style: { fg: t.textMuted } }];
113
+ const width = modalTextWidth(screenWidth, screenHeight);
114
+ const height = modalHeightFor(screenWidth, screenHeight, {}, textBlockHeight(lines, width));
115
+ const f = beginModal(screenWidth, screenHeight, { title: 'Context inspector', height, center: true });
116
+ drawTextBlock(f.canvas, f.l, f.top, f.r - f.l + 1, lines, f.bottom);
117
+ return finishModal(f);
118
+ }
143
119
 
144
- if (contextWindow > 0 && totalTokens / contextWindow >= 0.80) {
145
- sections.push({
146
- type: 'text',
147
- content: 'WARNING: context is 80%+ full. Run /compact to free space.',
148
- style: { fg: UI_TONES.state.warn, bold: true },
149
- });
120
+ const largeThreshold = total * 0.10;
121
+ const large = entries.filter((e) => e.tokens > largeThreshold);
122
+ const capacity = contextWindow > 0 ? ` · ${fmtN(total)} of ${fmtN(contextWindow)} (${fmtPct(total / contextWindow)})` : '';
123
+ const topLines: TextLine[] = [
124
+ { text: `Total ~${fmtN(total)} tokens in ${entries.length} message${entries.length === 1 ? '' : 's'}${capacity}`, style: { fg: t.text, bold: true } },
125
+ ];
126
+ if (contextWindow > 0 && total / contextWindow >= 0.80) {
127
+ topLines.push({ text: 'Context is 80% full or more. Run /compact to free space.', style: { fg: t.warning, bold: true } });
128
+ }
129
+ const bottomLines: TextLine[] = [];
130
+ if (large.length > 0) {
131
+ const share = fmtPct(large.reduce((s, e) => s + e.tokens, 0) / total);
132
+ bottomLines.push({ text: `● ${large.length} message${large.length > 1 ? 's use' : ' uses'} ${share} of context (each over 10%).`, style: { fg: t.accent } });
133
+ bottomLines.push({ text: 'Run /compact to summarise and reduce context size.', style: { fg: t.textFaint } });
150
134
  }
151
135
 
152
- sections.push({ type: 'separator' });
136
+ const hints: KitHint[] = [['↑↓', 'scroll']];
137
+ const f = beginModal(screenWidth, screenHeight, { title: 'Context inspector', hints });
138
+ const width = f.r - f.l + 1;
139
+ let top = drawTextBlock(f.canvas, f.l, f.top, width, topLines, f.bottom) + 1;
140
+ const bottomRows = bottomLines.length > 0 ? textBlockHeight(bottomLines, width) + 1 : 0;
141
+ const listBottom = f.bottom - bottomRows;
142
+ if (bottomRows > 0) drawTextBlock(f.canvas, f.l, listBottom + 2, width, bottomLines, f.bottom);
153
143
 
154
- // Per-message list (up to 20 rows to keep modal manageable)
155
- const maxVisibleRows = getOverlayContentBudget(viewportHeight, {
156
- chromeRows: 7,
157
- minContentRows: 6,
158
- maxContentRows: 10,
144
+ const rows: KitRow[] = entries.map((e) => {
145
+ const isLarge = e.tokens > largeThreshold;
146
+ return {
147
+ label: e.label,
148
+ right: `${fmtPct(total > 0 ? e.tokens / total : 0)} · ~${fmtN(e.tokens)}`,
149
+ mark: isLarge ? '●' : undefined,
150
+ markFg: t.warning,
151
+ labelFg: isLarge ? t.warning : undefined,
152
+ bold: isLarge,
153
+ };
159
154
  });
160
- const display = entries.slice(-maxVisibleRows);
161
- const startOffset = entries.length - display.length;
162
- if (startOffset > 0) {
163
- sections.push({
164
- type: 'text',
165
- content: `(${startOffset} older messages not shown)`,
166
- style: { dim: true },
167
- });
168
- }
169
155
 
170
- for (let i = 0; i < display.length; i++) {
171
- const e = display[i];
172
- const pct = totalTokens > 0 ? e.tokens / totalTokens : 0;
173
- const pctStr = fmtPct(pct).padStart(6);
174
- const tokStr = `~${fmtN(e.tokens)}`.padStart(8);
175
- const isLarge = e.tokens > largeThreshold;
176
- const marker = isLarge ? '* ' : ' ';
177
- const line = `${marker}${pctStr} ${tokStr} ${e.label}`;
178
- sections.push({
179
- type: 'text',
180
- content: line,
181
- style: isLarge ? { fg: UI_TONES.state.warn, bold: true } : {},
182
- });
156
+ // Open on the newest messages: find the first row of the tail that fits,
157
+ // then step back by the modal's scroll position.
158
+ const listCapacity = Math.max(1, listBottom - top + 1);
159
+ let tailStart = rows.length;
160
+ let used = 0;
161
+ while (tailStart > 0 && used + measureRow(rows[tailStart - 1]!, f.l, f.r) <= listCapacity) {
162
+ tailStart--;
163
+ used += measureRow(rows[tailStart]!, f.l, f.r);
183
164
  }
184
-
185
- // Compaction suggestions
186
- const largeMsgs = entries.filter((e) => e.tokens > largeThreshold);
187
- if (largeMsgs.length > 0) {
188
- sections.push({ type: 'separator' });
189
- const largePct = fmtPct(
190
- largeMsgs.reduce((s, e) => s + e.tokens, 0) / totalTokens,
191
- );
192
- sections.push({
193
- type: 'text',
194
- content: `Compaction hint: ${largeMsgs.length} message${largeMsgs.length > 1 ? 's' : ''} use ${largePct} of context.`,
195
- style: { fg: '#00ffcc' },
196
- });
197
- sections.push({
198
- type: 'text',
199
- content: 'Run /compact to summarise and reduce context size.',
200
- style: { dim: true },
201
- });
165
+ if (modal) {
166
+ modal.maxScrollBack = tailStart;
167
+ modal.scrollBack = Math.min(modal.scrollBack, tailStart);
202
168
  }
203
-
204
- return ModalFactory.createModal({
205
- title: 'Context Inspector',
206
- width: metrics.boxWidth,
207
- margin: metrics.margin,
208
- targetContentRows,
209
- sections,
210
- hints: ['[*] >10% of context', '[Esc] Close'],
211
- }, width);
169
+ const scrollStart = Math.max(0, tailStart - (modal?.scrollBack ?? 0));
170
+ if (top > listBottom) top = listBottom;
171
+ const res = drawList(f.canvas, { rows, top, bottom: listBottom, x0: f.l, x1: f.r, scrollStart });
172
+ f.hintRight = scrollCountText(res.above, res.below);
173
+ return finishModal(f);
212
174
  }