nastechai-desktop 18.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (546) hide show
  1. package/.prettierrc +11 -0
  2. package/DESIGN.md +167 -0
  3. package/README.md +141 -0
  4. package/assets/icon.icns +0 -0
  5. package/assets/icon.ico +0 -0
  6. package/assets/icon.png +0 -0
  7. package/components.json +21 -0
  8. package/electron/backend-env.cjs +112 -0
  9. package/electron/backend-env.test.cjs +111 -0
  10. package/electron/backend-probes.cjs +106 -0
  11. package/electron/backend-probes.test.cjs +82 -0
  12. package/electron/backend-ready.cjs +66 -0
  13. package/electron/bootstrap-platform.cjs +91 -0
  14. package/electron/bootstrap-platform.test.cjs +111 -0
  15. package/electron/bootstrap-runner.cjs +720 -0
  16. package/electron/bootstrap-runner.test.cjs +138 -0
  17. package/electron/connection-config.cjs +254 -0
  18. package/electron/connection-config.test.cjs +329 -0
  19. package/electron/dashboard-token.cjs +99 -0
  20. package/electron/dashboard-token.test.cjs +142 -0
  21. package/electron/desktop-uninstall.cjs +232 -0
  22. package/electron/desktop-uninstall.test.cjs +246 -0
  23. package/electron/entitlements.mac.inherit.plist +14 -0
  24. package/electron/entitlements.mac.plist +14 -0
  25. package/electron/fs-read-dir.cjs +109 -0
  26. package/electron/fs-read-dir.test.cjs +364 -0
  27. package/electron/gateway-ws-probe.cjs +188 -0
  28. package/electron/gateway-ws-probe.test.cjs +122 -0
  29. package/electron/git-root.cjs +54 -0
  30. package/electron/git-root.test.cjs +40 -0
  31. package/electron/git-worktrees.cjs +174 -0
  32. package/electron/hardening.cjs +184 -0
  33. package/electron/hardening.test.cjs +116 -0
  34. package/electron/main.cjs +5762 -0
  35. package/electron/oauth-net-request.cjs +20 -0
  36. package/electron/oauth-net-request.test.cjs +34 -0
  37. package/electron/preload.cjs +135 -0
  38. package/electron/session-windows.cjs +99 -0
  39. package/electron/session-windows.test.cjs +177 -0
  40. package/electron/update-remote.cjs +56 -0
  41. package/electron/update-remote.test.cjs +78 -0
  42. package/electron/vscode-marketplace.cjs +331 -0
  43. package/electron/vscode-marketplace.test.cjs +113 -0
  44. package/electron/windows-child-process.test.cjs +57 -0
  45. package/electron/windows-user-env.cjs +76 -0
  46. package/electron/windows-user-env.test.cjs +90 -0
  47. package/electron/workspace-cwd.cjs +38 -0
  48. package/electron/workspace-cwd.test.cjs +45 -0
  49. package/eslint.config.mjs +122 -0
  50. package/index.html +17 -0
  51. package/package.json +254 -0
  52. package/pr-assets/session-source-folders.png +0 -0
  53. package/preview-demo.html +65 -0
  54. package/public/apple-touch-icon.png +0 -0
  55. package/public/ds-assets/filler-bg0.jpg +0 -0
  56. package/public/nastech-frames/nastech-frame-0.png +0 -0
  57. package/public/nastech-frames/nastech-frame-1.png +0 -0
  58. package/public/nastech-frames/nastech-frame-2.png +0 -0
  59. package/public/nastech-frames/nastech-frame-3.png +0 -0
  60. package/public/nastech-frames/nastech-frame-4.png +0 -0
  61. package/public/nastech-frames/nastech-frame-5.png +0 -0
  62. package/public/nastech-frames/nastech-frame-6.png +0 -0
  63. package/public/nastech-frames/nastech-frame-7.png +0 -0
  64. package/public/nastech-girl.jpg +0 -0
  65. package/public/nastech-sprite.png +0 -0
  66. package/public/nastech.png +0 -0
  67. package/scripts/after-pack.cjs +41 -0
  68. package/scripts/assert-dist-built.cjs +70 -0
  69. package/scripts/assert-dist-built.test.cjs +84 -0
  70. package/scripts/assert-root-install.cjs +13 -0
  71. package/scripts/before-build.cjs +11 -0
  72. package/scripts/before-pack.cjs +78 -0
  73. package/scripts/before-pack.test.cjs +53 -0
  74. package/scripts/click-session.mjs +51 -0
  75. package/scripts/dev-no-hmr.mjs +22 -0
  76. package/scripts/diag-jump.mjs +115 -0
  77. package/scripts/diag-scroll-reset.mjs +229 -0
  78. package/scripts/eval.mjs +21 -0
  79. package/scripts/leak-typing.mjs +222 -0
  80. package/scripts/measure-jump.mjs +108 -0
  81. package/scripts/measure-latency.mjs +184 -0
  82. package/scripts/measure-real-stream.mjs +252 -0
  83. package/scripts/measure-submit.mjs +179 -0
  84. package/scripts/measure-synthetic-stream.mjs +322 -0
  85. package/scripts/notarize-artifact.cjs +77 -0
  86. package/scripts/notarize.cjs +100 -0
  87. package/scripts/patch-electron-builder-mac-binary.cjs +59 -0
  88. package/scripts/probe-renderer.mjs +38 -0
  89. package/scripts/probe-thread.mjs +40 -0
  90. package/scripts/profile-long-stream.mjs +191 -0
  91. package/scripts/profile-real-stream.mjs +137 -0
  92. package/scripts/profile-synth-stream.mjs +103 -0
  93. package/scripts/profile-typing-lag.md +381 -0
  94. package/scripts/profile-typing.mjs +260 -0
  95. package/scripts/reload-renderer.mjs +25 -0
  96. package/scripts/reload.mjs +36 -0
  97. package/scripts/set-exe-identity.cjs +94 -0
  98. package/scripts/stage-native-deps.cjs +159 -0
  99. package/scripts/test-desktop.mjs +425 -0
  100. package/scripts/write-build-stamp.cjs +126 -0
  101. package/src/app/agents/index.tsx +398 -0
  102. package/src/app/artifacts/index.test.ts +62 -0
  103. package/src/app/artifacts/index.tsx +906 -0
  104. package/src/app/chat/chat-drop-overlay.tsx +48 -0
  105. package/src/app/chat/chat-swap-overlay.tsx +47 -0
  106. package/src/app/chat/composer/attachments.tsx +114 -0
  107. package/src/app/chat/composer/completion-drawer.tsx +63 -0
  108. package/src/app/chat/composer/context-menu.tsx +172 -0
  109. package/src/app/chat/composer/controls.tsx +289 -0
  110. package/src/app/chat/composer/drop-affordance.ts +2 -0
  111. package/src/app/chat/composer/enter-submit-dom-race.test.tsx +218 -0
  112. package/src/app/chat/composer/focus.ts +134 -0
  113. package/src/app/chat/composer/help-hint.tsx +59 -0
  114. package/src/app/chat/composer/hooks/use-at-completions.ts +141 -0
  115. package/src/app/chat/composer/hooks/use-live-completion-adapter.ts +119 -0
  116. package/src/app/chat/composer/hooks/use-mic-recorder.ts +291 -0
  117. package/src/app/chat/composer/hooks/use-slash-completions.ts +114 -0
  118. package/src/app/chat/composer/hooks/use-voice-conversation.ts +390 -0
  119. package/src/app/chat/composer/hooks/use-voice-recorder.ts +116 -0
  120. package/src/app/chat/composer/ime-composition-dom-repro.test.tsx +108 -0
  121. package/src/app/chat/composer/index.tsx +1611 -0
  122. package/src/app/chat/composer/inline-refs.ts +138 -0
  123. package/src/app/chat/composer/model-pill.tsx +86 -0
  124. package/src/app/chat/composer/queue-panel.tsx +130 -0
  125. package/src/app/chat/composer/rich-editor.test.ts +18 -0
  126. package/src/app/chat/composer/rich-editor.ts +165 -0
  127. package/src/app/chat/composer/skin-slash-popover.tsx +61 -0
  128. package/src/app/chat/composer/slash-nav-dom-repro.test.tsx +186 -0
  129. package/src/app/chat/composer/status-stack/index.tsx +202 -0
  130. package/src/app/chat/composer/status-stack/status-row.tsx +155 -0
  131. package/src/app/chat/composer/text-utils.test.ts +77 -0
  132. package/src/app/chat/composer/text-utils.ts +107 -0
  133. package/src/app/chat/composer/trigger-popover.test.tsx +42 -0
  134. package/src/app/chat/composer/trigger-popover.tsx +116 -0
  135. package/src/app/chat/composer/types.ts +64 -0
  136. package/src/app/chat/composer/url-dialog.tsx +82 -0
  137. package/src/app/chat/composer/voice-activity.tsx +252 -0
  138. package/src/app/chat/hooks/use-composer-actions.test.ts +57 -0
  139. package/src/app/chat/hooks/use-composer-actions.ts +525 -0
  140. package/src/app/chat/hooks/use-file-drop-zone.ts +118 -0
  141. package/src/app/chat/index.tsx +390 -0
  142. package/src/app/chat/perf-probe.tsx +269 -0
  143. package/src/app/chat/right-rail/index.ts +1 -0
  144. package/src/app/chat/right-rail/preview-console-state.ts +82 -0
  145. package/src/app/chat/right-rail/preview-console.tsx +290 -0
  146. package/src/app/chat/right-rail/preview-file.tsx +559 -0
  147. package/src/app/chat/right-rail/preview-pane.test.tsx +43 -0
  148. package/src/app/chat/right-rail/preview-pane.tsx +657 -0
  149. package/src/app/chat/right-rail/preview.tsx +171 -0
  150. package/src/app/chat/scroll-to-bottom-button.test.tsx +67 -0
  151. package/src/app/chat/scroll-to-bottom-button.tsx +74 -0
  152. package/src/app/chat/sidebar/cron-jobs-section.tsx +325 -0
  153. package/src/app/chat/sidebar/index.tsx +1219 -0
  154. package/src/app/chat/sidebar/load-more-row.tsx +30 -0
  155. package/src/app/chat/sidebar/order.test.ts +21 -0
  156. package/src/app/chat/sidebar/order.ts +17 -0
  157. package/src/app/chat/sidebar/profile-switcher.tsx +516 -0
  158. package/src/app/chat/sidebar/session-actions-menu.tsx +264 -0
  159. package/src/app/chat/sidebar/session-row.tsx +257 -0
  160. package/src/app/chat/sidebar/virtual-session-list.tsx +154 -0
  161. package/src/app/chat/sidebar/workspace-groups.test.ts +149 -0
  162. package/src/app/chat/sidebar/workspace-groups.ts +326 -0
  163. package/src/app/chat/thread-loading.test.ts +34 -0
  164. package/src/app/chat/thread-loading.ts +26 -0
  165. package/src/app/command-center/index.tsx +654 -0
  166. package/src/app/command-palette/index.tsx +513 -0
  167. package/src/app/command-palette/marketplace-theme-page.tsx +157 -0
  168. package/src/app/cron/index.tsx +942 -0
  169. package/src/app/cron/job-state.ts +29 -0
  170. package/src/app/desktop-controller.tsx +938 -0
  171. package/src/app/floating-hud.ts +22 -0
  172. package/src/app/gateway/hooks/use-gateway-boot.test.tsx +265 -0
  173. package/src/app/gateway/hooks/use-gateway-boot.ts +387 -0
  174. package/src/app/gateway/hooks/use-gateway-request.ts +138 -0
  175. package/src/app/hooks/use-keybinds.ts +186 -0
  176. package/src/app/hooks/use-refresh-hotkey.ts +45 -0
  177. package/src/app/hooks/use-route-enum-param.ts +38 -0
  178. package/src/app/index.tsx +1 -0
  179. package/src/app/layout-constants.ts +13 -0
  180. package/src/app/messaging/index.tsx +648 -0
  181. package/src/app/messaging/platform-icon.tsx +93 -0
  182. package/src/app/model-picker-overlay.tsx +42 -0
  183. package/src/app/model-visibility-overlay.tsx +31 -0
  184. package/src/app/overlays/overlay-chrome.tsx +66 -0
  185. package/src/app/overlays/overlay-search-input.tsx +33 -0
  186. package/src/app/overlays/overlay-split-layout.tsx +130 -0
  187. package/src/app/overlays/overlay-view.tsx +91 -0
  188. package/src/app/page-search-shell.tsx +75 -0
  189. package/src/app/profiles/create-profile-dialog.tsx +154 -0
  190. package/src/app/profiles/delete-profile-dialog.tsx +65 -0
  191. package/src/app/profiles/index.tsx +671 -0
  192. package/src/app/profiles/rename-profile-dialog.tsx +125 -0
  193. package/src/app/right-sidebar/files/dnd-manager.ts +27 -0
  194. package/src/app/right-sidebar/files/ipc.test.ts +100 -0
  195. package/src/app/right-sidebar/files/ipc.ts +161 -0
  196. package/src/app/right-sidebar/files/remote-picker.tsx +177 -0
  197. package/src/app/right-sidebar/files/tree.tsx +224 -0
  198. package/src/app/right-sidebar/files/use-project-tree.test.ts +190 -0
  199. package/src/app/right-sidebar/files/use-project-tree.ts +268 -0
  200. package/src/app/right-sidebar/index.test.tsx +75 -0
  201. package/src/app/right-sidebar/index.tsx +395 -0
  202. package/src/app/right-sidebar/store.ts +15 -0
  203. package/src/app/right-sidebar/terminal/buffer.ts +65 -0
  204. package/src/app/right-sidebar/terminal/index.tsx +98 -0
  205. package/src/app/right-sidebar/terminal/persistent.tsx +122 -0
  206. package/src/app/right-sidebar/terminal/selection.ts +75 -0
  207. package/src/app/right-sidebar/terminal/use-terminal-session.ts +504 -0
  208. package/src/app/routes.ts +88 -0
  209. package/src/app/session/hooks/use-context-suggestions.ts +58 -0
  210. package/src/app/session/hooks/use-cwd-actions.ts +109 -0
  211. package/src/app/session/hooks/use-message-stream.ts +957 -0
  212. package/src/app/session/hooks/use-model-controls.test.tsx +198 -0
  213. package/src/app/session/hooks/use-model-controls.ts +106 -0
  214. package/src/app/session/hooks/use-nastech-config.ts +74 -0
  215. package/src/app/session/hooks/use-preview-routing.test.tsx +168 -0
  216. package/src/app/session/hooks/use-preview-routing.ts +223 -0
  217. package/src/app/session/hooks/use-prompt-actions.test.tsx +316 -0
  218. package/src/app/session/hooks/use-prompt-actions.ts +1030 -0
  219. package/src/app/session/hooks/use-route-resume.test.tsx +136 -0
  220. package/src/app/session/hooks/use-route-resume.ts +115 -0
  221. package/src/app/session/hooks/use-session-actions.test.tsx +119 -0
  222. package/src/app/session/hooks/use-session-actions.ts +885 -0
  223. package/src/app/session/hooks/use-session-state-cache.test.tsx +118 -0
  224. package/src/app/session/hooks/use-session-state-cache.ts +191 -0
  225. package/src/app/session-picker-overlay.tsx +32 -0
  226. package/src/app/session-switcher.tsx +107 -0
  227. package/src/app/settings/about-settings.tsx +173 -0
  228. package/src/app/settings/appearance-settings.tsx +162 -0
  229. package/src/app/settings/config-settings.tsx +384 -0
  230. package/src/app/settings/constants.ts +545 -0
  231. package/src/app/settings/credential-key-ui.tsx +373 -0
  232. package/src/app/settings/env-credentials.tsx +198 -0
  233. package/src/app/settings/env-var-actions-menu.tsx +136 -0
  234. package/src/app/settings/field-copy.ts +56 -0
  235. package/src/app/settings/gateway-settings.tsx +620 -0
  236. package/src/app/settings/helpers.test.ts +138 -0
  237. package/src/app/settings/helpers.ts +151 -0
  238. package/src/app/settings/index.tsx +237 -0
  239. package/src/app/settings/keys-settings.tsx +96 -0
  240. package/src/app/settings/mcp-settings.tsx +271 -0
  241. package/src/app/settings/model-settings.test.tsx +157 -0
  242. package/src/app/settings/model-settings.tsx +559 -0
  243. package/src/app/settings/notifications-settings.tsx +150 -0
  244. package/src/app/settings/primitives.tsx +115 -0
  245. package/src/app/settings/providers-settings.test.tsx +100 -0
  246. package/src/app/settings/providers-settings.tsx +258 -0
  247. package/src/app/settings/sessions-settings.tsx +276 -0
  248. package/src/app/settings/toolset-config-panel.test.tsx +289 -0
  249. package/src/app/settings/toolset-config-panel.tsx +449 -0
  250. package/src/app/settings/types.ts +42 -0
  251. package/src/app/settings/uninstall-section.tsx +185 -0
  252. package/src/app/settings/use-deep-link-highlight.ts +60 -0
  253. package/src/app/shell/app-shell.tsx +167 -0
  254. package/src/app/shell/gateway-menu-panel.tsx +150 -0
  255. package/src/app/shell/hooks/use-overlay-routing.ts +71 -0
  256. package/src/app/shell/hooks/use-status-snapshot.ts +57 -0
  257. package/src/app/shell/hooks/use-statusbar-items.tsx +403 -0
  258. package/src/app/shell/keybind-panel.tsx +220 -0
  259. package/src/app/shell/model-edit-submenu.test.tsx +84 -0
  260. package/src/app/shell/model-edit-submenu.tsx +245 -0
  261. package/src/app/shell/model-menu-panel.tsx +295 -0
  262. package/src/app/shell/sidebar-label.tsx +22 -0
  263. package/src/app/shell/statusbar-controls.tsx +185 -0
  264. package/src/app/shell/titlebar-controls.tsx +244 -0
  265. package/src/app/shell/titlebar.test.ts +26 -0
  266. package/src/app/shell/titlebar.ts +45 -0
  267. package/src/app/shell/use-group-registry.ts +39 -0
  268. package/src/app/skills/index.test.tsx +103 -0
  269. package/src/app/skills/index.tsx +371 -0
  270. package/src/app/types.ts +99 -0
  271. package/src/app/updates-overlay.tsx +369 -0
  272. package/src/components/Backdrop.tsx +114 -0
  273. package/src/components/assistant-ui/ansi-text.tsx +34 -0
  274. package/src/components/assistant-ui/clarify-tool.tsx +281 -0
  275. package/src/components/assistant-ui/directive-text.test.ts +39 -0
  276. package/src/components/assistant-ui/directive-text.tsx +389 -0
  277. package/src/components/assistant-ui/markdown-text.test.ts +204 -0
  278. package/src/components/assistant-ui/markdown-text.tsx +497 -0
  279. package/src/components/assistant-ui/message-render-boundary.test.tsx +80 -0
  280. package/src/components/assistant-ui/message-render-boundary.tsx +48 -0
  281. package/src/components/assistant-ui/streaming.test.tsx +739 -0
  282. package/src/components/assistant-ui/thread-list.tsx +307 -0
  283. package/src/components/assistant-ui/thread-virtualizer.tsx +512 -0
  284. package/src/components/assistant-ui/thread.tsx +1474 -0
  285. package/src/components/assistant-ui/todo-tool.tsx +109 -0
  286. package/src/components/assistant-ui/tool-approval-group.test.tsx +158 -0
  287. package/src/components/assistant-ui/tool-approval.test.tsx +81 -0
  288. package/src/components/assistant-ui/tool-approval.tsx +209 -0
  289. package/src/components/assistant-ui/tool-fallback-model.test.ts +66 -0
  290. package/src/components/assistant-ui/tool-fallback-model.ts +1368 -0
  291. package/src/components/assistant-ui/tool-fallback.tsx +466 -0
  292. package/src/components/assistant-ui/tooltip-icon-button.tsx +33 -0
  293. package/src/components/assistant-ui/user-message-edit.test.tsx +141 -0
  294. package/src/components/assistant-ui/user-message-text.tsx +150 -0
  295. package/src/components/boot-failure-overlay.tsx +246 -0
  296. package/src/components/boot-failure-reauth.test.ts +100 -0
  297. package/src/components/boot-failure-reauth.ts +81 -0
  298. package/src/components/brand-mark.tsx +19 -0
  299. package/src/components/chat/activity-timer-text.tsx +24 -0
  300. package/src/components/chat/activity-timer.test.tsx +43 -0
  301. package/src/components/chat/activity-timer.ts +64 -0
  302. package/src/components/chat/code-card.tsx +78 -0
  303. package/src/components/chat/compact-markdown.tsx +113 -0
  304. package/src/components/chat/composer-dock.ts +31 -0
  305. package/src/components/chat/diff-lines.tsx +54 -0
  306. package/src/components/chat/disclosure-row.tsx +63 -0
  307. package/src/components/chat/generated-image-context.tsx +19 -0
  308. package/src/components/chat/generated-image-result.tsx +174 -0
  309. package/src/components/chat/image-generation-placeholder.tsx +279 -0
  310. package/src/components/chat/intro-copy.jsonl +75 -0
  311. package/src/components/chat/intro.tsx +182 -0
  312. package/src/components/chat/preview-attachment.tsx +125 -0
  313. package/src/components/chat/shiki-highlighter.tsx +107 -0
  314. package/src/components/chat/status-row.tsx +70 -0
  315. package/src/components/chat/status-section.tsx +42 -0
  316. package/src/components/chat/terminal-output.tsx +50 -0
  317. package/src/components/chat/zoomable-image.tsx +177 -0
  318. package/src/components/desktop-install-overlay.tsx +595 -0
  319. package/src/components/desktop-onboarding-overlay.test.tsx +100 -0
  320. package/src/components/desktop-onboarding-overlay.tsx +1286 -0
  321. package/src/components/error-boundary.tsx +77 -0
  322. package/src/components/gateway-connecting-overlay.test.tsx +143 -0
  323. package/src/components/gateway-connecting-overlay.tsx +183 -0
  324. package/src/components/haptics-provider.tsx +19 -0
  325. package/src/components/language-switcher.test.tsx +53 -0
  326. package/src/components/language-switcher.tsx +175 -0
  327. package/src/components/model-picker.tsx +340 -0
  328. package/src/components/model-visibility-dialog.tsx +155 -0
  329. package/src/components/notifications.tsx +196 -0
  330. package/src/components/page-loader.tsx +34 -0
  331. package/src/components/pane-shell/context.ts +14 -0
  332. package/src/components/pane-shell/index.ts +4 -0
  333. package/src/components/pane-shell/pane-shell.test.tsx +333 -0
  334. package/src/components/pane-shell/pane-shell.tsx +330 -0
  335. package/src/components/prompt-overlays.tsx +234 -0
  336. package/src/components/session-picker.tsx +108 -0
  337. package/src/components/status-dot.tsx +26 -0
  338. package/src/components/ui/action-status.tsx +25 -0
  339. package/src/components/ui/alert.tsx +53 -0
  340. package/src/components/ui/badge.tsx +35 -0
  341. package/src/components/ui/braille-spinner.tsx +61 -0
  342. package/src/components/ui/button.tsx +81 -0
  343. package/src/components/ui/checkbox.tsx +27 -0
  344. package/src/components/ui/codicon.tsx +20 -0
  345. package/src/components/ui/command.tsx +111 -0
  346. package/src/components/ui/confirm-dialog.tsx +109 -0
  347. package/src/components/ui/context-menu.tsx +141 -0
  348. package/src/components/ui/control.ts +25 -0
  349. package/src/components/ui/copy-button.test.tsx +36 -0
  350. package/src/components/ui/copy-button.tsx +229 -0
  351. package/src/components/ui/dialog.tsx +152 -0
  352. package/src/components/ui/disclosure-caret.tsx +20 -0
  353. package/src/components/ui/dropdown-menu.tsx +291 -0
  354. package/src/components/ui/error-state.tsx +50 -0
  355. package/src/components/ui/fade-text.tsx +110 -0
  356. package/src/components/ui/glyph-spinner.tsx +63 -0
  357. package/src/components/ui/input.tsx +22 -0
  358. package/src/components/ui/kbd.tsx +37 -0
  359. package/src/components/ui/loader.tsx +558 -0
  360. package/src/components/ui/log-view.tsx +17 -0
  361. package/src/components/ui/pagination.tsx +114 -0
  362. package/src/components/ui/popover.tsx +44 -0
  363. package/src/components/ui/scroll-area.tsx +43 -0
  364. package/src/components/ui/search-field.tsx +80 -0
  365. package/src/components/ui/segmented-control.tsx +51 -0
  366. package/src/components/ui/select.tsx +92 -0
  367. package/src/components/ui/separator.tsx +26 -0
  368. package/src/components/ui/sheet.tsx +116 -0
  369. package/src/components/ui/sidebar.tsx +674 -0
  370. package/src/components/ui/skeleton.tsx +7 -0
  371. package/src/components/ui/switch.tsx +49 -0
  372. package/src/components/ui/tabs.tsx +36 -0
  373. package/src/components/ui/text-tab.tsx +43 -0
  374. package/src/components/ui/textarea.tsx +11 -0
  375. package/src/components/ui/tool-icon.tsx +65 -0
  376. package/src/components/ui/tooltip.tsx +69 -0
  377. package/src/fonts/JetBrainsMono-Bold.woff2 +0 -0
  378. package/src/fonts/JetBrainsMono-Italic.woff2 +0 -0
  379. package/src/fonts/JetBrainsMono-Regular.woff2 +0 -0
  380. package/src/global.d.ts +457 -0
  381. package/src/hooks/use-image-download.ts +85 -0
  382. package/src/hooks/use-media-query.ts +24 -0
  383. package/src/hooks/use-mobile.ts +3 -0
  384. package/src/hooks/use-resize-observer.ts +38 -0
  385. package/src/hooks/use-worktree-info.ts +68 -0
  386. package/src/i18n/catalog.ts +12 -0
  387. package/src/i18n/context.test.tsx +232 -0
  388. package/src/i18n/context.tsx +183 -0
  389. package/src/i18n/define-locale.ts +41 -0
  390. package/src/i18n/en.ts +1779 -0
  391. package/src/i18n/index.ts +20 -0
  392. package/src/i18n/ja.ts +1890 -0
  393. package/src/i18n/languages.test.ts +43 -0
  394. package/src/i18n/languages.ts +86 -0
  395. package/src/i18n/runtime.test.ts +75 -0
  396. package/src/i18n/runtime.ts +53 -0
  397. package/src/i18n/types.ts +1452 -0
  398. package/src/i18n/zh-hant.ts +1849 -0
  399. package/src/i18n/zh.ts +1923 -0
  400. package/src/lib/ansi.test.ts +123 -0
  401. package/src/lib/ansi.ts +175 -0
  402. package/src/lib/chat-messages.test.ts +708 -0
  403. package/src/lib/chat-messages.ts +885 -0
  404. package/src/lib/chat-runtime.test.ts +18 -0
  405. package/src/lib/chat-runtime.ts +335 -0
  406. package/src/lib/clipboard.ts +28 -0
  407. package/src/lib/commit-changelog.test.ts +114 -0
  408. package/src/lib/commit-changelog.ts +177 -0
  409. package/src/lib/completion-sound.ts +519 -0
  410. package/src/lib/desktop-fs.test.ts +116 -0
  411. package/src/lib/desktop-fs.ts +113 -0
  412. package/src/lib/desktop-slash-commands.test.ts +126 -0
  413. package/src/lib/desktop-slash-commands.ts +286 -0
  414. package/src/lib/embedded-images.test.ts +35 -0
  415. package/src/lib/embedded-images.ts +60 -0
  416. package/src/lib/external-link.test.tsx +168 -0
  417. package/src/lib/external-link.tsx +303 -0
  418. package/src/lib/gateway-events.test.ts +27 -0
  419. package/src/lib/gateway-events.ts +49 -0
  420. package/src/lib/gateway-ws-url.test.ts +78 -0
  421. package/src/lib/gateway-ws-url.ts +91 -0
  422. package/src/lib/generated-images.test.ts +97 -0
  423. package/src/lib/generated-images.ts +116 -0
  424. package/src/lib/haptics.ts +129 -0
  425. package/src/lib/icons.ts +203 -0
  426. package/src/lib/incremental-external-store-runtime.ts +188 -0
  427. package/src/lib/katex-memo.ts +260 -0
  428. package/src/lib/keybinds/actions.ts +125 -0
  429. package/src/lib/keybinds/combo.test.ts +86 -0
  430. package/src/lib/keybinds/combo.ts +169 -0
  431. package/src/lib/local-preview.ts +126 -0
  432. package/src/lib/markdown-code.test.ts +23 -0
  433. package/src/lib/markdown-code.ts +195 -0
  434. package/src/lib/markdown-preprocess.ts +386 -0
  435. package/src/lib/media.remote.test.ts +58 -0
  436. package/src/lib/media.ts +111 -0
  437. package/src/lib/model-status-label.test.ts +31 -0
  438. package/src/lib/model-status-label.ts +103 -0
  439. package/src/lib/mutable-ref.ts +6 -0
  440. package/src/lib/preview-targets.test.ts +27 -0
  441. package/src/lib/preview-targets.ts +63 -0
  442. package/src/lib/profile-color.ts +58 -0
  443. package/src/lib/provider-setup-errors.test.ts +26 -0
  444. package/src/lib/provider-setup-errors.ts +12 -0
  445. package/src/lib/query-client.ts +13 -0
  446. package/src/lib/remend-tail.test.ts +105 -0
  447. package/src/lib/remend-tail.ts +108 -0
  448. package/src/lib/runtime-readiness.test.ts +65 -0
  449. package/src/lib/runtime-readiness.ts +147 -0
  450. package/src/lib/session-export.ts +57 -0
  451. package/src/lib/session-search.test.ts +58 -0
  452. package/src/lib/session-search.ts +19 -0
  453. package/src/lib/session-source.ts +62 -0
  454. package/src/lib/speech-text.ts +35 -0
  455. package/src/lib/statusbar.ts +91 -0
  456. package/src/lib/storage.test.ts +25 -0
  457. package/src/lib/storage.ts +107 -0
  458. package/src/lib/todos.test.ts +35 -0
  459. package/src/lib/todos.ts +51 -0
  460. package/src/lib/tool-result-summary.test.ts +106 -0
  461. package/src/lib/tool-result-summary.ts +467 -0
  462. package/src/lib/update-copy.test.ts +38 -0
  463. package/src/lib/update-copy.ts +44 -0
  464. package/src/lib/use-enter-animation.ts +100 -0
  465. package/src/lib/utils.ts +6 -0
  466. package/src/lib/voice-playback.ts +128 -0
  467. package/src/lib/yolo-session.ts +26 -0
  468. package/src/main.tsx +43 -0
  469. package/src/nastech.test.ts +49 -0
  470. package/src/nastech.ts +718 -0
  471. package/src/store/activity.ts +100 -0
  472. package/src/store/boot.ts +91 -0
  473. package/src/store/clarify.test.ts +81 -0
  474. package/src/store/clarify.ts +69 -0
  475. package/src/store/command-palette.ts +20 -0
  476. package/src/store/compaction.test.ts +53 -0
  477. package/src/store/compaction.ts +38 -0
  478. package/src/store/completion-sound.ts +32 -0
  479. package/src/store/composer-input-history.test.ts +147 -0
  480. package/src/store/composer-input-history.ts +158 -0
  481. package/src/store/composer-queue.test.ts +148 -0
  482. package/src/store/composer-queue.ts +239 -0
  483. package/src/store/composer-status.test.ts +99 -0
  484. package/src/store/composer-status.ts +277 -0
  485. package/src/store/composer.test.ts +106 -0
  486. package/src/store/composer.ts +184 -0
  487. package/src/store/cron.ts +19 -0
  488. package/src/store/gateway.ts +290 -0
  489. package/src/store/haptics.ts +17 -0
  490. package/src/store/keybinds.ts +139 -0
  491. package/src/store/layout.ts +176 -0
  492. package/src/store/model-presets.test.ts +51 -0
  493. package/src/store/model-presets.ts +86 -0
  494. package/src/store/model-visibility.test.ts +37 -0
  495. package/src/store/model-visibility.ts +108 -0
  496. package/src/store/native-notifications.test.ts +192 -0
  497. package/src/store/native-notifications.ts +203 -0
  498. package/src/store/notifications.ts +165 -0
  499. package/src/store/onboarding.test.ts +372 -0
  500. package/src/store/onboarding.ts +866 -0
  501. package/src/store/panes.test.ts +146 -0
  502. package/src/store/panes.ts +145 -0
  503. package/src/store/preview.test.ts +135 -0
  504. package/src/store/preview.ts +466 -0
  505. package/src/store/profile.test.ts +89 -0
  506. package/src/store/profile.ts +365 -0
  507. package/src/store/prompts.test.ts +121 -0
  508. package/src/store/prompts.ts +115 -0
  509. package/src/store/session-switcher.test.ts +115 -0
  510. package/src/store/session-switcher.ts +128 -0
  511. package/src/store/session-sync.ts +25 -0
  512. package/src/store/session.test.ts +131 -0
  513. package/src/store/session.ts +255 -0
  514. package/src/store/subagents.test.ts +111 -0
  515. package/src/store/subagents.ts +260 -0
  516. package/src/store/thread-scroll.ts +46 -0
  517. package/src/store/todos.test.ts +47 -0
  518. package/src/store/todos.ts +64 -0
  519. package/src/store/tool-diffs.ts +23 -0
  520. package/src/store/tool-dismiss.ts +45 -0
  521. package/src/store/tool-view.ts +91 -0
  522. package/src/store/translucency.ts +38 -0
  523. package/src/store/updates.test.ts +77 -0
  524. package/src/store/updates.ts +315 -0
  525. package/src/store/voice-playback.ts +24 -0
  526. package/src/store/windows.test.ts +143 -0
  527. package/src/store/windows.ts +77 -0
  528. package/src/styles.css +1235 -0
  529. package/src/themes/color.ts +142 -0
  530. package/src/themes/context.tsx +339 -0
  531. package/src/themes/index.ts +3 -0
  532. package/src/themes/install.test.ts +119 -0
  533. package/src/themes/install.ts +95 -0
  534. package/src/themes/presets.test.ts +33 -0
  535. package/src/themes/presets.ts +293 -0
  536. package/src/themes/profile-theme.test.ts +41 -0
  537. package/src/themes/types.ts +66 -0
  538. package/src/themes/use-skin-command.ts +60 -0
  539. package/src/themes/user-themes.test.ts +63 -0
  540. package/src/themes/user-themes.ts +122 -0
  541. package/src/themes/vscode.test.ts +171 -0
  542. package/src/themes/vscode.ts +343 -0
  543. package/src/types/nastech.ts +646 -0
  544. package/src/vite-env.d.ts +1 -0
  545. package/tsconfig.json +25 -0
  546. package/vite.config.ts +56 -0
