@pellux/goodvibes-tui 2.0.20 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (388) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/README.md +16 -16
  3. package/docs/README.md +4 -4
  4. package/docs/foundation-artifacts/operator-contract.json +59 -4
  5. package/package.json +16 -6
  6. package/src/cli/management.ts +5 -2
  7. package/src/cli/tui-startup.ts +34 -39
  8. package/src/core/agent-view-render.ts +157 -0
  9. package/src/core/approval-alert.ts +49 -11
  10. package/src/core/bookmark-navigation.ts +7 -1
  11. package/src/core/budget-breach-notifier.ts +27 -8
  12. package/src/core/compaction-receipt.ts +1 -1
  13. package/src/core/context-usage.ts +1 -1
  14. package/src/core/conversation-compaction-render.ts +3 -3
  15. package/src/core/conversation-display-only.ts +86 -0
  16. package/src/core/conversation-event-navigation.ts +27 -0
  17. package/src/core/conversation-line-cache.ts +216 -447
  18. package/src/core/conversation-render-context.ts +20 -27
  19. package/src/core/conversation-rendering.ts +157 -575
  20. package/src/core/conversation-splash-state.ts +6 -1
  21. package/src/core/conversation-types.ts +35 -0
  22. package/src/core/conversation-usage.ts +12 -0
  23. package/src/core/conversation.ts +130 -131
  24. package/src/core/long-task-notifier.ts +100 -31
  25. package/src/core/notices.ts +75 -0
  26. package/src/core/pairing-tailscale-gateway.ts +1 -1
  27. package/src/core/process-output-log.ts +143 -0
  28. package/src/core/rewind-receipt.ts +1 -1
  29. package/src/core/session-focus.ts +190 -0
  30. package/src/core/session-resume-core.ts +23 -81
  31. package/src/core/stream-event-wiring.ts +31 -5
  32. package/src/core/system-message-noise.ts +2 -2
  33. package/src/core/system-message-router.ts +16 -18
  34. package/src/core/turn-event-wiring.ts +272 -33
  35. package/src/core/work-tree-focus.ts +290 -0
  36. package/src/core/work-tree-fold-store.ts +143 -0
  37. package/src/core/work-tree-model.ts +799 -0
  38. package/src/core/work-tree-render.ts +384 -0
  39. package/src/core/work-tree-sources.ts +184 -0
  40. package/src/core/work-tree-wiring.ts +176 -0
  41. package/src/core/workstream-notification.ts +36 -6
  42. package/src/core/wrfc-notice-titles.ts +37 -0
  43. package/src/input/agents-modal-text.ts +119 -0
  44. package/src/input/agents-modal.ts +699 -0
  45. package/src/input/bookmark-modal.ts +33 -9
  46. package/src/input/changes-git.ts +251 -0
  47. package/src/input/changes-modal.ts +629 -0
  48. package/src/input/changes-model.ts +125 -0
  49. package/src/input/command-palette-types.ts +41 -0
  50. package/src/input/command-palette.ts +272 -0
  51. package/src/input/command-registry.ts +77 -35
  52. package/src/input/commands/acp-runtime.ts +19 -9
  53. package/src/input/commands/channel-runtime.ts +2 -2
  54. package/src/input/commands/ci-runtime.ts +1 -1
  55. package/src/input/commands/codebase-runtime.ts +1 -1
  56. package/src/input/commands/config.ts +7 -1
  57. package/src/input/commands/context-window.ts +25 -19
  58. package/src/input/commands/control-room-runtime.ts +18 -18
  59. package/src/input/commands/conversation-runtime.ts +0 -1
  60. package/src/input/commands/cost-runtime.ts +29 -20
  61. package/src/input/commands/diff-runtime.ts +35 -181
  62. package/src/input/commands/eval.ts +1 -1
  63. package/src/input/commands/experience-runtime.ts +3 -7
  64. package/src/input/commands/fresh-conversation.ts +86 -0
  65. package/src/input/commands/git-runtime.ts +5 -22
  66. package/src/input/commands/graph-runtime.ts +1 -1
  67. package/src/input/commands/guidance-runtime.ts +1 -1
  68. package/src/input/commands/health-runtime.ts +8 -9
  69. package/src/input/commands/hooks-runtime.ts +2 -2
  70. package/src/input/commands/hosted-runtime.ts +3 -3
  71. package/src/input/commands/incident-runtime.ts +3 -3
  72. package/src/input/commands/intelligence-runtime.ts +6 -9
  73. package/src/input/commands/knowledge.ts +2 -2
  74. package/src/input/commands/legacy-panel-command.ts +57 -0
  75. package/src/input/commands/local-auth-runtime.ts +8 -9
  76. package/src/input/commands/local-provider-runtime.ts +7 -4
  77. package/src/input/commands/local-runtime.ts +12 -15
  78. package/src/input/commands/local-setup-review.ts +1 -1
  79. package/src/input/commands/local-setup.ts +13 -19
  80. package/src/input/commands/memory.ts +1 -1
  81. package/src/input/commands/notify-runtime.ts +9 -0
  82. package/src/input/commands/operator-runtime.ts +10 -12
  83. package/src/input/commands/permissions-provenance.ts +3 -3
  84. package/src/input/commands/planning-runtime.ts +7 -7
  85. package/src/input/commands/platform-sandbox-runtime.ts +2 -2
  86. package/src/input/commands/platform-services-runtime.ts +1 -1
  87. package/src/input/commands/policy-dispatch.ts +2 -2
  88. package/src/input/commands/policy.ts +3 -3
  89. package/src/input/commands/provider.ts +1 -1
  90. package/src/input/commands/remote-runtime.ts +1 -1
  91. package/src/input/commands/review-runtime.ts +39 -94
  92. package/src/input/commands/rewind-runtime.ts +81 -108
  93. package/src/input/commands/runtime-services.ts +15 -17
  94. package/src/input/commands/services-runtime.ts +2 -2
  95. package/src/input/commands/session-content.ts +21 -5
  96. package/src/input/commands/session-workflow.ts +13 -38
  97. package/src/input/commands/settings-sync-runtime.ts +2 -2
  98. package/src/input/commands/shell-core.ts +19 -4
  99. package/src/input/commands/skills-runtime.ts +2 -2
  100. package/src/input/commands/status-runtime.ts +13 -0
  101. package/src/input/commands/subscription-runtime.ts +1 -1
  102. package/src/input/commands/tasks-runtime.ts +2 -8
  103. package/src/input/commands/work-plan-runtime.ts +4 -4
  104. package/src/input/commands/workstream-attempts.ts +24 -47
  105. package/src/input/commands/workstream-runtime.ts +2 -2
  106. package/src/input/commands/worktree-runtime.ts +3 -3
  107. package/src/input/commands.ts +3 -1
  108. package/src/input/composer-editor.ts +44 -0
  109. package/src/input/concealed-input.ts +1 -1
  110. package/src/input/config-modal-types.ts +20 -8
  111. package/src/input/config-modal.ts +106 -105
  112. package/src/input/confirm-dialog-types.ts +28 -0
  113. package/src/input/confirm-dialog.ts +70 -0
  114. package/src/input/delete-key-policy.ts +3 -3
  115. package/src/input/feed-context-factory.ts +15 -16
  116. package/src/input/handler-command-route.ts +7 -42
  117. package/src/input/handler-content-actions.ts +16 -15
  118. package/src/input/handler-feed-routes.ts +26 -211
  119. package/src/input/handler-feed.ts +68 -95
  120. package/src/input/handler-interactions.ts +5 -3
  121. package/src/input/handler-modal-routes.ts +215 -216
  122. package/src/input/handler-modal-stack.ts +81 -11
  123. package/src/input/handler-modal-token-routes.ts +4 -0
  124. package/src/input/handler-onboarding.ts +0 -1
  125. package/src/input/handler-picker-routes.ts +65 -107
  126. package/src/input/handler-session-view-route.ts +149 -0
  127. package/src/input/handler-shortcuts.ts +16 -154
  128. package/src/input/handler-types.ts +4 -2
  129. package/src/input/handler-ui-state.ts +45 -15
  130. package/src/input/handler-work-tree-route.ts +123 -0
  131. package/src/input/handler.ts +25 -70
  132. package/src/input/keybindings.ts +16 -71
  133. package/src/input/masked-entry-modal.ts +117 -0
  134. package/src/input/mcp-workspace.ts +48 -17
  135. package/src/input/model-picker-filter.ts +8 -0
  136. package/src/input/model-picker-open.ts +99 -0
  137. package/src/input/model-picker-types.ts +1 -1
  138. package/src/input/model-picker.ts +92 -9
  139. package/src/input/notifications-modal.ts +123 -0
  140. package/src/input/overlay-filter.ts +26 -0
  141. package/src/input/profile-picker-modal.ts +37 -8
  142. package/src/input/search.ts +3 -3
  143. package/src/input/selection-modal-queue.ts +2 -0
  144. package/src/input/selection-modal.ts +28 -16
  145. package/src/input/session-picker-modal.ts +33 -8
  146. package/src/input/settings-modal-activation.ts +7 -2
  147. package/src/input/settings-modal-data.ts +11 -6
  148. package/src/input/settings-modal-types.ts +1 -1
  149. package/src/input/settings-modal.ts +20 -20
  150. package/src/input/submission-intent.ts +1 -1
  151. package/src/input/submission-router.ts +4 -9
  152. package/src/input/surface-modal-host.ts +110 -0
  153. package/src/input/theme-settings-actions.ts +82 -0
  154. package/src/input/usage-modal.ts +183 -0
  155. package/src/input/views.ts +97 -0
  156. package/src/main.ts +175 -165
  157. package/src/permissions/hunk-selection.ts +1 -1
  158. package/src/permissions/prompt-card.ts +511 -0
  159. package/src/permissions/prompt.ts +59 -622
  160. package/src/renderer/agents-modal.ts +310 -0
  161. package/src/renderer/autocomplete-overlay.ts +42 -220
  162. package/src/renderer/back-to-bottom.ts +80 -0
  163. package/src/renderer/block-actions-overlay.ts +30 -48
  164. package/src/renderer/bookmark-modal.ts +48 -79
  165. package/src/renderer/changes-modal.ts +325 -0
  166. package/src/renderer/code-block.ts +184 -106
  167. package/src/renderer/command-palette.ts +93 -0
  168. package/src/renderer/composer-fragments.ts +4 -4
  169. package/src/renderer/composer.ts +146 -0
  170. package/src/renderer/compositor.ts +26 -160
  171. package/src/renderer/config-modal.ts +185 -81
  172. package/src/renderer/confirm-dialog.ts +50 -0
  173. package/src/renderer/context-inspector.ts +116 -154
  174. package/src/renderer/context-status-hint.ts +4 -4
  175. package/src/renderer/conversation-layout.ts +20 -0
  176. package/src/renderer/conversation-overlays.ts +94 -84
  177. package/src/renderer/conversation-surface.ts +19 -38
  178. package/src/renderer/diff-tint.ts +98 -0
  179. package/src/renderer/file-picker-overlay.ts +63 -90
  180. package/src/renderer/footer-targets.ts +38 -0
  181. package/src/renderer/git-status.ts +3 -4
  182. package/src/renderer/header-line.ts +140 -117
  183. package/src/renderer/help-overlay.ts +133 -219
  184. package/src/renderer/history-search-overlay.ts +15 -12
  185. package/src/renderer/lane-graph/bead.ts +800 -0
  186. package/src/renderer/lane-graph/glyphs.ts +134 -0
  187. package/src/renderer/lane-graph/layout.ts +248 -0
  188. package/src/renderer/lane-graph/paint.ts +537 -0
  189. package/src/renderer/lane-graph/semantic-memo.ts +77 -0
  190. package/src/renderer/layout-engine.ts +2 -57
  191. package/src/renderer/layout.ts +7 -13
  192. package/src/renderer/markdown-inline.ts +2 -1
  193. package/src/renderer/markdown-table.ts +17 -12
  194. package/src/renderer/markdown.ts +92 -34
  195. package/src/renderer/masked-entry-modal.ts +78 -0
  196. package/src/renderer/mcp-workspace.ts +166 -203
  197. package/src/renderer/model-workspace.ts +381 -436
  198. package/src/renderer/notifications-modal.ts +116 -0
  199. package/src/renderer/onboarding/onboarding-wizard.ts +178 -588
  200. package/src/renderer/process-view.ts +192 -0
  201. package/src/renderer/profile-picker-modal.ts +58 -110
  202. package/src/renderer/search-overlay.ts +6 -3
  203. package/src/renderer/selection-modal-overlay.ts +87 -327
  204. package/src/renderer/semantic-diff.ts +2 -2
  205. package/src/renderer/session-chips.ts +102 -0
  206. package/src/renderer/session-picker-modal.ts +177 -297
  207. package/src/renderer/settings-modal-connections.ts +14 -23
  208. package/src/renderer/settings-modal-context.ts +367 -0
  209. package/src/renderer/settings-modal-helpers.ts +6 -11
  210. package/src/renderer/settings-modal.ts +322 -726
  211. package/src/renderer/shell-surface.ts +277 -175
  212. package/src/renderer/shortcuts-overlay.ts +217 -0
  213. package/src/renderer/status-line.ts +475 -0
  214. package/src/renderer/surface-compose.ts +174 -0
  215. package/src/renderer/surface-kit-extra.ts +303 -0
  216. package/src/renderer/surface-kit-list.ts +366 -0
  217. package/src/renderer/surface-kit-parts.ts +360 -0
  218. package/src/renderer/surface-kit.ts +568 -0
  219. package/src/renderer/syntax-highlighter.ts +280 -197
  220. package/src/renderer/syntax-theme.ts +51 -0
  221. package/src/renderer/system-message.ts +0 -1
  222. package/src/renderer/term-caps.ts +42 -1
  223. package/src/renderer/terminal-bg-probe.ts +116 -19
  224. package/src/renderer/terminal-palette-probe.ts +322 -0
  225. package/src/renderer/terminal-palette-reader.ts +183 -0
  226. package/src/renderer/terminal-palette.ts +58 -0
  227. package/src/renderer/theme-mode-config.ts +16 -1
  228. package/src/renderer/theme.ts +234 -207
  229. package/src/renderer/thinking.ts +0 -1
  230. package/src/renderer/throbber.ts +188 -0
  231. package/src/renderer/toast-center.ts +119 -0
  232. package/src/renderer/ui-factory.ts +39 -602
  233. package/src/renderer/ui-primitives.ts +8 -25
  234. package/src/renderer/usage-modal.ts +353 -0
  235. package/src/renderer/user-message.ts +58 -0
  236. package/src/renderer/voice-capture-chip.ts +53 -0
  237. package/src/runtime/bootstrap-command-context.ts +0 -5
  238. package/src/runtime/bootstrap-command-parts.ts +38 -89
  239. package/src/runtime/bootstrap-core.ts +13 -15
  240. package/src/runtime/bootstrap-hook-bridge.ts +12 -21
  241. package/src/runtime/bootstrap-notifier-sync.ts +17 -1
  242. package/src/runtime/bootstrap-shell.ts +16 -50
  243. package/src/runtime/bootstrap.ts +14 -14
  244. package/src/runtime/client/fleet-union.ts +5 -5
  245. package/src/runtime/client/hosted-exit.ts +2 -2
  246. package/src/runtime/client/hosted-sessions.ts +1 -1
  247. package/src/runtime/client/spine-adoption.ts +1 -1
  248. package/src/runtime/context-accounting-source.ts +5 -4
  249. package/src/runtime/index.ts +1 -0
  250. package/src/runtime/notification-dispatch.ts +102 -29
  251. package/src/runtime/recovery-offer-wiring.ts +4 -1
  252. package/src/runtime/recovery-prompt.ts +31 -15
  253. package/src/runtime/runtime-services-types.ts +10 -6
  254. package/src/runtime/services.ts +17 -12
  255. package/src/runtime/session-continuity-hints.ts +2 -5
  256. package/src/runtime/session-inbound-inputs.ts +1 -1
  257. package/src/runtime/session-resume-liveness-confirm.ts +14 -2
  258. package/src/runtime/startup-typeahead-gate.ts +103 -0
  259. package/src/runtime/store/domains/domain-read-matrix.ts +0 -1
  260. package/src/runtime/store/domains/index.ts +0 -8
  261. package/src/runtime/store/selectors/index.ts +0 -28
  262. package/src/runtime/store/state.ts +0 -8
  263. package/src/runtime/ui-services.ts +4 -6
  264. package/src/runtime/usage-tracker.ts +348 -0
  265. package/src/runtime/wrfc-persistence.ts +4 -4
  266. package/src/shell/blocking-input.ts +92 -2
  267. package/src/shell/recovery-input-helpers.ts +1 -1
  268. package/src/shell/session-views.ts +539 -0
  269. package/src/shell/status-report.ts +86 -0
  270. package/src/shell/transcript-scroll.ts +68 -0
  271. package/src/shell/ui-openers.ts +84 -172
  272. package/src/shell/view-openers.ts +205 -0
  273. package/src/tools/wrfc-agent-guard.ts +22 -2
  274. package/src/utils/format-duration.ts +4 -8
  275. package/src/utils/splash-lines.ts +9 -1
  276. package/src/utils/terminal-width.ts +56 -0
  277. package/src/verification/verification-ledger.ts +18 -16
  278. package/src/{panels → views}/agent-inspector-shared.ts +2 -2
  279. package/src/{panels → views}/builtin-modals.ts +22 -21
  280. package/src/views/builtin-views.ts +135 -0
  281. package/src/{panels → views}/confirm-state.ts +9 -9
  282. package/src/{panels → views}/diff-review-model.ts +10 -3
  283. package/src/{panels → views}/eval-registry.ts +1 -1
  284. package/src/{panels → views}/fleet-acts.ts +49 -85
  285. package/src/{panels/fleet-panel-format.ts → views/fleet-format.ts} +40 -40
  286. package/src/{panels → views}/fleet-gateway.ts +5 -5
  287. package/src/{panels → views}/fleet-observed-render.ts +16 -16
  288. package/src/{panels → views}/fleet-read-model.ts +9 -9
  289. package/src/{panels/fleet-tabs.ts → views/fleet-session-tabs.ts} +13 -13
  290. package/src/{panels → views}/fleet-spawn.ts +33 -49
  291. package/src/{panels → views}/fleet-steer.ts +12 -12
  292. package/src/{panels → views}/fleet-stop.ts +4 -4
  293. package/src/{panels → views}/fleet-transcript.ts +26 -27
  294. package/src/{panels/fleet-panel-worktree-detail.ts → views/fleet-worktree-detail.ts} +7 -7
  295. package/src/{panels → views}/hosted-session-feed.ts +6 -6
  296. package/src/views/modal-surface-registry.ts +41 -0
  297. package/src/{panels → views}/modals/devices-modal.ts +2 -2
  298. package/src/{panels → views}/modals/hooks-modal.ts +13 -13
  299. package/src/{panels → views}/modals/keybindings-modal.ts +19 -27
  300. package/src/{panels → views}/modals/knowledge-modal.ts +6 -5
  301. package/src/{panels → views}/modals/local-auth-modal.ts +22 -21
  302. package/src/{panels → views}/modals/marketplace-modal.ts +11 -11
  303. package/src/{panels → views}/modals/memory-modal.ts +8 -8
  304. package/src/{panels → views}/modals/modal-surface-helpers.ts +15 -5
  305. package/src/{panels → views}/modals/pairing-modal.ts +9 -9
  306. package/src/{panels → views}/modals/planning-modal.ts +15 -20
  307. package/src/{panels → views}/modals/plugins-modal.ts +6 -6
  308. package/src/{panels → views}/modals/policy-modal.ts +13 -5
  309. package/src/{panels → views}/modals/provider-health-modal.ts +4 -4
  310. package/src/{panels → views}/modals/remote-modal.ts +3 -3
  311. package/src/{panels → views}/modals/sandbox-modal.ts +4 -4
  312. package/src/{panels → views}/modals/security-modal.ts +22 -16
  313. package/src/{panels → views}/modals/services-modal.ts +3 -3
  314. package/src/{panels → views}/modals/settings-sync-modal.ts +4 -4
  315. package/src/{panels → views}/modals/skills-modal.ts +9 -9
  316. package/src/{panels → views}/modals/subscription-modal.ts +3 -3
  317. package/src/{panels → views}/modals/work-plan-modal.ts +10 -13
  318. package/src/views/notifications-feed.ts +314 -0
  319. package/src/{panels → views}/polish-core.ts +38 -36
  320. package/src/{panels → views}/polish-tables.ts +35 -26
  321. package/src/{panels → views}/polish.ts +101 -95
  322. package/src/{panels → views}/project-planning-answer-actions.ts +3 -3
  323. package/src/{panels → views}/provider-health-routes.ts +14 -14
  324. package/src/{panels → views}/search-focus.ts +8 -8
  325. package/src/{panels → views}/session-maintenance.ts +11 -11
  326. package/src/views/skills-discovery.ts +159 -0
  327. package/src/views/view-deps.ts +80 -0
  328. package/src/views/view-panel-adapter.ts +53 -0
  329. package/src/{panels → views}/workstream-graph-render.ts +1 -1
  330. package/src/core/conversation-fold.ts +0 -101
  331. package/src/core/conversation-turn-structure.ts +0 -464
  332. package/src/core/conversation-user-receipts.ts +0 -42
  333. package/src/input/commands/operator-panel-runtime.ts +0 -186
  334. package/src/input/panel-integration-actions.ts +0 -33
  335. package/src/input/panel-mouse-geometry.ts +0 -97
  336. package/src/input/panel-paste-flood-guard.ts +0 -86
  337. package/src/panels/base-panel.ts +0 -301
  338. package/src/panels/builtin/agent.ts +0 -27
  339. package/src/panels/builtin/development.ts +0 -92
  340. package/src/panels/builtin/knowledge.ts +0 -12
  341. package/src/panels/builtin/operations.ts +0 -177
  342. package/src/panels/builtin/session.ts +0 -81
  343. package/src/panels/builtin/shared.ts +0 -357
  344. package/src/panels/builtin-panels.ts +0 -28
  345. package/src/panels/cost-tracker-panel.ts +0 -746
  346. package/src/panels/diff-panel.ts +0 -797
  347. package/src/panels/diff-review-panel.ts +0 -395
  348. package/src/panels/expandable-list-panel.ts +0 -189
  349. package/src/panels/fleet-deep-link.ts +0 -31
  350. package/src/panels/fleet-panel.ts +0 -799
  351. package/src/panels/git-panel.ts +0 -800
  352. package/src/panels/hosted-session-panel.ts +0 -221
  353. package/src/panels/index.ts +0 -21
  354. package/src/panels/local-auth-panel.ts +0 -478
  355. package/src/panels/notifications-feed.ts +0 -127
  356. package/src/panels/notifications-panel.ts +0 -160
  357. package/src/panels/panel-confirm-overlay.ts +0 -72
  358. package/src/panels/panel-manager.ts +0 -791
  359. package/src/panels/plugins-panel.ts +0 -371
  360. package/src/panels/scrollable-list-panel.ts +0 -498
  361. package/src/panels/skills-panel.ts +0 -454
  362. package/src/panels/token-budget-panel.ts +0 -688
  363. package/src/panels/types.ts +0 -191
  364. package/src/panels/wrfc-panel-format.ts +0 -133
  365. package/src/renderer/diff-view.ts +0 -134
  366. package/src/renderer/fleet-tab-strip.ts +0 -56
  367. package/src/renderer/footer-tips.ts +0 -53
  368. package/src/renderer/fullscreen-primitives.ts +0 -146
  369. package/src/renderer/fullscreen-workspace.ts +0 -194
  370. package/src/renderer/hint-grammar.ts +0 -52
  371. package/src/renderer/modal-factory.ts +0 -480
  372. package/src/renderer/overlay-box.ts +0 -154
  373. package/src/renderer/overlay-viewport.ts +0 -104
  374. package/src/renderer/panel-composite.ts +0 -145
  375. package/src/renderer/panel-workspace-bar.ts +0 -54
  376. package/src/renderer/process-indicator.ts +0 -163
  377. package/src/renderer/tab-strip.ts +0 -222
  378. package/src/renderer/text-layout.ts +0 -31
  379. package/src/renderer/tool-call.ts +0 -276
  380. package/src/renderer/tool-result-expanded-lines.ts +0 -48
  381. package/src/renderer/tool-result-summary.ts +0 -219
  382. package/src/runtime/diagnostics/panels/index.ts +0 -21
  383. package/src/runtime/diagnostics/panels/ops.ts +0 -156
  384. package/src/runtime/perf/panel-contracts.ts +0 -32
  385. package/src/runtime/store/domains/panels.ts +0 -117
  386. /package/src/{panels → views}/memory-consolidation-gateway.ts +0 -0
  387. /package/src/{panels → views}/modals/modal-theme.ts +0 -0
  388. /package/src/{panels → views}/provider-health-domains.ts +0 -0
