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,885 @@
1
+ import type { MutableRefObject } from 'react'
2
+ import { useCallback, useRef } from 'react'
3
+ import type { NavigateFunction } from 'react-router-dom'
4
+
5
+ import { deleteSession, getSessionMessages, setSessionArchived } from '@/nastech'
6
+ import { useI18n } from '@/i18n'
7
+ import { type ChatMessage, chatMessageText, preserveLocalAssistantErrors, toChatMessages } from '@/lib/chat-messages'
8
+ import { normalizePersonalityValue } from '@/lib/chat-runtime'
9
+ import { embeddedImageUrls, textWithoutEmbeddedImages } from '@/lib/embedded-images'
10
+ import { setSessionYolo } from '@/lib/yolo-session'
11
+ import { clearComposerAttachments, clearComposerDraft } from '@/store/composer'
12
+ import { clearQueuedPrompts } from '@/store/composer-queue'
13
+ import { $pinnedSessionIds } from '@/store/layout'
14
+ import { clearNotifications, notify, notifyError } from '@/store/notifications'
15
+ import { requestDesktopOnboarding } from '@/store/onboarding'
16
+ import { $activeGatewayProfile, $newChatProfile, ensureGatewayProfile, normalizeProfileKey } from '@/store/profile'
17
+ import {
18
+ $currentCwd,
19
+ $messages,
20
+ $sessions,
21
+ $yoloActive,
22
+ getRememberedWorkspaceCwd,
23
+ sessionPinId,
24
+ setActiveSessionId,
25
+ setAwaitingResponse,
26
+ setBusy,
27
+ setCurrentBranch,
28
+ setCurrentCwd,
29
+ setCurrentFastMode,
30
+ setCurrentModel,
31
+ setCurrentPersonality,
32
+ setCurrentProvider,
33
+ setCurrentReasoningEffort,
34
+ setCurrentServiceTier,
35
+ setCurrentUsage,
36
+ setFreshDraftReady,
37
+ setIntroSeed,
38
+ setMessages,
39
+ setSelectedStoredSessionId,
40
+ setSessions,
41
+ setSessionStartedAt,
42
+ setSessionsTotal,
43
+ setTurnStartedAt,
44
+ setYoloActive
45
+ } from '@/store/session'
46
+ import { reportBackendContract } from '@/store/updates'
47
+ import type { SessionCreateResponse, SessionInfo, SessionResumeResponse, UsageStats } from '@/types/nastech'
48
+
49
+ import { NEW_CHAT_ROUTE, sessionRoute, SETTINGS_ROUTE } from '../../routes'
50
+ import type { ClientSessionState, SidebarNavItem } from '../../types'
51
+
52
+ interface SessionActionsOptions {
53
+ activeSessionId: string | null
54
+ activeSessionIdRef: MutableRefObject<string | null>
55
+ busyRef: MutableRefObject<boolean>
56
+ creatingSessionRef: MutableRefObject<boolean>
57
+ ensureSessionState: (sessionId: string, storedSessionId?: string | null) => ClientSessionState
58
+ getRouteToken: () => string
59
+ navigate: NavigateFunction
60
+ requestGateway: <T>(method: string, params?: Record<string, unknown>) => Promise<T>
61
+ runtimeIdByStoredSessionIdRef: MutableRefObject<Map<string, string>>
62
+ selectedStoredSessionId: string | null
63
+ selectedStoredSessionIdRef: MutableRefObject<string | null>
64
+ sessionStateByRuntimeIdRef: MutableRefObject<Map<string, ClientSessionState>>
65
+ syncSessionStateToView: (sessionId: string, state: ClientSessionState) => void
66
+ updateSessionState: (
67
+ sessionId: string,
68
+ updater: (state: ClientSessionState) => ClientSessionState,
69
+ storedSessionId?: string | null
70
+ ) => ClientSessionState
71
+ }
72
+
73
+ function withAppendedText(message: ChatMessage, suffix: string): ChatMessage {
74
+ let appended = false
75
+
76
+ const parts = message.parts.map(part => {
77
+ if (part.type !== 'text' || appended) {
78
+ return part
79
+ }
80
+
81
+ appended = true
82
+
83
+ return { ...part, text: `${part.text}${suffix}` }
84
+ })
85
+
86
+ return appended ? { ...message, parts } : message
87
+ }
88
+
89
+ function preserveReasoningParts(message: ChatMessage, previous: ChatMessage): ChatMessage {
90
+ if (message.parts.some(part => part.type === 'reasoning')) {
91
+ return message
92
+ }
93
+
94
+ const reasoningParts = previous.parts.filter(part => part.type === 'reasoning')
95
+
96
+ return reasoningParts.length ? { ...message, parts: [...reasoningParts, ...message.parts] } : message
97
+ }
98
+
99
+ function chatMessagesEquivalent(a: ChatMessage, b: ChatMessage): boolean {
100
+ if (
101
+ a.id !== b.id ||
102
+ a.role !== b.role ||
103
+ a.pending !== b.pending ||
104
+ a.error !== b.error ||
105
+ a.hidden !== b.hidden ||
106
+ a.branchGroupId !== b.branchGroupId
107
+ ) {
108
+ return false
109
+ }
110
+
111
+ if (a.parts.length !== b.parts.length) {
112
+ return false
113
+ }
114
+
115
+ return a.parts.every((part, index) => JSON.stringify(part) === JSON.stringify(b.parts[index]))
116
+ }
117
+
118
+ function chatMessageArraysEquivalent(a: ChatMessage[], b: ChatMessage[]): boolean {
119
+ return a.length === b.length && a.every((message, index) => chatMessagesEquivalent(message, b[index]))
120
+ }
121
+
122
+ function reconcileResumeMessages(nextMessages: ChatMessage[], previousMessages: ChatMessage[]): ChatMessage[] {
123
+ if (!previousMessages.length) {
124
+ return nextMessages
125
+ }
126
+
127
+ const previousByRoleOrdinal = new Map<string, ChatMessage>()
128
+ const previousRoleCounts = new Map<string, number>()
129
+
130
+ for (const message of previousMessages) {
131
+ const ordinal = previousRoleCounts.get(message.role) ?? 0
132
+ previousRoleCounts.set(message.role, ordinal + 1)
133
+ previousByRoleOrdinal.set(`${message.role}:${ordinal}`, message)
134
+ }
135
+
136
+ const nextRoleCounts = new Map<string, number>()
137
+
138
+ return nextMessages.map(message => {
139
+ const ordinal = nextRoleCounts.get(message.role) ?? 0
140
+ nextRoleCounts.set(message.role, ordinal + 1)
141
+
142
+ const previous = previousByRoleOrdinal.get(`${message.role}:${ordinal}`)
143
+
144
+ if (!previous) {
145
+ return message
146
+ }
147
+
148
+ const nextText = chatMessageText(message).trim()
149
+ const previousText = chatMessageText(previous)
150
+ const previousVisibleText = textWithoutEmbeddedImages(previousText)
151
+ let preserved = message
152
+
153
+ if (nextText === previousVisibleText || nextText === previousText.trim()) {
154
+ preserved = preserveReasoningParts(preserved, previous)
155
+ }
156
+
157
+ const previousImages = embeddedImageUrls(previousText)
158
+
159
+ if (!previousImages.length || embeddedImageUrls(chatMessageText(preserved)).length) {
160
+ return preserved
161
+ }
162
+
163
+ if (nextText !== previousVisibleText) {
164
+ return preserved
165
+ }
166
+
167
+ return withAppendedText(preserved, previousImages.map(url => `\n${url}`).join(''))
168
+ })
169
+ }
170
+
171
+ function upsertOptimisticSession(
172
+ created: SessionCreateResponse,
173
+ id: string,
174
+ title: string | null = null,
175
+ preview: string | null = null
176
+ ) {
177
+ const now = Date.now() / 1000
178
+ // Stamp the profile the session was just created on (= the live gateway's
179
+ // profile) so the scoped sidebar shows the new row immediately instead of
180
+ // filtering it out as "default" until the aggregator re-fetches.
181
+ const profileKey = normalizeProfileKey($activeGatewayProfile.get())
182
+
183
+ const session: SessionInfo = {
184
+ cwd: created.info?.cwd ?? null,
185
+ ended_at: null,
186
+ id,
187
+ input_tokens: 0,
188
+ is_active: true,
189
+ is_default_profile: profileKey === 'default',
190
+ last_active: now,
191
+ message_count: created.message_count ?? created.messages?.length ?? 0,
192
+ model: created.info?.model ?? null,
193
+ output_tokens: 0,
194
+ preview,
195
+ profile: profileKey,
196
+ source: 'tui',
197
+ started_at: now,
198
+ title,
199
+ tool_call_count: 0
200
+ }
201
+
202
+ setSessions(prev => [session, ...prev.filter(s => s.id !== id)])
203
+ }
204
+
205
+ function patchSessionWorkspace(sessionId: string, cwd: string | undefined) {
206
+ if (!cwd) {
207
+ return
208
+ }
209
+
210
+ setSessions(prev => prev.map(session => (session.id === sessionId ? { ...session, cwd } : session)))
211
+ }
212
+
213
+ function applyRuntimeInfo(
214
+ info: SessionCreateResponse['info'] | undefined
215
+ ): Partial<Pick<ClientSessionState, 'branch' | 'cwd'>> | null {
216
+ if (!info) {
217
+ return null
218
+ }
219
+
220
+ const sessionState: Partial<Pick<ClientSessionState, 'branch' | 'cwd'>> = {}
221
+
222
+ reportBackendContract(info.desktop_contract)
223
+
224
+ if (info.credential_warning) {
225
+ requestDesktopOnboarding(info.credential_warning)
226
+ }
227
+
228
+ if (info.model) {
229
+ setCurrentModel(info.model)
230
+ }
231
+
232
+ if (info.provider) {
233
+ setCurrentProvider(info.provider)
234
+ }
235
+
236
+ if (info.cwd) {
237
+ setCurrentCwd(info.cwd)
238
+ sessionState.cwd = info.cwd
239
+ }
240
+
241
+ if (info.branch !== undefined) {
242
+ setCurrentBranch(info.branch || '')
243
+ sessionState.branch = info.branch || ''
244
+ }
245
+
246
+ if (typeof info.personality === 'string') {
247
+ setCurrentPersonality(normalizePersonalityValue(info.personality))
248
+ }
249
+
250
+ if (typeof info.reasoning_effort === 'string') {
251
+ setCurrentReasoningEffort(info.reasoning_effort)
252
+ }
253
+
254
+ if (typeof info.service_tier === 'string') {
255
+ setCurrentServiceTier(info.service_tier)
256
+ }
257
+
258
+ if (typeof info.fast === 'boolean') {
259
+ setCurrentFastMode(info.fast)
260
+ }
261
+
262
+ if (typeof info.yolo === 'boolean') {
263
+ setYoloActive(info.yolo)
264
+ }
265
+
266
+ if (info.usage) {
267
+ setCurrentUsage(current => ({ ...current, ...info.usage }))
268
+ }
269
+
270
+ return sessionState
271
+ }
272
+
273
+ export function useSessionActions({
274
+ activeSessionId,
275
+ activeSessionIdRef,
276
+ busyRef,
277
+ creatingSessionRef,
278
+ ensureSessionState,
279
+ getRouteToken,
280
+ navigate,
281
+ requestGateway,
282
+ runtimeIdByStoredSessionIdRef,
283
+ selectedStoredSessionId,
284
+ selectedStoredSessionIdRef,
285
+ sessionStateByRuntimeIdRef,
286
+ syncSessionStateToView,
287
+ updateSessionState
288
+ }: SessionActionsOptions) {
289
+ const { t } = useI18n()
290
+ const copy = t.desktop
291
+ const resumeRequestRef = useRef(0)
292
+
293
+ const startFreshSessionDraft = useCallback(
294
+ (replaceRoute = false) => {
295
+ busyRef.current = false
296
+ setBusy(false)
297
+ setAwaitingResponse(false)
298
+ clearNotifications()
299
+ setIntroSeed(seed => seed + 1)
300
+ navigate(NEW_CHAT_ROUTE, { replace: replaceRoute })
301
+ setActiveSessionId(null)
302
+ activeSessionIdRef.current = null
303
+ setSelectedStoredSessionId(null)
304
+ selectedStoredSessionIdRef.current = null
305
+ setMessages([])
306
+ setCurrentUsage({
307
+ calls: 0,
308
+ input: 0,
309
+ output: 0,
310
+ total: 0
311
+ })
312
+ setSessionStartedAt(null)
313
+ setTurnStartedAt(null)
314
+ // New chats inherit the current workspace.
315
+ setCurrentCwd(getRememberedWorkspaceCwd())
316
+ setCurrentBranch('')
317
+ clearComposerDraft()
318
+ clearComposerAttachments()
319
+ setFreshDraftReady(true)
320
+ },
321
+ [activeSessionIdRef, busyRef, navigate, selectedStoredSessionIdRef]
322
+ )
323
+
324
+ const createBackendSessionForSend = useCallback(
325
+ async (preview: string | null = null): Promise<string | null> => {
326
+ const startingActiveSessionId = activeSessionIdRef.current
327
+ const startingStoredSessionId = selectedStoredSessionIdRef.current
328
+ const startingRouteToken = getRouteToken()
329
+
330
+ creatingSessionRef.current = true
331
+
332
+ try {
333
+ // Route the new chat to the chosen profile's backend (null = primary,
334
+ // so single-profile users are unaffected).
335
+ await ensureGatewayProfile($newChatProfile.get())
336
+ const cwd = $currentCwd.get().trim() || getRememberedWorkspaceCwd()
337
+ // Pass the owning profile so a new chat under a non-launch profile (global
338
+ // remote mode) builds its agent + persists against THAT profile's home/db.
339
+ const newChatProfile = $newChatProfile.get()
340
+ const created = await requestGateway<SessionCreateResponse>('session.create', {
341
+ cols: 96,
342
+ ...(cwd && { cwd }),
343
+ ...(newChatProfile ? { profile: newChatProfile } : {})
344
+ })
345
+ const stored = created.stored_session_id ?? null
346
+
347
+ if (
348
+ activeSessionIdRef.current !== startingActiveSessionId ||
349
+ selectedStoredSessionIdRef.current !== startingStoredSessionId ||
350
+ getRouteToken() !== startingRouteToken
351
+ ) {
352
+ await requestGateway('session.close', { session_id: created.session_id }).catch(() => undefined)
353
+
354
+ return null
355
+ }
356
+
357
+ activeSessionIdRef.current = created.session_id
358
+ selectedStoredSessionIdRef.current = stored
359
+ ensureSessionState(created.session_id, stored)
360
+
361
+ if (stored) {
362
+ // Seed the sidebar preview with the user's first message so the row
363
+ // reads meaningfully while the turn is in flight, instead of flashing
364
+ // "Untitled session" until the turn persists and auto-title runs. The
365
+ // server later returns its own preview/title and supersedes this.
366
+ upsertOptimisticSession(created, stored, null, preview?.trim() || null)
367
+ navigate(sessionRoute(stored), { replace: true })
368
+ }
369
+
370
+ setFreshDraftReady(false)
371
+ setActiveSessionId(created.session_id)
372
+ setSelectedStoredSessionId(stored)
373
+ setSessionStartedAt(Date.now())
374
+ const yoloArmed = $yoloActive.get()
375
+ const runtimeInfo = applyRuntimeInfo(created.info)
376
+
377
+ if (runtimeInfo) {
378
+ updateSessionState(created.session_id, state => ({ ...state, ...runtimeInfo }), stored)
379
+ }
380
+
381
+ // User may have armed YOLO on the new-chat draft before the runtime
382
+ // session existed — apply it to the freshly created session.
383
+ if (yoloArmed) {
384
+ await setSessionYolo(requestGateway, created.session_id, true).catch(() => undefined)
385
+ }
386
+
387
+ return created.session_id
388
+ } finally {
389
+ window.setTimeout(() => {
390
+ creatingSessionRef.current = false
391
+ }, 0)
392
+ }
393
+ },
394
+ [
395
+ activeSessionIdRef,
396
+ creatingSessionRef,
397
+ ensureSessionState,
398
+ getRouteToken,
399
+ navigate,
400
+ requestGateway,
401
+ selectedStoredSessionIdRef,
402
+ updateSessionState
403
+ ]
404
+ )
405
+
406
+ const selectSidebarItem = useCallback(
407
+ (item: SidebarNavItem) => {
408
+ if (item.action === 'new-session') {
409
+ startFreshSessionDraft()
410
+
411
+ return
412
+ }
413
+
414
+ if (item.route) {
415
+ navigate(item.route)
416
+ }
417
+ },
418
+ [navigate, startFreshSessionDraft]
419
+ )
420
+
421
+ const openSettings = useCallback(() => {
422
+ navigate(SETTINGS_ROUTE)
423
+ }, [navigate])
424
+
425
+ const closeSettings = useCallback(() => {
426
+ if (selectedStoredSessionId) {
427
+ navigate(sessionRoute(selectedStoredSessionId))
428
+
429
+ return
430
+ }
431
+
432
+ navigate(NEW_CHAT_ROUTE)
433
+ }, [navigate, selectedStoredSessionId])
434
+
435
+ const resumeSession = useCallback(
436
+ async (storedSessionId: string, replaceRoute = false) => {
437
+ const requestId = resumeRequestRef.current + 1
438
+ resumeRequestRef.current = requestId
439
+
440
+ const isCurrentResume = () =>
441
+ resumeRequestRef.current === requestId && selectedStoredSessionIdRef.current === storedSessionId
442
+
443
+ // Swap the single live gateway to this session's profile before any
444
+ // gateway call (no-op when it's already on that profile / single-profile).
445
+ const storedForProfile = $sessions.get().find(session => session.id === storedSessionId)
446
+ const sessionProfile = storedForProfile?.profile
447
+ await ensureGatewayProfile(sessionProfile)
448
+
449
+ const cachedRuntimeId = runtimeIdByStoredSessionIdRef.current.get(storedSessionId)
450
+ const cachedState = cachedRuntimeId && sessionStateByRuntimeIdRef.current.get(cachedRuntimeId)
451
+
452
+ if (cachedRuntimeId && cachedState) {
453
+ setFreshDraftReady(false)
454
+ clearNotifications()
455
+ setSelectedStoredSessionId(storedSessionId)
456
+ selectedStoredSessionIdRef.current = storedSessionId
457
+ setActiveSessionId(cachedRuntimeId)
458
+ activeSessionIdRef.current = cachedRuntimeId
459
+ syncSessionStateToView(cachedRuntimeId, cachedState)
460
+ setCurrentCwd(cachedState.cwd)
461
+ setCurrentBranch(cachedState.branch)
462
+ setSessionStartedAt(Date.now())
463
+ clearComposerDraft()
464
+ clearComposerAttachments()
465
+
466
+ try {
467
+ const usage = await requestGateway<UsageStats>('session.usage', { session_id: cachedRuntimeId })
468
+
469
+ if (!isCurrentResume()) {
470
+ return
471
+ }
472
+
473
+ if (usage) {
474
+ setCurrentUsage(current => ({ ...current, ...usage }))
475
+ }
476
+
477
+ return
478
+ } catch {
479
+ // The cached runtime id was minted by a prior backend instance. A
480
+ // pooled profile backend that gets idle-reaped (pruneSecondaryGateways)
481
+ // and respawned across a profile swap mints fresh ids, so this mapping
482
+ // now 404s ("session not found"). Drop it and fall through to a full
483
+ // resume that rebinds a live runtime id.
484
+ if (!isCurrentResume()) {
485
+ return
486
+ }
487
+
488
+ runtimeIdByStoredSessionIdRef.current.delete(storedSessionId)
489
+ sessionStateByRuntimeIdRef.current.delete(cachedRuntimeId)
490
+ }
491
+ }
492
+
493
+ setFreshDraftReady(false)
494
+ setActiveSessionId(null)
495
+ activeSessionIdRef.current = null
496
+ busyRef.current = true
497
+ setBusy(true)
498
+ setAwaitingResponse(false)
499
+ clearNotifications()
500
+ setSelectedStoredSessionId(storedSessionId)
501
+ selectedStoredSessionIdRef.current = storedSessionId
502
+ setSessionStartedAt(Date.now())
503
+ const stored = $sessions.get().find(session => session.id === storedSessionId)
504
+
505
+ if (stored) {
506
+ setCurrentUsage(current => ({
507
+ ...current,
508
+ input: stored.input_tokens || 0,
509
+ output: stored.output_tokens || 0,
510
+ total: (stored.input_tokens || 0) + (stored.output_tokens || 0)
511
+ }))
512
+ }
513
+
514
+ try {
515
+ // Load the local snapshot first, then ask the gateway to resume.
516
+ // Previously these raced:
517
+ // 1. clear messages to []
518
+ // 2. local getSessionMessages -> 45 msgs
519
+ // 3. a second resume path cleared [] again
520
+ // 4. gateway resume -> 43 msgs
521
+ // That is the ctrl+R flash chain. Avoid showing an empty thread
522
+ // while we already have a route-scoped session id, and don't race the
523
+ // local snapshot against gateway resume.
524
+ let localSnapshot = $messages.get()
525
+
526
+ try {
527
+ const storedMessages = await getSessionMessages(storedSessionId, sessionProfile)
528
+
529
+ if (isCurrentResume()) {
530
+ localSnapshot = preserveLocalAssistantErrors(toChatMessages(storedMessages.messages), $messages.get())
531
+
532
+ if (!chatMessageArraysEquivalent($messages.get(), localSnapshot)) {
533
+ setMessages(localSnapshot)
534
+ }
535
+ }
536
+ } catch {
537
+ // Non-fatal: gateway resume below can still hydrate the session.
538
+ }
539
+
540
+ const resumed = await requestGateway<SessionResumeResponse>('session.resume', {
541
+ session_id: storedSessionId,
542
+ cols: 96,
543
+ // Owning profile: in app-global remote mode one backend serves every
544
+ // profile, so the gateway opens this profile's state.db + home to
545
+ // resume + persist the right session (no-op for single/launch profile).
546
+ ...(sessionProfile ? { profile: sessionProfile } : {})
547
+ })
548
+
549
+ if (!isCurrentResume()) {
550
+ return
551
+ }
552
+
553
+ const currentMessages = $messages.get()
554
+
555
+ const resumedMessages = preserveLocalAssistantErrors(
556
+ reconcileResumeMessages(toChatMessages(resumed.messages), currentMessages),
557
+ currentMessages
558
+ )
559
+ // Avoid a second visible transcript rebuild on resume/switch.
560
+ // `getSessionMessages()` is the stable stored transcript snapshot and
561
+ // paints first; `session.resume` can return a slightly different
562
+ // runtime-shaped projection (e.g. tool/system coalescing), which was
563
+ // causing a second full message-list replacement a second later.
564
+ // Keep the already-painted local snapshot for the view/cache when it
565
+ // exists; use gateway messages only as a fallback when no local
566
+ // snapshot was available.
567
+
568
+ const preferredMessages =
569
+ localSnapshot.length > 0
570
+ ? localSnapshot
571
+ : chatMessageArraysEquivalent(currentMessages, resumedMessages)
572
+ ? currentMessages
573
+ : resumedMessages
574
+
575
+ const messagesForView = preserveLocalAssistantErrors(preferredMessages, currentMessages)
576
+
577
+ setActiveSessionId(resumed.session_id)
578
+ activeSessionIdRef.current = resumed.session_id
579
+ const runtimeInfo = applyRuntimeInfo(resumed.info)
580
+
581
+ patchSessionWorkspace(storedSessionId, runtimeInfo?.cwd)
582
+
583
+ updateSessionState(
584
+ resumed.session_id,
585
+ state => ({
586
+ ...state,
587
+ ...(runtimeInfo ?? {}),
588
+ messages: messagesForView,
589
+ busy: false,
590
+ awaitingResponse: false
591
+ }),
592
+ storedSessionId
593
+ )
594
+ clearComposerDraft()
595
+ clearComposerAttachments()
596
+ } catch (err) {
597
+ if (!isCurrentResume()) {
598
+ return
599
+ }
600
+
601
+ const fallback = await getSessionMessages(storedSessionId, sessionProfile)
602
+
603
+ if (!isCurrentResume()) {
604
+ return
605
+ }
606
+
607
+ setMessages(preserveLocalAssistantErrors(toChatMessages(fallback.messages), $messages.get()))
608
+ notifyError(err, copy.resumeFailed)
609
+ } finally {
610
+ if (isCurrentResume()) {
611
+ busyRef.current = false
612
+ setBusy(false)
613
+ setAwaitingResponse(false)
614
+ }
615
+ }
616
+ },
617
+ [
618
+ activeSessionIdRef,
619
+ busyRef,
620
+ copy,
621
+ requestGateway,
622
+ runtimeIdByStoredSessionIdRef,
623
+ selectedStoredSessionIdRef,
624
+ sessionStateByRuntimeIdRef,
625
+ syncSessionStateToView,
626
+ updateSessionState
627
+ ]
628
+ )
629
+
630
+ const branchCurrentSession = useCallback(
631
+ async (messageId?: string): Promise<boolean> => {
632
+ const sourceSessionId = activeSessionIdRef.current
633
+
634
+ if (!sourceSessionId) {
635
+ notify({
636
+ kind: 'warning',
637
+ title: copy.nothingToBranch,
638
+ message: copy.branchNeedsChat
639
+ })
640
+
641
+ return false
642
+ }
643
+
644
+ if (busyRef.current) {
645
+ notify({
646
+ kind: 'warning',
647
+ title: copy.sessionBusy,
648
+ message: copy.branchStopCurrent
649
+ })
650
+
651
+ return false
652
+ }
653
+
654
+ creatingSessionRef.current = true
655
+
656
+ try {
657
+ const currentMessages = $messages.get()
658
+
659
+ const targetIndex = messageId
660
+ ? currentMessages.findIndex(message => message.id === messageId)
661
+ : currentMessages.findLastIndex(message => message.role === 'assistant' || message.role === 'user')
662
+
663
+ const branchStart = targetIndex >= 0 ? targetIndex : Math.max(currentMessages.length - 1, 0)
664
+ const branchEnd = targetIndex >= 0 ? targetIndex + 1 : currentMessages.length
665
+
666
+ const branchMessages = currentMessages
667
+ .slice(branchStart, branchEnd)
668
+ .map(message => ({
669
+ content: chatMessageText(message),
670
+ source: message,
671
+ role: message.role
672
+ }))
673
+ .filter(message => message.content.trim() && ['assistant', 'user'].includes(message.role))
674
+
675
+ if (!branchMessages.length) {
676
+ notify({
677
+ kind: 'warning',
678
+ title: copy.nothingToBranch,
679
+ message: copy.branchNoText
680
+ })
681
+
682
+ return false
683
+ }
684
+
685
+ clearNotifications()
686
+
687
+ const cwd = $currentCwd.get().trim()
688
+
689
+ const branched = await requestGateway<SessionCreateResponse>('session.create', {
690
+ cols: 96,
691
+ ...(cwd && { cwd }),
692
+ messages: branchMessages.map(({ content, role }) => ({ content, role })),
693
+ title: copy.branchTitle
694
+ })
695
+
696
+ const routedSessionId = branched.stored_session_id ?? branched.session_id
697
+ const preview = branchMessages.map(({ content }) => content).find(Boolean) ?? null
698
+
699
+ setFreshDraftReady(false)
700
+ upsertOptimisticSession(branched, routedSessionId, copy.branchTitle, preview)
701
+ ensureSessionState(branched.session_id, routedSessionId)
702
+ setActiveSessionId(branched.session_id)
703
+ activeSessionIdRef.current = branched.session_id
704
+ updateSessionState(
705
+ branched.session_id,
706
+ state => ({
707
+ ...state,
708
+ messages: branchMessages.map(({ source }) => source),
709
+ busy: false,
710
+ awaitingResponse: false
711
+ }),
712
+ routedSessionId
713
+ )
714
+ setSelectedStoredSessionId(routedSessionId)
715
+ selectedStoredSessionIdRef.current = routedSessionId
716
+ navigate(sessionRoute(routedSessionId))
717
+
718
+ clearComposerDraft()
719
+ clearComposerAttachments()
720
+ const runtimeInfo = applyRuntimeInfo(branched.info)
721
+
722
+ patchSessionWorkspace(routedSessionId, runtimeInfo?.cwd)
723
+
724
+ if (runtimeInfo) {
725
+ updateSessionState(branched.session_id, state => ({ ...state, ...runtimeInfo }), routedSessionId)
726
+ }
727
+
728
+ return true
729
+ } catch (err) {
730
+ notifyError(err, copy.branchFailed)
731
+
732
+ return false
733
+ } finally {
734
+ window.setTimeout(() => {
735
+ creatingSessionRef.current = false
736
+ }, 0)
737
+ }
738
+ },
739
+ [
740
+ activeSessionIdRef,
741
+ busyRef,
742
+ copy,
743
+ creatingSessionRef,
744
+ ensureSessionState,
745
+ navigate,
746
+ requestGateway,
747
+ selectedStoredSessionIdRef,
748
+ updateSessionState
749
+ ]
750
+ )
751
+
752
+ const removeSession = useCallback(
753
+ async (storedSessionId: string) => {
754
+ clearNotifications()
755
+
756
+ const removed = $sessions.get().find(s => s.id === storedSessionId)
757
+ const wasSelected = selectedStoredSessionId === storedSessionId
758
+ const closingRuntimeId = wasSelected ? activeSessionId : null
759
+ const previousMessages = $messages.get()
760
+ const previousPinned = $pinnedSessionIds.get()
761
+ // Pins are keyed on the durable lineage-root id; the stored id may be the
762
+ // live tip after compression. Drop both so the pin can't linger.
763
+ const removedPinId = removed ? sessionPinId(removed) : storedSessionId
764
+
765
+ setSessions(prev => prev.filter(s => s.id !== storedSessionId))
766
+ // Keep $sessionsTotal in sync so the sidebar's "Load N more" footer
767
+ // doesn't keep claiming the removed row is still on the server.
768
+ setSessionsTotal(prev => Math.max(0, prev - 1))
769
+ $pinnedSessionIds.set(previousPinned.filter(id => id !== storedSessionId && id !== removedPinId))
770
+
771
+ // Tear down before awaiting so the route effect can't resume the
772
+ // doomed session via the stale /<sid> URL.
773
+ if (wasSelected) {
774
+ startFreshSessionDraft(true)
775
+ }
776
+
777
+ try {
778
+ if (closingRuntimeId) {
779
+ await requestGateway('session.close', { session_id: closingRuntimeId }).catch(() => undefined)
780
+ }
781
+
782
+ await deleteSession(storedSessionId, removed?.profile)
783
+ clearQueuedPrompts(storedSessionId)
784
+
785
+ if (closingRuntimeId) {
786
+ clearQueuedPrompts(closingRuntimeId)
787
+ }
788
+ } catch (err) {
789
+ if (removed) {
790
+ setSessions(prev => [removed, ...prev])
791
+ setSessionsTotal(prev => prev + 1)
792
+ }
793
+
794
+ $pinnedSessionIds.set(previousPinned)
795
+
796
+ if (wasSelected) {
797
+ setFreshDraftReady(false)
798
+ setSelectedStoredSessionId(storedSessionId)
799
+ selectedStoredSessionIdRef.current = storedSessionId
800
+ const stored = $sessions.get().find(session => session.id === storedSessionId)
801
+
802
+ if (stored) {
803
+ setCurrentUsage(current => ({
804
+ ...current,
805
+ input: stored.input_tokens || 0,
806
+ output: stored.output_tokens || 0,
807
+ total: (stored.input_tokens || 0) + (stored.output_tokens || 0)
808
+ }))
809
+ }
810
+
811
+ setMessages(previousMessages)
812
+ navigate(sessionRoute(storedSessionId), { replace: true })
813
+
814
+ if (closingRuntimeId) {
815
+ setActiveSessionId(closingRuntimeId)
816
+ activeSessionIdRef.current = closingRuntimeId
817
+ }
818
+ }
819
+
820
+ notifyError(err, copy.deleteFailed)
821
+ }
822
+ },
823
+ [
824
+ activeSessionId,
825
+ activeSessionIdRef,
826
+ copy,
827
+ navigate,
828
+ requestGateway,
829
+ selectedStoredSessionId,
830
+ selectedStoredSessionIdRef,
831
+ startFreshSessionDraft
832
+ ]
833
+ )
834
+
835
+ const archiveSession = useCallback(
836
+ async (storedSessionId: string) => {
837
+ clearNotifications()
838
+
839
+ const archived = $sessions.get().find(s => s.id === storedSessionId)
840
+ const wasSelected = selectedStoredSessionId === storedSessionId
841
+ const previousPinned = $pinnedSessionIds.get()
842
+ // Pins are keyed on the durable lineage-root id; the stored id may be the
843
+ // live tip after compression. Drop both so the pin can't linger.
844
+ const archivedPinId = archived ? sessionPinId(archived) : storedSessionId
845
+
846
+ // Soft-hide: drop from the sidebar immediately, keep the data.
847
+ setSessions(prev => prev.filter(s => s.id !== storedSessionId))
848
+ // Archived sessions are hidden by the listSessions(min_messages=1) query
849
+ // on the next refresh, so they count as "removed" for the load-more
850
+ // footer math.
851
+ setSessionsTotal(prev => Math.max(0, prev - 1))
852
+ $pinnedSessionIds.set(previousPinned.filter(id => id !== storedSessionId && id !== archivedPinId))
853
+
854
+ if (wasSelected) {
855
+ startFreshSessionDraft(true)
856
+ }
857
+
858
+ try {
859
+ await setSessionArchived(storedSessionId, true, archived?.profile)
860
+ notify({ durationMs: 2_000, kind: 'success', message: copy.archived })
861
+ } catch (err) {
862
+ if (archived) {
863
+ setSessions(prev => [archived, ...prev.filter(s => s.id !== storedSessionId)])
864
+ setSessionsTotal(prev => prev + 1)
865
+ }
866
+
867
+ $pinnedSessionIds.set(previousPinned)
868
+ notifyError(err, copy.archiveFailed)
869
+ }
870
+ },
871
+ [copy, selectedStoredSessionId, startFreshSessionDraft]
872
+ )
873
+
874
+ return {
875
+ archiveSession,
876
+ branchCurrentSession,
877
+ closeSettings,
878
+ createBackendSessionForSend,
879
+ openSettings,
880
+ removeSession,
881
+ resumeSession,
882
+ selectSidebarItem,
883
+ startFreshSessionDraft
884
+ }
885
+ }