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,77 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * What to TELL the user after a provider was added or re-fetched.
8
+ *
9
+ * The message depends on the shape that was written, not on the model count,
10
+ * because two of the three shapes have no local model list BY DESIGN: a
11
+ * discovery provider is listed by omp at runtime, and an endpoint override
12
+ * leaves the models to omp's own catalog. Reporting "0 models" for either made
13
+ * a registration that succeeded read as a failure.
14
+ */
15
+
16
+ import type { ProviderItem } from '@/shared/types';
17
+
18
+ /** The success/failure line for an Add Provider submission. */
19
+ export function addProviderMessage(
20
+ provider: ProviderItem,
21
+ options: { fetchedCount: number; ompNote?: string },
22
+ ): { text: string; tone: 'success' | 'error' } {
23
+ if (provider.modelSource === 'discovery') {
24
+ return { text: `${provider.name} registered — omp discovers its models on each run.`, tone: 'success' };
25
+ }
26
+ if (provider.modelSource === 'override') {
27
+ return {
28
+ text: `${provider.name} endpoint registered — omp keeps its own model list for this provider.`,
29
+ tone: 'success',
30
+ };
31
+ }
32
+ if (options.fetchedCount > 0) {
33
+ return {
34
+ text: `Fetched ${options.fetchedCount} new model${options.fetchedCount === 1 ? '' : 's'} from the provider.`,
35
+ tone: 'success',
36
+ };
37
+ }
38
+ if (options.fetchedCount === -1) {
39
+ // The listing failed. When omp reported a reason it is the actionable half,
40
+ // so it is quoted rather than replaced by a generic line.
41
+ return {
42
+ text: options.ompNote
43
+ ? `Provider registered, but models could not be listed: ${options.ompNote}`
44
+ : 'Auto-fetch models failed — provider added with default models.',
45
+ tone: 'error',
46
+ };
47
+ }
48
+ return { text: `${provider.name} registered — no new models to add.`, tone: 'success' };
49
+ }
50
+
51
+ /**
52
+ * The extra line reported after a manual "fetch models": what the chamber wrote
53
+ * into `models.yml` versus what omp already had. A file write and a model-count
54
+ * change are different outcomes, and conflating them hid a failed write.
55
+ */
56
+ export function fetchModelsNote(result: {
57
+ omp?: { written: boolean; addedCount: number; backfilledCount: number; reason?: string };
58
+ }): { text: string; tone: 'success' | 'error' } | null {
59
+ if (!result.omp) return null;
60
+ if (result.omp.written) {
61
+ const parts: string[] = [];
62
+ if (result.omp.addedCount > 0) {
63
+ parts.push(`${result.omp.addedCount} new model${result.omp.addedCount === 1 ? '' : 's'} registered`);
64
+ }
65
+ if (result.omp.backfilledCount > 0) {
66
+ parts.push(`${result.omp.backfilledCount} model${result.omp.backfilledCount === 1 ? '' : 's'} enriched`);
67
+ }
68
+ return {
69
+ text: `omp models.yml updated${parts.length > 0 ? `: ${parts.join(', ')}` : ''}.`,
70
+ tone: 'success',
71
+ };
72
+ }
73
+ if (result.omp.reason) {
74
+ return { text: `omp models.yml not updated: ${result.omp.reason}`, tone: 'error' };
75
+ }
76
+ return null;
77
+ }
@@ -5,24 +5,29 @@ export interface UsageReportState {
5
5
  report: UsageReport | null;
6
6
  isLoading: boolean;
7
7
  error: string | null;
8
- reload: (opts?: { silent?: boolean }) => void;
8
+ reload: (opts?: { silent?: boolean; force?: boolean }) => void;
9
9
  }
10
10
 
11
11
  /**
12
12
  * Shared `GET /api/settings/usage` loader for the Settings → Usage panel and
13
13
  * the Usage right panel. Both surfaces mount independently, so each keeps its
14
14
  * own copy of the last successful report until reload completes.
15
+ *
16
+ * `force` asks the server to re-probe the providers instead of answering from
17
+ * its one-minute quota cache — used by the user's own Refresh button, never by
18
+ * the background poll.
15
19
  */
