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,739 @@
1
+ import { AssistantRuntimeProvider, type ThreadMessage, useExternalStoreRuntime } from '@assistant-ui/react'
2
+ import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react'
3
+ import { useEffect, useState } from 'react'
4
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
5
+
6
+ import { Thread } from './thread'
7
+
8
+ const createdAt = new Date('2026-05-01T00:00:00.000Z')
9
+
10
+ const resizeObservers = new Set<TestResizeObserver>()
11
+
12
+ class TestResizeObserver {
13
+ private target: Element | null = null
14
+
15
+ constructor(private readonly callback: ResizeObserverCallback) {
16
+ resizeObservers.add(this)
17
+ }
18
+
19
+ observe(target: Element) {
20
+ this.target = target
21
+ }
22
+
23
+ unobserve() {}
24
+
25
+ disconnect() {
26
+ resizeObservers.delete(this)
27
+ }
28
+
29
+ trigger(height: number) {
30
+ if (!this.target) {
31
+ return
32
+ }
33
+
34
+ this.callback(
35
+ [
36
+ {
37
+ contentRect: { height } as DOMRectReadOnly,
38
+ target: this.target
39
+ } as ResizeObserverEntry
40
+ ],
41
+ this as unknown as ResizeObserver
42
+ )
43
+ }
44
+ }
45
+
46
+ vi.stubGlobal('ResizeObserver', TestResizeObserver)
47
+ vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) =>
48
+ window.setTimeout(() => callback(performance.now()), 0)
49
+ )
50
+ vi.stubGlobal('cancelAnimationFrame', (id: number) => window.clearTimeout(id))
51
+
52
+ Element.prototype.scrollTo = function scrollTo() {}
53
+
54
+ Element.prototype.animate = function animate() {
55
+ return {
56
+ cancel: () => {},
57
+ finished: Promise.resolve()
58
+ } as unknown as Animation
59
+ }
60
+
61
+ // jsdom returns 0 for offset*; the virtualizer reads those to size its
62
+ // viewport. Fall through to client* (which tests can override) or a sane
63
+ // default so virtualized items render.
64
+ function stubOffsetDimension(
65
+ prop: 'offsetHeight' | 'offsetWidth',
66
+ clientProp: 'clientHeight' | 'clientWidth',
67
+ fallback: number
68
+ ) {
69
+ const previous = Object.getOwnPropertyDescriptor(HTMLElement.prototype, prop)
70
+
71
+ Object.defineProperty(HTMLElement.prototype, prop, {
72
+ configurable: true,
73
+ get() {
74
+ return previous?.get?.call(this) || (this as HTMLElement)[clientProp] || fallback
75
+ }
76
+ })
77
+ }
78
+
79
+ stubOffsetDimension('offsetWidth', 'clientWidth', 800)
80
+ stubOffsetDimension('offsetHeight', 'clientHeight', 600)
81
+
82
+ async function wait(ms: number) {
83
+ await act(async () => {
84
+ await new Promise(resolve => window.setTimeout(resolve, ms))
85
+ })
86
+ }
87
+
88
+ function userMessage(): ThreadMessage {
89
+ return {
90
+ id: 'user-1',
91
+ role: 'user',
92
+ content: [{ type: 'text', text: 'Stream a response' }],
93
+ attachments: [],
94
+ createdAt,
95
+ metadata: { custom: {} }
96
+ } as ThreadMessage
97
+ }
98
+
99
+ function assistantMessage(text: string, running = true): ThreadMessage {
100
+ return {
101
+ id: 'assistant-1',
102
+ role: 'assistant',
103
+ content: [{ type: 'text', text }],
104
+ status: running ? { type: 'running' } : { type: 'complete', reason: 'stop' },
105
+ createdAt,
106
+ metadata: {
107
+ unstable_state: null,
108
+ unstable_annotations: [],
109
+ unstable_data: [],
110
+ steps: [],
111
+ custom: {}
112
+ }
113
+ } as ThreadMessage
114
+ }
115
+
116
+ function assistantErrorMessage(error: string): ThreadMessage {
117
+ return {
118
+ id: 'assistant-error-1',
119
+ role: 'assistant',
120
+ content: [],
121
+ status: { type: 'incomplete', reason: 'error', error },
122
+ createdAt,
123
+ metadata: {
124
+ unstable_state: null,
125
+ unstable_annotations: [],
126
+ unstable_data: [],
127
+ steps: [],
128
+ custom: {}
129
+ }
130
+ } as ThreadMessage
131
+ }
132
+
133
+ function assistantReasoningMessage(text: string, running = false): ThreadMessage {
134
+ return {
135
+ id: 'assistant-reasoning-1',
136
+ role: 'assistant',
137
+ content: [{ type: 'reasoning', text }],
138
+ status: running ? { type: 'running' } : { type: 'complete', reason: 'stop' },
139
+ createdAt,
140
+ metadata: {
141
+ unstable_state: null,
142
+ unstable_annotations: [],
143
+ unstable_data: [],
144
+ steps: [],
145
+ custom: {}
146
+ }
147
+ } as ThreadMessage
148
+ }
149
+
150
+ function assistantMultiReasoningMessage(texts: string[]): ThreadMessage {
151
+ return {
152
+ id: 'assistant-reasoning-multi-1',
153
+ role: 'assistant',
154
+ content: texts.map(text => ({ type: 'reasoning', text })),
155
+ status: { type: 'complete', reason: 'stop' },
156
+ createdAt,
157
+ metadata: {
158
+ unstable_state: null,
159
+ unstable_annotations: [],
160
+ unstable_data: [],
161
+ steps: [],
162
+ custom: {}
163
+ }
164
+ } as ThreadMessage
165
+ }
166
+
167
+ function assistantTodoMessage(
168
+ todos: Array<{ content: string; id: string; status: 'cancelled' | 'completed' | 'in_progress' | 'pending' }>,
169
+ running = true
170
+ ): ThreadMessage {
171
+ const suffix = todos.map(todo => `${todo.id}:${todo.status}`).join('|') || 'empty'
172
+
173
+ return {
174
+ id: `assistant-todo-${running ? 'running' : 'done'}-${suffix}`,
175
+ role: 'assistant',
176
+ content: [
177
+ {
178
+ type: 'tool-call',
179
+ toolCallId: 'todo-1',
180
+ toolName: 'todo',
181
+ args: { todos },
182
+ argsText: JSON.stringify({ todos }),
183
+ ...(running ? {} : { result: { todos } })
184
+ }
185
+ ],
186
+ status: running ? { type: 'running' } : { type: 'complete', reason: 'stop' },
187
+ createdAt,
188
+ metadata: {
189
+ unstable_state: null,
190
+ unstable_annotations: [],
191
+ unstable_data: [],
192
+ steps: [],
193
+ custom: {}
194
+ }
195
+ } as ThreadMessage
196
+ }
197
+
198
+ function assistantReasoningTodoMessage(
199
+ todos: Array<{ content: string; id: string; status: 'cancelled' | 'completed' | 'in_progress' | 'pending' }>
200
+ ): ThreadMessage {
201
+ return {
202
+ id: 'assistant-reasoning-todo-1',
203
+ role: 'assistant',
204
+ content: [
205
+ { type: 'reasoning', text: 'Let me make a quick todo list.' },
206
+ {
207
+ type: 'tool-call',
208
+ toolCallId: 'todo-1',
209
+ toolName: 'todo',
210
+ args: { todos },
211
+ argsText: JSON.stringify({ todos }),
212
+ result: { todos }
213
+ },
214
+ { type: 'text', text: 'Done — fake list created.' }
215
+ ],
216
+ status: { type: 'complete', reason: 'stop' },
217
+ createdAt,
218
+ metadata: {
219
+ unstable_state: null,
220
+ unstable_annotations: [],
221
+ unstable_data: [],
222
+ steps: [],
223
+ custom: {}
224
+ }
225
+ } as ThreadMessage
226
+ }
227
+
228
+ function StreamingHarness() {
229
+ const [messages, setMessages] = useState<ThreadMessage[]>([userMessage()])
230
+ const [isRunning, setIsRunning] = useState(true)
231
+
232
+ useEffect(() => {
233
+ const first = window.setTimeout(() => {
234
+ setMessages([userMessage(), assistantMessage('first chunk')])
235
+ }, 50)
236
+
237
+ const second = window.setTimeout(() => {
238
+ setMessages([userMessage(), assistantMessage('first chunk second chunk')])
239
+ }, 500)
240
+
241
+ const complete = window.setTimeout(() => {
242
+ setMessages([userMessage(), assistantMessage('first chunk second chunk', false)])
243
+ setIsRunning(false)
244
+ }, 700)
245
+
246
+ return () => {
247
+ window.clearTimeout(first)
248
+ window.clearTimeout(second)
249
+ window.clearTimeout(complete)
250
+ }
251
+ }, [])
252
+
253
+ const runtime = useExternalStoreRuntime<ThreadMessage>({
254
+ messages,
255
+ isRunning,
256
+ onNew: async () => {}
257
+ })
258
+
259
+ return (
260
+ <AssistantRuntimeProvider runtime={runtime}>
261
+ <Thread loading={isRunning && messages.at(-1)?.role !== 'assistant' ? 'response' : undefined} />
262
+ </AssistantRuntimeProvider>
263
+ )
264
+ }
265
+
266
+ function StaticThreadHarness() {
267
+ const runtime = useExternalStoreRuntime<ThreadMessage>({
268
+ messages: [userMessage(), assistantMessage('complete response', false)],
269
+ isRunning: false,
270
+ onNew: async () => {}
271
+ })
272
+
273
+ return (
274
+ <AssistantRuntimeProvider runtime={runtime}>
275
+ <Thread />
276
+ </AssistantRuntimeProvider>
277
+ )
278
+ }
279
+
280
+ function TodoHarness({ message }: { message: ThreadMessage }) {
281
+ const runtime = useExternalStoreRuntime<ThreadMessage>({
282
+ messages: [message],
283
+ isRunning: message.status?.type === 'running',
284
+ onNew: async () => {}
285
+ })
286
+
287
+ return (
288
+ <AssistantRuntimeProvider runtime={runtime}>
289
+ <Thread />
290
+ </AssistantRuntimeProvider>
291
+ )
292
+ }
293
+
294
+ function MessageHarness({ message }: { message: ThreadMessage }) {
295
+ const runtime = useExternalStoreRuntime<ThreadMessage>({
296
+ messages: [message],
297
+ isRunning: false,
298
+ onNew: async () => {}
299
+ })
300
+
301
+ return (
302
+ <AssistantRuntimeProvider runtime={runtime}>
303
+ <Thread />
304
+ </AssistantRuntimeProvider>
305
+ )
306
+ }
307
+
308
+ function RunningMessageHarness({ message }: { message: ThreadMessage }) {
309
+ const runtime = useExternalStoreRuntime<ThreadMessage>({
310
+ messages: [message],
311
+ isRunning: true,
312
+ onNew: async () => {}
313
+ })
314
+
315
+ return (
316
+ <AssistantRuntimeProvider runtime={runtime}>
317
+ <Thread />
318
+ </AssistantRuntimeProvider>
319
+ )
320
+ }
321
+
322
+ function ReasoningHarness() {
323
+ const runtime = useExternalStoreRuntime<ThreadMessage>({
324
+ messages: [assistantReasoningMessage(' The user is asking what this file is.')],
325
+ isRunning: false,
326
+ onNew: async () => {}
327
+ })
328
+
329
+ return (
330
+ <AssistantRuntimeProvider runtime={runtime}>
331
+ <Thread />
332
+ </AssistantRuntimeProvider>
333
+ )
334
+ }
335
+
336
+ function RunningReasoningHarness() {
337
+ const runtime = useExternalStoreRuntime<ThreadMessage>({
338
+ messages: [assistantReasoningMessage('```ts\nconst answer = 42\n', true)],
339
+ isRunning: true,
340
+ onNew: async () => {}
341
+ })
342
+
343
+ return (
344
+ <AssistantRuntimeProvider runtime={runtime}>
345
+ <Thread />
346
+ </AssistantRuntimeProvider>
347
+ )
348
+ }
349
+
350
+ function GroupedReasoningHarness() {
351
+ const runtime = useExternalStoreRuntime<ThreadMessage>({
352
+ messages: [assistantMultiReasoningMessage([' First thought.', ' Second thought.'])],
353
+ isRunning: false,
354
+ onNew: async () => {}
355
+ })
356
+
357
+ return (
358
+ <AssistantRuntimeProvider runtime={runtime}>
359
+ <Thread />
360
+ </AssistantRuntimeProvider>
361
+ )
362
+ }
363
+
364
+ function IntroHarness() {
365
+ const runtime = useExternalStoreRuntime<ThreadMessage>({
366
+ messages: [],
367
+ isRunning: false,
368
+ onNew: async () => {}
369
+ })
370
+
371
+ return (
372
+ <AssistantRuntimeProvider runtime={runtime}>
373
+ <Thread intro={{ personality: 'default', seed: 1 }} />
374
+ </AssistantRuntimeProvider>
375
+ )
376
+ }
377
+
378
+ describe('assistant-ui streaming renderer', () => {
379
+ beforeEach(() => {
380
+ resizeObservers.clear()
381
+ })
382
+
383
+ it('renders assistant text incrementally before completion', async () => {
384
+ const { container } = render(<StreamingHarness />)
385
+
386
+ expect(screen.getByRole('status', { name: 'NasTech is loading a response' })).toBeTruthy()
387
+
388
+ await wait(80)
389
+
390
+ await waitFor(() => {
391
+ expect(container.textContent).toContain('first chunk')
392
+ })
393
+ expect(container.textContent).not.toContain('second chunk')
394
+ expect(screen.queryByRole('status', { name: 'NasTech is loading a response' })).toBeNull()
395
+
396
+ await wait(500)
397
+
398
+ await waitFor(() => {
399
+ expect(container.textContent).toContain('first chunk second chunk')
400
+ })
401
+
402
+ await wait(250)
403
+
404
+ await waitFor(() => {
405
+ expect(container.textContent).toContain('first chunk second chunk')
406
+ })
407
+ })
408
+
409
+ it('does not render composer clearance for intro-only threads', () => {
410
+ const { container } = render(<IntroHarness />)
411
+
412
+ expect(container.querySelector('[data-slot="aui_composer-clearance"]')).toBeNull()
413
+ })
414
+
415
+ it('renders assistant provider errors inline', () => {
416
+ render(<MessageHarness message={assistantErrorMessage('OpenRouter rejected the request (403).')} />)
417
+
418
+ expect(screen.getByRole('alert').textContent).toContain('OpenRouter rejected the request (403).')
419
+ })
420
+
421
+ it('does not pull the viewport back down after the user scrolls up during streaming', async () => {
422
+ const { container } = render(<StreamingHarness />)
423
+
424
+ const content = container.querySelector('[data-slot="aui_thread-content"]') as HTMLDivElement
425
+ const viewport = content.parentElement as HTMLDivElement
426
+ let scrollHeight = 1_000
427
+
428
+ Object.defineProperty(viewport, 'clientHeight', { configurable: true, value: 200 })
429
+ Object.defineProperty(viewport, 'scrollHeight', {
430
+ configurable: true,
431
+ get: () => scrollHeight
432
+ })
433
+
434
+ await wait(80)
435
+
436
+ await act(async () => {
437
+ viewport.scrollTop = 800
438
+ fireEvent.scroll(viewport)
439
+ })
440
+ await wait(0)
441
+
442
+ await act(async () => {
443
+ fireEvent.wheel(viewport, { deltaY: -120 })
444
+ viewport.scrollTop = 420
445
+ fireEvent.scroll(viewport)
446
+ })
447
+
448
+ scrollHeight = 1_200
449
+
450
+ await act(async () => {
451
+ for (const observer of resizeObservers) {
452
+ observer.trigger(1_200)
453
+ }
454
+ })
455
+ await wait(0)
456
+
457
+ expect(viewport.scrollTop).toBe(420)
458
+ })
459
+
460
+ it('does not auto-follow idle layout shifts', async () => {
461
+ const { container } = render(<StaticThreadHarness />)
462
+
463
+ const content = container.querySelector('[data-slot="aui_thread-content"]') as HTMLDivElement
464
+ const viewport = content.parentElement as HTMLDivElement
465
+ let scrollHeight = 1_000
466
+
467
+ Object.defineProperty(viewport, 'clientHeight', { configurable: true, value: 200 })
468
+ Object.defineProperty(viewport, 'scrollHeight', {
469
+ configurable: true,
470
+ get: () => scrollHeight
471
+ })
472
+
473
+ await wait(80)
474
+
475
+ await act(async () => {
476
+ viewport.scrollTop = 420
477
+ fireEvent.scroll(viewport)
478
+ })
479
+
480
+ scrollHeight = 1_200
481
+
482
+ await act(async () => {
483
+ for (const observer of resizeObservers) {
484
+ observer.trigger(1_200)
485
+ }
486
+ })
487
+ await wait(0)
488
+
489
+ expect(viewport.scrollTop).toBe(420)
490
+ })
491
+
492
+ it('keeps sticky-bottom armed through viewport height changes during streaming', async () => {
493
+ const { container } = render(<StreamingHarness />)
494
+
495
+ const content = container.querySelector('[data-slot="aui_thread-content"]') as HTMLDivElement
496
+ const viewport = content.parentElement as HTMLDivElement
497
+ let clientHeight = 200
498
+ let scrollHeight = 1_000
499
+
500
+ Object.defineProperty(viewport, 'clientHeight', {
501
+ configurable: true,
502
+ get: () => clientHeight
503
+ })
504
+ Object.defineProperty(viewport, 'scrollHeight', {
505
+ configurable: true,
506
+ get: () => scrollHeight
507
+ })
508
+
509
+ await wait(80)
510
+
511
+ await act(async () => {
512
+ viewport.scrollTop = 800
513
+ fireEvent.scroll(viewport)
514
+ })
515
+
516
+ clientHeight = 240
517
+
518
+ await act(async () => {
519
+ viewport.scrollTop = 760
520
+ fireEvent.scroll(viewport)
521
+ })
522
+
523
+ scrollHeight = 1_200
524
+
525
+ await act(async () => {
526
+ for (const observer of resizeObservers) {
527
+ observer.trigger(1_200)
528
+ }
529
+ })
530
+ await wait(0)
531
+
532
+ expect(viewport.scrollTop).toBe(1_200)
533
+ })
534
+
535
+ it('honors the first upward wheel scroll even when a programmatic bottom-pin scroll event is still pending', async () => {
536
+ const { container } = render(<StreamingHarness />)
537
+
538
+ const content = container.querySelector('[data-slot="aui_thread-content"]') as HTMLDivElement
539
+ const viewport = content.parentElement as HTMLDivElement
540
+ let scrollHeight = 1_000
541
+
542
+ Object.defineProperty(viewport, 'clientHeight', { configurable: true, value: 200 })
543
+ Object.defineProperty(viewport, 'scrollHeight', {
544
+ configurable: true,
545
+ get: () => scrollHeight
546
+ })
547
+
548
+ await wait(80)
549
+ await wait(0)
550
+
551
+ await act(async () => {
552
+ fireEvent.wheel(viewport, { deltaY: -120 })
553
+ viewport.scrollTop = 420
554
+ fireEvent.scroll(viewport)
555
+ })
556
+
557
+ scrollHeight = 1_200
558
+
559
+ await act(async () => {
560
+ for (const observer of resizeObservers) {
561
+ observer.trigger(1_200)
562
+ }
563
+ })
564
+ await wait(0)
565
+
566
+ expect(viewport.scrollTop).toBe(420)
567
+ })
568
+
569
+ it('keeps following final code-highlight growth when a run completes at bottom', async () => {
570
+ const { container } = render(<StreamingHarness />)
571
+
572
+ const content = container.querySelector('[data-slot="aui_thread-content"]') as HTMLDivElement
573
+ const viewport = content.parentElement as HTMLDivElement
574
+ let scrollHeight = 1_000
575
+
576
+ Object.defineProperty(viewport, 'clientHeight', { configurable: true, value: 200 })
577
+ Object.defineProperty(viewport, 'scrollHeight', {
578
+ configurable: true,
579
+ get: () => scrollHeight
580
+ })
581
+
582
+ await wait(80)
583
+
584
+ await act(async () => {
585
+ viewport.scrollTop = 800
586
+ fireEvent.scroll(viewport)
587
+ })
588
+
589
+ await wait(650)
590
+
591
+ scrollHeight = 1_700
592
+ await wait(0)
593
+
594
+ expect(viewport.scrollTop).toBe(1_700)
595
+ })
596
+
597
+ it('does not restart bottom-follow after completion when the user scrolled up', async () => {
598
+ const { container } = render(<StreamingHarness />)
599
+
600
+ const content = container.querySelector('[data-slot="aui_thread-content"]') as HTMLDivElement
601
+ const viewport = content.parentElement as HTMLDivElement
602
+ let scrollHeight = 1_000
603
+
604
+ Object.defineProperty(viewport, 'clientHeight', { configurable: true, value: 200 })
605
+ Object.defineProperty(viewport, 'scrollHeight', {
606
+ configurable: true,
607
+ get: () => scrollHeight
608
+ })
609
+
610
+ await wait(80)
611
+
612
+ await act(async () => {
613
+ viewport.scrollTop = 800
614
+ fireEvent.scroll(viewport)
615
+ })
616
+
617
+ await act(async () => {
618
+ fireEvent.wheel(viewport, { deltaY: -120 })
619
+ viewport.scrollTop = 420
620
+ fireEvent.scroll(viewport)
621
+ })
622
+
623
+ await wait(650)
624
+
625
+ scrollHeight = 1_700
626
+ await wait(0)
627
+
628
+ expect(viewport.scrollTop).toBe(420)
629
+ })
630
+
631
+ it('renders an incomplete streaming fenced code block as a code card', async () => {
632
+ const { container } = render(<RunningMessageHarness message={assistantMessage('```ts\nconst answer = 42\n')} />)
633
+
634
+ await waitFor(() => {
635
+ expect(container.querySelector('[data-slot="code-card"]')).toBeTruthy()
636
+ })
637
+
638
+ expect(container.textContent).toContain('const answer = 42')
639
+ expect(container.textContent).not.toContain('```ts')
640
+ })
641
+
642
+ it('renders an incomplete streaming reasoning fenced code block as a code card', async () => {
643
+ const { container } = render(<RunningReasoningHarness />)
644
+ const ui = within(container)
645
+
646
+ fireEvent.click(ui.getByRole('button', { name: /thinking/i }))
647
+
648
+ await waitFor(() => {
649
+ expect(container.querySelector('[data-slot="code-card"]')).toBeTruthy()
650
+ })
651
+
652
+ expect(container.querySelector('[data-slot="aui_reasoning-text"]')?.textContent).toContain('const answer = 42')
653
+ expect(container.textContent).not.toContain('```ts')
654
+ })
655
+
656
+ it('renders reasoning text without a leading token space', () => {
657
+ const { container } = render(<ReasoningHarness />)
658
+ const ui = within(container)
659
+
660
+ fireEvent.click(ui.getByRole('button', { name: /thinking/i }))
661
+
662
+ expect(container.querySelector('[data-slot="aui_reasoning-text"]')?.textContent).toBe(
663
+ 'The user is asking what this file is.'
664
+ )
665
+ })
666
+
667
+ it('groups consecutive reasoning parts under one thinking disclosure', () => {
668
+ const { container } = render(<GroupedReasoningHarness />)
669
+
670
+ const disclosures = container.querySelectorAll('[data-slot="aui_thinking-disclosure"]')
671
+ expect(disclosures.length).toBe(1)
672
+
673
+ fireEvent.click(disclosures[0].querySelector('button')!)
674
+
675
+ const reasoningParts = container.querySelectorAll('[data-slot="aui_reasoning-text"]')
676
+ expect(reasoningParts.length).toBe(2)
677
+ expect(reasoningParts[0]?.textContent).toBe('First thought.')
678
+ expect(reasoningParts[1]?.textContent).toBe('Second thought.')
679
+ })
680
+
681
+ it('renders live todo rows during a running turn', () => {
682
+ const { container } = render(
683
+ <TodoHarness
684
+ message={assistantTodoMessage([
685
+ { content: 'Gather ingredients', id: 'prep', status: 'completed' },
686
+ { content: 'Boil water', id: 'boil', status: 'in_progress' }
687
+ ])}
688
+ />
689
+ )
690
+
691
+ const ui = within(container)
692
+
693
+ expect(container.querySelector('[data-slot="aui_todo-hoisted"]')).toBeTruthy()
694
+ expect(ui.getAllByText('Boil water').length).toBeGreaterThan(0)
695
+ expect(ui.getByText('Gather ingredients')).toBeTruthy()
696
+ expect(ui.queryByText(/pending/i)).toBeNull()
697
+ expect(ui.queryByRole('button', { name: /todo/i })).toBeNull()
698
+ })
699
+
700
+ it('renders archived todos after turn completion regardless of pending state', () => {
701
+ const first = render(
702
+ <TodoHarness message={assistantTodoMessage([{ content: 'Boil water', id: 'boil', status: 'pending' }], false)} />
703
+ )
704
+
705
+ const ui = within(first.container)
706
+
707
+ expect(ui.getAllByText('Boil water').length).toBeGreaterThan(0)
708
+
709
+ first.unmount()
710
+
711
+ const second = render(
712
+ <TodoHarness
713
+ message={assistantTodoMessage([{ content: 'Serve latte', id: 'serve', status: 'completed' }], false)}
714
+ />
715
+ )
716
+
717
+ const archivedUi = within(second.container)
718
+
719
+ expect(archivedUi.getAllByText('Serve latte').length).toBeGreaterThan(0)
720
+ })
721
+
722
+ it('hoists todo outside the thinking disclosure when reasoning is present', () => {
723
+ const { container } = render(
724
+ <TodoHarness
725
+ message={assistantReasoningTodoMessage([
726
+ { content: 'Buy oats', id: 'oats', status: 'completed' },
727
+ { content: "Reply to Sam's email", id: 'email', status: 'in_progress' }
728
+ ])}
729
+ />
730
+ )
731
+
732
+ const todoPanel = container.querySelector('[data-slot="aui_todo-hoisted"]')
733
+ const thinkingDisclosure = container.querySelector('[data-slot="aui_thinking-disclosure"]')
734
+
735
+ expect(todoPanel).toBeTruthy()
736
+ expect(thinkingDisclosure).toBeTruthy()
737
+ expect(Boolean(thinkingDisclosure?.contains(todoPanel as Node))).toBe(false)
738
+ })
739
+ })