@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,1178 @@
1
+ import type { IntegrationMethod, IntegrationOauthConnectOutput } from "@neurocode-ai/client/promise"
2
+ import { Button } from "@neurocode-ai/ui/button"
3
+ import { useDialog } from "@neurocode-ai/ui/context/dialog"
4
+ import { Dialog } from "@neurocode-ai/ui/dialog"
5
+ import { Icon } from "@neurocode-ai/ui/icon"
6
+ import { IconButton } from "@neurocode-ai/ui/icon-button"
7
+ import { List, type ListRef } from "@neurocode-ai/ui/list"
8
+ import { ProviderIcon } from "@neurocode-ai/ui/provider-icon"
9
+ import { Spinner } from "@neurocode-ai/ui/spinner"
10
+ import { Tag } from "@neurocode-ai/ui/tag"
11
+ import { TextField } from "@neurocode-ai/ui/text-field"
12
+ import { ButtonV2 } from "@neurocode-ai/ui/v2/button-v2"
13
+ import { DialogBody, DialogHeader, DialogTitle, DialogV2 } from "@neurocode-ai/ui/v2/dialog-v2"
14
+ import { TextInputV2 } from "@neurocode-ai/ui/v2/text-input-v2"
15
+ import { showToast } from "@/utils/toast"
16
+ import {
17
+ type Accessor,
18
+ type Component,
19
+ createEffect,
20
+ createMemo,
21
+ createResource,
22
+ createUniqueId,
23
+ For,
24
+ Match,
25
+ onCleanup,
26
+ onMount,
27
+ Show,
28
+ Switch,
29
+ } from "solid-js"
30
+ import { createStore, produce } from "solid-js/store"
31
+ import { useQueryClient } from "@tanstack/solid-query"
32
+ import { useParams } from "@solidjs/router"
33
+ import { Link } from "@/components/link"
34
+ import { useServerSDK } from "@/context/server-sdk"
35
+ import { useServerSync } from "@/context/server-sync"
36
+ import { useLanguage } from "@/context/language"
37
+ import { useSettings } from "@/context/settings"
38
+ import { popularProviders, useProviders } from "@/hooks/use-providers"
39
+ import { CustomProviderForm } from "./dialog-custom-provider"
40
+ import { decode64 } from "@/utils/base64"
41
+ import { pathKey } from "@/utils/path-key"
42
+
43
+ const CUSTOM_ID = "_custom"
44
+ type ConnectMethod = Extract<IntegrationMethod, { type: "key" | "oauth" }>
45
+
46
+ export function useProviderConnectController(options: { onBack?: () => void } = {}) {
47
+ const [store, setStore] = createStore({ selected: undefined as string | undefined })
48
+ const reset = () => setStore("selected", undefined)
49
+
50
+ return {
51
+ selected: () => store.selected,
52
+ select: (provider?: string) => setStore("selected", provider),
53
+ back: options.onBack ?? reset,
54
+ }
55
+ }
56
+
57
+ export const DialogConnectProvider: Component<{
58
+ directory?: Accessor<string | undefined>
59
+ controller?: ReturnType<typeof useProviderConnectController>
60
+ }> = (props) => {
61
+ const fallback = useProviderConnectController()
62
+ const controller = props.controller ?? fallback
63
+ const language = useLanguage()
64
+ const settings = useSettings()
65
+ const newLayout = settings.general.newLayoutDesigns
66
+ const reset = controller.back
67
+ const back = { current: reset }
68
+ let focusHost: HTMLDivElement | undefined
69
+ const holdFocus = () => focusHost?.focus({ preventScroll: true })
70
+ const select = (provider?: string) => {
71
+ back.current = reset
72
+ controller.select(provider)
73
+ }
74
+
75
+ function Content() {
76
+ return (
77
+ <Switch>
78
+ <Match when={controller.selected() === CUSTOM_ID}>
79
+ <CustomProviderForm autofocus={!newLayout()} />
80
+ </Match>
81
+ <Match when={controller.selected() && controller.selected() !== CUSTOM_ID ? controller.selected() : undefined}>
82
+ {(provider) => (
83
+ <ProviderConnection
84
+ provider={provider()}
85
+ directory={props.directory}
86
+ onBack={reset}
87
+ setBack={(handler) => (back.current = handler)}
88
+ />
89
+ )}
90
+ </Match>
91
+ <Match when={true}>
92
+ <ProviderPicker
93
+ directory={props.directory}
94
+ onSelect={select}
95
+ onPrepare={newLayout() ? holdFocus : undefined}
96
+ />
97
+ </Match>
98
+ </Switch>
99
+ )
100
+ }
101
+
102
+ return (
103
+ <Show
104
+ when={newLayout()}
105
+ fallback={
106
+ <Dialog
107
+ class="h-full"
108
+ transition
109
+ title={
110
+ <Show when={controller.selected()} fallback={language.t("command.provider.connect")}>
111
+ <IconButton
112
+ tabIndex={-1}
113
+ icon="arrow-left"
114
+ variant="ghost"
115
+ onClick={() => back.current()}
116
+ aria-label={language.t("common.goBack")}
117
+ />
118
+ </Show>
119
+ }
120
+ >
121
+ <Content />
122
+ </Dialog>
123
+ }
124
+ >
125
+ <DialogV2
126
+ containerClass="!h-[min(calc(100vh_-_16px),512px)] !w-[min(calc(100vw_-_16px),640px)]"
127
+ class="[font-family:var(--v2-font-family-sans)] [&_[data-slot=dialog-header]]:!px-5 [&_[data-slot=dialog-header-title]]:!text-[15px] [&_[data-slot=dialog-header-title]]:!tracking-[-0.13px]"
128
+ >
129
+ <DialogHeader closeLabel={language.t("common.close")}>
130
+ <Show
131
+ when={controller.selected()}
132
+ fallback={<DialogTitle>{language.t("command.provider.connect")}</DialogTitle>}
133
+ >
134
+ <button
135
+ type="button"
136
+ class="flex size-5 items-center justify-center rounded-sm text-v2-icon-icon-muted hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none"
137
+ onClick={() => back.current()}
138
+ aria-label={language.t("common.goBack")}
139
+ >
140
+ <Icon name="arrow-left" size="small" />
141
+ </button>
142
+ </Show>
143
+ </DialogHeader>
144
+ <DialogBody class="min-h-0 flex-1 overflow-hidden px-2 pb-2">
145
+ <div ref={focusHost} tabIndex={-1} class="flex min-h-0 flex-1 flex-col outline-none">
146
+ <Content />
147
+ </div>
148
+ </DialogBody>
149
+ </DialogV2>
150
+ </Show>
151
+ )
152
+ }
153
+
154
+ function ProviderPicker(props: {
155
+ directory?: Accessor<string | undefined>
156
+ onSelect: (provider: string) => void
157
+ onPrepare?: () => void
158
+ }) {
159
+ const settings = useSettings()
160
+ if (settings.general.newLayoutDesigns())
161
+ return <ProviderPickerV2 directory={props.directory} onSelect={props.onSelect} onPrepare={props.onPrepare} />
162
+ const providers = useProviders(props.directory)
163
+ const language = useLanguage()
164
+ const popularGroup = () => language.t("dialog.provider.group.popular")
165
+ const otherGroup = () => language.t("dialog.provider.group.other")
166
+ const customLabel = () => language.t("settings.providers.tag.custom")
167
+ const note = (id: string) => {
168
+ if (id === "anthropic") return language.t("dialog.provider.anthropic.note")
169
+ if (id === "openai") return language.t("dialog.provider.openai.note")
170
+ if (id.startsWith("github-copilot")) return language.t("dialog.provider.copilot.note")
171
+ if (id === "neurocode-go") return language.t("dialog.provider.opencodeGo.tagline")
172
+ return undefined
173
+ }
174
+
175
+ return (
176
+ <List
177
+ class="px-3"
178
+ search={{ placeholder: language.t("dialog.provider.search.placeholder"), autofocus: true }}
179
+ emptyMessage={language.t("dialog.provider.empty")}
180
+ activeIcon="plus-small"
181
+ key={(x) => x?.id}
182
+ items={() => {
183
+ language.locale()
184
+ return [{ id: CUSTOM_ID, name: customLabel() }, ...providers.all().values()]
185
+ }}
186
+ filterKeys={["id", "name"]}
187
+ groupBy={(x) => (popularProviders.includes(x.id) ? popularGroup() : otherGroup())}
188
+ sortBy={(a, b) => {
189
+ if (a.id === CUSTOM_ID) return -1
190
+ if (b.id === CUSTOM_ID) return 1
191
+ if (popularProviders.includes(a.id) && popularProviders.includes(b.id))
192
+ return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)
193
+ return a.name.localeCompare(b.name)
194
+ }}
195
+ sortGroupsBy={(a, b) => {
196
+ const popular = popularGroup()
197
+ if (a.category === popular && b.category !== popular) return -1
198
+ if (b.category === popular && a.category !== popular) return 1
199
+ return 0
200
+ }}
201
+ onSelect={(x) => {
202
+ if (!x) return
203
+ props.onSelect(x.id)
204
+ }}
205
+ >
206
+ {(i) => (
207
+ <div class="px-1.25 w-full flex items-center gap-x-3">
208
+ <ProviderIcon data-slot="list-item-extra-icon" id={i.id} />
209
+ <span>{i.name}</span>
210
+ <Show when={i.id === "neurocode"}>
211
+ <div class="text-14-regular text-text-weak">{language.t("dialog.provider.neurocode.tagline")}</div>
212
+ </Show>
213
+ <Show when={i.id === CUSTOM_ID}>
214
+ <Tag>{language.t("settings.providers.tag.custom")}</Tag>
215
+ </Show>
216
+ <Show when={i.id === "neurocode"}>
217
+ <Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
218
+ </Show>
219
+ <Show when={note(i.id)}>{(value) => <div class="text-14-regular text-text-weak">{value()}</div>}</Show>
220
+ <Show when={i.id === "neurocode-go"}>
221
+ <Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
222
+ </Show>
223
+ </div>
224
+ )}
225
+ </List>
226
+ )
227
+ }
228
+
229
+ function ProviderPickerV2(props: {
230
+ directory?: Accessor<string | undefined>
231
+ onSelect: (provider: string) => void
232
+ onPrepare?: () => void
233
+ }) {
234
+ const providers = useProviders(props.directory)
235
+ const language = useLanguage()
236
+ const [store, setStore] = createStore({
237
+ filter: "",
238
+ active: undefined as string | undefined,
239
+ connecting: undefined as string | undefined,
240
+ })
241
+ const featured = ["neurocode", "neurocode-go", "anthropic", "openai", "google", "openrouter", "vercel"]
242
+ const custom = () => ({ id: CUSTOM_ID, name: language.t("dialog.provider.custom.label") })
243
+ const all = createMemo(() => {
244
+ language.locale()
245
+ const query = store.filter.trim().toLowerCase()
246
+ const values = [custom(), ...providers.all().values()]
247
+ if (!query) return values
248
+ return values.filter((provider) => `${provider.id} ${provider.name}`.toLowerCase().includes(query))
249
+ })
250
+ const popular = createMemo(() =>
251
+ all()
252
+ .filter((provider) => featured.includes(provider.id))
253
+ .sort((a, b) => featured.indexOf(a.id) - featured.indexOf(b.id)),
254
+ )
255
+ const other = createMemo(() =>
256
+ all()
257
+ .filter((provider) => !featured.includes(provider.id))
258
+ .sort((a, b) => {
259
+ if (a.id === CUSTOM_ID) return -1
260
+ if (b.id === CUSTOM_ID) return 1
261
+ return a.name.localeCompare(b.name)
262
+ }),
263
+ )
264
+ const rows = createMemo(() => [...popular(), ...other()])
265
+ let picker: HTMLDivElement | undefined
266
+ let search: HTMLInputElement | undefined
267
+
268
+ onMount(() => search?.focus({ preventScroll: true }))
269
+
270
+ const connect = (provider: string) => {
271
+ props.onPrepare?.()
272
+ props.onSelect(provider)
273
+ }
274
+
275
+ const move = (event: KeyboardEvent, direction: number) => {
276
+ const items = rows()
277
+ if (items.length === 0) return
278
+ const index = items.findIndex((provider) => provider.id === store.active)
279
+ const next = index < 0 ? (direction > 0 ? 0 : items.length - 1) : (index + direction + items.length) % items.length
280
+ setStore("active", items[next].id)
281
+ picker
282
+ ?.querySelector<HTMLElement>(`[data-provider-id="${CSS.escape(items[next].id)}"]`)
283
+ ?.focus({ preventScroll: true })
284
+ event.preventDefault()
285
+ }
286
+
287
+ const handleKeyDown = (event: KeyboardEvent) => {
288
+ if (event.key === "ArrowDown") return move(event, 1)
289
+ if (event.key === "ArrowUp") return move(event, -1)
290
+ if (event.key !== "Enter" || !store.active) return
291
+ connect(store.active)
292
+ event.preventDefault()
293
+ }
294
+
295
+ return (
296
+ <div ref={picker} class="flex min-h-0 flex-1 flex-col gap-4" onKeyDown={handleKeyDown}>
297
+ <div class="shrink-0 px-1 pt-px">
298
+ <TextInputV2
299
+ ref={search}
300
+ type="search"
301
+ class="!w-full [font-family:var(--v2-font-family-sans)]"
302
+ leadingIcon={<Icon name="magnifying-glass" size="small" />}
303
+ placeholder={language.t("dialog.provider.search.placeholder")}
304
+ value={store.filter}
305
+ onInput={(event) => {
306
+ setStore({ filter: event.currentTarget.value, active: undefined })
307
+ }}
308
+ />
309
+ </div>
310
+ <div class="relative min-h-0 flex-1">
311
+ <div class="flex size-full min-h-0 flex-col gap-4 overflow-y-auto pb-8 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
312
+ <For
313
+ each={[
314
+ { title: language.t("dialog.provider.group.popular"), items: popular },
315
+ { title: language.t("dialog.provider.group.other"), items: other },
316
+ ]}
317
+ >
318
+ {(group) => (
319
+ <Show when={group.items().length > 0}>
320
+ <section class="flex flex-col">
321
+ <div class="px-3 pb-2 text-[13px] font-[440] leading-none tracking-[-0.04px] text-v2-text-text-muted">
322
+ {group.title}
323
+ </div>
324
+ <For each={group.items()}>
325
+ {(provider) => (
326
+ <button
327
+ type="button"
328
+ data-provider-id={provider.id}
329
+ class="flex min-h-9 w-full items-center gap-2 rounded-md px-3 py-2.5 text-left text-[13px] leading-none tracking-[-0.04px] hover:bg-v2-overlay-simple-overlay-hover focus:bg-v2-overlay-simple-overlay-hover focus:outline-none"
330
+ classList={{ "bg-v2-overlay-simple-overlay-hover": store.active === provider.id }}
331
+ onMouseEnter={() => setStore("active", provider.id)}
332
+ disabled={store.connecting !== undefined}
333
+ aria-busy={store.connecting === provider.id}
334
+ onClick={() => connect(provider.id)}
335
+ >
336
+ <ProviderIcon id={provider.id} class="size-4 shrink-0 text-v2-icon-icon-base" />
337
+ <span class="min-w-0 truncate font-[530] text-v2-text-text-base">{provider.name}</span>
338
+ <Show when={provider.id === "neurocode" || provider.id === "neurocode-go"}>
339
+ <span class="min-w-0 truncate font-[440] text-v2-text-text-muted">
340
+ {language.t(
341
+ provider.id === "neurocode"
342
+ ? "dialog.provider.neurocode.tagline"
343
+ : "dialog.provider.opencodeGo.tagline",
344
+ )}
345
+ </span>
346
+ <span class="flex h-4 shrink-0 items-center rounded-xs border-[0.5px] border-v2-border-border-base bg-v2-background-bg-layer-03 px-1 text-[11px] font-[530] leading-none tracking-[0.05px] text-v2-text-text-muted">
347
+ {language.t("dialog.provider.tag.recommended")}
348
+ </span>
349
+ </Show>
350
+ <Show when={provider.id === CUSTOM_ID}>
351
+ <span class="flex h-4 shrink-0 items-center rounded-xs border-[0.5px] border-v2-border-border-base bg-v2-background-bg-layer-03 px-1 text-[11px] font-[530] leading-none tracking-[0.05px] text-v2-text-text-muted">
352
+ {language.t("settings.providers.tag.custom")}
353
+ </span>
354
+ </Show>
355
+ <Show when={store.connecting === provider.id}>
356
+ <Spinner class="ml-auto size-4 shrink-0 text-v2-icon-icon-muted" />
357
+ </Show>
358
+ </button>
359
+ )}
360
+ </For>
361
+ </section>
362
+ </Show>
363
+ )}
364
+ </For>
365
+ <Show when={rows().length === 0}>
366
+ <div class="flex h-24 items-center justify-center text-[13px] font-[440] text-v2-text-text-muted">
367
+ {language.t("dialog.provider.empty")}
368
+ </div>
369
+ </Show>
370
+ </div>
371
+ <div
372
+ class="pointer-events-none absolute inset-x-0 bottom-0 h-10"
373
+ style={{ background: "linear-gradient(to bottom, transparent, var(--v2-background-bg-layer-01))" }}
374
+ />
375
+ </div>
376
+ </div>
377
+ )
378
+ }
379
+
380
+ function ProviderConnection(props: {
381
+ provider: string
382
+ directory?: Accessor<string | undefined>
383
+ onBack: () => void
384
+ setBack: (handler: () => void) => void
385
+ }) {
386
+ const dialog = useDialog()
387
+ const serverSync = useServerSync()
388
+ const serverSDK = useServerSDK()
389
+ const queryClient = useQueryClient()
390
+ const params = useParams()
391
+ const language = useLanguage()
392
+ const settings = useSettings()
393
+ const newLayout = settings.general.newLayoutDesigns
394
+ const providers = useProviders(props.directory)
395
+ const directory = () => props.directory?.() ?? decode64(params.dir)
396
+ const location = () => {
397
+ const value = directory()
398
+ return value ? { directory: value } : undefined
399
+ }
400
+
401
+ const alive = { value: true }
402
+ const timer = { current: undefined as ReturnType<typeof setTimeout> | undefined }
403
+
404
+ onCleanup(() => {
405
+ alive.value = false
406
+ if (timer.current === undefined) return
407
+ clearTimeout(timer.current)
408
+ timer.current = undefined
409
+ })
410
+
411
+ const provider = createMemo(
412
+ () => providers.all().get(props.provider) ?? serverSync().data.provider.all.get(props.provider)!,
413
+ )
414
+ const fallback = createMemo<ConnectMethod[]>(() => [
415
+ {
416
+ type: "key" as const,
417
+ label: language.t("provider.connect.method.apiKey"),
418
+ },
419
+ ])
420
+ const [integration] = createResource(
421
+ () => ({ provider: props.provider, directory: directory() }),
422
+ (input) =>
423
+ serverSDK()
424
+ .api.integration.get({
425
+ integrationID: input.provider,
426
+ location: input.directory ? { directory: input.directory } : undefined,
427
+ })
428
+ .then((result) => result.data),
429
+ )
430
+ const loading = createMemo(() => integration.loading)
431
+ const methods = createMemo<ConnectMethod[]>(() => {
432
+ const values = integration.latest?.methods.filter(
433
+ (method): method is ConnectMethod => method.type === "key" || method.type === "oauth",
434
+ )
435
+ return values?.length ? values : fallback()
436
+ })
437
+ const [store, setStore] = createStore({
438
+ methodIndex: undefined as undefined | number,
439
+ authorization: undefined as undefined | IntegrationOauthConnectOutput["data"],
440
+ promptInputs: undefined as undefined | Record<string, string>,
441
+ state: "pending" as undefined | "pending" | "complete" | "error" | "prompt",
442
+ error: undefined as string | undefined,
443
+ })
444
+
445
+ type Action =
446
+ | { type: "method.select"; index: number }
447
+ | { type: "method.reset" }
448
+ | { type: "auth.prompt" }
449
+ | { type: "auth.inputs"; inputs: Record<string, string> }
450
+ | { type: "auth.pending" }
451
+ | { type: "auth.complete"; authorization: IntegrationOauthConnectOutput["data"] }
452
+ | { type: "auth.error"; error: string }
453
+
454
+ function dispatch(action: Action) {
455
+ setStore(
456
+ produce((draft) => {
457
+ if (action.type === "method.select") {
458
+ draft.methodIndex = action.index
459
+ draft.authorization = undefined
460
+ draft.promptInputs = undefined
461
+ draft.state = undefined
462
+ draft.error = undefined
463
+ return
464
+ }
465
+ if (action.type === "method.reset") {
466
+ draft.methodIndex = undefined
467
+ draft.authorization = undefined
468
+ draft.promptInputs = undefined
469
+ draft.state = undefined
470
+ draft.error = undefined
471
+ return
472
+ }
473
+ if (action.type === "auth.prompt") {
474
+ draft.state = "prompt"
475
+ draft.error = undefined
476
+ return
477
+ }
478
+ if (action.type === "auth.inputs") {
479
+ draft.promptInputs = action.inputs
480
+ draft.state = undefined
481
+ draft.error = undefined
482
+ return
483
+ }
484
+ if (action.type === "auth.pending") {
485
+ draft.state = "pending"
486
+ draft.error = undefined
487
+ return
488
+ }
489
+ if (action.type === "auth.complete") {
490
+ draft.state = "complete"
491
+ draft.authorization = action.authorization
492
+ draft.error = undefined
493
+ return
494
+ }
495
+ draft.state = "error"
496
+ draft.error = action.error
497
+ }),
498
+ )
499
+ }
500
+
501
+ const method = createMemo(() => (store.methodIndex !== undefined ? methods().at(store.methodIndex!) : undefined))
502
+
503
+ const methodLabel = (value?: { type?: string; label?: string }) => {
504
+ if (!value) return ""
505
+ if (value.type === "key") return language.t("provider.connect.method.apiKey")
506
+ return value.label ?? ""
507
+ }
508
+
509
+ const methodDetails = (value?: { type?: string; label?: string }) => {
510
+ const label = methodLabel(value)
511
+ const suffix = value?.label?.match(/\s+\((browser|headless)\)$/i)
512
+ const hint = suffix?.[1]
513
+ return {
514
+ label: suffix ? label.slice(0, -suffix[0].length) : label,
515
+ hint: hint ? hint[0].toUpperCase() + hint.slice(1) : value?.type === "key" ? "Browser" : undefined,
516
+ }
517
+ }
518
+
519
+ function formatError(value: unknown, fallback: string): string {
520
+ if (value && typeof value === "object" && "data" in value) {
521
+ const data = (value as { data?: { message?: unknown } }).data
522
+ if (typeof data?.message === "string" && data.message) return data.message
523
+ }
524
+ if (value && typeof value === "object" && "error" in value) {
525
+ const nested = formatError((value as { error?: unknown }).error, "")
526
+ if (nested) return nested
527
+ }
528
+ if (value && typeof value === "object" && "message" in value) {
529
+ const message = (value as { message?: unknown }).message
530
+ if (typeof message === "string" && message) return message
531
+ }
532
+ if (value instanceof Error && value.message) return value.message
533
+ if (typeof value === "string" && value) return value
534
+ return fallback
535
+ }
536
+
537
+ async function selectMethod(index: number, inputs?: Record<string, string>) {
538
+ if (timer.current !== undefined) {
539
+ clearTimeout(timer.current)
540
+ timer.current = undefined
541
+ }
542
+
543
+ const method = methods()[index]
544
+ dispatch({ type: "method.select", index })
545
+
546
+ if (method.type === "oauth") {
547
+ if (method.prompts?.length && !inputs) {
548
+ dispatch({ type: "auth.prompt" })
549
+ return
550
+ }
551
+ dispatch({ type: "auth.pending" })
552
+ await serverSDK()
553
+ .api.integration.oauth.connect({
554
+ integrationID: props.provider,
555
+ methodID: method.id,
556
+ inputs: inputs ?? {},
557
+ location: location(),
558
+ })
559
+ .then((x) => {
560
+ if (!alive.value) return
561
+ dispatch({ type: "auth.complete", authorization: x.data })
562
+ })
563
+ .catch((e) => {
564
+ if (!alive.value) return
565
+ dispatch({ type: "auth.error", error: formatError(e, language.t("common.requestFailed")) })
566
+ })
567
+ }
568
+ }
569
+
570
+ function AuthPromptsView() {
571
+ const [formStore, setFormStore] = createStore({
572
+ value: {} as Record<string, string>,
573
+ index: 0,
574
+ })
575
+
576
+ const prompts = createMemo(() => {
577
+ const value = method()
578
+ return value?.type === "oauth" ? (value.prompts ?? []) : []
579
+ })
580
+ const matches = (prompt: NonNullable<ReturnType<typeof prompts>[number]>, value: Record<string, string>) => {
581
+ if (!prompt.when) return true
582
+ const actual = value[prompt.when.key]
583
+ if (actual === undefined) return false
584
+ return prompt.when.op === "eq" ? actual === prompt.when.value : actual !== prompt.when.value
585
+ }
586
+ const current = createMemo(() => {
587
+ const all = prompts()
588
+ const index = all.findIndex((prompt, index) => index >= formStore.index && matches(prompt, formStore.value))
589
+ if (index === -1) return
590
+ return {
591
+ index,
592
+ prompt: all[index],
593
+ }
594
+ })
595
+ const valid = createMemo(() => {
596
+ const item = current()
597
+ if (!item || item.prompt.type !== "text") return false
598
+ const value = formStore.value[item.prompt.key] ?? ""
599
+ return value.trim().length > 0
600
+ })
601
+
602
+ async function next(index: number, value: Record<string, string>) {
603
+ if (store.methodIndex === undefined) return
604
+ const next = prompts().findIndex((prompt, i) => i > index && matches(prompt, value))
605
+ if (next !== -1) {
606
+ setFormStore("index", next)
607
+ return
608
+ }
609
+ await selectMethod(store.methodIndex, value)
610
+ }
611
+
612
+ async function handleSubmit(e: SubmitEvent) {
613
+ e.preventDefault()
614
+ const item = current()
615
+ if (!item || item.prompt.type !== "text") return
616
+ if (!valid()) return
617
+ await next(item.index, formStore.value)
618
+ }
619
+
620
+ const item = () => current()
621
+ const text = createMemo(() => {
622
+ const prompt = item()?.prompt
623
+ if (!prompt || prompt.type !== "text") return
624
+ return prompt
625
+ })
626
+ const select = createMemo(() => {
627
+ const prompt = item()?.prompt
628
+ if (!prompt || prompt.type !== "select") return
629
+ return prompt
630
+ })
631
+
632
+ return (
633
+ <form onSubmit={handleSubmit} class="flex flex-col items-start gap-4">
634
+ <Switch>
635
+ <Match when={item()?.prompt.type === "text"}>
636
+ <TextField
637
+ type="text"
638
+ label={text()?.message ?? ""}
639
+ placeholder={text()?.placeholder}
640
+ value={text() ? (formStore.value[text()!.key] ?? "") : ""}
641
+ onChange={(value) => {
642
+ const prompt = text()
643
+ if (!prompt) return
644
+ setFormStore("value", prompt.key, value)
645
+ }}
646
+ />
647
+ <Button class="w-auto" type="submit" size="large" variant="primary" disabled={!valid()}>
648
+ {language.t("common.continue")}
649
+ </Button>
650
+ </Match>
651
+ <Match when={item()?.prompt.type === "select"}>
652
+ <div class="w-full flex flex-col gap-1.5">
653
+ <div class="text-14-regular text-text-base">{select()?.message}</div>
654
+ <div>
655
+ <List
656
+ class="px-3"
657
+ items={select()?.options ?? []}
658
+ key={(x) => x.value}
659
+ current={select()?.options.find((x) => x.value === formStore.value[select()!.key])}
660
+ onSelect={(value) => {
661
+ if (!value) return
662
+ const prompt = select()
663
+ if (!prompt) return
664
+ const nextValue = {
665
+ ...formStore.value,
666
+ [prompt.key]: value.value,
667
+ }
668
+ setFormStore("value", prompt.key, value.value)
669
+ void next(item()!.index, nextValue)
670
+ }}
671
+ >
672
+ {(option) => (
673
+ <div class="w-full flex items-center gap-x-2">
674
+ <div class="w-4 h-2 rounded-[1px] bg-input-base shadow-xs-border-base flex items-center justify-center">
675
+ <div class="w-2.5 h-0.5 ml-0 bg-icon-strong-base hidden" data-slot="list-item-extra-icon" />
676
+ </div>
677
+ <span>{option.label}</span>
678
+ <span class="text-14-regular text-text-weak">{option.hint}</span>
679
+ </div>
680
+ )}
681
+ </List>
682
+ </div>
683
+ </div>
684
+ </Match>
685
+ </Switch>
686
+ </form>
687
+ )
688
+ }
689
+
690
+ let listRef: ListRef | undefined
691
+ function handleKey(e: KeyboardEvent) {
692
+ if (e.key === "Enter" && e.target instanceof HTMLInputElement) {
693
+ return
694
+ }
695
+ if (e.key === "Escape") return
696
+ listRef?.onKeyDown(e)
697
+ }
698
+
699
+ let auto = false
700
+ createEffect(() => {
701
+ if (auto) return
702
+ if (loading()) return
703
+ if (methods().length === 1) {
704
+ auto = true
705
+ void selectMethod(0)
706
+ }
707
+ })
708
+
709
+ async function complete() {
710
+ const value = directory()
711
+ await queryClient
712
+ .refetchQueries(serverSync().queryOptions.providers(value ? pathKey(value) : null))
713
+ .catch(() => undefined)
714
+ dialog.close()
715
+ showToast({
716
+ variant: "success",
717
+ icon: "circle-check",
718
+ title: language.t("provider.connect.toast.connected.title", { provider: provider().name }),
719
+ description: language.t("provider.connect.toast.connected.description", { provider: provider().name }),
720
+ })
721
+ }
722
+
723
+ function goBack() {
724
+ if (methods().length > 1 && store.methodIndex !== undefined) {
725
+ dispatch({ type: "method.reset" })
726
+ return
727
+ }
728
+ props.onBack()
729
+ }
730
+
731
+ props.setBack(goBack)
732
+
733
+ function MethodSelection() {
734
+ if (newLayout())
735
+ return (
736
+ <div class="flex flex-col gap-2">
737
+ <div class="px-3 text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-muted">
738
+ {language.t("provider.connect.selectMethod", { provider: provider().name })}
739
+ </div>
740
+ <div class="flex flex-col">
741
+ <For each={methods()}>
742
+ {(item, index) => {
743
+ const details = () => methodDetails(item)
744
+ return (
745
+ <button
746
+ type="button"
747
+ class="group flex h-9 w-full items-center gap-2 rounded-md px-3 text-left text-[13px] leading-5 tracking-[-0.04px] hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none"
748
+ onClick={() => void selectMethod(index())}
749
+ >
750
+ <span class="flex h-2 w-4 shrink-0 items-center justify-center rounded-[1px] bg-v2-background-bg-base shadow-[var(--v2-elevation-button-neutral)]">
751
+ <span class="hidden h-0.5 w-2.5 bg-v2-icon-icon-base group-hover:block group-focus-visible:block" />
752
+ </span>
753
+ <span class="font-[530] text-v2-text-text-base">{details().label}</span>
754
+ <Show when={details().hint}>
755
+ {(hint) => <span class="font-[440] text-v2-text-text-muted">{hint()}</span>}
756
+ </Show>
757
+ </button>
758
+ )
759
+ }}
760
+ </For>
761
+ </div>
762
+ </div>
763
+ )
764
+
765
+ return (
766
+ <>
767
+ <div class="text-14-regular text-text-base">
768
+ {language.t("provider.connect.selectMethod", { provider: provider().name })}
769
+ </div>
770
+ <div>
771
+ <List
772
+ class="px-3"
773
+ ref={(ref) => {
774
+ listRef = ref
775
+ }}
776
+ items={methods}
777
+ key={(m) => m?.label ?? m?.type}
778
+ onSelect={async (selected, index) => {
779
+ if (!selected) return
780
+ void selectMethod(index)
781
+ }}
782
+ >
783
+ {(i) => (
784
+ <div class="w-full flex items-center gap-x-2">
785
+ <div class="w-4 h-2 rounded-[1px] bg-input-base shadow-xs-border-base flex items-center justify-center">
786
+ <div class="w-2.5 h-0.5 ml-0 bg-icon-strong-base hidden" data-slot="list-item-extra-icon" />
787
+ </div>
788
+ <span>{methodLabel(i)}</span>
789
+ </div>
790
+ )}
791
+ </List>
792
+ </div>
793
+ </>
794
+ )
795
+ }
796
+
797
+ function ApiAuthView() {
798
+ let apiKey: HTMLInputElement | undefined
799
+ const errorID = createUniqueId()
800
+ const [formStore, setFormStore] = createStore({
801
+ value: "",
802
+ error: undefined as string | undefined,
803
+ })
804
+
805
+ onMount(() => {
806
+ if (!newLayout()) return
807
+ apiKey?.focus({ preventScroll: true })
808
+ })
809
+
810
+ async function handleSubmit(e: SubmitEvent) {
811
+ e.preventDefault()
812
+
813
+ const form = e.currentTarget as HTMLFormElement
814
+ const formData = new FormData(form)
815
+ const apiKey = formData.get("apiKey") as string
816
+
817
+ if (!apiKey?.trim()) {
818
+ setFormStore("error", language.t("provider.connect.apiKey.required"))
819
+ return
820
+ }
821
+
822
+ setFormStore("error", undefined)
823
+ await serverSDK().api.integration.connect.key({
824
+ integrationID: props.provider,
825
+ location: location(),
826
+ key: apiKey,
827
+ })
828
+ await complete()
829
+ }
830
+
831
+ if (newLayout())
832
+ return (
833
+ <div class="flex flex-col gap-5 px-3 text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-muted">
834
+ <Show
835
+ when={provider().id === "neurocode"}
836
+ fallback={language.t("provider.connect.apiKey.description", { provider: provider().name })}
837
+ >
838
+ <div class="flex flex-col gap-5">
839
+ <div>{language.t("provider.connect.opencodeZen.line1")}</div>
840
+ <div>{language.t("provider.connect.opencodeZen.line2")}</div>
841
+ <div>
842
+ {language.t("provider.connect.opencodeZen.visit.prefix")}
843
+ <Link
844
+ href="https://neurocode.ai/zen"
845
+ class="text-v2-text-text-base focus-visible:rounded-xs focus-visible:outline-2 focus-visible:outline-v2-border-border-focus"
846
+ >
847
+ {language.t("provider.connect.opencodeZen.visit.link")}
848
+ </Link>
849
+ {language.t("provider.connect.opencodeZen.visit.suffix")}
850
+ </div>
851
+ </div>
852
+ </Show>
853
+ <form onSubmit={handleSubmit} class="flex flex-col items-start gap-5 self-stretch">
854
+ <label class="flex w-full flex-col gap-1 font-[530] leading-4 text-v2-text-text-base">
855
+ {language.t("provider.connect.apiKey.label", { provider: provider().name })}
856
+ <TextInputV2
857
+ ref={apiKey}
858
+ class="!w-full"
859
+ name="apiKey"
860
+ placeholder={language.t("provider.connect.apiKey.placeholder")}
861
+ value={formStore.value}
862
+ invalid={formStore.error !== undefined}
863
+ aria-describedby={formStore.error ? errorID : undefined}
864
+ autocomplete="off"
865
+ spellcheck={false}
866
+ onInput={(event) => setFormStore("value", event.currentTarget.value)}
867
+ />
868
+ </label>
869
+ <Show when={formStore.error}>
870
+ {(error) => (
871
+ <div id={errorID} role="alert" class="-mt-4 text-xs text-v2-state-fg-danger">
872
+ {error()}
873
+ </div>
874
+ )}
875
+ </Show>
876
+ <ButtonV2 type="submit" variant="contrast">
877
+ {language.t("common.continue")}
878
+ </ButtonV2>
879
+ </form>
880
+ </div>
881
+ )
882
+
883
+ return (
884
+ <div class="flex flex-col gap-6">
885
+ <Switch>
886
+ <Match when={provider().id === "neurocode"}>
887
+ <div class="flex flex-col gap-4">
888
+ <div class="text-14-regular text-text-base">{language.t("provider.connect.opencodeZen.line1")}</div>
889
+ <div class="text-14-regular text-text-base">{language.t("provider.connect.opencodeZen.line2")}</div>
890
+ <div class="text-14-regular text-text-base">
891
+ {language.t("provider.connect.opencodeZen.visit.prefix")}
892
+ <Link href="https://neurocode.ai/zen" tabIndex={-1}>
893
+ {language.t("provider.connect.opencodeZen.visit.link")}
894
+ </Link>
895
+ {language.t("provider.connect.opencodeZen.visit.suffix")}
896
+ </div>
897
+ </div>
898
+ </Match>
899
+ <Match when={true}>
900
+ <div class="text-14-regular text-text-base">
901
+ {language.t("provider.connect.apiKey.description", { provider: provider().name })}
902
+ </div>
903
+ </Match>
904
+ </Switch>
905
+ <form onSubmit={handleSubmit} class="flex flex-col items-start gap-4">
906
+ <TextField
907
+ autofocus={!newLayout()}
908
+ ref={apiKey}
909
+ type="text"
910
+ label={language.t("provider.connect.apiKey.label", { provider: provider().name })}
911
+ placeholder={language.t("provider.connect.apiKey.placeholder")}
912
+ name="apiKey"
913
+ value={formStore.value}
914
+ onChange={(v) => setFormStore("value", v)}
915
+ validationState={formStore.error ? "invalid" : undefined}
916
+ error={formStore.error}
917
+ />
918
+ <Button class="w-auto" type="submit" size="large" variant="primary">
919
+ {language.t("common.continue")}
920
+ </Button>
921
+ </form>
922
+ </div>
923
+ )
924
+ }
925
+
926
+ function OAuthCodeView() {
927
+ let codeInput: HTMLInputElement | undefined
928
+ const errorID = createUniqueId()
929
+ const [formStore, setFormStore] = createStore({
930
+ value: "",
931
+ error: undefined as string | undefined,
932
+ })
933
+
934
+ onMount(() => {
935
+ if (!newLayout()) return
936
+ codeInput?.focus({ preventScroll: true })
937
+ })
938
+
939
+ async function handleSubmit(e: SubmitEvent) {
940
+ e.preventDefault()
941
+
942
+ const form = e.currentTarget as HTMLFormElement
943
+ const formData = new FormData(form)
944
+ const code = formData.get("code") as string
945
+
946
+ if (!code?.trim()) {
947
+ setFormStore("error", language.t("provider.connect.oauth.code.required"))
948
+ return
949
+ }
950
+
951
+ setFormStore("error", undefined)
952
+ const result = await serverSDK()
953
+ .api.integration.oauth.complete({
954
+ integrationID: props.provider,
955
+ attemptID: store.authorization!.attemptID,
956
+ location: location(),
957
+ code,
958
+ })
959
+ .then(() => ({ ok: true as const }))
960
+ .catch((error) => ({ ok: false as const, error }))
961
+ if (result.ok) {
962
+ await complete()
963
+ return
964
+ }
965
+ setFormStore("error", formatError(result.error, language.t("provider.connect.oauth.code.invalid")))
966
+ }
967
+
968
+ if (newLayout())
969
+ return (
970
+ <div class="flex flex-col gap-5 px-3 text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-muted">
971
+ <div>
972
+ {language.t("provider.connect.oauth.code.visit.prefix")}
973
+ <Link href={store.authorization!.url} class="text-v2-text-text-base">
974
+ {language.t("provider.connect.oauth.code.visit.link")}
975
+ </Link>
976
+ {language.t("provider.connect.oauth.code.visit.suffix", { provider: provider().name })}
977
+ </div>
978
+ <form onSubmit={handleSubmit} class="flex flex-col items-start gap-5 self-stretch">
979
+ <label class="flex w-full flex-col gap-1 font-[530] leading-4 text-v2-text-text-base">
980
+ {language.t("provider.connect.oauth.code.label", { method: method()?.label ?? "" })}
981
+ <TextInputV2
982
+ ref={codeInput}
983
+ class="!w-full"
984
+ name="code"
985
+ placeholder={language.t("provider.connect.oauth.code.placeholder")}
986
+ value={formStore.value}
987
+ invalid={formStore.error !== undefined}
988
+ aria-describedby={formStore.error ? errorID : undefined}
989
+ autocomplete="off"
990
+ spellcheck={false}
991
+ onInput={(event) => setFormStore("value", event.currentTarget.value)}
992
+ />
993
+ </label>
994
+ <Show when={formStore.error}>
995
+ {(error) => (
996
+ <div id={errorID} role="alert" class="-mt-4 text-xs text-v2-state-fg-danger">
997
+ {error()}
998
+ </div>
999
+ )}
1000
+ </Show>
1001
+ <ButtonV2 type="submit" variant="contrast">
1002
+ {language.t("common.continue")}
1003
+ </ButtonV2>
1004
+ </form>
1005
+ </div>
1006
+ )
1007
+
1008
+ return (
1009
+ <div class="flex flex-col gap-6">
1010
+ <div class="text-14-regular text-text-base">
1011
+ {language.t("provider.connect.oauth.code.visit.prefix")}
1012
+ <Link href={store.authorization!.url}>{language.t("provider.connect.oauth.code.visit.link")}</Link>
1013
+ {language.t("provider.connect.oauth.code.visit.suffix", { provider: provider().name })}
1014
+ </div>
1015
+ <form onSubmit={handleSubmit} class="flex flex-col items-start gap-4">
1016
+ <TextField
1017
+ autofocus={!newLayout()}
1018
+ ref={codeInput}
1019
+ type="text"
1020
+ label={language.t("provider.connect.oauth.code.label", { method: method()?.label ?? "" })}
1021
+ placeholder={language.t("provider.connect.oauth.code.placeholder")}
1022
+ name="code"
1023
+ value={formStore.value}
1024
+ onChange={(v) => setFormStore("value", v)}
1025
+ validationState={formStore.error ? "invalid" : undefined}
1026
+ error={formStore.error}
1027
+ />
1028
+ <Button class="w-auto" type="submit" size="large" variant="primary">
1029
+ {language.t("common.continue")}
1030
+ </Button>
1031
+ </form>
1032
+ </div>
1033
+ )
1034
+ }
1035
+
1036
+ function OAuthAutoView() {
1037
+ const code = createMemo(() => {
1038
+ const instructions = store.authorization?.instructions
1039
+ if (instructions?.includes(":")) {
1040
+ return instructions.split(":").pop()?.trim()
1041
+ }
1042
+ return instructions
1043
+ })
1044
+
1045
+ onMount(() => {
1046
+ const poll = async () => {
1047
+ const authorization = store.authorization
1048
+ if (!authorization || !alive.value) return
1049
+ const result = await serverSDK()
1050
+ .api.integration.oauth.status({
1051
+ integrationID: props.provider,
1052
+ attemptID: authorization.attemptID,
1053
+ location: location(),
1054
+ })
1055
+ .then((value) => ({ ok: true as const, status: value.data }))
1056
+ .catch((error) => ({ ok: false as const, error }))
1057
+ if (!alive.value) return
1058
+ if (!result.ok) {
1059
+ dispatch({ type: "auth.error", error: formatError(result.error, language.t("common.requestFailed")) })
1060
+ return
1061
+ }
1062
+ if (result.status.status === "complete") {
1063
+ await complete()
1064
+ return
1065
+ }
1066
+ if (result.status.status === "failed") {
1067
+ dispatch({ type: "auth.error", error: result.status.message })
1068
+ return
1069
+ }
1070
+ if (result.status.status === "expired") {
1071
+ dispatch({ type: "auth.error", error: language.t("common.requestFailed") })
1072
+ return
1073
+ }
1074
+ timer.current = setTimeout(poll, 1_000)
1075
+ }
1076
+ void poll()
1077
+ })
1078
+
1079
+ return (
1080
+ <div class="flex flex-col gap-6">
1081
+ <div class="text-14-regular text-text-base">
1082
+ {language.t("provider.connect.oauth.auto.visit.prefix")}
1083
+ <Link href={store.authorization!.url}>{language.t("provider.connect.oauth.auto.visit.link")}</Link>
1084
+ {language.t("provider.connect.oauth.auto.visit.suffix", { provider: provider().name })}
1085
+ </div>
1086
+ <TextField
1087
+ label={language.t("provider.connect.oauth.auto.confirmationCode")}
1088
+ class="font-mono"
1089
+ value={code()}
1090
+ readOnly
1091
+ copyable
1092
+ />
1093
+ <div class="text-14-regular text-text-base flex items-center gap-4">
1094
+ <Spinner />
1095
+ <span>{language.t("provider.connect.status.waiting")}</span>
1096
+ </div>
1097
+ </div>
1098
+ )
1099
+ }
1100
+
1101
+ return (
1102
+ <div class={newLayout() ? "flex min-h-0 flex-1 flex-col" : "flex flex-col gap-6 px-2.5 pb-3"}>
1103
+ <div class={newLayout() ? "flex h-10 shrink-0 items-start gap-2 px-3" : "flex items-center gap-4 px-2.5"}>
1104
+ <ProviderIcon
1105
+ id={props.provider}
1106
+ class={newLayout() ? "mt-0.5 size-4 shrink-0 text-v2-icon-icon-base" : "size-5 shrink-0 icon-strong-base"}
1107
+ />
1108
+ <div
1109
+ class={
1110
+ newLayout()
1111
+ ? "text-[15px] font-[530] leading-5 tracking-[-0.13px] text-v2-text-text-base"
1112
+ : "text-16-medium text-text-strong"
1113
+ }
1114
+ >
1115
+ <Switch>
1116
+ <Match when={props.provider === "anthropic" && method()?.label?.toLowerCase().includes("max")}>
1117
+ {language.t("provider.connect.title.anthropicProMax")}
1118
+ </Match>
1119
+ <Match when={true}>{language.t("provider.connect.title", { provider: provider().name })}</Match>
1120
+ </Switch>
1121
+ </div>
1122
+ </div>
1123
+ <div class={newLayout() ? "flex min-h-0 flex-1 flex-col" : "flex flex-col gap-6 px-2.5 pb-10"}>
1124
+ <div
1125
+ onKeyDown={handleKey}
1126
+ tabIndex={newLayout() ? undefined : 0}
1127
+ autofocus={!newLayout() && store.methodIndex === undefined ? true : undefined}
1128
+ >
1129
+ <Switch>
1130
+ <Match when={loading()}>
1131
+ <div class="text-14-regular text-text-base">
1132
+ <div class="flex items-center gap-x-2">
1133
+ <Spinner />
1134
+ <span>{language.t("provider.connect.status.inProgress")}</span>
1135
+ </div>
1136
+ </div>
1137
+ </Match>
1138
+ <Match when={store.methodIndex === undefined}>
1139
+ <MethodSelection />
1140
+ </Match>
1141
+ <Match when={store.state === "pending"}>
1142
+ <div class="text-14-regular text-text-base">
1143
+ <div class="flex items-center gap-x-2">
1144
+ <Spinner />
1145
+ <span>{language.t("provider.connect.status.inProgress")}</span>
1146
+ </div>
1147
+ </div>
1148
+ </Match>
1149
+ <Match when={store.state === "prompt"}>
1150
+ <AuthPromptsView />
1151
+ </Match>
1152
+ <Match when={store.state === "error"}>
1153
+ <div class="text-14-regular text-text-base">
1154
+ <div class="flex items-center gap-x-2">
1155
+ <Icon name="circle-ban-sign" class="text-icon-critical-base" />
1156
+ <span>{language.t("provider.connect.status.failed", { error: store.error ?? "" })}</span>
1157
+ </div>
1158
+ </div>
1159
+ </Match>
1160
+ <Match when={method()?.type === "key"}>
1161
+ <ApiAuthView />
1162
+ </Match>
1163
+ <Match when={method()?.type === "oauth"}>
1164
+ <Switch>
1165
+ <Match when={store.authorization?.mode === "code"}>
1166
+ <OAuthCodeView />
1167
+ </Match>
1168
+ <Match when={store.authorization?.mode === "auto"}>
1169
+ <OAuthAutoView />
1170
+ </Match>
1171
+ </Switch>
1172
+ </Match>
1173
+ </Switch>
1174
+ </div>
1175
+ </div>
1176
+ </div>
1177
+ )
1178
+ }