@@ -0,0 +1,938 @@
1
+ import { useStore } from '@nanostores/react'
2
+ import { useQueryClient } from '@tanstack/react-query'
3
+ import { lazy, Suspense, useCallback, useEffect, useMemo, useRef } from 'react'
4
+ import { Navigate, Route, Routes, useLocation, useNavigate, useParams } from 'react-router-dom'
5
+
6
+ import { BootFailureOverlay } from '@/components/boot-failure-overlay'
7
+ import { DesktopInstallOverlay } from '@/components/desktop-install-overlay'
8
+ import { DesktopOnboardingOverlay } from '@/components/desktop-onboarding-overlay'
9
+ import { GatewayConnectingOverlay } from '@/components/gateway-connecting-overlay'
10
+ import { Pane, PaneMain } from '@/components/pane-shell'
11
+ import { useSkinCommand } from '@/themes/use-skin-command'
12
+
13
+ import { formatRefValue } from '../components/assistant-ui/directive-text'
14
+ import { getCronJobs, getSessionMessages, listAllProfileSessions, type SessionInfo, triggerCronJob } from '../nastech'
15
+ import { preserveLocalAssistantErrors, toChatMessages } from '../lib/chat-messages'
16
+ import { setCronFocusJobId, setCronJobs } from '../store/cron'
17
+ import {
18
+ $panesFlipped,
19
+ $pinnedSessionIds,
20
+ $sessionsLimit,
21
+ bumpSessionsLimit,
22
+ FILE_BROWSER_DEFAULT_WIDTH,
23
+ FILE_BROWSER_MAX_WIDTH,
24
+ FILE_BROWSER_MIN_WIDTH,
25
+ pinSession,
26
+ SIDEBAR_DEFAULT_WIDTH,
27
+ SIDEBAR_MAX_WIDTH,
28
+ SIDEBAR_SESSIONS_PAGE_SIZE,
29
+ unpinSession
30
+ } from '../store/layout'
31
+ import { $filePreviewTarget, $previewTarget, closeActiveRightRailTab } from '../store/preview'
32
+ import {
33
+ $activeGatewayProfile,
34
+ $freshSessionRequest,
35
+ $profileScope,
36
+ ALL_PROFILES,
37
+ normalizeProfileKey,
38
+ refreshActiveProfile
39
+ } from '../store/profile'
40
+ import {
41
+ $activeSessionId,
42
+ $currentCwd,
43
+ $freshDraftReady,
44
+ $gatewayState,
45
+ $selectedStoredSessionId,
46
+ $sessions,
47
+ $workingSessionIds,
48
+ CRON_SECTION_LIMIT,
49
+ mergeSessionPage,
50
+ sessionPinId,
51
+ setAwaitingResponse,
52
+ setBusy,
53
+ setCronSessions,
54
+ setCurrentBranch,
55
+ setCurrentCwd,
56
+ setCurrentModel,
57
+ setCurrentProvider,
58
+ setMessages,
59
+ setSessionProfileTotals,
60
+ setSessions,
61
+ setSessionsLoading,
62
+ setSessionsTotal
63
+ } from '../store/session'
64
+ import { openUpdatesWindow, startUpdatePoller, stopUpdatePoller } from '../store/updates'
65
+
66
+ import { ChatView } from './chat'
67
+ import { useComposerActions } from './chat/hooks/use-composer-actions'
68
+ import {
69
+ ChatPreviewRail,
70
+ PREVIEW_RAIL_MAX_WIDTH,
71
+ PREVIEW_RAIL_MIN_WIDTH,
72
+ PREVIEW_RAIL_PANE_WIDTH
73
+ } from './chat/right-rail'
74
+ import { ChatSidebar } from './chat/sidebar'
75
+ import { CommandPalette } from './command-palette'
76
+ import { useGatewayBoot } from './gateway/hooks/use-gateway-boot'
77
+ import { useGatewayRequest } from './gateway/hooks/use-gateway-request'
78
+ import { useKeybinds } from './hooks/use-keybinds'
79
+ import { ModelPickerOverlay } from './model-picker-overlay'
80
+ import { ModelVisibilityOverlay } from './model-visibility-overlay'
81
+ import { RightSidebarPane } from './right-sidebar'
82
+ import { $terminalTakeover } from './right-sidebar/store'
83
+ import { PersistentTerminal, TerminalSlot } from './right-sidebar/terminal/persistent'
84
+ import { CRON_ROUTE, NEW_CHAT_ROUTE, routeSessionId, sessionRoute, SETTINGS_ROUTE } from './routes'
85
+ import { useContextSuggestions } from './session/hooks/use-context-suggestions'
86
+ import { useCwdActions } from './session/hooks/use-cwd-actions'
87
+ import { useNasTechConfig } from './session/hooks/use-nastech-config'
88
+ import { useMessageStream } from './session/hooks/use-message-stream'
89
+ import { useModelControls } from './session/hooks/use-model-controls'
90
+ import { usePreviewRouting } from './session/hooks/use-preview-routing'
91
+ import { usePromptActions } from './session/hooks/use-prompt-actions'
92
+ import { useRouteResume } from './session/hooks/use-route-resume'
93
+ import { useSessionActions } from './session/hooks/use-session-actions'
94
+ import { useSessionStateCache } from './session/hooks/use-session-state-cache'
95
+ import { AppShell } from './shell/app-shell'
96
+ import { useOverlayRouting } from './shell/hooks/use-overlay-routing'
97
+ import { useStatusSnapshot } from './shell/hooks/use-status-snapshot'
98
+ import { useStatusbarItems } from './shell/hooks/use-statusbar-items'
99
+ import { ModelMenuPanel } from './shell/model-menu-panel'
100
+ import type { StatusbarItem } from './shell/statusbar-controls'
101
+ import type { TitlebarTool } from './shell/titlebar-controls'
102
+ import { useGroupRegistry } from './shell/use-group-registry'
103
+ import { UpdatesOverlay } from './updates-overlay'
104
+
105
+ const AgentsView = lazy(async () => ({ default: (await import('./agents')).AgentsView }))
106
+ const ArtifactsView = lazy(async () => ({ default: (await import('./artifacts')).ArtifactsView }))
107
+ const CommandCenterView = lazy(async () => ({ default: (await import('./command-center')).CommandCenterView }))
108
+ const CronView = lazy(async () => ({ default: (await import('./cron')).CronView }))
109
+ const MessagingView = lazy(async () => ({ default: (await import('./messaging')).MessagingView }))
110
+ const ProfilesView = lazy(async () => ({ default: (await import('./profiles')).ProfilesView }))
111
+ const SettingsView = lazy(async () => ({ default: (await import('./settings')).SettingsView }))
112
+ const SkillsView = lazy(async () => ({ default: (await import('./skills')).SkillsView }))
113
+
114
+ // Latest cron-job sessions surfaced in the collapsed "Cron jobs" section. The
115
+ // Cron sessions are written by a background scheduler tick (the desktop
116
+ // backend), so no user action signals the UI. Poll the bounded cron list on
117
+ // this cadence while the app is open + visible so new runs surface promptly
118
+ // instead of waiting for the next user-triggered refreshSessions().
119
+ const CRON_POLL_INTERVAL_MS = 30_000
120
+
121
+ // Cheap signature compare so the poll only swaps the atom (and re-renders the
122
+ // sidebar) when the visible cron rows actually changed.
123
+ function sameCronSignature(a: SessionInfo[], b: SessionInfo[]): boolean {
124
+ if (a.length !== b.length) {return false}
125
+
126
+ return a.every((session, i) => session.id === b[i]?.id && session.title === b[i]?.title)
127
+ }
128
+
129
+ // Rows a session refresh must preserve even if the aggregator omits them:
130
+ // in-flight first turns (message_count 0), pinned rows aged off the page, and
131
+ // the actively-viewed chat (its "working" flag clears a beat before the
132
+ // aggregator sees the persisted row). Pass `scope` to only keep the active row
133
+ // when it belongs to the profile being paged.
134
+ function sessionsToKeep(scope?: string): Set<string> {
135
+ const keep = new Set<string>([...$workingSessionIds.get(), ...$pinnedSessionIds.get()])
136
+ const active = $selectedStoredSessionId.get()
137
+
138
+ if (active) {
139
+ const session = scope ? $sessions.get().find(s => s.id === active) : null
140
+
141
+ if (!scope || !session || normalizeProfileKey(session.profile) === scope) {
142
+ keep.add(active)
143
+ }
144
+ }
145
+
146
+ return keep
147
+ }
148
+
149
+ export function DesktopController() {
150
+ const queryClient = useQueryClient()
151
+ const location = useLocation()
152
+ const navigate = useNavigate()
153
+
154
+ const busyRef = useRef(false)
155
+ const creatingSessionRef = useRef(false)
156
+ const refreshSessionsRequestRef = useRef(0)
157
+
158
+ const gatewayState = useStore($gatewayState)
159
+ const activeSessionId = useStore($activeSessionId)
160
+ const currentCwd = useStore($currentCwd)
161
+ const freshDraftReady = useStore($freshDraftReady)
162
+ const filePreviewTarget = useStore($filePreviewTarget)
163
+ const previewTarget = useStore($previewTarget)
164
+ const selectedStoredSessionId = useStore($selectedStoredSessionId)
165
+ const terminalTakeover = useStore($terminalTakeover)
166
+ const panesFlipped = useStore($panesFlipped)
167
+ const profileScope = useStore($profileScope)
168
+
169
+ const routedSessionId = routeSessionId(location.pathname)
170
+ const routeToken = `${location.pathname}:${location.search}:${location.hash}`
171
+ const routeTokenRef = useRef(routeToken)
172
+ routeTokenRef.current = routeToken
173
+ const getRouteToken = useCallback(() => routeTokenRef.current, [])
174
+
175
+ const {
176
+ agentsOpen,
177
+ chatOpen,
178
+ closeOverlayToPreviousRoute,
179
+ commandCenterInitialSection,
180
+ commandCenterOpen,
181
+ cronOpen,
182
+ currentView,
183
+ openAgents,
184
+ openCommandCenterSection,
185
+ profilesOpen,
186
+ settingsOpen,
187
+ toggleCommandCenter
188
+ } = useOverlayRouting()
189
+
190
+ const terminalTakeoverActive = chatOpen && terminalTakeover
191
+
192
+ const titlebarToolGroups = useGroupRegistry<TitlebarTool>()
193
+ const statusbarItemGroups = useGroupRegistry<StatusbarItem>()
194
+ const setTitlebarToolGroup = titlebarToolGroups.set
195
+ const setStatusbarItemGroup = statusbarItemGroups.set
196
+
197
+ const {
198
+ activeSessionIdRef,
199
+ ensureSessionState,
200
+ runtimeIdByStoredSessionIdRef,
201
+ selectedStoredSessionIdRef,
202
+ sessionStateByRuntimeIdRef,
203
+ syncSessionStateToView,
204
+ updateSessionState
205
+ } = useSessionStateCache({
206
+ activeSessionId,
207
+ busyRef,
208
+ selectedStoredSessionId,
209
+ setAwaitingResponse,
210
+ setBusy,
211
+ setMessages
212
+ })
213
+
214
+ const { connectionRef, gatewayRef, requestGateway } = useGatewayRequest()
215
+
216
+ useEffect(() => {
217
+ window.NASTECHDesktop?.setPreviewShortcutActive?.(Boolean(chatOpen && (filePreviewTarget || previewTarget)))
218
+ }, [chatOpen, filePreviewTarget, previewTarget])
219
+
220
+ useEffect(() => {
221
+ startUpdatePoller()
222
+ const unsubscribe = window.NASTECHDesktop?.onOpenUpdatesRequested?.(() => openUpdatesWindow())
223
+
224
+ return () => {
225
+ unsubscribe?.()
226
+ stopUpdatePoller()
227
+ }
228
+ }, [])
229
+
230
+ useEffect(() => {
231
+ const onKeyDown = (event: KeyboardEvent) => {
232
+ if (!$filePreviewTarget.get() && !$previewTarget.get()) {
233
+ return
234
+ }
235
+
236
+ if ((event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && event.key.toLowerCase() === 'w') {
237
+ event.preventDefault()
238
+ event.stopPropagation()
239
+ closeActiveRightRailTab()
240
+ }
241
+ }
242
+
243
+ const unsubscribe = window.NASTECHDesktop?.onClosePreviewRequested?.(closeActiveRightRailTab)
244
+
245
+ window.addEventListener('keydown', onKeyDown, { capture: true })
246
+
247
+ return () => {
248
+ unsubscribe?.()
249
+ window.removeEventListener('keydown', onKeyDown, { capture: true })
250
+ }
251
+ }, [])
252
+
253
+ // Cron-job sessions as their own list (latest N). Independent of the recents
254
+ // page so the two never compete for slots. Cheap + bounded. Kept (even though
255
+ // the sidebar now lists cron *jobs*, not run sessions) so a pinned cron run
256
+ // still resolves into the Pinned section via sessionByAnyId.
257
+ const refreshCronSessions = useCallback(async () => {
258
+ try {
259
+ const { sessions } = await listAllProfileSessions(CRON_SECTION_LIMIT, 1, 'exclude', 'recent', 'all', {
260
+ source: 'cron'
261
+ })
262
+
263
+ setCronSessions(prev => (sameCronSignature(prev, sessions) ? prev : sessions))
264
+ } catch {
265
+ // Non-fatal: the cron section just stays empty/stale.
266
+ }
267
+ }, [])
268
+
269
+ // Cron *jobs* drive the sidebar "Cron jobs" section. Jobs are created
270
+ // synchronously (agent tool call or the cron UI), so refreshing here right
271
+ // after an agent turn surfaces a new job immediately; the interval poll keeps
272
+ // next-run/state fresh as the scheduler advances them.
273
+ const refreshCronJobs = useCallback(async () => {
274
+ try {
275
+ const jobs = await getCronJobs()
276
+
277
+ setCronJobs(jobs)
278
+ } catch {
279
+ // Non-fatal: the cron section just keeps its last-known jobs.
280
+ }
281
+ }, [])
282
+
283
+ const refreshSessions = useCallback(async () => {
284
+ const requestId = refreshSessionsRequestRef.current + 1
285
+ refreshSessionsRequestRef.current = requestId
286
+ setSessionsLoading(true)
287
+
288
+ try {
289
+ const limit = $sessionsLimit.get()
290
+
291
+ // Require at least one message so abandoned/empty "Untitled" drafts (one
292
+ // was created per TUI/desktop launch before the lazy-create fix) don't
293
+ // clutter the sidebar.
294
+ // Unified cross-profile list (served read-only off each profile's
295
+ // state.db; no per-profile backend is spawned). Single-profile users get
296
+ // the same rows tagged profile="default". Cron sessions are excluded here
297
+ // and fetched separately (refreshCronSessions) so the scheduler's
298
+ // always-newest rows can't consume the recents page budget.
299
+ // Scope the fetch to the active profile (not always 'all') so a profile
300
+ // with few recent sessions isn't windowed out of the cross-profile
301
+ // recency page — the empty-history-on-profile-switch bug.
302
+ const sessionProfile = profileScope === ALL_PROFILES ? 'all' : profileScope
303
+ const result = await listAllProfileSessions(limit, 1, 'exclude', 'recent', sessionProfile, {
304
+ excludeSources: ['cron']
305
+ })
306
+
307
+ if (refreshSessionsRequestRef.current === requestId) {
308
+ setSessions(prev => mergeSessionPage(prev, result.sessions, sessionsToKeep()))
309
+ setSessionsTotal(typeof result.total === 'number' ? result.total : result.sessions.length)
310
+ setSessionProfileTotals(result.profile_totals ?? {})
311
+ }
312
+ } finally {
313
+ if (refreshSessionsRequestRef.current === requestId) {
314
+ setSessionsLoading(false)
315
+ }
316
+ }
317
+
318
+ void refreshCronSessions()
319
+ void refreshCronJobs()
320
+ }, [profileScope, refreshCronSessions, refreshCronJobs])
321
+
322
+ const loadMoreSessions = useCallback(() => {
323
+ bumpSessionsLimit()
324
+ void refreshSessions()
325
+ }, [refreshSessions])
326
+
327
+ // ALL-profiles view pages one profile at a time: fetch that profile's next
328
+ // page and merge it in place, leaving every other profile's rows untouched.
329
+ const loadMoreSessionsForProfile = useCallback(async (profile: string) => {
330
+ const key = normalizeProfileKey(profile)
331
+ const inKey = (s: SessionInfo) => normalizeProfileKey(s.profile) === key
332
+ const loaded = $sessions.get().filter(inKey).length
333
+
334
+ const result = await listAllProfileSessions(loaded + SIDEBAR_SESSIONS_PAGE_SIZE, 1, 'exclude', 'recent', key, {
335
+ excludeSources: ['cron']
336
+ })
337
+
338
+ const keep = sessionsToKeep(key)
339
+
340
+ setSessions(prev => [...prev.filter(s => !inKey(s)), ...mergeSessionPage(prev.filter(inKey), result.sessions, keep)])
341
+
342
+ const total = result.profile_totals?.[key] ?? result.total ?? result.sessions.length
343
+ setSessionProfileTotals(prev => ({ ...prev, [key]: Math.max(total, result.sessions.length) }))
344
+ }, [])
345
+
346
+ const toggleSelectedPin = useCallback(() => {
347
+ const sessionId = $selectedStoredSessionId.get()
348
+
349
+ if (!sessionId) {
350
+ return
351
+ }
352
+
353
+ // Pin on the durable lineage-root id so the pin survives auto-compression.
354
+ const session = $sessions.get().find(s => s.id === sessionId || s._lineage_root_id === sessionId)
355
+ const pinId = session ? sessionPinId(session) : sessionId
356
+
357
+ if ($pinnedSessionIds.get().includes(pinId)) {
358
+ unpinSession(pinId)
359
+ } else {
360
+ pinSession(pinId)
361
+ }
362
+ }, [])
363
+
364
+ const { gatewayLogLines, inferenceStatus, statusSnapshot } = useStatusSnapshot(gatewayState, requestGateway)
365
+
366
+ const updateActiveSessionRuntimeInfo = useCallback(
367
+ (info: { branch?: string; cwd?: string }) => {
368
+ const sessionId = activeSessionIdRef.current
369
+
370
+ if (!sessionId) {
371
+ return
372
+ }
373
+
374
+ updateSessionState(sessionId, state => ({
375
+ ...state,
376
+ branch: info.branch ?? state.branch,
377
+ cwd: info.cwd ?? state.cwd
378
+ }))
379
+ },
380
+ [activeSessionIdRef, updateSessionState]
381
+ )
382
+
383
+ const { changeSessionCwd, refreshProjectBranch } = useCwdActions({
384
+ activeSessionId,
385
+ activeSessionIdRef,
386
+ onSessionRuntimeInfo: updateActiveSessionRuntimeInfo,
387
+ requestGateway
388
+ })
389
+
390
+ const { refreshNasTechConfig, sttEnabled, voiceMaxRecordingSeconds } = useNasTechConfig({
391
+ activeSessionIdRef,
392
+ refreshProjectBranch
393
+ })
394
+
395
+ const { refreshCurrentModel, selectModel, updateModelOptionsCache } = useModelControls({
396
+ activeSessionId,
397
+ queryClient,
398
+ requestGateway
399
+ })
400
+
401
+ const openProviderSettings = useCallback(() => {
402
+ navigate(`${SETTINGS_ROUTE}?tab=providers`)
403
+ }, [navigate])
404
+
405
+ const modelMenuContent = useMemo(
406
+ () =>
407
+ gatewayState === 'open' ? (
408
+ <ModelMenuPanel
409
+ gateway={gatewayRef.current || undefined}
410
+ onSelectModel={selectModel}
411
+ requestGateway={requestGateway}
412
+ />
413
+ ) : null,
414
+ [gatewayRef, gatewayState, requestGateway, selectModel]
415
+ )
416
+
417
+ useContextSuggestions({
418
+ activeSessionId,
419
+ activeSessionIdRef,
420
+ currentCwd,
421
+ gatewayState,
422
+ requestGateway
423
+ })
424
+
425
+ const hydrateFromStoredSession = useCallback(
426
+ async (
427
+ attempts = 1,
428
+ storedSessionId = selectedStoredSessionIdRef.current,
429
+ runtimeSessionId = activeSessionIdRef.current
430
+ ) => {
431
+ if (!storedSessionId || !runtimeSessionId) {
432
+ return
433
+ }
434
+
435
+ const storedProfile = $sessions.get().find(session => session.id === storedSessionId)?.profile
436
+
437
+ for (let index = 0; index < Math.max(1, attempts); index += 1) {
438
+ try {
439
+ const latest = await getSessionMessages(storedSessionId, storedProfile)
440
+ updateSessionState(
441
+ runtimeSessionId,
442
+ state => ({
443
+ ...state,
444
+ messages: preserveLocalAssistantErrors(toChatMessages(latest.messages), state.messages)
445
+ }),
446
+ storedSessionId
447
+ )
448
+
449
+ return
450
+ } catch {
451
+ // Best-effort fallback when live stream payloads are empty.
452
+ }
453
+
454
+ if (index < attempts - 1) {
455
+ await new Promise(resolve => window.setTimeout(resolve, 250))
456
+ }
457
+ }
458
+ },
459
+ [activeSessionIdRef, selectedStoredSessionIdRef, updateSessionState]
460
+ )
461
+
462
+ const { handleGatewayEvent } = useMessageStream({
463
+ activeSessionIdRef,
464
+ hydrateFromStoredSession,
465
+ queryClient,
466
+ refreshNasTechConfig,
467
+ refreshSessions,
468
+ updateSessionState
469
+ })
470
+
471
+ const { handleDesktopGatewayEvent, restartPreviewServer } = usePreviewRouting({
472
+ activeSessionIdRef,
473
+ baseHandleGatewayEvent: handleGatewayEvent,
474
+ currentCwd,
475
+ currentView,
476
+ requestGateway,
477
+ routedSessionId,
478
+ selectedStoredSessionId
479
+ })
480
+
481
+ const {
482
+ archiveSession,
483
+ branchCurrentSession,
484
+ createBackendSessionForSend,
485
+ openSettings,
486
+ removeSession,
487
+ resumeSession,
488
+ selectSidebarItem,
489
+ startFreshSessionDraft
490
+ } = useSessionActions({
491
+ activeSessionId,
492
+ activeSessionIdRef,
493
+ busyRef,
494
+ creatingSessionRef,
495
+ ensureSessionState,
496
+ getRouteToken,
497
+ navigate,
498
+ requestGateway,
499
+ runtimeIdByStoredSessionIdRef,
500
+ selectedStoredSessionId,
501
+ selectedStoredSessionIdRef,
502
+ sessionStateByRuntimeIdRef,
503
+ syncSessionStateToView,
504
+ updateSessionState
505
+ })
506
+
507
+ // Single global listener for every rebindable hotkey (incl. profile switching)
508
+ // plus the on-screen keybind editor's capture mode.
509
+ useKeybinds({
510
+ startFreshSession: startFreshSessionDraft,
511
+ toggleCommandCenter,
512
+ toggleSelectedPin
513
+ })
514
+
515
+ // A profile switch/create drops to a fresh new-session draft so the previously
516
+ // open session doesn't bleed across contexts. Skip the initial value.
517
+ const freshSessionRequest = useStore($freshSessionRequest)
518
+ const lastFreshRef = useRef(freshSessionRequest)
519
+
520
+ useEffect(() => {
521
+ if (freshSessionRequest === lastFreshRef.current) {
522
+ return
523
+ }
524
+
525
+ lastFreshRef.current = freshSessionRequest
526
+ startFreshSessionDraft()
527
+ }, [freshSessionRequest, startFreshSessionDraft])
528
+
529
+ // Swapping the live gateway to another profile must re-pull that profile's
530
+ // global model + active-profile pill. Both are nanostores, so the blanket
531
+ // invalidateQueries() the profile store fires on swap doesn't touch them —
532
+ // without this the statusbar keeps showing the previous profile's model
533
+ // (the "forgets the LLM setting" report). gatewayState stays 'open' across a
534
+ // swap (background sockets persist), so the open→open effect won't re-run.
535
+ const activeGatewayProfile = useStore($activeGatewayProfile)
536
+ const lastGatewayProfileRef = useRef(activeGatewayProfile)
537
+
538
+ useEffect(() => {
539
+ if (activeGatewayProfile === lastGatewayProfileRef.current) {
540
+ return
541
+ }
542
+
543
+ lastGatewayProfileRef.current = activeGatewayProfile
544
+ void refreshCurrentModel()
545
+ void refreshActiveProfile()
546
+ }, [activeGatewayProfile, refreshCurrentModel])
547
+
548
+ const composer = useComposerActions({
549
+ activeSessionId,
550
+ currentCwd,
551
+ requestGateway
552
+ })
553
+
554
+ const branchInNewChat = useCallback(
555
+ async (messageId?: string) => {
556
+ const branched = await branchCurrentSession(messageId)
557
+
558
+ if (branched) {
559
+ await refreshSessions().catch(() => undefined)
560
+ }
561
+
562
+ return branched
563
+ },
564
+ [branchCurrentSession, refreshSessions]
565
+ )
566
+
567
+ const startSessionInWorkspace = useCallback(
568
+ (path: null | string) => {
569
+ startFreshSessionDraft()
570
+
571
+ const target = path?.trim()
572
+
573
+ if (!target) {
574
+ return
575
+ }
576
+
577
+ // The next message creates the backend session in $currentCwd, so seed
578
+ // it (and the branch) from the workspace the user clicked the + on.
579
+ setCurrentCwd(target)
580
+ void requestGateway<{ branch?: string; cwd?: string }>('config.get', { key: 'project', cwd: target })
581
+ .then(info => {
582
+ setCurrentCwd(info.cwd || target)
583
+ setCurrentBranch(info.branch || '')
584
+ })
585
+ .catch(() => undefined)
586
+ },
587
+ [requestGateway, startFreshSessionDraft]
588
+ )
589
+
590
+ const handleSkinCommand = useSkinCommand()
591
+
592
+ const {
593
+ cancelRun,
594
+ editMessage,
595
+ handleThreadMessagesChange,
596
+ reloadFromMessage,
597
+ steerPrompt,
598
+ submitText,
599
+ transcribeVoiceAudio
600
+ } = usePromptActions({
601
+ activeSessionId,
602
+ activeSessionIdRef,
603
+ branchCurrentSession: branchInNewChat,
604
+ busyRef,
605
+ createBackendSessionForSend,
606
+ handleSkinCommand,
607
+ refreshSessions,
608
+ requestGateway,
609
+ selectedStoredSessionIdRef,
610
+ startFreshSessionDraft,
611
+ sttEnabled,
612
+ updateSessionState
613
+ })
614
+
615
+ useGatewayBoot({
616
+ handleGatewayEvent: handleDesktopGatewayEvent,
617
+ onConnectionReady: c => {
618
+ connectionRef.current = c
619
+ },
620
+ onGatewayReady: g => {
621
+ gatewayRef.current = g
622
+ },
623
+ refreshNasTechConfig,
624
+ refreshSessions
625
+ })
626
+
627
+ useEffect(() => {
628
+ if (gatewayState === 'open') {
629
+ void refreshCurrentModel()
630
+ void refreshActiveProfile()
631
+ void refreshSessions().catch(() => undefined)
632
+ }
633
+ }, [gatewayState, refreshCurrentModel, refreshSessions])
634
+
635
+ // Keep the cron jobs section live without a user action: the scheduler ticks
636
+ // in the background (advancing next-run/state and creating runs), so poll the
637
+ // job list on an interval (and on tab re-focus) while connected.
638
+ useEffect(() => {
639
+ if (gatewayState !== 'open') {return}
640
+
641
+ const tick = () => {
642
+ if (document.visibilityState === 'visible') {void refreshCronJobs()}
643
+ }
644
+
645
+ const intervalId = window.setInterval(tick, CRON_POLL_INTERVAL_MS)
646
+ document.addEventListener('visibilitychange', tick)
647
+
648
+ return () => {
649
+ window.clearInterval(intervalId)
650
+ document.removeEventListener('visibilitychange', tick)
651
+ }
652
+ }, [gatewayState, refreshCronJobs])
653
+
654
+ useRouteResume({
655
+ activeSessionId,
656
+ activeSessionIdRef,
657
+ creatingSessionRef,
658
+ currentView,
659
+ freshDraftReady,
660
+ gatewayState,
661
+ locationPathname: location.pathname,
662
+ resumeSession,
663
+ routedSessionId,
664
+ runtimeIdByStoredSessionIdRef,
665
+ selectedStoredSessionId,
666
+ selectedStoredSessionIdRef,
667
+ startFreshSessionDraft
668
+ })
669
+
670
+ const { leftStatusbarItems, statusbarItems } = useStatusbarItems({
671
+ agentsOpen,
672
+ commandCenterOpen,
673
+ extraLeftItems: statusbarItemGroups.flat.left,
674
+ extraRightItems: statusbarItemGroups.flat.right,
675
+ gatewayLogLines,
676
+ gatewayState,
677
+ inferenceStatus,
678
+ modelMenuContent,
679
+ openAgents,
680
+ freshDraftReady,
681
+ openCommandCenterSection,
682
+ requestGateway,
683
+ statusSnapshot,
684
+ toggleCommandCenter
685
+ })
686
+
687
+ const sidebar = (
688
+ <ChatSidebar
689
+ currentView={currentView}
690
+ onArchiveSession={sessionId => void archiveSession(sessionId)}
691
+ onDeleteSession={sessionId => void removeSession(sessionId)}
692
+ onLoadMoreProfileSessions={loadMoreSessionsForProfile}
693
+ onLoadMoreSessions={loadMoreSessions}
694
+ onManageCronJob={jobId => {
695
+ setCronFocusJobId(jobId)
696
+ navigate(CRON_ROUTE)
697
+ }}
698
+ onNavigate={selectSidebarItem}
699
+ onNewSessionInWorkspace={startSessionInWorkspace}
700
+ onResumeSession={sessionId => navigate(sessionRoute(sessionId))}
701
+ onTriggerCronJob={jobId => {
702
+ void triggerCronJob(jobId)
703
+ .then(() => refreshCronJobs())
704
+ .catch(() => undefined)
705
+ }}
706
+ />
707
+ )
708
+
709
+ const overlays = (
710
+ <>
711
+ <DesktopInstallOverlay />
712
+ {/* One PTY-backed terminal mounted forever; <TerminalSlot /> placeholders
713
+ decide where it shows. Toggling fullscreen never rebuilds the shell. */}
714
+ <PersistentTerminal cwd={currentCwd} onAddSelectionToChat={composer.addTerminalSelectionAttachment} />
715
+ <DesktopOnboardingOverlay
716
+ enabled={gatewayState === 'open'}
717
+ onCompleted={() => {
718
+ void refreshNasTechConfig()
719
+ void refreshCurrentModel()
720
+ void queryClient.invalidateQueries({ queryKey: ['model-options'] })
721
+ }}
722
+ requestGateway={requestGateway}
723
+ />
724
+ <ModelPickerOverlay gateway={gatewayRef.current || undefined} onSelect={selectModel} />
725
+ <ModelVisibilityOverlay gateway={gatewayRef.current || undefined} onOpenProviders={openProviderSettings} />
726
+ <UpdatesOverlay />
727
+ <GatewayConnectingOverlay />
728
+ <BootFailureOverlay />
729
+ <CommandPalette />
730
+
731
+ {settingsOpen && (
732
+ <Suspense fallback={null}>
733
+ <SettingsView
734
+ gateway={gatewayRef.current}
735
+ onClose={closeOverlayToPreviousRoute}
736
+ onConfigSaved={() => {
737
+ void refreshNasTechConfig()
738
+ void refreshCurrentModel()
739
+ void queryClient.invalidateQueries({ queryKey: ['model-options'] })
740
+ }}
741
+ onMainModelChanged={(provider, model) => {
742
+ setCurrentProvider(provider)
743
+ setCurrentModel(model)
744
+ updateModelOptionsCache(provider, model, true)
745
+ void refreshCurrentModel()
746
+ void queryClient.invalidateQueries({ queryKey: ['model-options'] })
747
+ }}
748
+ />
749
+ </Suspense>
750
+ )}
751
+
752
+ {commandCenterOpen && (
753
+ <Suspense fallback={null}>
754
+ <CommandCenterView
755
+ initialSection={commandCenterInitialSection}
756
+ onClose={closeOverlayToPreviousRoute}
757
+ onDeleteSession={removeSession}
758
+ onNavigateRoute={path => navigate(path)}
759
+ onOpenSession={sessionId => navigate(sessionRoute(sessionId))}
760
+ />
761
+ </Suspense>
762
+ )}
763
+
764
+ {agentsOpen && (
765
+ <Suspense fallback={null}>
766
+ <AgentsView onClose={closeOverlayToPreviousRoute} />
767
+ </Suspense>
768
+ )}
769
+
770
+ {cronOpen && (
771
+ <Suspense fallback={null}>
772
+ <CronView
773
+ onClose={closeOverlayToPreviousRoute}
774
+ onOpenSession={sessionId => navigate(sessionRoute(sessionId))}
775
+ />
776
+ </Suspense>
777
+ )}
778
+
779
+ {profilesOpen && (
780
+ <Suspense fallback={null}>
781
+ <ProfilesView onClose={closeOverlayToPreviousRoute} />
782
+ </Suspense>
783
+ )}
784
+ </>
785
+ )
786
+
787
+ const chatView = (
788
+ <ChatView
789
+ gateway={gatewayRef.current}
790
+ maxVoiceRecordingSeconds={voiceMaxRecordingSeconds}
791
+ onAddContextRef={composer.addContextRefAttachment}
792
+ onAddUrl={url => composer.addContextRefAttachment(`@url:${formatRefValue(url)}`, url)}
793
+ onAttachDroppedItems={composer.attachDroppedItems}
794
+ onAttachImageBlob={composer.attachImageBlob}
795
+ onBranchInNewChat={branchInNewChat}
796
+ onCancel={cancelRun}
797
+ onDeleteSelectedSession={() => {
798
+ if (selectedStoredSessionId) {
799
+ void removeSession(selectedStoredSessionId)
800
+ }
801
+ }}
802
+ onEdit={editMessage}
803
+ onPasteClipboardImage={() => void composer.pasteClipboardImage()}
804
+ onPickFiles={() => void composer.pickContextPaths('file')}
805
+ onPickFolders={() => void composer.pickContextPaths('folder')}
806
+ onPickImages={() => void composer.pickImages()}
807
+ onReload={reloadFromMessage}
808
+ onRemoveAttachment={id => void composer.removeAttachment(id)}
809
+ onSteer={steerPrompt}
810
+ onSubmit={submitText}
811
+ onThreadMessagesChange={handleThreadMessagesChange}
812
+ onToggleSelectedPin={toggleSelectedPin}
813
+ onTranscribeAudio={transcribeVoiceAudio}
814
+ />
815
+ )
816
+
817
+ const takeoverTerminalView = (
818
+ <div className="relative flex h-full min-h-0 min-w-0 flex-col overflow-hidden bg-(--ui-chat-surface-background) pt-(--titlebar-height)">
819
+ <TerminalSlot />
820
+ </div>
821
+ )
822
+
823
+ // Flipped layout mirrors the default: sessions sidebar → right, file
824
+ // browser + preview rail → left. Same panes, swapped sides.
825
+ const sidebarSide = panesFlipped ? 'right' : 'left'
826
+ const railSide = panesFlipped ? 'left' : 'right'
827
+
828
+ const previewPane = (
829
+ <Pane
830
+ disabled={!chatOpen || (!previewTarget && !filePreviewTarget)}
831
+ id="preview"
832
+ key="preview"
833
+ maxWidth={PREVIEW_RAIL_MAX_WIDTH}
834
+ minWidth={PREVIEW_RAIL_MIN_WIDTH}
835
+ resizable
836
+ side={railSide}
837
+ width={PREVIEW_RAIL_PANE_WIDTH}
838
+ >
839
+ {chatOpen ? (
840
+ <ChatPreviewRail onRestartServer={restartPreviewServer} setTitlebarToolGroup={setTitlebarToolGroup} />
841
+ ) : null}
842
+ </Pane>
843
+ )
844
+
845
+ const fileBrowserPane = (
846
+ <Pane
847
+ defaultOpen={false}
848
+ disabled={!chatOpen}
849
+ id="file-browser"
850
+ key="file-browser"
851
+ maxWidth={FILE_BROWSER_MAX_WIDTH}
852
+ minWidth={FILE_BROWSER_MIN_WIDTH}
853
+ resizable
854
+ side={railSide}
855
+ width={FILE_BROWSER_DEFAULT_WIDTH}
856
+ >
857
+ <RightSidebarPane
858
+ onActivateFile={composer.attachContextFilePath}
859
+ onActivateFolder={composer.attachContextFolderPath}
860
+ onChangeCwd={changeSessionCwd}
861
+ />
862
+ </Pane>
863
+ )
864
+
865
+ return (
866
+ <AppShell
867
+ leftStatusbarItems={leftStatusbarItems}
868
+ leftTitlebarTools={titlebarToolGroups.flat.left}
869
+ onOpenSettings={openSettings}
870
+ overlays={overlays}
871
+ statusbarItems={statusbarItems}
872
+ titlebarTools={titlebarToolGroups.flat.right}
873
+ >
874
+ <Pane
875
+ disabled={terminalTakeoverActive}
876
+ id="chat-sidebar"
877
+ maxWidth={SIDEBAR_MAX_WIDTH}
878
+ minWidth={SIDEBAR_DEFAULT_WIDTH}
879
+ resizable
880
+ side={sidebarSide}
881
+ width={`${SIDEBAR_DEFAULT_WIDTH}px`}
882
+ >
883
+ {sidebar}
884
+ </Pane>
885
+ <PaneMain>
886
+ <Routes>
887
+ <Route element={terminalTakeoverActive ? takeoverTerminalView : chatView} index />
888
+ <Route element={terminalTakeoverActive ? takeoverTerminalView : chatView} path=":sessionId" />
889
+ <Route
890
+ element={
891
+ <Suspense fallback={null}>
892
+ <SkillsView setStatusbarItemGroup={setStatusbarItemGroup} />
893
+ </Suspense>
894
+ }
895
+ path="skills"
896
+ />
897
+ <Route
898
+ element={
899
+ <Suspense fallback={null}>
900
+ <MessagingView setStatusbarItemGroup={setStatusbarItemGroup} />
901
+ </Suspense>
902
+ }
903
+ path="messaging"
904
+ />
905
+ <Route
906
+ element={
907
+ <Suspense fallback={null}>
908
+ <ArtifactsView setStatusbarItemGroup={setStatusbarItemGroup} />
909
+ </Suspense>
910
+ }
911
+ path="artifacts"
912
+ />
913
+ <Route element={null} path="cron" />
914
+ <Route element={null} path="profiles" />
915
+ <Route element={null} path="settings" />
916
+ <Route element={null} path="command-center" />
917
+ <Route element={null} path="agents" />
918
+ <Route element={<Navigate replace to={NEW_CHAT_ROUTE} />} path="new" />
919
+ <Route element={<LegacySessionRedirect />} path="sessions/:sessionId" />
920
+ <Route element={<Navigate replace to={NEW_CHAT_ROUTE} />} path="*" />
921
+ </Routes>
922
+ </PaneMain>
923
+ {/*
924
+ Order within a side maps to column order. Default (rail on the right):
925
+ main | preview | file-browser. Flipped (rail on the left): mirror it to
926
+ file-browser | preview | main so preview stays adjacent to the chat.
927
+ */}
928
+ {panesFlipped ? fileBrowserPane : previewPane}
929
+ {panesFlipped ? previewPane : fileBrowserPane}
930
+ </AppShell>
931
+ )
932
+ }
933
+
934
+ function LegacySessionRedirect() {
935
+ const { sessionId } = useParams()
936
+
937
+ return <Navigate replace to={sessionId ? sessionRoute(sessionId) : NEW_CHAT_ROUTE} />
938
+ }