ompchamber 2.0.2 → 3.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 (419) hide show
  1. package/README.md +31 -11
  2. package/bunfig.toml +11 -0
  3. package/dist/client/index.html +16 -5
  4. package/dist/client/index.js +1393 -0
  5. package/dist/client/static/js/index-8t70jhyj.js +5149 -0
  6. package/dist/client/static/js/index-n4wkqj7t.css +1 -0
  7. package/index.html +54 -0
  8. package/package.json +9 -10
  9. package/public/apple-touch-icon.png +0 -0
  10. package/public/icon-192x192.png +0 -0
  11. package/public/icon.svg +60 -0
  12. package/public/manifest.webmanifest +38 -0
  13. package/src/cli/lib/commands/serve.js +9 -5
  14. package/src/cli/lib/runtime.js +18 -7
  15. package/src/client/components/common/code-editor/handler.test.ts +274 -0
  16. package/src/client/components/common/code-editor/handler.ts +175 -0
  17. package/src/client/components/common/code-editor/index.tsx +185 -220
  18. package/src/client/components/common/code-surface/index.tsx +109 -12
  19. package/src/client/components/common/lazy-panels.tsx +5 -1
  20. package/src/client/components/common/provider-icon/glyphs.ts +118 -0
  21. package/src/client/components/common/provider-icon/index.tsx +85 -0
  22. package/src/client/components/common/viewport.ts +5 -3
  23. package/src/client/components/layout/RightActivityBar.tsx +2 -1
  24. package/src/client/components/layout/desktop-layout/TopNavbar.tsx +21 -4
  25. package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +4 -0
  26. package/src/client/components/layout/desktop-layout/index.tsx +26 -4
  27. package/src/client/components/mobile/LayoutWrapper.tsx +16 -0
  28. package/src/client/components/mobile/RightSidebar.tsx +11 -4
  29. package/src/client/components/mobile/SessionSidebar.tsx +10 -0
  30. package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +0 -1
  31. package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +16 -1
  32. package/src/client/components/mobile/mobile-session-sidebar/List.tsx +3 -0
  33. package/src/client/components/settings/categories/ProviderSettings.tsx +45 -4
  34. package/src/client/components/settings/categories/UsageSettings.tsx +22 -7
  35. package/src/client/components/settings/categories/provider-settings/AddModelModal.tsx +322 -0
  36. package/src/client/components/settings/categories/provider-settings/AddProviderModal.tsx +152 -56
  37. package/src/client/components/settings/categories/provider-settings/AuthSection.tsx +52 -1
  38. package/src/client/components/settings/categories/provider-settings/DeleteProviderModal.tsx +133 -0
  39. package/src/client/components/settings/categories/provider-settings/DialectFields.tsx +136 -0
  40. package/src/client/components/settings/categories/provider-settings/EffortLadder.tsx +89 -0
  41. package/src/client/components/settings/categories/provider-settings/Header.tsx +2 -2
  42. package/src/client/components/settings/categories/provider-settings/LoginModal.tsx +2 -2
  43. package/src/client/components/settings/categories/provider-settings/ModelsList.tsx +22 -2
  44. package/src/client/components/settings/categories/provider-settings/PresetPicker.tsx +82 -0
  45. package/src/client/components/settings/categories/provider-settings/PriceFields.tsx +107 -0
  46. package/src/client/components/settings/categories/provider-settings/ReconnectModal.tsx +43 -22
  47. package/src/client/components/settings/categories/provider-settings/SidebarList.tsx +2 -2
  48. package/src/client/components/settings/categories/usage-settings/LimitsSection.tsx +115 -0
  49. package/src/client/components/settings/categories/usage-settings/ProviderCard.tsx +19 -26
  50. package/src/client/components/settings/categories/usage-settings/ProviderDetail.tsx +29 -22
  51. package/src/client/components/settings/categories/usage-settings/SidebarList.tsx +60 -48
  52. package/src/client/components/settings/categories/usage-settings/format.ts +36 -0
  53. package/src/client/components/settings/categories/usage-settings/providers.ts +52 -28
  54. package/src/client/components/workspace/browser-panel/AddressBar.tsx +1 -10
  55. package/src/client/components/workspace/browser-panel/Screencast.tsx +4 -5
  56. package/src/client/components/workspace/browser-panel/index.tsx +6 -6
  57. package/src/client/components/workspace/browser-toolbar/index.tsx +22 -12
  58. package/src/client/components/workspace/chat-timeline/AiMessageFooter.tsx +1 -1
  59. package/src/client/components/workspace/chat-timeline/GeneratingIndicator.tsx +1 -1
  60. package/src/client/components/workspace/chat-timeline/ToolCallCard.tsx +1 -1
  61. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Todo.tsx +1 -1
  62. package/src/client/components/workspace/diff-panel/Toolbar.tsx +71 -38
  63. package/src/client/components/workspace/diff-panel/index.tsx +139 -53
  64. package/src/client/components/workspace/editor/CommandPalette.test.ts +242 -0
  65. package/src/client/components/workspace/editor/CommandPalette.tsx +138 -0
  66. package/src/client/components/workspace/editor/FindWidget.test.ts +261 -0
  67. package/src/client/components/workspace/editor/FindWidget.tsx +219 -0
  68. package/src/client/components/workspace/editor/Tabs.tsx +2 -2
  69. package/src/client/components/workspace/editor/Toolbar.tsx +129 -26
  70. package/src/client/components/workspace/editor/index.tsx +180 -62
  71. package/src/client/components/workspace/file-explorer/Modals.tsx +79 -2
  72. package/src/client/components/workspace/file-explorer/TreeItem.tsx +43 -113
  73. package/src/client/components/workspace/model-dropdown/Item.tsx +13 -23
  74. package/src/client/components/workspace/model-dropdown/Panel.tsx +1 -1
  75. package/src/client/components/workspace/model-dropdown/Section.tsx +13 -4
  76. package/src/client/components/workspace/model-dropdown/index.tsx +7 -3
  77. package/src/client/components/workspace/todo-panel/PhaseGroup.tsx +76 -0
  78. package/src/client/components/workspace/todo-panel/Row.tsx +69 -0
  79. package/src/client/components/workspace/todo-panel/index.tsx +161 -0
  80. package/src/client/components/workspace/usage-panel/ProviderSelect.tsx +10 -5
  81. package/src/client/components/workspace/usage-panel/index.tsx +36 -18
  82. package/src/client/data/samples/dialogue-session.ts +1 -1
  83. package/src/client/data/samples/sample-tools/other.ts +1 -1
  84. package/src/client/data/settings/provider.ts +141 -9
  85. package/src/client/hooks/editor/find-harness.test-util.ts +170 -0
  86. package/src/client/hooks/editor/scroll-container.test.ts +102 -0
  87. package/src/client/hooks/editor/scroll-container.ts +26 -0
  88. package/src/client/hooks/editor/use-code-window.ts +2 -20
  89. package/src/client/hooks/editor/use-editor-find-write.test.ts +170 -0
  90. package/src/client/hooks/editor/use-editor-find.test.ts +118 -0
  91. package/src/client/hooks/editor/use-editor-find.ts +321 -0
  92. package/src/client/hooks/editor/use-editor-history.test.ts +216 -0
  93. package/src/client/hooks/editor/use-editor-history.ts +134 -0
  94. package/src/client/hooks/editor/use-file-editor.ts +24 -27
  95. package/src/client/hooks/editor/use-occurrences.test.ts +236 -0
  96. package/src/client/hooks/editor/use-occurrences.ts +98 -0
  97. package/src/client/hooks/models/use-provider-names.ts +1 -1
  98. package/src/client/hooks/settings/providers/actions.ts +337 -0
  99. package/src/client/hooks/settings/providers/api.ts +69 -0
  100. package/src/client/hooks/settings/providers/index.ts +138 -0
  101. package/src/client/hooks/settings/providers/messages.ts +77 -0
  102. package/src/client/hooks/settings/useUsageReport.ts +8 -3
  103. package/src/client/hooks/ui/theme.ts +3 -3
  104. package/src/client/hooks/workspace/file-tabs.ts +62 -11
  105. package/src/client/hooks/workspace/file-tree-actions.test.ts +348 -0
  106. package/src/client/hooks/workspace/file-tree-actions.ts +250 -0
  107. package/src/client/hooks/workspace/session-todos.ts +102 -0
  108. package/src/client/stubs/elkjs.ts +6 -1
  109. package/src/client/tailwind.css +30 -0
  110. package/src/server/index.ts +20 -2
  111. package/src/server/lib/assets/font-css.server.ts +86 -0
  112. package/src/server/lib/assets/fonts.server.ts +117 -0
  113. package/src/server/lib/bundler/css.ts +116 -0
  114. package/src/server/lib/lifecycle/shell-route.ts +17 -0
  115. package/src/server/lib/models/provider-listing.server.ts +257 -0
  116. package/src/server/lib/models/provider-registry.server.test.ts +122 -0
  117. package/src/server/lib/models/provider-registry.server.ts +76 -25
  118. package/src/server/lib/omp/config/models-config.ts +11 -0
  119. package/src/server/lib/omp/config/models-validation.test.ts +137 -0
  120. package/src/server/lib/omp/config/models-validation.ts +111 -2
  121. package/src/server/lib/omp/config/provider-seeds.test.ts +167 -0
  122. package/src/server/lib/omp/config/provider-seeds.ts +176 -0
  123. package/src/server/lib/omp/config/providers.ts +94 -100
  124. package/src/server/lib/omp/core/auth-credentials.ts +45 -0
  125. package/src/server/lib/omp/session/todos.ts +100 -0
  126. package/src/server/lib/omp/session/{usage.ts → usage/aggregate.ts} +1 -118
  127. package/src/server/lib/omp/session/usage/shaping.ts +130 -0
  128. package/src/server/lib/usage/deepseek.ts +4 -5
  129. package/src/server/lib/usage/kenari.ts +7 -6
  130. package/src/server/lib/usage/omp-usage.server.test.ts +126 -0
  131. package/src/server/lib/usage/omp-usage.server.ts +194 -0
  132. package/src/server/lib/usage/provider-key.ts +81 -3
  133. package/src/server/lib/usage/providers.server.test.ts +47 -0
  134. package/src/server/lib/usage/providers.server.ts +208 -0
  135. package/src/server/lib/usage/vendor-balance.server.test.ts +104 -0
  136. package/src/server/lib/usage/vendor-balance.server.ts +284 -0
  137. package/src/server/plugins/shell.server.ts +56 -0
  138. package/src/server/plugins/ssr.ts +24 -36
  139. package/src/server/plugins/static.test.ts +26 -29
  140. package/src/server/plugins/static.ts +40 -19
  141. package/src/server/routes/fs/action-create.test.ts +90 -0
  142. package/src/server/routes/fs/action.ts +23 -0
  143. package/src/server/routes/omp/index.ts +2 -0
  144. package/src/server/routes/omp/session-todos.ts +80 -0
  145. package/src/server/routes/settings/index.ts +2 -0
  146. package/src/server/routes/settings/provider-model.ts +107 -0
  147. package/src/server/routes/settings/provider-models.ts +65 -202
  148. package/src/server/routes/settings/providers.ts +22 -7
  149. package/src/server/routes/settings/usage.ts +125 -45
  150. package/src/server/routes/telemetry/tokens.ts +2 -1
  151. package/src/shared/lib/chat/{todo-parser.ts → todo/parser.ts} +17 -0
  152. package/src/shared/lib/chat/todo/progress.ts +97 -0
  153. package/src/shared/lib/chat/todo/snapshot.test.ts +156 -0
  154. package/src/shared/lib/chat/todo/snapshot.ts +215 -0
  155. package/src/shared/lib/code/editor/commands.test.ts +155 -0
  156. package/src/shared/lib/code/editor/commands.ts +260 -0
  157. package/src/shared/lib/code/editor/edits.test.ts +100 -0
  158. package/src/shared/lib/code/editor/edits.ts +122 -0
  159. package/src/shared/lib/code/editor/find-status.test.ts +50 -0
  160. package/src/shared/lib/code/editor/find-status.ts +36 -0
  161. package/src/shared/lib/code/editor/find.test.ts +203 -0
  162. package/src/shared/lib/code/editor/find.ts +224 -0
  163. package/src/{client/components/common/code-editor → shared/lib/code/editor}/history.test.ts +1 -1
  164. package/src/shared/lib/code/editor/keymap.test.ts +109 -0
  165. package/src/shared/lib/code/editor/keymap.ts +151 -0
  166. package/src/shared/lib/code/editor/layers.test.ts +77 -0
  167. package/src/shared/lib/code/editor/layers.ts +67 -0
  168. package/src/shared/lib/code/editor/line-edits.test.ts +131 -0
  169. package/src/shared/lib/code/editor/line-edits.ts +159 -0
  170. package/src/shared/lib/code/editor/lines.test.ts +81 -0
  171. package/src/shared/lib/code/editor/lines.ts +83 -0
  172. package/src/shared/lib/code/editor/marks.test.ts +86 -0
  173. package/src/shared/lib/code/editor/marks.ts +106 -0
  174. package/src/shared/lib/code/editor/occurrences.test.ts +117 -0
  175. package/src/shared/lib/code/editor/occurrences.ts +111 -0
  176. package/src/shared/lib/code/editor/palette.test.ts +91 -0
  177. package/src/shared/lib/code/editor/palette.ts +92 -0
  178. package/src/shared/lib/code/editor/reset-css.test.ts +35 -0
  179. package/src/shared/lib/code/editor/reset-css.ts +32 -0
  180. package/src/shared/lib/code/editor/text-commands.test.ts +178 -0
  181. package/src/shared/lib/code/editor/text-commands.ts +154 -0
  182. package/src/shared/lib/code/syntax-highlight.test.ts +75 -0
  183. package/src/shared/lib/code/syntax-highlight.ts +29 -17
  184. package/src/shared/lib/code/windowed-highlight.ts +52 -1
  185. package/src/shared/lib/fs/diff-parser.test.ts +116 -0
  186. package/src/shared/lib/fs/diff-parser.ts +86 -33
  187. package/src/shared/lib/models/{provider-connection.ts → provider/connection.ts} +37 -0
  188. package/src/shared/lib/models/provider/dialect.ts +179 -0
  189. package/src/shared/lib/models/provider/glyph.test.ts +116 -0
  190. package/src/shared/lib/models/provider/glyph.ts +200 -0
  191. package/src/shared/lib/models/{provider-models.ts → provider/models.ts} +91 -6
  192. package/src/shared/lib/models/provider/presets.test.ts +172 -0
  193. package/src/shared/lib/models/provider/presets.ts +83 -0
  194. package/src/shared/lib/ui/key-binding.test.ts +115 -0
  195. package/src/shared/lib/ui/key-binding.ts +143 -0
  196. package/src/shared/lib/util/platform.test.ts +87 -0
  197. package/src/shared/lib/util/platform.ts +30 -0
  198. package/src/shared/lib/workspace/file-tab-id.ts +29 -0
  199. package/src/shared/lib/workspace/keymap.test.ts +82 -0
  200. package/src/shared/lib/workspace/keymap.ts +30 -0
  201. package/src/shared/lib/workspace/refresh-cadence.ts +9 -0
  202. package/src/shared/lib/workspace/right-panels.ts +8 -0
  203. package/src/shared/lib/workspace/session-state/store.ts +0 -1
  204. package/src/shared/types/index.ts +1 -0
  205. package/src/shared/types/settings/provider.ts +81 -0
  206. package/src/shared/types/settings/usage.ts +88 -14
  207. package/src/shared/types/todo.ts +108 -0
  208. package/tsconfig.json +3 -1
  209. package/dist/client/static/css/index.5f630421b7.css +0 -2
  210. package/dist/client/static/font/KaTeX_AMS-Regular.a79f1c3119.woff2 +0 -0
  211. package/dist/client/static/font/KaTeX_Caligraphic-Bold.ec17d13264.woff2 +0 -0
  212. package/dist/client/static/font/KaTeX_Caligraphic-Regular.55fac25845.woff2 +0 -0
  213. package/dist/client/static/font/KaTeX_Fraktur-Bold.d42a5579b0.woff2 +0 -0
  214. package/dist/client/static/font/KaTeX_Fraktur-Regular.d3c882a649.woff2 +0 -0
  215. package/dist/client/static/font/KaTeX_Main-Bold.c3fb5ac22f.woff2 +0 -0
  216. package/dist/client/static/font/KaTeX_Main-BoldItalic.6f2bb1dff2.woff2 +0 -0
  217. package/dist/client/static/font/KaTeX_Main-Italic.8916142bec.woff2 +0 -0
  218. package/dist/client/static/font/KaTeX_Main-Regular.0462f03bdf.woff2 +0 -0
  219. package/dist/client/static/font/KaTeX_Math-BoldItalic.572d331f69.woff2 +0 -0
  220. package/dist/client/static/font/KaTeX_Math-Italic.f28c23acad.woff2 +0 -0
  221. package/dist/client/static/font/KaTeX_SansSerif-Bold.8c5b5494b6.woff2 +0 -0
  222. package/dist/client/static/font/KaTeX_SansSerif-Italic.3b1e59b3ba.woff2 +0 -0
  223. package/dist/client/static/font/KaTeX_SansSerif-Regular.ba21ed5f84.woff2 +0 -0
  224. package/dist/client/static/font/KaTeX_Script-Regular.03e9641d6f.woff2 +0 -0
  225. package/dist/client/static/font/KaTeX_Size1-Regular.eae34984b3.woff2 +0 -0
  226. package/dist/client/static/font/KaTeX_Size2-Regular.5916a24fa3.woff2 +0 -0
  227. package/dist/client/static/font/KaTeX_Size4-Regular.10d95fd3a2.woff2 +0 -0
  228. package/dist/client/static/font/KaTeX_Typewriter-Regular.a8709e3622.woff2 +0 -0
  229. package/dist/client/static/font/fira-code-latin-400-normal.2655b10c55.woff2 +0 -0
  230. package/dist/client/static/font/fira-code-latin-500-normal.92a991f204.woff2 +0 -0
  231. package/dist/client/static/font/fira-code-latin-600-normal.43788eb7f1.woff2 +0 -0
  232. package/dist/client/static/font/fira-code-latin-700-normal.2667215d33.woff2 +0 -0
  233. package/dist/client/static/js/77.0d81c74cf0.js +0 -139
  234. package/dist/client/static/js/77.0d81c74cf0.js.LICENSE.txt +0 -8
  235. package/dist/client/static/js/async/0.8e59f2eb5b.js +0 -1
  236. package/dist/client/static/js/async/0k.5852e6b4fa.js +0 -1
  237. package/dist/client/static/js/async/0w.9ed36d5860.js +0 -1
  238. package/dist/client/static/js/async/1.f387c12b63.js +0 -1
  239. package/dist/client/static/js/async/12.d091f91c99.js +0 -1
  240. package/dist/client/static/js/async/13.8f810a8a6c.js +0 -1
  241. package/dist/client/static/js/async/19.6fe909d55a.js +0 -160
  242. package/dist/client/static/js/async/1b.23f1141a38.js +0 -1
  243. package/dist/client/static/js/async/1k.973e8e21fc.js +0 -2
  244. package/dist/client/static/js/async/1k.973e8e21fc.js.LICENSE.txt +0 -11
  245. package/dist/client/static/js/async/1p.0d5cddda96.js +0 -1
  246. package/dist/client/static/js/async/2.b589f1733b.js +0 -1
  247. package/dist/client/static/js/async/2v.96165101de.js +0 -1
  248. package/dist/client/static/js/async/3.0cacc08423.js +0 -1
  249. package/dist/client/static/js/async/37.b28b2f61cd.js +0 -1
  250. package/dist/client/static/js/async/3d.5085fb913f.js +0 -1
  251. package/dist/client/static/js/async/3j.357c80ff38.js +0 -2
  252. package/dist/client/static/js/async/3j.357c80ff38.js.LICENSE.txt +0 -11
  253. package/dist/client/static/js/async/3p.bcf32a0983.js +0 -1
  254. package/dist/client/static/js/async/3r.3a69e98ee0.js +0 -64
  255. package/dist/client/static/js/async/3u.1228460e2a.js +0 -1
  256. package/dist/client/static/js/async/3y.3530b824ad.js +0 -198
  257. package/dist/client/static/js/async/3y.3530b824ad.js.LICENSE.txt +0 -5
  258. package/dist/client/static/js/async/4.9442eeb472.js +0 -1
  259. package/dist/client/static/js/async/4e.678285ca55.js +0 -1
  260. package/dist/client/static/js/async/4k.33a0c7eeb4.js +0 -1
  261. package/dist/client/static/js/async/4m.928a1dde85.js +0 -31
  262. package/dist/client/static/js/async/4m.928a1dde85.js.LICENSE.txt +0 -11
  263. package/dist/client/static/js/async/4q.aad20dab25.js +0 -1
  264. package/dist/client/static/js/async/5.ebaf8b89cc.js +0 -36
  265. package/dist/client/static/js/async/51.95bcae98af.js +0 -2
  266. package/dist/client/static/js/async/51.95bcae98af.js.LICENSE.txt +0 -6
  267. package/dist/client/static/js/async/5b.d88304bc13.js +0 -1
  268. package/dist/client/static/js/async/5j.d6407c4248.js +0 -287
  269. package/dist/client/static/js/async/5o.88316d4460.js +0 -62
  270. package/dist/client/static/js/async/6.f034c9b528.js +0 -1
  271. package/dist/client/static/js/async/66.5dfa29f957.js +0 -93
  272. package/dist/client/static/js/async/6o.5b1a06a382.js +0 -1
  273. package/dist/client/static/js/async/7.e4a9288dd0.js +0 -1
  274. package/dist/client/static/js/async/73.308dff4da8.js +0 -1
  275. package/dist/client/static/js/async/7e.a1cc9bfd88.js +0 -1
  276. package/dist/client/static/js/async/7g.a7814350c3.js +0 -1
  277. package/dist/client/static/js/async/7l.fadbbc0583.js +0 -1
  278. package/dist/client/static/js/async/8.5067439d8c.js +0 -1
  279. package/dist/client/static/js/async/8j.ea110663fb.js +0 -1
  280. package/dist/client/static/js/async/a.bce4542392.js +0 -1
  281. package/dist/client/static/js/async/aa.65d3852576.js +0 -1
  282. package/dist/client/static/js/async/ar.9303e5f2e8.js +0 -2
  283. package/dist/client/static/js/async/ar.9303e5f2e8.js.LICENSE.txt +0 -11
  284. package/dist/client/static/js/async/az.d656062b0f.js +0 -1
  285. package/dist/client/static/js/async/b.b4f6f06515.js +0 -1
  286. package/dist/client/static/js/async/bi.09688ba75c.js +0 -1
  287. package/dist/client/static/js/async/bq.586bc738d1.js +0 -1
  288. package/dist/client/static/js/async/c.797d933882.js +0 -1
  289. package/dist/client/static/js/async/c0.0af1e640d3.js +0 -24
  290. package/dist/client/static/js/async/ca.8a52cfd447.js +0 -1
  291. package/dist/client/static/js/async/cf.d34fff5d67.js +0 -1
  292. package/dist/client/static/js/async/ct.b74928f7a3.js +0 -1
  293. package/dist/client/static/js/async/cx.3cb0242a3a.js +0 -226
  294. package/dist/client/static/js/async/d.a5fc8fe1b3.js +0 -1
  295. package/dist/client/static/js/async/da.f62c70f6c8.js +0 -1
  296. package/dist/client/static/js/async/dp.1725d6432c.js +0 -1
  297. package/dist/client/static/js/async/e.93214d622b.js +0 -79
  298. package/dist/client/static/js/async/f.ab50467719.js +0 -19
  299. package/dist/client/static/js/async/f.ab50467719.js.LICENSE.txt +0 -12
  300. package/dist/client/static/js/async/f7.46971e9d6d.js +0 -2
  301. package/dist/client/static/js/async/f7.46971e9d6d.js.LICENSE.txt +0 -6
  302. package/dist/client/static/js/async/f8.bbf0dbae70.js +0 -1
  303. package/dist/client/static/js/async/g.cb81980498.js +0 -2
  304. package/dist/client/static/js/async/g.cb81980498.js.LICENSE.txt +0 -11
  305. package/dist/client/static/js/async/h.4e790471e4.js +0 -27
  306. package/dist/client/static/js/async/h1.393f066d09.js +0 -1
  307. package/dist/client/static/js/async/h3.216e9798ab.js +0 -148
  308. package/dist/client/static/js/async/hd.0da931b622.js +0 -1
  309. package/dist/client/static/js/async/hk.ba7fb04d41.js +0 -1
  310. package/dist/client/static/js/async/hn.b645041e00.js +0 -1
  311. package/dist/client/static/js/async/ho.475a5e1ae4.js +0 -1
  312. package/dist/client/static/js/async/hp.e9fe2e395a.js +0 -2
  313. package/dist/client/static/js/async/hp.e9fe2e395a.js.LICENSE.txt +0 -17
  314. package/dist/client/static/js/async/hs.e7c59d8090.js +0 -41
  315. package/dist/client/static/js/async/i.7c148e8440.js +0 -1
  316. package/dist/client/static/js/async/id.573df20f29.js +0 -1
  317. package/dist/client/static/js/async/j.ab925882d8.js +0 -1
  318. package/dist/client/static/js/async/jd.63886a1f09.js +0 -1
  319. package/dist/client/static/js/async/jg.d86ebd4013.js +0 -86
  320. package/dist/client/static/js/async/jl.874d352278.js +0 -1
  321. package/dist/client/static/js/async/jll.11313b455a.js +0 -1
  322. package/dist/client/static/js/async/js.67182759ef.js +0 -1
  323. package/dist/client/static/js/async/jsc.23b70d6f40.js +0 -1
  324. package/dist/client/static/js/async/jse.334588ff5d.js +0 -1
  325. package/dist/client/static/js/async/jz.72eb929106.js +0 -77
  326. package/dist/client/static/js/async/k.3210286401.js +0 -217
  327. package/dist/client/static/js/async/ka.c1990ecaa3.js +0 -115
  328. package/dist/client/static/js/async/katex-vendor.de6a87c308.js +0 -1
  329. package/dist/client/static/js/async/ku.29c8a59d0c.js +0 -1
  330. package/dist/client/static/js/async/ky.8dab1dade0.js +0 -1
  331. package/dist/client/static/js/async/l.12dd34e1be.js +0 -1
  332. package/dist/client/static/js/async/l9.949710adaf.js +0 -83
  333. package/dist/client/static/js/async/l9.949710adaf.js.LICENSE.txt +0 -13
  334. package/dist/client/static/js/async/ls.9d9a01026c.js +0 -104
  335. package/dist/client/static/js/async/m.eb2e6752e2.js +0 -28
  336. package/dist/client/static/js/async/m.eb2e6752e2.js.LICENSE.txt +0 -11
  337. package/dist/client/static/js/async/m3.d2b05f9604.js +0 -2
  338. package/dist/client/static/js/async/m3.d2b05f9604.js.LICENSE.txt +0 -6
  339. package/dist/client/static/js/async/ma.2c261b04dd.js +0 -2
  340. package/dist/client/static/js/async/ma.2c261b04dd.js.LICENSE.txt +0 -6
  341. package/dist/client/static/js/async/me.8ace414f80.js +0 -23
  342. package/dist/client/static/js/async/ms.98960af4eb.js +0 -67
  343. package/dist/client/static/js/async/ms.98960af4eb.js.LICENSE.txt +0 -12
  344. package/dist/client/static/js/async/mt.0352e976d8.js +0 -1
  345. package/dist/client/static/js/async/n.a9f1c51701.js +0 -1
  346. package/dist/client/static/js/async/na.a36cd532d6.js +0 -1
  347. package/dist/client/static/js/async/nk.fc7e58325e.js +0 -1
  348. package/dist/client/static/js/async/o.36353dfb88.js +0 -1
  349. package/dist/client/static/js/async/oc.b73042e8bd.js +0 -8
  350. package/dist/client/static/js/async/og.8eb577893a.js +0 -193
  351. package/dist/client/static/js/async/p.48ecfcd14e.js +0 -1
  352. package/dist/client/static/js/async/px.fc6877924b.js +0 -1
  353. package/dist/client/static/js/async/q.22588845a9.js +0 -21
  354. package/dist/client/static/js/async/q.22588845a9.js.LICENSE.txt +0 -6
  355. package/dist/client/static/js/async/q5.97dc1481b6.js +0 -286
  356. package/dist/client/static/js/async/qs.2a9ddb3c72.js +0 -1
  357. package/dist/client/static/js/async/r.51010ad87c.js +0 -1
  358. package/dist/client/static/js/async/rd.cb6bf495bb.js +0 -2
  359. package/dist/client/static/js/async/rd.cb6bf495bb.js.LICENSE.txt +0 -4
  360. package/dist/client/static/js/async/s.0f395cdda5.js +0 -94
  361. package/dist/client/static/js/async/t.4dca0e2ba8.js +0 -1
  362. package/dist/client/static/js/async/t9.d44f57d518.js +0 -1
  363. package/dist/client/static/js/async/ta.973534bbec.js +0 -1
  364. package/dist/client/static/js/async/tr.e878658140.js +0 -309
  365. package/dist/client/static/js/async/u.0871ac5b22.js +0 -1
  366. package/dist/client/static/js/async/ud.3065bfee31.js +0 -2
  367. package/dist/client/static/js/async/ud.3065bfee31.js.LICENSE.txt +0 -4
  368. package/dist/client/static/js/async/up.ea990da0ff.js +0 -32
  369. package/dist/client/static/js/async/uw.8890abbabc.js +0 -1
  370. package/dist/client/static/js/async/v.7d28d3c241.js +0 -34
  371. package/dist/client/static/js/async/vp.9b13bf8610.js +0 -1
  372. package/dist/client/static/js/async/vt.42416b707d.js +0 -1
  373. package/dist/client/static/js/async/w.08ac210f60.js +0 -1
  374. package/dist/client/static/js/async/wt.ad92393a90.js +0 -1
  375. package/dist/client/static/js/async/x.563f72ca6b.js +0 -1
  376. package/dist/client/static/js/async/x0.475516418f.js +0 -1
  377. package/dist/client/static/js/async/x1.da8687159b.js +0 -90
  378. package/dist/client/static/js/async/xe.da8dcc2015.js +0 -2
  379. package/dist/client/static/js/async/xe.da8dcc2015.js.LICENSE.txt +0 -11
  380. package/dist/client/static/js/async/xl.a3c562a58c.js +0 -1
  381. package/dist/client/static/js/async/xn.6510c0abe2.js +0 -2
  382. package/dist/client/static/js/async/xn.6510c0abe2.js.LICENSE.txt +0 -6
  383. package/dist/client/static/js/async/xn1.83a7784fe1.js +0 -10
  384. package/dist/client/static/js/async/xn1.83a7784fe1.js.LICENSE.txt +0 -11
  385. package/dist/client/static/js/async/xnl.feb0021e50.js +0 -2
  386. package/dist/client/static/js/async/xnl.feb0021e50.js.LICENSE.txt +0 -12
  387. package/dist/client/static/js/async/y.1a9ed7a5d8.js +0 -1
  388. package/dist/client/static/js/async/y6.43c57defe4.js +0 -1
  389. package/dist/client/static/js/async/yb.8c1342401d.js +0 -1
  390. package/dist/client/static/js/async/ybq.4491f2f2dc.js +0 -1
  391. package/dist/client/static/js/async/yg.35bebd6d0c.js +0 -149
  392. package/dist/client/static/js/async/yq.917ddd7ba2.js +0 -1
  393. package/dist/client/static/js/async/ys.00687cf82d.js +0 -38
  394. package/dist/client/static/js/async/yv.d99d829136.js +0 -1
  395. package/dist/client/static/js/async/z.56f61a4374.js +0 -1
  396. package/dist/client/static/js/async/zi.5fdb07b86a.js +0 -24
  397. package/dist/client/static/js/async/zj.654f37e8ff.js +0 -1
  398. package/dist/client/static/js/async/zn.966d6f1cc6.js +0 -1
  399. package/dist/client/static/js/async/zo.26213b40c0.js +0 -1
  400. package/dist/client/static/js/async/zx.e9f4105e00.js +0 -1
  401. package/dist/client/static/js/index.e6a257402a.js +0 -38
  402. package/dist/client/static/js/index.e6a257402a.js.LICENSE.txt +0 -4
  403. package/dist/client/static/js/markdown-vendor.71e43278b3.js +0 -144
  404. package/dist/client/static/js/shiki-vendor.cb8452ddcd.js +0 -1
  405. package/src/client/components/settings/categories/provider-settings/Icons.tsx +0 -35
  406. package/src/client/hooks/settings/providers.ts +0 -326
  407. package/src/server/plugins/dev-assets.ts +0 -49
  408. package/src/shared/lib/code/editor-utils.ts +0 -16
  409. package/src/shared/lib/models/provider-presets.ts +0 -29
  410. /package/dist/client/{apple-touch-icon.png → static/apple-touch-icon-dskx5sr3.png} +0 -0
  411. /package/dist/client/{icon-192x192.png → static/icon-192x192-6gn5cr2f.png} +0 -0
  412. /package/dist/client/{icon.svg → static/icon-d07z9dtz.svg} +0 -0
  413. /package/dist/client/{manifest.webmanifest → static/manifest-bnyy75kt.webmanifest} +0 -0
  414. /package/{dist/client → public}/icon-512x512.png +0 -0
  415. /package/{dist/client → public}/icon-maskable-512x512.png +0 -0
  416. /package/{dist/client → public}/sw.js +0 -0
  417. /package/src/{client/components/common/code-editor → shared/lib/code/editor}/history.ts +0 -0
  418. /package/src/shared/lib/models/{provider-cleanup.ts → provider/cleanup.ts} +0 -0
  419. /package/src/shared/lib/models/{provider-label.ts → provider/label.ts} +0 -0