@@ -1,28 +1,20 @@
1
1
  /**
2
- * renderSessionPickerModal, renders the /sessions picker modal as Line[]
3
- * using ModalFactory.
2
+ * renderSessionPickerModal, the /sessions picker drawn with the modal surface kit.
4
3
  *
5
- * Shows a list of saved sessions with:
6
- * - name, timestamp (formatted), message count
7
- * Footer hints: [Enter] Load [d] Delete [Esc] Close
8
- *
9
- * When the modal was wired with a cross-surface session union
10
- * (`modal.crossSurfaceView.mode !== 'local'`), an additional read-only
11
- * "Cross-surface sessions" section is appended, badged kind/status/project
12
- * (parity with the webui SessionsView) with one of three honest states,
13
- * true-empty, offline, or stale, never a silently-collapsed list. In
14
- * 'local' mode (no sessionBroker wired, e.g. every pre-existing caller/test)
15
- * this section is entirely absent and the box sizing is unchanged.
4
+ * Saved sessions grouped by recency (today, yesterday, this week, earlier)
5
+ * with message count and age right-aligned and muted, an always-live search
6
+ * row, and two read-only groups: sessions hosted on the daemon and the
7
+ * cross-surface session union (each with its honest state: never read,
8
+ * offline, stale, empty). Deleting arms the row in red and asks for the same
9
+ * key again, in place.
16
10
  */
