@pellux/goodvibes-tui 2.0.21 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (391) hide show
  1. package/CHANGELOG.md +65 -1
  2. package/README.md +30 -29
  3. package/docs/README.md +8 -5
  4. package/docs/foundation-artifacts/operator-contract.json +1 -1
  5. package/package.json +28 -19
  6. package/src/cli/management.ts +5 -2
  7. package/src/cli/tui-startup.ts +34 -39
  8. package/src/core/agent-view-render.ts +157 -0
  9. package/src/core/approval-alert.ts +49 -11
  10. package/src/core/bookmark-navigation.ts +7 -1
  11. package/src/core/budget-breach-notifier.ts +27 -8
  12. package/src/core/compaction-receipt.ts +1 -1
  13. package/src/core/context-usage.ts +1 -1
  14. package/src/core/conversation-compaction-render.ts +3 -3
  15. package/src/core/conversation-display-only.ts +86 -0
  16. package/src/core/conversation-event-navigation.ts +27 -0
  17. package/src/core/conversation-line-cache.ts +216 -447
  18. package/src/core/conversation-render-context.ts +20 -27
  19. package/src/core/conversation-rendering.ts +157 -575
  20. package/src/core/conversation-splash-state.ts +6 -1
  21. package/src/core/conversation-types.ts +35 -0
  22. package/src/core/conversation-usage.ts +12 -0
  23. package/src/core/conversation.ts +130 -131
  24. package/src/core/long-task-notifier.ts +100 -31
  25. package/src/core/notices.ts +75 -0
  26. package/src/core/pairing-tailscale-gateway.ts +1 -1
  27. package/src/core/process-output-log.ts +143 -0
  28. package/src/core/rewind-receipt.ts +1 -1
  29. package/src/core/session-focus.ts +190 -0
  30. package/src/core/session-resume-core.ts +23 -81
  31. package/src/core/stream-event-wiring.ts +31 -5
  32. package/src/core/system-message-noise.ts +2 -2
  33. package/src/core/system-message-router.ts +16 -18
  34. package/src/core/turn-event-wiring.ts +272 -33
  35. package/src/core/work-tree-focus.ts +290 -0
  36. package/src/core/work-tree-fold-store.ts +143 -0
  37. package/src/core/work-tree-model.ts +799 -0
  38. package/src/core/work-tree-render.ts +384 -0
  39. package/src/core/work-tree-sources.ts +184 -0
  40. package/src/core/work-tree-wiring.ts +176 -0
  41. package/src/core/workstream-notification.ts +36 -6
  42. package/src/core/wrfc-notice-titles.ts +37 -0
  43. package/src/input/agents-modal-text.ts +119 -0
  44. package/src/input/agents-modal.ts +699 -0
  45. package/src/input/bookmark-modal.ts +33 -9
  46. package/src/input/changes-git.ts +251 -0
  47. package/src/input/changes-modal.ts +629 -0
  48. package/src/input/changes-model.ts +125 -0
  49. package/src/input/command-palette-types.ts +41 -0
  50. package/src/input/command-palette.ts +272 -0
  51. package/src/input/command-reference.ts +2 -2
  52. package/src/input/command-registry.ts +77 -35
  53. package/src/input/commands/acp-runtime.ts +19 -9
  54. package/src/input/commands/channel-runtime.ts +2 -2
  55. package/src/input/commands/ci-runtime.ts +1 -1
  56. package/src/input/commands/codebase-runtime.ts +1 -1
  57. package/src/input/commands/config.ts +7 -1
  58. package/src/input/commands/context-window.ts +25 -19
  59. package/src/input/commands/control-room-runtime.ts +18 -18
  60. package/src/input/commands/conversation-runtime.ts +0 -1
  61. package/src/input/commands/cost-runtime.ts +29 -20
  62. package/src/input/commands/diff-runtime.ts +35 -181
  63. package/src/input/commands/eval.ts +1 -1
  64. package/src/input/commands/experience-runtime.ts +3 -7
  65. package/src/input/commands/fresh-conversation.ts +86 -0
  66. package/src/input/commands/git-runtime.ts +5 -22
  67. package/src/input/commands/graph-runtime.ts +1 -1
  68. package/src/input/commands/guidance-runtime.ts +1 -1
  69. package/src/input/commands/health-runtime.ts +8 -9
  70. package/src/input/commands/hooks-runtime.ts +2 -2
  71. package/src/input/commands/hosted-runtime.ts +3 -3
  72. package/src/input/commands/incident-runtime.ts +3 -3
  73. package/src/input/commands/intelligence-runtime.ts +6 -9
  74. package/src/input/commands/knowledge.ts +2 -2
  75. package/src/input/commands/legacy-panel-command.ts +57 -0
  76. package/src/input/commands/local-auth-runtime.ts +8 -9
  77. package/src/input/commands/local-provider-runtime.ts +7 -4
  78. package/src/input/commands/local-runtime.ts +12 -15
  79. package/src/input/commands/local-setup-review.ts +1 -1
  80. package/src/input/commands/local-setup.ts +13 -19
  81. package/src/input/commands/memory.ts +1 -1
  82. package/src/input/commands/notify-runtime.ts +9 -0
  83. package/src/input/commands/operator-runtime.ts +10 -12
  84. package/src/input/commands/permissions-provenance.ts +3 -3
  85. package/src/input/commands/planning-runtime.ts +7 -7
  86. package/src/input/commands/platform-sandbox-runtime.ts +2 -2
  87. package/src/input/commands/platform-services-runtime.ts +1 -1
  88. package/src/input/commands/policy-dispatch.ts +2 -2
  89. package/src/input/commands/policy.ts +3 -3
  90. package/src/input/commands/provider.ts +1 -1
  91. package/src/input/commands/remote-runtime.ts +1 -1
  92. package/src/input/commands/review-runtime.ts +39 -94
  93. package/src/input/commands/rewind-runtime.ts +81 -108
  94. package/src/input/commands/runtime-services.ts +15 -17
  95. package/src/input/commands/services-runtime.ts +2 -2
  96. package/src/input/commands/session-content.ts +21 -5
  97. package/src/input/commands/session-workflow.ts +13 -38
  98. package/src/input/commands/settings-sync-runtime.ts +2 -2
  99. package/src/input/commands/shell-core.ts +19 -4
  100. package/src/input/commands/skills-runtime.ts +2 -2
  101. package/src/input/commands/status-runtime.ts +13 -0
  102. package/src/input/commands/subscription-runtime.ts +1 -1
  103. package/src/input/commands/tasks-runtime.ts +2 -8
  104. package/src/input/commands/work-plan-runtime.ts +4 -4
  105. package/src/input/commands/workstream-attempts.ts +24 -47
  106. package/src/input/commands/workstream-runtime.ts +2 -2
  107. package/src/input/commands/worktree-runtime.ts +3 -3
  108. package/src/input/commands.ts +3 -1
  109. package/src/input/composer-editor.ts +44 -0
  110. package/src/input/concealed-input.ts +1 -1
  111. package/src/input/config-modal-types.ts +20 -8
  112. package/src/input/config-modal.ts +106 -105
  113. package/src/input/confirm-dialog-types.ts +28 -0
  114. package/src/input/confirm-dialog.ts +70 -0
  115. package/src/input/delete-key-policy.ts +3 -3
  116. package/src/input/feed-context-factory.ts +15 -16
  117. package/src/input/handler-command-route.ts +7 -42
  118. package/src/input/handler-content-actions.ts +16 -15
  119. package/src/input/handler-feed-routes.ts +26 -211
  120. package/src/input/handler-feed.ts +68 -95
  121. package/src/input/handler-interactions.ts +5 -3
  122. package/src/input/handler-modal-routes.ts +215 -216
  123. package/src/input/handler-modal-stack.ts +81 -11
  124. package/src/input/handler-modal-token-routes.ts +4 -0
  125. package/src/input/handler-onboarding.ts +0 -1
  126. package/src/input/handler-picker-routes.ts +65 -107
  127. package/src/input/handler-session-view-route.ts +149 -0
  128. package/src/input/handler-shortcuts.ts +16 -154
  129. package/src/input/handler-types.ts +4 -2
  130. package/src/input/handler-ui-state.ts +45 -15
  131. package/src/input/handler-work-tree-route.ts +123 -0
  132. package/src/input/handler.ts +25 -70
  133. package/src/input/keybindings.ts +16 -71
  134. package/src/input/masked-entry-modal.ts +117 -0
  135. package/src/input/mcp-workspace.ts +48 -17
  136. package/src/input/model-picker-filter.ts +8 -0
  137. package/src/input/model-picker-open.ts +99 -0
  138. package/src/input/model-picker-types.ts +1 -1
  139. package/src/input/model-picker.ts +92 -9
  140. package/src/input/notifications-modal.ts +123 -0
  141. package/src/input/overlay-filter.ts +26 -0
  142. package/src/input/profile-picker-modal.ts +37 -8
  143. package/src/input/search.ts +3 -3
  144. package/src/input/selection-modal-queue.ts +2 -0
  145. package/src/input/selection-modal.ts +28 -16
  146. package/src/input/session-picker-modal.ts +33 -8
  147. package/src/input/settings-modal-activation.ts +7 -2
  148. package/src/input/settings-modal-data.ts +11 -6
  149. package/src/input/settings-modal-types.ts +1 -1
  150. package/src/input/settings-modal.ts +20 -20
  151. package/src/input/submission-intent.ts +1 -1
  152. package/src/input/submission-router.ts +4 -9
  153. package/src/input/surface-modal-host.ts +110 -0
  154. package/src/input/theme-settings-actions.ts +82 -0
  155. package/src/input/usage-modal.ts +183 -0
  156. package/src/input/views.ts +97 -0
  157. package/src/main.ts +175 -165
  158. package/src/permissions/hunk-selection.ts +1 -1
  159. package/src/permissions/prompt-card.ts +511 -0
  160. package/src/permissions/prompt.ts +59 -622
  161. package/src/renderer/agents-modal.ts +310 -0
  162. package/src/renderer/autocomplete-overlay.ts +42 -220
  163. package/src/renderer/back-to-bottom.ts +80 -0
  164. package/src/renderer/block-actions-overlay.ts +30 -48
  165. package/src/renderer/bookmark-modal.ts +48 -79
  166. package/src/renderer/changes-modal.ts +325 -0
  167. package/src/renderer/code-block.ts +184 -106
  168. package/src/renderer/command-palette.ts +93 -0
  169. package/src/renderer/composer-fragments.ts +4 -4
  170. package/src/renderer/composer.ts +146 -0
  171. package/src/renderer/compositor.ts +26 -160
  172. package/src/renderer/config-modal.ts +185 -81
  173. package/src/renderer/confirm-dialog.ts +50 -0
  174. package/src/renderer/context-inspector.ts +116 -154
  175. package/src/renderer/context-status-hint.ts +4 -4
  176. package/src/renderer/conversation-layout.ts +20 -0
  177. package/src/renderer/conversation-overlays.ts +94 -84
  178. package/src/renderer/conversation-surface.ts +19 -38
  179. package/src/renderer/diff-tint.ts +98 -0
  180. package/src/renderer/file-picker-overlay.ts +63 -90
  181. package/src/renderer/footer-targets.ts +38 -0
  182. package/src/renderer/git-status.ts +3 -4
  183. package/src/renderer/header-line.ts +140 -117
  184. package/src/renderer/help-overlay.ts +133 -219
  185. package/src/renderer/history-search-overlay.ts +15 -12
  186. package/src/renderer/lane-graph/bead.ts +800 -0
  187. package/src/renderer/lane-graph/glyphs.ts +134 -0
  188. package/src/renderer/lane-graph/layout.ts +248 -0
  189. package/src/renderer/lane-graph/paint.ts +537 -0
  190. package/src/renderer/lane-graph/semantic-memo.ts +77 -0
  191. package/src/renderer/layout-engine.ts +2 -57
  192. package/src/renderer/layout.ts +7 -13
  193. package/src/renderer/markdown-inline.ts +2 -1
  194. package/src/renderer/markdown-table.ts +17 -12
  195. package/src/renderer/markdown.ts +92 -34
  196. package/src/renderer/masked-entry-modal.ts +78 -0
  197. package/src/renderer/mcp-workspace.ts +166 -203
  198. package/src/renderer/model-workspace.ts +381 -436
  199. package/src/renderer/notifications-modal.ts +116 -0
  200. package/src/renderer/onboarding/onboarding-wizard.ts +178 -588
  201. package/src/renderer/process-view.ts +192 -0
  202. package/src/renderer/profile-picker-modal.ts +58 -110
  203. package/src/renderer/search-overlay.ts +6 -3
  204. package/src/renderer/selection-modal-overlay.ts +87 -327
  205. package/src/renderer/semantic-diff.ts +2 -2
  206. package/src/renderer/session-chips.ts +102 -0
  207. package/src/renderer/session-picker-modal.ts +177 -297
  208. package/src/renderer/settings-modal-connections.ts +14 -23
  209. package/src/renderer/settings-modal-context.ts +367 -0
  210. package/src/renderer/settings-modal-helpers.ts +6 -11
  211. package/src/renderer/settings-modal.ts +322 -726
  212. package/src/renderer/shell-surface.ts +277 -175
  213. package/src/renderer/shortcuts-overlay.ts +217 -0
  214. package/src/renderer/status-line.ts +475 -0
  215. package/src/renderer/surface-compose.ts +174 -0
  216. package/src/renderer/surface-kit-extra.ts +303 -0
  217. package/src/renderer/surface-kit-list.ts +366 -0
  218. package/src/renderer/surface-kit-parts.ts +360 -0
  219. package/src/renderer/surface-kit.ts +568 -0
  220. package/src/renderer/syntax-highlighter.ts +280 -197
  221. package/src/renderer/syntax-theme.ts +51 -0
  222. package/src/renderer/system-message.ts +0 -1
  223. package/src/renderer/term-caps.ts +42 -1
  224. package/src/renderer/terminal-bg-probe.ts +116 -19
  225. package/src/renderer/terminal-palette-probe.ts +322 -0
  226. package/src/renderer/terminal-palette-reader.ts +183 -0
  227. package/src/renderer/terminal-palette.ts +58 -0
  228. package/src/renderer/theme-mode-config.ts +16 -1
  229. package/src/renderer/theme.ts +234 -207
  230. package/src/renderer/thinking.ts +0 -1
  231. package/src/renderer/throbber.ts +188 -0
  232. package/src/renderer/toast-center.ts +119 -0
  233. package/src/renderer/ui-factory.ts +39 -602
  234. package/src/renderer/ui-primitives.ts +8 -25
  235. package/src/renderer/usage-modal.ts +353 -0
  236. package/src/renderer/user-message.ts +58 -0
  237. package/src/renderer/voice-capture-chip.ts +53 -0
  238. package/src/runtime/bootstrap-command-context.ts +0 -5
  239. package/src/runtime/bootstrap-command-parts.ts +38 -89
  240. package/src/runtime/bootstrap-core.ts +13 -15
  241. package/src/runtime/bootstrap-hook-bridge.ts +12 -21
  242. package/src/runtime/bootstrap-notifier-sync.ts +17 -1
  243. package/src/runtime/bootstrap-shell.ts +16 -50
  244. package/src/runtime/bootstrap.ts +14 -14
  245. package/src/runtime/client/fleet-union.ts +5 -5
  246. package/src/runtime/client/hosted-exit.ts +2 -2
  247. package/src/runtime/client/hosted-sessions.ts +1 -1
  248. package/src/runtime/client/spine-adoption.ts +1 -1
  249. package/src/runtime/context-accounting-source.ts +5 -4
  250. package/src/runtime/index.ts +1 -0
  251. package/src/runtime/notification-dispatch.ts +102 -29
  252. package/src/runtime/recovery-offer-wiring.ts +4 -1
  253. package/src/runtime/recovery-prompt.ts +31 -15
  254. package/src/runtime/runtime-services-types.ts +10 -6
  255. package/src/runtime/services.ts +17 -12
  256. package/src/runtime/session-continuity-hints.ts +2 -5
  257. package/src/runtime/session-inbound-inputs.ts +1 -1
  258. package/src/runtime/session-resume-liveness-confirm.ts +14 -2
  259. package/src/runtime/startup-typeahead-gate.ts +103 -0
  260. package/src/runtime/store/domains/domain-read-matrix.ts +0 -1
  261. package/src/runtime/store/domains/index.ts +0 -8
  262. package/src/runtime/store/selectors/index.ts +0 -28
  263. package/src/runtime/store/state.ts +0 -8
  264. package/src/runtime/ui-services.ts +4 -6
  265. package/src/runtime/usage-tracker.ts +348 -0
  266. package/src/runtime/wrfc-persistence.ts +4 -4
  267. package/src/shell/blocking-input.ts +92 -2
  268. package/src/shell/recovery-input-helpers.ts +1 -1
  269. package/src/shell/session-views.ts +539 -0
  270. package/src/shell/status-report.ts +86 -0
  271. package/src/shell/transcript-scroll.ts +68 -0
  272. package/src/shell/ui-openers.ts +84 -172
  273. package/src/shell/view-openers.ts +205 -0
  274. package/src/tools/wrfc-agent-guard.ts +22 -2
  275. package/src/utils/format-duration.ts +4 -8
  276. package/src/utils/splash-lines.ts +9 -1
  277. package/src/utils/terminal-width.ts +56 -0
  278. package/src/verification/live-verifier.ts +0 -10
  279. package/src/version.ts +1 -1
  280. package/src/{panels → views}/agent-inspector-shared.ts +2 -2
  281. package/src/{panels → views}/builtin-modals.ts +22 -21
  282. package/src/views/builtin-views.ts +135 -0
  283. package/src/{panels → views}/confirm-state.ts +9 -9
  284. package/src/{panels → views}/diff-review-model.ts +10 -3
  285. package/src/{panels → views}/eval-registry.ts +1 -1
  286. package/src/{panels → views}/fleet-acts.ts +49 -85
  287. package/src/{panels/fleet-panel-format.ts → views/fleet-format.ts} +40 -40
  288. package/src/{panels → views}/fleet-gateway.ts +5 -5
  289. package/src/{panels → views}/fleet-observed-render.ts +16 -16
  290. package/src/{panels → views}/fleet-read-model.ts +9 -9
  291. package/src/{panels/fleet-tabs.ts → views/fleet-session-tabs.ts} +13 -13
  292. package/src/{panels → views}/fleet-spawn.ts +33 -49
  293. package/src/{panels → views}/fleet-steer.ts +12 -12
  294. package/src/{panels → views}/fleet-stop.ts +4 -4
  295. package/src/{panels → views}/fleet-transcript.ts +26 -27
  296. package/src/{panels/fleet-panel-worktree-detail.ts → views/fleet-worktree-detail.ts} +7 -7
  297. package/src/{panels → views}/hosted-session-feed.ts +6 -6
  298. package/src/views/modal-surface-registry.ts +41 -0
  299. package/src/{panels → views}/modals/devices-modal.ts +2 -2
  300. package/src/{panels → views}/modals/hooks-modal.ts +13 -13
  301. package/src/{panels → views}/modals/keybindings-modal.ts +19 -27
  302. package/src/{panels → views}/modals/knowledge-modal.ts +6 -5
  303. package/src/{panels → views}/modals/local-auth-modal.ts +22 -21
  304. package/src/{panels → views}/modals/marketplace-modal.ts +11 -11
  305. package/src/{panels → views}/modals/memory-modal.ts +8 -8
  306. package/src/{panels → views}/modals/modal-surface-helpers.ts +15 -5
  307. package/src/{panels → views}/modals/pairing-modal.ts +9 -9
  308. package/src/{panels → views}/modals/planning-modal.ts +15 -20
  309. package/src/{panels → views}/modals/plugins-modal.ts +6 -6
  310. package/src/{panels → views}/modals/policy-modal.ts +13 -5
  311. package/src/{panels → views}/modals/provider-health-modal.ts +4 -4
  312. package/src/{panels → views}/modals/remote-modal.ts +3 -3
  313. package/src/{panels → views}/modals/sandbox-modal.ts +4 -4
  314. package/src/{panels → views}/modals/security-modal.ts +22 -16
  315. package/src/{panels → views}/modals/services-modal.ts +3 -3
  316. package/src/{panels → views}/modals/settings-sync-modal.ts +4 -4
  317. package/src/{panels → views}/modals/skills-modal.ts +9 -9
  318. package/src/{panels → views}/modals/subscription-modal.ts +3 -3
  319. package/src/{panels → views}/modals/work-plan-modal.ts +10 -13
  320. package/src/views/notifications-feed.ts +314 -0
  321. package/src/{panels → views}/polish-core.ts +38 -36
  322. package/src/{panels → views}/polish-tables.ts +35 -26
  323. package/src/{panels → views}/polish.ts +101 -95
  324. package/src/{panels → views}/project-planning-answer-actions.ts +3 -3
  325. package/src/{panels → views}/provider-health-routes.ts +14 -14
  326. package/src/{panels → views}/search-focus.ts +8 -8
  327. package/src/{panels → views}/session-maintenance.ts +11 -11
  328. package/src/views/skills-discovery.ts +159 -0
  329. package/src/views/view-deps.ts +80 -0
  330. package/src/views/view-panel-adapter.ts +53 -0
  331. package/src/{panels → views}/workstream-graph-render.ts +1 -1
  332. package/src/core/conversation-fold.ts +0 -101
  333. package/src/core/conversation-turn-structure.ts +0 -464
  334. package/src/core/conversation-user-receipts.ts +0 -42
  335. package/src/input/commands/operator-panel-runtime.ts +0 -186
  336. package/src/input/panel-integration-actions.ts +0 -33
  337. package/src/input/panel-mouse-geometry.ts +0 -97
  338. package/src/input/panel-paste-flood-guard.ts +0 -86
  339. package/src/panels/base-panel.ts +0 -301
  340. package/src/panels/builtin/agent.ts +0 -27
  341. package/src/panels/builtin/development.ts +0 -92
  342. package/src/panels/builtin/knowledge.ts +0 -12
  343. package/src/panels/builtin/operations.ts +0 -177
  344. package/src/panels/builtin/session.ts +0 -81
  345. package/src/panels/builtin/shared.ts +0 -357
  346. package/src/panels/builtin-panels.ts +0 -28
  347. package/src/panels/cost-tracker-panel.ts +0 -746
  348. package/src/panels/diff-panel.ts +0 -797
  349. package/src/panels/diff-review-panel.ts +0 -395
  350. package/src/panels/expandable-list-panel.ts +0 -189
  351. package/src/panels/fleet-deep-link.ts +0 -31
  352. package/src/panels/fleet-panel.ts +0 -799
  353. package/src/panels/git-panel.ts +0 -800
  354. package/src/panels/hosted-session-panel.ts +0 -221
  355. package/src/panels/index.ts +0 -21
  356. package/src/panels/local-auth-panel.ts +0 -478
  357. package/src/panels/notifications-feed.ts +0 -127
  358. package/src/panels/notifications-panel.ts +0 -160
  359. package/src/panels/panel-confirm-overlay.ts +0 -72
  360. package/src/panels/panel-manager.ts +0 -791
  361. package/src/panels/plugins-panel.ts +0 -371
  362. package/src/panels/scrollable-list-panel.ts +0 -498
  363. package/src/panels/skills-panel.ts +0 -454
  364. package/src/panels/token-budget-panel.ts +0 -688
  365. package/src/panels/types.ts +0 -191
  366. package/src/panels/wrfc-panel-format.ts +0 -133
  367. package/src/renderer/diff-view.ts +0 -134
  368. package/src/renderer/fleet-tab-strip.ts +0 -56
  369. package/src/renderer/footer-tips.ts +0 -53
  370. package/src/renderer/fullscreen-primitives.ts +0 -146
  371. package/src/renderer/fullscreen-workspace.ts +0 -194
  372. package/src/renderer/hint-grammar.ts +0 -52
  373. package/src/renderer/modal-factory.ts +0 -480
  374. package/src/renderer/overlay-box.ts +0 -154
  375. package/src/renderer/overlay-viewport.ts +0 -104
  376. package/src/renderer/panel-composite.ts +0 -145
  377. package/src/renderer/panel-workspace-bar.ts +0 -54
  378. package/src/renderer/process-indicator.ts +0 -163
  379. package/src/renderer/tab-strip.ts +0 -222
  380. package/src/renderer/text-layout.ts +0 -31
  381. package/src/renderer/tool-call.ts +0 -276
  382. package/src/renderer/tool-result-expanded-lines.ts +0 -48
  383. package/src/renderer/tool-result-summary.ts +0 -219
  384. package/src/runtime/diagnostics/panels/index.ts +0 -21
  385. package/src/runtime/diagnostics/panels/ops.ts +0 -156
  386. package/src/runtime/perf/panel-contracts.ts +0 -32
  387. package/src/runtime/store/domains/panels.ts +0 -117
  388. package/src/verification/verification-ledger.ts +0 -707
  389. /package/src/{panels → views}/memory-consolidation-gateway.ts +0 -0
  390. /package/src/{panels → views}/modals/modal-theme.ts +0 -0
  391. /package/src/{panels → views}/provider-health-domains.ts +0 -0
@@ -1,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
- }