@pellux/goodvibes-tui 2.0.21 → 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 +58 -0
  2. package/README.md +16 -16
  3. package/docs/README.md +4 -4
  4. package/docs/foundation-artifacts/operator-contract.json +1 -1
  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
@@ -3,7 +3,6 @@ export interface ShellLayoutRequest {
3
3
  readonly height: number;
4
4
  readonly headerHeight: number;
5
5
  readonly footerHeight: number;
6
- readonly panelWidth?: number;
7
6
  }
8
7
 
9
8
  export interface Rect {
@@ -19,23 +18,13 @@ export interface ShellLayout {
19
18
  readonly body: Rect;
20
19
  readonly footer: Rect;
21
20
  readonly conversation: Rect;
22
- readonly panel?: Rect;
23
- readonly separatorX?: number;
24
- }
25
-
26
- export interface SplitPaneLayout {
27
- readonly totalRows: number;
28
- readonly topTabRows: number;
29
- readonly bottomTabRows: number;
30
- readonly separatorRows: number;
31
- readonly topContentRows: number;
32
- readonly bottomContentRows: number;
33
21
  }
34
22
 
35
23
  function clamp(value: number, min: number, max: number): number {
36
24
  return Math.max(min, Math.min(max, value));
37
25
  }
38
26
 
27
+ /** The screen's rows: header, the conversation body (full width), footer. */
39
28
  export function createShellLayout(request: ShellLayoutRequest): ShellLayout {
40
29
  const width = Math.max(1, request.width);
41
30
  const height = Math.max(1, request.height);
@@ -43,55 +32,11 @@ export function createShellLayout(request: ShellLayoutRequest): ShellLayout {
43
32
  const footerHeight = clamp(request.footerHeight, 0, Math.max(0, height - headerHeight));
44
33
  const bodyHeight = Math.max(0, height - headerHeight - footerHeight);
45
34
 
46
- const hasPanel = typeof request.panelWidth === 'number' && request.panelWidth > 0;
47
- const safePanelWidth = hasPanel
48
- ? clamp(request.panelWidth as number, 1, Math.max(1, width - 2))
49
- : 0;
50
- const separatorWidth = hasPanel ? 1 : 0;
51
- const conversationWidth = hasPanel ? Math.max(1, width - safePanelWidth - separatorWidth) : width;
52
-
53
35
  return {
54
36
  screen: { x: 0, y: 0, width, height },
55
37
  header: { x: 0, y: 0, width, height: headerHeight },
56
38
  body: { x: 0, y: headerHeight, width, height: bodyHeight },
57
39
  footer: { x: 0, y: headerHeight + bodyHeight, width, height: footerHeight },
58
- conversation: { x: 0, y: headerHeight, width: conversationWidth, height: bodyHeight },
59
- panel: hasPanel
60
- ? {
61
- x: conversationWidth + separatorWidth,
62
- y: headerHeight,
63
- width: safePanelWidth,
64
- height: bodyHeight,
65
- }
66
- : undefined,
67
- separatorX: hasPanel ? conversationWidth : undefined,
68
- };
69
- }
70
-
71
- export function createSplitPaneLayout(
72
- totalRows: number,
73
- ratio: number,
74
- options: {
75
- readonly topTabRows?: number;
76
- readonly bottomTabRows?: number;
77
- readonly separatorRows?: number;
78
- } = {},
79
- ): SplitPaneLayout {
80
- const topTabRows = options.topTabRows ?? 1;
81
- const bottomTabRows = options.bottomTabRows ?? 1;
82
- const separatorRows = options.separatorRows ?? 1;
83
- const chromeRows = topTabRows + bottomTabRows + separatorRows;
84
- const contentRows = Math.max(0, totalRows - chromeRows);
85
- const normalizedRatio = clamp(ratio, 0.2, 0.8);
86
- const topContentRows = contentRows <= 1 ? contentRows : Math.max(1, Math.floor(contentRows * normalizedRatio));
87
- const bottomContentRows = contentRows <= 1 ? 0 : Math.max(1, contentRows - topContentRows);
88
-
89
- return {
90
- totalRows,
91
- topTabRows,
92
- bottomTabRows,
93
- separatorRows,
94
- topContentRows,
95
- bottomContentRows,
40
+ conversation: { x: 0, y: headerHeight, width, height: bodyHeight },
96
41
  };
97
42
  }
@@ -1,4 +1,5 @@
1
- import { GLYPHS, SPINNER_FRAMES, UI_TONES } from './ui-primitives.ts';
1
+ import { GLYPHS, SPINNER_FRAMES } from './ui-primitives.ts';
2
+ import { activeUiTones } from './theme.ts';
2
3
 
3
4
  /**
4
5
  * Layout constants, single source of truth for margins and content width.
@@ -9,17 +10,10 @@ import { GLYPHS, SPINNER_FRAMES, UI_TONES } from './ui-primitives.ts';
9
10
  */
10
11
  export { TRANSCRIPT_LAYOUT as LAYOUT } from '@pellux/goodvibes-terminal-shell';
11
12
 
12
- export const TOOL_STATUS = {
13
- SUCCESS_ICON: GLYPHS.status.success,
14
- SPINNER_FRAMES,
15
- FAIL_ICON: GLYPHS.status.failure,
16
- PENDING_ICON: GLYPHS.status.pending,
17
- TOOL_NAME_PAD: 8,
18
- } as const;
19
-
13
+ // `color` is a getter so it always reads the active theme.
20
14
  export const BORDERS = {
21
- THINKING: { char: '▌', color: UI_TONES.state.reasoning },
22
- ERROR: { char: '▌', color: UI_TONES.state.bad },
23
- WARNING: { char: '▌', color: UI_TONES.state.warn },
24
- INFO: { char: '▌', color: UI_TONES.state.info },
15
+ THINKING: { char: '▌', get color(): string { return activeUiTones().state.reasoning; } },
16
+ ERROR: { char: '▌', get color(): string { return activeUiTones().state.bad; } },
17
+ WARNING: { char: '▌', get color(): string { return activeUiTones().state.warn; } },
18
+ INFO: { char: '▌', get color(): string { return activeUiTones().state.info; } },
25
19
  } as const;
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  import type { Cell } from '@pellux/goodvibes-sdk/platform/types';
12
+ import { activeTheme } from './theme.ts';
12
13
 
13
14
  /** Module-level set of inline markdown special characters (hoisted out of hot loop). */
14
15
  const INLINE_SPECIAL_CHARS = new Set(['[', '`', '*', '_', '~']);
@@ -102,7 +103,7 @@ export function renderInlineMarkdown(text: string): InlineToken[] {
102
103
  if (text.slice(i, i + 2) === '~~') {
103
104
  const end = text.indexOf('~~', i + 2);
104
105
  if (end !== -1) {
105
- tokens.push({ type: 'text', text: text.slice(i + 2, end), style: { strikethrough: true, fg: '244' } });
106
+ tokens.push({ type: 'text', text: text.slice(i + 2, end), style: { strikethrough: true, fg: activeTheme().strikethrough } });
106
107
  i = end + 2;
107
108
  continue;
108
109
  }
@@ -41,7 +41,7 @@
41
41
  import { type Line, type Cell, createStyledCell } from '@pellux/goodvibes-sdk/platform/types';
42
42
  import { UIFactory } from './ui-factory.ts';
43
43
  import { getDisplayWidth } from '../utils/terminal-width.ts';
44
- import { activeTheme } from './theme.ts';
44
+ import { activeTheme, activeTokens } from './theme.ts';
45
45
  import { renderInlineMarkdown } from './markdown-inline.ts';
46
46
 
47
47
  /** Width a column is given when the budget allows, even if its content is narrower. */
@@ -111,7 +111,7 @@ function flattenCellChars(raw: string, isHdr: boolean): StyledChar[] {
111
111
  for (const token of renderInlineMarkdown(raw)) {
112
112
  let style: Partial<Cell>;
113
113
  if (token.type === 'code') {
114
- style = { fg: T.inlineCodeFg, bold: true };
114
+ style = { fg: activeTokens().markdownCode };
115
115
  } else if (token.type === 'link') {
116
116
  style = { fg: T.link, underline: true };
117
117
  } else {
@@ -121,7 +121,7 @@ function flattenCellChars(raw: string, isHdr: boolean): StyledChar[] {
121
121
  style.fg = style.fg || T.heading1;
122
122
  style.bold = true;
123
123
  } else {
124
- style.fg = style.fg || '252';
124
+ style.fg = style.fg || activeTokens().text;
125
125
  }
126
126
  for (const ch of token.text) out.push({ char: ch, style });
127
127
  }
@@ -203,7 +203,7 @@ function padLineToCells(lineChars: readonly StyledChar[], maxW: number, isHdr: b
203
203
  w += cw;
204
204
  }
205
205
  while (w < maxW) {
206
- cells.push(createStyledCell(' ', isHdr ? { fg: T.heading1 } : { fg: '252' }));
206
+ cells.push(createStyledCell(' ', isHdr ? { fg: T.heading1 } : { fg: activeTokens().text }));
207
207
  w++;
208
208
  }
209
209
  return cells;
@@ -315,16 +315,17 @@ function renderStackedTable(
315
315
  hasSeparator: boolean,
316
316
  width: number,
317
317
  indent: number,
318
+ rightGutter: number,
318
319
  ): Line[] {
319
320
  const lines: Line[] = [];
320
- const availW = Math.max(1, width - indent);
321
+ const availW = Math.max(1, width - rightGutter - indent);
321
322
  const headerRow = hasSeparator ? parsedRows[0] : undefined;
322
323
  const bodyRows = hasSeparator ? parsedRows.slice(1) : [...parsedRows];
323
324
  // A header-only table still has to show its headers, so it renders as one
324
325
  // record with empty values rather than as nothing at all.
325
326
  const records: readonly string[][] = bodyRows.length > 0 ? bodyRows : [[]];
326
- const separatorStyle: Partial<Cell> = { fg: '240' };
327
- const rule = UIFactory.stringToLine(' '.repeat(indent) + '─'.repeat(availW), width, { fg: '240' });
327
+ const separatorStyle: Partial<Cell> = { fg: activeTokens().textFaint };
328
+ const rule = UIFactory.stringToLine(' '.repeat(indent) + '─'.repeat(availW), width, { fg: activeTokens().textFaint });
328
329
 
329
330
  for (let r = 0; r < records.length; r++) {
330
331
  if (r > 0) lines.push(rule);
@@ -348,7 +349,11 @@ function renderStackedTable(
348
349
  * Render a markdown table with box-drawing borders. Cells wrap to as many
349
350
  * physical lines as their content needs; nothing is ever truncated.
350
351
  */
351
- export function renderTable(rows: string[], width: number, indent: number): Line[] {
352
+ /**
353
+ * `rightGutter` columns on the right stay clear: the table (boxed or stacked)
354
+ * ends at width - rightGutter - 1 or earlier.
355
+ */
356
+ export function renderTable(rows: string[], width: number, indent: number, rightGutter = 0): Line[] {
352
357
  const lines: Line[] = [];
353
358
 
354
359
  // Parse rows into cells, skip separator
@@ -367,7 +372,7 @@ export function renderTable(rows: string[], width: number, indent: number): Line
367
372
  if (parsedRows.length === 0) return lines;
368
373
 
369
374
  const colCount = Math.max(...parsedRows.map((r) => r.length));
370
- const availW = width - indent;
375
+ const availW = width - rightGutter - indent;
371
376
 
372
377
  // Measure using stripped text (no markdown markers), both the natural width
373
378
  // and the widest single word, the latter is what a column needs before it
@@ -391,11 +396,11 @@ export function renderTable(rows: string[], width: number, indent: number): Line
391
396
  // the end of the line buffer and silently dropped, so the box is abandoned
392
397
  // for the stacked layout instead.
393
398
  const boxWidth = indent + 1 + colWidths.reduce((sum, w) => sum + w + 3, 0);
394
- if (boxWidth > width) {
395
- return renderStackedTable(parsedRows, colCount, hasSeparator, width, indent);
399
+ if (boxWidth > width - rightGutter) {
400
+ return renderStackedTable(parsedRows, colCount, hasSeparator, width, indent, rightGutter);
396
401
  }
397
402
 
398
- const bc = '240'; // border color
403
+ const bc = activeTokens().textFaint; // border color
399
404
 
400
405
  const makeBorder = (left: string, mid: string, right: string, horiz: string): Line => {
401
406
  let s = ' '.repeat(indent) + left;
@@ -2,8 +2,7 @@ import { type Line, type Cell, createStyledCell } from '@pellux/goodvibes-sdk/pl
2
2
  import { UIFactory } from './ui-factory.ts';
3
3
  import { renderCodeBlock } from './code-block.ts';
4
4
  import { getDisplayWidth } from '../utils/terminal-width.ts';
5
- import { LAYOUT } from './layout.ts';
6
- import { activeTheme } from './theme.ts';
5
+ import { activeTheme, activeTokens } from './theme.ts';
7
6
  import { renderInlineMarkdown, type InlineToken } from './markdown-inline.ts';
8
7
  import { isLikelyTableHeaderRow, isLikelyTableSeparatorRow, renderTable } from './markdown-table.ts';
9
8
 
@@ -25,6 +24,11 @@ export interface MarkdownRenderOptions {
25
24
  * renderMarkdown - Parse markdown text into styled Line[].
26
25
  * Thin wrapper over renderMarkdownTracked for callers that don't need code-block metadata.
27
26
  */
27
+ /** Column where markdown prose starts (the Measurements table's text column). */
28
+ export const MARKDOWN_TEXT_COL = 5;
29
+ /** Columns kept clear on the right: wrapped prose ends at width-6 or earlier. */
30
+ const MARKDOWN_RIGHT_GUTTER = 5;
31
+
28
32
  export function renderMarkdown(text: string, width: number, options: MarkdownRenderOptions = {}): Line[] {
29
33
  return renderMarkdownTracked(text, width, options).lines;
30
34
  }
@@ -58,14 +62,39 @@ export function renderMarkdownTracked(
58
62
  let codeBlockLines: string[] = [];
59
63
  let fenceChar = '`';
60
64
  let fenceIndent = 0;
61
- const indent = LAYOUT.LEFT_MARGIN;
62
- const contentWidth = LAYOUT.contentWidth(width);
65
+ // Prose starts at column 5 (the Measurements table's text column, the
66
+ // same column user-message and composer text use) and wraps before
67
+ // width-5, so the right edge never reaches a fill's padding band.
68
+ const indent = MARKDOWN_TEXT_COL;
69
+ const contentWidth = Math.max(1, width - MARKDOWN_RIGHT_GUTTER - indent);
70
+ const tk = activeTokens();
71
+
72
+ // Code blocks, tables, block quotes and rules get exactly one blank row
73
+ // above and below, whatever the source had. `openBlock` trims the blank
74
+ // rows already emitted to one; `blankAfter` owes one blank row to whatever
75
+ // comes next (and swallows extra source blanks until then).
76
+ const isBlankLine = (line: Line): boolean => line.every((c) => c.char === ' ' || c.char === '');
77
+ let blankAfter = false;
78
+ let inQuote = false;
79
+ const openBlock = (): void => {
80
+ blankAfter = false;
81
+ while (lines.length > 0 && isBlankLine(lines[lines.length - 1]!)) lines.pop();
82
+ if (lines.length > 0) lines.push(UIFactory.stringToLine('', width));
83
+ };
84
+ const closeBlock = (): void => { blankAfter = true; };
85
+ const beforeContent = (): void => {
86
+ if (blankAfter) {
87
+ lines.push(UIFactory.stringToLine('', width));
88
+ blankAfter = false;
89
+ }
90
+ };
63
91
 
64
92
  for (let i = 0; i < rawLines.length; i++) {
65
93
  const raw = rawLines[i];
66
94
 
67
95
  const fenceMatch = raw.match(/^(\s*)(```|~~~)\s*([\w-]*)/);
68
96
  if (fenceMatch && !inCodeBlock) {
97
+ if (inQuote) { inQuote = false; closeBlock(); }
69
98
  inCodeBlock = true;
70
99
  fenceIndent = fenceMatch[1].length;
71
100
  fenceChar = fenceMatch[2][0]; // '`' or '~'
@@ -77,6 +106,7 @@ export function renderMarkdownTracked(
77
106
  // Close fence: same char, same or less indentation, at least 3 of that char
78
107
  const closeFenceRe = new RegExp(`^\\s{0,${fenceIndent}}${fenceChar === '`' ? '```' : '~~~'}`);
79
108
  if (closeFenceRe.test(raw)) {
109
+ openBlock();
80
110
  const blockStart = lines.length;
81
111
  const rendered = renderCodeBlock(codeBlockLines, codeBlockLang, width, {
82
112
  showLineNumbers: options.codeBlockLineNumbers ?? true,
@@ -88,6 +118,7 @@ export function renderMarkdownTracked(
88
118
  rawContent: codeBlockLines.join('\n'),
89
119
  });
90
120
  lines.push(...rendered);
121
+ closeBlock();
91
122
  inCodeBlock = false;
92
123
  codeBlockLang = '';
93
124
  codeBlockLines = [];
@@ -100,25 +131,32 @@ export function renderMarkdownTracked(
100
131
  }
101
132
 
102
133
  if (raw.trim() === '') {
134
+ if (inQuote) { inQuote = false; closeBlock(); }
135
+ if (blankAfter) continue; // the owed blank row is emitted before the next content
103
136
  lines.push(UIFactory.stringToLine('', width));
104
137
  continue;
105
138
  }
106
139
 
140
+ const isQuoteLine = /^> ?/.test(raw);
141
+ if (inQuote && !isQuoteLine) { inQuote = false; closeBlock(); }
142
+ const isRule = /^[-*_]{3,}$/.test(raw.trim());
143
+ const isTableStart = raw.includes('|') && i + 1 < rawLines.length && isLikelyTableHeaderRow(raw) && isLikelyTableSeparatorRow(rawLines[i + 1]);
144
+ if (!isQuoteLine && !isRule && !isTableStart) beforeContent();
145
+
107
146
  const h3 = raw.match(/^### (.+)/);
108
147
  const h2 = raw.match(/^## (.+)/);
109
148
  const h1 = raw.match(/^# (.+)/);
110
- if (h1) {
111
- lines.push(UIFactory.stringToLine(' '.repeat(indent) + h1[1].toUpperCase(), width, { fg: T.heading1, bold: true }));
112
- lines.push(UIFactory.stringToLine(' '.repeat(indent) + '━'.repeat(Math.min(getDisplayWidth(h1[1]), contentWidth)), width, { fg: '244' }));
113
- continue;
114
- }
115
- if (h2) {
116
- lines.push(UIFactory.stringToLine(' '.repeat(indent) + h2[1], width, { fg: T.heading2, bold: true }));
117
- lines.push(UIFactory.stringToLine(' '.repeat(indent) + '─'.repeat(Math.min(getDisplayWidth(h2[1]), contentWidth)), width, { fg: '240' }));
118
- continue;
119
- }
120
- if (h3) {
121
- lines.push(UIFactory.stringToLine(' '.repeat(indent) + h3[1], width, { fg: T.heading3, bold: true }));
149
+ // Headings keep their own case and carry no rule rows; the heading
150
+ // color and weight are what mark them.
151
+ const heading = h1?.[1] ?? h2?.[1] ?? h3?.[1];
152
+ if (heading !== undefined) {
153
+ // A heading is a block: one blank row above (unless it opens the
154
+ // message) and one below.
155
+ openBlock();
156
+ lines.push(...compositeInlineLine(' '.repeat(indent), renderInlineMarkdown(heading).map((tok) => (
157
+ tok.type === 'text' ? { ...tok, style: { ...tok.style, fg: tk.markdownHeading, bold: true } } : tok
158
+ )), width, {}, indent));
159
+ closeBlock();
122
160
  continue;
123
161
  }
124
162
 
@@ -131,8 +169,8 @@ export function renderMarkdownTracked(
131
169
  const checkbox = checked ? '\u2611 ' : '\u2610 ';
132
170
  const rendered = renderInlineMarkdown(taskMatch[3]);
133
171
  const prefix = ' '.repeat(bulletX) + checkbox;
134
- const style = checked ? { fg: '244', strikethrough: true } : {};
135
- lines.push(...compositeInlineLine(prefix, rendered, width, { fg: checked ? T.checkboxChecked : '252', ...style }, textStartX));
172
+ const style = checked ? { fg: activeTokens().textMuted, strikethrough: true } : {};
173
+ lines.push(...compositeInlineLine(prefix, rendered, width, { fg: checked ? T.checkboxChecked : activeTokens().text, ...style }, textStartX));
136
174
  continue;
137
175
  }
138
176
 
@@ -143,7 +181,7 @@ export function renderMarkdownTracked(
143
181
  const textStartX = bulletX + 2;
144
182
  const rendered = renderInlineMarkdown(ulMatch[2]);
145
183
  const prefix = ' '.repeat(bulletX) + '• ';
146
- lines.push(...compositeInlineLine(prefix, rendered, width, { fg: '135', bold: false }, textStartX));
184
+ lines.push(...compositeInlineLine(prefix, rendered, width, { fg: tk.textMuted, bold: false }, textStartX));
147
185
  continue;
148
186
  }
149
187
 
@@ -155,24 +193,32 @@ export function renderMarkdownTracked(
155
193
  const textStartX = bulletX + numStr.length;
156
194
  const rendered = renderInlineMarkdown(olMatch[3]);
157
195
  const prefix = ' '.repeat(bulletX) + numStr;
158
- lines.push(...compositeInlineLine(prefix, rendered, width, { fg: '135', bold: false }, textStartX));
196
+ lines.push(...compositeInlineLine(prefix, rendered, width, { fg: tk.textMuted, bold: false }, textStartX));
159
197
  continue;
160
198
  }
161
199
 
162
- if (/^[-*_]{3,}$/.test(raw.trim())) {
163
- lines.push(UIFactory.stringToLine(' '.repeat(indent) + '─'.repeat(contentWidth), width, { fg: '240' }));
200
+ if (isRule) {
201
+ openBlock();
202
+ lines.push(UIFactory.stringToLine(' '.repeat(indent) + '─'.repeat(contentWidth), width, { fg: tk.markdownHorizontalRule }));
203
+ closeBlock();
164
204
  continue;
165
205
  }
166
206
 
167
- const bqMatch = raw.match(/^> (.*)/);
207
+ const bqMatch = raw.match(/^> ?(.*)/);
168
208
  if (bqMatch) {
169
- const rendered = renderInlineMarkdown(bqMatch[1]);
170
- const prefix = ' '.repeat(indent) + '┃ ';
171
- lines.push(...compositeInlineLine(prefix, rendered, width, { fg: '244', italic: true }, indent + 3));
209
+ if (!inQuote) { openBlock(); inQuote = true; }
210
+ // The thin bar runs down every wrapped row of the quote, and the text
211
+ // keeps the quote color and italic on every row.
212
+ const rendered = renderInlineMarkdown(bqMatch[1]).map((tok) => (
213
+ tok.type === 'text' ? { ...tok, style: { fg: tk.markdownBlockQuote, italic: true, ...tok.style } } : tok
214
+ ));
215
+ const prefix = ' '.repeat(indent) + '│ ';
216
+ lines.push(...compositeInlineLine(prefix, rendered, width, { fg: tk.markdownBlockQuote }, indent + 2, prefix));
172
217
  continue;
173
218
  }
174
219
 
175
- if (raw.includes('|') && i + 1 < rawLines.length && isLikelyTableHeaderRow(raw) && isLikelyTableSeparatorRow(rawLines[i + 1])) {
220
+ if (isTableStart) {
221
+ openBlock();
176
222
  const tableRows: string[] = [];
177
223
  let j = i;
178
224
  while (j < rawLines.length && rawLines[j].includes('|')) {
@@ -180,7 +226,8 @@ export function renderMarkdownTracked(
180
226
  j++;
181
227
  }
182
228
  i = j - 1;
183
- lines.push(...renderTable(tableRows, width, indent));
229
+ lines.push(...renderTable(tableRows, width, indent, MARKDOWN_RIGHT_GUTTER));
230
+ closeBlock();
184
231
  continue;
185
232
  }
186
233
 
@@ -189,6 +236,7 @@ export function renderMarkdownTracked(
189
236
  }
190
237
 
191
238
  if (inCodeBlock && codeBlockLines.length > 0) {
239
+ openBlock();
192
240
  const blockStart = lines.length;
193
241
  const rendered = renderCodeBlock(codeBlockLines, codeBlockLang, width, {
194
242
  showLineNumbers: options.codeBlockLineNumbers ?? true,
@@ -214,9 +262,12 @@ function compositeInlineLine(
214
262
  tokens: InlineToken[],
215
263
  width: number,
216
264
  prefixStyle: Partial<Cell>,
217
- textStartX: number
265
+ textStartX: number,
266
+ /** Drawn on every wrapped row after the first (a block quote's bar); blank when omitted. */
267
+ continuationPrefix?: string,
218
268
  ): Line[] {
219
269
  const T = activeTheme();
270
+ const tk = activeTokens();
220
271
  const lines: Line[] = [];
221
272
 
222
273
  // Flatten tokens to [char, style] pairs
@@ -227,7 +278,7 @@ function compositeInlineLine(
227
278
  if (token.type === 'text') {
228
279
  for (const ch of token.text) chars.push({ char: ch, style: token.style });
229
280
  } else if (token.type === 'code') {
230
- for (const ch of token.text) chars.push({ char: ch, style: { fg: T.inlineCodeFg, bold: true } });
281
+ for (const ch of token.text) chars.push({ char: ch, style: { fg: tk.markdownCode } });
231
282
  } else if (token.type === 'link') {
232
283
  // Resolve URL: if url is empty or relative, treat as text; if it's a file path, use file:// protocol
233
284
  let resolvedUrl = token.url;
@@ -238,8 +289,8 @@ function compositeInlineLine(
238
289
  }
239
290
  }
240
291
 
241
- // Render with simple line-breaking at width
242
- const availW = width - textStartX;
292
+ // Wrap before the right gutter so text keeps clear of the screen edge.
293
+ const availW = width - MARKDOWN_RIGHT_GUTTER - textStartX;
243
294
  if (availW <= 0) return lines;
244
295
 
245
296
  let lineChars: StyledChar[] = [];
@@ -248,9 +299,10 @@ function compositeInlineLine(
248
299
  const flushLine = (isFirst: boolean) => {
249
300
  const line = new Array(width).fill(null).map(() => createStyledCell(' ')) as Cell[];
250
301
  // Write prefix on first line
251
- if (isFirst) {
302
+ const rowPrefix = isFirst ? prefix : continuationPrefix;
303
+ if (rowPrefix !== undefined) {
252
304
  let px = 0;
253
- for (const ch of prefix) {
305
+ for (const ch of rowPrefix) {
254
306
  if (px >= width) break;
255
307
  const cw = getDisplayWidth(ch);
256
308
  line[px] = createStyledCell(ch, { fg: prefixStyle.fg, bg: prefixStyle.bg, bold: prefixStyle.bold, dim: prefixStyle.dim, underline: prefixStyle.underline, italic: prefixStyle.italic, strikethrough: prefixStyle.strikethrough });
@@ -313,9 +365,15 @@ function compositeInlineLine(
313
365
  // Space: flush current word, then add the space
314
366
  flushWord();
315
367
  if (lineW + cw > availW && lineW > 0) {
368
+ // The row is full: this space is the break itself and is consumed by
369
+ // it, so the next row starts at the text column, not one to the right.
316
370
  flushLine(isFirstLine);
317
371
  isFirstLine = false;
372
+ continue;
318
373
  }
374
+ // A wrapped row never opens with a break space (a run of spaces that
375
+ // straddles a break). The first row keeps its leading spaces.
376
+ if (!isFirstLine && lineW === 0) continue;
319
377
  lineChars.push(sc);
320
378
  lineW += cw;
321
379
  } else {
@@ -0,0 +1,78 @@
1
+ /**
2
+ * renderMaskedEntryModal, the local-auth password prompt.
3
+ *
4
+ * ✦ Rotate password alice esc
5
+ *
6
+ * The password is typed here and never shown, stored in history, or
7
+ * written to the transcript.
8
+ *
9
+ * Password ●●●●●●●●▏
10
+ *
11
+ * ⏎ save esc cancel
12
+ *
13
+ * The password row shows one dot per character; the characters themselves
14
+ * never reach a cell. Adding a user first asks for the username (plain text,
15
+ * usernames are not secret) when none was given.
16
+ */
17
+
18
+ import { activeTokens } from './theme.ts';
19
+ import { beginModal, finishModal, clipText, type KitHint, type SurfaceLayer } from './surface-kit.ts';
20
+ import { drawTextBlock, modalHeightFor, modalTextWidth, textBlockHeight } from './surface-kit-extra.ts';
21
+
22
+ /** What the renderer reads from the modal (never the password itself). */
23
+ export interface MaskedEntryView {
24
+ readonly kind: 'add-user' | 'rotate-password';
25
+ /** The username (typed or given). */
26
+ readonly username: string;
27
+ /** Which field is being typed. */
28
+ readonly step: 'username' | 'password';
29
+ /** Characters typed into the password field (only the count is drawn). */
30
+ readonly passwordLength: number;
31
+ /** An error from the last attempt, or null. */
32
+ readonly error: string | null;
33
+ }
34
+
35
+ const MODAL_WIDTH = 72;
36
+
37
+ function intro(view: MaskedEntryView): string {
38
+ return view.step === 'username'
39
+ ? 'Type the username for the new local user, then its password on the next step.'
40
+ : 'The password is typed here and never shown, kept in history, or written to the transcript.';
41
+ }
42
+
43
+ export function renderMaskedEntryModal(view: MaskedEntryView, screenWidth: number, screenHeight: number): SurfaceLayer {
44
+ const t = activeTokens();
45
+ const hints: KitHint[] = [['⏎', view.step === 'username' ? 'next' : 'save'], ['esc', 'cancel']];
46
+ const width = modalTextWidth(screenWidth, screenHeight, MODAL_WIDTH);
47
+ const introRows = textBlockHeight([{ text: intro(view) }], width);
48
+ const errorRows = view.error ? textBlockHeight([{ text: view.error }], width) + 1 : 0;
49
+ const fieldRows = view.kind === 'add-user' ? 2 : 1;
50
+ const height = modalHeightFor(screenWidth, screenHeight, { width: MODAL_WIDTH, hints }, introRows + 1 + fieldRows + errorRows);
51
+ const f = beginModal(screenWidth, screenHeight, {
52
+ title: view.kind === 'add-user' ? 'Add local user' : 'Rotate password',
53
+ sub: view.step === 'password' ? view.username : undefined,
54
+ width: MODAL_WIDTH,
55
+ height,
56
+ center: true,
57
+ cap: 'warning',
58
+ hints,
59
+ });
60
+ let y = drawTextBlock(f.canvas, f.l, f.top, width, [{ text: intro(view), style: { fg: t.textMuted } }], f.bottom) + 1;
61
+ const labelW = 11;
62
+ const valueX = f.l + labelW;
63
+ const room = Math.max(1, f.r - valueX);
64
+ if (view.kind === 'add-user' && y <= f.bottom) {
65
+ f.canvas.put(f.l, y, 'Username', { fg: t.textMuted });
66
+ const end = f.canvas.put(valueX, y, clipText(view.username, room), { fg: t.text });
67
+ if (view.step === 'username') f.canvas.put(end, y, '▏', { fg: t.brand });
68
+ y++;
69
+ }
70
+ if (view.step === 'password' && y <= f.bottom) {
71
+ f.canvas.put(f.l, y, 'Password', { fg: t.textMuted });
72
+ const end = f.canvas.put(valueX, y, '●'.repeat(Math.min(view.passwordLength, room)), { fg: t.text });
73
+ f.canvas.put(end, y, '▏', { fg: t.brand });
74
+ y++;
75
+ }
76
+ if (view.error) drawTextBlock(f.canvas, f.l, y + 1, width, [{ text: view.error, style: { fg: t.error } }], f.bottom);
77
+ return finishModal(f);
78
+ }