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,512 @@
1
+ import { ThreadPrimitive, useAuiEvent, useAuiState } from '@assistant-ui/react'
2
+ import { useVirtualizer, type Virtualizer } from '@tanstack/react-virtual'
3
+ import {
4
+ type ComponentProps,
5
+ type FC,
6
+ memo,
7
+ type ReactNode,
8
+ useCallback,
9
+ useEffect,
10
+ useLayoutEffect,
11
+ useMemo,
12
+ useRef
13
+ } from 'react'
14
+
15
+ import { setMutableRef } from '@/lib/mutable-ref'
16
+ import { cn } from '@/lib/utils'
17
+ import { setThreadScrolledUp } from '@/store/thread-scroll'
18
+
19
+ const ESTIMATED_ITEM_HEIGHT = 220
20
+ const OVERSCAN = 4
21
+ const AT_BOTTOM_THRESHOLD = 4
22
+ const POST_RUN_BOTTOM_LOCK_MS = 1_200
23
+
24
+ type ThreadMessageComponents = ComponentProps<typeof ThreadPrimitive.MessageByIndex>['components']
25
+
26
+ type MessageGroup = { id: string; index: number; kind: 'standalone' } | { id: string; indices: number[]; kind: 'turn' }
27
+
28
+ interface VirtualizedThreadProps {
29
+ clampToComposer: boolean
30
+ components: ThreadMessageComponents
31
+ emptyPlaceholder?: ReactNode
32
+ loadingIndicator?: ReactNode
33
+ sessionKey?: string | null
34
+ }
35
+
36
+ function buildGroups(signature: string): MessageGroup[] {
37
+ if (!signature) {
38
+ return []
39
+ }
40
+
41
+ const messages = signature.split('\n').map(row => {
42
+ const [index, id, role] = row.split(':')
43
+
44
+ return { id, index: Number(index), role }
45
+ })
46
+
47
+ const groups: MessageGroup[] = []
48
+
49
+ for (let i = 0; i < messages.length; i++) {
50
+ const message = messages[i]
51
+
52
+ if (message.role !== 'user') {
53
+ groups.push({ id: message.id, index: message.index, kind: 'standalone' })
54
+
55
+ continue
56
+ }
57
+
58
+ const indices = [message.index]
59
+
60
+ while (i + 1 < messages.length && messages[i + 1].role !== 'user') {
61
+ indices.push(messages[++i].index)
62
+ }
63
+
64
+ groups.push({ id: message.id, indices, kind: 'turn' })
65
+ }
66
+
67
+ return groups
68
+ }
69
+
70
+ const VirtualizedThreadInner: FC<VirtualizedThreadProps> = ({
71
+ clampToComposer,
72
+ components,
73
+ emptyPlaceholder,
74
+ loadingIndicator,
75
+ sessionKey
76
+ }) => {
77
+ const messageSignature = useAuiState(s =>
78
+ s.thread.messages.map((message, index) => `${index}:${message.id}:${message.role}`).join('\n')
79
+ )
80
+
81
+ const isRunning = useAuiState(s => s.thread.isRunning)
82
+
83
+ const groups = useMemo(() => buildGroups(messageSignature), [messageSignature])
84
+ const renderEmpty = groups.length === 0 && Boolean(emptyPlaceholder)
85
+ const scrollerRef = useRef<HTMLDivElement | null>(null)
86
+
87
+ // Shared ref so scrollToFn can check whether the user is parked at the
88
+ // bottom without needing a ref from inside useThreadScrollAnchor.
89
+ const stickyBottomRef = useRef(true)
90
+
91
+ const virtualizer = useVirtualizer({
92
+ count: groups.length,
93
+ estimateSize: () => ESTIMATED_ITEM_HEIGHT,
94
+ getItemKey: index => groups[index]?.id ?? index,
95
+ getScrollElement: () => scrollerRef.current,
96
+ // Seed the rect so the initial range mounts something before
97
+ // `observeElementRect` reports the real layout (it overrides this).
98
+ initialRect: { height: 600, width: 800 },
99
+ overscan: OVERSCAN,
100
+ // When the virtualizer adjusts scroll due to item measurement changes,
101
+ // skip the adjustment if the user is at the bottom. Our ResizeObserver +
102
+ // pinToBottom loop handles scroll anchoring; letting the virtualizer also
103
+ // adjust creates a feedback loop where the two fight each other,
104
+ // producing visible rubber-banding (the view snaps to the composer
105
+ // then jumps back up).
106
+ scrollToFn: (offset, _options, instance) => {
107
+ const el = instance.scrollElement
108
+
109
+ if (!el) {
110
+ return
111
+ }
112
+
113
+ if (stickyBottomRef.current) {
114
+ const maxScroll = el.scrollHeight - el.clientHeight
115
+ const distFromBottom = maxScroll - el.scrollTop
116
+
117
+ if (distFromBottom <= AT_BOTTOM_THRESHOLD && offset < maxScroll) {
118
+ return
119
+ }
120
+ }
121
+
122
+ ;(el as HTMLElement).scrollTo(0, offset)
123
+ }
124
+ })
125
+
126
+ useThreadScrollAnchor({
127
+ enabled: !renderEmpty,
128
+ groupCount: groups.length,
129
+ isRunning,
130
+ scrollerRef,
131
+ sessionKey: sessionKey ?? null,
132
+ stickyBottomRef,
133
+ virtualizer
134
+ })
135
+
136
+ const virtualItems = virtualizer.getVirtualItems()
137
+ const totalSize = virtualizer.getTotalSize()
138
+ const paddingTop = virtualItems[0]?.start ?? 0
139
+ const paddingBottom = Math.max(0, totalSize - (virtualItems.at(-1)?.end ?? 0))
140
+
141
+ return (
142
+ <div
143
+ className="relative min-h-0 max-w-full overflow-hidden contain-[layout_paint]"
144
+ style={{ height: clampToComposer ? 'var(--thread-viewport-height)' : '100%' }}
145
+ >
146
+ <div
147
+ className="size-full overflow-x-hidden overflow-y-auto overscroll-contain"
148
+ data-slot="aui_thread-viewport"
149
+ ref={scrollerRef}
150
+ >
151
+ {renderEmpty ? (
152
+ <div
153
+ className="mx-auto grid h-full w-full max-w-(--composer-width) grid-rows-[minmax(0,1fr)_auto] min-w-0 gap-(--conversation-turn-gap) px-6 py-8"
154
+ data-slot="aui_thread-content"
155
+ >
156
+ {emptyPlaceholder}
157
+ </div>
158
+ ) : (
159
+ <div
160
+ className={cn(
161
+ 'mx-auto flex w-full max-w-(--composer-width) min-w-0 flex-col px-6 pt-[calc(var(--titlebar-height)+1.5rem)]'
162
+ )}
163
+ data-slot="aui_thread-content"
164
+ >
165
+ {/* Natural-flow virtualization: mounted items render as normal
166
+ flex siblings so `position: sticky` on the human bubble
167
+ resolves against the scroller without transform interference.
168
+ Padding spacers reserve scroll space for unmounted items. */}
169
+ <div style={{ paddingBottom: `${paddingBottom}px`, paddingTop: `${paddingTop}px` }}>
170
+ {virtualItems.map(virtualItem => {
171
+ const group = groups[virtualItem.index]
172
+
173
+ if (!group) {
174
+ return null
175
+ }
176
+
177
+ return (
178
+ <div
179
+ className="flex min-w-0 flex-col gap-(--conversation-turn-gap) pb-(--conversation-turn-gap)"
180
+ data-index={virtualItem.index}
181
+ key={virtualItem.key}
182
+ ref={virtualizer.measureElement}
183
+ >
184
+ {group.kind === 'turn' ? (
185
+ <div
186
+ className="composer-human-ai-pair-container relative flex min-w-0 flex-col gap-(--conversation-turn-gap)"
187
+ data-slot="aui_turn-pair"
188
+ >
189
+ {group.indices.map(index => (
190
+ <ThreadPrimitive.MessageByIndex components={components} index={index} key={index} />
191
+ ))}
192
+ </div>
193
+ ) : (
194
+ <ThreadPrimitive.MessageByIndex components={components} index={group.index} />
195
+ )}
196
+ </div>
197
+ )
198
+ })}
199
+ </div>
200
+ {loadingIndicator}
201
+ {clampToComposer && (
202
+ <div
203
+ aria-hidden="true"
204
+ className="shrink-0"
205
+ data-slot="aui_composer-clearance"
206
+ style={{ height: 'var(--thread-last-message-clearance)' }}
207
+ />
208
+ )}
209
+ </div>
210
+ )}
211
+ </div>
212
+ </div>
213
+ )
214
+ }
215
+
216
+ export const VirtualizedThread = memo(VirtualizedThreadInner)
217
+
218
+ function scrollElementToBottom(el: HTMLDivElement) {
219
+ el.scrollTop = el.scrollHeight
220
+ }
221
+
222
+ interface ScrollAnchorOptions {
223
+ enabled: boolean
224
+ groupCount: number
225
+ isRunning: boolean
226
+ scrollerRef: React.RefObject<HTMLDivElement | null>
227
+ sessionKey: string | null
228
+ stickyBottomRef: React.MutableRefObject<boolean>
229
+ virtualizer: Virtualizer<HTMLDivElement, Element>
230
+ }
231
+
232
+ function useThreadScrollAnchor({
233
+ enabled,
234
+ groupCount,
235
+ isRunning,
236
+ scrollerRef,
237
+ sessionKey,
238
+ stickyBottomRef,
239
+ virtualizer
240
+ }: ScrollAnchorOptions) {
241
+ // `stickyBottomRef` = parked at bottom, content growth should follow. Cleared on
242
+ // user-driven upward scroll; re-armed when they reach bottom again.
243
+ // This is a shared ref — scrollToFn reads it to prevent the virtualizer's
244
+ // measurement adjustments from fighting our pinToBottom.
245
+ const lastTopRef = useRef(0)
246
+ const lastHeightRef = useRef(0)
247
+ const lastClientHeightRef = useRef(0)
248
+ // Counter that tracks how many scroll events we expect to be ours rather
249
+ // than the user's. `pinToBottom` writes `el.scrollTop`, which fires an
250
+ // async `scroll` event; without this guard the on-scroll handler can race
251
+ // with the programmatic write (because content also grew, the *resulting*
252
+ // scrollTop can be lower than `lastTopRef` from the previous frame) and
253
+ // misread the programmatic pin as the user scrolling up — which disarms
254
+ // sticky-bottom and the user's just-submitted message slides above the
255
+ // fold. See `apps/desktop/scripts/measure-jump.mjs` for the repro
256
+ // (distFromBottom 0 → 49 within one frame, sticking forever).
257
+ const programmaticScrollPendingRef = useRef(0)
258
+ const prevSessionKeyRef = useRef(sessionKey)
259
+ const prevGroupCountRef = useRef(0)
260
+
261
+ const pinToBottom = useCallback(() => {
262
+ const el = scrollerRef.current
263
+
264
+ if (!el) {
265
+ return
266
+ }
267
+
268
+ // Hold the disarm gate across the scroll event the next line will fire.
269
+ programmaticScrollPendingRef.current += 1
270
+ scrollElementToBottom(el)
271
+ lastTopRef.current = el.scrollTop
272
+ lastHeightRef.current = el.scrollHeight
273
+ lastClientHeightRef.current = el.clientHeight
274
+ }, [scrollerRef])
275
+
276
+ const jumpToBottom = useCallback(() => {
277
+ setMutableRef(stickyBottomRef, true)
278
+
279
+ if (groupCount > 0) {
280
+ virtualizer.scrollToIndex(groupCount - 1, { align: 'end', behavior: 'auto' })
281
+ }
282
+
283
+ requestAnimationFrame(() => {
284
+ if (stickyBottomRef.current) {
285
+ pinToBottom()
286
+ }
287
+ })
288
+ }, [groupCount, pinToBottom, stickyBottomRef, virtualizer])
289
+
290
+ useEffect(() => () => setThreadScrolledUp(false), [])
291
+
292
+ // Track at-bottom state, dim composer when scrolled up, disarm on user
293
+ // scroll/wheel/touch.
294
+ useEffect(() => {
295
+ const el = scrollerRef.current
296
+
297
+ if (!el) {
298
+ return undefined
299
+ }
300
+
301
+ const disarm = () => {
302
+ setMutableRef(stickyBottomRef, false)
303
+ programmaticScrollPendingRef.current = 0
304
+ }
305
+
306
+ const onScroll = () => {
307
+ const top = el.scrollTop
308
+
309
+ // If this scroll event is the consequence of `pinToBottom` writing
310
+ // `el.scrollTop`, treat it as ours: don't disarm. The RO + rAF pin
311
+ // loop will re-pin on the next frame if the browser clamped us
312
+ // short of bottom (because content grew in the same frame).
313
+ // Without this guard the post-pin scrollTop gets misread as the
314
+ // user scrolling up, disarming sticky-bottom permanently and
315
+ // leaving the just-submitted message below the fold.
316
+ if (programmaticScrollPendingRef.current > 0) {
317
+ programmaticScrollPendingRef.current -= 1
318
+ lastTopRef.current = top
319
+ lastHeightRef.current = el.scrollHeight
320
+ lastClientHeightRef.current = el.clientHeight
321
+ // Always re-arm — sticky-bottom should hold through clamp races.
322
+ setMutableRef(stickyBottomRef, true)
323
+ const atBottom = el.scrollHeight - (top + el.clientHeight) <= AT_BOTTOM_THRESHOLD
324
+ setThreadScrolledUp(!atBottom)
325
+
326
+ return
327
+ }
328
+
329
+ // Disarm only when `scrollTop` decreases while both content height and
330
+ // viewport height are stable. A bare `top < lastTopRef.current` check is
331
+ // unsafe: virtualizer measurement, streaming markdown, composer resizing,
332
+ // window resizing, and toolbar/status updates can all move scrollTop as a
333
+ // layout side effect. Wheel-up and touchmove still disarm immediately via
334
+ // their own listeners below, so real user intent remains covered.
335
+ const heightGrew = el.scrollHeight > lastHeightRef.current
336
+ const clientHeightChanged = Math.abs(el.clientHeight - lastClientHeightRef.current) > 1
337
+
338
+ if (!heightGrew && !clientHeightChanged && top + 1 < lastTopRef.current) {
339
+ setMutableRef(stickyBottomRef, false)
340
+ }
341
+
342
+ lastTopRef.current = top
343
+ lastHeightRef.current = el.scrollHeight
344
+ lastClientHeightRef.current = el.clientHeight
345
+
346
+ const atBottom = el.scrollHeight - (top + el.clientHeight) <= AT_BOTTOM_THRESHOLD
347
+
348
+ if (atBottom) {
349
+ setMutableRef(stickyBottomRef, true)
350
+ }
351
+
352
+ setThreadScrolledUp(!atBottom)
353
+ }
354
+
355
+ const onWheel = (event: WheelEvent) => {
356
+ if (event.deltaY < 0) {
357
+ disarm()
358
+ }
359
+ }
360
+
361
+ el.addEventListener('scroll', onScroll, { passive: true })
362
+ el.addEventListener('wheel', onWheel, { passive: true })
363
+ el.addEventListener('touchmove', disarm, { passive: true })
364
+
365
+ return () => {
366
+ el.removeEventListener('scroll', onScroll)
367
+ el.removeEventListener('wheel', onWheel)
368
+ el.removeEventListener('touchmove', disarm)
369
+ }
370
+ }, [scrollerRef, stickyBottomRef])
371
+
372
+ // Follow content growth (streaming, item measurements, loading indicator)
373
+ // while armed. During fast streaming the ResizeObserver can fire many
374
+ // times per frame as Streamdown re-tokenizes; coalesce to one pin per
375
+ // animation frame so we don't run the scroll-event/re-pin chain
376
+ // (~20+ ms self in `Virtualizer.getMaxScrollOffset`) several times per
377
+ // token.
378
+ useEffect(() => {
379
+ if (!enabled || !isRunning) {
380
+ return undefined
381
+ }
382
+
383
+ const el = scrollerRef.current
384
+
385
+ if (!el) {
386
+ return undefined
387
+ }
388
+
389
+ let pinRafScheduled = false
390
+
391
+ const schedulePin = () => {
392
+ if (pinRafScheduled || !stickyBottomRef.current) {
393
+ return
394
+ }
395
+
396
+ pinRafScheduled = true
397
+ requestAnimationFrame(() => {
398
+ pinRafScheduled = false
399
+
400
+ if (stickyBottomRef.current) {
401
+ pinToBottom()
402
+ }
403
+ })
404
+ }
405
+
406
+ const observer = new ResizeObserver(schedulePin)
407
+
408
+ // Observe ONLY the content (firstElementChild), not the scroller `el`
409
+ // itself. Resizes of the viewport/scroller (window resize, devtools
410
+ // panel toggle) shouldn't trigger a pin — only content growth should.
411
+ if (el.firstElementChild) {
412
+ observer.observe(el.firstElementChild)
413
+ }
414
+
415
+ return () => observer.disconnect()
416
+ }, [enabled, isRunning, pinToBottom, scrollerRef, stickyBottomRef])
417
+
418
+ // Jump to bottom on session change OR when an empty thread first gets
419
+ // content. Both share the same intent and the same effect.
420
+ useEffect(() => {
421
+ const sessionChanged = prevSessionKeyRef.current !== sessionKey
422
+ const becameNonEmpty = prevGroupCountRef.current === 0 && groupCount > 0
423
+
424
+ prevSessionKeyRef.current = sessionKey
425
+ prevGroupCountRef.current = groupCount
426
+
427
+ if (enabled && (sessionChanged || becameNonEmpty)) {
428
+ jumpToBottom()
429
+ }
430
+ }, [enabled, groupCount, jumpToBottom, sessionKey])
431
+
432
+ // Pre-paint pin: when groupCount increases while armed (optimistic user
433
+ // message insert, streaming assistant turn arriving, etc.), pin BEFORE
434
+ // the browser commits the layout to screen. Using useLayoutEffect rather
435
+ // than useEffect so this runs synchronously after React commits the DOM
436
+ // mutation but before the browser paints. Without this, there's a ~50ms
437
+ // visual window where the new message sits below the fold while we wait
438
+ // for the ResizeObserver / scroll event chain to fire and re-pin.
439
+ //
440
+ // We pin TWICE in this critical path — once synchronously, then once on
441
+ // the next rAF. The second pin catches the case where React mounts the
442
+ // new message in the second commit (after our layout effect ran), which
443
+ // grows scrollHeight again; without the rAF pin the user briefly sees a
444
+ // ~15 px gap below the new message until the RO catches up. Streaming
445
+ // tokens use the rate-limited RO path only; only the group-count change
446
+ // (which fires once per user submit / new turn arrival) pays for the
447
+ // extra pin.
448
+ const prevGroupCountForLayoutRef = useRef(groupCount)
449
+ useLayoutEffect(() => {
450
+ if (!enabled) {
451
+ return
452
+ }
453
+
454
+ if (groupCount > prevGroupCountForLayoutRef.current && stickyBottomRef.current) {
455
+ // Defer to rAF so that browser scroll/wheel events from the current
456
+ // frame are processed first. Without this deferral, a trackpad
457
+ // scroll-up during streaming can race with this effect: the wheel
458
+ // event hasn't fired yet so stickyBottomRef is still true, and the
459
+ // immediate pinToBottom() would snap the viewport back to bottom
460
+ // against the user's intent.
461
+ requestAnimationFrame(() => {
462
+ if (stickyBottomRef.current) {
463
+ pinToBottom()
464
+ }
465
+ })
466
+ }
467
+
468
+ prevGroupCountForLayoutRef.current = groupCount
469
+ }, [enabled, groupCount, pinToBottom, stickyBottomRef])
470
+
471
+ // Completion swaps streaming placeholders/plain code for final rendered DOM
472
+ // (notably Shiki-highlighted code). Keep following the bottom briefly after
473
+ // `isRunning` flips false so that final measurement pass cannot strand the
474
+ // viewport near the top of a large code block.
475
+ const prevIsRunningForLayoutRef = useRef(isRunning)
476
+ useLayoutEffect(() => {
477
+ const finishedRun = prevIsRunningForLayoutRef.current && !isRunning
478
+ prevIsRunningForLayoutRef.current = isRunning
479
+
480
+ if (!enabled || !finishedRun || !stickyBottomRef.current) {
481
+ return undefined
482
+ }
483
+
484
+ const lockUntil = performance.now() + POST_RUN_BOTTOM_LOCK_MS
485
+ let lockRaf: number | null = null
486
+
487
+ const lockFrame = () => {
488
+ lockRaf = null
489
+
490
+ if (!stickyBottomRef.current) {
491
+ return
492
+ }
493
+
494
+ pinToBottom()
495
+
496
+ if (performance.now() < lockUntil) {
497
+ lockRaf = requestAnimationFrame(lockFrame)
498
+ }
499
+ }
500
+
501
+ pinToBottom()
502
+ lockRaf = requestAnimationFrame(lockFrame)
503
+
504
+ return () => {
505
+ if (lockRaf !== null) {
506
+ cancelAnimationFrame(lockRaf)
507
+ }
508
+ }
509
+ }, [enabled, isRunning, pinToBottom, stickyBottomRef])
510
+
511
+ useAuiEvent('thread.runStart', jumpToBottom)
512
+ }