@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,109 @@
1
+ export type ConfigInvalidError = {
2
+ name: "ConfigInvalidError"
3
+ data: {
4
+ path?: string
5
+ message?: string
6
+ issues?: Array<{ message: string; path: string[] }>
7
+ }
8
+ }
9
+
10
+ export type ProviderModelNotFoundError = {
11
+ name: "ProviderModelNotFoundError"
12
+ data: {
13
+ providerID: string
14
+ modelID: string
15
+ suggestions?: string[]
16
+ }
17
+ }
18
+
19
+ type Translator = (key: string, vars?: Record<string, string | number>) => string
20
+
21
+ function tr(translator: Translator | undefined, key: string, text: string, vars?: Record<string, string | number>) {
22
+ if (!translator) return text
23
+ const out = translator(key, vars)
24
+ if (!out || out === key) return text
25
+ return out
26
+ }
27
+
28
+ export function formatServerError(error: unknown, translate?: Translator, fallback?: string) {
29
+ const unwrapped = unwrapNamedError(error)
30
+ if (isConfigInvalidErrorLike(unwrapped)) return parseReadableConfigInvalidError(unwrapped, translate)
31
+ if (isProviderModelNotFoundErrorLike(unwrapped)) return parseReadableProviderModelNotFoundError(unwrapped, translate)
32
+ if (error instanceof Error && error.message) return error.message
33
+ if (typeof error === "string" && error) return error
34
+ if (fallback) return fallback
35
+ return tr(translate, "error.chain.unknown", "Unknown error")
36
+ }
37
+
38
+ function unwrapNamedError(error: unknown): unknown {
39
+ if (error instanceof Error && error.cause && typeof error.cause === "object" && "body" in error.cause) {
40
+ return (error.cause as Record<string, unknown>).body
41
+ }
42
+ return error
43
+ }
44
+
45
+ // Client-synthesized session not-found errors share one constructor and
46
+ // predicate so the message contract cannot drift between the sync store
47
+ // (server-session.ts), the route lineage (session-lineage.ts), and the
48
+ // not-found fallback matching (session.tsx).
49
+ const sessionNotFoundMessage = (sessionID: string) => `Session not found: ${sessionID}`
50
+
51
+ export function sessionNotFoundError(sessionID: string) {
52
+ return new Error(sessionNotFoundMessage(sessionID))
53
+ }
54
+
55
+ export function isLocalSessionNotFoundError(error: unknown, sessionID: string) {
56
+ return error instanceof Error && error.message === sessionNotFoundMessage(sessionID)
57
+ }
58
+
59
+ export function isSessionNotFoundError(error: unknown, sessionID: string) {
60
+ const unwrapped = unwrapNamedError(error)
61
+ if (typeof unwrapped !== "object" || unwrapped === null) return false
62
+ const value = unwrapped as Record<string, unknown>
63
+ return value._tag === "SessionNotFoundError" && value.sessionID === sessionID
64
+ }
65
+
66
+ function isConfigInvalidErrorLike(error: unknown): error is ConfigInvalidError {
67
+ if (typeof error !== "object" || error === null) return false
68
+ const o = error as Record<string, unknown>
69
+ return o.name === "ConfigInvalidError" && typeof o.data === "object" && o.data !== null
70
+ }
71
+
72
+ function isProviderModelNotFoundErrorLike(error: unknown): error is ProviderModelNotFoundError {
73
+ if (typeof error !== "object" || error === null) return false
74
+ const o = error as Record<string, unknown>
75
+ return o.name === "ProviderModelNotFoundError" && typeof o.data === "object" && o.data !== null
76
+ }
77
+
78
+ export function parseReadableConfigInvalidError(errorInput: ConfigInvalidError, translator?: Translator) {
79
+ const file = errorInput.data.path && errorInput.data.path !== "config" ? errorInput.data.path : "config"
80
+ const detail = errorInput.data.message?.trim() ?? ""
81
+ const issues = (errorInput.data.issues ?? [])
82
+ .map((issue) => {
83
+ const msg = issue.message.trim()
84
+ if (!issue.path.length) return msg
85
+ return `${issue.path.join(".")}: ${msg}`
86
+ })
87
+ .filter(Boolean)
88
+ const msg = issues.length ? issues.join("\n") : detail
89
+ if (!msg) return tr(translator, "error.chain.configInvalid", `Config file at ${file} is invalid`, { path: file })
90
+ return tr(translator, "error.chain.configInvalidWithMessage", `Config file at ${file} is invalid: ${msg}`, {
91
+ path: file,
92
+ message: msg,
93
+ })
94
+ }
95
+
96
+ function parseReadableProviderModelNotFoundError(errorInput: ProviderModelNotFoundError, translator?: Translator) {
97
+ const p = errorInput.data.providerID.trim()
98
+ const m = errorInput.data.modelID.trim()
99
+ const list = (errorInput.data.suggestions ?? []).map((v) => v.trim()).filter(Boolean)
100
+ const body = tr(translator, "error.chain.modelNotFound", `Model not found: ${p}/${m}`, { provider: p, model: m })
101
+ const tail = tr(translator, "error.chain.checkConfig", "Check your config (neurocode.json) provider/model names")
102
+ if (list.length) {
103
+ const suggestions = list.slice(0, 5).join(", ")
104
+ return [body, tr(translator, "error.chain.didYouMean", `Did you mean: ${suggestions}`, { suggestions }), tail].join(
105
+ "\n",
106
+ )
107
+ }
108
+ return [body, tail].join("\n")
109
+ }
@@ -0,0 +1,178 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import type { ServerConnection } from "@/context/server"
3
+ import { checkServerHealth } from "./server-health"
4
+
5
+ const server: ServerConnection.HttpBase = {
6
+ url: "http://localhost:4096",
7
+ }
8
+
9
+ function abortFromInput(input: RequestInfo | URL, init?: RequestInit) {
10
+ if (init?.signal) return init.signal
11
+ if (input instanceof Request) return input.signal
12
+ return undefined
13
+ }
14
+
15
+ describe("checkServerHealth", () => {
16
+ test("returns healthy response with version", async () => {
17
+ let request: URL | undefined
18
+ const fetch = (async (input: RequestInfo | URL) => {
19
+ request = input instanceof URL ? input : new URL(input instanceof Request ? input.url : input)
20
+ return new Response(JSON.stringify({ healthy: true, version: "1.2.3" }), {
21
+ status: 200,
22
+ headers: { "content-type": "application/json" },
23
+ })
24
+ }) as unknown as typeof globalThis.fetch
25
+
26
+ const result = await checkServerHealth(server, fetch)
27
+
28
+ expect(result).toEqual({ healthy: true, version: "1.2.3" })
29
+ expect(request?.pathname).toBe("/api/health")
30
+ })
31
+
32
+ test("falls back to the V1 health endpoint", async () => {
33
+ const paths: string[] = []
34
+ const fetch = (async (input: RequestInfo | URL) => {
35
+ const url = input instanceof URL ? input : new URL(input instanceof Request ? input.url : input)
36
+ paths.push(url.pathname)
37
+ if (url.pathname === "/api/health") return new Response(undefined, { status: 404 })
38
+ return Response.json({ healthy: true, version: "1.18.4" })
39
+ }) as unknown as typeof globalThis.fetch
40
+
41
+ expect(await checkServerHealth(server, fetch)).toEqual({ healthy: true, version: "1.18.4" })
42
+ expect(paths).toEqual(["/api/health", "/global/health"])
43
+ })
44
+
45
+ test("falls back when the current health response is malformed", async () => {
46
+ const paths: string[] = []
47
+ const fetch = (async (input: RequestInfo | URL) => {
48
+ const url = input instanceof URL ? input : new URL(input instanceof Request ? input.url : input)
49
+ paths.push(url.pathname)
50
+ if (url.pathname === "/api/health") return Response.json({})
51
+ return Response.json({ healthy: true, version: "1.18.4" })
52
+ }) as unknown as typeof globalThis.fetch
53
+
54
+ expect(await checkServerHealth(server, fetch)).toEqual({ healthy: true, version: "1.18.4" })
55
+ expect(paths).toEqual(["/api/health", "/global/health"])
56
+ })
57
+
58
+ test("allows slow servers thirty seconds by default", async () => {
59
+ const timeout = Object.getOwnPropertyDescriptor(AbortSignal, "timeout")
60
+ let timeoutMs = 0
61
+ Object.defineProperty(AbortSignal, "timeout", {
62
+ configurable: true,
63
+ value: (ms: number) => {
64
+ timeoutMs = ms
65
+ return new AbortController().signal
66
+ },
67
+ })
68
+
69
+ const fetch = (async () =>
70
+ new Response(JSON.stringify({ healthy: true, version: "1.2.3" }), {
71
+ status: 200,
72
+ headers: { "content-type": "application/json" },
73
+ })) as unknown as typeof globalThis.fetch
74
+
75
+ await checkServerHealth(server, fetch).finally(() => {
76
+ if (timeout) Object.defineProperty(AbortSignal, "timeout", timeout)
77
+ if (!timeout) Reflect.deleteProperty(AbortSignal, "timeout")
78
+ })
79
+
80
+ expect(timeoutMs).toBe(30_000)
81
+ })
82
+
83
+ test("returns unhealthy when request fails", async () => {
84
+ const fetch = (async () => {
85
+ throw new Error("network")
86
+ }) as unknown as typeof globalThis.fetch
87
+
88
+ const result = await checkServerHealth(server, fetch)
89
+
90
+ expect(result).toEqual({ healthy: false })
91
+ })
92
+
93
+ test("uses timeout fallback when AbortSignal.timeout is unavailable", async () => {
94
+ const timeout = Object.getOwnPropertyDescriptor(AbortSignal, "timeout")
95
+ Object.defineProperty(AbortSignal, "timeout", {
96
+ configurable: true,
97
+ value: undefined,
98
+ })
99
+
100
+ let aborted = false
101
+ const fetch = ((input: RequestInfo | URL, init?: RequestInit) =>
102
+ new Promise<Response>((_resolve, reject) => {
103
+ const signal = abortFromInput(input, init)
104
+ signal?.addEventListener(
105
+ "abort",
106
+ () => {
107
+ aborted = true
108
+ reject(new DOMException("Aborted", "AbortError"))
109
+ },
110
+ { once: true },
111
+ )
112
+ })) as unknown as typeof globalThis.fetch
113
+
114
+ const result = await checkServerHealth(server, fetch, {
115
+ timeoutMs: 10,
116
+ }).finally(() => {
117
+ if (timeout) Object.defineProperty(AbortSignal, "timeout", timeout)
118
+ if (!timeout) Reflect.deleteProperty(AbortSignal, "timeout")
119
+ })
120
+
121
+ expect(aborted).toBe(true)
122
+ expect(result).toEqual({ healthy: false })
123
+ })
124
+
125
+ test("uses provided abort signal", async () => {
126
+ let signal: AbortSignal | undefined
127
+ const fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
128
+ signal = abortFromInput(input, init)
129
+ return new Response(JSON.stringify({ healthy: true, version: "1.2.3" }), {
130
+ status: 200,
131
+ headers: { "content-type": "application/json" },
132
+ })
133
+ }) as unknown as typeof globalThis.fetch
134
+
135
+ const abort = new AbortController()
136
+ await checkServerHealth(server, fetch, {
137
+ signal: abort.signal,
138
+ })
139
+
140
+ expect(signal).toBe(abort.signal)
141
+ })
142
+
143
+ test("retries transient failures and eventually succeeds", async () => {
144
+ let count = 0
145
+ const fetch = (async () => {
146
+ count += 1
147
+ if (count < 3) throw new TypeError("network")
148
+ return new Response(JSON.stringify({ healthy: true, version: "1.2.3" }), {
149
+ status: 200,
150
+ headers: { "content-type": "application/json" },
151
+ })
152
+ }) as unknown as typeof globalThis.fetch
153
+
154
+ const result = await checkServerHealth(server, fetch, {
155
+ retryCount: 2,
156
+ retryDelayMs: 1,
157
+ })
158
+
159
+ expect(count).toBe(3)
160
+ expect(result).toEqual({ healthy: true, version: "1.2.3" })
161
+ })
162
+
163
+ test("returns unhealthy when retries are exhausted", async () => {
164
+ let count = 0
165
+ const fetch = (async () => {
166
+ count += 1
167
+ throw new TypeError("network")
168
+ }) as unknown as typeof globalThis.fetch
169
+
170
+ const result = await checkServerHealth(server, fetch, {
171
+ retryCount: 2,
172
+ retryDelayMs: 1,
173
+ })
174
+
175
+ expect(count).toBe(6)
176
+ expect(result).toEqual({ healthy: false })
177
+ })
178
+ })
@@ -0,0 +1,172 @@
1
+ import { usePlatform } from "@/context/platform"
2
+ import { ServerConnection } from "@/context/server"
3
+ import { authTokenFromCredentials, createSdkForServer } from "./server"
4
+ import { ClientError, NeuroCode } from "@neurocode-ai/client"
5
+ import { Accessor, createEffect, onCleanup } from "solid-js"
6
+ import { createStore, reconcile } from "solid-js/store"
7
+
8
+ export type ServerHealth = { healthy: boolean; version?: string }
9
+
10
+ interface CheckServerHealthOptions {
11
+ timeoutMs?: number
12
+ signal?: AbortSignal
13
+ retryCount?: number
14
+ retryDelayMs?: number
15
+ }
16
+
17
+ const defaultTimeoutMs = 30_000
18
+ const defaultRetryCount = 2
19
+ const defaultRetryDelayMs = 100
20
+ const cacheMs = 750
21
+ const healthCache = new Map<
22
+ string,
23
+ { at: number; done: boolean; fetch: typeof globalThis.fetch; promise: Promise<ServerHealth> }
24
+ >()
25
+
26
+ function cacheKey(server: ServerConnection.HttpBase) {
27
+ return `${server.url}\n${server.username ?? ""}\n${server.password ?? ""}`
28
+ }
29
+
30
+ function timeoutSignal(timeoutMs: number) {
31
+ const timeout = (AbortSignal as unknown as { timeout?: (ms: number) => AbortSignal }).timeout
32
+ if (timeout) {
33
+ try {
34
+ return {
35
+ signal: timeout.call(AbortSignal, timeoutMs),
36
+ clear: undefined as (() => void) | undefined,
37
+ }
38
+ } catch {}
39
+ }
40
+ const controller = new AbortController()
41
+ const timer = setTimeout(() => controller.abort(), timeoutMs)
42
+ return { signal: controller.signal, clear: () => clearTimeout(timer) }
43
+ }
44
+
45
+ function wait(ms: number, signal?: AbortSignal) {
46
+ return new Promise<void>((resolve, reject) => {
47
+ if (signal?.aborted) {
48
+ reject(new DOMException("Aborted", "AbortError"))
49
+ return
50
+ }
51
+ const timer = setTimeout(() => {
52
+ signal?.removeEventListener("abort", onAbort)
53
+ resolve()
54
+ }, ms)
55
+ const onAbort = () => {
56
+ clearTimeout(timer)
57
+ reject(new DOMException("Aborted", "AbortError"))
58
+ }
59
+ signal?.addEventListener("abort", onAbort, { once: true })
60
+ })
61
+ }
62
+
63
+ function retryable(error: unknown, signal?: AbortSignal) {
64
+ if (signal?.aborted) return false
65
+ if (error instanceof ClientError) return error.reason === "Transport"
66
+ if (!(error instanceof Error)) return false
67
+ if (error.name === "AbortError" || error.name === "TimeoutError") return false
68
+ if (error instanceof TypeError) return true
69
+ return /network|fetch|econnreset|econnrefused|enotfound|timedout/i.test(error.message)
70
+ }
71
+
72
+ export async function checkServerHealth(
73
+ server: ServerConnection.HttpBase,
74
+ fetch: typeof globalThis.fetch,
75
+ opts?: CheckServerHealthOptions,
76
+ ): Promise<ServerHealth> {
77
+ const timeout = opts?.signal ? undefined : timeoutSignal(opts?.timeoutMs ?? defaultTimeoutMs)
78
+ const signal = opts?.signal ?? timeout?.signal
79
+ const retryCount = opts?.retryCount ?? defaultRetryCount
80
+ const retryDelayMs = opts?.retryDelayMs ?? defaultRetryDelayMs
81
+ const next = (count: number, error: unknown) => {
82
+ if (count >= retryCount || !retryable(error, signal)) return Promise.resolve({ healthy: false } as const)
83
+ return wait(retryDelayMs * (count + 1), signal)
84
+ .then(() => attempt(count + 1))
85
+ .catch(() => ({ healthy: false }))
86
+ }
87
+ const attempt = async (count: number): Promise<ServerHealth> => {
88
+ const current = await NeuroCode.make({
89
+ baseUrl: server.url,
90
+ fetch,
91
+ headers: server.password
92
+ ? {
93
+ Authorization: `Basic ${authTokenFromCredentials({ username: server.username, password: server.password })}`,
94
+ }
95
+ : undefined,
96
+ })
97
+ .health.get({ signal })
98
+ .then((x) =>
99
+ typeof x.healthy === "boolean"
100
+ ? { data: { healthy: x.healthy, version: x.version } }
101
+ : { error: new Error("Invalid health response") },
102
+ )
103
+ .catch((error) => ({ error }))
104
+ if ("data" in current && current.data) return current.data
105
+ if (signal?.aborted) return { healthy: false }
106
+
107
+ return createSdkForServer({ server, fetch, signal })
108
+ .global.health()
109
+ .then((x) => (x.error ? next(count, x.error) : { healthy: x.data?.healthy === true, version: x.data?.version }))
110
+ .catch((error) => next(count, error))
111
+ }
112
+ return attempt(0).finally(() => timeout?.clear?.())
113
+ }
114
+
115
+ const pollMs = 10_000
116
+
117
+ export function useCheckServerHealth() {
118
+ const platform = usePlatform()
119
+ const fetcher = platform.fetch ?? globalThis.fetch
120
+
121
+ return (http: ServerConnection.HttpBase) => {
122
+ const key = cacheKey(http)
123
+ const hit = healthCache.get(key)
124
+ const now = Date.now()
125
+ if (hit && hit.fetch === fetcher && (!hit.done || now - hit.at < cacheMs)) return hit.promise
126
+ const promise = checkServerHealth(http, fetcher).finally(() => {
127
+ const next = healthCache.get(key)
128
+ if (!next || next.promise !== promise) return
129
+ next.done = true
130
+ next.at = Date.now()
131
+ })
132
+ healthCache.set(key, { at: now, done: false, fetch: fetcher, promise })
133
+ return promise
134
+ }
135
+ }
136
+
137
+ export const useServerHealth = (servers: Accessor<ServerConnection.Any[]>, enabled: Accessor<boolean>) => {
138
+ const checkServerHealth = useCheckServerHealth()
139
+ const [status, setStatus] = createStore({} as Record<ServerConnection.Key, ServerHealth | undefined>)
140
+
141
+ createEffect(() => {
142
+ if (!enabled()) {
143
+ setStatus(reconcile({}))
144
+ return
145
+ }
146
+ const list = servers()
147
+ let dead = false
148
+
149
+ const refresh = async () => {
150
+ const results: Record<string, ServerHealth> = {}
151
+ await Promise.all(
152
+ list.map(async (conn) => {
153
+ const key = ServerConnection.key(conn)
154
+ const result = await checkServerHealth(conn.http)
155
+ results[key] = result
156
+ if (!dead) setStatus(key, result)
157
+ }),
158
+ )
159
+ if (dead) return
160
+ setStatus(reconcile(results))
161
+ }
162
+
163
+ void refresh()
164
+ const id = setInterval(() => void refresh(), pollMs)
165
+ onCleanup(() => {
166
+ dead = true
167
+ clearInterval(id)
168
+ })
169
+ })
170
+
171
+ return status
172
+ }
@@ -0,0 +1,40 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { detectServerProtocol } from "./server-protocol"
3
+
4
+ const server = { url: "http://localhost:4096" }
5
+ const json = (value: unknown, status = 200) =>
6
+ new Response(JSON.stringify(value), { status, headers: { "content-type": "application/json" } })
7
+ const mockFetch = (run: (input: string | URL | Request) => Promise<Response>) =>
8
+ Object.assign(run, { preconnect: globalThis.fetch.preconnect })
9
+
10
+ describe("detectServerProtocol", () => {
11
+ test("prefers the legacy health endpoint when both API generations exist", async () => {
12
+ const fetcher = mockFetch((input) => {
13
+ const path = new URL(input instanceof Request ? input.url : input).pathname
14
+ if (path === "/global/health") return Promise.resolve(json({ healthy: true, version: "1.18.4" }))
15
+ return Promise.resolve(json({ healthy: true, version: "2.0.0", pid: 123 }))
16
+ })
17
+
18
+ expect(await detectServerProtocol(server, fetcher)).toBe("v1")
19
+ })
20
+
21
+ test("recognizes V2 health by its process identifier", async () => {
22
+ const fetcher = mockFetch((input) => {
23
+ const path = new URL(input instanceof Request ? input.url : input).pathname
24
+ if (path === "/global/health") return Promise.resolve(json({}, 404))
25
+ return Promise.resolve(json({ healthy: true, version: "2.0.0", pid: 123 }))
26
+ })
27
+
28
+ expect(await detectServerProtocol(server, fetcher)).toBe("v2")
29
+ })
30
+
31
+ test("recognizes the transitional V1 API health response", async () => {
32
+ const fetcher = mockFetch((input) => {
33
+ const path = new URL(input instanceof Request ? input.url : input).pathname
34
+ if (path === "/global/health") return Promise.resolve(json({}, 404))
35
+ return Promise.resolve(json({ healthy: true }))
36
+ })
37
+
38
+ expect(await detectServerProtocol(server, fetcher)).toBe("v1")
39
+ })
40
+ })
@@ -0,0 +1,35 @@
1
+ import type { ServerConnection } from "@/context/server"
2
+ import { authTokenFromCredentials } from "./server"
3
+
4
+ export type ServerProtocol = "v1" | "v2"
5
+
6
+ function headers(server: ServerConnection.HttpBase) {
7
+ if (!server.password) return
8
+ return {
9
+ Authorization: `Basic ${authTokenFromCredentials({ username: server.username, password: server.password })}`,
10
+ }
11
+ }
12
+
13
+ async function probe(server: ServerConnection.HttpBase, fetch: typeof globalThis.fetch, path: string) {
14
+ const response = await fetch(new URL(path, server.url), {
15
+ headers: headers(server),
16
+ signal: AbortSignal.timeout(5_000),
17
+ })
18
+ if (!response.ok || !response.headers.get("content-type")?.includes("application/json")) return
19
+ const value: unknown = await response.json()
20
+ if (!value || typeof value !== "object") return
21
+ return value
22
+ }
23
+
24
+ export async function detectServerProtocol(
25
+ server: ServerConnection.HttpBase,
26
+ fetch: typeof globalThis.fetch,
27
+ ): Promise<ServerProtocol> {
28
+ const legacy = await probe(server, fetch, "/global/health").catch(() => undefined)
29
+ if (legacy && "healthy" in legacy && legacy.healthy === true) return "v1"
30
+
31
+ const current = await probe(server, fetch, "/api/health").catch(() => undefined)
32
+ if (current && "pid" in current && typeof current.pid === "number") return "v2"
33
+ if (current && "healthy" in current && current.healthy === true) return "v1"
34
+ return "v2"
35
+ }
@@ -0,0 +1,61 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { ScopedKey, ServerScope, SessionRouteKey, SessionStateKey, migrateLegacySessionStateKeys } from "./server-scope"
3
+
4
+ describe("ServerScope", () => {
5
+ test("uses a stable local scope for the canonical sidecar", () => {
6
+ expect(String(ServerScope.fromServerKey("sidecar" as Parameters<typeof ServerScope.fromServerKey>[0]))).toBe(
7
+ "local",
8
+ )
9
+ })
10
+
11
+ test("keeps configured loopback servers distinct from the canonical sidecar", () => {
12
+ expect(
13
+ String(ServerScope.fromServerKey("http://localhost:4096" as Parameters<typeof ServerScope.fromServerKey>[0])),
14
+ ).toBe("http://localhost:4096")
15
+ })
16
+
17
+ test("uses a stable local scope for an explicit canonical web server", () => {
18
+ const key = "http://localhost:4096" as Parameters<typeof ServerScope.fromServerKey>[0]
19
+ expect(String(ServerScope.fromServerKey(key, key))).toBe("local")
20
+ })
21
+ })
22
+
23
+ describe("SessionStateKey", () => {
24
+ test("combines local and remote scope with route identity", () => {
25
+ const route = SessionRouteKey.fromRoute("cmVwbw", "session-1")
26
+ expect(String(SessionStateKey.from(ServerScope.local, route))).toBe("local\0cmVwbw/session-1")
27
+ expect(String(SessionStateKey.from("https://windows.example" as ServerScope, route))).toBe(
28
+ "https://windows.example\0cmVwbw/session-1",
29
+ )
30
+ expect(SessionStateKey.from("https://debian.example" as ServerScope, route)).not.toBe(
31
+ SessionStateKey.from("https://windows.example" as ServerScope, route),
32
+ )
33
+ })
34
+
35
+ test("extracts route keys from scoped and legacy state keys", () => {
36
+ expect(String(SessionStateKey.route("cmVwbw/session-1"))).toBe("cmVwbw/session-1")
37
+ expect(String(SessionStateKey.route("local\0cmVwbw/session-1"))).toBe("cmVwbw/session-1")
38
+ expect(String(SessionStateKey.route("https://debian.example\0cmVwbw/session-1"))).toBe("cmVwbw/session-1")
39
+ })
40
+ })
41
+
42
+ describe("migrateLegacySessionStateKeys", () => {
43
+ test("copies legacy route keys into local scope without overwriting scoped state", () => {
44
+ expect(
45
+ migrateLegacySessionStateKeys({
46
+ "cmVwbw/session-1": { active: "legacy" },
47
+ "local\0cmVwbw/session-1": { active: "scoped" },
48
+ "https://debian.example\0cmVwbw/session-1": { active: "remote" },
49
+ }),
50
+ ).toEqual({
51
+ "local\0cmVwbw/session-1": { active: "scoped" },
52
+ "https://debian.example\0cmVwbw/session-1": { active: "remote" },
53
+ })
54
+ })
55
+
56
+ test("rejects invalid identity fragments", () => {
57
+ expect(() => ScopedKey.from(ServerScope.local, "bad\0directory")).toThrow(
58
+ "Scoped key part cannot contain null bytes",
59
+ )
60
+ })
61
+ })
@@ -0,0 +1,73 @@
1
+ import type { ServerConnection } from "@/context/server"
2
+
3
+ export type ServerScope = string & { readonly __brand: "ServerScope" }
4
+ export type SessionRouteKey = string & { readonly __brand: "SessionRouteKey" }
5
+ export type SessionStateKey = string & { readonly __brand: "SessionStateKey" }
6
+ export type ScopedKey = string & { readonly __brand: "ScopedKey" }
7
+
8
+ const separator = "\u0000"
9
+
10
+ function fragment(label: string, value: string) {
11
+ if (value.includes(separator)) throw new Error(`${label} cannot contain null bytes`)
12
+ return value
13
+ }
14
+
15
+ function compose(scope: ServerScope, parts: string[]) {
16
+ return [fragment("Server scope", scope), ...parts.map((part) => fragment("Scoped key part", part))].join(separator)
17
+ }
18
+
19
+ export const ServerScope = {
20
+ local: "local" as ServerScope,
21
+ fromServerKey(key: ServerConnection.Key, canonicalLocalServer?: ServerConnection.Key) {
22
+ return fragment(
23
+ "Server scope",
24
+ key === "sidecar" || key === canonicalLocalServer ? ServerScope.local : key,
25
+ ) as ServerScope
26
+ },
27
+ }
28
+
29
+ export const SessionRouteKey = {
30
+ fromRoute(dir: string | undefined, sessionID?: string) {
31
+ return fragment("Session route", `${dir ?? ""}${sessionID ? "/" + sessionID : ""}`) as SessionRouteKey
32
+ },
33
+ fromLegacy(key: string) {
34
+ return fragment("Legacy session route", key) as SessionRouteKey
35
+ },
36
+ }
37
+
38
+ export const SessionStateKey = {
39
+ from(scope: ServerScope, route: SessionRouteKey) {
40
+ return compose(scope, [route]) as SessionStateKey
41
+ },
42
+ route(key: string) {
43
+ const split = key.lastIndexOf(separator)
44
+ return SessionRouteKey.fromLegacy(split === -1 ? key : key.slice(split + 1))
45
+ },
46
+ scope(key: string) {
47
+ const split = key.indexOf(separator)
48
+ if (split === -1) return ServerScope.local
49
+ return fragment("Stored server scope", key.slice(0, split)) as ServerScope
50
+ },
51
+ }
52
+
53
+ export const ScopedKey = {
54
+ from(scope: ServerScope, ...parts: string[]) {
55
+ return compose(scope, parts) as ScopedKey
56
+ },
57
+ prefix(scope: ServerScope, ...parts: string[]) {
58
+ return `${ScopedKey.from(scope, ...parts)}${separator}`
59
+ },
60
+ }
61
+
62
+ export function migrateLegacySessionStateKeys(value: unknown) {
63
+ if (!value || typeof value !== "object" || Array.isArray(value)) return value
64
+ const entries = Object.entries(value)
65
+ if (entries.every(([key]) => key.includes(separator))) return value
66
+ const scoped = Object.fromEntries(entries.filter(([key]) => key.includes(separator)))
67
+ for (const [key, item] of entries) {
68
+ if (key.includes(separator)) continue
69
+ const next = SessionStateKey.from(ServerScope.local, SessionRouteKey.fromLegacy(key))
70
+ if (!(next in scoped)) scoped[next] = item
71
+ }
72
+ return scoped
73
+ }