16
20
  export function useUsageReport(): UsageReportState {
17
21
  const [report, setReport] = useState<UsageReport | null>(null);
18
22
  const [isLoading, setIsLoading] = useState(true);
19
23
  const [error, setError] = useState<string | null>(null);
20
24
 
21
- const reload = useCallback(async (opts?: { silent?: boolean }) => {
25
+ const reload = useCallback(async (opts?: { silent?: boolean; force?: boolean }) => {
22
26
  if (!opts?.silent) setIsLoading(true);
23
27
  setError(null);
24
28
  try {
25
- const response = await fetch('/api/settings/usage');
29
+ const url = opts?.force ? '/api/settings/usage?refresh=1' : '/api/settings/usage';
30
+ const response = await fetch(url);
26
31
  if (!response.ok) throw new Error(`Usage request failed (${response.status})`);
27
32
  const data = (await response.json()) as UsageReport;
28
33
  setReport(data);
@@ -35,9 +35,9 @@ export function currentThemeId(): string {
35
35
  * Put the catalog on the document before the first render.
36
36
  *
37
37
  * The SSR shell already inlines the stylesheet and both attributes; this is the
38
- * guard for a document that predates them — a cached page, or `dist/client/
39
- * index.html` left over from an older build while only the server was restarted.
40
- * Without it the page keeps `:root`'s default palette and no variant attribute,
38
+ * guard for a document that predates them — a cached page, or markup rendered
39
+ * before the server restarted. Without it the page keeps `:root`'s default
40
+ * palette and no variant attribute,
41
41
  * which is a wrong palette, not merely a stale one.
42
42
  *
43
43
  * Called once from `main.tsx` rather than from a hook: the only components that
@@ -2,8 +2,18 @@ import { useCallback, useEffect, useRef } from 'preact/hooks';
2
2
  import { useSessionState } from '@/client/hooks/workspace/session-state';
3
3
  import { useSessionStateContext } from '@/client/hooks/workspace/session-state/context';
4
4
  import { useChamberEvent } from '@/client/hooks/ui/window-event';
5
+ import { diffTabId, diffTabName, fileTabId } from '@/shared/lib/workspace/file-tab-id';
5
6
  import type { OpenedFile } from '@/shared/types/fs';
6
7
 
8
+ export interface OpenFileInput {
9
+ id?: number | string;
10
+ name: string;
11
+ path: string;
12
+ content?: string;
13
+ root?: string;
14
+ repo?: string;
15
+ }
16
+
7
17
  export function useFileTabs(
8
18
  activeProjectPath: string | null,
9
19
  sessionId: string,
@@ -25,9 +35,12 @@ export function useFileTabs(
25
35
  setActiveFileId(null);
26
36
  }, [activeProjectPath, sessionId, layoutReady, setOpenedFiles, setActiveFileId]);
27
37
 
28
- const handleOpenFile = useCallback((file: any) => {
38
+ const handleOpenFile = useCallback((file: OpenFileInput) => {
29
39
  const fileEntry: OpenedFile = {
30
40
  ...file,
41
+ // One id scheme for a path, so a file opened from the explorer, a chat
42
+ // link and a converted diff tab all land on the same tab.
43
+ id: file.id ?? fileTabId(file.path),
31
44
  root: file.root ?? activeProjectPath ?? undefined,
32
45
  };
33
46
  setOpenedFiles(prev => {
@@ -52,13 +65,12 @@ export function useFileTabs(
52
65
  root?: string;
53
66
  }) => {
54
67
  const rawPath = diffInfo.file.replace(/^\/+/, '');
55
- const fileName = rawPath.split('/').pop() || rawPath;
56
68
  const isStaged = Boolean(diffInfo.staged);
57
- const diffId = `diff-${isStaged ? 'staged' : 'working'}-${rawPath}`;
69
+ const diffId = diffTabId(rawPath, isStaged);
58
70
 
59
71
  const diffEntry: OpenedFile = {
60
72
  id: diffId,
61
- name: `${fileName} (Diff)`,
73
+ name: diffTabName(rawPath),
62
74
  path: rawPath,
63
75
  isDiff: true,
64
76
  diffStatus: diffInfo.status || 'M',
@@ -91,6 +103,50 @@ export function useFileTabs(
91
103
  });
92
104
  }, [activeFileId, setOpenedFiles, setActiveFileId]);
93
105
 
106
+ /**
107
+ * Turn a diff tab into the plain editor tab for the same file.
108
+ *
109
+ * The panel used to do this by assigning `activeFile.isDiff = false` on the
110
+ * object it was handed. That object is the one inside `layout.openedFiles`,
111
+ * so the flag was mutated in place: React never saw a change (the array
112
+ * identity was identical), the tab kept rendering the diff until the next
113
+ * unrelated render, and — because the array is what gets persisted — the
114
+ * mutation reached the database. After a reload the "diff" tab came back as
115
+ * a file tab, and re-opening that file's diff found an entry that already
116
+ * matched, so the diff never came back at all.
117
+ *
118
+ * The target id is `fileTabId`, so if that file already has a tab the two
119
+ * entries would share an id and React would render duplicate keys. The diff
120
+ * entry is dropped in that case and the existing tab activated instead — the
121
+ * user asked to see the file, and it is already open.
122
+ */
123
+ const convertDiffToEditor = useCallback((diffId: number | string) => {
124
+ setOpenedFiles(prev => {
125
+ const index = prev.findIndex(f => f.id === diffId);
126
+ if (index === -1) return prev;
127
+ const diff = prev[index];
128
+ const fileId = fileTabId(diff.path);
129
+ const alreadyOpen = prev.find(f => f.id === fileId);
130
+
131
+ if (alreadyOpen) {
132
+ setActiveFileId(alreadyOpen.id);
133
+ return prev.filter(f => f.id !== diffId);
134
+ }
135
+
136
+ const next = [...prev];
137
+ next[index] = {
138
+ id: fileId,
139
+ name: diff.path.split('/').pop() || diff.name,
140
+ path: diff.path,
141
+ root: diff.root,
142
+ repo: diff.repo,
143
+ };
144
+ setActiveFileId(fileId);
145
+ return next;
146
+ });
147
+ onOpenTab?.();
148
+ }, [onOpenTab, setOpenedFiles, setActiveFileId]);
149
+
94
150
  // Global listeners for omp:open-file and omp:open-diff
95
151
  useChamberEvent('omp:open-file', (e) => {
96
152
  const customEvent = e as CustomEvent<{ path: string; name?: string; id?: number; content?: string; root?: string; repo?: string }>;
@@ -98,15 +154,9 @@ export function useFileTabs(
98
154
 
99
155
  const rawPath = customEvent.detail.path.replace(/^\/+/, '');
100
156
  const name = customEvent.detail.name || rawPath.split('/').pop() || 'file';
101
- let hash = 0;
102
- for (let i = 0; i < rawPath.length; i++) {
103
- hash = (hash << 5) - hash + rawPath.charCodeAt(i);
104
- hash |= 0;
105
- }
106
- const id = customEvent.detail.id || Math.abs(hash) || Date.now();
107
157
 
108
158
  handleOpenFile({
109
- id,
159
+ id: customEvent.detail.id || fileTabId(rawPath),
110
160
  name,
111
161
  path: rawPath,
112
162
  content: customEvent.detail.content,
@@ -136,5 +186,6 @@ export function useFileTabs(
136
186
  handleOpenFile,
137
187
  handleOpenDiff,
138
188
  handleCloseFile,
189
+ convertDiffToEditor,
139
190
  };
140
191
  }
@@ -0,0 +1,348 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * `useFileActions` — the Files panel's server contract.
8
+ *
9
+ * Three rules are asserted here because each was a bug: a create that SUCCEEDS
10
+ * opens the new file (the dialog used to close and leave the user hunting for
11
+ * an empty row), a create that is REFUSED keeps the dialog open with the reason
12
+ * beside the field (rather than closing as if it had worked), and a settled
13
+ * response is acted on exactly ONCE — an unguarded effect re-ran on every render
14
+ * and pinned the tab the moment a file was created.
15
+ *
16
+ * Runs against a real DOM (happy-dom) because it is a Preact hook driving
17
+ * fetches. The modules below are therefore imported DYNAMICALLY, inside
18
+ * `beforeAll`: Preact binds its environment at evaluation time, so a static
19
+ * import would capture the DOM-less one and the render would fail — the same
20
+ * boundary `use-file-editor.test.ts` documents.
21
+ */
22
+
23
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from 'bun:test';
24
+ import { Window } from 'happy-dom';
25
+ import type { h as PreactH, render as PreactRender } from 'preact';
26
+ import type { act as PreactAct } from 'preact/test-utils';
27
+ import type { FileActions, useFileActions as UseFileActionsHook } from '@/client/hooks/workspace/file-tree-actions';
28
+
29
+ let useFileActions: typeof UseFileActionsHook;
30
+ let h: typeof PreactH;
31
+ let render: typeof PreactRender;
32
+ let act: typeof PreactAct;
33
+
34
+ let mounted: FileActions | null = null;
35
+ let container: HTMLElement;
36
+
37
+ /** What the stubbed endpoint was asked, and what it answers. */
38
+ interface Server {
39
+ requests: Array<Record<string, string>>;
40
+ /** Response for the next create: `success` with a path, or a refusal. */
41
+ createResult: { success?: boolean; path?: string; error?: string };
42
+ }
43
+
44
+ const opened: Array<{ path: string; name: string }> = [];
45
+ let refreshes = 0;
46
+
47
+ function stubFetch(server: Server) {
48
+ globalThis.fetch = (async (_input: string | URL | Request, init?: RequestInit) => {
49
+ const form = init?.body as FormData;
50
+ const fields: Record<string, string> = {};
51
+ for (const [key, value] of form.entries()) fields[key] = String(value);
52
+ server.requests.push(fields);
53
+ // The fetcher reads `text()` and parses it itself, so a stub that only
54
+ // offers `json()` would leave every response looking empty — the request
55
+ // would be recorded and the effect would never see an answer.
56
+ const body = JSON.stringify(server.createResult);
57
+ return { ok: true, text: async () => body };
58
+ }) as unknown as typeof fetch;
59
+ }
60
+
61
+ /**
62
+ * Let the request chain and the render it queues land.
63
+ *
64
+ * Several `act` boundaries, each draining microtasks: the fetcher's chain
65
+ * (`fetch` → `text()` → parse → `setData` → `setState`) resolves over several
66
+ * turns, and a state update queued outside an `act` boundary is only flushed
67
+ * when the next one arrives. No timers — the hook resolves on promises, so
68
+ * waiting on the clock would only be guessing at how many turns it needs.
69
+ */
70
+ async function settle() {
71
+ for (let i = 0; i < 8; i++) {
72
+ await act(async () => {
73
+ for (let turn = 0; turn < 8; turn++) await Promise.resolve();
74
+ });
75
+ }
76
+ }
77
+
78
+ function mount(file: { path: string; name: string }, diff?: { staged?: boolean; status?: string }) {
79
+ const Probe = () => {
80
+ mounted = useFileActions({
81
+ // `basePath` is the ABSOLUTE listing root the server reported, and it is
82
+ // what Copy Path anchors on — `rootPath` is the client's own (possibly
83
+ // `~`-relative) workspace path, which is not resolvable outside the panel.
84
+ file: { ...file, basePath: '/root', rootPath: '/root', repo: '.' },
85
+ diff,
86
+ onOpenFile: (target) => opened.push({ path: target.path, name: target.name }),
87
+ onActionComplete: () => refreshes++,
88
+ });
89
+ return null;
90
+ };
91
+ return act(async () => {
92
+ render(h(Probe, {}), container);
93
+ });
94
+ }
95
+
96
+ /** Open the create dialog the way the context menu does. */
97
+ async function openCreateDialog() {
98
+ await act(async () => {
99
+ mounted?.handleAction('new_file');
100
+ });
101
+ }
102
+
103
+ /**
104
+ * Submit the create dialog with `name` and let the response land — the whole
105
+ * user sequence (open the dialog, type, press Create), because the state under
106
+ * test is what survives it.
107
+ */
108
+ async function createNamed(name: string) {
109
+ await openCreateDialog();
110
+ await act(async () => {
111
+ mounted?.setCreateName(name);
112
+ });
113
+ await act(async () => {
114
+ mounted?.submitCreate({ preventDefault() {} } as never);
115
+ });
116
+ await settle();
117
+ }
118
+
119
+ function domGlobals(win: Window): Record<string, unknown> {
120
+ return {
121
+ window: win,
122
+ document: win.document,
123
+ navigator: win.navigator,
124
+ Node: win.Node,
125
+ Element: win.Element,
126
+ HTMLElement: win.HTMLElement,
127
+ };
128
+ }
129
+
130
+ const installed: Record<string, unknown> = {};
131
+ const displaced: Record<string, unknown> = {};
132
+
133
+ beforeAll(async () => {
134
+ const win = new Window({ url: 'http://localhost' });
135
+ for (const [key, value] of Object.entries(domGlobals(win))) {
136
+ displaced[key] = (globalThis as Record<string, unknown>)[key];
137
+ installed[key] = value;
138
+ (globalThis as Record<string, unknown>)[key] = value;
139
+ }
140
+ ({ useFileActions } = await import('@/client/hooks/workspace/file-tree-actions'));
141
+ ({ h, render } = await import('preact'));
142
+ ({ act } = await import('preact/test-utils'));
143
+ });
144
+
145
+ afterAll(() => {
146
+ for (const key of Object.keys(installed)) {
147
+ if (displaced[key] === undefined) delete (globalThis as Record<string, unknown>)[key];
148
+ else (globalThis as Record<string, unknown>)[key] = displaced[key];
149
+ }
150
+ });
151
+
152
+ beforeEach(() => {
153
+ mounted = null;
154
+ opened.length = 0;
155
+ refreshes = 0;
156
+ container = document.createElement('div');
157
+ document.body.appendChild(container);
158
+ });
159
+
160
+ afterEach(() => {
161
+ render(null, container);
162
+ container.remove();
163
+ });
164
+
165
+ function server(createResult: Server['createResult']): Server {
166
+ return { requests: [], createResult };
167
+ }
168
+
169
+ describe('useFileActions create', () => {
170
+ test('a created file opens in the editor and refreshes the tree', async () => {
171
+ const srv = server({ success: true, path: 'src/new-file.ts' });
172
+ stubFetch(srv);
173
+ await mount({ path: 'src', name: 'src' });
174
+ await createNamed('new-file.ts');
175
+
176
+ expect(srv.requests[0]).toEqual({ actionType: 'create', path: 'src', name: 'new-file.ts', root: '/root' });
177
+ expect(opened).toEqual([{ path: 'src/new-file.ts', name: 'new-file.ts' }]);
178
+ expect(refreshes).toBe(1);
179
+ expect(mounted?.showCreateModal).toBe(false);
180
+ });
181
+
182
+ test('a refusal keeps the dialog open and reports the server’s reason', async () => {
183
+ stubFetch(server({ error: 'new-file.ts already exists' }));
184
+ await mount({ path: 'src', name: 'src' });
185
+ await createNamed('new-file.ts');
186
+
187
+ expect(mounted?.showCreateModal).toBe(true);
188
+ expect(mounted?.createError).toBe('new-file.ts already exists');
189
+ expect(opened).toEqual([]);
190
+ expect(refreshes).toBe(0);
191
+ });
192
+
193
+ test('a refusal with no message still says something', async () => {
194
+ stubFetch(server({}));
195
+ await mount({ path: 'src', name: 'src' });
196
+ await createNamed('new-file.ts');
197
+
198
+ expect(mounted?.createError).toBe('Could not create the file');
199
+ });
200
+
201
+ test('a settled response is acted on once, however many times the row re-renders', async () => {
202
+ // The unguarded version re-ran this effect on every render and re-opened
203
+ // the file forever, which pinned the tab.
204
+ const srv = server({ success: true, path: 'src/once.ts' });
205
+ stubFetch(srv);
206
+ await mount({ path: 'src', name: 'src' });
207
+ await createNamed('once.ts');
208
+
209
+ for (let i = 0; i < 3; i++) {
210
+ await act(async () => {
211
+ mounted?.setCreateName('');
212
+ });
213
+ }
214
+ await settle();
215
+
216
+ expect(refreshes).toBe(1);
217
+ expect(opened.length).toBe(1);
218
+ });
219
+
220
+ test('an empty name never reaches the server', async () => {
221
+ const srv = server({ success: true, path: 'src/x.ts' });
222
+ stubFetch(srv);
223
+ await mount({ path: 'src', name: 'src' });
224
+
225
+ await act(async () => {
226
+ mounted?.setCreateName(' ');
227
+ });
228
+ await act(async () => {
229
+ mounted?.submitCreate({ preventDefault() {} } as never);
230
+ });
231
+ await settle();
232
+
233
+ expect(srv.requests).toEqual([]);
234
+ expect(mounted?.showCreateModal).toBe(false);
235
+ });
236
+ });
237
+
238
+ describe('useFileActions context menu', () => {
239
+ test('opening the create dialog resets the previous attempt', async () => {
240
+ stubFetch(server({ error: 'taken' }));
241
+ await mount({ path: 'src', name: 'src' });
242
+ await createNamed('taken.ts');
243
+ expect(mounted?.createError).toBe('taken');
244
+
245
+ await act(async () => {
246
+ mounted?.handleAction('new_file');
247
+ });
248
+
249
+ expect(mounted?.showCreateModal).toBe(true);
250
+ expect(mounted?.createError).toBeNull();
251
+ expect(mounted?.createName).toBe('');
252
+ });
253
+
254
+ test('copying the path reports the ABSOLUTE path, and does not call the server', async () => {
255
+ const srv = server({});
256
+ stubFetch(srv);
257
+ const writes: string[] = [];
258
+ // `copyToClipboard` prefers the async clipboard API, and only when the page
259
+ // is a secure context — the legacy textarea path needs `execCommand`, which
260
+ // happy-dom does not implement.
261
+ Object.defineProperty(globalThis.window, 'isSecureContext', { value: true, configurable: true });
262
+ Object.defineProperty(globalThis.navigator, 'clipboard', {
263
+ value: {
264
+ writeText: async (text: string) => {
265
+ writes.push(text);
266
+ },
267
+ },
268
+ configurable: true,
269
+ });
270
+ await mount({ path: 'src/deep/file.ts', name: 'file.ts' });
271
+
272
+ await act(async () => {
273
+ mounted?.handleAction('copy_path');
274
+ });
275
+ await settle();
276
+
277
+ expect(writes).toEqual(['/root/src/deep/file.ts']);
278
+ expect(srv.requests).toEqual([]);
279
+ });
280
+
281
+ test('a folder gets the create action; a file does not offer it', async () => {
282
+ stubFetch(server({}));
283
+ await mount({ path: 'src', name: 'src' });
284
+
285
+ await act(async () => {
286
+ mounted?.handleAction('new_file');
287
+ });
288
+ expect(mounted?.showCreateModal).toBe(true);
289
+
290
+ await act(async () => {
291
+ mounted?.closeCreate();
292
+ });
293
+ expect(mounted?.showCreateModal).toBe(false);
294
+ });
295
+ });
296
+
297
+ describe('useFileActions rename and delete', () => {
298
+ test('rename sends the new path and closes on success', async () => {
299
+ const srv = server({ success: true });
300
+ stubFetch(srv);
301
+ await mount({ path: 'src/old.ts', name: 'old.ts' });
302
+
303
+ await act(async () => {
304
+ mounted?.handleAction('rename');
305
+ });
306
+ await act(async () => {
307
+ mounted?.setRenameValue('src/new.ts');
308
+ });
309
+ await act(async () => {
310
+ mounted?.submitRename({ preventDefault() {} } as never);
311
+ });
312
+ await settle();
313
+
314
+ expect(srv.requests[0]).toEqual({ actionType: 'rename', path: 'src/old.ts', newPath: 'src/new.ts', root: '/root' });
315
+ expect(mounted?.showRenameModal).toBe(false);
316
+ expect(refreshes).toBe(1);
317
+ });
318
+
319
+ test('delete posts the delete action for the row’s own path', async () => {
320
+ const srv = server({ success: true });
321
+ stubFetch(srv);
322
+ await mount({ path: 'src/gone.ts', name: 'gone.ts' });
323
+
324
+ await act(async () => {
325
+ mounted?.submitDelete();
326
+ });
327
+ await settle();
328
+
329
+ expect(srv.requests[0]).toEqual({ actionType: 'delete', path: 'src/gone.ts', root: '/root' });
330
+ expect(refreshes).toBe(1);
331
+ });
332
+
333
+ test('a git-history response does not close the dialogs or refresh the tree', async () => {
334
+ // `type` marks the history payload; treating it as a mutation would refresh
335
+ // the tree under an open modal.
336
+ const srv = server({ success: true, type: 'history', data: [] } as never);
337
+ stubFetch(srv);
338
+ await mount({ path: 'src/a.ts', name: 'a.ts' });
339
+
340
+ await act(async () => {
341
+ mounted?.handleAction('history');
342
+ });
343
+ await settle();
344
+
345
+ expect(mounted?.showHistoryModal).toBe(true);
346
+ expect(refreshes).toBe(0);
347
+ });
348
+ });