17
11
 
18
- import type { Line } from '@pellux/goodvibes-sdk/platform/types';
19
12
  import type { SharedSessionRecord } from '@pellux/goodvibes-sdk/platform/control-plane';
20
- import { ModalFactory } from './modal-factory.ts';
21
- import { UI_TONES } from './ui-primitives.ts';
13
+ import { activeTokens } from './theme.ts';
22
14
  import type { SessionPickerModal } from '../input/session-picker-modal.ts';
23
15
  import { formatTimestamp } from './modal-utils.ts';
24
- import { fitDisplay } from '../utils/terminal-width.ts';
25
- import { getOverlaySurfaceMetrics, getStableOverlayContentRows } from './overlay-viewport.ts';
16
+ import { beginModal, drawWrapped, finishModal, scrollCountText, searchRow, wrapLines, type KitHint, type SurfaceLayer } from './surface-kit.ts';
17
+ import { drawList, drawScrollingList, measureRow, type KitRow } from './surface-kit-list.ts';
26
18
 
27
19
  // ---------------------------------------------------------------------------
28
20
  // Cross-surface badge helpers (parity with webui's src/lib/sessions-union.ts)
@@ -110,31 +102,6 @@ function hostedRosterNote(roster: SessionPickerModal['hostedRoster']): string |
110
102
  return null;