@@ -0,0 +1,35 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The editor's reset stylesheet.
8
+ *
9
+ * The rule is what makes the editor work at all: the `<textarea>` must paint the
10
+ * caret and the selection while its glyphs stay invisible. A regression here is
11
+ * silent in review (it is a string) and immediately visible in the app, so it is
12
+ * asserted directly — the two selectors and the one deliberate exception.
13
+ */
14
+
15
+ import { describe, expect, test } from 'bun:test';
16
+
17
+ import { CODE_EDITOR_RESET_CSS } from '@/shared/lib/code/editor/reset-css';
18
+
19
+ describe('CODE_EDITOR_RESET_CSS', () => {
20
+ test('hides the textarea’s own glyphs', () => {
21
+ expect(CODE_EDITOR_RESET_CSS).toContain('-webkit-text-fill-color: transparent');
22
+ });
23
+
24
+ test('keeps the placeholder visible, since no <pre> sits behind it', () => {
25
+ // Without this exception an empty editor's hint inherits the transparent
26
+ // fill and the field looks blank rather than empty.
27
+ expect(CODE_EDITOR_RESET_CSS).toContain('.code-editor-native-textarea::placeholder');
28
+ expect(CODE_EDITOR_RESET_CSS).toContain('var(--theme-ink)');
29
+ });
30
+
31
+ test('is scoped to the editor’s own class, not to every textarea', () => {
32
+ expect(CODE_EDITOR_RESET_CSS).not.toMatch(/^\s*textarea\s*\{/m);
33
+ expect(CODE_EDITOR_RESET_CSS).toContain('.code-editor-native-textarea {');
34
+ });
35
+ });
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The stylesheet the editor's two layers depend on.
8
+ *
9
+ * `CODE_EDITOR_RESET_CSS` is a rule, not a component: the transparent
10
+ * `<textarea>` paints the caret and the selection while its glyphs must stay
11
+ * invisible (the highlighted `<pre>` underneath supplies them), and the
12
+ * placeholder is the one thing that must remain visible. Keeping the rule in a
13
+ * `.ts` module rather than beside a JSX `<style>` element means a test can
14
+ * assert it without a DOM — and there is nothing to render but this string.
15
+ */
16
+
17
+ /**
18
+ * The reset rule, scoped to the textarea's own class.
19
+ *
20
+ * `::placeholder` is deliberately excluded: an empty editor's hint has no
21
+ * `<pre>` behind it, so it keeps the theme's ink and a readable opacity
22
+ * instead of inheriting the transparent fill.
23
+ */
24
+ export const CODE_EDITOR_RESET_CSS = `
25
+ .code-editor-native-textarea {
26
+ -webkit-text-fill-color: transparent;
27
+ }
28
+ .code-editor-native-textarea::placeholder {
29
+ -webkit-text-fill-color: var(--theme-ink);
30
+ opacity: 0.45;
31
+ }
32
+ `;
@@ -0,0 +1,178 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The text-command dispatch.
8
+ *
9
+ * `runTextCommand` is the single entry point both the keyboard and the command
10
+ * palette go through, so what it asserts is the ROUTING: which commands it
11
+ * claims, which it leaves to the component (Escape, undo, Tab) and which it
12
+ * hands back to the panel. A command that returns `false` silently does nothing
13
+ * when a chord reaches it, which is why the membership table is asserted
14
+ * directly rather than only through the commands themselves.
15
+ */
16
+
17
+ import { describe, expect, test } from 'bun:test';
18
+
19
+ import { isTextCommand, runTextCommand, type CommandContext } from '@/shared/lib/code/editor/text-commands';
20
+ import type { EditorCommand } from '@/shared/lib/code/editor/keymap';
21
+
22
+ interface Harness {
23
+ ctx: CommandContext;
24
+ committed: Array<{ value: string; selectionStart: number; selectionEnd: number }>;
25
+ carets: number[];
26
+ selections: Array<[number, number]>;
27
+ edges: string[];
28
+ }
29
+
30
+ function harness(options: { value: string; start: number; end?: number; extend?: boolean; language?: string }): Harness {
31
+ const committed: Harness['committed'] = [];
32
+ const carets: number[] = [];
33
+ const selections: Array<[number, number]> = [];
34
+ const edges: string[] = [];
35
+ return {
36
+ committed,
37
+ carets,
38
+ selections,
39
+ edges,
40
+ ctx: {
41
+ value: options.value,
42
+ selectionStart: options.start,
43
+ selectionEnd: options.end ?? options.start,
44
+ extend: options.extend ?? false,
45
+ language: options.language ?? 'typescript',
46
+ commit: (edit) => committed.push(edit),
47
+ setCaret: (offset) => carets.push(offset),
48
+ selectRange: (start, end) => selections.push([start, end]),
49
+ setDocumentEdge: (edge) => edges.push(edge),
50
+ },
51
+ };
52
+ }
53
+
54
+ describe('isTextCommand', () => {
55
+ test('claims the line and caret commands', () => {
56
+ for (const command of ['moveLineUp', 'copyLineDown', 'deleteLine', 'toggleComment', 'indent', 'lineStart'] as const) {
57
+ expect(isTextCommand(command)).toBe(true);
58
+ }
59
+ });
60
+
61
+ test('leaves the DOM-owned and panel-owned commands alone', () => {
62
+ // Undo/redo step the history, Escape is about focus, and the find-bar chords
63
+ // belong to the panel — none of them is buffer arithmetic.
64
+ for (const command of ['undo', 'redo', 'escape', 'find', 'selectNextOccurrence', 'toggleWordWrap'] as const) {
65
+ expect(isTextCommand(command)).toBe(false);
66
+ }
67
+ });
68
+ });
69
+
70
+ describe('runTextCommand', () => {
71
+ test('reports an unowned command rather than pretending to run it', () => {
72
+ const h = harness({ value: 'one', start: 0 });
73
+ expect(runTextCommand('find', h.ctx)).toBe(false);
74
+ expect(h.committed).toEqual([]);
75
+ });
76
+
77
+ test('moves a line through the commit path, not by writing the field', () => {
78
+ const h = harness({ value: 'one\ntwo', start: 4, end: 7 });
79
+ expect(runTextCommand('moveLineUp', h.ctx)).toBe(true);
80
+
81
+ expect(h.committed[0].value).toBe('two\none');
82
+ });
83
+
84
+ test('indents the caret’s whole line, not the text at the caret', () => {
85
+ // ⌘] is a line command: on a bare caret it widens to that line first, which
86
+ // is what keeps it from inserting spaces mid-line.
87
+ const h = harness({ value: 'const a = 1;', start: 10 });
88
+ runTextCommand('indent', h.ctx);
89
+
90
+ expect(h.committed[0].value).toBe(' const a = 1;');
91
+ });
92
+
93
+ test('indents every line of a selection', () => {
94
+ const h = harness({ value: 'one\ntwo', start: 0, end: 7 });
95
+ runTextCommand('indent', h.ctx);
96
+
97
+ expect(h.committed[0].value).toBe(' one\n two');
98
+ });
99
+
100
+ test('outdenting a line that has no indent commits nothing', () => {
101
+ const h = harness({ value: 'one', start: 1 });
102
+ runTextCommand('outdent', h.ctx);
103
+
104
+ expect(h.committed).toEqual([]);
105
+ });
106
+
107
+ test('comments with the language’s own token', () => {
108
+ const h = harness({ value: 'key: value', start: 0, language: 'yaml' });
109
+ runTextCommand('toggleComment', h.ctx);
110
+
111
+ expect(h.committed[0].value).toBe('# key: value');
112
+ });
113
+
114
+ test('caret moves collapse the selection', () => {
115
+ const h = harness({ value: 'one\ntwo', start: 5 });
116
+ runTextCommand('lineStart', h.ctx);
117
+
118
+ expect(h.carets).toEqual([4]);
119
+ expect(h.selections).toEqual([]);
120
+ });
121
+
122
+ test('shifted caret moves extend instead', () => {
123
+ const h = harness({ value: 'one\ntwo', start: 5, extend: true });
124
+ runTextCommand('lineEnd', h.ctx);
125
+
126
+ expect(h.selections).toEqual([[5, 7]]);
127
+ expect(h.carets).toEqual([]);
128
+ });
129
+
130
+ test('file edges go through the document-edge path', () => {
131
+ const h = harness({ value: 'one\ntwo', start: 5 });
132
+ runTextCommand('documentStart', h.ctx);
133
+ runTextCommand('documentEnd', h.ctx);
134
+
135
+ expect(h.edges).toEqual(['start', 'end']);
136
+ });
137
+
138
+ test('a shifted document move selects instead of jumping', () => {
139
+ const h = harness({ value: 'one\ntwo', start: 5, extend: true });
140
+ runTextCommand('documentStart', h.ctx);
141
+
142
+ expect(h.selections).toEqual([[0, 5]]);
143
+ expect(h.edges).toEqual([]);
144
+ });
145
+
146
+ test('the palette reaches the same commands with no event at all', () => {
147
+ // This is the path the command palette uses; a command reachable by chord
148
+ // must be reachable this way too.
149
+ const h = harness({ value: 'one\ntwo', start: 0, end: 3 });
150
+ runTextCommand('copyLineDown', h.ctx);
151
+
152
+ expect(h.committed[0].value).toBe('one\none\ntwo');
153
+ });
154
+ });
155
+
156
+ describe('command coverage', () => {
157
+ test('every text command is one the editor keymap can produce', () => {
158
+ // A command in the table that no key or palette entry names would be dead
159
+ // code; this is the check that keeps the table honest.
160
+ const reachable: EditorCommand[] = [
161
+ 'moveLineUp',
162
+ 'moveLineDown',
163
+ 'copyLineUp',
164
+ 'copyLineDown',
165
+ 'deleteLine',
166
+ 'toggleComment',
167
+ 'insertLineAbove',
168
+ 'insertLineBelow',
169
+ 'indent',
170
+ 'outdent',
171
+ 'lineStart',
172
+ 'lineEnd',
173
+ 'documentStart',
174
+ 'documentEnd',
175
+ ];
176
+ for (const command of reachable) expect(isTextCommand(command)).toBe(true);
177
+ });
178
+ });
@@ -0,0 +1,154 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The editor's text commands, as a dispatch table.
8
+ *
9
+ * Extracted from the component because it is dispatch rather than rendering,
10
+ * and because it keeps the component under the repository's line ceiling. The
11
+ * component still owns the keys whose behaviour is about the DOM node rather
12
+ * than the buffer — Escape, Tab, Backspace, Enter, undo/redo — and calls in
13
+ * here for everything that is pure string arithmetic.
14
+ */
15
+
16
+ import {
17
+ commentSyntaxFor,
18
+ deleteLines,
19
+ duplicateLines,
20
+ insertLine,
21
+ lineEdges,
22
+ moveLines,
23
+ toggleLineComment,
24
+ type TextRange,
25
+ } from '@/shared/lib/code/editor/commands';
26
+ import { editForTab } from '@/shared/lib/code/editor/edits';
27
+ import { lineRangeAt } from '@/shared/lib/code/editor/lines';
28
+ import type { EditorCommand } from '@/shared/lib/code/editor/keymap';
29
+
30
+ /** A whole-buffer edit plus where the caret lands. */
31
+ export interface CommittedEdit {
32
+ value: string;
33
+ selectionStart: number;
34
+ selectionEnd: number;
35
+ }
36
+
37
+ export interface CommandContext {
38
+ /** Current buffer and selection, read from the live textarea. */
39
+ value: string;
40
+ selectionStart: number;
41
+ selectionEnd: number;
42
+ /** True while the event carried Shift (⌘↑ with Shift extends instead of moving). */
43
+ extend: boolean;
44
+ /** Shiki language id, which picks the comment syntax. */
45
+ language: string;
46
+ /** Commit a whole-buffer edit as ONE undoable step. */
47
+ commit: (edit: CommittedEdit) => void;
48
+ /** Move the caret, collapsing any selection. */
49
+ setCaret: (offset: number) => void;
50
+ /** Move the caret while keeping the selection's other end anchored. */
51
+ selectRange: (start: number, end: number) => void;
52
+ /** Jump to the buffer's first or last offset. */
53
+ setDocumentEdge: (edge: 'start' | 'end') => void;
54
+ }
55
+
56
+ /**
57
+ * Commands that edit or move within the buffer. Everything else (the find bar's
58
+ * chords, word wrap, save) belongs to the panel and is delegated by the caller.
59
+ */
60
+ const TEXT_COMMANDS: Record<string, true> = {
61
+ moveLineUp: true,
62
+ moveLineDown: true,
63
+ copyLineUp: true,
64
+ copyLineDown: true,
65
+ deleteLine: true,
66
+ toggleComment: true,
67
+ insertLineAbove: true,
68
+ insertLineBelow: true,
69
+ indent: true,
70
+ outdent: true,
71
+ lineStart: true,
72
+ lineEnd: true,
73
+ documentStart: true,
74
+ documentEnd: true,
75
+ };
76
+
77
+ export function isTextCommand(command: EditorCommand): boolean {
78
+ return TEXT_COMMANDS[command] === true;
79
+ }
80
+
81
+ /**
82
+ * Run one text command. Returns true when the key was consumed.
83
+ *
84
+ * The buffer-changing branches all commit through `ctx.commit`, which is the
85
+ * component's single write path: one undo entry per command, and the highlight
86
+ * layer follows because the commit goes through the controlled value.
87
+ */
88
+ export function runTextCommand(command: EditorCommand, ctx: CommandContext): boolean {
89
+ const selection: TextRange = { start: ctx.selectionStart, end: ctx.selectionEnd };
90
+
91
+ switch (command) {
92
+ case 'moveLineUp':
93
+ ctx.commit(moveLines(ctx.value, selection, -1));
94
+ return true;
95
+ case 'moveLineDown':
96
+ ctx.commit(moveLines(ctx.value, selection, 1));
97
+ return true;
98
+ case 'copyLineUp':
99
+ ctx.commit(duplicateLines(ctx.value, selection, -1));
100
+ return true;
101
+ case 'copyLineDown':
102
+ ctx.commit(duplicateLines(ctx.value, selection, 1));
103
+ return true;
104
+ case 'deleteLine':
105
+ ctx.commit(deleteLines(ctx.value, selection));
106
+ return true;
107
+ case 'toggleComment':
108
+ ctx.commit(toggleLineComment(ctx.value, selection, commentSyntaxFor(ctx.language)));
109
+ return true;
110
+ case 'insertLineAbove':
111
+ ctx.commit(insertLine(ctx.value, ctx.selectionStart, 'above'));
112
+ return true;
113
+ case 'insertLineBelow':
114
+ ctx.commit(insertLine(ctx.value, ctx.selectionStart, 'below'));
115
+ return true;
116
+
117
+ case 'indent':
118
+ case 'outdent': {
119
+ // ⌘] / ⌘[ act on the LINES the selection touches — on a bare caret that
120
+ // is the caret's own line, which is why the range is widened first. Tab
121
+ // itself still indents at the caret (that is what Tab means in an
122
+ // editor), but a line command that shifted text mid-line would not be
123
+ // indenting anything.
124
+ const range = ctx.selectionStart === ctx.selectionEnd ? lineRangeAt(ctx.value, ctx.selectionStart) : selection;
125
+ const edit = editForTab(ctx.value, range.start, range.end, command === 'outdent');
126
+ if (edit.value !== ctx.value) ctx.commit(edit);
127
+ return true;
128
+ }
129
+
130
+ case 'lineStart': {
131
+ const edges = lineEdges(ctx.value, ctx.selectionStart);
132
+ if (ctx.extend) ctx.selectRange(edges.start, ctx.selectionEnd);
133
+ else ctx.setCaret(edges.start);
134
+ return true;
135
+ }
136
+ case 'lineEnd': {
137
+ const edges = lineEdges(ctx.value, ctx.selectionStart);
138
+ if (ctx.extend) ctx.selectRange(ctx.selectionStart, edges.end);
139
+ else ctx.setCaret(edges.end);
140
+ return true;
141
+ }
142
+ case 'documentStart':
143
+ if (ctx.extend) ctx.selectRange(0, ctx.selectionEnd);
144
+ else ctx.setDocumentEdge('start');
145
+ return true;
146
+ case 'documentEnd':
147
+ if (ctx.extend) ctx.selectRange(ctx.selectionStart, ctx.value.length);
148
+ else ctx.setDocumentEdge('end');
149
+ return true;
150
+
151
+ default:
152
+ return false;
153
+ }
154
+ }
@@ -6,6 +6,7 @@
6
6
  import { beforeAll, describe, expect, test } from 'bun:test';
7
7
 
8
8
  import { bootSyntax, onLanguageReady, requestLanguage } from '@/shared/lib/code/highlighter';
9
+ import { findMatches } from '@/shared/lib/code/editor/find';
9
10
  import { getLanguageFromPath, highlightCode, highlightLines } from '@/shared/lib/code/syntax-highlight';
10
11
  import { highlightCodeWindow } from '@/shared/lib/code/windowed-highlight';
11
12
 
@@ -161,6 +162,80 @@ describe('highlightCodeWindow', () => {
161
162
  });
162
163
  });
