@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
@@ -1,37 +1,40 @@
1
- import type { Line } from '@pellux/goodvibes-sdk/platform/types';
2
- import { fitDisplay, getDisplayWidth, truncateDisplay, wrapText } from '../../utils/terminal-width.ts';
1
+ /**
2
+ * renderOnboardingWizard, the onboarding wizard on the modal surface kit.
3
+ *
4
+ * The wizard owns the screen (the shell hides its header and footer), so the
5
+ * surface takes the full width minus 1 column per side and the full height:
6
+ * the gradient ▄ cap on the first row, the surface ▀ cap on the last, 4
7
+ * columns of inner padding, the ✦ title row with the mode, the step position
8
+ * and the changed-screen count, and keycap hints on the second-to-last row.
9
+ * No box-drawing frames.
10
+ *
11
+ * Wide screens: the steps as a kit list on the left (the current step is the
12
+ * gradient row; ✓ complete, ◈ changed, • untouched, completion right-aligned)
13
+ * with the step summary under it, and on the right the step title, its
14
+ * description, the field rows and a focus box that wraps the selected
15
+ * field's hint in full. Narrow screens drop the step list and keep the rest.
16
+ */
17
+
18
+ import { activeTokens, activeUiTones } from '../theme.ts';
3
19
  import {
4
- createOverlayBoxLayout,
5
- createOverlayContentLine,
6
- createOverlayFilledBorderLine,
7
- DEFAULT_OVERLAY_PALETTE,
8
- OVERLAY_GLYPHS,
9
- putOverlayText,
10
- } from '../overlay-box.ts';
11
- import { clamp, drawVerticalRule, fillWidth } from '../fullscreen-primitives.ts';
12
- import { UI_TONES } from '../ui-primitives.ts';
13
- import {
14
- getOnboardingWizardBodyRows,
15
- getOnboardingWizardVisibleFieldCount,
16
- type OnboardingWizardController,
17
- type OnboardingWizardFieldDefinition,
18
- type OnboardingWizardStepDefinition,
20
+ beginModal,
21
+ finishModal,
22
+ scrollCountText,
23
+ type KitHint,
24
+ type SurfaceLayer,
25
+ } from '../surface-kit.ts';
26
+ import { drawList, type KitRow } from '../surface-kit-list.ts';
27
+ import { inset } from '../surface-kit-parts.ts';
28
+ import { drawTextBlock, textBlockHeight, type TextLine } from '../surface-kit-extra.ts';
29
+ import type {
30
+ OnboardingWizardController,
31
+ OnboardingWizardFieldDefinition,
19
32
  } from '../../input/onboarding/onboarding-wizard.ts';
20
33
 
21
- type RenderedFieldRow =
22
- | { readonly kind: 'empty' }
23
- | { readonly kind: 'moreAbove'; readonly text: string }
24
- | { readonly kind: 'moreBelow'; readonly text: string }
25
- | {
26
- readonly kind: 'field';
27
- readonly field: OnboardingWizardFieldDefinition;
28
- readonly absoluteIndex: number;
29
- };
30
-
31
34
  function modeLabel(mode: OnboardingWizardController['mode']): string {
32
- if (mode === 'edit') return 'Edit existing';
33
- if (mode === 'reopen') return 'Reopen review';
34
- return 'New setup';
35
+ if (mode === 'edit') return 'edit existing';
36
+ if (mode === 'reopen') return 'reopen review';
37
+ return 'new setup';
35
38
  }
36
39
 