111
103
  }
112
104
 
113
- /**
114
- * One hosted row: the id first, then what it is doing, what leaving it would
115
- * do, and finally its title.
116
- *
117
- * The id leads because it is the ACTIONABLE part, the row exists so `/hosted
118
- * attach <id>` can be typed off it, and a narrow terminal clips the tail. What
119
- * gets clipped must therefore be the descriptive end of the line, never the
120
- * thing the user has to retype.
121
- */
122
- function hostedRowLabel(
123
- record: SessionPickerModal['hostedRoster']['sessions'][number],
124
- contentWidth: number,
125
- ): string {
126
- const policy = record.effectiveDetachPolicy === 'survive' ? 'survives detach' : 'ends on detach';
127
- const attached = record.attachedClients.length > 0 ? `${record.attachedClients.length} attached` : 'nobody attached';
128
- // Fitted to the content width MINUS the list indent modal-factory adds: at
129
- // the full width the row wraps to a second line, which the section's row
130
- // accounting does not reserve, and the trailing attach hint is then what the
131
- // tail clip eats.
132
- return fitDisplay(
133
- `${record.id} · ${record.status} · ${policy} · ${attached} · ${record.title || 'untitled'}`,
134
- Math.max(8, contentWidth - 4),
135
- );
136
- }
137
-
138
105
  /**
139
106
  * Whether the hosted section renders at all.
140
107
  *
@@ -148,25 +115,6 @@ function hostedSectionVisible(modal: SessionPickerModal): boolean {
148
115
  return roster.sessions.length > 0 || roster.capturedAt !== null || roster.note !== null;
149
116
  }
150
117
 
151
- function hostedRowCount(modal: SessionPickerModal): number {
152
- return Math.min(MAX_HOSTED_ROWS, modal.hostedRoster.sessions.length);
153
- }
154
-
155
- /**
156
- * Extra content rows the hosted section needs: separator + header (+note)
157
- * + rows (+overflow line + the attach hint). Counted for the same reason the
158
- * cross-surface section counts its own, an uncounted trailing row is silently
159
- * eaten by modal-factory's tail clip.
160
- */
161
- function hostedExtraRows(modal: SessionPickerModal): number {
162
- if (!hostedSectionVisible(modal)) return 0;
163
- const rows = hostedRowCount(modal);
164
- const noteRow = hostedRosterNote(modal.hostedRoster) ? 1 : 0;
165
- const overflowRow = modal.hostedRoster.sessions.length > MAX_HOSTED_ROWS ? 1 : 0;
166
- const hintRow = rows > 0 ? 1 : 0;
167
- return 2 + noteRow + overflowRow + hintRow + rows;
168
- }
169
-
170
118
  /**
171
119
  * The exact honest note for the current state, or null when the union view
172
120
  * needs no caveat (fresh, with rows). Precedence: offline > stale >
@@ -185,27 +133,108 @@ function crossSurfaceNote(view: SessionPickerModal['crossSurfaceView'], rowCount
185
133
  return null;
186
134
  }
187
135
 
188
- function crossSurfaceRowCount(modal: SessionPickerModal): number {
189
- return modal.crossSurfaceView.mode === 'local' ? 0 : Math.min(MAX_CROSS_SURFACE_ROWS, modal.crossSurfaceSessions.length);
136
+ // ---------------------------------------------------------------------------
137
+ // Rows
138
+ // ---------------------------------------------------------------------------
139
+
140
+ const DAY_MS = 86_400_000;
141
+ const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
142
+
143
+ type RecencyGroup = 'Today' | 'Yesterday' | 'This week' | 'Earlier';
144
+
145
+ function startOfDay(ms: number): number {
146
+ const d = new Date(ms);
147
+ d.setHours(0, 0, 0, 0);
148
+ return d.getTime();
149
+ }
150
+
151
+ function recencyGroup(ts: number, now: number): RecencyGroup {
152
+ const today = startOfDay(now);
153
+ if (ts >= today) return 'Today';
154
+ if (ts >= today - DAY_MS) return 'Yesterday';
155
+ if (ts >= today - 6 * DAY_MS) return 'This week';
156
+ return 'Earlier';
190
157
  }
191
158
 
192
- /**
193
- * Extra content rows the cross-surface section needs: separator + header
194
- * (+note) + rows (+the "[showing N of M]" overflow line when the union has
195
- * more records than MAX_CROSS_SURFACE_ROWS, previously uncounted here, which
196
- * let modal-factory's tail-clip silently eat that trailing line even though
197
- * the rest of the budget accounting held; see W3 Finding 1).
198
- */
199
- function crossSurfaceExtraRows(modal: SessionPickerModal): number {
200
- if (modal.crossSurfaceView.mode === 'local') return 0;
201
- const rows = crossSurfaceRowCount(modal);
202
- const noteRow = crossSurfaceNote(modal.crossSurfaceView, rows) ? 1 : 0;
203
- const overflowRow = modal.crossSurfaceSessions.length > MAX_CROSS_SURFACE_ROWS ? 1 : 0;
204
- // W4 UX-lens: the plain-language 'reaped' explainer is its own row,
205
- // uncounted here it would be silently eaten by modal-factory's tail-clip
206
- // exactly the way W3 Finding 1 describes above.
207
- const reapedHintRow = hasVisibleReapedRow(modal) ? 1 : 0;
208
- return 2 + noteRow + overflowRow + reapedHintRow + Math.max(rows, noteRow === 1 && rows === 0 ? 0 : rows);
159
+ /** Short age for the right column: HH:MM today, "Mon D" this year, the full date otherwise. */
160
+ function shortWhen(ts: number, now: number): string {
161
+ if (!ts) return 'unknown';
162
+ const d = new Date(ts);
163
+ if (ts >= startOfDay(now)) return formatTimestamp(ts).slice(11);
164
+ if (d.getFullYear() === new Date(now).getFullYear()) return `${MONTHS[d.getMonth()]} ${d.getDate()}`;
165
+ return formatTimestamp(ts).slice(0, 10);
166
+ }
167
+
168
+ function hostedRows(modal: SessionPickerModal): KitRow[] {
169
+ if (!hostedSectionVisible(modal)) return [];
170
+ const t = activeTokens();
171
+ const roster = modal.hostedRoster;
172
+ const rows: KitRow[] = [{ header: 'Hosted', headerRight: 'on the daemon' }];
173
+ const note = hostedRosterNote(roster);
174
+ if (note) rows.push({ label: note, labelFg: roster.note ? t.warning : t.textFaint });
175
+ for (const record of roster.sessions.slice(0, MAX_HOSTED_ROWS)) {
176
+ const policy = record.effectiveDetachPolicy === 'survive' ? 'survives detach' : 'ends on detach';
177
+ const attached = record.attachedClients.length > 0 ? `${record.attachedClients.length} attached` : 'nobody attached';
178
+ const running = record.status === 'running';
179
+ const terminated = record.status === 'terminated';
180
+ rows.push({
181
+ label: record.title || 'untitled',
182
+ desc: `${record.id} · ${record.status} · ${policy} · ${attached}`,
183
+ mark: running ? '◐' : terminated ? '○' : '●',
184
+ markFg: running ? t.brand : terminated ? t.textFaint : t.textMuted,
185
+ muted: terminated,
186
+ });
187
+ }
188
+ if (roster.sessions.length > MAX_HOSTED_ROWS) rows.push({ label: `showing ${MAX_HOSTED_ROWS} of ${roster.sessions.length}`, labelFg: t.textFaint });
189
+ if (roster.sessions.length > 0) rows.push({ label: 'Join one with /hosted attach <id>', labelFg: t.textFaint });
190
+ return rows;
191
+ }
192
+
193
+ function crossSurfaceRows(modal: SessionPickerModal): KitRow[] {
194
+ if (modal.crossSurfaceView.mode === 'local') return [];
195
+ const t = activeTokens();
196
+ const rows: KitRow[] = [{ header: 'Other surfaces', headerRight: modal.crossSurfaceView.mode }];
197
+ const note = crossSurfaceNote(modal.crossSurfaceView, modal.crossSurfaceSessions.length);
198
+ if (note) rows.push({ label: note, labelFg: modal.crossSurfaceView.offlineNote ? t.warning : t.textFaint });
199
+ for (const record of modal.crossSurfaceSessions.slice(0, MAX_CROSS_SURFACE_ROWS)) {
200
+ const reaped = isReapedRecord(record);
201
+ const closed = isClosedStatus(record.status);
202
+ rows.push({
203
+ label: record.title || record.id,
204
+ desc: `${kindLabel(record.kind)} · ${statusLabel(record)} · ${projectLabel(record.project)}`,
205
+ // Reaped rows get their own tone: they reopen on the next heartbeat, unlike a deliberate close.
206
+ labelFg: reaped ? t.info : undefined,
207
+ muted: closed && !reaped,
208
+ });
209
+ }
210
+ if (modal.crossSurfaceSessions.length > MAX_CROSS_SURFACE_ROWS) {
211
+ rows.push({ label: `showing ${MAX_CROSS_SURFACE_ROWS} of ${modal.crossSurfaceSessions.length}`, labelFg: t.textFaint });
212
+ }
213
+ if (hasVisibleReapedRow(modal)) rows.push({ label: REAPED_BADGE_HINT, labelFg: t.info });
214
+ return rows;
215
+ }
216
+
217
+ function localRows(modal: SessionPickerModal, now: number): Map<RecencyGroup, KitRow[]> {
218
+ const t = activeTokens();
219
+ const groups = new Map<RecencyGroup, KitRow[]>();
220
+ modal.visibleSessions().forEach((sess, index) => {
221
+ const selected = index === modal.selectedIndex;
222
+ const armed = modal.deleteConfirmationTarget === sess.name && selected;
223
+ const group = recencyGroup(sess.timestamp, now);
224
+ const title = sess.title && sess.title !== sess.name ? sess.title : '';
225
+ const row: KitRow = armed
226
+ ? { label: `Press d again to delete "${sess.name}"`, right: `${sess.messageCount} msgs · ${shortWhen(sess.timestamp, now)}`, danger: true, mark: '✕', markFg: t.error, selected }
227
+ : {
228
+ label: title || sess.name,
229
+ desc: title ? sess.name : undefined,
230
+ right: `${sess.messageCount} msgs · ${shortWhen(sess.timestamp, now)}`,
231
+ selected,
232
+ };
233
+ const list = groups.get(group) ?? [];
234
+ list.push(row);
235
+ groups.set(group, list);
236
+ });
237
+ return groups;
209
238
  }
210
239
 
211
240
  // ---------------------------------------------------------------------------
@@ -213,231 +242,82 @@ function crossSurfaceExtraRows(modal: SessionPickerModal): number {
213
242
  // ---------------------------------------------------------------------------
214
243
 
215
244
  /**
216
- * Render the session picker modal as Line[] for overlay in the viewport.
245
+ * Render the session picker as a modal layer.
217
246
  *
218
- * @param modal SessionPickerModal state object.
219
- * @param width Terminal width.
247
+ * @param modal SessionPickerModal state.
248
+ * @param screenWidth Terminal width.
249
+ * @param screenHeight Terminal height.
250
+ * @param now Clock for the recency groups (tests pass a fixed value).
220
251
  */
221
252
  export function renderSessionPickerModal(
222
253
  modal: SessionPickerModal,
223
- width: number,
224
- viewportHeight = 24,
225
- ): Line[] {
226
- const extraRows = crossSurfaceExtraRows(modal) + hostedExtraRows(modal);
227
- const metrics = getOverlaySurfaceMetrics(width, viewportHeight, {
228
- chromeRows: 6,
229
- minContentRows: 5,
230
- maxContentRows: 9 + extraRows,
231
- });
232
- const boxMargin = metrics.margin;
233
- const boxW = metrics.boxWidth;
234
- const contentW = metrics.contentWidth;
235
- const targetContentRows = getStableOverlayContentRows(metrics.contentRows, 8 + extraRows);
236
-
237
- // W3 Finding 1: when a cross-surface union section is present, cap the
238
- // LOCAL list's window to what's left after reserving extraRows for the
239
- // union, otherwise the local list expands to fill metrics.contentRows
240
- // (which already includes the union's reservation) and modal-factory's
241
- // single tail-clip (createModal, targetContentRows) silently drops the
242
- // union section, or its trailing rows, once the box fills up. In local
243
- // mode (extraRows === 0, every pre-existing caller/test) this reduces to
244
- // the exact pre-T2 formula, so local-only output is byte-for-byte
245
- // unaffected.
246
- let visibleRows: number;
247
- if (extraRows === 0) {
248
- visibleRows = metrics.contentRows;
249
- } else {
250
- const localBudget = Math.max(0, targetContentRows - extraRows);
251
- const overhead = 2; // header + separator row, present whenever sessions.length > 0
252
- const remaining = Math.max(0, localBudget - overhead);
253
- visibleRows = modal.sessions.length > remaining
254
- ? Math.max(3, remaining - 2) // reserve 2 more rows for the local "[x-y of N]" overflow line
255
- : Math.max(3, remaining);
254
+ screenWidth: number,
255
+ screenHeight = 24,
256
+ now: number = Date.now(),
257
+ ): SurfaceLayer {
258
+ const t = activeTokens();
259
+ const armed = modal.deleteConfirmationTarget !== null;
260
+ const hints: KitHint[] = modal.query.length === 0
261
+ ? [['↑↓', 'move'], ['⏎', 'open'], ['d', armed ? 'confirm delete' : 'delete']]
262
+ : [['↑↓', 'move'], ['⏎', 'open'], ['⌫', 'edit search']];
263
+ const f = beginModal(screenWidth, screenHeight, { title: 'Sessions', hints });
264
+
265
+ const visible = modal.visibleSessions();
266
+ const count = modal.query.length > 0 ? `${visible.length} of ${modal.sessions.length}` : `${modal.sessions.length} saved`;
267
+ searchRow(f, f.top, modal.query, 'Search sessions', count);
268
+
269
+ // Status and the armed-delete note sit at the bottom of the body, wrapped in full.
270
+ const footLines: Array<{ text: string; fg: string }> = [];
271
+ const width = f.r - f.l + 1;
272
+ // While armed, the row itself carries the "press d again" prompt.
273
+ if (modal.statusMessage && !armed) {
274
+ for (const line of wrapLines(modal.statusMessage, width)) footLines.push({ text: line, fg: armed ? t.warning : t.accent });
256
275
  }
257
- modal.setVisibleRows(visibleRows);
258
-
259
- const sections: import('./modal-factory.ts').ModalSection[] = [];
260
-
261
- if (modal.sessions.length === 0) {
262
- sections.push({
263
- type: 'text',
264
- content: 'No saved sessions.',
265
- style: { fg: '244', dim: true },
266
- });
267
- sections.push({
268
- type: 'text',
269
- content: 'Use /save [name] to save the current session.',
270
- style: { fg: '240', dim: true },
271
- });
272
- } else {
273
- // Proportional column widths that adapt to the modal's content width:
274
- // timestamp ~22% (clamped 10..16), messages ~12% (clamped 4..8), and the
275
- // name column absorbs the remainder so the row always fills contentW.
276
- const tsW = Math.min(16, Math.max(10, Math.floor(contentW * 0.22)));
277
- const msgW = Math.min(8, Math.max(4, Math.floor(contentW * 0.12)));
278
- const nameW = Math.max(8, contentW - tsW - msgW - 4); // 4 = separators/spaces
279
-
280
- // Column header
281
- const nameHdr = fitDisplay('Name', nameW);
282
- const tsHdr = fitDisplay('Saved', tsW);
283
- const msgHdr = fitDisplay('Msgs', msgW);
284
- sections.push({
285
- type: 'text',
286
- content: `${nameHdr} ${tsHdr} ${msgHdr}`,
287
- style: { fg: '240', dim: true },
288
- });
289
- sections.push({ type: 'separator' });
290
-
291
- const visibleSessions = modal.sessions.slice(modal.scrollOffset, modal.scrollOffset + visibleRows);
292
- const listItems: import('./modal-factory.ts').ModalListItem[] = visibleSessions.map((sess, idx) => {
293
- const isSelected = modal.scrollOffset + idx === modal.selectedIndex;
294
-
295
- const nameStr = fitDisplay(sess.name, nameW);
296
-
297
- const tsStr = fitDisplay(formatTimestamp(sess.timestamp), tsW);
298
- const msgStr = fitDisplay(String(sess.messageCount), msgW);
299
-
300
- const label = `${nameStr} ${tsStr} ${msgStr}`;
301
- return { label, selected: isSelected };
302
- });
303
-
304
- sections.push({ type: 'list', items: listItems });
305
- if (modal.sessions.length > visibleRows) {
306
- sections.push({ type: 'separator' });
307
- sections.push({
308
- type: 'text',
309
- content: `[${modal.scrollOffset + 1}-${Math.min(modal.sessions.length, modal.scrollOffset + visibleRows)} of ${modal.sessions.length}]`,
310
- style: { fg: '244', dim: true },
311
- });
312
- }
276
+ if (armed) {
277
+ for (const line of wrapLines(`Deletion is armed for ${modal.deleteConfirmationTarget}. Move selection or press Esc to cancel.`, width)) footLines.push({ text: line, fg: t.textFaint });
313
278
  }
314
-
315
- // Cross-surface session union, visible only when a sessionBroker
316
- // was wired (mode !== 'local'); absent entirely otherwise, so the box size
317
- // and content of every pre-existing (local-only) caller is unaffected.
318
- if (modal.crossSurfaceView.mode !== 'local') {
319
- sections.push({ type: 'separator' });
320
- sections.push({
321
- type: 'text',
322
- content: `Cross-surface sessions (${modal.crossSurfaceView.mode})`,
323
- style: { fg: '240', dim: true, bold: true },
324
- });
325
- const note = crossSurfaceNote(modal.crossSurfaceView, modal.crossSurfaceSessions.length);
326
- if (note) {
327
- sections.push({
328
- type: 'text',
329
- content: note,
330
- style: { fg: modal.crossSurfaceView.offlineNote ? UI_TONES.state.warn : '244', dim: !modal.crossSurfaceView.offlineNote },
331
- });
332
- }
333
- if (modal.crossSurfaceSessions.length > 0) {
334
- const rows = modal.crossSurfaceSessions.slice(0, MAX_CROSS_SURFACE_ROWS);
335
- const listItems: import('./modal-factory.ts').ModalListItem[] = rows.map((record) => {
336
- const reaped = isReapedRecord(record);
337
- const closed = isClosedStatus(record.status);
338
- const label = fitDisplay(
339
- `${record.title || record.id} · ${kindLabel(record.kind)} · ${statusLabel(record)} · ${projectLabel(record.project)}`,
340
- contentW,
341
- );
342
- // Reaped rows get their own tone (not plain dim-closed): the session
343
- // will auto-reopen on the next heartbeat, unlike a deliberate close.
344
- const style = reaped ? { fg: UI_TONES.state.info } : closed ? { fg: '244', dim: true } : undefined;
345
- return { label, style };
346
- });
347
- sections.push({ type: 'list', items: listItems });
348
- if (modal.crossSurfaceSessions.length > MAX_CROSS_SURFACE_ROWS) {
349
- sections.push({
350
- type: 'text',
351
- content: `[showing ${MAX_CROSS_SURFACE_ROWS} of ${modal.crossSurfaceSessions.length}]`,
352
- style: { fg: '244', dim: true },
353
- });
354
- }
355
- // W4 UX-lens: only ever rendered when a visible row actually carries
356
- // the 'reaped' badge, never speculative noise.
357
- if (hasVisibleReapedRow(modal)) {
358
- sections.push({
359
- type: 'text',
360
- content: REAPED_BADGE_HINT,
361
- style: { fg: UI_TONES.state.info, dim: true },
362
- });
363
- }
364
- }
279
+ const listBottom = footLines.length > 0 ? f.bottom - footLines.length - 1 : f.bottom;
280
+
281
+ const rows: KitRow[] = [];
282
+ const local = localRows(modal, now);
283
+ const pushGroup = (name: RecencyGroup): void => {
284
+ const group = local.get(name);
285
+ if (group && group.length > 0) rows.push({ header: name }, ...group);
286
+ };
287
+ if (modal.sessions.length === 0) {
288
+ rows.push({ label: 'No saved sessions.', labelFg: t.textFaint });
289
+ rows.push({ label: 'Use /save [name] to save the current session.', labelFg: t.textFaint });
290
+ } else if (visible.length === 0) {
291
+ rows.push({ label: `No sessions match "${modal.query}".`, labelFg: t.textFaint });
365
292
  }
366
-
367
- // Daemon-hosted sessions, conversations whose loop runs in the daemon rather
368
- // than in this terminal. Read-only here: joining one is `/hosted attach <id>`,
369
- // which opens a live stream this modal has no business owning, so the full id
370
- // is rendered for the command to be typed straight off the row.
371
- if (hostedSectionVisible(modal)) {
372
- sections.push({ type: 'separator' });
373
- sections.push({
374
- type: 'text',
375
- content: 'Daemon-hosted sessions',
376
- style: { fg: '240', dim: true, bold: true },
377
- });
378
- const note = hostedRosterNote(modal.hostedRoster);
379
- if (note) {
380
- sections.push({
381
- type: 'text',
382
- content: note,
383
- style: { fg: modal.hostedRoster.note ? UI_TONES.state.warn : '244', dim: !modal.hostedRoster.note },
384
- });
385
- }
386
- if (modal.hostedRoster.sessions.length > 0) {
387
- const rows = modal.hostedRoster.sessions.slice(0, MAX_HOSTED_ROWS);
388
- sections.push({
389
- type: 'list',
390
- items: rows.map((record) => ({
391
- label: hostedRowLabel(record, contentW),
392
- // A terminated hosted session is kept with its reason until retention
393
- // retires it; dimming it keeps it readable without looking live.
394
- style: record.status === 'terminated'
395
- ? { fg: '244', dim: true }
396
- : record.status === 'running' ? { fg: UI_TONES.state.good } : undefined,
397
- })),
398
- });
399
- if (modal.hostedRoster.sessions.length > MAX_HOSTED_ROWS) {
400
- sections.push({
401
- type: 'text',
402
- content: `[showing ${MAX_HOSTED_ROWS} of ${modal.hostedRoster.sessions.length}]`,
403
- style: { fg: '244', dim: true },
404
- });
405
- }
406
- sections.push({
407
- type: 'text',
408
- content: 'Join one with /hosted attach <id>',
409
- style: { fg: '244', dim: true },
410
- });
411
- }
293
+ pushGroup('Today');
294
+ pushGroup('Yesterday');
295
+ pushGroup('This week');
296
+ pushGroup('Earlier');
297
+
298
+ // Hosted and cross-surface sessions are read-only (not selectable), so they
299
+ // are pinned under the saved sessions instead of scrolling with them: a
300
+ // long saved list can never push them out of reach. Past half the body
301
+ // they count what they hide.
302
+ const pinned: KitRow[] = [...hostedRows(modal), ...crossSurfaceRows(modal)];
303
+ const top = f.top + 2;
304
+ let savedBottom = listBottom;
305
+ if (pinned.length > 0) {
306
+ const pinnedNeed = pinned.reduce((sum, row, i) => sum + measureRow(row, f.l, f.r) + (i > 0 && row.header !== undefined ? 1 : 0), 0);
307
+ const pinnedRows = Math.min(pinnedNeed, Math.max(3, Math.floor((listBottom - top + 1) / 2)));
308
+ const pinnedTop = listBottom - pinnedRows + 1;
309
+ drawScrollingList(f.canvas, { rows: pinned, top: pinnedTop, bottom: listBottom, x0: f.l, x1: f.r });
310
+ savedBottom = pinnedTop - 2;
412
311
  }
413
312
 
414
- // Status message if present
415
- if (modal.statusMessage) {
416
- sections.push({ type: 'separator' });
417
- sections.push({
418
- type: 'text',
419
- content: modal.statusMessage,
420
- style: { fg: modal.deleteConfirmationTarget ? '#f59e0b' : '#00ffcc' },
421
- });
422
- }
313
+ modal.setVisibleRows(Math.max(3, savedBottom - top + 1));
314
+ const result = drawList(f.canvas, { rows, top, bottom: savedBottom, x0: f.l, x1: f.r, scrollKey: { owner: modal, name: 'sessions' } });
315
+ f.hintRight = scrollCountText(result.above, result.below);
423
316
 
424
- if (modal.deleteConfirmationTarget) {
425
- sections.push({
426
- type: 'text',
427
- content: `Deletion is armed for ${modal.deleteConfirmationTarget}. Move selection or press Esc to cancel.`,
428
- style: { fg: '244', dim: true },
429
- });
317
+ let y = listBottom + 2;
318
+ for (const line of footLines) {
319
+ drawWrapped(f.canvas, f.l, y, width, line.text, { fg: line.fg }, f.bottom);
320
+ y++;
430
321
  }
431
-
432
- return ModalFactory.createModal(
433
- {
434
- title: 'Sessions',
435
- width: boxW,
436
- margin: boxMargin,
437
- targetContentRows,
438
- sections,
439
- hints: ['[\u2191\u2193] Navigate', '[Enter] Load', '[d] Arm / Delete', '[Esc] Close'],
440
- },
441
- width,
442
- );
322
+ return finishModal(f);
443
323
  }
@@ -9,40 +9,31 @@
9
9
 
10
10
  import type { SettingsModal } from '../input/settings-modal.ts';
11
11
  import { selectedConnectionEntry } from '../input/settings-modal-connections.ts';
12
- import { connectionSurfaceLabel, type ConnectionStatus } from '../input/commands/connection-status.ts';
13
- import { GLYPHS } from './ui-primitives.ts';
14
- import { clamp, padDisplay, stableWindow } from './fullscreen-workspace.ts';
12
+ import { connectionSurfaceLabel } from '../input/commands/connection-status.ts';
13
+ import type { KitRow } from './surface-kit-list.ts';
15
14
 
16
15
  /**
17
- * The Connections rows: one line per surface, saying what it actually is.
16
+ * The Connections rows: one row per surface, saying what it actually is.
18
17
  *
19
18
  * `checking` is rendered as its own state rather than blanked, because a row
20
19
  * that showed nothing while the probe was in flight would read as "nothing
21
20
  * configured", the one thing this category exists to stop claiming falsely.
22
21
  */
23
- export function renderConnectionRows(modal: SettingsModal, width: number, height: number): string[] {
24
- const rows: string[] = [];
22
+ export function connectionRows(modal: SettingsModal, selectable: boolean): KitRow[] {
25
23
  const items = modal.connectionEntries;
26
- if (items.length === 0) return ['No connection surfaces are known to this build.'];
27
- const selectedIndex = clamp(modal.selectedIndex, 0, items.length - 1);
28
- const surfaceWidth = clamp(Math.floor(width * 0.16), 10, 20);
29
- const stateWidth = 12;
30
- const detailWidth = Math.max(16, width - surfaceWidth - stateWidth - 8);
31
- rows.push(` ${padDisplay('Surface', surfaceWidth)} ${padDisplay('State', stateWidth)} ${padDisplay('Detail', detailWidth)}`);
32
- const window = stableWindow(items.length, selectedIndex, Math.max(1, height - 2));
33
- if (window.start > 0) rows.push(`${GLYPHS.navigation.moreAbove} ${window.start} more connection(s) above`);
34
- for (let index = window.start; index < window.end; index += 1) {
35
- const entry = items[index]!;
36
- const selected = index === selectedIndex;
37
- const marker = selected ? (modal.focusPane === 'settings' ? GLYPHS.navigation.selected : '\u2022') : ' ';
38
- rows.push(`${marker} ${padDisplay(connectionSurfaceLabel(entry.surface), surfaceWidth)} ${padDisplay(entry.state, stateWidth)} ${padDisplay(entry.detail, detailWidth)}`);
39
- }
40
- if (window.end < items.length) rows.push(`${GLYPHS.navigation.moreBelow} ${items.length - window.end} more connection(s) below`);
41
- return rows.slice(0, height);
24
+ if (items.length === 0) return [{ label: 'No connection surfaces are known to this build.', muted: true }];
25
+ const selectedIndex = Math.max(0, Math.min(modal.selectedIndex, items.length - 1));
26
+ return items.map((entry, index) => ({
27
+ label: connectionSurfaceLabel(entry.surface),
28
+ desc: entry.detail,
29
+ right: entry.state,
30
+ selected: selectable && index === selectedIndex,
31
+ current: !selectable && index === selectedIndex,
32
+ }));
42
33
  }
43
34
 
44
35
  /**
45
- * The detail pane for one connection: the state, what it means, and every next
36
+ * The detail for one connection: the state, what it means, and every next
46
37
  * step verbatim. The next steps are the daemon's requirements, so they are
47
38
  * listed in full rather than summarized, a truncated instruction is a wrong
48
39
  * instruction.