@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
@@ -7,22 +7,16 @@
7
7
  * - Falls back to empty array (caller uses regex tokenizer) when parser not ready
8
8
  * - Caches parsed results keyed by language + content hash to avoid re-parsing
9
9
  *
10
- * Vaporwave color theme:
11
- * Keywords: #d000ff (purple)
12
- * Strings: #00ff88 (green)
13
- * Numbers: #ffcc00 (yellow)
14
- * Comments: #666666 (dim grey)
15
- * Functions/methods: #00ffff (cyan)
16
- * Types/classes: #ff6b9d (pink)
17
- * Operators: #ffffff (white)
18
- * Properties: #87ceeb (light blue)
19
- * Built-ins/special: #ff8c00 (orange)
20
- * Default: 252 (light grey)
10
+ * Colours: every node type maps to a syntax role (keyword, string, number,
11
+ * comment, function, type, operator, property, builtin, plain); the active
12
+ * theme's syntax tokens colour each role (syntax-theme.ts). The cache holds
13
+ * roles, so a theme change re-colours cached blocks without re-parsing.
21
14
  */
22
15
  import type { Node } from 'web-tree-sitter';
23
16
  import { TreeSitterService } from '@pellux/goodvibes-sdk/platform/intelligence';
24
17
  import { logger } from '@pellux/goodvibes-sdk/platform/utils';
25
18
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
19
+ import { syntaxStyles, type SyntaxRole, type SyntaxStyle } from './syntax-theme.ts';
26
20
 
27
21
  // ─── Types ───────────────────────────────────────────────────────────────────
28
22
 
@@ -62,193 +56,194 @@ const FENCE_TO_LANG_ID: Record<string, string> = {
62
56
  zsh: 'bash',
63
57
  };
64
58
 
65
- // ─── Vaporwave Color Mapping ─────────────────────────────────────────────────
59
+ // ─── Node Type → Syntax Role ─────────────────────────────────────────────────
66
60
 
67
- // Map tree-sitter node types to vaporwave theme colors.
61
+ // Map tree-sitter node types to syntax roles; syntax-theme.ts turns a role into
62
+ // the active theme's colour (shared with code-block.ts's regex fallback).
68
63
  // The node types are specific to each grammar's output.