37
40
  function changedScreensLabel(wizard: OnboardingWizardController): string {
@@ -40,590 +43,177 @@ function changedScreensLabel(wizard: OnboardingWizardController): string {
40
43
  return `${wizard.dirtyStepCount} changed screens`;
41
44
  }
42
45
 
43
- function stepGlyph(
44
- wizard: OnboardingWizardController,
45
- step: OnboardingWizardStepDefinition,
46
- stepIndex: number,
47
- ): { readonly glyph: string; readonly fg: string } {
48
- if (stepIndex === wizard.stepIndex) {
49
- return { glyph: OVERLAY_GLYPHS.selected, fg: UI_TONES.state.active };
50
- }
51
-
52
- const total = wizard.getStepFieldCount(stepIndex);
53
- const completed = wizard.getCompletedFieldCount(stepIndex);
54
- if (wizard.isStepDirty(stepIndex)) {
55
- return { glyph: '◈', fg: UI_TONES.state.warn };
56
- }
57
- if (total > 0 && completed === total) {
58
- return { glyph: '✓', fg: UI_TONES.state.good };
59
- }
60
- return { glyph: '•', fg: UI_TONES.fg.muted };
46
+ function stepRows(wizard: OnboardingWizardController): KitRow[] {
47
+ const tones = activeUiTones();
48
+ return wizard.steps.map((step, index): KitRow => {
49
+ const total = wizard.getStepFieldCount(index);
50
+ const completed = wizard.getCompletedFieldCount(index);
51
+ const dirty = wizard.isStepDirty(index);
52
+ const complete = total > 0 && completed === total;
53
+ return {
54
+ label: `${index + 1}. ${step.shortLabel}`,
55
+ mark: dirty ? '◈' : complete ? '✓' : '•',
56
+ markFg: dirty ? tones.state.warn : complete ? tones.state.good : tones.fg.muted,
57
+ right: `${completed}/${total}`,
58
+ rightFg: dirty ? tones.state.warn : undefined,
59
+ selected: index === wizard.stepIndex,
60
+ };
61
+ });
61
62
  }
62
63
 
63
- function fieldBadgeTone(
64
- wizard: OnboardingWizardController,
65
- field: OnboardingWizardFieldDefinition,
66
- ): string {
67
- if (field.kind === 'status') return UI_TONES.state.info;
68
- if (field.kind === 'modelPicker') return UI_TONES.state.info;
64
+ function fieldBadgeTone(wizard: OnboardingWizardController, field: OnboardingWizardFieldDefinition): string {
65
+ const tones = activeUiTones();
66
+ if (field.kind === 'status' || field.kind === 'modelPicker') return tones.state.info;
69
67
  if (field.kind === 'acknowledgement') {
70
68
  const label = wizard.getFieldValueLabel(field);
71
- return label === 'Accepted' ? UI_TONES.state.good : label === 'Pending' ? UI_TONES.state.warn : UI_TONES.fg.muted;
72
- }
73
- if (field.kind === 'checklist') {
74
- return wizard.getFieldValue(field) ? UI_TONES.state.good : UI_TONES.fg.muted;
69
+ return label === 'Accepted' ? tones.state.good : label === 'Pending' ? tones.state.warn : tones.fg.muted;
75
70
  }
76
- if (field.kind === 'radio') return UI_TONES.state.active;
71
+ if (field.kind === 'checklist') return wizard.getFieldValue(field) ? tones.state.good : tones.fg.muted;
72
+ if (field.kind === 'radio') return tones.state.active;
77
73
  if (field.kind === 'text' || field.kind === 'masked') {
78
- const missingRequired = wizard.getFieldValueLabel(field) === 'Missing';
79
- if (missingRequired) return UI_TONES.state.warn;
80
- if (field.kind === 'masked') return UI_TONES.state.warn;
74
+ if (wizard.getFieldValueLabel(field) === 'Missing') return tones.state.warn;
75
+ if (field.kind === 'masked') return tones.state.warn;
81
76
  }
82
- return UI_TONES.fg.secondary;
77
+ return tones.fg.secondary;
83
78
  }
84
79
 
85
- function buildFieldRows(
86
- wizard: OnboardingWizardController,
87
- visibleFields: number,
88
- capacity: number,
89
- ): readonly RenderedFieldRow[] {
90
- wizard.ensureSelectionVisible(visibleFields);
91
- const fields = wizard.currentStep.fields;
92
- const rows: RenderedFieldRow[] = [];
93
- if (fields.length === 0 || capacity <= 0) return rows;
94
-
95
- const allRows: RenderedFieldRow[] = [];
96
- fields.forEach((field, absoluteIndex) => {
97
- const spacerRows = Math.max(0, field.spacerBeforeRows ?? 0);
98
- for (let index = 0; index < spacerRows; index += 1) {
99
- allRows.push({ kind: 'empty' });
100
- }
101
- allRows.push({ kind: 'field', field, absoluteIndex });
102
- });
103
-
104
- const selectedFieldIndex = wizard.getSelectedFieldIndex();
105
- const selectedRowIndex = Math.max(0, allRows.findIndex((row) => row.kind === 'field' && row.absoluteIndex === selectedFieldIndex));
106
- const scrollFieldIndex = wizard.scrollOffsets[wizard.stepIndex] ?? 0;
107
- const scrollRowIndex = allRows.findIndex((row) => row.kind === 'field' && row.absoluteIndex === scrollFieldIndex);
108
- const maxStart = Math.max(0, allRows.length - capacity);
109
- let start = clamp(scrollRowIndex >= 0 ? scrollRowIndex : 0, 0, maxStart);
110
-
111
- if (selectedRowIndex < start) start = selectedRowIndex;
112
- if (selectedRowIndex >= start + capacity) start = selectedRowIndex - capacity + 1;
113
- start = clamp(start, 0, maxStart);
114
-
115
- if (start > 0 && selectedRowIndex === start) start = Math.max(0, start - 1);
116
- if (start + capacity < allRows.length && selectedRowIndex === start + capacity - 1) {
117
- start = Math.min(maxStart, start + 1);
118
- }
119
-
120
- rows.push(...allRows.slice(start, start + capacity));
121
- if (start > 0 && rows.length > 0) {
122
- rows[0] = {
123
- kind: 'moreAbove',
124
- text: `${OVERLAY_GLYPHS.moreAbove} ${start} more above`,
125
- };
126
- }
127
-
128
- const hiddenBelow = Math.max(0, allRows.length - (start + capacity));
129
- if (hiddenBelow > 0 && rows.length > 0) {
130
- rows[rows.length - 1] = {
131
- kind: 'moreBelow',
132
- text: `${OVERLAY_GLYPHS.moreBelow} ${hiddenBelow} more below`,
133
- };
134
- }
135
-
136
- while (rows.length < capacity) rows.push({ kind: 'empty' });
137
- return rows.slice(0, capacity);
80
+ function fieldMark(wizard: OnboardingWizardController, field: OnboardingWizardFieldDefinition): string | undefined {
81
+ if (wizard.isFieldDirty(field.id)) return '◇';
82
+ if (field.kind === 'checklist' || field.kind === 'acknowledgement') return wizard.getFieldValue(field) ? '✓' : '□';
83
+ if (field.kind === 'action') return '▶';
84
+ if (field.kind === 'radio') return '◉';
85
+ return undefined;
138
86
  }
139
87
 
140
- function fieldHint(
141
- wizard: OnboardingWizardController,
142
- field: OnboardingWizardFieldDefinition,
143
- selected: boolean,
144
- ): string {
145
- if (
146
- selected
147
- && wizard.isEditingTextField()
148
- && wizard.editingFieldId === field.id
149
- && (field.kind === 'text' || field.kind === 'masked')
150
- ) {
151
- const rawValue = wizard.editBuffer.length > 0 ? wizard.editBuffer : field.placeholder;
152
- const editingValue = field.kind === 'masked' && wizard.editBuffer.length > 0
153
- ? '•'.repeat(Math.min(12, Math.max(4, wizard.editBuffer.length)))
154
- : rawValue;
155
- return `Editing: ${editingValue}█`;
156
- }
157
-
158
- if (selected && field.kind === 'modelPicker') return `${field.hint} Press Enter to open picker.`;
159
- if (selected && field.kind === 'text') return `${field.hint} Press Enter to edit inline.`;
160
- if (selected && field.kind === 'masked') return `${field.hint} Press Enter to edit inline.`;
161
- return field.hint;
162
- }
163
-
164
- function fieldRowPrefix(
165
- wizard: OnboardingWizardController,
166
- field: OnboardingWizardFieldDefinition,
167
- selected: boolean,
168
- ): string {
169
- if (selected) return `${OVERLAY_GLYPHS.selected} `;
170
- if (wizard.isFieldDirty(field.id)) return '◇ ';
171
- if (field.kind === 'checklist') return (wizard.getFieldValue(field) as boolean) ? '✓ ' : '□ ';
172
- if (field.kind === 'acknowledgement') return (wizard.getFieldValue(field) as boolean) ? '✓ ' : '□ ';
173
- if (field.kind === 'action') return '▶ ';
174
- if (field.kind === 'radio') return '◉ ';
175
- return ' ';
88
+ /** Field rows; spacer rows before a field are kept as blank rows. */
89
+ function fieldRows(wizard: OnboardingWizardController): { rows: KitRow[]; scrollStart: number } {
90
+ const t = activeTokens();
91
+ const rows: KitRow[] = [];
92
+ const selectedIndex = wizard.getSelectedFieldIndex();
93
+ const scrollField = wizard.scrollOffsets[wizard.stepIndex] ?? 0;
94
+ let scrollStart = 0;
95
+ wizard.currentStep.fields.forEach((field, index) => {
96
+ for (let k = 0; k < Math.max(0, field.spacerBeforeRows ?? 0); k++) rows.push({ label: '' });
97
+ if (index === scrollField) scrollStart = rows.length;
98
+ rows.push({
99
+ label: field.label,
100
+ mark: fieldMark(wizard, field),
101
+ markFg: wizard.isFieldDirty(field.id) ? t.warning : t.brand,
102
+ right: `[${wizard.getFieldValueLabel(field)}]`,
103
+ rightFg: fieldBadgeTone(wizard, field),
104
+ selected: index === selectedIndex,
105
+ });
106
+ });
107
+ return { rows, scrollStart };
176
108
  }
177
109
 
178
- function selectedFieldText(wizard: OnboardingWizardController): {
179
- readonly title: string;
180
- readonly hint: string;
181
- } {
110
+ /** The focus box's lines: what the selected field is and what it does, in full. */
111
+ function focusLines(wizard: OnboardingWizardController): TextLine[] {
112
+ const t = activeTokens();
182
113
  if (wizard.isEditingTextField() && wizard.editingFieldId !== null) {
183
- const editingField = wizard.getFieldById(wizard.editingFieldId);
184
- if (editingField) {
185
- return {
186
- title: `Editing: ${editingField.label}`,
187
- hint: fieldHint(wizard, editingField, true),
188
- };
114
+ const field = wizard.getFieldById(wizard.editingFieldId);
115
+ if (field && (field.kind === 'text' || field.kind === 'masked')) {
116
+ const raw = wizard.editBuffer.length > 0 ? wizard.editBuffer : field.placeholder;
117
+ const shown = field.kind === 'masked' && wizard.editBuffer.length > 0
118
+ ? '•'.repeat(Math.min(12, Math.max(4, wizard.editBuffer.length)))
119
+ : raw;
120
+ return [
121
+ { text: `Editing: ${field.label}`, style: { fg: t.text, bold: true } },
122
+ { text: `${shown}▏`, style: { fg: t.text } },
123
+ { text: field.hint, style: { fg: t.textMuted } },
124
+ ];
189
125
  }
190
126
  }
191
-
192
127
  const field = wizard.getSelectedField();
193
- if (!field) {
194
- return {
195
- title: 'Selected: none',
196
- hint: 'No selectable row is active on this screen.',
197
- };
198
- }
199
-
200
- return {
201
- title: `Selected: ${field.label} [${wizard.getFieldValueLabel(field)}]`,
202
- hint: fieldHint(wizard, field, true),
203
- };
204
- }
205
-
206
- function renderFieldRow(
207
- line: Line,
208
- wizard: OnboardingWizardController,
209
- fieldRow: RenderedFieldRow,
210
- startX: number,
211
- width: number,
212
- ): void {
213
- if (fieldRow.kind === 'empty') return;
214
-
215
- if (fieldRow.kind === 'moreAbove' || fieldRow.kind === 'moreBelow') {
216
- putOverlayText(line, startX + 1, width - 2, truncateDisplay(fieldRow.text, width - 2), {
217
- fg: UI_TONES.fg.muted,
218
- bg: UI_TONES.bg.base,
219
- dim: true,
220
- });
221
- return;
222
- }
223
-
224
- const selected = fieldRow.absoluteIndex === wizard.getSelectedFieldIndex();
225
- const field = fieldRow.field;
226
- const fieldBg = selected ? DEFAULT_OVERLAY_PALETTE.selectedBg : UI_TONES.bg.base;
227
- fillWidth(line, startX, width, fieldBg);
228
-
229
- const badge = truncateDisplay(`[${wizard.getFieldValueLabel(field)}]`, Math.max(8, Math.floor(width * 0.34)));
230
- const badgeWidth = getDisplayWidth(badge);
231
- const labelWidth = Math.max(0, width - badgeWidth - 4);
232
- const prefix = fieldRowPrefix(wizard, field, selected);
233
-
234
- putOverlayText(line, startX + 1, labelWidth, truncateDisplay(`${prefix}${field.label}`, labelWidth), {
235
- fg: UI_TONES.fg.primary,
236
- bg: fieldBg,
237
- bold: selected,
238
- });
239
- putOverlayText(line, startX + width - badgeWidth - 1, badgeWidth, badge, {
240
- fg: fieldBadgeTone(wizard, field),
241
- bg: fieldBg,
242
- bold: selected,
243
- });
244
- }
245
-
246
- function footerText(wizard: OnboardingWizardController): string {
247
- if (wizard.isEditingTextField()) {
248
- return '[Enter] Save value [Esc] Cancel edit [Backspace] Delete char [Del/Ctrl+U] Clear value';
249
- }
250
-
251
- return '[Enter] Toggle/open [Esc] Close [Tab/Shift+Tab] Screen [↑↓] Move [Del/Ctrl+U] Clear input';
128
+ if (!field) return [{ text: 'No selectable row is active on this screen.', style: { fg: t.textMuted } }];
129
+ let hint = field.hint;
130
+ if (field.kind === 'modelPicker') hint = `${hint} Press Enter to open the picker.`;
131
+ if (field.kind === 'text' || field.kind === 'masked') hint = `${hint} Press Enter to edit inline.`;
132
+ return [
133
+ { text: `Focus: ${field.label} [${wizard.getFieldValueLabel(field)}]`, style: { fg: t.text, bold: true } },
134
+ { text: hint, style: { fg: t.textMuted } },
135
+ ];
252
136
  }
253
137
 
254
- function controlsText(wizard: OnboardingWizardController): string {
138
+ function hintsFor(wizard: OnboardingWizardController): KitHint[] {
255
139
  if (wizard.isEditingTextField()) {
256
- return 'Controls: Enter saves this value, Esc cancels editing, Backspace deletes one character, Delete or Ctrl+U clears the field.';
257
- }
258
- return 'Controls: Enter or Space changes the selected row; Delete or Ctrl+U clears selected text inputs; Tab/Shift+Tab changes screens; arrows move.';
259
- }
260
-
261
- function renderWideLayout(
262
- wizard: OnboardingWizardController,
263
- width: number,
264
- viewportHeight: number,
265
- layout: ReturnType<typeof createOverlayBoxLayout>,
266
- ): Line[] {
267
- const lines: Line[] = [];
268
- const bodyRows = getOnboardingWizardBodyRows(viewportHeight);
269
- const visibleFields = getOnboardingWizardVisibleFieldCount(viewportHeight);
270
- const currentStep = wizard.currentStep;
271
- const borderFg = DEFAULT_OVERLAY_PALETTE.borderFg;
272
- const headerBg = UI_TONES.bg.title;
273
- const railBg = UI_TONES.bg.section;
274
- const bodyBg = UI_TONES.bg.base;
275
- const summaryBg = UI_TONES.bg.summary;
276
- const innerLeft = layout.margin + 1;
277
- const availableInner = layout.innerWidth - 2;
278
- const leftWidthBase = layout.innerWidth >= 150 ? 32 : layout.innerWidth >= 108 ? 28 : 24;
279
- const rightWidthBase = layout.innerWidth >= 150 ? 34 : layout.innerWidth >= 108 ? 32 : 24;
280
- const minCenterWidth = layout.innerWidth >= 120 ? 48 : 40;
281
- let leftWidth = Math.min(leftWidthBase, Math.max(20, availableInner - minCenterWidth - 12));
282
- let rightWidth = Math.min(rightWidthBase, Math.max(22, availableInner - minCenterWidth - leftWidth));
283
- let centerWidth = layout.innerWidth - leftWidth - rightWidth - 2;
284
-
285
- if (centerWidth < minCenterWidth) {
286
- const deficit = minCenterWidth - centerWidth;
287
- const leftCut = Math.min(Math.max(0, leftWidth - 20), Math.ceil(deficit / 2));
288
- leftWidth -= leftCut;
289
- rightWidth -= Math.min(Math.max(0, rightWidth - 22), deficit - leftCut);
290
- centerWidth = layout.innerWidth - leftWidth - rightWidth - 2;
291
- }
292
-
293
- const leftStart = innerLeft;
294
- const leftSeparatorX = leftStart + leftWidth;
295
- const centerStart = leftSeparatorX + 1;
296
- const rightSeparatorX = centerStart + centerWidth;
297
- const rightStart = rightSeparatorX + 1;
298
- const summaryLines = [
299
- currentStep.summaryTitle,
300
- ...currentStep.summaryLines.slice(0, 2),
301
- `Fields ${wizard.getCompletedFieldCount(wizard.stepIndex)}/${wizard.getStepFieldCount(wizard.stepIndex)} complete`,
302
- changedScreensLabel(wizard),
303
- ];
304
- const centerTextWidth = Math.max(18, centerWidth - 2);
305
- const descriptionLines = wrapText(currentStep.description, centerTextWidth).slice(0, 2);
306
- const selectedText = selectedFieldText(wizard);
307
- // The selected field's own hint is the direct analog of the reported
308
- // defect (descriptive text explaining what the currently-focused option
309
- // does), some hints run past 400 characters, e.g. security-caveat text.
310
- // It is never clipped to a single row: it wraps onto as many lines as it
311
- // needs, and the field list below starts after it, growing/shrinking the
312
- // field-list area rather than losing hint text. Title/description/controls
313
- // stay fixed-row (short chrome strings, not the field-specific
314
- // explanation), matching prior layout exactly when the hint fits on one line.
315
- const hintLines = wrapText(selectedText.hint, centerTextWidth);
316
- const fieldStartRow = 5 + hintLines.length;
317
- const fieldRows = buildFieldRows(wizard, visibleFields, Math.max(0, bodyRows - fieldStartRow));
318
-
319
- const topLine = createOverlayFilledBorderLine(
320
- width,
321
- layout,
322
- OVERLAY_GLYPHS.topLeft,
323
- OVERLAY_GLYPHS.horizontal,
324
- OVERLAY_GLYPHS.topRight,
325
- borderFg,
326
- headerBg,
327
- );
328
- putOverlayText(topLine, layout.margin + 2, layout.width - 4, 'Onboarding Wizard', {
329
- fg: UI_TONES.fg.primary,
330
- bg: headerBg,
331
- bold: true,
332
- });
333
- const meta = `${modeLabel(wizard.mode)} ${wizard.stepIndex + 1}/${wizard.steps.length} ${changedScreensLabel(wizard)}`;
334
- putOverlayText(topLine, Math.max(layout.margin + 2, layout.margin + layout.width - getDisplayWidth(meta) - 3), layout.width - 4, meta, {
335
- fg: UI_TONES.fg.secondary,
336
- bg: headerBg,
337
- });
338
- lines.push(topLine);
339
-
340
- const headerLine = createOverlayContentLine(width, layout, borderFg, headerBg);
341
- fillWidth(headerLine, leftStart, leftWidth, railBg);
342
- fillWidth(headerLine, centerStart, centerWidth, headerBg);
343
- fillWidth(headerLine, rightStart, rightWidth, summaryBg);
344
- drawVerticalRule(headerLine, leftSeparatorX, borderFg, headerBg);
345
- drawVerticalRule(headerLine, rightSeparatorX, borderFg, headerBg);
346
- putOverlayText(headerLine, leftStart + 1, leftWidth - 2, 'Steps', {
347
- fg: UI_TONES.fg.secondary,
348
- bg: railBg,
349
- bold: true,
350
- });
351
- putOverlayText(headerLine, centerStart + 1, centerWidth - 2, truncateDisplay(currentStep.title, centerWidth - 2), {
352
- fg: UI_TONES.state.active,
353
- bg: headerBg,
354
- bold: true,
355
- });
356
- putOverlayText(headerLine, rightStart + 1, rightWidth - 2, 'Summary', {
357
- fg: UI_TONES.fg.secondary,
358
- bg: summaryBg,
359
- bold: true,
360
- });
361
- lines.push(headerLine);
362
-
363
- lines.push(createOverlayFilledBorderLine(
364
- width,
365
- layout,
366
- OVERLAY_GLYPHS.teeLeft,
367
- OVERLAY_GLYPHS.horizontal,
368
- OVERLAY_GLYPHS.teeRight,
369
- borderFg,
370
- bodyBg,
371
- ));
372
-
373
- for (let row = 0; row < bodyRows; row += 1) {
374
- const line = createOverlayContentLine(width, layout, borderFg, bodyBg);
375
- fillWidth(line, leftStart, leftWidth, railBg);
376
- fillWidth(line, centerStart, centerWidth, bodyBg);
377
- fillWidth(line, rightStart, rightWidth, summaryBg);
378
- drawVerticalRule(line, leftSeparatorX, borderFg);
379
- drawVerticalRule(line, rightSeparatorX, borderFg);
380
-
381
- if (row === 0) {
382
- putOverlayText(line, centerStart + 1, centerWidth - 2, truncateDisplay(currentStep.title, centerWidth - 2), {
383
- fg: UI_TONES.fg.primary,
384
- bg: bodyBg,
385
- bold: true,
386
- });
387
- } else if (row === 1) {
388
- putOverlayText(line, centerStart + 1, centerWidth - 2, descriptionLines[0] ?? '', {
389
- fg: UI_TONES.fg.secondary,
390
- bg: bodyBg,
391
- });
392
- } else if (row === 2) {
393
- putOverlayText(line, centerStart + 1, centerWidth - 2, descriptionLines[1] ?? '', {
394
- fg: UI_TONES.fg.secondary,
395
- bg: bodyBg,
396
- });
397
- } else if (row === 3) {
398
- fillWidth(line, centerStart, centerWidth, railBg);
399
- putOverlayText(line, centerStart + 1, centerWidth - 2, truncateDisplay(controlsText(wizard), centerWidth - 2), {
400
- fg: UI_TONES.state.info,
401
- bg: railBg,
402
- });
403
- } else if (row === 4) {
404
- fillWidth(line, centerStart, centerWidth, DEFAULT_OVERLAY_PALETTE.selectedBg);
405
- putOverlayText(line, centerStart + 1, centerWidth - 2, truncateDisplay(`Focus: ${selectedText.title.replace(/^Selected: /, '')}`, centerWidth - 2), {
406
- fg: UI_TONES.fg.primary,
407
- bg: DEFAULT_OVERLAY_PALETTE.selectedBg,
408
- bold: true,
409
- });
410
- } else if (row < fieldStartRow) {
411
- fillWidth(line, centerStart, centerWidth, DEFAULT_OVERLAY_PALETTE.selectedBg);
412
- putOverlayText(line, centerStart + 1, centerWidth - 2, hintLines[row - 5] ?? '', {
413
- fg: UI_TONES.fg.secondary,
414
- bg: DEFAULT_OVERLAY_PALETTE.selectedBg,
415
- });
416
- } else {
417
- renderFieldRow(line, wizard, fieldRows[row - fieldStartRow] ?? { kind: 'empty' }, centerStart, centerWidth);
418
- }
419
-
420
- const step = wizard.steps[row] ?? null;
421
- if (step) {
422
- const stepState = stepGlyph(wizard, step, row);
423
- const completion = `${wizard.getCompletedFieldCount(row)}/${wizard.getStepFieldCount(row)}`;
424
- const completionWidth = getDisplayWidth(completion);
425
- const stepLabelWidth = Math.max(0, leftWidth - completionWidth - 4);
426
- putOverlayText(line, leftStart + 1, stepLabelWidth, truncateDisplay(`${stepState.glyph} ${row + 1}. ${step.shortLabel}`, stepLabelWidth), {
427
- fg: stepState.fg,
428
- bg: railBg,
429
- bold: row === wizard.stepIndex,
430
- });
431
- putOverlayText(line, Math.max(leftStart + 1, leftStart + leftWidth - completionWidth - 2), completionWidth, completion, {
432
- fg: wizard.isStepDirty(row) ? UI_TONES.state.warn : UI_TONES.fg.muted,
433
- bg: railBg,
434
- });
435
- }
436
-
437
- const summaryText = summaryLines[row];
438
- if (summaryText) {
439
- putOverlayText(line, rightStart + 1, rightWidth - 2, truncateDisplay(summaryText, rightWidth - 2), {
440
- fg: row === 0 ? UI_TONES.state.info : UI_TONES.fg.secondary,
441
- bg: summaryBg,
442
- bold: row === 0,
443
- dim: row > 0,
444
- });
445
- }
446
-
447
- lines.push(line);
140
+ return [['⏎', 'save'], ['esc', 'cancel edit'], ['bksp', 'delete char'], ['del', 'clear']];
448
141
  }
449
-
450
- lines.push(createOverlayFilledBorderLine(
451
- width,
452
- layout,
453
- OVERLAY_GLYPHS.teeLeft,
454
- OVERLAY_GLYPHS.horizontal,
455
- OVERLAY_GLYPHS.teeRight,
456
- borderFg,
457
- bodyBg,
458
- ));
459
-
460
- const footer = createOverlayFilledBorderLine(
461
- width,
462
- layout,
463
- OVERLAY_GLYPHS.bottomLeft,
464
- OVERLAY_GLYPHS.horizontal,
465
- OVERLAY_GLYPHS.bottomRight,
466
- borderFg,
467
- headerBg,
468
- );
469
- putOverlayText(footer, layout.margin + 2, layout.width - 4, truncateDisplay(footerText(wizard), layout.width - 4), {
470
- fg: UI_TONES.fg.muted,
471
- bg: headerBg,
472
- dim: true,
473
- });
474
- lines.push(footer);
475
-
476
- return lines.slice(0, viewportHeight);
142
+ return [['↑↓', 'move'], ['⏎', 'change'], ['tab', 'next screen'], ['shift+tab', 'back'], ['del', 'clear']];
477
143
  }
478
144
 
479
- function renderCollapsedLayout(
480
- wizard: OnboardingWizardController,
481
- width: number,
482
- viewportHeight: number,
483
- layout: ReturnType<typeof createOverlayBoxLayout>,
484
- ): Line[] {
485
- const lines: Line[] = [];
486
- const bodyRows = getOnboardingWizardBodyRows(viewportHeight);
487
- const visibleFields = getOnboardingWizardVisibleFieldCount(viewportHeight);
488
- const currentStep = wizard.currentStep;
489
- const borderFg = DEFAULT_OVERLAY_PALETTE.borderFg;
490
- const headerBg = UI_TONES.bg.title;
491
- const bodyBg = UI_TONES.bg.base;
492
- const innerStart = layout.margin + 1;
493
- const innerWidth = layout.innerWidth;
494
- const innerTextWidth = Math.max(14, innerWidth - 2);
495
- const descriptionLines = wrapText(currentStep.description, innerTextWidth).slice(0, 2);
496
- const selectedText = selectedFieldText(wizard);
497
- // Same rule as the wide layout: the selected field's hint wraps onto as
498
- // many lines as it needs rather than being clipped to a single row.
499
- const hintLines = wrapText(selectedText.hint, innerTextWidth);
500
- const fieldStartRow = 5 + hintLines.length;
501
- const fieldRows = buildFieldRows(wizard, visibleFields, Math.max(0, bodyRows - fieldStartRow));
502
-
503
- const topLine = createOverlayFilledBorderLine(
504
- width,
505
- layout,
506
- OVERLAY_GLYPHS.topLeft,
507
- OVERLAY_GLYPHS.horizontal,
508
- OVERLAY_GLYPHS.topRight,
509
- borderFg,
510
- headerBg,
511
- );
512
- putOverlayText(topLine, layout.margin + 2, layout.width - 4, 'Onboarding Wizard', {
513
- fg: UI_TONES.fg.primary,
514
- bg: headerBg,
515
- bold: true,
516
- });
517
- const meta = `${wizard.stepIndex + 1}/${wizard.steps.length} • ${changedScreensLabel(wizard)}`;
518
- putOverlayText(topLine, Math.max(layout.margin + 2, layout.margin + layout.width - getDisplayWidth(meta) - 3), layout.width - 4, meta, {
519
- fg: UI_TONES.fg.secondary,
520
- bg: headerBg,
521
- });
522
- lines.push(topLine);
523
-
524
- const headerLine = createOverlayContentLine(width, layout, borderFg, headerBg);
525
- fillWidth(headerLine, innerStart, innerWidth, headerBg);
526
- putOverlayText(headerLine, innerStart + 1, innerWidth - 2, fitDisplay(`${modeLabel(wizard.mode)} • ${currentStep.shortLabel}`, innerWidth - 2), {
527
- fg: UI_TONES.state.active,
528
- bg: headerBg,
529
- bold: true,
530
- });
531
- lines.push(headerLine);
532
-
533
- lines.push(createOverlayFilledBorderLine(
534
- width,
535
- layout,
536
- OVERLAY_GLYPHS.teeLeft,
537
- OVERLAY_GLYPHS.horizontal,
538
- OVERLAY_GLYPHS.teeRight,
539
- borderFg,
540
- bodyBg,
541
- ));
542
-
543
- for (let row = 0; row < bodyRows; row += 1) {
544
- const line = createOverlayContentLine(width, layout, borderFg, bodyBg);
545
- fillWidth(line, innerStart, innerWidth, bodyBg);
546
-
547
- if (row === 0) {
548
- putOverlayText(line, innerStart + 1, innerWidth - 2, truncateDisplay(currentStep.title, innerWidth - 2), {
549
- fg: UI_TONES.fg.primary,
550
- bg: bodyBg,
551
- bold: true,
552
- });
553
- } else if (row === 1) {
554
- putOverlayText(line, innerStart + 1, innerWidth - 2, descriptionLines[0] ?? '', {
555
- fg: UI_TONES.fg.secondary,
556
- bg: bodyBg,
557
- });
558
- } else if (row === 2) {
559
- putOverlayText(line, innerStart + 1, innerWidth - 2, descriptionLines[1] ?? '', {
560
- fg: UI_TONES.fg.secondary,
561
- bg: bodyBg,
562
- });
563
- } else if (row === 3) {
564
- fillWidth(line, innerStart, innerWidth, UI_TONES.bg.section);
565
- putOverlayText(line, innerStart + 1, innerWidth - 2, truncateDisplay(controlsText(wizard), innerWidth - 2), {
566
- fg: UI_TONES.state.info,
567
- bg: UI_TONES.bg.section,
568
- });
569
- } else if (row === 4) {
570
- fillWidth(line, innerStart, innerWidth, DEFAULT_OVERLAY_PALETTE.selectedBg);
571
- putOverlayText(line, innerStart + 1, innerWidth - 2, truncateDisplay(`Focus: ${selectedText.title.replace(/^Selected: /, '')}`, innerWidth - 2), {
572
- fg: UI_TONES.fg.primary,
573
- bg: DEFAULT_OVERLAY_PALETTE.selectedBg,
574
- bold: true,
575
- });
576
- } else if (row < fieldStartRow) {
577
- fillWidth(line, innerStart, innerWidth, DEFAULT_OVERLAY_PALETTE.selectedBg);
578
- putOverlayText(line, innerStart + 1, innerWidth - 2, hintLines[row - 5] ?? '', {
579
- fg: UI_TONES.fg.secondary,
580
- bg: DEFAULT_OVERLAY_PALETTE.selectedBg,
581
- });
582
- } else {
583
- renderFieldRow(line, wizard, fieldRows[row - fieldStartRow] ?? { kind: 'empty' }, innerStart, innerWidth);
584
- }
585
-
586
- lines.push(line);
587
- }
588
-
589
- lines.push(createOverlayFilledBorderLine(
590
- width,
591
- layout,
592
- OVERLAY_GLYPHS.teeLeft,
593
- OVERLAY_GLYPHS.horizontal,
594
- OVERLAY_GLYPHS.teeRight,
595
- borderFg,
596
- bodyBg,
597
- ));
598
-
599
- const footer = createOverlayFilledBorderLine(
600
- width,
601
- layout,
602
- OVERLAY_GLYPHS.bottomLeft,
603
- OVERLAY_GLYPHS.horizontal,
604
- OVERLAY_GLYPHS.bottomRight,
605
- borderFg,
606
- headerBg,
607
- );
608
- putOverlayText(footer, layout.margin + 2, layout.width - 4, truncateDisplay(footerText(wizard), layout.width - 4), {
609
- fg: UI_TONES.fg.muted,
610
- bg: headerBg,
611
- dim: true,
612
- });
613
- lines.push(footer);
614
-
615
- return lines.slice(0, viewportHeight);
616
- }
145
+ /** Text width at which the step list sits beside the step body. */
146
+ const WIDE_WIDTH = 90;
617
147
 
148
+ /**
149
+ * Render the wizard as a SurfaceLayer covering the screen (x 1, y 0).
150
+ * `width` and `viewportHeight` are the screen size the wizard owns.
151
+ */
618
152
  export function renderOnboardingWizard(
619
153
  wizard: OnboardingWizardController,
620
154
  width: number,
621
155
  viewportHeight: number,
622
- ): Line[] {
623
- const margin = width >= 64 ? 1 : 0;
624
- const layout = createOverlayBoxLayout(width, margin, Math.max(20, width - margin * 2));
625
- const collapsed = layout.innerWidth < 86;
626
- return collapsed
627
- ? renderCollapsedLayout(wizard, width, viewportHeight, layout)
628
- : renderWideLayout(wizard, width, viewportHeight, layout);
156
+ ): SurfaceLayer {
157
+ const t = activeTokens();
158
+ const step = wizard.currentStep;
159
+ const hints = hintsFor(wizard);
160
+ // The wizard owns the screen: full width minus 1 per side, full height.
161
+ // beginModal sizes against a screen tall enough that the requested height
162
+ // is never clamped; the layer is then placed at the top row.
163
+ const height = Math.max(5, viewportHeight - 2);
164
+ const f = beginModal(width, Math.max(viewportHeight * 2, 12), {
165
+ title: 'Onboarding',
166
+ sub: `${modeLabel(wizard.mode)} · step ${wizard.stepIndex + 1} of ${wizard.steps.length} · ${changedScreensLabel(wizard)}`,
167
+ hints,
168
+ width: width - 2,
169
+ height,
170
+ dim: false,
171
+ });
172
+ const inner = f.r - f.l + 1;
173
+ const wide = inner >= WIDE_WIDTH;
174
+
175
+ let x0 = f.l;
176
+ if (wide) {
177
+ const railW = inner >= 140 ? 32 : 28;
178
+ const railX1 = f.l + railW - 1;
179
+ const steps = stepRows(wizard);
180
+ const res = drawList(f.canvas, { rows: steps, top: f.top, bottom: f.bottom, x0: f.l, x1: railX1 });
181
+ const summary: TextLine[] = [
182
+ { text: step.summaryTitle, style: { fg: t.info, bold: true } },
183
+ ...step.summaryLines.slice(0, 2).map((text) => ({ text, style: { fg: t.textMuted } })),
184
+ { text: `Fields ${wizard.getCompletedFieldCount(wizard.stepIndex)}/${wizard.getStepFieldCount(wizard.stepIndex)} complete`, style: { fg: t.textMuted } },
185
+ ];
186
+ const summaryTop = res.endY + 1;
187
+ if (summaryTop + textBlockHeight(summary, railW) - 1 <= f.bottom) drawTextBlock(f.canvas, f.l, summaryTop, railW, summary, f.bottom);
188
+ x0 = railX1 + 6;
189
+ }
190
+ const x1 = f.r;
191
+ const textW = x1 - x0 + 1;
192
+
193
+ // Step title and its description, in full.
194
+ let y = drawTextBlock(f.canvas, x0, f.top, textW, [
195
+ { text: step.title, style: { fg: t.text, bold: true } },
196
+ { text: step.description, style: { fg: t.textMuted } },
197
+ ], f.bottom) + 1;
198
+
199
+ // The focus box at the bottom sizes to its text (never clipped); the
200
+ // field rows get the rows between.
201
+ const focus = focusLines(wizard);
202
+ const insetTextW = textW;
203
+ const focusRows = textBlockHeight(focus, insetTextW);
204
+ const detailH = Math.min(focusRows + 2, Math.max(3, f.bottom - y - 1));
205
+ const detailY = f.bottom - detailH + 1;
206
+ const p = inset(f.canvas, x0 - 2, detailY, textW + 4, detailH);
207
+ drawTextBlock(f.canvas, p.l, p.top, p.r - p.l + 1, focus, p.bottom);
208
+
209
+ const listBottom = detailY - 2;
210
+ if (y > listBottom) y = listBottom;
211
+ const fields = fieldRows(wizard);
212
+ if (fields.rows.length === 0) {
213
+ drawTextBlock(f.canvas, x0, y, textW, [{ text: 'This screen has no fields.', style: { fg: t.textMuted } }], listBottom);
214
+ } else {
215
+ const res = drawList(f.canvas, { rows: fields.rows, top: y, bottom: listBottom, x0, x1, scrollStart: fields.scrollStart });
216
+ f.hintRight = scrollCountText(res.above, res.below);
217
+ }
218
+ return { ...finishModal(f), y: 0 };
629
219
  }