@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,2441 @@
1
+ import {
2
+ createEffect,
3
+ createMemo,
4
+ createResource,
5
+ For,
6
+ on,
7
+ onCleanup,
8
+ onMount,
9
+ ParentProps,
10
+ Show,
11
+ untrack,
12
+ type Accessor,
13
+ } from "solid-js"
14
+ import { makeEventListener } from "@solid-primitives/event-listener"
15
+ import { useNavigate, useParams } from "@solidjs/router"
16
+ import { useLayout, LocalProject } from "@/context/layout"
17
+ import { useServerSync } from "@/context/server-sync"
18
+ import { Persist, persisted } from "@/utils/persist"
19
+ import { base64Encode } from "@neurocode-ai/core/util/encode"
20
+ import { decode64 } from "@/utils/base64"
21
+ import { ResizeHandle } from "@neurocode-ai/ui/resize-handle"
22
+ import { Button } from "@neurocode-ai/ui/button"
23
+ import { Icon as IconV2 } from "@neurocode-ai/ui/v2/icon"
24
+ import { IconButton } from "@neurocode-ai/ui/icon-button"
25
+ import { Tooltip } from "@neurocode-ai/ui/tooltip"
26
+ import { DropdownMenu } from "@neurocode-ai/ui/dropdown-menu"
27
+ import { Dialog } from "@neurocode-ai/ui/dialog"
28
+ import { getFilename } from "@neurocode-ai/core/util/path"
29
+ import { Session } from "@neurocode-ai/sdk/v2/client"
30
+ import { usePlatform } from "@/context/platform"
31
+ import { useSettings } from "@/context/settings"
32
+ import { createStore, produce, reconcile } from "solid-js/store"
33
+ import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd"
34
+ import type { DragEvent } from "@thisbeyond/solid-dnd"
35
+ import { useProviders } from "@/hooks/use-providers"
36
+ import { toaster } from "@neurocode-ai/ui/toast"
37
+ import { setV2Toast, showToast, ToastRegion } from "@/utils/toast"
38
+ import { useServerSDK } from "@/context/server-sdk"
39
+ import { normalizeProjectInfo } from "@/context/global-sync/utils"
40
+ import { clearWorkspaceTerminals } from "@/context/terminal"
41
+ import { pickSessionCacheEvictions } from "@/context/global-sync/session-cache"
42
+ import { useNotification } from "@/context/notification"
43
+ import { usePermission } from "@/context/permission"
44
+ import { Binary } from "@neurocode-ai/core/util/binary"
45
+ import { retry } from "@neurocode-ai/core/util/retry"
46
+ import { playSoundById } from "@/utils/sound"
47
+ import { createAim } from "@/utils/aim"
48
+ import { setNavigate } from "@/utils/notification-click"
49
+ import { Worktree as WorktreeState } from "@/utils/worktree"
50
+ import { setSessionHandoff } from "@/pages/session/handoff"
51
+ import { SessionRouteKey, SessionStateKey } from "@/utils/server-scope"
52
+ import { listAllSessions } from "@/utils/session"
53
+
54
+ import { useDialog } from "@neurocode-ai/ui/context/dialog"
55
+ import { useTheme, type ColorScheme } from "@neurocode-ai/ui/theme/context"
56
+ import { useCommand, type CommandOption } from "@/context/command"
57
+ import { ConstrainDragXAxis, getDraggableId } from "@/utils/solid-dnd"
58
+ import { DebugBar } from "@/components/debug-bar"
59
+ import { TabsInfoPopup } from "@/components/help-button"
60
+ import { Titlebar, type TitlebarUpdate } from "@/components/titlebar"
61
+ import { useDirectoryPicker } from "@/components/directory-picker"
62
+ import { ServerConnection, useServer } from "@/context/server"
63
+ import { useLanguage, type Locale } from "@/context/language"
64
+ import { pathKey } from "@/utils/path-key"
65
+ import {
66
+ displayName,
67
+ effectiveWorkspaceOrder,
68
+ errorMessage,
69
+ latestRootSession,
70
+ sortedRootSessions,
71
+ } from "./layout/helpers"
72
+ import {
73
+ collectNewSessionDeepLinks,
74
+ collectOpenProjectDeepLinks,
75
+ deepLinkEvent,
76
+ drainPendingDeepLinks,
77
+ } from "./layout/deep-links"
78
+ import { createInlineEditorController } from "./layout/inline-editor"
79
+ import {
80
+ LocalWorkspace,
81
+ SortableWorkspace,
82
+ WorkspaceDragOverlay,
83
+ type WorkspaceSidebarContext,
84
+ } from "./layout/sidebar-workspace"
85
+ import { ProjectDragOverlay, SortableProject, type ProjectSidebarContext } from "./layout/sidebar-project"
86
+ import { SidebarContent } from "./layout/sidebar-shell"
87
+
88
+ export default function LegacyLayout(props: ParentProps) {
89
+ const serverSDK = useServerSDK()
90
+ const [store, setStore, , ready] = persisted(
91
+ Persist.serverGlobal(serverSDK().scope, "layout.page", ["layout.page.v1"]),
92
+ createStore({
93
+ lastProjectSession: {} as { [directory: string]: { directory: string; id: string; at: number } },
94
+ activeProject: undefined as string | undefined,
95
+ activeWorkspace: undefined as string | undefined,
96
+ workspaceOrder: {} as Record<string, string[]>,
97
+ workspaceName: {} as Record<string, string>,
98
+ workspaceBranchName: {} as Record<string, Record<string, string>>,
99
+ workspaceExpanded: {} as Record<string, boolean>,
100
+ gettingStartedDismissed: false,
101
+ }),
102
+ )
103
+
104
+ const pageReady = createMemo(() => ready())
105
+
106
+ let scrollContainerRef: HTMLDivElement | undefined
107
+ let dialogRun = 0
108
+ let dialogDead = false
109
+
110
+ const params = useParams()
111
+ const serverSync = useServerSync()
112
+ const layout = useLayout()
113
+ const layoutReady = createMemo(() => layout.ready())
114
+ const platform = usePlatform()
115
+ const pickDirectory = useDirectoryPicker()
116
+ const settings = useSettings()
117
+ const server = useServer()
118
+ const notification = useNotification()
119
+ const permission = usePermission()
120
+ const navigate = useNavigate()
121
+ setNavigate(navigate)
122
+ const providers = useProviders()
123
+ const dialog = useDialog()
124
+ const command = useCommand()
125
+ const theme = useTheme()
126
+ const language = useLanguage()
127
+ createEffect(() => setV2Toast(false))
128
+ const initialDirectory = decode64(params.dir)
129
+ const route = createMemo(() => {
130
+ const slug = params.dir
131
+ if (!slug) return { slug, dir: "" }
132
+ const dir = decode64(slug)
133
+ if (!dir) return { slug, dir: "" }
134
+ const store = serverSync().peek(dir, { bootstrap: false })
135
+ return {
136
+ slug,
137
+ store,
138
+ dir: store[0].path.directory || dir,
139
+ }
140
+ })
141
+ const availableThemeEntries = createMemo(() => theme.ids().map((id) => [id, theme.themes()[id]] as const))
142
+ const colorSchemeOrder: ColorScheme[] = ["system", "light", "dark"]
143
+ const colorSchemeKey: Record<ColorScheme, "theme.scheme.system" | "theme.scheme.light" | "theme.scheme.dark"> = {
144
+ system: "theme.scheme.system",
145
+ light: "theme.scheme.light",
146
+ dark: "theme.scheme.dark",
147
+ }
148
+ const colorSchemeLabel = (scheme: ColorScheme) => language.t(colorSchemeKey[scheme])
149
+ const currentDir = createMemo(() => route().dir)
150
+
151
+ const [state, setState] = createStore({
152
+ autoselect: !initialDirectory,
153
+ busyWorkspaces: {} as Record<string, boolean>,
154
+ hoverProject: undefined as string | undefined,
155
+ scrollSessionKey: undefined as string | undefined,
156
+ nav: undefined as HTMLElement | undefined,
157
+ sortNow: Date.now(),
158
+ sizing: false,
159
+ peek: undefined as string | undefined,
160
+ peeked: false,
161
+ debugTools: true,
162
+ })
163
+
164
+ const updateVersion = () => {
165
+ const state = platform.updater?.state()
166
+ if (state?.status !== "ready") return
167
+ return state.version
168
+ }
169
+ const installUpdate = () => void platform.updater?.install()
170
+ const titlebarUpdate: TitlebarUpdate = {
171
+ version: updateVersion,
172
+ installing: () => platform.updater?.state().status === "installing",
173
+ install: installUpdate,
174
+ }
175
+
176
+ const editor = createInlineEditorController()
177
+ const setBusy = (directory: string, value: boolean) => {
178
+ const key = pathKey(directory)
179
+ if (value) {
180
+ setState("busyWorkspaces", key, true)
181
+ return
182
+ }
183
+ setState(
184
+ "busyWorkspaces",
185
+ produce((draft) => {
186
+ delete draft[key]
187
+ }),
188
+ )
189
+ }
190
+ const isBusy = (directory: string) => !!state.busyWorkspaces[pathKey(directory)]
191
+ const navLeave = { current: undefined as number | undefined }
192
+ const sortNow = () => state.sortNow
193
+ let sizet: number | undefined
194
+ let sortNowInterval: ReturnType<typeof setInterval> | undefined
195
+ const sortNowTimeout = setTimeout(
196
+ () => {
197
+ setState("sortNow", Date.now())
198
+ sortNowInterval = setInterval(() => setState("sortNow", Date.now()), 60_000)
199
+ },
200
+ 60_000 - (Date.now() % 60_000),
201
+ )
202
+
203
+ const aim = createAim({
204
+ enabled: () => !layout.sidebar.opened(),
205
+ active: () => state.hoverProject,
206
+ el: () => state.nav?.querySelector<HTMLElement>("[data-component='sidebar-rail']") ?? state.nav,
207
+ onActivate: (directory) => {
208
+ serverSync().child(directory)
209
+ setState("hoverProject", directory)
210
+ },
211
+ })
212
+
213
+ onCleanup(() => {
214
+ dialogDead = true
215
+ dialogRun += 1
216
+ if (navLeave.current !== undefined) clearTimeout(navLeave.current)
217
+ clearTimeout(sortNowTimeout)
218
+ if (sortNowInterval) clearInterval(sortNowInterval)
219
+ if (sizet !== undefined) clearTimeout(sizet)
220
+ if (peekt !== undefined) clearTimeout(peekt)
221
+ aim.reset()
222
+ })
223
+
224
+ onMount(() => {
225
+ const stop = () => setState("sizing", false)
226
+ const blur = () => reset()
227
+ const hide = () => {
228
+ if (document.visibilityState !== "hidden") return
229
+ reset()
230
+ }
231
+ makeEventListener(window, "pointerup", stop)
232
+ makeEventListener(window, "pointercancel", stop)
233
+ makeEventListener(window, "blur", stop)
234
+ makeEventListener(window, "blur", blur)
235
+ makeEventListener(document, "visibilitychange", hide)
236
+ })
237
+
238
+ const sidebarHovering = createMemo(() => !layout.sidebar.opened() && state.hoverProject !== undefined)
239
+ const sidebarExpanded = createMemo(() => layout.sidebar.opened() || sidebarHovering())
240
+ const setHoverProject = (value: string | undefined) => {
241
+ setState("hoverProject", value)
242
+ if (value !== undefined) return
243
+ aim.reset()
244
+ }
245
+ const clearHoverProjectSoon = () => queueMicrotask(() => setHoverProject(undefined))
246
+
247
+ const disarm = () => {
248
+ if (navLeave.current === undefined) return
249
+ clearTimeout(navLeave.current)
250
+ navLeave.current = undefined
251
+ }
252
+
253
+ const reset = () => {
254
+ disarm()
255
+ setHoverProject(undefined)
256
+ }
257
+
258
+ const arm = () => {
259
+ if (layout.sidebar.opened()) return
260
+ if (state.hoverProject === undefined) return
261
+ disarm()
262
+ navLeave.current = window.setTimeout(() => {
263
+ navLeave.current = undefined
264
+ setHoverProject(undefined)
265
+ }, 300)
266
+ }
267
+
268
+ let peekt: number | undefined
269
+
270
+ const hoverProjectData = createMemo(() => {
271
+ const id = state.hoverProject
272
+ if (!id) return
273
+ return layout.projects.list().find((project) => project.worktree === id)
274
+ })
275
+
276
+ const peekProject = createMemo(() => {
277
+ const id = state.peek
278
+ if (!id) return
279
+ return layout.projects.list().find((project) => project.worktree === id)
280
+ })
281
+
282
+ createEffect(() => {
283
+ const p = hoverProjectData()
284
+ if (p) {
285
+ if (peekt !== undefined) {
286
+ clearTimeout(peekt)
287
+ peekt = undefined
288
+ }
289
+ setState("peek", p.worktree)
290
+ setState("peeked", true)
291
+ return
292
+ }
293
+
294
+ setState("peeked", false)
295
+ if (state.peek === undefined) return
296
+ if (peekt !== undefined) clearTimeout(peekt)
297
+ peekt = window.setTimeout(() => {
298
+ peekt = undefined
299
+ setState("peek", undefined)
300
+ }, 180)
301
+ })
302
+
303
+ createEffect(() => {
304
+ if (!layout.sidebar.opened()) return
305
+ setHoverProject(undefined)
306
+ })
307
+
308
+ createEffect(() => {
309
+ if (!state.autoselect) return
310
+ const dir = params.dir
311
+ if (!dir) return
312
+ const directory = decode64(dir)
313
+ if (!directory) return
314
+ setState("autoselect", false)
315
+ })
316
+
317
+ const editorOpen = editor.editorOpen
318
+ const openEditor = editor.openEditor
319
+ const closeEditor = editor.closeEditor
320
+ const setEditor = editor.setEditor
321
+ const InlineEditor = editor.InlineEditor
322
+
323
+ const clearSidebarHoverState = () => {
324
+ if (layout.sidebar.opened()) return
325
+ reset()
326
+ }
327
+
328
+ const navigateWithSidebarReset = (href: string) => {
329
+ clearSidebarHoverState()
330
+ navigate(href)
331
+ layout.mobileSidebar.hide()
332
+ }
333
+
334
+ function cycleTheme(direction = 1) {
335
+ const ids = availableThemeEntries().map(([id]) => id)
336
+ if (ids.length === 0) return
337
+ const currentIndex = ids.indexOf(theme.themeId())
338
+ const nextIndex = currentIndex === -1 ? 0 : (currentIndex + direction + ids.length) % ids.length
339
+ const nextThemeId = ids[nextIndex]
340
+ theme.setTheme(nextThemeId)
341
+ showToast({
342
+ title: language.t("toast.theme.title"),
343
+ description: theme.name(nextThemeId),
344
+ })
345
+ }
346
+
347
+ function cycleColorScheme(direction = 1) {
348
+ const current = theme.colorScheme()
349
+ const currentIndex = colorSchemeOrder.indexOf(current)
350
+ const nextIndex =
351
+ currentIndex === -1 ? 0 : (currentIndex + direction + colorSchemeOrder.length) % colorSchemeOrder.length
352
+ const next = colorSchemeOrder[nextIndex]
353
+ theme.setColorScheme(next)
354
+ showToast({
355
+ title: language.t("toast.scheme.title"),
356
+ description: colorSchemeLabel(next),
357
+ })
358
+ }
359
+
360
+ function setLocale(next: Locale) {
361
+ if (next === language.locale()) return
362
+ language.setLocale(next)
363
+ showToast({
364
+ title: language.t("toast.language.title"),
365
+ description: language.t("toast.language.description", { language: language.label(next) }),
366
+ })
367
+ }
368
+
369
+ function cycleLanguage(direction = 1) {
370
+ const locales = language.locales
371
+ const currentIndex = locales.indexOf(language.locale())
372
+ const nextIndex = currentIndex === -1 ? 0 : (currentIndex + direction + locales.length) % locales.length
373
+ const next = locales[nextIndex]
374
+ if (!next) return
375
+ setLocale(next)
376
+ }
377
+
378
+ const useSDKNotificationToasts = () =>
379
+ onMount(() => {
380
+ const toastBySession = new Map<string, number>()
381
+ const alertedAtBySession = new Map<string, number>()
382
+ const cooldownMs = 5000
383
+
384
+ const dismissSessionAlert = (sessionKey: string) => {
385
+ const toastId = toastBySession.get(sessionKey)
386
+ if (toastId === undefined) return
387
+ toaster.dismiss(toastId)
388
+ toastBySession.delete(sessionKey)
389
+ alertedAtBySession.delete(sessionKey)
390
+ }
391
+
392
+ const unsub = serverSDK().event.listen((e) => {
393
+ if (e.details?.type === "worktree.ready") {
394
+ setBusy(e.name, false)
395
+ WorktreeState.ready(serverSDK().scope, e.name)
396
+ return
397
+ }
398
+
399
+ if (e.details?.type === "worktree.failed") {
400
+ setBusy(e.name, false)
401
+ WorktreeState.failed(
402
+ serverSDK().scope,
403
+ e.name,
404
+ e.details.properties?.message ?? language.t("common.requestFailed"),
405
+ )
406
+ return
407
+ }
408
+
409
+ if (
410
+ e.details?.type === "question.replied" ||
411
+ e.details?.type === "question.rejected" ||
412
+ e.details?.type === "permission.replied"
413
+ ) {
414
+ const props = e.details.properties as { sessionID: string }
415
+ const sessionKey = `${e.name}:${props.sessionID}`
416
+ dismissSessionAlert(sessionKey)
417
+ return
418
+ }
419
+
420
+ if (e.details?.type !== "permission.asked" && e.details?.type !== "question.asked") return
421
+ const title =
422
+ e.details.type === "permission.asked"
423
+ ? language.t("notification.permission.title")
424
+ : language.t("notification.question.title")
425
+ const icon = e.details.type === "permission.asked" ? ("checklist" as const) : ("bubble-5" as const)
426
+ const directory = e.name
427
+ const props = e.details.properties
428
+ if (e.details.type === "permission.asked" && permission.autoResponds(e.details.properties, directory)) return
429
+
430
+ const [store] = serverSync().child(directory, { bootstrap: false })
431
+ const session = store.session.find((s) => s.id === props.sessionID)
432
+ const sessionKey = `${directory}:${props.sessionID}`
433
+
434
+ const sessionTitle = session?.title ?? language.t("command.session.new")
435
+ const projectName = getFilename(directory)
436
+ const description =
437
+ e.details.type === "permission.asked"
438
+ ? language.t("notification.permission.description", { sessionTitle, projectName })
439
+ : language.t("notification.question.description", { sessionTitle, projectName })
440
+ const href = `/${base64Encode(directory)}/session/${props.sessionID}`
441
+
442
+ const now = Date.now()
443
+ const lastAlerted = alertedAtBySession.get(sessionKey) ?? 0
444
+ if (now - lastAlerted < cooldownMs) return
445
+ alertedAtBySession.set(sessionKey, now)
446
+
447
+ if (e.details.type === "permission.asked") {
448
+ if (settings.sounds.permissionsEnabled()) {
449
+ void playSoundById(settings.sounds.permissions())
450
+ }
451
+ if (settings.notifications.permissions()) {
452
+ void platform.notify(title, description, href)
453
+ }
454
+ }
455
+
456
+ if (e.details.type === "question.asked") {
457
+ if (settings.notifications.agent()) {
458
+ void platform.notify(title, description, href)
459
+ }
460
+ }
461
+
462
+ const currentSession = params.id
463
+ if (pathKey(directory) === pathKey(currentDir()) && props.sessionID === currentSession) return
464
+ if (pathKey(directory) === pathKey(currentDir()) && session?.parentID === currentSession) return
465
+
466
+ dismissSessionAlert(sessionKey)
467
+
468
+ const toastId = showToast({
469
+ persistent: true,
470
+ icon,
471
+ title,
472
+ description,
473
+ actions: [
474
+ {
475
+ label: language.t("notification.action.goToSession"),
476
+ onClick: () => navigate(href),
477
+ },
478
+ {
479
+ label: language.t("common.dismiss"),
480
+ onClick: "dismiss",
481
+ },
482
+ ],
483
+ })
484
+ toastBySession.set(sessionKey, toastId)
485
+ })
486
+ onCleanup(unsub)
487
+
488
+ createEffect(() => {
489
+ const currentSession = params.id
490
+ if (!currentDir() || !currentSession) return
491
+ const sessionKey = `${currentDir()}:${currentSession}`
492
+ dismissSessionAlert(sessionKey)
493
+ const [store] = serverSync().child(currentDir(), { bootstrap: false })
494
+ const childSessions = store.session.filter((s) => s.parentID === currentSession)
495
+ for (const child of childSessions) {
496
+ dismissSessionAlert(`${currentDir()}:${child.id}`)
497
+ }
498
+ })
499
+ })
500
+
501
+ useSDKNotificationToasts()
502
+
503
+ function scrollToSession(sessionId: string, sessionKey: string) {
504
+ if (!scrollContainerRef) return
505
+ if (state.scrollSessionKey === sessionKey) return
506
+ const element = scrollContainerRef.querySelector(`[data-session-id="${sessionId}"]`)
507
+ if (!element) return
508
+ const containerRect = scrollContainerRef.getBoundingClientRect()
509
+ const elementRect = element.getBoundingClientRect()
510
+ if (elementRect.top >= containerRect.top && elementRect.bottom <= containerRect.bottom) {
511
+ setState("scrollSessionKey", sessionKey)
512
+ return
513
+ }
514
+ setState("scrollSessionKey", sessionKey)
515
+ element.scrollIntoView({ block: "nearest", behavior: "smooth" })
516
+ }
517
+
518
+ const currentProject = createMemo(() => {
519
+ const directory = currentDir()
520
+ if (!directory) return
521
+ const key = pathKey(directory)
522
+
523
+ const projects = layout.projects.list()
524
+
525
+ const sandbox = projects.find((p) => p.sandboxes?.some((item) => pathKey(item) === key))
526
+ if (sandbox) return sandbox
527
+
528
+ const direct = projects.find((p) => pathKey(p.worktree) === key)
529
+ if (direct) return direct
530
+
531
+ const [child] = serverSync().child(directory, { bootstrap: false })
532
+ const id = child.project
533
+ if (!id) return
534
+
535
+ const meta = serverSync().data.project.find((p) => p.id === id)
536
+ const root = meta?.worktree
537
+ if (!root) return
538
+
539
+ return projects.find((p) => p.worktree === root)
540
+ })
541
+
542
+ const [autoselecting] = createResource(async () => {
543
+ await ready.promise
544
+ await layout.ready.promise
545
+ if (!untrack(() => state.autoselect)) return
546
+
547
+ const list = layout.projects.list()
548
+ const last = server.projects.last()
549
+
550
+ if (list.length === 0) {
551
+ if (!last) return
552
+ await openProject(last, true)
553
+ } else {
554
+ const next = list.find((project) => project.worktree === last) ?? list[0]
555
+ if (!next) return
556
+ await openProject(next.worktree, true)
557
+ }
558
+ })
559
+
560
+ const workspaceName = (directory: string, projectId?: string, branch?: string) => {
561
+ const key = pathKey(directory)
562
+ const direct = store.workspaceName[key] ?? store.workspaceName[directory]
563
+ if (direct) return direct
564
+ if (!projectId) return
565
+ if (!branch) return
566
+ return store.workspaceBranchName[projectId]?.[branch]
567
+ }
568
+
569
+ const setWorkspaceName = (directory: string, next: string, projectId?: string, branch?: string) => {
570
+ const key = pathKey(directory)
571
+ setStore("workspaceName", key, next)
572
+ if (!projectId) return
573
+ if (!branch) return
574
+ if (!store.workspaceBranchName[projectId]) {
575
+ setStore("workspaceBranchName", projectId, {})
576
+ }
577
+ setStore("workspaceBranchName", projectId, branch, next)
578
+ }
579
+
580
+ const workspaceLabel = (directory: string, branch?: string, projectId?: string) =>
581
+ workspaceName(directory, projectId, branch) ?? branch ?? getFilename(directory)
582
+
583
+ const workspaceSetting = createMemo(() => {
584
+ const project = currentProject()
585
+ if (!project) return false
586
+ if (project.vcs !== "git") return false
587
+ return layout.sidebar.workspaces(project.worktree)()
588
+ })
589
+
590
+ const visibleSessionDirs = createMemo(() => {
591
+ const project = currentProject()
592
+ if (!project) return [] as string[]
593
+ if (!workspaceSetting()) return [project.worktree]
594
+
595
+ const activeDir = currentDir()
596
+ return workspaceIds(project).filter((directory) => {
597
+ const expanded = store.workspaceExpanded[directory] ?? directory === project.worktree
598
+ const active = pathKey(directory) === pathKey(activeDir)
599
+ return expanded || active
600
+ })
601
+ })
602
+
603
+ createEffect(() => {
604
+ if (!pageReady()) return
605
+ if (!layoutReady()) return
606
+ const projects = layout.projects.list()
607
+ for (const [directory, expanded] of Object.entries(store.workspaceExpanded)) {
608
+ if (!expanded) continue
609
+ const key = pathKey(directory)
610
+ const project = projects.find(
611
+ (item) => pathKey(item.worktree) === key || item.sandboxes?.some((sandbox) => pathKey(sandbox) === key),
612
+ )
613
+ if (!project) continue
614
+ if (project.vcs === "git" && layout.sidebar.workspaces(project.worktree)()) continue
615
+ setStore("workspaceExpanded", directory, false)
616
+ }
617
+ })
618
+
619
+ const currentSessions = createMemo(() => {
620
+ const now = Date.now()
621
+ const dirs = visibleSessionDirs()
622
+ if (dirs.length === 0) return [] as Session[]
623
+
624
+ const result: Session[] = []
625
+ for (const dir of dirs) {
626
+ const [dirStore] = serverSync().child(dir, { bootstrap: true })
627
+ const dirSessions = sortedRootSessions(dirStore, now)
628
+ result.push(...dirSessions)
629
+ }
630
+ return result
631
+ })
632
+
633
+ type PrefetchQueue = {
634
+ inflight: Set<string>
635
+ pending: string[]
636
+ pendingSet: Set<string>
637
+ running: number
638
+ }
639
+
640
+ const prefetchChunk = 200
641
+ const prefetchConcurrency = 2
642
+ const prefetchPendingLimit = 10
643
+ const span = 4
644
+ const prefetchToken = { value: 0 }
645
+ const prefetchQueues = new Map<string, PrefetchQueue>()
646
+
647
+ const PREFETCH_MAX_SESSIONS_PER_DIR = 10
648
+ const prefetchedByDir = new Map<string, Set<string>>()
649
+
650
+ const lruFor = (directory: string) => {
651
+ const existing = prefetchedByDir.get(directory)
652
+ if (existing) return existing
653
+ const created = new Set<string>()
654
+ prefetchedByDir.set(directory, created)
655
+ return created
656
+ }
657
+
658
+ const markPrefetched = (directory: string, sessionID: string) => {
659
+ const lru = lruFor(directory)
660
+ return pickSessionCacheEvictions({
661
+ seen: lru,
662
+ keep: sessionID,
663
+ limit: PREFETCH_MAX_SESSIONS_PER_DIR,
664
+ preserve: params.id && pathKey(directory) === pathKey(currentDir()) ? [params.id] : undefined,
665
+ })
666
+ }
667
+
668
+ createEffect(() => {
669
+ const active = new Set(visibleSessionDirs())
670
+ for (const directory of prefetchedByDir.keys()) {
671
+ if (active.has(directory)) continue
672
+ prefetchedByDir.delete(directory)
673
+ }
674
+ })
675
+
676
+ createEffect(() => {
677
+ route()
678
+ serverSDK().url
679
+
680
+ prefetchToken.value += 1
681
+ prefetchQueues.clear()
682
+ })
683
+
684
+ createEffect(() => {
685
+ const visible = new Set(visibleSessionDirs())
686
+ for (const [directory, q] of prefetchQueues) {
687
+ if (visible.has(directory)) continue
688
+ q.pending.length = 0
689
+ q.pendingSet.clear()
690
+ if (q.running === 0) prefetchQueues.delete(directory)
691
+ }
692
+ })
693
+
694
+ const queueFor = (directory: string) => {
695
+ const existing = prefetchQueues.get(directory)
696
+ if (existing) return existing
697
+
698
+ const created: PrefetchQueue = {
699
+ inflight: new Set(),
700
+ pending: [],
701
+ pendingSet: new Set(),
702
+ running: 0,
703
+ }
704
+ prefetchQueues.set(directory, created)
705
+ return created
706
+ }
707
+
708
+ async function prefetchMessages(directory: string, sessionID: string, token: number) {
709
+ await serverSync()
710
+ .session.prefetch(sessionID, prefetchChunk)
711
+ .catch(() => {})
712
+ if (prefetchToken.value !== token) return
713
+ for (const stale of markPrefetched(directory, sessionID)) serverSync().session.evict(stale)
714
+ }
715
+
716
+ const pumpPrefetch = (directory: string) => {
717
+ const q = queueFor(directory)
718
+ if (q.running >= prefetchConcurrency) return
719
+
720
+ const sessionID = q.pending.shift()
721
+ if (!sessionID) return
722
+
723
+ q.pendingSet.delete(sessionID)
724
+ q.inflight.add(sessionID)
725
+ q.running += 1
726
+
727
+ const token = prefetchToken.value
728
+
729
+ void prefetchMessages(directory, sessionID, token).finally(() => {
730
+ q.running -= 1
731
+ q.inflight.delete(sessionID)
732
+ pumpPrefetch(directory)
733
+ })
734
+ }
735
+
736
+ const prefetchSession = (session: Session, priority: "high" | "low" = "low") => {
737
+ const directory = session.directory
738
+ if (!directory) return
739
+
740
+ const cached = untrack(() => !serverSync().session.shouldPrefetch(session.id, prefetchChunk))
741
+ if (cached) return
742
+
743
+ const q = queueFor(directory)
744
+ if (q.inflight.has(session.id)) return
745
+ if (q.pendingSet.has(session.id)) {
746
+ if (priority !== "high") return
747
+ const index = q.pending.indexOf(session.id)
748
+ if (index > 0) {
749
+ q.pending.splice(index, 1)
750
+ q.pending.unshift(session.id)
751
+ }
752
+ return
753
+ }
754
+
755
+ const lru = lruFor(directory)
756
+ const known = lru.has(session.id)
757
+ if (!known && lru.size >= PREFETCH_MAX_SESSIONS_PER_DIR && priority !== "high") return
758
+
759
+ if (priority === "high") q.pending.unshift(session.id)
760
+ if (priority !== "high") q.pending.push(session.id)
761
+ q.pendingSet.add(session.id)
762
+
763
+ while (q.pending.length > prefetchPendingLimit) {
764
+ const dropped = q.pending.pop()
765
+ if (!dropped) continue
766
+ q.pendingSet.delete(dropped)
767
+ }
768
+
769
+ pumpPrefetch(directory)
770
+ }
771
+
772
+ const warm = (sessions: Session[], index: number) => {
773
+ for (let offset = 1; offset <= span; offset++) {
774
+ const next = sessions[index + offset]
775
+ if (next) prefetchSession(next, offset === 1 ? "high" : "low")
776
+
777
+ const prev = sessions[index - offset]
778
+ if (prev) prefetchSession(prev, offset === 1 ? "high" : "low")
779
+ }
780
+ }
781
+
782
+ createEffect(() => {
783
+ const sessions = currentSessions()
784
+ if (sessions.length === 0) return
785
+
786
+ const index = params.id ? sessions.findIndex((s) => s.id === params.id) : 0
787
+ if (index === -1) return
788
+
789
+ if (!params.id) {
790
+ const first = sessions[index]
791
+ if (first) prefetchSession(first, "high")
792
+ }
793
+
794
+ warm(sessions, index)
795
+ })
796
+
797
+ function navigateSessionByOffset(offset: number) {
798
+ const sessions = currentSessions()
799
+ if (sessions.length === 0) return
800
+
801
+ const sessionIndex = params.id ? sessions.findIndex((s) => s.id === params.id) : -1
802
+
803
+ let targetIndex: number
804
+ if (sessionIndex === -1) {
805
+ targetIndex = offset > 0 ? 0 : sessions.length - 1
806
+ } else {
807
+ targetIndex = (sessionIndex + offset + sessions.length) % sessions.length
808
+ }
809
+
810
+ const session = sessions[targetIndex]
811
+ if (!session) return
812
+
813
+ prefetchSession(session, "high")
814
+ warm(sessions, targetIndex)
815
+
816
+ navigateToSession(session)
817
+ }
818
+
819
+ function navigateProjectByOffset(offset: number) {
820
+ const projects = layout.projects.list()
821
+ if (projects.length === 0) return
822
+
823
+ const current = currentProject()?.worktree
824
+ const fallback = currentDir() ? projectRoot(currentDir()) : undefined
825
+ const active = current ?? fallback
826
+ const index = active ? projects.findIndex((project) => project.worktree === active) : -1
827
+
828
+ const target =
829
+ index === -1
830
+ ? offset > 0
831
+ ? projects[0]
832
+ : projects[projects.length - 1]
833
+ : projects[(index + offset + projects.length) % projects.length]
834
+ if (!target) return
835
+
836
+ // warm up child store to prevent flicker
837
+ serverSync().child(target.worktree)
838
+ void openProject(target.worktree)
839
+ }
840
+
841
+ function navigateToProjectIndex(index: number) {
842
+ const projects = layout.projects.list()
843
+ const target = projects[index]
844
+ if (!target) return
845
+
846
+ serverSync().child(target.worktree)
847
+ void openProject(target.worktree)
848
+ }
849
+
850
+ function navigateSessionByUnseen(offset: number) {
851
+ const sessions = currentSessions()
852
+ if (sessions.length === 0) return
853
+
854
+ const hasUnseen = sessions.some((session) => notification.session.unseenCount(session.id) > 0)
855
+ if (!hasUnseen) return
856
+
857
+ const activeIndex = params.id ? sessions.findIndex((s) => s.id === params.id) : -1
858
+ const start = activeIndex === -1 ? (offset > 0 ? -1 : 0) : activeIndex
859
+
860
+ for (let i = 1; i <= sessions.length; i++) {
861
+ const index = offset > 0 ? (start + i) % sessions.length : (start - i + sessions.length) % sessions.length
862
+ const session = sessions[index]
863
+ if (!session) continue
864
+ if (notification.session.unseenCount(session.id) === 0) continue
865
+
866
+ prefetchSession(session, "high")
867
+ warm(sessions, index)
868
+
869
+ navigateToSession(session)
870
+ return
871
+ }
872
+ }
873
+
874
+ async function archiveSession(session: Session) {
875
+ const [store, setStore] = serverSync().child(session.directory)
876
+ const sessions = store.session ?? []
877
+ const index = sessions.findIndex((s) => s.id === session.id)
878
+ const nextSession = sessions[index + 1] ?? sessions[index - 1]
879
+
880
+ await serverSDK().api.session.archive({ sessionID: session.id, directory: session.directory })
881
+ setStore(
882
+ produce((draft) => {
883
+ const match = Binary.search(draft.session, session.id, (s) => s.id)
884
+ if (match.found) draft.session.splice(match.index, 1)
885
+ }),
886
+ )
887
+ if (session.id === params.id) {
888
+ if (nextSession) {
889
+ navigate(`/${params.dir}/session/${nextSession.id}`)
890
+ } else {
891
+ navigate(`/${params.dir}/session`)
892
+ }
893
+ }
894
+ }
895
+
896
+ command.register("layout", () => {
897
+ const commands: CommandOption[] = [
898
+ {
899
+ id: "sidebar.toggle",
900
+ title: language.t("command.sidebar.toggle"),
901
+ category: language.t("command.category.view"),
902
+ keybind: "mod+b",
903
+ onSelect: () => layout.sidebar.toggle(),
904
+ },
905
+ {
906
+ id: "project.open",
907
+ title: language.t("command.project.open"),
908
+ category: language.t("command.category.project"),
909
+ keybind: "mod+o",
910
+ onSelect: () => chooseProject(),
911
+ },
912
+ {
913
+ id: "project.previous",
914
+ title: language.t("command.project.previous"),
915
+ category: language.t("command.category.project"),
916
+ keybind: "mod+alt+arrowup",
917
+ onSelect: () => navigateProjectByOffset(-1),
918
+ },
919
+ {
920
+ id: "project.next",
921
+ title: language.t("command.project.next"),
922
+ category: language.t("command.category.project"),
923
+ keybind: "mod+alt+arrowdown",
924
+ onSelect: () => navigateProjectByOffset(1),
925
+ },
926
+ {
927
+ id: "provider.connect",
928
+ title: language.t("command.provider.connect"),
929
+ category: language.t("command.category.provider"),
930
+ onSelect: () => connectProvider(),
931
+ },
932
+ {
933
+ id: "server.switch",
934
+ title: language.t("command.server.switch"),
935
+ category: language.t("command.category.server"),
936
+ onSelect: () => openServer(),
937
+ },
938
+ {
939
+ id: "settings.open",
940
+ title: language.t("command.settings.open"),
941
+ category: language.t("command.category.settings"),
942
+ keybind: "mod+comma",
943
+ onSelect: () => openSettings(),
944
+ },
945
+ {
946
+ id: "session.previous",
947
+ title: language.t("command.session.previous"),
948
+ category: language.t("command.category.session"),
949
+ keybind: "alt+arrowup",
950
+ onSelect: () => navigateSessionByOffset(-1),
951
+ },
952
+ {
953
+ id: "session.next",
954
+ title: language.t("command.session.next"),
955
+ category: language.t("command.category.session"),
956
+ keybind: "alt+arrowdown",
957
+ onSelect: () => navigateSessionByOffset(1),
958
+ },
959
+ {
960
+ id: "session.previous.unseen",
961
+ title: language.t("command.session.previous.unseen"),
962
+ category: language.t("command.category.session"),
963
+ keybind: "shift+alt+arrowup",
964
+ onSelect: () => navigateSessionByUnseen(-1),
965
+ },
966
+ {
967
+ id: "session.next.unseen",
968
+ title: language.t("command.session.next.unseen"),
969
+ category: language.t("command.category.session"),
970
+ keybind: "shift+alt+arrowdown",
971
+ onSelect: () => navigateSessionByUnseen(1),
972
+ },
973
+ {
974
+ id: "session.archive",
975
+ title: language.t("command.session.archive"),
976
+ category: language.t("command.category.session"),
977
+ keybind: "mod+shift+backspace",
978
+ disabled: !params.dir || !params.id,
979
+ onSelect: () => {
980
+ const session = currentSessions().find((s) => s.id === params.id)
981
+ if (session) void archiveSession(session)
982
+ },
983
+ },
984
+ {
985
+ id: "workspace.new",
986
+ title: language.t("workspace.new"),
987
+ category: language.t("command.category.workspace"),
988
+ keybind: "mod+shift+w",
989
+ disabled: !workspaceSetting(),
990
+ onSelect: () => {
991
+ const project = currentProject()
992
+ if (!project) return
993
+ return createWorkspace(project)
994
+ },
995
+ },
996
+ {
997
+ id: "workspace.toggle",
998
+ title: language.t("command.workspace.toggle"),
999
+ description: language.t("command.workspace.toggle.description"),
1000
+ category: language.t("command.category.workspace"),
1001
+ slash: "workspace",
1002
+ disabled: !currentProject() || currentProject()?.vcs !== "git",
1003
+ onSelect: () => {
1004
+ const project = currentProject()
1005
+ if (!project) return
1006
+ if (project.vcs !== "git") return
1007
+ const wasEnabled = layout.sidebar.workspaces(project.worktree)()
1008
+ layout.sidebar.toggleWorkspaces(project.worktree)
1009
+ showToast({
1010
+ title: wasEnabled
1011
+ ? language.t("toast.workspace.disabled.title")
1012
+ : language.t("toast.workspace.enabled.title"),
1013
+ description: wasEnabled
1014
+ ? language.t("toast.workspace.disabled.description")
1015
+ : language.t("toast.workspace.enabled.description"),
1016
+ })
1017
+ },
1018
+ },
1019
+ {
1020
+ id: "theme.cycle",
1021
+ title: language.t("command.theme.cycle"),
1022
+ category: language.t("command.category.theme"),
1023
+ keybind: "mod+shift+t",
1024
+ onSelect: () => cycleTheme(1),
1025
+ },
1026
+ ]
1027
+
1028
+ Array.from({ length: 9 }, (_, i) => {
1029
+ const index = i
1030
+ const number = index + 1
1031
+ commands.push({
1032
+ id: `project.${number}`,
1033
+ category: language.t("command.category.project"),
1034
+ title: `Open Project {number}`,
1035
+ keybind: `mod+${number}`,
1036
+ disabled: layout.projects.list().length <= index,
1037
+ hidden: true,
1038
+ onSelect: () => navigateToProjectIndex(index),
1039
+ })
1040
+ })
1041
+
1042
+ for (const [id] of availableThemeEntries()) {
1043
+ commands.push({
1044
+ id: `theme.set.${id}`,
1045
+ title: language.t("command.theme.set", { theme: theme.name(id) }),
1046
+ category: language.t("command.category.theme"),
1047
+ onSelect: () => theme.commitPreview(),
1048
+ onHighlight: () => {
1049
+ theme.previewTheme(id)
1050
+ return () => theme.cancelPreview()
1051
+ },
1052
+ })
1053
+ }
1054
+
1055
+ commands.push({
1056
+ id: "theme.scheme.cycle",
1057
+ title: language.t("command.theme.scheme.cycle"),
1058
+ category: language.t("command.category.theme"),
1059
+ keybind: "mod+shift+s",
1060
+ onSelect: () => cycleColorScheme(1),
1061
+ })
1062
+
1063
+ for (const scheme of colorSchemeOrder) {
1064
+ commands.push({
1065
+ id: `theme.scheme.${scheme}`,
1066
+ title: language.t("command.theme.scheme.set", { scheme: colorSchemeLabel(scheme) }),
1067
+ category: language.t("command.category.theme"),
1068
+ onSelect: () => theme.commitPreview(),
1069
+ onHighlight: () => {
1070
+ theme.previewColorScheme(scheme)
1071
+ return () => theme.cancelPreview()
1072
+ },
1073
+ })
1074
+ }
1075
+
1076
+ commands.push({
1077
+ id: "language.cycle",
1078
+ title: language.t("command.language.cycle"),
1079
+ category: language.t("command.category.language"),
1080
+ onSelect: () => cycleLanguage(1),
1081
+ })
1082
+
1083
+ for (const locale of language.locales) {
1084
+ commands.push({
1085
+ id: `language.set.${locale}`,
1086
+ title: language.t("command.language.set", { language: language.label(locale) }),
1087
+ category: language.t("command.category.language"),
1088
+ onSelect: () => setLocale(locale),
1089
+ })
1090
+ }
1091
+
1092
+ return commands
1093
+ })
1094
+
1095
+ function connectProvider() {
1096
+ const run = ++dialogRun
1097
+ void import("@/components/dialog-connect-provider").then((x) => {
1098
+ if (dialogDead || dialogRun !== run) return
1099
+ void dialog.show(() => <x.DialogConnectProvider />)
1100
+ })
1101
+ }
1102
+
1103
+ function openServer() {
1104
+ const run = ++dialogRun
1105
+ void import("@/components/dialog-select-server").then((x) => {
1106
+ if (dialogDead || dialogRun !== run) return
1107
+ dialog.show(() => <x.DialogSelectServer />)
1108
+ })
1109
+ }
1110
+
1111
+ function openSettings() {
1112
+ const run = ++dialogRun
1113
+ const module = settings.general.newLayoutDesigns()
1114
+ ? import("@/components/settings-v2")
1115
+ : import("@/components/dialog-settings")
1116
+ void module.then((x) => {
1117
+ if (dialogDead || dialogRun !== run) return
1118
+ dialog.show(() => <x.DialogSettings />)
1119
+ })
1120
+ }
1121
+
1122
+ function projectRoot(directory: string) {
1123
+ const key = pathKey(directory)
1124
+ const project = layout.projects
1125
+ .list()
1126
+ .find((item) => pathKey(item.worktree) === key || item.sandboxes?.some((sandbox) => pathKey(sandbox) === key))
1127
+ if (project) return project.worktree
1128
+
1129
+ const known = Object.entries(store.workspaceOrder).find(
1130
+ ([root, dirs]) => pathKey(root) === key || dirs.some((item) => pathKey(item) === key),
1131
+ )
1132
+ if (known) return known[0]
1133
+
1134
+ const [child] = serverSync().child(directory, { bootstrap: false })
1135
+ const id = child.project
1136
+ if (!id) return directory
1137
+
1138
+ const meta = serverSync().data.project.find((item) => item.id === id)
1139
+ return meta?.worktree ?? directory
1140
+ }
1141
+
1142
+ function activeProjectRoot(directory: string) {
1143
+ return currentProject()?.worktree ?? projectRoot(directory)
1144
+ }
1145
+
1146
+ function rememberSessionRoute(directory: string, id: string, root = activeProjectRoot(directory)) {
1147
+ setStore("lastProjectSession", root, { directory, id, at: Date.now() })
1148
+ return root
1149
+ }
1150
+
1151
+ function clearLastProjectSession(root: string) {
1152
+ if (!store.lastProjectSession[root]) return
1153
+ setStore(
1154
+ "lastProjectSession",
1155
+ produce((draft) => {
1156
+ delete draft[root]
1157
+ }),
1158
+ )
1159
+ }
1160
+
1161
+ function syncSessionRoute(directory: string, id: string, root = activeProjectRoot(directory)) {
1162
+ rememberSessionRoute(directory, id, root)
1163
+ notification.session.markViewed(id)
1164
+ const expanded = untrack(() => store.workspaceExpanded[directory])
1165
+ if (expanded === false) {
1166
+ setStore("workspaceExpanded", directory, true)
1167
+ }
1168
+ requestAnimationFrame(() => scrollToSession(id, `${directory}:${id}`))
1169
+ return root
1170
+ }
1171
+
1172
+ async function navigateToProject(directory: string | undefined) {
1173
+ if (!directory) return
1174
+ const root = projectRoot(directory)
1175
+ server.projects.touch(root)
1176
+ const project = layout.projects.list().find((item) => item.worktree === root)
1177
+ let dirs = project
1178
+ ? effectiveWorkspaceOrder(root, [root, ...(project.sandboxes ?? [])], store.workspaceOrder[root])
1179
+ : [root]
1180
+ const canOpen = (value: string | undefined) => {
1181
+ if (!value) return false
1182
+ return dirs.some((item) => pathKey(item) === pathKey(value))
1183
+ }
1184
+ const refreshDirs = async (target?: string) => {
1185
+ if (!target || target === root || canOpen(target)) return canOpen(target)
1186
+ const listed = await Promise.resolve(
1187
+ project?.id ?? serverSDK().api.project.current({ location: { directory: root } }),
1188
+ )
1189
+ .then((value) => (typeof value === "string" ? value : value.id))
1190
+ .then((projectID) => serverSDK().api.project.directories({ projectID, location: { directory: root } }))
1191
+ .then((items) => items.map((item) => item.directory).filter((item) => pathKey(item) !== pathKey(root)))
1192
+ .catch(() => [] as string[])
1193
+ dirs = effectiveWorkspaceOrder(root, [root, ...listed], store.workspaceOrder[root])
1194
+ return canOpen(target)
1195
+ }
1196
+ const openSession = async (target: { directory: string; id: string }) => {
1197
+ if (!canOpen(target.directory)) return false
1198
+ const sync = serverSync().ensureDirSyncContext(target.directory)
1199
+ if (sync.session.get(target.id)) {
1200
+ setStore("lastProjectSession", root, { directory: target.directory, id: target.id, at: Date.now() })
1201
+ navigateWithSidebarReset(`/${base64Encode(target.directory)}/session/${target.id}`)
1202
+ return true
1203
+ }
1204
+ const resolved = await sync.session
1205
+ .sync(target.id)
1206
+ .then(() => sync.session.get(target.id))
1207
+ .catch(() => undefined)
1208
+ if (!resolved?.directory) return false
1209
+ if (!canOpen(resolved.directory)) return false
1210
+ setStore("lastProjectSession", root, { directory: resolved.directory, id: resolved.id, at: Date.now() })
1211
+ navigateWithSidebarReset(`/${base64Encode(resolved.directory)}/session/${resolved.id}`)
1212
+ return true
1213
+ }
1214
+
1215
+ const projectSession = store.lastProjectSession[root]
1216
+ if (projectSession?.id) {
1217
+ await refreshDirs(projectSession.directory)
1218
+ const opened = await openSession(projectSession)
1219
+ if (opened) return
1220
+ clearLastProjectSession(root)
1221
+ }
1222
+
1223
+ const latest = latestRootSession(
1224
+ dirs.map((item) => serverSync().child(item, { bootstrap: false })[0]),
1225
+ Date.now(),
1226
+ )
1227
+ if (latest && (await openSession(latest))) {
1228
+ return
1229
+ }
1230
+
1231
+ const fetched = latestRootSession(
1232
+ await Promise.all(
1233
+ dirs.map(async (item) => ({
1234
+ path: { directory: item },
1235
+ session: await listAllSessions(serverSDK().api.session, {
1236
+ directory: item,
1237
+ parentID: null,
1238
+ order: "desc",
1239
+ }).catch(() => []),
1240
+ })),
1241
+ ),
1242
+ Date.now(),
1243
+ )
1244
+ if (fetched && (await openSession(fetched))) {
1245
+ return
1246
+ }
1247
+
1248
+ navigateWithSidebarReset(`/${base64Encode(root)}/session`)
1249
+ }
1250
+
1251
+ function navigateToSession(session: Session | undefined) {
1252
+ if (!session) return
1253
+ navigateWithSidebarReset(`/${base64Encode(session.directory)}/session/${session.id}`)
1254
+ }
1255
+
1256
+ function openProject(directory: string, navigate = true) {
1257
+ layout.projects.open(directory)
1258
+ if (navigate) return navigateToProject(directory)
1259
+ }
1260
+
1261
+ const handleDeepLinks = (urls: string[]) => {
1262
+ if (!server.isLocal()) return
1263
+
1264
+ for (const directory of collectOpenProjectDeepLinks(urls)) {
1265
+ void openProject(directory)
1266
+ }
1267
+
1268
+ for (const link of collectNewSessionDeepLinks(urls)) {
1269
+ void openProject(link.directory, false)
1270
+ const slug = base64Encode(link.directory)
1271
+ if (link.prompt) {
1272
+ setSessionHandoff(SessionStateKey.from(server.scope(), SessionRouteKey.fromLegacy(slug)), {
1273
+ prompt: link.prompt,
1274
+ })
1275
+ }
1276
+ const href = link.prompt ? `/${slug}/session?prompt=${encodeURIComponent(link.prompt)}` : `/${slug}/session`
1277
+ navigateWithSidebarReset(href)
1278
+ }
1279
+ }
1280
+
1281
+ onMount(() => {
1282
+ const handler = (event: Event) => {
1283
+ const detail = (event as CustomEvent<{ urls: string[] }>).detail
1284
+ const urls = detail?.urls ?? []
1285
+ if (urls.length === 0) return
1286
+ handleDeepLinks(urls)
1287
+ }
1288
+
1289
+ handleDeepLinks(drainPendingDeepLinks(window))
1290
+ makeEventListener(window, deepLinkEvent, handler as EventListener)
1291
+ })
1292
+
1293
+ async function renameProject(project: LocalProject, next: string) {
1294
+ const current = displayName(project)
1295
+ if (next === current) return
1296
+ const name = next === getFilename(project.worktree) ? "" : next
1297
+
1298
+ if (project.id && project.id !== "global") {
1299
+ const result = await serverSDK().api.project.update({ projectID: project.id, name })
1300
+ serverSync().set("project", (items) =>
1301
+ items.map((item) => (item.id === result.id ? normalizeProjectInfo(result) : item)),
1302
+ )
1303
+ return
1304
+ }
1305
+
1306
+ serverSync().project.meta(project.worktree, { name })
1307
+ }
1308
+
1309
+ const renameWorkspace = (directory: string, next: string, projectId?: string, branch?: string) => {
1310
+ const current = workspaceName(directory, projectId, branch) ?? branch ?? getFilename(directory)
1311
+ if (current === next) return
1312
+ setWorkspaceName(directory, next, projectId, branch)
1313
+ }
1314
+
1315
+ function closeProject(directory: string) {
1316
+ const list = layout.projects.list()
1317
+ const key = pathKey(directory)
1318
+ const index = list.findIndex((x) => pathKey(x.worktree) === key)
1319
+ const active = pathKey(currentProject()?.worktree ?? "") === key
1320
+ if (index === -1) return
1321
+
1322
+ if (!active) {
1323
+ layout.projects.close(directory)
1324
+ return
1325
+ }
1326
+
1327
+ if (list.length === 1) {
1328
+ layout.projects.close(directory)
1329
+ navigate("/")
1330
+ return
1331
+ }
1332
+
1333
+ const next = list[index + 1] ?? list[index - 1]
1334
+
1335
+ navigateWithSidebarReset(`/${base64Encode(next.worktree)}/session`)
1336
+ layout.projects.close(directory)
1337
+ queueMicrotask(() => {
1338
+ void navigateToProject(next.worktree)
1339
+ })
1340
+ }
1341
+
1342
+ function toggleProjectWorkspaces(project: LocalProject) {
1343
+ const enabled = layout.sidebar.workspaces(project.worktree)()
1344
+ if (enabled) {
1345
+ layout.sidebar.toggleWorkspaces(project.worktree)
1346
+ return
1347
+ }
1348
+ if (project.vcs !== "git") return
1349
+ layout.sidebar.toggleWorkspaces(project.worktree)
1350
+ }
1351
+
1352
+ const showEditProjectDialog = (conn: ServerConnection.Any, project: LocalProject) => {
1353
+ const run = ++dialogRun
1354
+ void import("@/components/dialog-edit-project").then((x) => {
1355
+ if (dialogDead || dialogRun !== run) return
1356
+ dialog.show(() => <x.DialogEditProject server={conn} project={project} />)
1357
+ })
1358
+ }
1359
+
1360
+ function chooseProject() {
1361
+ const conn = server.current
1362
+ if (!conn) return
1363
+ function resolve(result: string | string[] | null) {
1364
+ if (Array.isArray(result)) {
1365
+ for (const directory of result) {
1366
+ void openProject(directory, false)
1367
+ }
1368
+ void navigateToProject(result[0])
1369
+ } else if (result) {
1370
+ void openProject(result)
1371
+ }
1372
+ }
1373
+
1374
+ pickDirectory({
1375
+ server: conn,
1376
+ title: language.t("command.project.open"),
1377
+ multiple: true,
1378
+ onSelect: resolve,
1379
+ })
1380
+ }
1381
+
1382
+ const deleteWorkspace = async (root: string, directory: string, leaveDeletedWorkspace = false) => {
1383
+ if (directory === root) return
1384
+
1385
+ const current = currentDir()
1386
+ const currentKey = pathKey(current)
1387
+ const deletedKey = pathKey(directory)
1388
+ const shouldLeave = leaveDeletedWorkspace || (!!params.dir && currentKey === deletedKey)
1389
+ if (!leaveDeletedWorkspace && shouldLeave) {
1390
+ navigateWithSidebarReset(`/${base64Encode(root)}/session`)
1391
+ }
1392
+
1393
+ setBusy(directory, true)
1394
+
1395
+ const result = await serverSDK()
1396
+ .client.worktree.remove({ directory: root, worktreeRemoveInput: { directory } })
1397
+ .then((x) => x.data)
1398
+ .catch((err) => {
1399
+ showToast({
1400
+ title: language.t("workspace.delete.failed.title"),
1401
+ description: errorMessage(err, language.t("common.requestFailed")),
1402
+ })
1403
+ return false
1404
+ })
1405
+
1406
+ setBusy(directory, false)
1407
+
1408
+ if (!result) return
1409
+
1410
+ if (pathKey(store.lastProjectSession[root]?.directory ?? "") === pathKey(directory)) {
1411
+ clearLastProjectSession(root)
1412
+ }
1413
+
1414
+ serverSync().set(
1415
+ "project",
1416
+ produce((draft) => {
1417
+ const project = draft.find((item) => item.worktree === root)
1418
+ if (!project) return
1419
+ project.sandboxes = (project.sandboxes ?? []).filter((sandbox) => sandbox !== directory)
1420
+ }),
1421
+ )
1422
+ setStore("workspaceOrder", root, (order) => (order ?? []).filter((workspace) => workspace !== directory))
1423
+
1424
+ layout.projects.close(directory)
1425
+ layout.projects.open(root)
1426
+
1427
+ if (shouldLeave) return
1428
+
1429
+ const nextCurrent = currentDir()
1430
+ const nextKey = pathKey(nextCurrent)
1431
+ const project = layout.projects.list().find((item) => item.worktree === root)
1432
+ const dirs = project
1433
+ ? effectiveWorkspaceOrder(root, [root, ...(project.sandboxes ?? [])], store.workspaceOrder[root])
1434
+ : [root]
1435
+ const valid = dirs.some((item) => pathKey(item) === nextKey)
1436
+
1437
+ if (params.dir && projectRoot(nextCurrent) === root && !valid) {
1438
+ navigateWithSidebarReset(`/${base64Encode(root)}/session`)
1439
+ }
1440
+ }
1441
+
1442
+ const resetWorkspace = async (root: string, directory: string) => {
1443
+ if (directory === root) return
1444
+ setBusy(directory, true)
1445
+
1446
+ const progress = showToast({
1447
+ persistent: true,
1448
+ title: language.t("workspace.resetting.title"),
1449
+ description: language.t("workspace.resetting.description"),
1450
+ })
1451
+ const dismiss = () => toaster.dismiss(progress)
1452
+
1453
+ const sessions = await listAllSessions(serverSDK().api.session, { directory, order: "desc" }).catch(() => [])
1454
+
1455
+ clearWorkspaceTerminals(
1456
+ directory,
1457
+ sessions.map((s) => s.id),
1458
+ platform,
1459
+ serverSDK().scope,
1460
+ )
1461
+ await serverSDK()
1462
+ .client.instance.dispose({ directory })
1463
+ .catch(() => undefined)
1464
+
1465
+ const result = await serverSDK()
1466
+ .client.worktree.reset({ directory: root, worktreeResetInput: { directory } })
1467
+ .then((x) => x.data)
1468
+ .catch((err) => {
1469
+ showToast({
1470
+ title: language.t("workspace.reset.failed.title"),
1471
+ description: errorMessage(err, language.t("common.requestFailed")),
1472
+ })
1473
+ return false
1474
+ })
1475
+
1476
+ if (!result) {
1477
+ setBusy(directory, false)
1478
+ dismiss()
1479
+ return
1480
+ }
1481
+
1482
+ await Promise.all(
1483
+ sessions
1484
+ .filter((session) => session.time.archived === undefined)
1485
+ .map((session) =>
1486
+ serverSDK()
1487
+ .api.session.archive({ sessionID: session.id, directory: session.directory })
1488
+ .catch(() => undefined),
1489
+ ),
1490
+ )
1491
+
1492
+ setBusy(directory, false)
1493
+ dismiss()
1494
+
1495
+ showToast({
1496
+ title: language.t("workspace.reset.success.title"),
1497
+ description: language.t("workspace.reset.success.description"),
1498
+ actions: [
1499
+ {
1500
+ label: language.t("command.session.new"),
1501
+ onClick: () => {
1502
+ const href = `/${base64Encode(directory)}/session`
1503
+ navigate(href)
1504
+ layout.mobileSidebar.hide()
1505
+ },
1506
+ },
1507
+ {
1508
+ label: language.t("common.dismiss"),
1509
+ onClick: "dismiss",
1510
+ },
1511
+ ],
1512
+ })
1513
+ }
1514
+
1515
+ function DialogDeleteWorkspace(props: { root: string; directory: string }) {
1516
+ const name = createMemo(() => getFilename(props.directory))
1517
+ const [data, setData] = createStore({
1518
+ status: "loading" as "loading" | "ready" | "error",
1519
+ dirty: false,
1520
+ })
1521
+
1522
+ onMount(() => {
1523
+ serverSDK()
1524
+ .api.vcs.status({ location: { directory: props.directory } })
1525
+ .then((result) => {
1526
+ const files = result.data
1527
+ const dirty = files.length > 0
1528
+ setData({ status: "ready", dirty })
1529
+ })
1530
+ .catch(() => {
1531
+ setData({ status: "error", dirty: false })
1532
+ })
1533
+ })
1534
+
1535
+ const handleDelete = () => {
1536
+ const leaveDeletedWorkspace = !!params.dir && pathKey(currentDir()) === pathKey(props.directory)
1537
+ if (leaveDeletedWorkspace) {
1538
+ navigateWithSidebarReset(`/${base64Encode(props.root)}/session`)
1539
+ }
1540
+ dialog.close()
1541
+ void deleteWorkspace(props.root, props.directory, leaveDeletedWorkspace)
1542
+ }
1543
+
1544
+ const description = () => {
1545
+ if (data.status === "loading") return language.t("workspace.status.checking")
1546
+ if (data.status === "error") return language.t("workspace.status.error")
1547
+ if (!data.dirty) return language.t("workspace.status.clean")
1548
+ return language.t("workspace.status.dirty")
1549
+ }
1550
+
1551
+ return (
1552
+ <Dialog title={language.t("workspace.delete.title")} fit>
1553
+ <div class="flex flex-col gap-4 pl-6 pr-2.5 pb-3">
1554
+ <div class="flex flex-col gap-1">
1555
+ <span class="text-14-regular text-text-strong">
1556
+ {language.t("workspace.delete.confirm", { name: name() })}
1557
+ </span>
1558
+ <span class="text-12-regular text-text-weak">{description()}</span>
1559
+ </div>
1560
+ <div class="flex justify-end gap-2">
1561
+ <Button variant="ghost" size="large" onClick={() => dialog.close()}>
1562
+ {language.t("common.cancel")}
1563
+ </Button>
1564
+ <Button variant="primary" size="large" disabled={data.status === "loading"} onClick={handleDelete}>
1565
+ {language.t("workspace.delete.button")}
1566
+ </Button>
1567
+ </div>
1568
+ </div>
1569
+ </Dialog>
1570
+ )
1571
+ }
1572
+
1573
+ function DialogResetWorkspace(props: { root: string; directory: string }) {
1574
+ const name = createMemo(() => getFilename(props.directory))
1575
+ const [state, setState] = createStore({
1576
+ status: "loading" as "loading" | "ready" | "error",
1577
+ dirty: false,
1578
+ sessions: [] as Session[],
1579
+ })
1580
+
1581
+ const refresh = async () => {
1582
+ const sessions = await listAllSessions(serverSDK().api.session, {
1583
+ directory: props.directory,
1584
+ order: "desc",
1585
+ }).catch(() => [])
1586
+ const active = sessions.filter((session) => session.time.archived === undefined)
1587
+ setState({ sessions: active })
1588
+ }
1589
+
1590
+ onMount(() => {
1591
+ serverSDK()
1592
+ .api.vcs.status({ location: { directory: props.directory } })
1593
+ .then((result) => {
1594
+ const files = result.data
1595
+ const dirty = files.length > 0
1596
+ setState({ status: "ready", dirty })
1597
+ void refresh()
1598
+ })
1599
+ .catch(() => {
1600
+ setState({ status: "error", dirty: false })
1601
+ })
1602
+ })
1603
+
1604
+ const handleReset = () => {
1605
+ dialog.close()
1606
+ void resetWorkspace(props.root, props.directory)
1607
+ }
1608
+
1609
+ const archivedCount = () => state.sessions.length
1610
+
1611
+ const description = () => {
1612
+ if (state.status === "loading") return language.t("workspace.status.checking")
1613
+ if (state.status === "error") return language.t("workspace.status.error")
1614
+ if (!state.dirty) return language.t("workspace.status.clean")
1615
+ return language.t("workspace.status.dirty")
1616
+ }
1617
+
1618
+ const archivedLabel = () => {
1619
+ const count = archivedCount()
1620
+ if (count === 0) return language.t("workspace.reset.archived.none")
1621
+ if (count === 1) return language.t("workspace.reset.archived.one")
1622
+ return language.t("workspace.reset.archived.many", { count })
1623
+ }
1624
+
1625
+ return (
1626
+ <Dialog title={language.t("workspace.reset.title")} fit>
1627
+ <div class="flex flex-col gap-4 pl-6 pr-2.5 pb-3">
1628
+ <div class="flex flex-col gap-1">
1629
+ <span class="text-14-regular text-text-strong">
1630
+ {language.t("workspace.reset.confirm", { name: name() })}
1631
+ </span>
1632
+ <span class="text-12-regular text-text-weak">
1633
+ {description()} {archivedLabel()} {language.t("workspace.reset.note")}
1634
+ </span>
1635
+ </div>
1636
+ <div class="flex justify-end gap-2">
1637
+ <Button variant="ghost" size="large" onClick={() => dialog.close()}>
1638
+ {language.t("common.cancel")}
1639
+ </Button>
1640
+ <Button variant="primary" size="large" disabled={state.status === "loading"} onClick={handleReset}>
1641
+ {language.t("workspace.reset.button")}
1642
+ </Button>
1643
+ </div>
1644
+ </div>
1645
+ </Dialog>
1646
+ )
1647
+ }
1648
+
1649
+ const activeRoute = {
1650
+ session: "",
1651
+ sessionProject: "",
1652
+ directory: "",
1653
+ }
1654
+
1655
+ createEffect(
1656
+ on(
1657
+ () => {
1658
+ return [pageReady(), route().slug, params.id, currentProject()?.worktree, currentDir()] as const
1659
+ },
1660
+ ([ready, slug, id, root, dir]) => {
1661
+ if (!ready || !slug || !dir) {
1662
+ activeRoute.session = ""
1663
+ activeRoute.sessionProject = ""
1664
+ activeRoute.directory = ""
1665
+ return
1666
+ }
1667
+
1668
+ if (!id) {
1669
+ activeRoute.session = ""
1670
+ activeRoute.sessionProject = ""
1671
+ activeRoute.directory = ""
1672
+ return
1673
+ }
1674
+
1675
+ const session = `${slug}/${id}`
1676
+
1677
+ if (!root) {
1678
+ activeRoute.session = session
1679
+ activeRoute.directory = dir
1680
+ activeRoute.sessionProject = ""
1681
+ return
1682
+ }
1683
+
1684
+ if (server.projects.last() !== root) server.projects.touch(root)
1685
+
1686
+ const changed = session !== activeRoute.session || dir !== activeRoute.directory
1687
+ if (changed) {
1688
+ activeRoute.session = session
1689
+ activeRoute.directory = dir
1690
+ activeRoute.sessionProject = syncSessionRoute(dir, id, root)
1691
+ return
1692
+ }
1693
+
1694
+ if (root === activeRoute.sessionProject) return
1695
+ activeRoute.directory = dir
1696
+ activeRoute.sessionProject = rememberSessionRoute(dir, id, root)
1697
+ },
1698
+ ),
1699
+ )
1700
+
1701
+ createEffect(() => {
1702
+ document.documentElement.style.setProperty(
1703
+ "--dialog-left-margin",
1704
+ `${layout.sidebar.opened() ? layout.sidebar.width() : 48}px`,
1705
+ )
1706
+ })
1707
+
1708
+ const side = createMemo(() => Math.max(layout.sidebar.width(), 244))
1709
+ const panel = createMemo(() => Math.max(side() - 64, 0))
1710
+
1711
+ const loadedSessionDirs = new Set<string>()
1712
+
1713
+ createEffect(
1714
+ on(
1715
+ visibleSessionDirs,
1716
+ (dirs) => {
1717
+ if (dirs.length === 0) {
1718
+ loadedSessionDirs.clear()
1719
+ return
1720
+ }
1721
+
1722
+ const next = new Set(dirs)
1723
+ for (const directory of next) {
1724
+ if (loadedSessionDirs.has(directory)) continue
1725
+ void serverSync().project.loadSessions(directory)
1726
+ }
1727
+
1728
+ loadedSessionDirs.clear()
1729
+ for (const directory of next) {
1730
+ loadedSessionDirs.add(directory)
1731
+ }
1732
+ },
1733
+ { defer: true },
1734
+ ),
1735
+ )
1736
+
1737
+ function handleDragStart(event: unknown) {
1738
+ const id = getDraggableId(event)
1739
+ if (!id) return
1740
+ setHoverProject(undefined)
1741
+ setStore("activeProject", id)
1742
+ }
1743
+
1744
+ function handleDragOver(event: DragEvent) {
1745
+ const { draggable, droppable } = event
1746
+ if (draggable && droppable) {
1747
+ const projects = layout.projects.list()
1748
+ const fromIndex = projects.findIndex((p) => p.worktree === draggable.id.toString())
1749
+ const toIndex = projects.findIndex((p) => p.worktree === droppable.id.toString())
1750
+ if (fromIndex !== toIndex && toIndex !== -1) {
1751
+ layout.projects.move(draggable.id.toString(), toIndex)
1752
+ }
1753
+ }
1754
+ }
1755
+
1756
+ function handleDragEnd() {
1757
+ setStore("activeProject", undefined)
1758
+ }
1759
+
1760
+ function workspaceIds(project: LocalProject | undefined) {
1761
+ if (!project) return []
1762
+ const local = project.worktree
1763
+ const dirs = [local, ...(project.sandboxes ?? [])]
1764
+ const active = currentProject()
1765
+ const directory = pathKey(active?.worktree ?? "") === pathKey(project.worktree) ? currentDir() : undefined
1766
+ const extra =
1767
+ directory && pathKey(directory) !== pathKey(local) && !dirs.some((item) => pathKey(item) === pathKey(directory))
1768
+ ? directory
1769
+ : undefined
1770
+ const pending = extra ? WorktreeState.get(serverSDK().scope, extra)?.status === "pending" : false
1771
+
1772
+ const ordered = effectiveWorkspaceOrder(local, dirs, store.workspaceOrder[project.worktree])
1773
+ if (pending && extra) return [local, extra, ...ordered.filter((item) => item !== local)]
1774
+ if (!extra) return ordered
1775
+ if (pending) return ordered
1776
+ return [...ordered, extra]
1777
+ }
1778
+
1779
+ const sidebarProject = createMemo(() => {
1780
+ if (layout.sidebar.opened()) return currentProject()
1781
+ const hovered = hoverProjectData()
1782
+ if (hovered) return hovered
1783
+ return currentProject()
1784
+ })
1785
+
1786
+ function handleWorkspaceDragStart(event: unknown) {
1787
+ const id = getDraggableId(event)
1788
+ if (!id) return
1789
+ setStore("activeWorkspace", id)
1790
+ }
1791
+
1792
+ function handleWorkspaceDragOver(event: DragEvent) {
1793
+ const { draggable, droppable } = event
1794
+ if (!draggable || !droppable) return
1795
+
1796
+ const project = sidebarProject()
1797
+ if (!project) return
1798
+
1799
+ const ids = workspaceIds(project)
1800
+ const fromIndex = ids.findIndex((dir) => dir === draggable.id.toString())
1801
+ const toIndex = ids.findIndex((dir) => dir === droppable.id.toString())
1802
+ if (fromIndex === -1 || toIndex === -1) return
1803
+ if (fromIndex === toIndex) return
1804
+
1805
+ const result = ids.slice()
1806
+ const [item] = result.splice(fromIndex, 1)
1807
+ if (!item) return
1808
+ result.splice(toIndex, 0, item)
1809
+ setStore(
1810
+ "workspaceOrder",
1811
+ project.worktree,
1812
+ result.filter((directory) => pathKey(directory) !== pathKey(project.worktree)),
1813
+ )
1814
+ }
1815
+
1816
+ function handleWorkspaceDragEnd() {
1817
+ setStore("activeWorkspace", undefined)
1818
+ }
1819
+
1820
+ const createWorkspace = async (project: LocalProject) => {
1821
+ clearSidebarHoverState()
1822
+ const created = await serverSDK()
1823
+ .client.worktree.create({ directory: project.worktree })
1824
+ .then((x) => x.data)
1825
+ .catch((err) => {
1826
+ showToast({
1827
+ title: language.t("workspace.create.failed.title"),
1828
+ description: errorMessage(err, language.t("common.requestFailed")),
1829
+ })
1830
+ return undefined
1831
+ })
1832
+
1833
+ if (!created?.directory) return
1834
+
1835
+ setWorkspaceName(created.directory, created.branch ?? getFilename(created.directory), project.id, created.branch)
1836
+
1837
+ const local = project.worktree
1838
+ const key = pathKey(created.directory)
1839
+ const root = pathKey(local)
1840
+
1841
+ setBusy(created.directory, true)
1842
+ WorktreeState.pending(serverSDK().scope, created.directory)
1843
+ setStore("workspaceExpanded", key, true)
1844
+ if (key !== created.directory) {
1845
+ setStore("workspaceExpanded", created.directory, true)
1846
+ }
1847
+ setStore("workspaceOrder", project.worktree, (prev) => {
1848
+ const existing = prev ?? []
1849
+ const next = existing.filter((item) => {
1850
+ const id = pathKey(item)
1851
+ return id !== root && id !== key
1852
+ })
1853
+ return [created.directory, ...next]
1854
+ })
1855
+
1856
+ serverSync().child(created.directory)
1857
+ navigateWithSidebarReset(`/${base64Encode(created.directory)}/session`)
1858
+ }
1859
+
1860
+ const workspaceSidebarCtx: WorkspaceSidebarContext = {
1861
+ currentDir,
1862
+ navList: currentSessions,
1863
+ sidebarExpanded,
1864
+ sidebarHovering,
1865
+ clearHoverProjectSoon,
1866
+ prefetchSession,
1867
+ archiveSession,
1868
+ workspaceName,
1869
+ renameWorkspace,
1870
+ editorOpen,
1871
+ openEditor,
1872
+ closeEditor,
1873
+ setEditor,
1874
+ InlineEditor,
1875
+ isBusy,
1876
+ workspaceExpanded: (directory, local) => store.workspaceExpanded[directory] ?? local,
1877
+ setWorkspaceExpanded: (directory, value) => setStore("workspaceExpanded", directory, value),
1878
+ showResetWorkspaceDialog: (root, directory) =>
1879
+ dialog.show(() => <DialogResetWorkspace root={root} directory={directory} />),
1880
+ showDeleteWorkspaceDialog: (root, directory) =>
1881
+ dialog.show(() => <DialogDeleteWorkspace root={root} directory={directory} />),
1882
+ setScrollContainerRef: (el, mobile) => {
1883
+ if (!mobile) scrollContainerRef = el
1884
+ },
1885
+ }
1886
+
1887
+ const projectSidebarCtx: ProjectSidebarContext = {
1888
+ currentDir,
1889
+ currentProject,
1890
+ sidebarOpened: () => layout.sidebar.opened(),
1891
+ sidebarHovering,
1892
+ hoverProject: () => state.hoverProject,
1893
+ onProjectMouseEnter: (worktree, event) => aim.enter(worktree, event),
1894
+ onProjectMouseLeave: (worktree) => aim.leave(worktree),
1895
+ onProjectFocus: (worktree) => aim.activate(worktree),
1896
+ onHoverOpenChanged: (worktree, hoverOpen) => {
1897
+ if (!hoverOpen && state.hoverProject && state.hoverProject !== worktree) return
1898
+ setState("hoverProject", hoverOpen ? worktree : undefined)
1899
+ },
1900
+ navigateToProject,
1901
+ openSidebar: () => layout.sidebar.open(),
1902
+ closeProject,
1903
+ showEditProjectDialog: (proj) => showEditProjectDialog(server.current!, proj),
1904
+ toggleProjectWorkspaces,
1905
+ workspacesEnabled: (project) => project.vcs === "git" && layout.sidebar.workspaces(project.worktree)(),
1906
+ workspaceIds,
1907
+ workspaceLabel,
1908
+ sessionProps: {
1909
+ navList: currentSessions,
1910
+ sidebarExpanded,
1911
+ clearHoverProjectSoon,
1912
+ prefetchSession,
1913
+ archiveSession,
1914
+ },
1915
+ }
1916
+
1917
+ const SidebarPanel = (panelProps: {
1918
+ project: Accessor<LocalProject | undefined>
1919
+ mobile?: boolean
1920
+ merged?: boolean
1921
+ }) => {
1922
+ const project = panelProps.project
1923
+ const merged = createMemo(() => panelProps.mobile || (panelProps.merged ?? layout.sidebar.opened()))
1924
+ const hover = createMemo(() => !panelProps.mobile && panelProps.merged === false && !layout.sidebar.opened())
1925
+ const empty = createMemo(() => !params.dir && layout.projects.list().length === 0)
1926
+ const projectName = createMemo(() => {
1927
+ const item = project()
1928
+ if (!item) return ""
1929
+ return item.name || getFilename(item.worktree)
1930
+ })
1931
+ const projectId = createMemo(() => project()?.id ?? "")
1932
+ const worktree = createMemo(() => project()?.worktree ?? "")
1933
+ const slug = createMemo(() => {
1934
+ const dir = worktree()
1935
+ if (!dir) return ""
1936
+ return base64Encode(dir)
1937
+ })
1938
+ const workspaces = createMemo(() => {
1939
+ const item = project()
1940
+ if (!item) return [] as string[]
1941
+ return workspaceIds(item)
1942
+ })
1943
+ const unseenCount = createMemo(() =>
1944
+ workspaces().reduce((total, directory) => total + notification.project.unseenCount(directory), 0),
1945
+ )
1946
+ const clearNotifications = () =>
1947
+ workspaces()
1948
+ .filter((directory) => notification.project.unseenCount(directory) > 0)
1949
+ .forEach((directory) => notification.project.markViewed(directory))
1950
+ const workspacesEnabled = createMemo(() => {
1951
+ const item = project()
1952
+ if (!item) return false
1953
+ if (item.vcs !== "git") return false
1954
+ return layout.sidebar.workspaces(item.worktree)()
1955
+ })
1956
+ const canToggle = createMemo(() => {
1957
+ const item = project()
1958
+ if (!item) return false
1959
+ return item.vcs === "git" || layout.sidebar.workspaces(item.worktree)()
1960
+ })
1961
+ const homedir = createMemo(() => serverSync().data.path.home)
1962
+
1963
+ return (
1964
+ <div
1965
+ classList={{
1966
+ "flex flex-col min-h-0 min-w-0 box-border rounded-tl-[12px] px-3": true,
1967
+ "border border-b-0 border-border-weak-base": !merged(),
1968
+ "border-l border-t border-border-weaker-base": merged(),
1969
+ "bg-background-base": merged() || hover(),
1970
+ "bg-background-stronger": !merged() && !hover(),
1971
+ "flex-1 min-w-0": panelProps.mobile,
1972
+ "max-w-full overflow-hidden": panelProps.mobile,
1973
+ }}
1974
+ style={{
1975
+ width: panelProps.mobile ? undefined : `${panel()}px`,
1976
+ }}
1977
+ >
1978
+ <Show
1979
+ when={project()}
1980
+ fallback={
1981
+ <Show when={empty()}>
1982
+ <div class="flex-1 min-h-0 -mt-4 flex items-center justify-center px-6 pb-64 text-center">
1983
+ <div class="mt-8 flex max-w-60 flex-col items-center gap-6 text-center">
1984
+ <div class="flex flex-col gap-3">
1985
+ <div class="text-14-medium text-text-strong">{language.t("sidebar.empty.title")}</div>
1986
+ <div class="text-14-regular text-text-base" style={{ "line-height": "var(--line-height-normal)" }}>
1987
+ {language.t("sidebar.empty.description")}
1988
+ </div>
1989
+ </div>
1990
+ <Button size="large" icon="folder-add-left" onClick={chooseProject}>
1991
+ {language.t("command.project.open")}
1992
+ </Button>
1993
+ </div>
1994
+ </div>
1995
+ </Show>
1996
+ }
1997
+ keyed
1998
+ >
1999
+ {(project) => (
2000
+ <>
2001
+ <div class="shrink-0 pl-1 py-1">
2002
+ <div class="group/project flex items-start justify-between gap-2 py-2 pl-2 pr-0">
2003
+ <div class="flex flex-col min-w-0">
2004
+ <InlineEditor
2005
+ id={`project:${projectId()}`}
2006
+ value={projectName}
2007
+ onSave={(next) => {
2008
+ void renameProject(project, next)
2009
+ }}
2010
+ class="text-14-medium text-text-strong truncate"
2011
+ displayClass="text-14-medium text-text-strong truncate"
2012
+ stopPropagation
2013
+ />
2014
+
2015
+ <Tooltip
2016
+ placement="bottom"
2017
+ gutter={2}
2018
+ value={worktree()}
2019
+ class="shrink-0"
2020
+ contentStyle={{
2021
+ "max-width": "640px",
2022
+ transform: "translate3d(52px, 0, 0)",
2023
+ }}
2024
+ >
2025
+ <span class="text-12-regular text-text-base truncate select-text">
2026
+ {worktree().replace(homedir(), "~")}
2027
+ </span>
2028
+ </Tooltip>
2029
+ </div>
2030
+
2031
+ <DropdownMenu modal={!sidebarHovering()}>
2032
+ <DropdownMenu.Trigger
2033
+ as={IconButton}
2034
+ icon="dot-grid"
2035
+ variant="ghost"
2036
+ data-action="project-menu"
2037
+ data-project={slug()}
2038
+ class="shrink-0 size-6 rounded-md transition-opacity data-[expanded]:bg-surface-base-active"
2039
+ classList={{
2040
+ "opacity-100": panelProps.mobile || merged(),
2041
+ "opacity-0 group-hover/project:opacity-100 group-focus-within/project:opacity-100 data-[expanded]:opacity-100":
2042
+ !panelProps.mobile && !merged(),
2043
+ }}
2044
+ aria-label={language.t("common.moreOptions")}
2045
+ />
2046
+ <DropdownMenu.Portal>
2047
+ <DropdownMenu.Content class="mt-1">
2048
+ <DropdownMenu.Item
2049
+ onSelect={() => {
2050
+ showEditProjectDialog(server.current!, project)
2051
+ }}
2052
+ >
2053
+ <DropdownMenu.ItemLabel>{language.t("common.edit")}</DropdownMenu.ItemLabel>
2054
+ </DropdownMenu.Item>
2055
+ <DropdownMenu.Item
2056
+ data-action="project-workspaces-toggle"
2057
+ data-project={slug()}
2058
+ disabled={!canToggle()}
2059
+ onSelect={() => {
2060
+ toggleProjectWorkspaces(project)
2061
+ }}
2062
+ >
2063
+ <DropdownMenu.ItemLabel>
2064
+ {workspacesEnabled()
2065
+ ? language.t("sidebar.workspaces.disable")
2066
+ : language.t("sidebar.workspaces.enable")}
2067
+ </DropdownMenu.ItemLabel>
2068
+ </DropdownMenu.Item>
2069
+ <DropdownMenu.Item
2070
+ data-action="project-clear-notifications"
2071
+ data-project={slug()}
2072
+ disabled={unseenCount() === 0}
2073
+ onSelect={clearNotifications}
2074
+ >
2075
+ <DropdownMenu.ItemLabel>
2076
+ {language.t("sidebar.project.clearNotifications")}
2077
+ </DropdownMenu.ItemLabel>
2078
+ </DropdownMenu.Item>
2079
+ <DropdownMenu.Separator />
2080
+ <DropdownMenu.Item
2081
+ data-action="project-close-menu"
2082
+ data-project={slug()}
2083
+ onSelect={() => {
2084
+ const dir = worktree()
2085
+ if (!dir) return
2086
+ closeProject(dir)
2087
+ }}
2088
+ >
2089
+ <DropdownMenu.ItemLabel>{language.t("common.close")}</DropdownMenu.ItemLabel>
2090
+ </DropdownMenu.Item>
2091
+ </DropdownMenu.Content>
2092
+ </DropdownMenu.Portal>
2093
+ </DropdownMenu>
2094
+ </div>
2095
+ </div>
2096
+
2097
+ <div class="flex-1 min-h-0 flex flex-col">
2098
+ <Show
2099
+ when={workspacesEnabled()}
2100
+ fallback={
2101
+ <>
2102
+ <div class="shrink-0 py-4">
2103
+ <Button
2104
+ size="large"
2105
+ class="w-full"
2106
+ onClick={() => {
2107
+ const dir = worktree()
2108
+ if (!dir) return
2109
+ navigateWithSidebarReset(`/${base64Encode(dir)}/session`)
2110
+ }}
2111
+ >
2112
+ <IconV2 name="edit" size="small" />
2113
+ {language.t("command.session.new")}
2114
+ </Button>
2115
+ </div>
2116
+ <div class="flex-1 min-h-0">
2117
+ <LocalWorkspace
2118
+ ctx={workspaceSidebarCtx}
2119
+ project={project}
2120
+ sortNow={sortNow}
2121
+ mobile={panelProps.mobile}
2122
+ />
2123
+ </div>
2124
+ </>
2125
+ }
2126
+ >
2127
+ <>
2128
+ <div class="shrink-0 py-4">
2129
+ <Button
2130
+ size="large"
2131
+ icon="plus-small"
2132
+ class="w-full"
2133
+ onClick={() => {
2134
+ void createWorkspace(project)
2135
+ }}
2136
+ >
2137
+ {language.t("workspace.new")}
2138
+ </Button>
2139
+ </div>
2140
+ <div class="relative flex-1 min-h-0">
2141
+ <DragDropProvider
2142
+ onDragStart={handleWorkspaceDragStart}
2143
+ onDragEnd={handleWorkspaceDragEnd}
2144
+ onDragOver={handleWorkspaceDragOver}
2145
+ collisionDetector={closestCenter}
2146
+ >
2147
+ <DragDropSensors />
2148
+ <ConstrainDragXAxis />
2149
+ <div
2150
+ ref={(el) => {
2151
+ if (!panelProps.mobile) scrollContainerRef = el
2152
+ }}
2153
+ class="size-full flex flex-col py-2 gap-4 overflow-y-auto no-scrollbar [overflow-anchor:none]"
2154
+ >
2155
+ <SortableProvider ids={workspaces()}>
2156
+ <For each={workspaces()}>
2157
+ {(directory) => (
2158
+ <SortableWorkspace
2159
+ ctx={workspaceSidebarCtx}
2160
+ directory={directory}
2161
+ project={project}
2162
+ sortNow={sortNow}
2163
+ mobile={panelProps.mobile}
2164
+ />
2165
+ )}
2166
+ </For>
2167
+ </SortableProvider>
2168
+ </div>
2169
+ <DragOverlay>
2170
+ <WorkspaceDragOverlay
2171
+ sidebarProject={sidebarProject}
2172
+ activeWorkspace={() => store.activeWorkspace}
2173
+ workspaceLabel={workspaceLabel}
2174
+ />
2175
+ </DragOverlay>
2176
+ </DragDropProvider>
2177
+ </div>
2178
+ </>
2179
+ </Show>
2180
+ </div>
2181
+ </>
2182
+ )}
2183
+ </Show>
2184
+
2185
+ <div
2186
+ class="shrink-0 px-3 py-3"
2187
+ classList={{
2188
+ hidden: store.gettingStartedDismissed || !(providers.all().size > 0 && providers.paid().length === 0),
2189
+ }}
2190
+ >
2191
+ <div class="rounded-xl bg-background-base shadow-xs-border-base" data-component="getting-started">
2192
+ <div class="p-3 flex flex-col gap-6">
2193
+ <div class="flex flex-col gap-2">
2194
+ <div class="text-14-medium text-text-strong">{language.t("sidebar.gettingStarted.title")}</div>
2195
+ <div class="text-14-regular text-text-base" style={{ "line-height": "var(--line-height-normal)" }}>
2196
+ {language.t("sidebar.gettingStarted.line1")}
2197
+ </div>
2198
+ <div class="text-14-regular text-text-base" style={{ "line-height": "var(--line-height-normal)" }}>
2199
+ {language.t("sidebar.gettingStarted.line2")}
2200
+ </div>
2201
+ </div>
2202
+ <div data-component="getting-started-actions">
2203
+ <Button size="large" icon="plus-small" onClick={connectProvider}>
2204
+ {language.t("command.provider.connect")}
2205
+ </Button>
2206
+ <Button size="large" variant="ghost" onClick={() => setStore("gettingStartedDismissed", true)}>
2207
+ {language.t("toast.update.action.notYet")}
2208
+ </Button>
2209
+ </div>
2210
+ </div>
2211
+ </div>
2212
+ </div>
2213
+ </div>
2214
+ )
2215
+ }
2216
+
2217
+ const projects = () => layout.projects.list()
2218
+ const projectOverlay = () => <ProjectDragOverlay projects={projects} activeProject={() => store.activeProject} />
2219
+ const sidebarContent = (mobile?: boolean) => (
2220
+ <SidebarContent
2221
+ mobile={mobile}
2222
+ opened={() => layout.sidebar.opened()}
2223
+ aimMove={aim.move}
2224
+ projects={projects}
2225
+ renderProject={(project) => (
2226
+ <SortableProject ctx={projectSidebarCtx} project={project} sortNow={sortNow} mobile={mobile} />
2227
+ )}
2228
+ handleDragStart={handleDragStart}
2229
+ handleDragEnd={handleDragEnd}
2230
+ handleDragOver={handleDragOver}
2231
+ openProjectLabel={language.t("command.project.open")}
2232
+ openProjectKeybind={() => command.keybind("project.open")}
2233
+ onOpenProject={chooseProject}
2234
+ renderProjectOverlay={projectOverlay}
2235
+ settingsLabel={() => language.t("sidebar.settings")}
2236
+ settingsKeybind={() => command.keybind("settings.open")}
2237
+ onOpenSettings={openSettings}
2238
+ helpLabel={() => language.t("sidebar.help")}
2239
+ onOpenHelp={() => platform.openLink("https://neurocode.ai/desktop-feedback")}
2240
+ renderPanel={() =>
2241
+ mobile ? <SidebarPanel project={currentProject} mobile /> : <SidebarPanel project={currentProject} merged />
2242
+ }
2243
+ />
2244
+ )
2245
+
2246
+ return (
2247
+ <div class="relative bg-background-base flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
2248
+ {autoselecting() ?? ""}
2249
+ <Titlebar
2250
+ update={titlebarUpdate}
2251
+ debugTools={
2252
+ import.meta.env.DEV && import.meta.env.VITE_DISABLE_DEBUG_BAR !== "1"
2253
+ ? { visible: state.debugTools, toggle: () => setState("debugTools", (value) => !value) }
2254
+ : undefined
2255
+ }
2256
+ />
2257
+ <Show when={updateVersion() !== undefined}>
2258
+ <UpdateAvailableToast version={updateVersion() ?? ""} install={installUpdate} language={language} />
2259
+ </Show>
2260
+ <div class="flex-1 min-h-0 min-w-0 flex">
2261
+ <div class="flex-1 min-h-0 relative">
2262
+ <div class="size-full relative overflow-x-hidden">
2263
+ <nav
2264
+ aria-label={language.t("sidebar.nav.projectsAndSessions")}
2265
+ data-component="sidebar-nav-desktop"
2266
+ classList={{
2267
+ "hidden xl:block": true,
2268
+ "absolute inset-y-0 left-0": true,
2269
+ "z-10": true,
2270
+ }}
2271
+ style={{ width: `${side()}px` }}
2272
+ ref={(el) => {
2273
+ setState("nav", el)
2274
+ }}
2275
+ onMouseEnter={() => {
2276
+ disarm()
2277
+ }}
2278
+ onMouseLeave={() => {
2279
+ aim.reset()
2280
+ if (!sidebarHovering()) return
2281
+
2282
+ arm()
2283
+ }}
2284
+ >
2285
+ <div class="@container w-full h-full contain-strict">{sidebarContent()}</div>
2286
+ </nav>
2287
+
2288
+ <Show when={layout.sidebar.opened()}>
2289
+ <div
2290
+ class="hidden xl:block absolute inset-y-0 z-30 w-0 overflow-visible"
2291
+ style={{ left: `${side()}px` }}
2292
+ onPointerDown={() => setState("sizing", true)}
2293
+ >
2294
+ <ResizeHandle
2295
+ direction="horizontal"
2296
+ size={layout.sidebar.width()}
2297
+ min={244}
2298
+ max={typeof window === "undefined" ? 1000 : window.innerWidth * 0.3 + 64}
2299
+ onResize={(w) => {
2300
+ setState("sizing", true)
2301
+ if (sizet !== undefined) clearTimeout(sizet)
2302
+ sizet = window.setTimeout(() => setState("sizing", false), 120)
2303
+ layout.sidebar.resize(w)
2304
+ }}
2305
+ />
2306
+ </div>
2307
+ </Show>
2308
+
2309
+ <div
2310
+ class="hidden xl:block pointer-events-none absolute top-0 right-0 z-0 border-t border-border-weaker-base"
2311
+ style={{ left: "calc(4rem + 12px)" }}
2312
+ />
2313
+
2314
+ <div class="xl:hidden">
2315
+ <div
2316
+ classList={{
2317
+ "fixed inset-x-0 top-10 bottom-0 z-40 transition-opacity duration-200": true,
2318
+ "opacity-100 pointer-events-auto": layout.mobileSidebar.opened(),
2319
+ "opacity-0 pointer-events-none": !layout.mobileSidebar.opened(),
2320
+ }}
2321
+ onClick={(e) => {
2322
+ if (e.target === e.currentTarget) layout.mobileSidebar.hide()
2323
+ }}
2324
+ />
2325
+ <nav
2326
+ aria-label={language.t("sidebar.nav.projectsAndSessions")}
2327
+ data-component="sidebar-nav-mobile"
2328
+ classList={{
2329
+ "@container fixed top-10 bottom-0 left-0 z-50 w-full max-w-[400px] overflow-hidden border-r border-border-weaker-base bg-background-base transition-transform duration-200 ease-out": true,
2330
+ "translate-x-0": layout.mobileSidebar.opened(),
2331
+ "-translate-x-full": !layout.mobileSidebar.opened(),
2332
+ }}
2333
+ onClick={(e) => e.stopPropagation()}
2334
+ >
2335
+ {sidebarContent(true)}
2336
+ </nav>
2337
+ </div>
2338
+
2339
+ <div
2340
+ classList={{
2341
+ "absolute inset-0": true,
2342
+ "xl:inset-y-0 xl:right-0 xl:left-[var(--main-left)]": true,
2343
+ "z-20": true,
2344
+ "transition-[left] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[left] motion-reduce:transition-none":
2345
+ !state.sizing,
2346
+ }}
2347
+ style={{
2348
+ "--main-left": layout.sidebar.opened() ? `${side()}px` : "4rem",
2349
+ }}
2350
+ >
2351
+ <main
2352
+ classList={{
2353
+ "size-full overflow-x-hidden flex flex-col items-start contain-strict border-t border-border-weak-base bg-background-base xl:border-l xl:rounded-tl-[12px]": true,
2354
+ }}
2355
+ >
2356
+ <Show when={!autoselecting.loading} fallback={<div class="size-full" />}>
2357
+ {props.children}
2358
+ </Show>
2359
+ </main>
2360
+ </div>
2361
+
2362
+ <div
2363
+ classList={{
2364
+ "hidden xl:flex absolute inset-y-0 left-16 z-30": true,
2365
+ "opacity-100 translate-x-0 pointer-events-auto": state.peeked && !layout.sidebar.opened(),
2366
+ "opacity-0 -translate-x-2 pointer-events-none": !state.peeked || layout.sidebar.opened(),
2367
+ "transition-[opacity,transform] motion-reduce:transition-none": true,
2368
+ "duration-180 ease-out": state.peeked && !layout.sidebar.opened(),
2369
+ "duration-120 ease-in": !state.peeked || layout.sidebar.opened(),
2370
+ }}
2371
+ onMouseMove={disarm}
2372
+ onMouseEnter={() => {
2373
+ disarm()
2374
+ aim.reset()
2375
+ }}
2376
+ onPointerDown={disarm}
2377
+ onMouseLeave={() => {
2378
+ arm()
2379
+ }}
2380
+ >
2381
+ <Show when={peekProject()}>
2382
+ <SidebarPanel project={peekProject} merged={false} />
2383
+ </Show>
2384
+ </div>
2385
+
2386
+ <div
2387
+ classList={{
2388
+ "hidden xl:block pointer-events-none absolute inset-y-0 right-0 z-25 overflow-hidden": true,
2389
+ "opacity-100 translate-x-0": state.peeked && !layout.sidebar.opened(),
2390
+ "opacity-0 -translate-x-2": !state.peeked || layout.sidebar.opened(),
2391
+ "transition-[opacity,transform] motion-reduce:transition-none": true,
2392
+ "duration-180 ease-out": state.peeked && !layout.sidebar.opened(),
2393
+ "duration-120 ease-in": !state.peeked || layout.sidebar.opened(),
2394
+ }}
2395
+ style={{ left: `calc(4rem + ${panel()}px)` }}
2396
+ >
2397
+ <div class="h-full w-px" style={{ "box-shadow": "var(--shadow-sidebar-overlay)" }} />
2398
+ </div>
2399
+ </div>
2400
+ </div>
2401
+ {import.meta.env.DEV && import.meta.env.VITE_DISABLE_DEBUG_BAR !== "1" && state.debugTools && <DebugBar />}
2402
+ </div>
2403
+ <TabsInfoPopup />
2404
+ <ToastRegion v2={false} />
2405
+ </div>
2406
+ )
2407
+ }
2408
+
2409
+ function UpdateAvailableToast(props: {
2410
+ version: string
2411
+ install: () => void
2412
+ language: ReturnType<typeof useLanguage>
2413
+ }) {
2414
+ let toastId: number | undefined
2415
+
2416
+ onMount(() => {
2417
+ toastId = showToast({
2418
+ persistent: true,
2419
+ icon: "download",
2420
+ title: props.language.t("toast.update.title"),
2421
+ description: props.language.t("toast.update.description", { version: props.version }),
2422
+ actions: [
2423
+ {
2424
+ label: props.language.t("toast.update.action.installRestart"),
2425
+ onClick: props.install,
2426
+ },
2427
+ {
2428
+ label: props.language.t("toast.update.action.notYet"),
2429
+ onClick: "dismiss",
2430
+ },
2431
+ ],
2432
+ })
2433
+ })
2434
+
2435
+ onCleanup(() => {
2436
+ if (toastId === undefined) return
2437
+ toaster.dismiss(toastId)
2438
+ })
2439
+
2440
+ return null
2441
+ }