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
@@ -1,13 +1,23 @@
1
- import { Check, Copy, Download, Eye, EyeOff, Maximize, Save, WrapText, ZoomIn, ZoomOut } from 'lucide-preact';
1
+ import { AlertCircle, Check, Copy, Download, Eye, EyeOff, Maximize, Minimize, Save, Search, WrapText, ZoomIn, ZoomOut } from 'lucide-preact';
2
+
3
+ export type EditorSaveStatus = 'idle' | 'saving' | 'saved' | 'error';
2
4
 
3
5
  interface EditorToolbarProps {
4
6
  path: string;
5
- saveStatus: 'idle' | 'saving' | 'saved';
7
+ saveStatus: EditorSaveStatus;
6
8
  wordWrap: boolean;
7
9
  isMd: boolean;
8
10
  isPreview: boolean;
11
+ /** True for the 1.5s after a copy landed, so the button can confirm it. */
12
+ copied: boolean;
9
13
  /** Raster image: no text to save or wrap, and zoom belongs to the picture. */
10
14
  isImage?: boolean;
15
+ /** A read failure left no trustworthy buffer, so there is nothing to save. */
16
+ saveDisabled?: boolean;
17
+ isMaximized?: boolean;
18
+ /** Find/replace is open — the button reflects it and toggles the bar. */
19
+ findOpen?: boolean;
20
+ onToggleFind: () => void;
11
21
  onSave: () => void;
12
22
  onTogglePreview: () => void;
13
23
  onToggleWordWrap: () => void;
@@ -18,13 +28,27 @@ interface EditorToolbarProps {
18
28
  onToggleMaximize: () => void;
19
29
  }
20
30
 
31
+ /**
32
+ * Shared icon-button shell. Every action here is icon-only, so each one carries
33
+ * a `title` (the tooltip) and an `aria-label` (the accessible name) — a bare
34
+ * `<svg onClick>` is unreachable by keyboard and unlabeled to a screen reader.
35
+ * `p-1.5` also gives the 14px glyph a ~26px hit target.
36
+ */
37
+ const ACTION_CLASS =
38
+ 'flex items-center justify-center p-1.5 rounded transition-colors cursor-pointer disabled:cursor-default disabled:opacity-50';
39
+
21
40
  export function EditorToolbar({
22
41
  path,
23
42
  saveStatus,
24
43
  wordWrap,
25
44
  isMd,
26
45
  isPreview,
46
+ copied,
27
47
  isImage = false,
48
+ saveDisabled = false,
49
+ isMaximized = false,
50
+ findOpen = false,
51
+ onToggleFind,
28
52
  onSave,
29
53
  onTogglePreview,
30
54
  onToggleWordWrap,
@@ -34,65 +58,144 @@ export function EditorToolbar({
34
58
  onDownload,
35
59
  onToggleMaximize,
36
60
  }: EditorToolbarProps) {
61
+ const saveTitle =
62
+ saveDisabled
63
+ ? 'Nothing to save — this file could not be read'
64
+ : saveStatus === 'saving'
65
+ ? 'Saving…'
66
+ : saveStatus === 'error'
67
+ ? 'Save failed — the file on disk was not changed'
68
+ : 'Save file (Ctrl+S)';
69
+
37
70
  return (
38
- <div className="flex items-center justify-between px-3 py-2 border-b border-ink/10 bg-paper">
71
+ <div className="flex items-center justify-between px-3 py-2 border-b border-ink/10 bg-paper flex-shrink-0">
39
72
  {/* Left status */}
40
- <div className="flex items-center space-x-2 text-xs font-mono text-ink/40">
73
+ <div className="flex items-center space-x-2 text-xs font-mono text-ink/40 min-w-0">
41
74
  <span className="truncate max-w-[300px]" title={path}>{path}</span>
42
75
  </div>
43
76
 
44
77
  {/* Right actions */}
45
- <div className="flex items-center space-x-3 text-ink/40">
78
+ <div className="flex items-center space-x-1 text-ink/40 flex-shrink-0">
46
79
  {!isImage && (
47
- <div className="flex items-center pr-3 border-r border-ink/10">
80
+ <div className="flex items-center pr-2 mr-1 border-r border-ink/10">
48
81
  <button
49
82
  type="button"
50
83
  onClick={onSave}
51
- className="flex items-center justify-center rounded hover:bg-ink/5 transition-colors"
52
- title="Save file (Ctrl+S)"
84
+ disabled={saveDisabled || saveStatus === 'saving'}
85
+ className={`${ACTION_CLASS} hover:bg-ink/5`}
86
+ title={saveTitle}
87
+ aria-label={saveTitle}
53
88
  >
54
89
  {saveStatus === 'saving' ? (
55
90
  <Save size={14} className="text-amber-500 animate-pulse" />
56
91
  ) : saveStatus === 'saved' ? (
57
92
  <Check size={14} className="text-success" />
93
+ ) : saveStatus === 'error' ? (
94
+ <AlertCircle size={14} className="text-error" />
58
95
  ) : (
59
- <Save size={14} className="hover:text-ink cursor-pointer" />
96
+ <Save size={14} className="hover:text-ink" />
60
97
  )}
61
98
  </button>
62
99
  </div>
63
100
  )}
64
101
 
65
102
  {isMd && (
66
- <div onClick={onTogglePreview} className="flex items-center pr-3 border-r border-ink/10">
67
- {isPreview ? (
68
- <EyeOff size={14} className="hover:text-ink cursor-pointer" />
69
- ) : (
70
- <Eye size={14} className="hover:text-ink cursor-pointer" />
71
- )}
103
+ <div className="flex items-center pr-2 mr-1 border-r border-ink/10">
104
+ <button
105
+ type="button"
106
+ onClick={onTogglePreview}
107
+ className={`${ACTION_CLASS} hover:bg-ink/5 ${isPreview ? 'text-ink' : 'hover:text-ink'}`}
108
+ title={isPreview ? 'Show markdown source' : 'Preview markdown'}
109
+ aria-label={isPreview ? 'Show markdown source' : 'Preview markdown'}
110
+ aria-pressed={isPreview}
111
+ >
112
+ {isPreview ? <EyeOff size={14} /> : <Eye size={14} />}
113
+ </button>
72
114
  </div>
73
115
  )}
74
116
 
75
117
  {!isImage && (
76
118
  <>
77
- <div className="flex items-center space-x-2 pr-3 border-r border-ink/10">
78
- <WrapText size={14} className={`cursor-pointer ${wordWrap ? 'text-ink' : 'hover:text-ink'}`} onClick={onToggleWordWrap} />
119
+ <div className="flex items-center pr-2 mr-1 border-r border-ink/10">
120
+ <button
121
+ type="button"
122
+ onClick={onToggleFind}
123
+ className={`${ACTION_CLASS} hover:bg-ink/5 ${findOpen ? 'text-ink' : 'hover:text-ink'}`}
124
+ title="Find and replace (Ctrl+F)"
125
+ aria-label="Find and replace"
126
+ aria-pressed={findOpen}
127
+ >
128
+ <Search size={14} />
129
+ </button>
79
130
  </div>
80
131
 
81
- <div className="flex items-center space-x-2 pr-3 border-r border-ink/10">
82
- <ZoomOut size={14} className="hover:text-ink cursor-pointer" onClick={onZoomOut} />
83
- <ZoomIn size={14} className="hover:text-ink cursor-pointer" onClick={onZoomIn} />
132
+ <div className="flex items-center pr-2 mr-1 border-r border-ink/10">
133
+ <button
134
+ type="button"
135
+ onClick={onToggleWordWrap}
136
+ className={`${ACTION_CLASS} hover:bg-ink/5 ${wordWrap ? 'text-ink' : 'hover:text-ink'}`}
137
+ title={wordWrap ? 'Word wrap on' : 'Word wrap off'}
138
+ aria-label={wordWrap ? 'Word wrap on' : 'Word wrap off'}
139
+ aria-pressed={wordWrap}
140
+ >
141
+ <WrapText size={14} />
142
+ </button>
143
+ </div>
144
+
145
+ <div className="flex items-center pr-2 mr-1 border-r border-ink/10">
146
+ <button
147
+ type="button"
148
+ onClick={onZoomOut}
149
+ className={`${ACTION_CLASS} hover:bg-ink/5 hover:text-ink`}
150
+ title="Decrease font size"
151
+ aria-label="Decrease font size"
152
+ >
153
+ <ZoomOut size={14} />
154
+ </button>
155
+ <button
156
+ type="button"
157
+ onClick={onZoomIn}
158
+ className={`${ACTION_CLASS} hover:bg-ink/5 hover:text-ink`}
159
+ title="Increase font size"
160
+ aria-label="Increase font size"
161
+ >
162
+ <ZoomIn size={14} />
163
+ </button>
84
164
  </div>
85
165
  </>
86
166
  )}
87
167
 
88
- <div className="flex items-center space-x-2 pr-3 border-r border-ink/10">
89
- <Copy size={14} className="hover:text-ink cursor-pointer" onClick={onCopy} />
90
- <Download size={14} className="hover:text-ink cursor-pointer" onClick={onDownload} />
168
+ <div className="flex items-center pr-2 mr-1 border-r border-ink/10">
169
+ <button
170
+ type="button"
171
+ onClick={onCopy}
172
+ className={`${ACTION_CLASS} hover:bg-ink/5 hover:text-ink`}
173
+ title={copied ? 'Copied' : isImage ? 'Copy image' : 'Copy content'}
174
+ aria-label={copied ? 'Copied' : isImage ? 'Copy image' : 'Copy content'}
175
+ >
176
+ {copied ? <Check size={14} className="text-success" /> : <Copy size={14} />}
177
+ </button>
178
+ <button
179
+ type="button"
180
+ onClick={onDownload}
181
+ className={`${ACTION_CLASS} hover:bg-ink/5 hover:text-ink`}
182
+ title="Download file"
183
+ aria-label="Download file"
184
+ >
185
+ <Download size={14} />
186
+ </button>
91
187
  </div>
92
188
 
93
- <div className="flex items-center pl-1">
94
- <Maximize size={14} className="hover:text-ink cursor-pointer" onClick={onToggleMaximize} />
95
- </div>
189
+ <button
190
+ type="button"
191
+ onClick={onToggleMaximize}
192
+ className={`${ACTION_CLASS} hover:bg-ink/5 hover:text-ink ${isMaximized ? 'text-ink' : ''}`}
193
+ title={isMaximized ? 'Exit full screen' : 'Full screen'}
194
+ aria-label={isMaximized ? 'Exit full screen' : 'Full screen'}
195
+ aria-pressed={isMaximized}
196
+ >
197
+ {isMaximized ? <Minimize size={14} /> : <Maximize size={14} />}
198
+ </button>
96
199
  </div>
97
200
  </div>
98
201
  );
@@ -1,17 +1,24 @@
1
- import { useEffect, useState } from 'preact/hooks';
1
+ import { useEffect, useRef, useState } from 'preact/hooks';
2
+ import type { TargetedKeyboardEvent } from 'preact';
3
+ import { AlertTriangle, Loader2 } from 'lucide-preact';
2
4
  import { FileIcon } from '@/client/components/common/file-icon';
3
- import { CodeSurface } from '@/client/components/common/code-surface';
5
+ import { CodeSurface, type CodeSurfaceHandle } from '@/client/components/common/code-surface';
4
6
  import { MarkdownRenderer } from '@/client/components/common/MarkdownRenderer';
5
7
  import { useScrollbarFade, scrollbarFadeClass } from '@/client/hooks/ui/scrollbar-fade';
6
8
  import { EditorTabs } from '@/client/components/workspace/editor/Tabs';
7
9
  import { EditorToolbar } from '@/client/components/workspace/editor/Toolbar';
10
+ import { FindWidget } from '@/client/components/workspace/editor/FindWidget';
11
+ import { CommandPalette } from '@/client/components/workspace/editor/CommandPalette';
12
+ import { EDITOR_KEY_BINDINGS, isFindBarCommand, type EditorCommand } from '@/shared/lib/code/editor/keymap';
13
+ import type { TextRange } from '@/shared/lib/code/editor/commands';
14
+ import { resolveBinding } from '@/shared/lib/ui/key-binding';
8
15
  import { ImageViewer } from '@/client/components/common/image-viewer';
9
- import { getDefaultContent } from '@/shared/lib/code/editor-utils';
10
16
  import { DiffPanel } from '@/client/components/workspace/diff-panel';
11
17
  import { useSessionState } from '@/client/hooks/workspace/session-state';
12
18
  import { useSessionStateContext } from '@/client/hooks/workspace/session-state/context';
13
19
  import { getSessionValue } from '@/shared/lib/workspace/session-state/store';
14
20
  import { useFileEditor } from '@/client/hooks/editor/use-file-editor';
21
+ import { useEditorFind } from '@/client/hooks/editor/use-editor-find';
15
22
 
16
23
  interface EditorProps {
17
24
  className?: string;
@@ -19,6 +26,8 @@ interface EditorProps {
19
26
  activeFileId: number | string | null;
20
27
  onSelectFile: (id: number | string) => void;
21
28
  onCloseFile: (id: number | string) => void;
29
+ /** Replace a diff tab with the plain editor tab for the same file. */
30
+ onConvertDiffToEditor: (id: number | string) => void;
22
31
  refreshKey?: number;
23
32
  onFileSaved?: () => void;
24
33
  }
@@ -29,6 +38,7 @@ export function Editor({
29
38
  activeFileId,
30
39
  onSelectFile,
31
40
  onCloseFile,
41
+ onConvertDiffToEditor,
32
42
  refreshKey = 0,
33
43
  onFileSaved
34
44
  }: EditorProps) {
@@ -38,14 +48,35 @@ export function Editor({
38
48
  const [zoomLevel, setZoomLevel] = useSessionState<number>('editor.zoomLevel', 12);
39
49
  const [isMaximized, setIsMaximized] = useState(false);
40
50
  const [wordWrap, setWordWrap] = useSessionState<boolean>('editor.wordWrap', true);
51
+ /** Extra ⌘D ranges; the surface paints them and the editor edits them by replication. */
52
+ const [occurrences, setOccurrences] = useState<readonly TextRange[]>([]);
53
+ const [paletteOpen, setPaletteOpen] = useState(false);
41
54
  const { sessionId } = useSessionStateContext();
42
55
  const { isScrolling, handleScroll } = useScrollbarFade();
56
+ const surfaceRef = useRef<CodeSurfaceHandle | null>(null);
43
57
 
44
58
  const editor = useFileEditor(activeFile ?? null, {
45
- fallback: getDefaultContent,
59
+ // A file that could not be read must report the failure, not substitute
60
+ // placeholder text — the mock sample used to be written straight over the
61
+ // real file by the next save.
62
+ reportSaveError: true,
46
63
  onFileSaved,
47
64
  });
48
65
 
66
+ const find = useEditorFind({
67
+ text: editor.content,
68
+ documentKey: `${activeFile?.id ?? ''}\u0000${editor.language}`,
69
+ readSelection: () => surfaceRef.current?.getSelection() ?? null,
70
+ select: (start, end, focus = true) => surfaceRef.current?.select(start, end, focus),
71
+ reveal: (offset) => surfaceRef.current?.revealOffset(offset),
72
+ applyDocument: (value, caretStart, caretEnd) => surfaceRef.current?.applyDocument(value, caretStart, caretEnd),
73
+ toggleWordWrap: () => setWordWrap((previous) => !previous),
74
+ });
75
+ // The capture handler below is rebuilt on every render anyway (it reads the
76
+ // find state), so it is kept in a ref and the listener attached once.
77
+ const findRef = useRef(find);
78
+ findRef.current = find;
79
+
49
80
  useEffect(() => {
50
81
  if (refreshKey > 0) editor.reset();
51
82
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -84,6 +115,69 @@ export function Editor({
84
115
  setPreviewMode(prev => ({ ...prev, [activeFile.id]: !prev[activeFile.id] }));
85
116
  };
86
117
 
118
+ /**
119
+ * The one place a command runs, whatever asked for it — a chord, a toolbar
120
+ * button or the palette. Splitting this by source is how a command ends up
121
+ * working from the keyboard but not from the palette (or the reverse).
122
+ */
123
+ const runCommand = (command: EditorCommand) => {
124
+ if (isFindBarCommand(command)) {
125
+ findRef.current.runCommand(command);
126
+ return;
127
+ }
128
+ if (command === 'toggleWordWrap') {
129
+ setWordWrap((previous) => !previous);
130
+ return;
131
+ }
132
+ // Everything else is the editor's, and it runs through the editor's own
133
+ // dispatcher — the palette is a second way IN, never a second implementation.
134
+ surfaceRef.current?.runCommand(command);
135
+ };
136
+
137
+ /**
138
+ * Editor shortcuts, on the CAPTURE phase of the panel.
139
+ *
140
+ * Capture is what makes them work while focus is inside the document: the
141
+ * `<textarea>` has its own handler (Tab, undo, Escape-to-blur) and would
142
+ * otherwise see these first. Only the commands the PANEL owns are consumed
143
+ * here — the editor's own keymap runs in the textarea, where it has the
144
+ * buffer and the caret in hand.
145
+ *
146
+ * The panel is the scope, not the window: another panel's text field keeps
147
+ * its own ⌘F, and the composer is untouched.
148
+ */
149
+ const handlePanelKeyDown = (event: TargetedKeyboardEvent<HTMLDivElement>) => {
150
+ const current = findRef.current;
151
+ if (event.key === 'Escape') {
152
+ if (paletteOpen) {
153
+ event.preventDefault();
154
+ event.stopPropagation();
155
+ setPaletteOpen(false);
156
+ return;
157
+ }
158
+ if (!current.open) return;
159
+ event.preventDefault();
160
+ event.stopPropagation();
161
+ current.close();
162
+ return;
163
+ }
164
+ // ⌘⇧P opens the palette — the one chord that is not an editor command.
165
+ if ((event.metaKey || event.ctrlKey) && event.shiftKey && event.code === 'KeyP') {
166
+ event.preventDefault();
167
+ event.stopPropagation();
168
+ setPaletteOpen(true);
169
+ return;
170
+ }
171
+ // While the bar is open, an unmodified key in one of ITS fields belongs to
172
+ // the field — but F3 and the ⌘-chords below still dispatch, so stepping to
173
+ // the next match from inside the find field works.
174
+ const command = resolveBinding(EDITOR_KEY_BINDINGS, event);
175
+ if (!command || !isFindBarCommand(command)) return;
176
+ event.preventDefault();
177
+ event.stopPropagation();
178
+ runCommand(command);
179
+ };
180
+
87
181
  if (openedFiles.length === 0) {
88
182
  return (
89
183
  <div className={`flex flex-col h-full bg-canvas items-center justify-center text-ink/40 ${className}`}>
@@ -96,14 +190,13 @@ export function Editor({
96
190
  const currentContent = editor.content;
97
191
  const isMd = activeFile?.name.endsWith('.md');
98
192
  const isPreview = activeFile ? previewMode[activeFile.id] : false;
99
- const toolbarSaveStatus = editor.saveStatus === 'error' ? 'idle' : editor.saveStatus;
100
193
 
101
194
  const editorContainerClass = isMaximized
102
195
  ? 'fixed inset-0 z-50 flex flex-col bg-canvas'
103
196
  : `flex flex-col h-full bg-canvas ${className}`;
104
197
 
105
198
  return (
106
- <div className={editorContainerClass}>
199
+ <div className={editorContainerClass} onKeyDownCapture={handlePanelKeyDown}>
107
200
  <EditorTabs
108
201
  openedFiles={openedFiles}
109
202
  activeFileId={activeFileId}
@@ -119,11 +212,7 @@ export function Editor({
119
212
  isStaged={Boolean(activeFile.diffStaged)}
120
213
  root={activeFile.root}
121
214
  repo={activeFile.repo || '.'}
122
- onOpenInEditor={() => {
123
- // Convert diff tab to regular editor tab
124
- activeFile.isDiff = false;
125
- onSelectFile(activeFile.id);
126
- }}
215
+ onOpenInEditor={() => onConvertDiffToEditor(activeFile.id)}
127
216
  onFileSaved={onFileSaved}
128
217
  className="flex-1"
129
218
  />
@@ -131,11 +220,16 @@ export function Editor({
131
220
  <>
132
221
  <EditorToolbar
133
222
  path={activeFile.path}
134
- saveStatus={toolbarSaveStatus}
223
+ saveStatus={editor.saveStatus}
135
224
  wordWrap={wordWrap}
136
225
  isMd={isMd}
137
226
  isPreview={isPreview}
227
+ copied={editor.copied}
138
228
  isImage={editor.isImage}
229
+ saveDisabled={Boolean(editor.loadError)}
230
+ isMaximized={isMaximized}
231
+ findOpen={find.open}
232
+ onToggleFind={() => (find.open ? find.close() : find.openFind())}
139
233
  onSave={editor.saveNow}
140
234
  onTogglePreview={togglePreview}
141
235
  onToggleWordWrap={() => setWordWrap(!wordWrap)}
@@ -151,58 +245,82 @@ export function Editor({
151
245
  // the scrolling text container — a wheel there would scroll the
152
246
  // pane instead of zooming the image.
153
247
  <ImageViewer src={editor.imageUrl} name={activeFile.name} className="flex-1 min-h-0" />
248
+ ) : editor.isLoading ? (
249
+ <div className="flex-1 flex items-center justify-center gap-2 text-ink/40 text-xs font-mono">
250
+ <Loader2 size={14} className="animate-spin" />
251
+ <span>Loading file content…</span>
252
+ </div>
253
+ ) : editor.loadError ? (
254
+ // A read that failed must say so. Without this the pane rendered
255
+ // an empty (or substituted) buffer, and the next save wrote it
256
+ // over the real file.
257
+ <div className="flex-1 flex flex-col items-center justify-center gap-2 px-6 text-center">
258
+ <AlertTriangle size={20} className="text-error" />
259
+ <span className="text-xs text-error font-sans">Could not read {activeFile.name}</span>
260
+ <span className="text-[11px] text-ink/50 font-mono break-all">{editor.loadError}</span>
261
+ </div>
154
262
  ) : (
155
- <div onScroll={handleScroll} className={`flex-1 overflow-auto bg-paper flex ${scrollbarFadeClass(isScrolling)}`}>
156
- {(isMd && isPreview) ? (
157
- // No `prose` wrapper: markdown is styled by `.prose-content`
158
- // alone (the same system the chat timeline uses). The
159
- // typography plugin fought it — `.prose img` added 2em
160
- // vertical margins that ballooned a badge row into its own
161
- // line box, and `--tw-prose-*` colors ignored the theme.
162
- <div className="p-6 max-w-4xl mx-auto font-sans flex-1" style={{ fontSize: `${zoomLevel}px` }}>
163
- <MarkdownRenderer
164
- content={currentContent}
165
- document
166
- scope={{ path: activeFile.path, root: activeFile.root, repo: activeFile.repo }}
263
+ <div className="relative flex-1 min-h-0 flex flex-col">
264
+ {find.open ? <FindWidget find={find} /> : null}
265
+ {paletteOpen ? <CommandPalette onRun={runCommand} onClose={() => setPaletteOpen(false)} /> : null}
266
+ <div onScroll={handleScroll} className={`flex-1 overflow-auto bg-paper flex ${scrollbarFadeClass(isScrolling)}`}>
267
+ {(isMd && isPreview) ? (
268
+ // No `prose` wrapper: markdown is styled by `.prose-content`
269
+ // alone (the same system the chat timeline uses). The
270
+ // typography plugin fought it — `.prose img` added 2em
271
+ // vertical margins that ballooned a badge row into its own
272
+ // line box, and `--tw-prose-*` colors ignored the theme.
273
+ <div className="p-6 max-w-4xl mx-auto font-sans flex-1" style={{ fontSize: `${zoomLevel}px` }}>
274
+ <MarkdownRenderer
275
+ content={currentContent}
276
+ document
277
+ scope={{ path: activeFile.path, root: activeFile.root, repo: activeFile.repo }}
278
+ />
279
+ </div>
280
+ ) : (
281
+ <CodeSurface
282
+ ref={surfaceRef}
283
+ value={currentContent}
284
+ onValueChange={editor.onChange}
285
+ language={editor.language}
286
+ wordWrap={wordWrap}
287
+ marks={find.open ? find.matches : undefined}
288
+ currentMark={find.currentIndex}
289
+ occurrences={occurrences}
290
+ onOccurrencesChange={setOccurrences}
291
+ onCommand={runCommand}
292
+ rootClassName="flex"
293
+ gutterClassName="flex flex-col text-right pl-4 pr-3 select-none text-ink/30 font-mono border-r border-ink/10 bg-canvas sticky left-0 z-10"
294
+ gutterStyle={{
295
+ fontSize: zoomLevel,
296
+ paddingTop: 16,
297
+ paddingBottom: 16,
298
+ lineHeight: 1.5,
299
+ fontFamily: '"Fira Code", "JetBrains Mono", "SF Mono", Consolas, monospace',
300
+ }}
301
+ gutterLineClassName="min-w-[1.5rem]"
302
+ // `min-w-max` keeps a long line intact and lets the panel
303
+ // scroll sideways; while wrapping it would instead widen the
304
+ // column past the panel, so the toggle had no effect at all.
305
+ editorWrapperClassName={`flex-1 code-surface ${wordWrap ? 'min-w-0' : 'min-w-max'}`}
306
+ editorPadding={16}
307
+ editorClassName="font-mono focus:outline-none"
308
+ editorStyle={{
309
+ fontFamily: '"Fira Code", "JetBrains Mono", "SF Mono", Consolas, monospace',
310
+ fontSize: zoomLevel,
311
+ lineHeight: 1.5,
312
+ minHeight: '100%',
313
+ // Breathing room under the last line. It has to live on the
314
+ // surface, not on the scroll container: a scroll container's
315
+ // own bottom padding is not part of its scrollable overflow,
316
+ // so `pb-*` on the scroller left the last line flush with the
317
+ // panel's edge (measured: padding-bottom 32px did not change
318
+ // scrollHeight by a single pixel).
319
+ paddingBottom: 16,
320
+ }}
167
321
  />
168
- </div>
169
- ) : (
170
- <CodeSurface
171
- value={currentContent}
172
- onValueChange={editor.onChange}
173
- language={editor.language}
174
- wordWrap={wordWrap}
175
- rootClassName="flex"
176
- gutterClassName="flex flex-col text-right pl-4 pr-3 select-none text-ink/30 font-mono border-r border-ink/10 bg-canvas sticky left-0 z-10"
177
- gutterStyle={{
178
- fontSize: zoomLevel,
179
- paddingTop: 16,
180
- paddingBottom: 16,
181
- lineHeight: 1.5,
182
- fontFamily: '"Fira Code", "JetBrains Mono", "SF Mono", Consolas, monospace',
183
- }}
184
- gutterLineClassName="min-w-[1.5rem]"
185
- // `min-w-max` keeps a long line intact and lets the panel
186
- // scroll sideways; while wrapping it would instead widen the
187
- // column past the panel, so the toggle had no effect at all.
188
- editorWrapperClassName={`flex-1 code-surface ${wordWrap ? 'min-w-0' : 'min-w-max'}`}
189
- editorPadding={16}
190
- editorClassName="font-mono focus:outline-none"
191
- editorStyle={{
192
- fontFamily: '"Fira Code", "JetBrains Mono", "SF Mono", Consolas, monospace',
193
- fontSize: zoomLevel,
194
- lineHeight: 1.5,
195
- minHeight: '100%',
196
- // Breathing room under the last line. It has to live on the
197
- // surface, not on the scroll container: a scroll container's
198
- // own bottom padding is not part of its scrollable overflow,
199
- // so `pb-*` on the scroller left the last line flush with the
200
- // panel's edge (measured: padding-bottom 32px did not change
201
- // scrollHeight by a single pixel).
202
- paddingBottom: 16,
203
- }}
204
- />
205
- )}
322
+ )}
323
+ </div>
206
324
  </div>
207
325
  )}
208
326
  </>
@@ -1,6 +1,6 @@
1
1
  import { createPortal } from 'preact/compat';
2
2
  import type { FormEvent, RefObject } from 'preact/compat';
3
- import { Copy, Edit2, ExternalLink, Eye, GitCompare, History, RefreshCw, Trash2, X } from 'lucide-preact';
3
+ import { Copy, Edit2, ExternalLink, Eye, FilePlus, GitCompare, History, RefreshCw, Trash2, X } from 'lucide-preact';
4
4
 
5
5
  interface ContextMenuProps {
6
6
  x: number;
@@ -13,7 +13,9 @@ interface ContextMenuProps {
13
13
 
14
14
  export function FileContextMenu({ x, y, isFolder, hasGitStatus, onAction, menuRef }: ContextMenuProps) {
15
15
  const menuWidth = 190;
16
- const menuHeight = isFolder ? 220 : hasGitStatus ? 285 : 255;
16
+ // The row count differs by kind, and the clamp below needs the real height —
17
+ // an under-estimate puts the menu's last rows off-screen at the bottom edge.
18
+ const menuHeight = isFolder ? 250 : hasGitStatus ? 285 : 255;
17
19
 
18
20
  const posX = typeof window !== 'undefined' ? Math.max(10, Math.min(x, window.innerWidth - menuWidth - 12)) : x;
19
21
  const posY = typeof window !== 'undefined' ? Math.max(10, Math.min(y, window.innerHeight - menuHeight - 12)) : y;
@@ -44,6 +46,15 @@ export function FileContextMenu({ x, y, isFolder, hasGitStatus, onAction, menuRe
44
46
  )}
45
47
  </>
46
48
  )}
49
+ {isFolder && (
50
+ <>
51
+ <button onClick={() => onAction('new_file')} className="w-full text-left px-3 py-1.5 hover:bg-ink/5 flex items-center space-x-2 transition-colors">
52
+ <FilePlus size={13} className="text-ink/60" />
53
+ <span>New File</span>
54
+ </button>
55
+ <div className="my-1 border-t border-ink/10"></div>
56
+ </>
57
+ )}
47
58
  <button onClick={() => onAction('explorer')} className="w-full text-left px-3 py-1.5 hover:bg-ink/5 flex items-center space-x-2 transition-colors">
48
59
  <ExternalLink size={13} className="text-ink/60" />
49
60
  <span>Go to Explorer</span>
@@ -77,6 +88,72 @@ export function FileContextMenu({ x, y, isFolder, hasGitStatus, onAction, menuRe
77
88
  );
78
89
  }
79
90
 
91
+ interface NewFileModalProps {
92
+ /** Folder the file will be created in, relative to the listed root. */
93
+ folderPath: string;
94
+ value: string;
95
+ /** Server's refusal (a name that already exists, a bad character), shown inline. */
96
+ error?: string | null;
97
+ isLoading: boolean;
98
+ onChange: (value: string) => void;
99
+ onCancel: () => void;
100
+ onSubmit: (e: FormEvent) => void;
101
+ }
102
+
103
+ /**
104
+ * The "New File" dialog.
105
+ *
106
+ * Only a name is asked for — the folder is the one that was right-clicked, and
107
+ * showing it as a fixed prefix is what makes the field's meaning unambiguous.
108
+ * The input starts empty (no `.txt` default): a name is what the user has in
109
+ * mind, and a prefilled suffix is a character to delete before every keystroke
110
+ * lands. The server's refusal is rendered INLINE rather than as a toast: the
111
+ * field is still open, and "already exists" is a message about what to type
112
+ * next, not a notification about something that happened.
113
+ */
114
+ export function FileCreateModal({ folderPath, value, error, isLoading, onChange, onCancel, onSubmit }: NewFileModalProps) {
115
+ return createPortal(
116
+ <div className="fixed inset-0 bg-ink/20 z-[100] flex items-center justify-center p-4 backdrop-blur-sm">
117
+ <form onSubmit={onSubmit} className="bg-paper rounded shadow-xl p-4 w-full max-w-sm border border-ink/10 font-sans">
118
+ <h3 className="font-semibold text-ink mb-3 text-sm">New File</h3>
119
+ <div className="flex items-center gap-1 mb-1 text-xs font-mono text-ink/50 min-w-0">
120
+ <span className="truncate" title={folderPath}>{folderPath || '.'}/</span>
121
+ </div>
122
+ <input
123
+ autoFocus
124
+ type="text"
125
+ value={value}
126
+ onChange={e => onChange(e.currentTarget.value)}
127
+ placeholder="file name"
128
+ aria-label="New file name"
129
+ className={`w-full bg-paper border rounded px-2 py-1.5 text-xs font-mono focus:outline-none mb-2 ${
130
+ error ? 'border-error' : 'border-ink/20 focus:border-ink'
131
+ }`}
132
+ />
133
+ {error && <p className="text-[11px] text-error mb-2">{error}</p>}
134
+ <div className="flex justify-end space-x-2 mt-2">
135
+ <button
136
+ type="button"
137
+ onClick={onCancel}
138
+ className="px-3 py-1.5 text-xs text-ink hover:bg-ink/5 rounded transition-colors"
139
+ disabled={isLoading}
140
+ >
141
+ Cancel
142
+ </button>
143
+ <button
144
+ type="submit"
145
+ className="px-3 py-1.5 text-xs bg-ink text-canvas rounded hover:bg-ink/90 transition-colors flex items-center"
146
+ disabled={isLoading || !value.trim()}
147
+ >
148
+ {isLoading ? 'Creating...' : 'Create'}
149
+ </button>
150
+ </div>
151
+ </form>
152
+ </div>,
153
+ document.body
154
+ );
155
+ }
156
+
80
157
  interface DeleteModalProps {
81
158
  fileName: string;
82
159
  isFolder: boolean;