@neurocode-ai/app 1.18.8

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 (577) hide show
  1. package/AGENTS.md +35 -0
  2. package/README.md +50 -0
  3. package/V1_API_MIGRATION.md +220 -0
  4. package/bunfig.toml +3 -0
  5. package/create-effect-simplification-spec.md +515 -0
  6. package/e2e/performance/AGENTS.md +13 -0
  7. package/e2e/performance/README.md +79 -0
  8. package/e2e/performance/benchmark.ts +144 -0
  9. package/e2e/performance/chrome-trace.ts +95 -0
  10. package/e2e/performance/playwright.config.ts +20 -0
  11. package/e2e/performance/playwright.uncapped.config.ts +13 -0
  12. package/e2e/performance/timeline/first-navigation-benchmark.spec.ts +87 -0
  13. package/e2e/performance/timeline/first-navigation-metrics.ts +32 -0
  14. package/e2e/performance/timeline/first-navigation-probe.ts +86 -0
  15. package/e2e/performance/timeline/home-tab-navigation-benchmark.spec.ts +114 -0
  16. package/e2e/performance/timeline/navigation-milestones.ts +128 -0
  17. package/e2e/performance/timeline/review-pane-scaling-benchmark.spec.ts +312 -0
  18. package/e2e/performance/timeline/session-parent-hydration-benchmark.spec.ts +172 -0
  19. package/e2e/performance/timeline/session-tab-flash.spec.ts +67 -0
  20. package/e2e/performance/timeline/session-tab-repaint-probe.ts +251 -0
  21. package/e2e/performance/timeline/session-tab-switch-benchmark.spec.ts +144 -0
  22. package/e2e/performance/timeline/session-tab-switch-metrics.ts +59 -0
  23. package/e2e/performance/timeline/session-tab-switch-probe.ts +226 -0
  24. package/e2e/performance/timeline/session-timeline-benchmark.fixture.ts +504 -0
  25. package/e2e/performance/timeline/session-timeline-benchmark.spec.ts +306 -0
  26. package/e2e/performance/timeline/session-timeline-profile.ts +40 -0
  27. package/e2e/performance/timeline/session-timeline-stream-probe.ts +547 -0
  28. package/e2e/performance/timeline/session-timeline-stress.fixture.ts +369 -0
  29. package/e2e/performance/timeline/timeline-test-helpers.ts +134 -0
  30. package/e2e/performance/timeline-stability/README.md +61 -0
  31. package/e2e/performance/timeline-stability/adverse.spec.ts +250 -0
  32. package/e2e/performance/timeline-stability/context-matrix.spec.ts +192 -0
  33. package/e2e/performance/timeline-stability/environment-matrix.spec.ts +113 -0
  34. package/e2e/performance/timeline-stability/file-matrix.spec.ts +121 -0
  35. package/e2e/performance/timeline-stability/file-mutation.spec.ts +113 -0
  36. package/e2e/performance/timeline-stability/fixture.test.ts +68 -0
  37. package/e2e/performance/timeline-stability/fixture.ts +566 -0
  38. package/e2e/performance/timeline-stability/interaction.spec.ts +242 -0
  39. package/e2e/performance/timeline-stability/lifecycle.spec.ts +157 -0
  40. package/e2e/performance/timeline-stability/oracle-browser.spec.ts +75 -0
  41. package/e2e/performance/timeline-stability/playwright.config.ts +17 -0
  42. package/e2e/performance/timeline-stability/scroll-interaction.spec.ts +353 -0
  43. package/e2e/performance/timeline-stability/shell-matrix.spec.ts +255 -0
  44. package/e2e/performance/timeline-stability/tool-mutation.spec.ts +106 -0
  45. package/e2e/performance/timeline-stability/tools.spec.ts +198 -0
  46. package/e2e/performance/timeline-stability/transition-matrix.spec.ts +272 -0
  47. package/e2e/performance/unit/chrome-trace-write.test.ts +15 -0
  48. package/e2e/performance/unit/first-navigation-metrics.test.ts +32 -0
  49. package/e2e/performance/unit/mock-server.test.ts +46 -0
  50. package/e2e/performance/unit/navigation-milestones.test.ts +34 -0
  51. package/e2e/performance/unit/session-tab-repaint-probe.test.ts +42 -0
  52. package/e2e/performance/unit/session-tab-switch-metrics.test.ts +86 -0
  53. package/e2e/performance/unit/session-tab-switch-probe.test.ts +55 -0
  54. package/e2e/performance/unit/session-timeline-stream-probe.test.ts +14 -0
  55. package/e2e/performance/unit/session-timeline-visual-tracking.test.ts +16 -0
  56. package/e2e/performance/unit/timeline-test-helpers.test.ts +10 -0
  57. package/e2e/performance/unit/visual-stability.test.ts +392 -0
  58. package/e2e/regression/cross-server-tab-close.spec.ts +153 -0
  59. package/e2e/regression/file-browser-sidebar-tab-switch.spec.ts +150 -0
  60. package/e2e/regression/legacy-new-session.spec.ts +41 -0
  61. package/e2e/regression/new-session-panel-corner.spec.ts +83 -0
  62. package/e2e/regression/prompt-input-v2-command-draft.spec.ts +50 -0
  63. package/e2e/regression/prompt-thinking-level.spec.ts +84 -0
  64. package/e2e/regression/remote-session-settings.spec.ts +302 -0
  65. package/e2e/regression/remote-tab-busy.spec.ts +131 -0
  66. package/e2e/regression/review-image-flash.spec.ts +202 -0
  67. package/e2e/regression/review-line-comment.spec.ts +158 -0
  68. package/e2e/regression/review-open-file.spec.ts +163 -0
  69. package/e2e/regression/review-state-persistence.spec.ts +155 -0
  70. package/e2e/regression/review-tab-switch.spec.ts +147 -0
  71. package/e2e/regression/review-terminal-stacked.spec.ts +308 -0
  72. package/e2e/regression/session-list-path-loading.spec.ts +41 -0
  73. package/e2e/regression/session-request-docks.spec.ts +221 -0
  74. package/e2e/regression/session-timeline-accessibility.spec.ts +22 -0
  75. package/e2e/regression/session-timeline-collapse-state.spec.ts +439 -0
  76. package/e2e/regression/session-timeline-context-resize.spec.ts +374 -0
  77. package/e2e/regression/session-timeline-context-state.spec.ts +31 -0
  78. package/e2e/regression/session-timeline-file-projection.spec.ts +52 -0
  79. package/e2e/regression/session-timeline-file-state.spec.ts +94 -0
  80. package/e2e/regression/session-timeline-history-root.spec.ts +242 -0
  81. package/e2e/regression/session-timeline-lifecycle-state.spec.ts +111 -0
  82. package/e2e/regression/session-timeline-locale-projection.spec.ts +25 -0
  83. package/e2e/regression/session-timeline-projection.spec.ts +287 -0
  84. package/e2e/regression/session-timeline-reasoning-projection.spec.ts +93 -0
  85. package/e2e/regression/session-timeline-reducer-projection.spec.ts +43 -0
  86. package/e2e/regression/session-timeline-shell-outline.spec.ts +228 -0
  87. package/e2e/regression/session-timeline-tool-projection.spec.ts +99 -0
  88. package/e2e/regression/session-timeline-tool-state.spec.ts +77 -0
  89. package/e2e/regression/session-timeline-transport.spec.ts +116 -0
  90. package/e2e/regression/session-todo-dock-navigation.spec.ts +190 -0
  91. package/e2e/regression/subagent-child-navigation.spec.ts +203 -0
  92. package/e2e/regression/tab-navigate-mousedown.spec.ts +127 -0
  93. package/e2e/regression/terminal-composer-focus.spec.ts +227 -0
  94. package/e2e/regression/terminal-hidden.spec.ts +117 -0
  95. package/e2e/regression/terminal-tab-switch.spec.ts +165 -0
  96. package/e2e/reproduction/timeline-suspense/index.html +34 -0
  97. package/e2e/reproduction/timeline-suspense/main.tsx +315 -0
  98. package/e2e/reproduction/timeline-suspense/playwright.config.ts +34 -0
  99. package/e2e/reproduction/timeline-suspense/timeline-suspense.repro.ts +179 -0
  100. package/e2e/reproduction/timeline-suspense/vite.config.ts +7 -0
  101. package/e2e/smoke/session-timeline.fixture.ts +315 -0
  102. package/e2e/smoke/session-timeline.spec.ts +740 -0
  103. package/e2e/tsconfig.json +20 -0
  104. package/e2e/utils/errors.ts +18 -0
  105. package/e2e/utils/mock-server.ts +443 -0
  106. package/e2e/utils/sse-transport.ts +312 -0
  107. package/e2e/utils/visual-stability/analyzer.ts +209 -0
  108. package/e2e/utils/visual-stability/capture.ts +51 -0
  109. package/e2e/utils/visual-stability/index.ts +8 -0
  110. package/e2e/utils/visual-stability/invariant.ts +112 -0
  111. package/e2e/utils/visual-stability/model.ts +47 -0
  112. package/e2e/utils/visual-stability/probe.ts +226 -0
  113. package/e2e/utils/visual-stability/regions.ts +18 -0
  114. package/e2e/utils/visual-stability/reporter.ts +63 -0
  115. package/e2e/utils/visual-stability/scenario.ts +20 -0
  116. package/e2e/utils/visual-stability.ts +54 -0
  117. package/e2e/utils/waits.ts +11 -0
  118. package/happydom.ts +75 -0
  119. package/index.html +28 -0
  120. package/package.json +96 -0
  121. package/playwright.config.ts +45 -0
  122. package/public/_headers +17 -0
  123. package/public/apple-touch-icon-v3.png +1 -0
  124. package/public/apple-touch-icon.png +1 -0
  125. package/public/assets/Inter.ttf +0 -0
  126. package/public/assets/JetBrainsMonoNerdFontMono-Regular.woff2 +0 -0
  127. package/public/favicon-96x96-v3.png +1 -0
  128. package/public/favicon-96x96.png +1 -0
  129. package/public/favicon-v3.ico +1 -0
  130. package/public/favicon-v3.svg +1 -0
  131. package/public/favicon.ico +1 -0
  132. package/public/favicon.svg +1 -0
  133. package/public/oc-theme-preload.js +40 -0
  134. package/public/site.webmanifest +1 -0
  135. package/public/social-share-zen.png +1 -0
  136. package/public/social-share.png +1 -0
  137. package/public/web-app-manifest-192x192.png +1 -0
  138. package/public/web-app-manifest-512x512.png +1 -0
  139. package/src/addons/serialize.test.ts +327 -0
  140. package/src/addons/serialize.ts +642 -0
  141. package/src/app.tsx +659 -0
  142. package/src/assets/help/home.png +0 -0
  143. package/src/assets/help/introducing-tabs.mp4 +0 -0
  144. package/src/assets/help/placeholder.png +0 -0
  145. package/src/assets/help/tabs.png +0 -0
  146. package/src/components/command-palette.ts +281 -0
  147. package/src/components/command-tooltip-keybind.test.ts +22 -0
  148. package/src/components/command-tooltip-keybind.ts +11 -0
  149. package/src/components/debug-bar.tsx +550 -0
  150. package/src/components/dialog-command-palette-v2.css +220 -0
  151. package/src/components/dialog-command-palette-v2.tsx +342 -0
  152. package/src/components/dialog-connect-provider.stories.tsx +73 -0
  153. package/src/components/dialog-connect-provider.tsx +1178 -0
  154. package/src/components/dialog-custom-provider-form.ts +158 -0
  155. package/src/components/dialog-custom-provider.test.ts +80 -0
  156. package/src/components/dialog-custom-provider.tsx +330 -0
  157. package/src/components/dialog-edit-project-v2.tsx +156 -0
  158. package/src/components/dialog-edit-project.tsx +170 -0
  159. package/src/components/dialog-fork.tsx +104 -0
  160. package/src/components/dialog-manage-models.tsx +265 -0
  161. package/src/components/dialog-release-notes.tsx +144 -0
  162. package/src/components/dialog-select-directory-v2.css +107 -0
  163. package/src/components/dialog-select-directory-v2.tsx +378 -0
  164. package/src/components/dialog-select-directory.tsx +191 -0
  165. package/src/components/dialog-select-file.tsx +189 -0
  166. package/src/components/dialog-select-mcp.tsx +92 -0
  167. package/src/components/dialog-select-model-search.test.ts +19 -0
  168. package/src/components/dialog-select-model-search.ts +18 -0
  169. package/src/components/dialog-select-model-unpaid-v2.stories.tsx +55 -0
  170. package/src/components/dialog-select-model-unpaid-v2.tsx +175 -0
  171. package/src/components/dialog-select-model-unpaid.tsx +147 -0
  172. package/src/components/dialog-select-model.tsx +549 -0
  173. package/src/components/dialog-select-server.tsx +722 -0
  174. package/src/components/dialog-settings.tsx +94 -0
  175. package/src/components/dialog-usage-exceeded.tsx +44 -0
  176. package/src/components/directory-picker-domain.test.ts +248 -0
  177. package/src/components/directory-picker-domain.ts +407 -0
  178. package/src/components/directory-picker-policy.ts +7 -0
  179. package/src/components/directory-picker.test.ts +21 -0
  180. package/src/components/directory-picker.tsx +45 -0
  181. package/src/components/edit-project.ts +122 -0
  182. package/src/components/file-tree-v2-model.test.ts +74 -0
  183. package/src/components/file-tree-v2-model.ts +107 -0
  184. package/src/components/file-tree-v2.tsx +299 -0
  185. package/src/components/file-tree.test.ts +80 -0
  186. package/src/components/file-tree.tsx +509 -0
  187. package/src/components/help-button.tsx +148 -0
  188. package/src/components/link.tsx +26 -0
  189. package/src/components/model-tooltip.tsx +124 -0
  190. package/src/components/pierre-tree.test.ts +23 -0
  191. package/src/components/prompt-input/attachments.test.ts +108 -0
  192. package/src/components/prompt-input/attachments.ts +228 -0
  193. package/src/components/prompt-input/build-request-parts.test.ts +396 -0
  194. package/src/components/prompt-input/build-request-parts.ts +213 -0
  195. package/src/components/prompt-input/context-items.tsx +98 -0
  196. package/src/components/prompt-input/contracts.ts +57 -0
  197. package/src/components/prompt-input/drag-overlay.tsx +25 -0
  198. package/src/components/prompt-input/editor-dom.test.ts +99 -0
  199. package/src/components/prompt-input/editor-dom.ts +148 -0
  200. package/src/components/prompt-input/files.ts +98 -0
  201. package/src/components/prompt-input/history-store.ts +47 -0
  202. package/src/components/prompt-input/history.test.ts +153 -0
  203. package/src/components/prompt-input/history.ts +256 -0
  204. package/src/components/prompt-input/image-attachments.css +43 -0
  205. package/src/components/prompt-input/image-attachments.tsx +168 -0
  206. package/src/components/prompt-input/paste.ts +24 -0
  207. package/src/components/prompt-input/placeholder.test.ts +48 -0
  208. package/src/components/prompt-input/placeholder.ts +20 -0
  209. package/src/components/prompt-input/slash-popover.tsx +376 -0
  210. package/src/components/prompt-input/submission-state.ts +33 -0
  211. package/src/components/prompt-input/submit.test.ts +598 -0
  212. package/src/components/prompt-input/submit.ts +634 -0
  213. package/src/components/prompt-input/transient-state.ts +46 -0
  214. package/src/components/prompt-input-v2.tsx +592 -0
  215. package/src/components/prompt-input.stories.tsx +223 -0
  216. package/src/components/prompt-input.tsx +1790 -0
  217. package/src/components/prompt-project-selector.tsx +593 -0
  218. package/src/components/prompt-workspace-selector.tsx +128 -0
  219. package/src/components/server/server-row-menu.tsx +96 -0
  220. package/src/components/server/server-row.tsx +127 -0
  221. package/src/components/session/index.ts +7 -0
  222. package/src/components/session/open-in-app-v2.tsx +98 -0
  223. package/src/components/session/open-in-app.tsx +229 -0
  224. package/src/components/session/session-context-breakdown.test.ts +61 -0
  225. package/src/components/session/session-context-breakdown.ts +132 -0
  226. package/src/components/session/session-context-format.ts +20 -0
  227. package/src/components/session/session-context-metrics.test.ts +99 -0
  228. package/src/components/session/session-context-metrics.ts +65 -0
  229. package/src/components/session/session-context-tab.tsx +343 -0
  230. package/src/components/session/session-header.tsx +568 -0
  231. package/src/components/session/session-new-design-view.tsx +16 -0
  232. package/src/components/session/session-new-view.tsx +91 -0
  233. package/src/components/session/session-sortable-tab-v2.tsx +74 -0
  234. package/src/components/session/session-sortable-tab.tsx +78 -0
  235. package/src/components/session/session-sortable-terminal-tab-v2.tsx +284 -0
  236. package/src/components/session/session-sortable-terminal-tab.tsx +193 -0
  237. package/src/components/session-context-usage.tsx +169 -0
  238. package/src/components/settings-dialog.tsx +43 -0
  239. package/src/components/settings-general.tsx +794 -0
  240. package/src/components/settings-keybinds.tsx +553 -0
  241. package/src/components/settings-list.tsx +5 -0
  242. package/src/components/settings-models.tsx +149 -0
  243. package/src/components/settings-providers.tsx +264 -0
  244. package/src/components/settings-server-picker.tsx +106 -0
  245. package/src/components/settings-servers.tsx +33 -0
  246. package/src/components/settings-v2/dialog-server-v2.tsx +134 -0
  247. package/src/components/settings-v2/dialog-settings-v2.tsx +98 -0
  248. package/src/components/settings-v2/general.tsx +722 -0
  249. package/src/components/settings-v2/index.tsx +1 -0
  250. package/src/components/settings-v2/interface-transition.stories.tsx +76 -0
  251. package/src/components/settings-v2/interface-transition.tsx +54 -0
  252. package/src/components/settings-v2/models.tsx +138 -0
  253. package/src/components/settings-v2/parts/list.tsx +6 -0
  254. package/src/components/settings-v2/parts/row.tsx +20 -0
  255. package/src/components/settings-v2/providers.tsx +264 -0
  256. package/src/components/settings-v2/servers.tsx +139 -0
  257. package/src/components/settings-v2/settings-v2.css +675 -0
  258. package/src/components/status-popover-body.tsx +514 -0
  259. package/src/components/status-popover-indicator.test.ts +36 -0
  260. package/src/components/status-popover-indicator.ts +20 -0
  261. package/src/components/status-popover.tsx +190 -0
  262. package/src/components/terminal.tsx +759 -0
  263. package/src/components/titlebar-history.test.ts +63 -0
  264. package/src/components/titlebar-history.ts +57 -0
  265. package/src/components/titlebar-session-events.test.ts +32 -0
  266. package/src/components/titlebar-session-events.ts +35 -0
  267. package/src/components/titlebar-tab-gesture.test.ts +33 -0
  268. package/src/components/titlebar-tab-gesture.ts +17 -0
  269. package/src/components/titlebar-tab-nav.css +100 -0
  270. package/src/components/titlebar-tab-nav.tsx +429 -0
  271. package/src/components/titlebar-tab-popover.css +127 -0
  272. package/src/components/titlebar-tab-popover.tsx +92 -0
  273. package/src/components/titlebar-tab-strip.tsx +333 -0
  274. package/src/components/titlebar.css +77 -0
  275. package/src/components/titlebar.tsx +776 -0
  276. package/src/components/ui/drawer.tsx +119 -0
  277. package/src/components/updater-action.test.ts +26 -0
  278. package/src/components/updater-action.ts +51 -0
  279. package/src/components/virtual-scroll-element.test.ts +18 -0
  280. package/src/components/virtual-scroll-element.ts +4 -0
  281. package/src/components/windows-app-menu.tsx +131 -0
  282. package/src/constants/file-picker.ts +89 -0
  283. package/src/context/closed-tabs.ts +40 -0
  284. package/src/context/command-keybind.test.ts +69 -0
  285. package/src/context/command.test.ts +55 -0
  286. package/src/context/command.tsx +467 -0
  287. package/src/context/comments.test.ts +188 -0
  288. package/src/context/comments.tsx +261 -0
  289. package/src/context/directory-sync.ts +155 -0
  290. package/src/context/file/content-cache.ts +88 -0
  291. package/src/context/file/path.test.ts +360 -0
  292. package/src/context/file/path.ts +151 -0
  293. package/src/context/file/tree-store.ts +174 -0
  294. package/src/context/file/types.ts +41 -0
  295. package/src/context/file/view-cache.ts +147 -0
  296. package/src/context/file/watcher.test.ts +149 -0
  297. package/src/context/file/watcher.ts +53 -0
  298. package/src/context/file-content-eviction-accounting.test.ts +65 -0
  299. package/src/context/file.tsx +303 -0
  300. package/src/context/global-sync/bootstrap.test.ts +231 -0
  301. package/src/context/global-sync/bootstrap.ts +536 -0
  302. package/src/context/global-sync/child-store.test.ts +276 -0
  303. package/src/context/global-sync/child-store.ts +396 -0
  304. package/src/context/global-sync/event-reducer.test.ts +616 -0
  305. package/src/context/global-sync/event-reducer.ts +478 -0
  306. package/src/context/global-sync/eviction.ts +28 -0
  307. package/src/context/global-sync/home-session-index.test.ts +154 -0
  308. package/src/context/global-sync/home-session-index.ts +174 -0
  309. package/src/context/global-sync/mcp.test.ts +54 -0
  310. package/src/context/global-sync/mcp.ts +19 -0
  311. package/src/context/global-sync/queue.test.ts +46 -0
  312. package/src/context/global-sync/queue.ts +87 -0
  313. package/src/context/global-sync/session-cache.test.ts +104 -0
  314. package/src/context/global-sync/session-cache.ts +62 -0
  315. package/src/context/global-sync/session-load.ts +33 -0
  316. package/src/context/global-sync/session-trim.test.ts +59 -0
  317. package/src/context/global-sync/session-trim.ts +57 -0
  318. package/src/context/global-sync/types.ts +135 -0
  319. package/src/context/global-sync/utils.test.ts +133 -0
  320. package/src/context/global-sync/utils.ts +171 -0
  321. package/src/context/global.tsx +161 -0
  322. package/src/context/highlights.tsx +233 -0
  323. package/src/context/language.tsx +243 -0
  324. package/src/context/layout-helpers.ts +38 -0
  325. package/src/context/layout-scroll.test.ts +64 -0
  326. package/src/context/layout-scroll.ts +126 -0
  327. package/src/context/layout-tabs.test.ts +82 -0
  328. package/src/context/layout-tabs.ts +103 -0
  329. package/src/context/layout.test.ts +69 -0
  330. package/src/context/layout.tsx +1075 -0
  331. package/src/context/local-agent.test.ts +29 -0
  332. package/src/context/local-agent.ts +7 -0
  333. package/src/context/local.tsx +417 -0
  334. package/src/context/mcp.ts +19 -0
  335. package/src/context/model-variant.test.ts +86 -0
  336. package/src/context/model-variant.ts +52 -0
  337. package/src/context/models.tsx +173 -0
  338. package/src/context/notification.tsx +478 -0
  339. package/src/context/permission-auto-respond.test.ts +125 -0
  340. package/src/context/permission-auto-respond.ts +60 -0
  341. package/src/context/permission.tsx +484 -0
  342. package/src/context/platform.tsx +142 -0
  343. package/src/context/prompt-state.test.ts +29 -0
  344. package/src/context/prompt-state.ts +266 -0
  345. package/src/context/prompt.tsx +170 -0
  346. package/src/context/sdk.tsx +17 -0
  347. package/src/context/server-sdk.test.ts +196 -0
  348. package/src/context/server-sdk.tsx +444 -0
  349. package/src/context/server-session-v2-reducer.test.ts +156 -0
  350. package/src/context/server-session-v2-reducer.ts +502 -0
  351. package/src/context/server-session.test.ts +1638 -0
  352. package/src/context/server-session.ts +1425 -0
  353. package/src/context/server-sync.test.ts +231 -0
  354. package/src/context/server-sync.tsx +753 -0
  355. package/src/context/server.test.ts +245 -0
  356. package/src/context/server.tsx +360 -0
  357. package/src/context/settings.test.ts +79 -0
  358. package/src/context/settings.tsx +531 -0
  359. package/src/context/sync-optimistic.test.ts +123 -0
  360. package/src/context/sync.tsx +119 -0
  361. package/src/context/tab-memory.ts +36 -0
  362. package/src/context/tabs.test.ts +107 -0
  363. package/src/context/tabs.tsx +387 -0
  364. package/src/context/terminal-title.ts +24 -0
  365. package/src/context/terminal.test.ts +91 -0
  366. package/src/context/terminal.tsx +546 -0
  367. package/src/custom-elements.d.ts +1 -0
  368. package/src/desktop-menu.test.ts +13 -0
  369. package/src/desktop-menu.ts +223 -0
  370. package/src/entry.tsx +182 -0
  371. package/src/env.d.ts +31 -0
  372. package/src/hooks/provider-catalog.test.ts +59 -0
  373. package/src/hooks/provider-catalog.ts +27 -0
  374. package/src/hooks/use-providers.ts +71 -0
  375. package/src/i18n/ar.ts +1008 -0
  376. package/src/i18n/br.ts +1024 -0
  377. package/src/i18n/bs.ts +1100 -0
  378. package/src/i18n/da.ts +1092 -0
  379. package/src/i18n/de.ts +1037 -0
  380. package/src/i18n/en.ts +1102 -0
  381. package/src/i18n/es.ts +1108 -0
  382. package/src/i18n/fr.ts +1037 -0
  383. package/src/i18n/ja.ts +1016 -0
  384. package/src/i18n/ko.ts +1015 -0
  385. package/src/i18n/no.ts +1111 -0
  386. package/src/i18n/parity.test.ts +118 -0
  387. package/src/i18n/pl.ts +1024 -0
  388. package/src/i18n/ru.ts +1103 -0
  389. package/src/i18n/th.ts +1087 -0
  390. package/src/i18n/tr.ts +1106 -0
  391. package/src/i18n/uk.ts +1105 -0
  392. package/src/i18n/zh.ts +1078 -0
  393. package/src/i18n/zht.ts +1074 -0
  394. package/src/index.css +330 -0
  395. package/src/index.ts +30 -0
  396. package/src/pages/directory-layout.tsx +122 -0
  397. package/src/pages/error-description.test.ts +17 -0
  398. package/src/pages/error-description.ts +11 -0
  399. package/src/pages/error.tsx +372 -0
  400. package/src/pages/home/home-controller.ts +108 -0
  401. package/src/pages/home/home-projects-controller.tsx +128 -0
  402. package/src/pages/home/home-projects-view.tsx +615 -0
  403. package/src/pages/home/home-projects.tsx +40 -0
  404. package/src/pages/home/home-scroll-controller.ts +145 -0
  405. package/src/pages/home/home-session-search-controller.ts +114 -0
  406. package/src/pages/home/home-sessions-controller.tsx +314 -0
  407. package/src/pages/home/home-sessions-view.tsx +550 -0
  408. package/src/pages/home/home-sessions.tsx +48 -0
  409. package/src/pages/home/legacy-home.tsx +142 -0
  410. package/src/pages/home-session-archive.test.ts +51 -0
  411. package/src/pages/home-session-archive.ts +27 -0
  412. package/src/pages/home-session-open.test.ts +31 -0
  413. package/src/pages/home-session-open.ts +14 -0
  414. package/src/pages/home.tsx +50 -0
  415. package/src/pages/layout/deep-links.ts +50 -0
  416. package/src/pages/layout/helpers.test.ts +321 -0
  417. package/src/pages/layout/helpers.ts +150 -0
  418. package/src/pages/layout/inline-editor.tsx +126 -0
  419. package/src/pages/layout/project-avatar-state.ts +49 -0
  420. package/src/pages/layout/session-tab-avatar.tsx +60 -0
  421. package/src/pages/layout/sidebar-items.tsx +335 -0
  422. package/src/pages/layout/sidebar-project.tsx +377 -0
  423. package/src/pages/layout/sidebar-shell.tsx +125 -0
  424. package/src/pages/layout/sidebar-workspace.tsx +488 -0
  425. package/src/pages/layout-new.tsx +53 -0
  426. package/src/pages/layout.tsx +2441 -0
  427. package/src/pages/new-session.tsx +282 -0
  428. package/src/pages/session/composer/index.ts +4 -0
  429. package/src/pages/session/composer/prompt-model-selection.ts +133 -0
  430. package/src/pages/session/composer/session-composer-controls.ts +129 -0
  431. package/src/pages/session/composer/session-composer-region-controller.ts +145 -0
  432. package/src/pages/session/composer/session-composer-region.tsx +168 -0
  433. package/src/pages/session/composer/session-composer-state.test.ts +138 -0
  434. package/src/pages/session/composer/session-composer-state.ts +204 -0
  435. package/src/pages/session/composer/session-followup-dock.tsx +109 -0
  436. package/src/pages/session/composer/session-permission-dock.tsx +74 -0
  437. package/src/pages/session/composer/session-question-dock.tsx +640 -0
  438. package/src/pages/session/composer/session-request-tree.ts +52 -0
  439. package/src/pages/session/composer/session-revert-dock.stories.tsx +124 -0
  440. package/src/pages/session/composer/session-revert-dock.tsx +188 -0
  441. package/src/pages/session/composer/session-todo-dock.tsx +277 -0
  442. package/src/pages/session/composer/todo-panel-motion.stories.tsx +622 -0
  443. package/src/pages/session/file-tab-scroll.test.ts +40 -0
  444. package/src/pages/session/file-tab-scroll.ts +67 -0
  445. package/src/pages/session/file-tabs.tsx +800 -0
  446. package/src/pages/session/handoff.ts +36 -0
  447. package/src/pages/session/helpers.test.ts +214 -0
  448. package/src/pages/session/helpers.ts +209 -0
  449. package/src/pages/session/message-gesture.test.ts +62 -0
  450. package/src/pages/session/message-gesture.ts +21 -0
  451. package/src/pages/session/message-id-from-hash.ts +6 -0
  452. package/src/pages/session/new-session-layout.ts +2 -0
  453. package/src/pages/session/review-tab.tsx +172 -0
  454. package/src/pages/session/session-layout.ts +30 -0
  455. package/src/pages/session/session-lineage.ts +69 -0
  456. package/src/pages/session/session-model-helpers.test.ts +151 -0
  457. package/src/pages/session/session-model-helpers.ts +63 -0
  458. package/src/pages/session/session-ownership.ts +37 -0
  459. package/src/pages/session/session-panel-layout.test.ts +19 -0
  460. package/src/pages/session/session-panel-layout.ts +6 -0
  461. package/src/pages/session/session-panel-width.test.ts +52 -0
  462. package/src/pages/session/session-panel-width.ts +19 -0
  463. package/src/pages/session/session-side-panel.tsx +865 -0
  464. package/src/pages/session/terminal-label.ts +16 -0
  465. package/src/pages/session/terminal-panel-v2.tsx +352 -0
  466. package/src/pages/session/terminal-panel.test.ts +25 -0
  467. package/src/pages/session/terminal-panel.tsx +343 -0
  468. package/src/pages/session/timeline/measure.test.ts +30 -0
  469. package/src/pages/session/timeline/measure.ts +5 -0
  470. package/src/pages/session/timeline/message-timeline.tsx +1864 -0
  471. package/src/pages/session/timeline/model.test.ts +95 -0
  472. package/src/pages/session/timeline/model.ts +128 -0
  473. package/src/pages/session/timeline/observe-element-offset.test.ts +199 -0
  474. package/src/pages/session/timeline/observe-element-offset.ts +73 -0
  475. package/src/pages/session/timeline/projection.test.ts +96 -0
  476. package/src/pages/session/timeline/projection.ts +81 -0
  477. package/src/pages/session/timeline/row-reconciliation.ts +56 -0
  478. package/src/pages/session/timeline/rows-current.test.ts +170 -0
  479. package/src/pages/session/timeline/rows.ts +344 -0
  480. package/src/pages/session/timeline/summary-diffs.test.ts +42 -0
  481. package/src/pages/session/timeline/summary-diffs.ts +20 -0
  482. package/src/pages/session/timeline/timeline-row.ts +80 -0
  483. package/src/pages/session/timeline/virtual-items.ts +3 -0
  484. package/src/pages/session/usage-exceeded-dialogs.tsx +104 -0
  485. package/src/pages/session/use-composer-commands.tsx +83 -0
  486. package/src/pages/session/use-session-commands.tsx +606 -0
  487. package/src/pages/session/use-session-hash-scroll.test.ts +16 -0
  488. package/src/pages/session/use-session-hash-scroll.ts +202 -0
  489. package/src/pages/session/v2/review-diff-kinds.test.ts +70 -0
  490. package/src/pages/session/v2/review-diff-kinds.ts +62 -0
  491. package/src/pages/session/v2/review-panel-v2-state.ts +41 -0
  492. package/src/pages/session/v2/review-panel-v2.tsx +255 -0
  493. package/src/pages/session/v2/session-file-browser-tab.tsx +181 -0
  494. package/src/pages/session/v2/session-file-list-v2.tsx +166 -0
  495. package/src/pages/session.tsx +2385 -0
  496. package/src/sst-env.d.ts +12 -0
  497. package/src/theme-preload.test.ts +46 -0
  498. package/src/updater.ts +17 -0
  499. package/src/utils/agent.ts +44 -0
  500. package/src/utils/aim.ts +138 -0
  501. package/src/utils/base64.ts +10 -0
  502. package/src/utils/comment-note.ts +88 -0
  503. package/src/utils/diffs.test.ts +75 -0
  504. package/src/utils/diffs.ts +50 -0
  505. package/src/utils/file-manager.ts +24 -0
  506. package/src/utils/id.ts +93 -0
  507. package/src/utils/notification-click.test.ts +27 -0
  508. package/src/utils/notification-click.ts +13 -0
  509. package/src/utils/path-key.ts +24 -0
  510. package/src/utils/persist.test.ts +211 -0
  511. package/src/utils/persist.ts +661 -0
  512. package/src/utils/prompt.test.ts +44 -0
  513. package/src/utils/prompt.ts +203 -0
  514. package/src/utils/refcount.test.ts +49 -0
  515. package/src/utils/refcount.ts +32 -0
  516. package/src/utils/runtime-adapters.test.ts +64 -0
  517. package/src/utils/runtime-adapters.ts +39 -0
  518. package/src/utils/same.ts +6 -0
  519. package/src/utils/scoped-cache.test.ts +69 -0
  520. package/src/utils/scoped-cache.ts +104 -0
  521. package/src/utils/search-keydown.ts +116 -0
  522. package/src/utils/server-compat.test.ts +193 -0
  523. package/src/utils/server-compat.ts +512 -0
  524. package/src/utils/server-errors.test.ts +175 -0
  525. package/src/utils/server-errors.ts +109 -0
  526. package/src/utils/server-health.test.ts +178 -0
  527. package/src/utils/server-health.ts +172 -0
  528. package/src/utils/server-protocol.test.ts +40 -0
  529. package/src/utils/server-protocol.ts +35 -0
  530. package/src/utils/server-scope.test.ts +61 -0
  531. package/src/utils/server-scope.ts +73 -0
  532. package/src/utils/server.test.ts +23 -0
  533. package/src/utils/server.ts +62 -0
  534. package/src/utils/session-message.test.ts +214 -0
  535. package/src/utils/session-message.ts +355 -0
  536. package/src/utils/session-route.test.ts +71 -0
  537. package/src/utils/session-route.ts +39 -0
  538. package/src/utils/session-title.ts +7 -0
  539. package/src/utils/session.test.ts +94 -0
  540. package/src/utils/session.ts +37 -0
  541. package/src/utils/solid-dnd.tsx +49 -0
  542. package/src/utils/sound.ts +102 -0
  543. package/src/utils/terminal-websocket-url.test.ts +80 -0
  544. package/src/utils/terminal-websocket-url.ts +35 -0
  545. package/src/utils/terminal-writer.test.ts +64 -0
  546. package/src/utils/terminal-writer.ts +65 -0
  547. package/src/utils/time.ts +22 -0
  548. package/src/utils/toast.tsx +34 -0
  549. package/src/utils/uuid.test.ts +78 -0
  550. package/src/utils/uuid.ts +12 -0
  551. package/src/utils/worktree.test.ts +58 -0
  552. package/src/utils/worktree.ts +76 -0
  553. package/src/wsl/add-server-probes.ts +57 -0
  554. package/src/wsl/context.tsx +36 -0
  555. package/src/wsl/dialog-add-server.tsx +463 -0
  556. package/src/wsl/dialog-add-wsl-server.css +392 -0
  557. package/src/wsl/settings-model.test.ts +231 -0
  558. package/src/wsl/settings-model.ts +341 -0
  559. package/src/wsl/settings.tsx +173 -0
  560. package/src/wsl/types.ts +85 -0
  561. package/sst-env.d.ts +10 -0
  562. package/test-browser/command-palette.test.ts +78 -0
  563. package/test-browser/motion-spring.test.ts +20 -0
  564. package/test-browser/prompt-attachments.test.ts +90 -0
  565. package/test-browser/prompt-persistence.test.ts +71 -0
  566. package/test-browser/prompt-scope.test.ts +14 -0
  567. package/test-browser/prompt-submission-state.test.ts +73 -0
  568. package/test-browser/prompt-transient-state.test.ts +38 -0
  569. package/test-browser/review-panel-v2-state.test.ts +65 -0
  570. package/test-browser/session-lineage.test.ts +231 -0
  571. package/test-browser/session-ownership.test.ts +45 -0
  572. package/test-browser/solid-virtual.test.ts +112 -0
  573. package/test-browser/toast-owner.test.ts +26 -0
  574. package/tsconfig.json +26 -0
  575. package/vendor/opencode-ai-client-1.17.13.tgz +0 -0
  576. package/vite.config.ts +33 -0
  577. package/vite.js +48 -0
