@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,478 +0,0 @@
1
- import type { Line } from '@pellux/goodvibes-sdk/platform/types';
2
- import { createEmptyLine } from '@pellux/goodvibes-sdk/platform/types';
3
- import { fitDisplay, truncateDisplay } from '../utils/terminal-width.ts';
4
- import { ScrollableListPanel } from './scrollable-list-panel.ts';
5
- import {
6
- buildDetailBlock,
7
- buildGuidanceLine,
8
- buildKeyboardHints,
9
- buildPanelListRow,
10
- buildPanelLine,
11
- buildSummaryBlock,
12
- buildPanelWorkspace,
13
- DEFAULT_PANEL_PALETTE,
14
- type PanelPalette,
15
- } from './polish.ts';
16
- import type { LocalAuthSnapshot, UserAuthManager } from '@pellux/goodvibes-sdk/platform/security';
17
- import type { LocalAuthInspectionQuery } from '@/runtime/index.ts';
18
- import type { KeyName } from './types.ts';
19
- import { isTextBackspace } from '../input/delete-key-policy.ts';
20
- import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
21
- import { type ConfirmState, handleConfirmInput } from './confirm-state.ts';
22
-
23
- // Base chrome only, state colors and text tokens come straight from
24
- // DEFAULT_PANEL_PALETTE.
25
- const C = DEFAULT_PANEL_PALETTE;
26
-
27
- function formatRoles(roles: readonly string[]): string {
28
- return roles.length > 0 ? roles.join(', ') : '(none)';
29
- }
30
-
31
- type LocalAuthUser = LocalAuthSnapshot['users'][number];
32
-
33
- /** Action kind for the masked password entry mode. */
34
- export type MaskedEntryKind = 'add-user' | 'rotate-password';
35
-
36
- interface MaskedEntryState {
37
- readonly kind: MaskedEntryKind;
38
- readonly username: string;
39
- readonly auth: UserAuthManager;
40
- buffer: string;
41
- }
42
-
43
- /** Mutation surface the panel needs for p/a/d/b, a subset of UserAuthManager. */
44
- type LocalAuthMutations = Pick<UserAuthManager, 'addUser' | 'deleteUser' | 'rotatePassword' | 'clearBootstrapCredentialFile'>;
45
-
46
- function hasLocalAuthMutations(value: LocalAuthInspectionQuery): value is LocalAuthInspectionQuery & LocalAuthMutations {
47
- const candidate = value as Partial<LocalAuthMutations>;
48
- return typeof candidate.addUser === 'function'
49
- && typeof candidate.deleteUser === 'function'
50
- && typeof candidate.rotatePassword === 'function'
51
- && typeof candidate.clearBootstrapCredentialFile === 'function';
52
- }
53
-
54
- export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
55
- private readonly authManager: LocalAuthInspectionQuery;
56
- private maskedState: MaskedEntryState | null = null;
57
- /** Pending delete-user confirmation, project-standard ConfirmState contract. */
58
- private deleteConfirm: ConfirmState<string> | null = null;
59
- /** Draft username buffer for the 'a' add-user flow's first step (username,
60
- * then openMaskedEntry for the password). Not masked, usernames aren't secret. */
61
- private usernameEntry: string | null = null;
62
- /** Cached inspect() snapshot for the current render pass, see render(). */
63
- private cachedSnapshot: LocalAuthSnapshot | null = null;
64
-
65
- public constructor(authManager: LocalAuthInspectionQuery) {
66
- super('local-auth', 'Local Auth', 'U', 'security-policy');
67
- this.showSelectionGutter = true; // I5: non-color selection affordance
68
- this.authManager = authManager;
69
- }
70
-
71
- /**
72
- * The full mutation surface (addUser/deleteUser/rotatePassword/
73
- * clearBootstrapCredentialFile), when the constructor's authManager
74
- * happens to be the real UserAuthManager rather than the narrower
75
- * read-only LocalAuthInspectionQuery some callers (and tests) supply.
76
- * Real production wiring (builtin/operations.ts) always passes the full
77
- * manager; test doubles that implement only inspect() correctly get null
78
- * here, and the p/a/d/b keys surface a clear error instead of throwing.
79
- */
80
- private get mutations(): LocalAuthMutations | null {
81
- return hasLocalAuthMutations(this.authManager) ? this.authManager : null;
82
- }
83
-
84
- /**
85
- * Activate masked password-entry mode for the given operation.
86
- * The panel's handleInput will capture keystrokes into a private buffer;
87
- * no plaintext is ever recorded in input history, transcript, or logs.
88
- */
89
- public openMaskedEntry(kind: MaskedEntryKind, username: string, auth: UserAuthManager): void {
90
- this.maskedState = { kind, username, auth, buffer: '' };
91
- this.invalidate();
92
- }
93
-
94
- /** Returns true when the panel is in masked-entry mode. */
95
- public get isMaskedEntryActive(): boolean {
96
- return this.maskedState !== null;
97
- }
98
-
99
- /**
100
- * Masked password/username entry wants every character of a burst (paste,
101
- * or fast typing landing in one input.feed() call) delivered one at a
102
- * time, same as it always has, see the interface doc on
103
- * `Panel.isCapturingTextBurst`.
104
- */
105
- public override isCapturingTextBurst(): boolean {
106
- return this.isMaskedEntryActive || this.usernameEntry !== null || super.isCapturingTextBurst();
107
- }
108
-
109
- public override handleInput(key: KeyName): boolean {
110
- // Masked entry takes priority when active, it must capture every
111
- // keystroke (including letters that would otherwise be p/a/d/b actions)
112
- // as part of the password being typed.
113
- if (this.maskedState !== null) {
114
- return this.handleMaskedEntryInput(key);
115
- }
116
-
117
- if (this.usernameEntry !== null) {
118
- return this.handleUsernameEntryInput(key);
119
- }
120
-
121
- if (this.deleteConfirm !== null) {
122
- return this.handleDeleteConfirmInput(key);
123
- }
124
-
125
- const selected = this.getSelectedItem();
126
-
127
- if (key === 'p') {
128
- if (!selected) return false;
129
- const mutations = this.mutations;
130
- if (!mutations) {
131
- this.setError('Local auth mutations are not available in this session.');
132
- return true;
133
- }
134
- this.openMaskedEntry('rotate-password', selected.username, mutations as unknown as UserAuthManager);
135
- return true;
136
- }
137
-
138
- if (key === 'a') {
139
- const mutations = this.mutations;
140
- if (!mutations) {
141
- this.setError('Local auth mutations are not available in this session.');
142
- return true;
143
- }
144
- this.usernameEntry = '';
145
- this.invalidate();
146
- return true;
147
- }
148
-
149
- if (key === 'd') {
150
- if (!selected) return false;
151
- this.deleteConfirm = { subject: selected.username, label: selected.username };
152
- this.invalidate();
153
- return true;
154
- }
155
-
156
- if (key === 'b') {
157
- const snapshot = this.cachedSnapshot ?? this.authManager.inspect();
158
- if (!snapshot.bootstrapCredentialPresent) return false;
159
- const mutations = this.mutations;
160
- if (!mutations) {
161
- this.setError('Local auth mutations are not available in this session.');
162
- return true;
163
- }
164
- const cleared = mutations.clearBootstrapCredentialFile();
165
- if (cleared) {
166
- this.cachedSnapshot = null; // force a fresh inspect() on the next render
167
- } else {
168
- this.setError('No bootstrap credential file was present.');
169
- }
170
- this.invalidate();
171
- return true;
172
- }
173
-
174
- // Delegate scroll/selection to ScrollableListPanel for everything else.
175
- return super.handleInput?.(key) ?? false;
176
- }
177
-
178
- private handleDeleteConfirmInput(key: KeyName): boolean {
179
- const result = handleConfirmInput(this.deleteConfirm, key);
180
- if (result === 'confirmed') {
181
- const username = this.deleteConfirm!.subject;
182
- this.deleteConfirm = null;
183
- const mutations = this.mutations;
184
- if (mutations) {
185
- try {
186
- const deleted = mutations.deleteUser(username);
187
- if (deleted) this.cachedSnapshot = null; // force a fresh inspect() on the next render
188
- else this.setError(`Unknown local auth user: ${username}`);
189
- } catch (error) {
190
- this.setError(summarizeError(error));
191
- }
192
- } else {
193
- this.setError('Local auth mutations are not available in this session.');
194
- }
195
- this.invalidate();
196
- return true;
197
- }
198
- if (result === 'cancelled') {
199
- this.deleteConfirm = null;
200
- this.invalidate();
201
- return true;
202
- }
203
- // 'absorbed' (or defensively, 'inactive', deleteConfirm is non-null here so this never fires)
204
- return true;
205
- }
206
-
207
- private handleUsernameEntryInput(key: KeyName): boolean {
208
- if (key === 'escape') {
209
- this.usernameEntry = null;
210
- this.invalidate();
211
- return true;
212
- }
213
- if (key === 'enter' || key === 'return') {
214
- const username = (this.usernameEntry ?? '').trim();
215
- this.usernameEntry = null;
216
- if (username.length === 0) {
217
- this.invalidate();
218
- return true; // no-op on empty username, mirroring the masked-entry empty-buffer no-op
219
- }
220
- const mutations = this.mutations;
221
- if (mutations) {
222
- this.openMaskedEntry('add-user', username, mutations as unknown as UserAuthManager);
223
- } else {
224
- this.setError('Local auth mutations are not available in this session.');
225
- this.invalidate();
226
- }
227
- return true;
228
- }
229
- if (isTextBackspace(key)) {
230
- if (this.usernameEntry && this.usernameEntry.length > 0) {
231
- this.usernameEntry = this.usernameEntry.slice(0, -1);
232
- this.invalidate();
233
- }
234
- return true;
235
- }
236
- if (key.length === 1) {
237
- this.usernameEntry = (this.usernameEntry ?? '') + key;
238
- this.invalidate();
239
- return true;
240
- }
241
- return true; // absorb everything else while username entry is active
242
- }
243
-
244
- private handleMaskedEntryInput(key: KeyName): boolean {
245
- // Masked entry is active, capture all keystrokes.
246
- const state = this.maskedState!;
247
-
248
- if (key === 'escape') {
249
- // Cancel: discard buffer, exit masked mode without persisting.
250
- this.maskedState = null;
251
- this.invalidate();
252
- return true;
253
- }
254
-
255
- if (key === 'enter' || key === 'return') {
256
- // Submit: call the auth API if a non-empty password was entered.
257
- if (state.buffer.length === 0) {
258
- return true; // no-op, require at least one character
259
- }
260
- const password = state.buffer;
261
- const { kind, username, auth } = state;
262
- // Clear the mutable state *before* the auth call so the secret
263
- // never lingers in the buffer after an exception.
264
- this.maskedState = null;
265
- try {
266
- if (kind === 'add-user') {
267
- auth.addUser(username, password, ['admin']);
268
- } else {
269
- auth.rotatePassword(username, password);
270
- }
271
- } catch (_error) {
272
- // Surface errors via the panel's error facility rather than logging.
273
- this.setError(summarizeError(_error));
274
- }
275
- this.invalidate();
276
- return true;
277
- }
278
-
279
- if (isTextBackspace(key)) {
280
- // Remove last character (Ink 6.8.0: raw-stdin raw 'backspace' only).
281
- if (state.buffer.length > 0) {
282
- state.buffer = state.buffer.slice(0, -1);
283
- this.invalidate();
284
- }
285
- return true;
286
- }
287
-
288
- // Single printable character: append to buffer.
289
- if (key.length === 1) {
290
- state.buffer += key;
291
- this.invalidate();
292
- return true;
293
- }
294
-
295
- // All other named keys are consumed (ignored) while masked mode is active.
296
- return true;
297
- }
298
-
299
- protected override getPalette(): PanelPalette {
300
- return C;
301
- }
302
-
303
- protected getItems(): readonly LocalAuthUser[] {
304
- // Reads the render-pass cache populated by render() below (single
305
- // inspect() per render). Falls back to a fresh inspect() only when
306
- // called before the first render (e.g. directly from a test).
307
- return (this.cachedSnapshot ?? this.authManager.inspect()).users;
308
- }
309
-
310
- protected renderItem(user: LocalAuthUser, _index: number, selected: boolean, width: number): Line {
311
- return buildPanelListRow(width, [
312
- { text: fitDisplay(user.username, 20), fg: C.value },
313
- { text: truncateDisplay(` roles=${formatRoles(user.roles)}`, Math.max(0, width - 24)), fg: C.info },
314
- ], C, { selected });
315
- }
316
-
317
- protected override getEmptyStateMessage(): string {
318
- return ' No local auth users configured.';
319
- }
320
-
321
- public render(width: number, height: number): Line[] {
322
- // When masked entry or username entry is active, render a dedicated
323
- // prompt instead of the normal panel content. No plaintext password
324
- // appears anywhere in output.
325
- if (this.maskedState !== null) {
326
- return this.renderMaskedPrompt(width, height);
327
- }
328
- if (this.usernameEntry !== null) {
329
- return this.renderUsernameEntryPrompt(width, height);
330
- }
331
-
332
- const intro = 'Manage local daemon and HTTP-listener auth users, bootstrap state, and active sessions.';
333
- // Single inspect() call for this whole render pass, getItems() (called
334
- // both directly below and again inside renderList()'s internal
335
- // getVisibleItems()) reads from this cache instead of re-inspecting.
336
- this.cachedSnapshot = this.authManager.inspect();
337
- const snapshot = this.cachedSnapshot;
338
- const users = this.getItems();
339
-
340
- const issueMessages: string[] = [];
341
- if (snapshot.bootstrapCredentialPresent) issueMessages.push('Bootstrap credential file still exists; press b to clear it after password rotation.');
342
- if (snapshot.sessionCount === 0) issueMessages.push('No active local auth sessions are currently tracked.');
343
-
344
- const headerLines: Line[] = [
345
- ...buildSummaryBlock(width, 'Local auth posture', [
346
- buildPanelLine(width, [
347
- [' users ', C.label],
348
- [String(snapshot.userCount), C.value],
349
- [' sessions ', C.label],
350
- [String(snapshot.sessionCount), snapshot.sessionCount > 0 ? C.info : C.dim],
351
- [' bootstrap ', C.label],
352
- [snapshot.bootstrapCredentialPresent ? 'present' : 'cleared', snapshot.bootstrapCredentialPresent ? C.warn : C.good],
353
- ]),
354
- buildPanelLine(width, [[' user store ', C.label], [truncateDisplay(snapshot.userStorePath, Math.max(0, width - 13)), C.dim]]),
355
- buildPanelLine(width, [[' bootstrap file ', C.label], [truncateDisplay(snapshot.bootstrapCredentialPath, Math.max(0, width - 18)), C.dim]]),
356
- ...(issueMessages.length > 0
357
- ? issueMessages.map((issue) => buildPanelLine(width, [[truncateDisplay(` issue: ${issue}`, width), C.warn]]))
358
- : [buildPanelLine(width, [[' local auth posture looks healthy.', C.good]])]),
359
- buildGuidanceLine(width, 'p / a / d', 'rotate password, add user, or delete the selected user via masked in-panel entry', C),
360
- ], C),
361
- ];
362
-
363
- if (users.length === 0) {
364
- const workspace = buildPanelWorkspace(width, height, {
365
- title: 'Local Auth Control Room',
366
- intro,
367
- sections: [{ lines: headerLines }],
368
- footerLines: [this.buildHintsLine(width, null)],
369
- palette: C,
370
- });
371
- while (workspace.length < height) workspace.push(createEmptyLine(width));
372
- return workspace.slice(0, height);
373
- }
374
-
375
- this.clampSelection();
376
- const selected = this.getSelectedItem();
377
-
378
- const footerLines: Line[] = [];
379
- if (selected) {
380
- const deleteActive = this.deleteConfirm?.subject === selected.username;
381
- footerLines.push(
382
- ...buildDetailBlock(width, 'Selected user', [
383
- buildPanelLine(width, [[' username ', C.label], [selected.username, C.value], [' roles ', C.label], [truncateDisplay(formatRoles(selected.roles), Math.max(0, width - 23)), C.info]]),
384
- deleteActive
385
- ? buildPanelLine(width, [[` Delete ${selected.username}? Press y or Enter to confirm, n or Esc to cancel.`, C.warn]])
386
- : buildPanelLine(width, [[' p: rotate password a: add user d: delete user', C.dim]]),
387
- ], C),
388
- );
389
- }
390
-
391
- if (snapshot.sessions.length > 0) {
392
- footerLines.push(buildPanelLine(width, [[` Active sessions (${snapshot.sessions.length})`, C.label]]));
393
- footerLines.push(
394
- ...snapshot.sessions.slice(0, 8).map((session) => buildPanelLine(width, [
395
- [' ', C.label],
396
- [fitDisplay(session.username, 18), C.value],
397
- [truncateDisplay(` expires ${new Date(session.expiresAt).toLocaleString()}`, Math.max(0, width - 20)), C.dim],
398
- ])),
399
- );
400
- }
401
-
402
- return this.renderList(width, height, {
403
- title: 'Local Auth Control Room',
404
- header: headerLines,
405
- footer: footerLines,
406
- hints: this.buildHints(selected ?? null, snapshot.bootstrapCredentialPresent),
407
- });
408
- }
409
-
410
- /** Footer keyboard hints, adapted to the current state, confirm-pending
411
- * vs. normal browsing, and whether there's a bootstrap credential to clear. */
412
- private buildHints(
413
- selected: LocalAuthUser | null,
414
- bootstrapCredentialPresent: boolean,
415
- ): ReadonlyArray<{ keys: string; label: string }> {
416
- if (this.deleteConfirm) {
417
- return [
418
- { keys: 'y/Enter', label: 'confirm delete' },
419
- { keys: 'n/Esc', label: 'cancel' },
420
- ];
421
- }
422
- const hints: Array<{ keys: string; label: string }> = [{ keys: '↑/↓', label: 'select user' }];
423
- if (selected) {
424
- hints.push({ keys: 'p', label: 'rotate password' }, { keys: 'd', label: 'delete user' });
425
- }
426
- hints.push({ keys: 'a', label: 'add user' });
427
- if (bootstrapCredentialPresent) hints.push({ keys: 'b', label: 'clear bootstrap credential' });
428
- return hints;
429
- }
430
-
431
- private buildHintsLine(width: number, selected: LocalAuthUser | null): Line {
432
- return buildKeyboardHints(width, this.buildHints(selected, false), C);
433
- }
434
-
435
- private renderUsernameEntryPrompt(width: number, height: number): Line[] {
436
- const draft = this.usernameEntry ?? '';
437
- const promptLines: Line[] = [
438
- buildPanelLine(width, [[' Add local auth user: enter a username, then set a masked password.', C.value]]),
439
- buildPanelLine(width, [['', C.label]]),
440
- buildPanelLine(width, [[' Username ', C.label], [`${draft}█`, C.info]]),
441
- buildPanelLine(width, [['', C.label]]),
442
- buildPanelLine(width, [[' [Enter] Continue to password entry [Esc] Cancel [Backspace] Delete char', C.dim]]),
443
- ];
444
- const workspace = buildPanelWorkspace(width, height, {
445
- title: 'Local Auth: Add User',
446
- intro: 'Type the new username and press Enter to continue to masked password entry.',
447
- sections: [{ lines: promptLines }],
448
- palette: C,
449
- });
450
- while (workspace.length < height) workspace.push(createEmptyLine(width));
451
- return workspace.slice(0, height);
452
- }
453
-
454
- private renderMaskedPrompt(width: number, height: number): Line[] {
455
- const state = this.maskedState!;
456
- const actionLabel = state.kind === 'add-user' ? 'Add user' : 'Rotate password';
457
- const dots = '•'.repeat(Math.min(32, state.buffer.length));
458
- const cursor = '█'; // block cursor
459
- const maskedDisplay = state.buffer.length > 0 ? `${dots}${cursor}` : cursor;
460
-
461
- const promptLines: Line[] = [
462
- buildPanelLine(width, [[` ${actionLabel}: ${state.username}`, C.value]]),
463
- buildPanelLine(width, [['', C.label]]),
464
- buildPanelLine(width, [[' Password ', C.label], [truncateDisplay(maskedDisplay, Math.max(0, width - 12)), C.info]]),
465
- buildPanelLine(width, [['', C.label]]),
466
- buildPanelLine(width, [[' [Enter] Confirm [Esc] Cancel [Backspace] Delete char', C.dim]]),
467
- ];
468
-
469
- const workspace = buildPanelWorkspace(width, height, {
470
- title: 'Local Auth: Password Entry',
471
- intro: `Type a password for ${state.username}. The value is never echoed in plaintext or stored in history.`,
472
- sections: [{ lines: promptLines }],
473
- palette: C,
474
- });
475
- while (workspace.length < height) workspace.push(createEmptyLine(width));
476
- return workspace;
477
- }
478
- }
@@ -1,127 +0,0 @@
1
- /**
2
- * PanelNotificationFeed, the read-model behind the `panel_only` notification
3
- * target.
4
- *
5
- * The SDK's NotificationRouter (`@/runtime/index.ts` -> platform runtime `ui`
6
- * barrel) can route a notification's `RoutingDecision.target` to
7
- * `'panel_only'`, meaning: don't surface this in the conversation or the
8
- * status bar, but it must still reach a real, visible place. Before this
9
- * file, nothing in this repo consumed that target outside of tests, a
10
- * panel-routed notification (including every burst- or batch-collapsed one)
11
- * had nowhere to go. This feed is that place: a caller holding a routed
12
- * `Notification` + `RoutingDecision` pair calls `record()`, and
13
- * `notifications-panel.ts` renders whatever has accumulated.
14
- *
15
- * Collapsed groups (reasonCode `burst_collapsed` / `batch_window_collapsed`)
16
- * are tracked by their `batchKey` and accumulate an honest running count
17
- * instead of rendering one row per suppressed duplicate.
18
- *
19
- * Production wiring, something in the running app actually calling
20
- * `record()` with live notifications, rides the SDK round that turns on
21
- * adaptive notification suppression by default; this feed and its panel are
22
- * the render target that work lands on, built ahead of it so the target is
23
- * never missing when that switch flips.
24
- */
25
- import type { Notification, RoutingDecision } from '@/runtime/index.ts';
26
-
27
- export interface PanelFeedEntry {
28
- readonly key: string;
29
- readonly domain: string;
30
- readonly level: Notification['level'];
31
- readonly title: string;
32
- readonly body: string | undefined;
33
- /** Unix ms of the most recent notification folded into this entry. */
34
- readonly timestamp: number;
35
- readonly reasonCode: RoutingDecision['reasonCode'];
36
- /** How many notifications this entry represents. 1 for a standalone item; >1 for a collapsed group. Always the true count, never estimated. */
37
- readonly collapsedCount: number;
38
- }
39
-
40
- const MAX_ENTRIES = 200;
41
-
42
- /** Reason codes whose notifications fold into one running-count entry per batch key, rather than one entry per occurrence. */
43
- const COLLAPSING_REASON_CODES: ReadonlySet<RoutingDecision['reasonCode']> = new Set([
44
- 'burst_collapsed',
45
- 'batch_window_collapsed',
46
- ]);
47
-
48
- export class PanelNotificationFeed {
49
- private readonly entries = new Map<string, PanelFeedEntry>();
50
- /** Insertion order of `entries` keys (oldest first), for bounded eviction. */
51
- private order: string[] = [];
52
- private readonly listeners = new Set<() => void>();
53
-
54
- /**
55
- * Record a routed notification. Only notifications actually targeted at
56
- * `panel_only` are kept, a caller passing anything else is a mistake (the
57
- * router sent it elsewhere), so it's dropped rather than shown in the
58
- * wrong place.
59
- */
60
- record(notification: Notification, decision: RoutingDecision): void {
61
- if (decision.target !== 'panel_only') return;
62
-
63
- const collapsing = COLLAPSING_REASON_CODES.has(decision.reasonCode) && Boolean(decision.batchKey);
64
- const key = collapsing ? `group:${decision.batchKey}` : `single:${notification.id}`;
65
- const previousCount = collapsing ? (this.entries.get(key)?.collapsedCount ?? 0) : 0;
66
-
67
- const entry: PanelFeedEntry = {
68
- key,
69
- domain: notification.domain,
70
- level: notification.level,
71
- title: notification.title,
72
- body: notification.body,
73
- timestamp: notification.timestamp,
74
- reasonCode: decision.reasonCode,
75
- collapsedCount: previousCount + 1,
76
- };
77
-
78
- if (!this.entries.has(key)) {
79
- this.order.push(key);
80
- if (this.order.length > MAX_ENTRIES) {
81
- const evicted = this.order.shift();
82
- if (evicted !== undefined) this.entries.delete(evicted);
83
- }
84
- }
85
- this.entries.set(key, entry);
86
- this.emitChange();
87
- }
88
-
89
- /** Every entry, most recently updated first. */
90
- list(): readonly PanelFeedEntry[] {
91
- const items: PanelFeedEntry[] = [];
92
- for (const key of this.order) {
93
- const entry = this.entries.get(key);
94
- if (entry) items.push(entry);
95
- }
96
- return items.reverse();
97
- }
98
-
99
- clear(): void {
100
- this.entries.clear();
101
- this.order = [];
102
- this.emitChange();
103
- }
104
-
105
- /** Subscribe to feed changes (e.g. to mark a panel dirty). Returns an unsubscribe function. */
106
- subscribe(listener: () => void): () => void {
107
- this.listeners.add(listener);
108
- return () => this.listeners.delete(listener);
109
- }
110
-
111
- private emitChange(): void {
112
- for (const listener of this.listeners) listener();
113
- }
114
- }
115
-
116
- let sharedFeed: PanelNotificationFeed | null = null;
117
-
118
- /**
119
- * The process-wide `panel_only` feed. Lazily created so the running app has
120
- * exactly one feed that every future caller and the NotificationsPanel agree
121
- * on, while tests construct their own isolated `PanelNotificationFeed`
122
- * instance instead of reaching for this one.
123
- */
124
- export function getSharedNotificationFeed(): PanelNotificationFeed {
125
- if (!sharedFeed) sharedFeed = new PanelNotificationFeed();
126
- return sharedFeed;
127
- }