69
- const NODE_TYPE_COLORS: Record<string, { fg: string; bold?: boolean; italic?: boolean }> = {
64
+ const NODE_TYPE_COLORS: Record<string, SyntaxRole> = {
70
65
  // ── Keywords
71
- 'if': { fg: '#d000ff', bold: true },
72
- 'else': { fg: '#d000ff', bold: true },
73
- 'return': { fg: '#d000ff', bold: true },
74
- 'const': { fg: '#d000ff', bold: true },
75
- 'let': { fg: '#d000ff', bold: true },
76
- 'var': { fg: '#d000ff', bold: true },
77
- 'function': { fg: '#d000ff', bold: true },
78
- 'class': { fg: '#d000ff', bold: true },
79
- 'import': { fg: '#d000ff', bold: true },
80
- 'export': { fg: '#d000ff', bold: true },
81
- 'from': { fg: '#d000ff', bold: true },
82
- 'new': { fg: '#d000ff', bold: true },
83
- 'typeof': { fg: '#d000ff', bold: true },
84
- 'instanceof': { fg: '#d000ff', bold: true },
85
- 'in': { fg: '#d000ff', bold: true },
86
- 'of': { fg: '#d000ff', bold: true },
87
- 'for': { fg: '#d000ff', bold: true },
88
- 'while': { fg: '#d000ff', bold: true },
89
- 'do': { fg: '#d000ff', bold: true },
90
- 'switch': { fg: '#d000ff', bold: true },
91
- 'case': { fg: '#d000ff', bold: true },
92
- 'break': { fg: '#d000ff', bold: true },
93
- 'continue': { fg: '#d000ff', bold: true },
94
- 'throw': { fg: '#d000ff', bold: true },
95
- 'try': { fg: '#d000ff', bold: true },
96
- 'catch': { fg: '#d000ff', bold: true },
97
- 'finally': { fg: '#d000ff', bold: true },
98
- 'async': { fg: '#d000ff', bold: true },
99
- 'await': { fg: '#d000ff', bold: true },
100
- 'yield': { fg: '#d000ff', bold: true },
101
- 'delete': { fg: '#d000ff', bold: true },
102
- 'void': { fg: '#d000ff', bold: true },
103
- 'static': { fg: '#d000ff', bold: true },
104
- 'extends': { fg: '#d000ff', bold: true },
105
- 'implements': { fg: '#d000ff', bold: true },
106
- 'interface': { fg: '#d000ff', bold: true },
107
- 'type': { fg: '#d000ff', bold: true },
108
- 'enum': { fg: '#d000ff', bold: true },
109
- 'namespace': { fg: '#d000ff', bold: true },
110
- 'abstract': { fg: '#d000ff', bold: true },
111
- 'readonly': { fg: '#d000ff', bold: true },
112
- 'as': { fg: '#d000ff', bold: true },
113
- 'satisfies': { fg: '#d000ff', bold: true },
66
+ 'if': 'keyword',
67
+ 'else': 'keyword',
68
+ 'return': 'keyword',
69
+ 'const': 'keyword',
70
+ 'let': 'keyword',
71
+ 'var': 'keyword',
72
+ 'function': 'keyword',
73
+ 'class': 'keyword',
74
+ 'import': 'keyword',
75
+ 'export': 'keyword',
76
+ 'from': 'keyword',
77
+ 'new': 'keyword',
78
+ 'typeof': 'keyword',
79
+ 'instanceof': 'keyword',
80
+ 'in': 'keyword',
81
+ 'of': 'keyword',
82
+ 'for': 'keyword',
83
+ 'while': 'keyword',
84
+ 'do': 'keyword',
85
+ 'switch': 'keyword',
86
+ 'case': 'keyword',
87
+ 'break': 'keyword',
88
+ 'continue': 'keyword',
89
+ 'throw': 'keyword',
90
+ 'try': 'keyword',
91
+ 'catch': 'keyword',
92
+ 'finally': 'keyword',
93
+ 'async': 'keyword',
94
+ 'await': 'keyword',
95
+ 'yield': 'keyword',
96
+ 'delete': 'keyword',
97
+ 'void': 'keyword',
98
+ 'static': 'keyword',
99
+ 'extends': 'keyword',
100
+ 'implements': 'keyword',
101
+ 'interface': 'keyword',
102
+ 'type': 'keyword',
103
+ 'enum': 'keyword',
104
+ 'namespace': 'keyword',
105
+ 'abstract': 'keyword',
106
+ 'readonly': 'keyword',
107
+ 'as': 'keyword',
108
+ 'satisfies': 'keyword',
114
109
  // Python keywords
115
- 'def': { fg: '#d000ff', bold: true },
116
- 'lambda': { fg: '#d000ff', bold: true },
117
- 'with': { fg: '#d000ff', bold: true },
118
- 'pass': { fg: '#d000ff', bold: true },
119
- 'global': { fg: '#d000ff', bold: true },
120
- 'nonlocal': { fg: '#d000ff', bold: true },
121
- 'assert': { fg: '#d000ff', bold: true },
122
- 'raise': { fg: '#d000ff', bold: true },
123
- 'except': { fg: '#d000ff', bold: true },
124
- 'elif': { fg: '#d000ff', bold: true },
125
- 'and': { fg: '#d000ff', bold: true },
126
- 'or': { fg: '#d000ff', bold: true },
127
- 'not': { fg: '#d000ff', bold: true },
128
- 'is': { fg: '#d000ff', bold: true },
110
+ 'def': 'keyword',
111
+ 'lambda': 'keyword',
112
+ 'with': 'keyword',
113
+ 'pass': 'keyword',
114
+ 'global': 'keyword',
115
+ 'nonlocal': 'keyword',
116
+ 'assert': 'keyword',
117
+ 'raise': 'keyword',
118
+ 'except': 'keyword',
119
+ 'elif': 'keyword',
120
+ 'and': 'keyword',
121
+ 'or': 'keyword',
122
+ 'not': 'keyword',
123
+ 'is': 'keyword',
129
124
  // Bash keywords
130
- 'then': { fg: '#d000ff', bold: true },
131
- 'fi': { fg: '#d000ff', bold: true },
132
- 'done': { fg: '#d000ff', bold: true },
133
- 'esac': { fg: '#d000ff', bold: true },
125
+ 'then': 'keyword',
126
+ 'fi': 'keyword',
127
+ 'done': 'keyword',
128
+ 'esac': 'keyword',
134
129
 
135
130
  // ── Strings
136
- 'string': { fg: '#00ff88' },
137
- 'string_fragment': { fg: '#00ff88' },
138
- 'template_string': { fg: '#00ff88' },
139
- 'escape_sequence': { fg: '#00ff88' },
140
- 'raw_string': { fg: '#00ff88' },
141
- 'concatenated_string': { fg: '#00ff88' },
142
- 'string_content': { fg: '#00ff88' },
143
- 'quoted_attribute_value': { fg: '#00ff88' },
144
- 'attribute_value': { fg: '#00ff88' },
145
- 'pair_value': { fg: '#00ff88' },
146
- 'plain_value': { fg: '#00ff88' },
131
+ 'string': 'string',
132
+ 'string_fragment': 'string',
133
+ 'template_string': 'string',
134
+ 'escape_sequence': 'string',
135
+ 'raw_string': 'string',
136
+ 'concatenated_string': 'string',
137
+ 'string_content': 'string',
138
+ 'quoted_attribute_value': 'string',
139
+ 'attribute_value': 'string',
140
+ 'pair_value': 'string',
141
+ 'plain_value': 'string',
147
142
 
148
143
  // ── Numbers
149
- 'number': { fg: '#ffcc00' },
150
- 'integer': { fg: '#ffcc00' },
151
- 'float': { fg: '#ffcc00' },
152
- 'decimal_integer_literal': { fg: '#ffcc00' },
153
- 'hex_integer_literal': { fg: '#ffcc00' },
154
- 'octal_integer_literal': { fg: '#ffcc00' },
155
- 'binary_integer_literal': { fg: '#ffcc00' },
144
+ 'number': 'number',
145
+ 'integer': 'number',
146
+ 'float': 'number',
147
+ 'decimal_integer_literal': 'number',
148
+ 'hex_integer_literal': 'number',
149
+ 'octal_integer_literal': 'number',
150
+ 'binary_integer_literal': 'number',
156
151
 
157
152
  // ── Comments
158
- 'comment': { fg: '#666666', italic: true },
159
- 'line_comment': { fg: '#666666', italic: true },
160
- 'block_comment': { fg: '#666666', italic: true },
161
- 'shebang': { fg: '#666666', italic: true },
153
+ 'comment': 'comment',
154
+ 'line_comment': 'comment',
155
+ 'block_comment': 'comment',
156
+ 'shebang': 'comment',
162
157
 
163
158
  // ── Functions/methods
164
- 'function_declaration': { fg: '#00ffff' },
165
- 'method_declaration': { fg: '#00ffff' },
166
- 'method_definition': { fg: '#00ffff' },
167
- 'arrow_function': { fg: '#00ffff' },
168
- 'function_expression': { fg: '#00ffff' },
169
- 'call_expression': { fg: '#00ffff' },
170
- 'function_definition': { fg: '#00ffff' }, // Python
159
+ 'function_declaration': 'function',
160
+ 'method_declaration': 'function',
161
+ 'method_definition': 'function',
162
+ 'arrow_function': 'function',
163
+ 'function_expression': 'function',
164
+ 'call_expression': 'function',
165
+ 'function_definition': 'function', // Python
171
166
 
172
167
  // ── Types and classes
173
- 'type_identifier': { fg: '#ff6b9d' },
174
- 'type_annotation': { fg: '#ff6b9d' },
175
- 'class_declaration': { fg: '#ff6b9d' },
176
- 'class_definition': { fg: '#ff6b9d' }, // Python
177
- 'interface_declaration': { fg: '#ff6b9d' },
178
- 'type_alias_declaration': { fg: '#ff6b9d' },
179
- 'predefined_type': { fg: '#ff6b9d' },
180
- 'builtin_type': { fg: '#ff6b9d' },
181
- 'tag_name': { fg: '#ff6b9d' },
182
- 'element': { fg: '#ff6b9d' },
168
+ 'type_identifier': 'type',
169
+ 'type_annotation': 'type',
170
+ 'class_declaration': 'type',
171
+ 'class_definition': 'type', // Python
172
+ 'interface_declaration': 'type',
173
+ 'type_alias_declaration': 'type',
174
+ 'predefined_type': 'type',
175
+ 'builtin_type': 'type',
176
+ 'tag_name': 'type',
177
+ 'element': 'type',
183
178
 
184
179
  // ── Operators
185
- '+': { fg: '#ffffff' },
186
- '-': { fg: '#ffffff' },
187
- '*': { fg: '#ffffff' },
188
- '/': { fg: '#ffffff' },
189
- '%': { fg: '#ffffff' },
190
- '=': { fg: '#ffffff' },
191
- '==': { fg: '#ffffff' },
192
- '===': { fg: '#ffffff' },
193
- '!=': { fg: '#ffffff' },
194
- '!==': { fg: '#ffffff' },
195
- '<': { fg: '#ffffff' },
196
- '>': { fg: '#ffffff' },
197
- '<=': { fg: '#ffffff' },
198
- '>=': { fg: '#ffffff' },
199
- '&&': { fg: '#ffffff' },
200
- '||': { fg: '#ffffff' },
201
- '??': { fg: '#ffffff' },
202
- '=>': { fg: '#ffffff' },
203
- '!': { fg: '#ffffff' },
204
- '&': { fg: '#ffffff' },
205
- '|': { fg: '#ffffff' },
206
- '^': { fg: '#ffffff' },
207
- '~': { fg: '#ffffff' },
208
- '<<': { fg: '#ffffff' },
209
- '>>': { fg: '#ffffff' },
210
- '>>>': { fg: '#ffffff' },
180
+ '+': 'operator',
181
+ '-': 'operator',
182
+ '*': 'operator',
183
+ '/': 'operator',
184
+ '%': 'operator',
185
+ '=': 'operator',
186
+ '==': 'operator',
187
+ '===': 'operator',
188
+ '!=': 'operator',
189
+ '!==': 'operator',
190
+ '<': 'operator',
191
+ '>': 'operator',
192
+ '<=': 'operator',
193
+ '>=': 'operator',
194
+ '&&': 'operator',
195
+ '||': 'operator',
196
+ '??': 'operator',
197
+ '=>': 'operator',
198
+ '!': 'operator',
199
+ '&': 'operator',
200
+ '|': 'operator',
201
+ '^': 'operator',
202
+ '~': 'operator',
203
+ '<<': 'operator',
204
+ '>>': 'operator',
205
+ '>>>': 'operator',
211
206
 
212
207
  // ── Properties
213
- 'property_identifier': { fg: '#87ceeb' },
214
- 'shorthand_property_identifier': { fg: '#87ceeb' },
215
- 'attribute_name': { fg: '#87ceeb' },
216
- 'property_name': { fg: '#87ceeb' },
217
- 'pair_key': { fg: '#87ceeb' },
208
+ 'property_identifier': 'property',
209
+ 'shorthand_property_identifier': 'property',
210
+ 'attribute_name': 'property',
211
+ 'property_name': 'property',
212
+ 'pair_key': 'property',
218
213
 
219
214
  // ── Built-ins / special values
220
- 'true': { fg: '#ff8c00' },
221
- 'false': { fg: '#ff8c00' },
222
- 'null': { fg: '#ff8c00' },
223
- 'undefined': { fg: '#ff8c00' },
224
- 'none': { fg: '#ff8c00' },
225
- 'None': { fg: '#ff8c00' },
226
- 'True': { fg: '#ff8c00' },
227
- 'False': { fg: '#ff8c00' },
228
- 'this': { fg: '#ff8c00' },
229
- 'super': { fg: '#ff8c00' },
230
- 'self': { fg: '#ff8c00' },
231
- 'boolean': { fg: '#ff8c00' },
215
+ 'true': 'builtin',
216
+ 'false': 'builtin',
217
+ 'null': 'builtin',
218
+ 'undefined': 'builtin',
219
+ 'none': 'builtin',
220
+ 'None': 'builtin',
221
+ 'True': 'builtin',
222
+ 'False': 'builtin',
223
+ 'this': 'builtin',
224
+ 'super': 'builtin',
225
+ 'self': 'builtin',
226
+ 'boolean': 'builtin',
232
227
 
233
228
  // ── JSON specific
234
- 'json_string': { fg: '#00ff88' },
235
- 'json_key': { fg: '#87ceeb' },
236
- 'json_number': { fg: '#ffcc00' },
229
+ 'json_string': 'string',
230
+ 'json_key': 'property',
231
+ 'json_number': 'number',
237
232
 
238
233
  // ── CSS specific
239
- 'class_selector': { fg: '#ff6b9d' },
240
- 'id_selector': { fg: '#ff6b9d' },
241
- 'pseudo_class_selector': { fg: '#d000ff' },
242
- 'pseudo_element_selector': { fg: '#d000ff' },
243
- 'property_name_css': { fg: '#87ceeb' },
244
- 'unit': { fg: '#ffcc00' },
245
- 'color_value': { fg: '#00ff88' },
246
- 'at_keyword': { fg: '#d000ff', bold: true },
247
- 'important': { fg: '#d000ff', bold: true },
234
+ 'class_selector': 'type',
235
+ 'id_selector': 'type',
236
+ 'pseudo_class_selector': 'keyword',
237
+ 'pseudo_element_selector': 'keyword',
238
+ 'property_name_css': 'property',
239
+ 'unit': 'number',
240
+ 'color_value': 'string',
241
+ 'at_keyword': 'keyword',
242
+ 'important': 'keyword',
248
243
  };
249
244
 
250
- // Default color for unrecognized node types
251
- const DEFAULT_FG = '252';
245
+ // Default role for unrecognized node types
246
+ const DEFAULT_ROLE: SyntaxRole = 'plain';
252
247
 
253
248
  // ─── Content Hash ─────────────────────────────────────────────────────────────
254
249
 
@@ -270,11 +265,17 @@ interface Span {
270
265
  endRow: number;
271
266
  endCol: number;
272
267
  text: string;
273
- fg: string;
274
- bold?: boolean;
275
- italic?: boolean;
268
+ role: SyntaxRole;
276
269
  }
277
270
 
271
+ /** A cached token: text plus its role (colour resolved per theme on read). */
272
+ interface RoleToken {
273
+ text: string;
274
+ role: SyntaxRole;
275
+ }
276
+
277
+ type RoleLine = RoleToken[];
278
+
278
279
  /**
279
280
  * Walk the AST and collect leaf nodes with their positions and colors.
280
281
  * Returns a flat array of colored spans that covers all tokens in the code.
@@ -282,7 +283,7 @@ interface Span {
282
283
  function collectSpans(root: Node, code: string): Span[] {
283
284
  const spans: Span[] = [];
284
285
 
285
- function getStyle(node: Node): { fg: string; bold?: boolean; italic?: boolean } | null {
286
+ function getStyle(node: Node): SyntaxRole | null {
286
287
  // Named nodes (keywords, identifiers, etc.)
287
288
  const namedStyle = NODE_TYPE_COLORS[node.type];
288
289
  if (namedStyle) return namedStyle;
@@ -309,9 +310,7 @@ function collectSpans(root: Node, code: string): Span[] {
309
310
  endRow: node.endPosition.row,
310
311
  endCol: node.endPosition.column,
311
312
  text,
312
- fg: style?.fg ?? DEFAULT_FG,
313
- bold: style?.bold,
314
- italic: style?.italic,
313
+ role: style ?? DEFAULT_ROLE,
315
314
  });
316
315
  return;
317
316
  }
@@ -329,9 +328,7 @@ function collectSpans(root: Node, code: string): Span[] {
329
328
  endRow: node.endPosition.row,
330
329
  endCol: node.endPosition.column,
331
330
  text,
332
- fg: style.fg,
333
- bold: style.bold,
334
- italic: style.italic,
331
+ role: style,
335
332
  });
336
333
  return;
337
334
  }
@@ -371,8 +368,8 @@ function isLeafLike(node: Node): boolean {
371
368
  * Convert a flat list of positioned spans into per-line SyntaxToken arrays.
372
369
  * Handles multi-line spans (e.g., block comments, template literals).
373
370
  */
374
- function spansToLines(spans: Span[], codeLines: string[]): HighlightedLine[] {
375
- const result: HighlightedLine[] = codeLines.map(() => []);
371
+ function spansToLines(spans: Span[], codeLines: string[]): RoleLine[] {
372
+ const result: RoleLine[] = codeLines.map(() => []);
376
373
 
377
374
  // Track the current position to emit default-colored text for gaps
378
375
  const linePositions: number[] = codeLines.map(() => 0);
@@ -390,12 +387,12 @@ function spansToLines(spans: Span[], codeLines: string[]): HighlightedLine[] {
390
387
  const currentCol = linePositions[row];
391
388
  if (currentCol < span.startCol) {
392
389
  const gapText = codeLines[row].slice(currentCol, span.startCol);
393
- if (gapText) result[row].push({ text: gapText, fg: DEFAULT_FG });
390
+ if (gapText) result[row].push({ text: gapText, role: DEFAULT_ROLE });
394
391
  }
395
392
 
396
393
  const tokenText = codeLines[row].slice(span.startCol, span.endCol);
397
394
  if (tokenText) {
398
- result[row].push({ text: tokenText, fg: span.fg, bold: span.bold, italic: span.italic });
395
+ result[row].push({ text: tokenText, role: span.role });
399
396
  }
400
397
  linePositions[row] = span.endCol;
401
398
  } else {
@@ -413,12 +410,12 @@ function spansToLines(spans: Span[], codeLines: string[]): HighlightedLine[] {
413
410
  const currentCol = linePositions[r];
414
411
  if (currentCol < colStart) {
415
412
  const gapText = codeLines[r].slice(currentCol, colStart);
416
- if (gapText) result[r].push({ text: gapText, fg: DEFAULT_FG });
413
+ if (gapText) result[r].push({ text: gapText, role: DEFAULT_ROLE });
417
414
  }
418
415
 
419
416
  const tokenText = codeLines[r].slice(colStart, colEnd);
420
417
  if (tokenText) {
421
- result[r].push({ text: tokenText, fg: span.fg, bold: span.bold, italic: span.italic });
418
+ result[r].push({ text: tokenText, role: span.role });
422
419
  }
423
420
  linePositions[r] = colEnd;
424
421
  }
@@ -428,7 +425,7 @@ function spansToLines(spans: Span[], codeLines: string[]): HighlightedLine[] {
428
425
  // Fill remaining text on each line with default color
429
426
  for (let r = 0; r < codeLines.length; r++) {
430
427
  const remaining = codeLines[r].slice(linePositions[r]);
431
- if (remaining) result[r].push({ text: remaining, fg: DEFAULT_FG });
428
+ if (remaining) result[r].push({ text: remaining, role: DEFAULT_ROLE });
432
429
  }
433
430
 
434
431
  return result;
@@ -438,10 +435,32 @@ function spansToLines(spans: Span[], codeLines: string[]): HighlightedLine[] {
438
435
 
439
436
  const MAX_HIGHLIGHT_CACHE = 200;
440
437
 
438
+ /** A parsed block: roles, plus the colours resolved for the last theme read. */
439
+ interface HighlightEntry {
440
+ readonly roles: RoleLine[];
441
+ resolved?: HighlightedLine[];
442
+ resolvedFor?: Readonly<Record<SyntaxRole, SyntaxStyle>>;
443
+ }
444
+
445
+ function resolveRoleLines(roles: RoleLine[], styles: Readonly<Record<SyntaxRole, SyntaxStyle>>): HighlightedLine[] {
446
+ return roles.map((line) => line.map(({ text, role }) => {
447
+ const style = styles[role];
448
+ return style.italic ? { text, fg: style.fg, italic: true } : { text, fg: style.fg };
449
+ }));
450
+ }
451
+
441
452
  export class SyntaxHighlighter {
442
453
  private service: TreeSitterService;
443
- private cache: Map<string, HighlightedLine[]> = new Map();
454
+ private cache: Map<string, HighlightEntry> = new Map();
444
455
  private pending: Set<string> = new Set();
456
+ /** Blocks whose parse cannot succeed (no grammar, parse failed): never rescheduled, always the regex tokens. */
457
+ private failed: Set<string> = new Set();
458
+ private inflight: Set<Promise<void>> = new Set();
459
+ private readonly readyListeners = new Set<() => void>();
460
+ private _generation = 0;
461
+ private _misses = 0;
462
+ /** Numbers each parse's virtual path, so no two parses in flight share a tree. */
463
+ private parseSeq = 0;
445
464
 
446
465
  constructor() {
447
466
  this.service = new TreeSitterService();
@@ -477,13 +496,24 @@ export class SyntaxHighlighter {
477
496
 
478
497
  const key = `${langId}:${hashString(code)}`;
479
498
  const cached = this.cache.get(key);
480
- if (cached) return cached;
499
+ if (cached) {
500
+ // Colours follow the active theme: re-resolve when the theme changed.
501
+ const styles = syntaxStyles();
502
+ if (cached.resolvedFor !== styles || cached.resolved === undefined) {
503
+ cached.resolved = resolveRoleLines(cached.roles, styles);
504
+ cached.resolvedFor = styles;
505
+ }
506
+ return cached.resolved;
507
+ }
481
508
 
482
509
  // Do not schedule background parse while the block is still being streamed.
483
510
  // The regex tokenizer serves during streaming (as designed). Schedule parse
484
511
  // only when isStreaming=false, i.e., the block has been finalized.
485
- if (!isStreaming && !this.pending.has(key)) {
486
- this.scheduleParse(code, langId, key);
512
+ if (!isStreaming && !this.failed.has(key)) {
513
+ // The caller draws the regex placeholder and a parse result is on its
514
+ // way: whatever it caches must be redrawn when the result lands.
515
+ this._misses++;
516
+ if (!this.pending.has(key)) this.scheduleParse(code, langId, key);
487
517
  }
488
518
 
489
519
  return null; // not ready yet
@@ -496,22 +526,33 @@ export class SyntaxHighlighter {
496
526
  private scheduleParse(code: string, langId: string, key: string): void {
497
527
  this.pending.add(key);
498
528
 
499
- // Use a stable virtual path for the parser cache key
500
- const virtualPath = `__highlight__.${langId}`;
501
-
502
- Promise.resolve().then(async () => {
529
+ // Each parse gets its own virtual path. The service keeps one tree per
530
+ // path and deletes the previous tree when the same path is parsed again,
531
+ // so a shared path let a second parse in flight free this parse's tree
532
+ // before collectSpans walked it: the spans came from freed memory, the
533
+ // wrong colours were cached for good, and which ones depended on what
534
+ // else was being highlighted at the time. The tree is released (the
535
+ // service's invalidate) once this parse has walked it.
536
+ const virtualPath = `__highlight__/${++this.parseSeq}.${langId}`;
537
+
538
+ const run = Promise.resolve().then(async () => {
539
+ let landed = false;
540
+ let parsed = false;
503
541
  try {
504
542
  // Ensure the grammar is loaded
505
543
  const language = await this.service.loadLanguage(langId);
506
544
  if (!language) {
507
545
  logger.debug('SyntaxHighlighter: grammar not available', { langId });
546
+ this.markFailed(key);
508
547
  return;
509
548
  }
510
549
 
511
550
  // Parse the code
512
551
  const tree = await this.service.parse(virtualPath, code, langId);
552
+ parsed = true;
513
553
  if (!tree) {
514
554
  logger.debug('SyntaxHighlighter: parse returned null', { langId });
555
+ this.markFailed(key);
515
556
  return;
516
557
  }
517
558
 
@@ -526,19 +567,61 @@ export class SyntaxHighlighter {
526
567
  if (firstKey !== undefined) this.cache.delete(firstKey);
527
568
  }
528
569
 
529
- this.cache.set(key, highlighted);
570
+ this.cache.set(key, { roles: highlighted });
571
+ this._generation++;
572
+ landed = true;
530
573
  logger.debug('SyntaxHighlighter: parsed and cached', { langId, lines: codeLines.length });
531
574
  } catch (err) {
532
575
  logger.warn('SyntaxHighlighter: parse error', { langId, error: summarizeError(err) });
576
+ this.markFailed(key);
533
577
  } finally {
578
+ if (parsed) this.service.invalidate(virtualPath);
534
579
  this.pending.delete(key);
535
580
  }
581
+ if (landed) for (const listener of this.readyListeners) listener();
536
582
  });
583
+ this.inflight.add(run);
584
+ void run.finally(() => { this.inflight.delete(run); });
585
+ }
586
+
587
+ private markFailed(key: string): void {
588
+ if (this.failed.size >= MAX_HIGHLIGHT_CACHE) {
589
+ const first = this.failed.values().next().value;
590
+ if (first !== undefined) this.failed.delete(first);
591
+ }
592
+ this.failed.add(key);
593
+ }
594
+
595
+ /**
596
+ * Bumped every time a parse result lands (and when the cache is cleared).
597
+ * Anything drawn while a parse was on its way (a miss) is stale once this
598
+ * moves on.
599
+ */
600
+ get generation(): number {
601
+ return this._generation;
602
+ }
603
+
604
+ /** Bumped every time highlight() hands back the placeholder while a parse result is on its way. */
605
+ get missCount(): number {
606
+ return this._misses;
607
+ }
608
+
609
+ /** Be told when a parse result lands (repaint what drew the placeholder). */
610
+ onReady(listener: () => void): () => void {
611
+ this.readyListeners.add(listener);
612
+ return () => { this.readyListeners.delete(listener); };
613
+ }
614
+
615
+ /** Resolves once no parse is in flight (tests capture settled frames with it). */
616
+ async settle(): Promise<void> {
617
+ while (this.inflight.size > 0) await Promise.all([...this.inflight]);
537
618
  }
538
619
 
539
620
  /** Clear all cached highlights (e.g., on theme change). */
540
621
  clearCache(): void {
541
622
  this.cache.clear();
623
+ this.failed.clear();
624
+ this._generation++;
542
625
  }
543
626
 
544
627
  /** Current cache size (for diagnostics). */