@@ -0,0 +1,360 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { createPathHelpers, stripQueryAndHash, unquoteGitPath, encodeFilePath } from "./path"
3
+
4
+ describe("file path helpers", () => {
5
+ test("normalizes file inputs against workspace root", () => {
6
+ const path = createPathHelpers(() => "/repo")
7
+ expect(path.normalize("file:///repo/src/app.ts?x=1#h")).toBe("src/app.ts")
8
+ expect(path.normalize("/repo/src/app.ts")).toBe("src/app.ts")
9
+ expect(path.normalize("./src/app.ts")).toBe("src/app.ts")
10
+ expect(path.normalizeDir("src/components///")).toBe("src/components")
11
+ expect(path.tab("src/app.ts")).toBe("file://src/app.ts")
12
+ expect(path.pathFromTab("file://src/app.ts")).toBe("src/app.ts")
13
+ expect(path.pathFromTab("other://src/app.ts")).toBeUndefined()
14
+ })
15
+
16
+ test("normalizes Windows absolute paths with mixed separators", () => {
17
+ const path = createPathHelpers(() => "C:\\repo")
18
+ expect(path.normalize("C:\\repo\\src\\app.ts")).toBe("src\\app.ts")
19
+ expect(path.normalize("C:/repo/src/app.ts")).toBe("src/app.ts")
20
+ expect(path.normalize("file://C:/repo/src/app.ts")).toBe("src/app.ts")
21
+ expect(path.normalize("c:\\repo\\src\\app.ts")).toBe("src\\app.ts")
22
+ })
23
+
24
+ test("keeps query/hash stripping behavior stable", () => {
25
+ expect(stripQueryAndHash("a/b.ts#L12?x=1")).toBe("a/b.ts")
26
+ expect(stripQueryAndHash("a/b.ts?x=1#L12")).toBe("a/b.ts")
27
+ expect(stripQueryAndHash("a/b.ts")).toBe("a/b.ts")
28
+ })
29
+
30
+ test("unquotes git escaped octal path strings", () => {
31
+ expect(unquoteGitPath('"a/\\303\\251.txt"')).toBe("a/\u00e9.txt")
32
+ expect(unquoteGitPath('"plain\\nname"')).toBe("plain\nname")
33
+ expect(unquoteGitPath("a/b/c.ts")).toBe("a/b/c.ts")
34
+ })
35
+ })
36
+
37
+ describe("encodeFilePath", () => {
38
+ describe("Linux/Unix paths", () => {
39
+ test("should handle Linux absolute path", () => {
40
+ const linuxPath = "/home/user/project/README.md"
41
+ const result = encodeFilePath(linuxPath)
42
+ const fileUrl = `file://${result}`
43
+
44
+ // Should create a valid URL
45
+ expect(() => new URL(fileUrl)).not.toThrow()
46
+ expect(result).toBe("/home/user/project/README.md")
47
+
48
+ const url = new URL(fileUrl)
49
+ expect(url.protocol).toBe("file:")
50
+ expect(url.pathname).toBe("/home/user/project/README.md")
51
+ })
52
+
53
+ test("should handle Linux path with special characters", () => {
54
+ const linuxPath = "/home/user/file#name with spaces.txt"
55
+ const result = encodeFilePath(linuxPath)
56
+ const fileUrl = `file://${result}`
57
+
58
+ expect(() => new URL(fileUrl)).not.toThrow()
59
+ expect(result).toBe("/home/user/file%23name%20with%20spaces.txt")
60
+ })
61
+
62
+ test("should handle Linux relative path", () => {
63
+ const relativePath = "src/components/App.tsx"
64
+ const result = encodeFilePath(relativePath)
65
+
66
+ expect(result).toBe("src/components/App.tsx")
67
+ })
68
+
69
+ test("should handle Linux root directory", () => {
70
+ const result = encodeFilePath("/")
71
+ expect(result).toBe("/")
72
+ })
73
+
74
+ test("should handle Linux path with all special chars", () => {
75
+ const path = "/path/to/file#with?special%chars&more.txt"
76
+ const result = encodeFilePath(path)
77
+ const fileUrl = `file://${result}`
78
+
79
+ expect(() => new URL(fileUrl)).not.toThrow()
80
+ expect(result).toContain("%23") // #
81
+ expect(result).toContain("%3F") // ?
82
+ expect(result).toContain("%25") // %
83
+ expect(result).toContain("%26") // &
84
+ })
85
+ })
86
+
87
+ describe("macOS paths", () => {
88
+ test("should handle macOS absolute path", () => {
89
+ const macPath = "/Users/kelvin/Projects/opencode/README.md"
90
+ const result = encodeFilePath(macPath)
91
+ const fileUrl = `file://${result}`
92
+
93
+ expect(() => new URL(fileUrl)).not.toThrow()
94
+ expect(result).toBe("/Users/kelvin/Projects/opencode/README.md")
95
+ })
96
+
97
+ test("should handle macOS path with spaces", () => {
98
+ const macPath = "/Users/kelvin/My Documents/file.txt"
99
+ const result = encodeFilePath(macPath)
100
+ const fileUrl = `file://${result}`
101
+
102
+ expect(() => new URL(fileUrl)).not.toThrow()
103
+ expect(result).toContain("My%20Documents")
104
+ })
105
+ })
106
+
107
+ describe("Windows paths", () => {
108
+ test("should handle Windows absolute path with backslashes", () => {
109
+ const windowsPath = "D:\\dev\\projects\\neurocode\\README.bs.md"
110
+ const result = encodeFilePath(windowsPath)
111
+ const fileUrl = `file://${result}`
112
+
113
+ // Should create a valid, parseable URL
114
+ expect(() => new URL(fileUrl)).not.toThrow()
115
+
116
+ const url = new URL(fileUrl)
117
+ expect(url.protocol).toBe("file:")
118
+ expect(url.pathname).toContain("README.bs.md")
119
+ expect(result).toBe("/D:/dev/projects/opencode/README.bs.md")
120
+ })
121
+
122
+ test("should handle mixed separator path (Windows + Unix)", () => {
123
+ // This is what happens in build-request-parts.ts when concatenating paths
124
+ const mixedPath = "D:\\dev\\projects\\opencode/README.bs.md"
125
+ const result = encodeFilePath(mixedPath)
126
+ const fileUrl = `file://${result}`
127
+
128
+ expect(() => new URL(fileUrl)).not.toThrow()
129
+ expect(result).toBe("/D:/dev/projects/opencode/README.bs.md")
130
+ })
131
+
132
+ test("should handle Windows path with spaces", () => {
133
+ const windowsPath = "C:\\Program Files\\MyApp\\file with spaces.txt"
134
+ const result = encodeFilePath(windowsPath)
135
+ const fileUrl = `file://${result}`
136
+
137
+ expect(() => new URL(fileUrl)).not.toThrow()
138
+ expect(result).toContain("Program%20Files")
139
+ expect(result).toContain("file%20with%20spaces.txt")
140
+ })
141
+
142
+ test("should handle Windows path with special chars in filename", () => {
143
+ const windowsPath = "D:\\projects\\file#name with ?marks.txt"
144
+ const result = encodeFilePath(windowsPath)
145
+ const fileUrl = `file://${result}`
146
+
147
+ expect(() => new URL(fileUrl)).not.toThrow()
148
+ expect(result).toContain("file%23name%20with%20%3Fmarks.txt")
149
+ })
150
+
151
+ test("should handle Windows root directory", () => {
152
+ const windowsPath = "C:\\"
153
+ const result = encodeFilePath(windowsPath)
154
+ const fileUrl = `file://${result}`
155
+
156
+ expect(() => new URL(fileUrl)).not.toThrow()
157
+ expect(result).toBe("/C:/")
158
+ })
159
+
160
+ test("should handle Windows relative path with backslashes", () => {
161
+ const windowsPath = "src\\components\\App.tsx"
162
+ const result = encodeFilePath(windowsPath)
163
+
164
+ // Relative paths shouldn't get the leading slash
165
+ expect(result).toBe("src/components/App.tsx")
166
+ })
167
+
168
+ test("should NOT create invalid URL like the bug report", () => {
169
+ // This is the exact scenario from bug report by @alexyaroshuk
170
+ const windowsPath = "D:\\dev\\projects\\neurocode\\README.bs.md"
171
+ const result = encodeFilePath(windowsPath)
172
+ const fileUrl = `file://${result}`
173
+
174
+ // The bug was creating: file://D%3A%5Cdev%5Cprojects%5Copencode/README.bs.md
175
+ expect(result).not.toContain("%5C") // Should not have encoded backslashes
176
+ expect(result).not.toBe("D%3A%5Cdev%5Cprojects%5Copencode/README.bs.md")
177
+
178
+ // Should be valid
179
+ expect(() => new URL(fileUrl)).not.toThrow()
180
+ })
181
+
182
+ test("should handle lowercase drive letters", () => {
183
+ const windowsPath = "c:\\users\\test\\file.txt"
184
+ const result = encodeFilePath(windowsPath)
185
+ const fileUrl = `file://${result}`
186
+
187
+ expect(() => new URL(fileUrl)).not.toThrow()
188
+ expect(result).toBe("/c:/users/test/file.txt")
189
+ })
190
+ })
191
+
192
+ describe("Cross-platform compatibility", () => {
193
+ test("should preserve Unix paths unchanged (except encoding)", () => {
194
+ const unixPath = "/usr/local/bin/app"
195
+ const result = encodeFilePath(unixPath)
196
+ expect(result).toBe("/usr/local/bin/app")
197
+ })
198
+
199
+ test("should normalize Windows paths for cross-platform use", () => {
200
+ const windowsPath = "C:\\Users\\test\\file.txt"
201
+ const result = encodeFilePath(windowsPath)
202
+ // Should convert to forward slashes and add leading /
203
+ expect(result).not.toContain("\\")
204
+ expect(result).toMatch(/^\/[A-Za-z]:\//)
205
+ })
206
+
207
+ test("should handle relative paths the same on all platforms", () => {
208
+ const unixRelative = "src/app.ts"
209
+ const windowsRelative = "src\\app.ts"
210
+
211
+ const unixResult = encodeFilePath(unixRelative)
212
+ const windowsResult = encodeFilePath(windowsRelative)
213
+
214
+ // Both should normalize to forward slashes
215
+ expect(unixResult).toBe("src/app.ts")
216
+ expect(windowsResult).toBe("src/app.ts")
217
+ })
218
+ })
219
+
220
+ describe("Edge cases", () => {
221
+ test("should handle empty path", () => {
222
+ const result = encodeFilePath("")
223
+ expect(result).toBe("")
224
+ })
225
+
226
+ test("should handle path with multiple consecutive slashes", () => {
227
+ const result = encodeFilePath("//path//to///file.txt")
228
+ // Multiple slashes should be preserved (backend handles normalization)
229
+ expect(result).toBe("//path//to///file.txt")
230
+ })
231
+
232
+ test("should encode Unicode characters", () => {
233
+ const unicodePath = "/home/user/文档/README.md"
234
+ const result = encodeFilePath(unicodePath)
235
+ const fileUrl = `file://${result}`
236
+
237
+ expect(() => new URL(fileUrl)).not.toThrow()
238
+ // Unicode should be encoded
239
+ expect(result).toContain("%E6%96%87%E6%A1%A3")
240
+ })
241
+
242
+ test("should handle already normalized Windows path", () => {
243
+ // Path that's already been normalized (has / before drive letter)
244
+ const alreadyNormalized = "/D:/path/file.txt"
245
+ const result = encodeFilePath(alreadyNormalized)
246
+
247
+ // Should not add another leading slash
248
+ expect(result).toBe("/D:/path/file.txt")
249
+ expect(result).not.toContain("//D")
250
+ })
251
+
252
+ test("should handle just drive letter", () => {
253
+ const justDrive = "D:"
254
+ const result = encodeFilePath(justDrive)
255
+ const fileUrl = `file://${result}`
256
+
257
+ expect(result).toBe("/D:")
258
+ expect(() => new URL(fileUrl)).not.toThrow()
259
+ })
260
+
261
+ test("should handle Windows path with trailing backslash", () => {
262
+ const trailingBackslash = "C:\\Users\\test\\"
263
+ const result = encodeFilePath(trailingBackslash)
264
+ const fileUrl = `file://${result}`
265
+
266
+ expect(() => new URL(fileUrl)).not.toThrow()
267
+ expect(result).toBe("/C:/Users/test/")
268
+ })
269
+
270
+ test("should handle very long paths", () => {
271
+ const longPath = "C:\\Users\\test\\" + "verylongdirectoryname\\".repeat(20) + "file.txt"
272
+ const result = encodeFilePath(longPath)
273
+ const fileUrl = `file://${result}`
274
+
275
+ expect(() => new URL(fileUrl)).not.toThrow()
276
+ expect(result).not.toContain("\\")
277
+ })
278
+
279
+ test("should handle paths with dots", () => {
280
+ const pathWithDots = "C:\\Users\\..\\test\\.\\file.txt"
281
+ const result = encodeFilePath(pathWithDots)
282
+ const fileUrl = `file://${result}`
283
+
284
+ expect(() => new URL(fileUrl)).not.toThrow()
285
+ // Dots should be preserved (backend normalizes)
286
+ expect(result).toContain("..")
287
+ expect(result).toContain("/./")
288
+ })
289
+ })
290
+
291
+ describe("Regression tests for PR #12424", () => {
292
+ test("should handle file with # in name", () => {
293
+ const path = "/path/to/file#name.txt"
294
+ const result = encodeFilePath(path)
295
+ const fileUrl = `file://${result}`
296
+
297
+ expect(() => new URL(fileUrl)).not.toThrow()
298
+ expect(result).toBe("/path/to/file%23name.txt")
299
+ })
300
+
301
+ test("should handle file with ? in name", () => {
302
+ const path = "/path/to/file?name.txt"
303
+ const result = encodeFilePath(path)
304
+ const fileUrl = `file://${result}`
305
+
306
+ expect(() => new URL(fileUrl)).not.toThrow()
307
+ expect(result).toBe("/path/to/file%3Fname.txt")
308
+ })
309
+
310
+ test("should handle file with % in name", () => {
311
+ const path = "/path/to/file%name.txt"
312
+ const result = encodeFilePath(path)
313
+ const fileUrl = `file://${result}`
314
+
315
+ expect(() => new URL(fileUrl)).not.toThrow()
316
+ expect(result).toBe("/path/to/file%25name.txt")
317
+ })
318
+ })
319
+
320
+ describe("Integration with file:// URL construction", () => {
321
+ test("should work with query parameters (Linux)", () => {
322
+ const path = "/home/user/file.txt"
323
+ const encoded = encodeFilePath(path)
324
+ const fileUrl = `file://${encoded}?start=10&end=20`
325
+
326
+ const url = new URL(fileUrl)
327
+ expect(url.searchParams.get("start")).toBe("10")
328
+ expect(url.searchParams.get("end")).toBe("20")
329
+ expect(url.pathname).toBe("/home/user/file.txt")
330
+ })
331
+
332
+ test("should work with query parameters (Windows)", () => {
333
+ const path = "C:\\Users\\test\\file.txt"
334
+ const encoded = encodeFilePath(path)
335
+ const fileUrl = `file://${encoded}?start=10&end=20`
336
+
337
+ const url = new URL(fileUrl)
338
+ expect(url.searchParams.get("start")).toBe("10")
339
+ expect(url.searchParams.get("end")).toBe("20")
340
+ })
341
+
342
+ test("should parse correctly in URL constructor (Linux)", () => {
343
+ const path = "/var/log/app.log"
344
+ const fileUrl = `file://${encodeFilePath(path)}`
345
+ const url = new URL(fileUrl)
346
+
347
+ expect(url.protocol).toBe("file:")
348
+ expect(url.pathname).toBe("/var/log/app.log")
349
+ })
350
+
351
+ test("should parse correctly in URL constructor (Windows)", () => {
352
+ const path = "D:\\logs\\app.log"
353
+ const fileUrl = `file://${encodeFilePath(path)}`
354
+ const url = new URL(fileUrl)
355
+
356
+ expect(url.protocol).toBe("file:")
357
+ expect(url.pathname).toContain("app.log")
358
+ })
359
+ })
360
+ })
@@ -0,0 +1,151 @@
1
+ export function stripFileProtocol(input: string) {
2
+ if (!input.startsWith("file://")) return input
3
+ return input.slice("file://".length)
4
+ }
5
+
6
+ export function stripQueryAndHash(input: string) {
7
+ const hashIndex = input.indexOf("#")
8
+ const queryIndex = input.indexOf("?")
9
+
10
+ if (hashIndex !== -1 && queryIndex !== -1) {
11
+ return input.slice(0, Math.min(hashIndex, queryIndex))
12
+ }
13
+
14
+ if (hashIndex !== -1) return input.slice(0, hashIndex)
15
+ if (queryIndex !== -1) return input.slice(0, queryIndex)
16
+ return input
17
+ }
18
+
19
+ export function unquoteGitPath(input: string) {
20
+ if (!input.startsWith('"')) return input
21
+ if (!input.endsWith('"')) return input
22
+ const body = input.slice(1, -1)
23
+ const bytes: number[] = []
24
+
25
+ for (let i = 0; i < body.length; i++) {
26
+ const char = body[i]!
27
+ if (char !== "\\") {
28
+ bytes.push(char.charCodeAt(0))
29
+ continue
30
+ }
31
+
32
+ const next = body[i + 1]
33
+ if (!next) {
34
+ bytes.push("\\".charCodeAt(0))
35
+ continue
36
+ }
37
+
38
+ if (next >= "0" && next <= "7") {
39
+ const chunk = body.slice(i + 1, i + 4)
40
+ const match = chunk.match(/^[0-7]{1,3}/)
41
+ if (!match) {
42
+ bytes.push(next.charCodeAt(0))
43
+ i++
44
+ continue
45
+ }
46
+ bytes.push(parseInt(match[0], 8))
47
+ i += match[0].length
48
+ continue
49
+ }
50
+
51
+ const escaped =
52
+ next === "n"
53
+ ? "\n"
54
+ : next === "r"
55
+ ? "\r"
56
+ : next === "t"
57
+ ? "\t"
58
+ : next === "b"
59
+ ? "\b"
60
+ : next === "f"
61
+ ? "\f"
62
+ : next === "v"
63
+ ? "\v"
64
+ : next === "\\" || next === '"'
65
+ ? next
66
+ : undefined
67
+
68
+ bytes.push((escaped ?? next).charCodeAt(0))
69
+ i++
70
+ }
71
+
72
+ return new TextDecoder().decode(new Uint8Array(bytes))
73
+ }
74
+
75
+ export function decodeFilePath(input: string) {
76
+ try {
77
+ return decodeURIComponent(input)
78
+ } catch {
79
+ return input
80
+ }
81
+ }
82
+
83
+ export function encodeFilePath(filepath: string): string {
84
+ // Normalize Windows paths: convert backslashes to forward slashes
85
+ let normalized = filepath.replace(/\\/g, "/")
86
+
87
+ // Handle Windows absolute paths (D:/path -> /D:/path for proper file:// URLs)
88
+ if (/^[A-Za-z]:/.test(normalized)) {
89
+ normalized = "/" + normalized
90
+ }
91
+
92
+ // Encode each path segment (preserving forward slashes as path separators)
93
+ // Keep the colon in Windows drive letters (`/C:/...`) so downstream file URL parsers
94
+ // can reliably detect drives.
95
+ return normalized
96
+ .split("/")
97
+ .map((segment, index) => {
98
+ if (index === 1 && /^[A-Za-z]:$/.test(segment)) return segment
99
+ return encodeURIComponent(segment)
100
+ })
101
+ .join("/")
102
+ }
103
+
104
+ export function createPathHelpers(scope: () => string) {
105
+ const normalize = (input: string) => {
106
+ const root = scope()
107
+
108
+ let path = unquoteGitPath(decodeFilePath(stripQueryAndHash(stripFileProtocol(input))))
109
+
110
+ // Separator-agnostic prefix stripping for Cygwin/native Windows compatibility
111
+ // Only case-insensitive on Windows (drive letter or UNC paths)
112
+ const windows = /^[A-Za-z]:/.test(root) || root.startsWith("\\\\")
113
+ const canonRoot = windows ? root.replace(/\\/g, "/").toLowerCase() : root.replace(/\\/g, "/")
114
+ const canonPath = windows ? path.replace(/\\/g, "/").toLowerCase() : path.replace(/\\/g, "/")
115
+ if (
116
+ canonPath.startsWith(canonRoot) &&
117
+ (canonRoot.endsWith("/") || canonPath === canonRoot || canonPath[canonRoot.length] === "/")
118
+ ) {
119
+ // Slice from original path to preserve native separators
120
+ path = path.slice(root.length)
121
+ }
122
+
123
+ if (path.startsWith("./") || path.startsWith(".\\")) {
124
+ path = path.slice(2)
125
+ }
126
+
127
+ if (path.startsWith("/") || path.startsWith("\\")) {
128
+ path = path.slice(1)
129
+ }
130
+ return path
131
+ }
132
+
133
+ const tab = (input: string) => {
134
+ const path = normalize(input)
135
+ return `file://${encodeFilePath(path)}`
136
+ }
137
+
138
+ const pathFromTab = (tabValue: string) => {
139
+ if (!tabValue.startsWith("file://")) return
140
+ return normalize(tabValue)
141
+ }
142
+
143
+ const normalizeDir = (input: string) => normalize(input).replace(/\/+$/, "")
144
+
145
+ return {
146
+ normalize,
147
+ tab,
148
+ pathFromTab,
149
+ normalizeDir,
150
+ }
151
+ }
@@ -0,0 +1,174 @@
1
+ import { createStore, produce, reconcile } from "solid-js/store"
2
+ import type { FileNode } from "@neurocode-ai/sdk/v2"
3
+
4
+ type DirectoryState = {
5
+ expanded: boolean
6
+ loaded?: boolean
7
+ loading?: boolean
8
+ error?: string
9
+ children?: string[]
10
+ }
11
+
12
+ type TreeStoreOptions = {
13
+ scope: () => string
14
+ normalizeDir: (input: string) => string
15
+ list: (input: string) => Promise<FileNode[]>
16
+ onError: (message: string) => void
17
+ }
18
+
19
+ export function createFileTreeStore(options: TreeStoreOptions) {
20
+ const [tree, setTree] = createStore<{
21
+ node: Record<string, FileNode>
22
+ dir: Record<string, DirectoryState>
23
+ }>({
24
+ node: {},
25
+ dir: { "": { expanded: true } },
26
+ })
27
+
28
+ const inflight = new Map<string, Promise<void>>()
29
+
30
+ const reset = () => {
31
+ inflight.clear()
32
+ setTree("node", reconcile({}))
33
+ setTree("dir", reconcile({}))
34
+ setTree("dir", "", { expanded: true })
35
+ }
36
+
37
+ const ensureDir = (path: string) => {
38
+ if (tree.dir[path]) return
39
+ setTree("dir", path, { expanded: false })
40
+ }
41
+
42
+ const listDir = (input: string, opts?: { force?: boolean }) => {
43
+ const dir = options.normalizeDir(input)
44
+ ensureDir(dir)
45
+
46
+ const current = tree.dir[dir]
47
+ if (!opts?.force && current?.loaded) return Promise.resolve()
48
+
49
+ const pending = inflight.get(dir)
50
+ if (pending) return pending
51
+
52
+ setTree(
53
+ "dir",
54
+ dir,
55
+ produce((draft) => {
56
+ draft.loading = true
57
+ draft.error = undefined
58
+ }),
59
+ )
60
+
61
+ const directory = options.scope()
62
+
63
+ const promise = options
64
+ .list(dir)
65
+ .then((nodes) => {
66
+ if (options.scope() !== directory) return
67
+ const prevChildren = tree.dir[dir]?.children ?? []
68
+ const nextChildren = nodes.map((node) => node.path)
69
+ const nextSet = new Set(nextChildren)
70
+
71
+ setTree(
72
+ "node",
73
+ produce((draft) => {
74
+ const removedDirs: string[] = []
75
+
76
+ for (const child of prevChildren) {
77
+ if (nextSet.has(child)) continue
78
+ const existing = draft[child]
79
+ if (existing?.type === "directory") removedDirs.push(child)
80
+ delete draft[child]
81
+ }
82
+
83
+ if (removedDirs.length > 0) {
84
+ const keys = Object.keys(draft)
85
+ for (const key of keys) {
86
+ for (const removed of removedDirs) {
87
+ if (!key.startsWith(removed + "/")) continue
88
+ delete draft[key]
89
+ break
90
+ }
91
+ }
92
+ }
93
+
94
+ for (const node of nodes) {
95
+ draft[node.path] = node
96
+ }
97
+ }),
98
+ )
99
+
100
+ setTree(
101
+ "dir",
102
+ dir,
103
+ produce((draft) => {
104
+ draft.loaded = true
105
+ draft.loading = false
106
+ draft.children = nextChildren
107
+ }),
108
+ )
109
+ })
110
+ .catch((e) => {
111
+ if (options.scope() !== directory) return
112
+ setTree(
113
+ "dir",
114
+ dir,
115
+ produce((draft) => {
116
+ draft.loading = false
117
+ draft.error = e.message
118
+ }),
119
+ )
120
+ options.onError(e.message)
121
+ })
122
+ .finally(() => {
123
+ inflight.delete(dir)
124
+ })
125
+
126
+ inflight.set(dir, promise)
127
+ return promise
128
+ }
129
+
130
+ // `list: false` marks a directory expanded without fetching its children, for
131
+ // trees whose nodes are synthesized from a filter; listing directories that
132
+ // only exist on a diff's base branch fails and surfaces error toasts.
133
+ const expandDir = (input: string, behavior?: { list?: boolean }) => {
134
+ const dir = options.normalizeDir(input)
135
+ ensureDir(dir)
136
+ setTree("dir", dir, "expanded", true)
137
+ if (behavior?.list === false) return
138
+ void listDir(dir)
139
+ }
140
+
141
+ const collapseDir = (input: string) => {
142
+ const dir = options.normalizeDir(input)
143
+ ensureDir(dir)
144
+ setTree("dir", dir, "expanded", false)
145
+ }
146
+
147
+ const dirState = (input: string) => {
148
+ const dir = options.normalizeDir(input)
149
+ return tree.dir[dir]
150
+ }
151
+
152
+ const children = (input: string) => {
153
+ const dir = options.normalizeDir(input)
154
+ const ids = tree.dir[dir]?.children
155
+ if (!ids) return []
156
+ const out: FileNode[] = []
157
+ for (const id of ids) {
158
+ const node = tree.node[id]
159
+ if (node) out.push(node)
160
+ }
161
+ return out
162
+ }
163
+
164
+ return {
165
+ listDir,
166
+ expandDir,
167
+ collapseDir,
168
+ dirState,
169
+ children,
170
+ node: (path: string) => tree.node[path],
171
+ isLoaded: (path: string) => Boolean(tree.dir[path]?.loaded),
172
+ reset,
173
+ }
174
+ }