163
164
 
165
+ describe('find marks', () => {
166
+ const code = 'const alpha = 1;\nconst beta = alpha + 2;\n';
167
+ const PLAIN = { matchCase: false, wholeWord: false, isRegex: false };
168
+ /** The text of every mark, in order. A match spanning tokens is several
169
+ * fragments, so the assertion is on the fragments JOINED. */
170
+ const marked = (html: string): string[] =>
171
+ Array.from(html.matchAll(/<mark class="find-match"[^>]*>(.*?)<\/mark>/g), (match) => match[1]);
172
+
173
+ test('splits a match across the tokens it spans, and marks every fragment current', () => {
174
+ // `const alpha` is three tokens (`const`, ` `, `alpha`): each carries its
175
+ // own fragment of the one match, which is why the CSS may not draw a
176
+ // vertical edge per fragment.
177
+ const line = highlightLines(code, 'typescript', { marks: [{ start: 0, end: 11 }], currentMark: 0 })[0];
178
+
179
+ expect(marked(line).join('')).toBe('const alpha');
180
+ expect(line.match(/data-find-current/g)?.length).toBe(3);
181
+ expect(line.match(/<\/mark>/g)?.length).toBe(3);
182
+ });
183
+
184
+ test('marks every occurrence, and only the current one as current', () => {
185
+ const marks = findMatches(code, 'alpha', PLAIN).matches;
186
+ const lines = highlightLines(code, 'typescript', { marks, currentMark: 1 });
187
+
188
+ expect(marked(lines[0]).join('')).toBe('alpha');
189
+ expect(marked(lines[1]).join('')).toBe('alpha');
190
+ expect(lines[0]).not.toContain('data-find-current');
191
+ expect(lines[1]).toContain('data-find-current');
192
+ });
193
+
194
+ test('escapes the matched text it wraps', () => {
195
+ const line = highlightLines('a < b', 'text', { marks: [{ start: 2, end: 3 }], currentMark: 0 })[0];
196
+
197
+ expect(line).toContain('<mark class="find-match" data-find-current="">&lt;</mark>');
198
+ });
199
+
200
+ test('leaves the markup untouched when there are no marks', () => {
201
+ const plain = highlightLines(code, 'typescript')[0];
202
+ const empty = highlightLines(code, 'typescript', { marks: [], currentMark: -1 })[0];
203
+
204
+ expect(empty).toBe(plain);
205
+ expect(plain).not.toContain('<mark');
206
+ });
207
+
208
+ test('keeps a windowed match on its own line, re-based to the slice', () => {
209
+ const marks = findMatches(code, 'alpha', PLAIN).matches;
210
+ // The window renders line 1 only. The match on line 0 must not reappear on
211
+ // line 1's text, and the current index (line 0's match) must not mark
212
+ // anything here.
213
+ const window = highlightCodeWindow(code, 'typescript', { start: 1, end: 2 }, { marks, currentMark: 0 });
214
+
215
+ expect(marked(window).join('')).toBe('alpha');
216
+ expect(window).not.toContain('data-find-current');
217
+ });
218
+
219
+ test('re-bases the current index onto the marks inside the window', () => {
220
+ const marks = findMatches(code, 'alpha', PLAIN).matches;
221
+ const window = highlightCodeWindow(code, 'typescript', { start: 1, end: 2 }, { marks, currentMark: 1 });
222
+
223
+ expect(window.match(/data-find-current/g)?.length).toBe(1);
224
+ expect(marked(window).join('')).toBe('alpha');
225
+ });
226
+
227
+ test('a mark reaching past the window is clipped to it', () => {
228
+ // The last line has no trailing newline, so a mark that runs to the end of
229
+ // the slice would be clamped by the rebase.
230
+ const window = highlightCodeWindow(code, 'typescript', { start: 1, end: 3 }, {
231
+ marks: [{ start: 30, end: 999 }],
232
+ currentMark: 0,
233
+ });
234
+
235
+ expect(marked(window).join('')).toBe('alpha + 2;');
236
+ });
237
+ });
238
+
164
239
  describe('getLanguageFromPath', () => {
165
240
  test.each([
166
241
  ['a.ts', 'typescript'],
@@ -1,8 +1,12 @@
1
1
  import type { HighlighterCore } from 'shiki/core';
2
2
 
3
3
  import { escapeCode, getHighlighterSync, requestLanguage } from '@/shared/lib/code/highlighter';
4
+ import { tokenMarkup, type HighlightLinesOptions } from '@/shared/lib/code/editor/marks';
5
+ import { lineStartOffset } from '@/shared/lib/code/editor/lines';
4
6
  import { SHIKI_THEMES } from '@/shared/lib/code/shiki-themes';
5
7
 
8
+ export type { HighlightLinesOptions };
9
+
6
10
  /** Map file extension to Shiki language id */
7
11
  export function getLanguageFromPath(filePath?: string): string {
8
12
  if (!filePath) return 'javascript';
@@ -139,8 +143,13 @@ function resolveReadyLang(hl: HighlighterCore, language: string): string {
139
143
  }
140
144
 
141
145
  /** Syntax highlight code string safely with fallback */
142
- export function highlightCode(code: string, language = 'javascript'): string {
146
+ export function highlightCode(code: string, language = 'javascript', options: HighlightLinesOptions = {}): string {
143
147
  if (!code) return '';
148
+ // A find in progress needs the match markup, which only the line-based path
149
+ // emits; without it the fast `codeToHtml` path is left exactly as it was.
150
+ if (options.marks && options.marks.length > 0) {
151
+ return `<span class="shiki">${highlightLines(code, language, options).join('\n')}</span>`;
152
+ }
144
153
  const hl = getHighlighterSync();
145
154
  if (!hl) return escapeCode(code);
146
155
  const lang = resolveReadyLang(hl, language);
@@ -268,14 +277,6 @@ export function highlightJson(code: string): string {
268
277
  return highlightCode(code, 'json');
269
278
  }
270
279
 
271
- export interface HighlightLinesOptions {
272
- /**
273
- * Lines at or above this length are emitted as plain text instead of being
274
- * tokenized — the guard a minified bundle (one line of hundreds of KB) needs.
275
- */
276
- maxLineLength?: number;
277
- }
278
-
279
280
  /**
280
281
  * Highlight code and return one HTML fragment per line, aligned 1:1 with
281
282
  * `code.split('\n')`. Tokenizes the whole input at once so multi-line grammar
@@ -287,9 +288,18 @@ export function highlightLines(
287
288
  options: HighlightLinesOptions = {},
288
289
  ): string[] {
289
290
  const rawLines = code.split('\n');
291
+ const marks = options.marks;
292
+ const currentMark = options.currentMark ?? -1;
293
+ const occurrenceFrom = options.occurrenceFrom ?? -1;
294
+ // A grammar that has not arrived yet, or a highlighter that failed to boot,
295
+ // still gets the find marks: the widget's feedback must not depend on Shiki.
296
+ const plain = (lineIndex: number): string => {
297
+ if (!marks || marks.length === 0) return escapeCode(rawLines[lineIndex]);
298
+ return tokenMarkup(rawLines[lineIndex], lineStartOffset(code, lineIndex), marks, currentMark, occurrenceFrom);
299
+ };
290
300
  if (!code) return rawLines;
291
301
  const hl = getHighlighterSync();
292
- if (!hl) return rawLines.map(escapeCode);
302
+ if (!hl) return rawLines.map((_line, index) => plain(index));
293
303
  const lang = resolveReadyLang(hl, language);
294
304
  try {
295
305
  const { tokens } = hl.codeToTokens(code, {
@@ -298,20 +308,22 @@ export function highlightLines(
298
308
  defaultColor: false,
299
309
  tokenizeMaxLineLength: options.maxLineLength ?? 0,
300
310
  });
301
- if (tokens.length !== rawLines.length) return rawLines.map(escapeCode);
302
- return tokens.map((lineTokens) =>
303
- lineTokens
311
+ if (tokens.length !== rawLines.length) return rawLines.map((_line, index) => plain(index));
312
+ return tokens.map((lineTokens, lineIndex) => {
313
+ let offset = lineStartOffset(code, lineIndex);
314
+ return lineTokens
304
315
  .map((tok) => {
305
- const content = escapeCode(tok.content);
316
+ const content = tokenMarkup(tok.content, offset, marks, currentMark, occurrenceFrom);
317
+ offset += tok.content.length;
306
318
  if (!tok.htmlStyle) return content;
307
319
  const style = Object.entries(tok.htmlStyle)
308
320
  .map(([key, value]) => `${key}:${value}`)
309
321
  .join(';');
310
322
  return style ? `<span style="${style}">${content}</span>` : content;
311
323
  })
312
- .join('')
313
- );
324
+ .join('');
325
+ });
314
326
  } catch {
315
- return rawLines.map(escapeCode);
327
+ return rawLines.map((_line, index) => plain(index));
316
328
  }
317
329
  }
@@ -4,6 +4,8 @@
4
4
  */
5
5
 
6
6
  import type { LineWindow } from '@/shared/lib/code/lazy-window';
7
+ import type { FindMatch } from '@/shared/lib/code/editor/find';
8
+ import { lineStartOffset } from '@/shared/lib/code/editor/lines';
7
9
  import { highlightLines } from '@/shared/lib/code/syntax-highlight';
8
10
 
9
11
  /**
@@ -28,6 +30,15 @@ export interface HighlightCodeWindowOptions {
28
30
  contextLines?: number;
29
31
  /** Length at which a line falls back to plain text. */
30
32
  maxLineLength?: number;
33
+ /**
34
+ * Find matches in DOCUMENT offsets. Only those inside the rendered window are
35
+ * passed on, re-based to the sliced source the tokenizer sees.
36
+ */
37
+ marks?: readonly FindMatch[];
38
+ /** Index of the current match within `marks`. */
39
+ currentMark?: number;
40
+ /** Marks from this index on are extra ⌘D selections rather than find matches. */
41
+ occurrenceFrom?: number;
31
42
  }
32
43
 
33
44
  /**
@@ -57,7 +68,47 @@ export function highlightCodeWindow(
57
68
  if (start === end) return '';
58
69
 
59
70
  const contextStart = Math.max(0, start - contextLines);
60
- const rows = highlightLines(lines.slice(contextStart, end).join('\n'), language, { maxLineLength });
71
+ const slice = lines.slice(contextStart, end).join('\n');
72
+ const base = lineStartOffset(code, contextStart);
73
+ const sliceEnd = base + slice.length;
74
+ const { marks, currentMark, occurrenceFrom } = rebaseMarks(
75
+ options.marks,
76
+ options.currentMark ?? -1,
77
+ options.occurrenceFrom ?? -1,
78
+ base,
79
+ sliceEnd,
80
+ );
81
+ const rows = highlightLines(slice, language, { maxLineLength, marks, currentMark, occurrenceFrom });
61
82
  const offset = start - contextStart;
62
83
  return `<span class="shiki">${rows.slice(offset, offset + (end - start)).join('\n')}</span>`;
63
84
  }
85
+
86
+ /**
87
+ * The marks inside `[base, sliceEnd)`, shifted to slice coordinates, with the
88
+ * current-match index remapped — the index points into the full list, so the
89
+ * rows below the window cannot simply reuse it.
90
+ */
91
+ function rebaseMarks(
92
+ marks: readonly FindMatch[] | undefined,
93
+ currentMark: number,
94
+ occurrenceFrom: number,
95
+ base: number,
96
+ sliceEnd: number,
97
+ ): { marks: FindMatch[]; currentMark: number; occurrenceFrom: number } {
98
+ if (!marks || marks.length === 0) return { marks: [], currentMark: -1, occurrenceFrom: -1 };
99
+ const kept: FindMatch[] = [];
100
+ let remapped = -1;
101
+ let remappedOccurrenceFrom = -1;
102
+ for (let index = 0; index < marks.length; index++) {
103
+ const mark = marks[index];
104
+ if (mark.end <= base) continue;
105
+ if (mark.start >= sliceEnd) break;
106
+ if (index === currentMark) remapped = kept.length;
107
+ // The first surviving occurrence keeps the split, wherever it landed: a
108
+ // window whose opening lines held only matches still has to paint the
109
+ // selections below them differently.
110
+ if (remappedOccurrenceFrom === -1 && occurrenceFrom >= 0 && index >= occurrenceFrom) remappedOccurrenceFrom = kept.length;
111
+ kept.push({ start: mark.start - base, end: Math.min(mark.end, sliceEnd) - base });
112
+ }
113
+ return { marks: kept, currentMark: remapped, occurrenceFrom: remappedOccurrenceFrom };
114
+ }