@pellux/goodvibes-tui 2.0.21 → 2.1.0

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