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,466 @@
1
+ 'use client'
2
+
3
+ import { type ToolCallMessagePartProps, useAuiState } from '@assistant-ui/react'
4
+ import { useStore } from '@nanostores/react'
5
+ import { createContext, type FC, type PropsWithChildren, type ReactNode, useContext, useMemo } from 'react'
6
+
7
+ import { AnsiText } from '@/components/assistant-ui/ansi-text'
8
+ import { useElapsedSeconds } from '@/components/chat/activity-timer'
9
+ import { ActivityTimerText } from '@/components/chat/activity-timer-text'
10
+ import { CompactMarkdown } from '@/components/chat/compact-markdown'
11
+ import { DiffLines } from '@/components/chat/diff-lines'
12
+ import { DisclosureRow } from '@/components/chat/disclosure-row'
13
+ import { PreviewAttachment } from '@/components/chat/preview-attachment'
14
+ import { ZoomableImage } from '@/components/chat/zoomable-image'
15
+ import { BrailleSpinner } from '@/components/ui/braille-spinner'
16
+ import { Codicon } from '@/components/ui/codicon'
17
+ import { CopyButton } from '@/components/ui/copy-button'
18
+ import { FadeText } from '@/components/ui/fade-text'
19
+ import { useI18n } from '@/i18n'
20
+ import { PrettyLink, LinkifiedText as SharedLinkifiedText, urlSlugTitleLabel } from '@/lib/external-link'
21
+ import { AlertCircle, CheckCircle2 } from '@/lib/icons'
22
+ import { useEnterAnimation } from '@/lib/use-enter-animation'
23
+ import { cn } from '@/lib/utils'
24
+ import { $toolInlineDiffs } from '@/store/tool-diffs'
25
+ import { $toolDisclosureOpen, $toolViewMode, setToolDisclosureOpen } from '@/store/tool-view'
26
+
27
+ import { PendingToolApproval } from './tool-approval'
28
+ import {
29
+ buildToolView,
30
+ cleanVisibleText,
31
+ inlineDiffFromResult,
32
+ isPreviewableTarget,
33
+ looksRedundant,
34
+ type SearchResultRow,
35
+ selectMessageRunning,
36
+ stripInlineDiffChrome,
37
+ toolCopyPayload,
38
+ type ToolPart,
39
+ toolPartDisclosureId,
40
+ type ToolStatus
41
+ } from './tool-fallback-model'
42
+
43
+ // `true` when a ToolEntry is rendered inside an embedding wrapper that owns
44
+ // the per-row chrome (timer / preview). The flat ToolGroupSlot sets this
45
+ // false, so every row currently owns its own chrome; kept as a seam for any
46
+ // future embedding surface.
47
+ const ToolEmbedContext = createContext(false)
48
+
49
+ // Shared header chrome for tool rows. Both the single-tool DisclosureRow
50
+ // and the multi-tool group header pass through these constants so a
51
+ // "Patch" row and a "Tool actions · 2 steps" row are visually identical.
52
+ const TOOL_HEADER_TITLE_CLASS =
53
+ 'text-[length:var(--conversation-tool-font-size)] font-medium leading-(--conversation-line-height) text-(--ui-text-secondary)'
54
+
55
+ const TOOL_HEADER_DURATION_CLASS = 'shrink-0 text-[0.625rem] tabular-nums text-(--ui-text-tertiary)'
56
+
57
+ const TOOL_HEADER_SUBTITLE_CLASS =
58
+ 'text-[length:var(--conversation-caption-font-size)] leading-(--conversation-caption-line-height) text-(--ui-text-tertiary)'
59
+
60
+ const TOOL_HEADER_GLYPH_WRAP_CLASS = 'grid size-3.5 shrink-0 place-items-center self-center'
61
+
62
+ // Glass-style section label that sits above any pre/JSON/output block.
63
+ // Lowercase tracking + tiny size so it reads as a quiet field label rather
64
+ // than a chrome heading. Used for "COMMAND OUTPUT", "INPUT", "OUTPUT", etc.
65
+ const TOOL_SECTION_LABEL_CLASS = 'mb-1 text-[0.65rem] font-medium uppercase tracking-[0.08em] text-(--ui-text-tertiary)'
66
+
67
+ // Inset scroll surface for any detail body. The expanded tool row owns the
68
+ // border; the payload itself is just clipped raw text.
69
+ const TOOL_SECTION_SURFACE_CLASS =
70
+ 'max-h-20 max-w-full overflow-auto bg-transparent px-2 py-1.5 text-(--ui-text-secondary)'
71
+
72
+ const TOOL_SECTION_PRE_CLASS = cn(TOOL_SECTION_SURFACE_CLASS, 'font-mono text-[0.7rem] leading-relaxed')
73
+
74
+ interface ToolStatusCopy {
75
+ statusDone: string
76
+ statusError: string
77
+ statusRecovered: string
78
+ statusRunning: string
79
+ }
80
+
81
+ function rawTechnicalTrace(args: unknown, result: unknown): string {
82
+ const parts = [args, result]
83
+ .filter(value => value !== undefined && value !== null)
84
+ .map(value => {
85
+ if (typeof value === 'string') {
86
+ return value
87
+ }
88
+
89
+ try {
90
+ return JSON.stringify(value)
91
+ } catch {
92
+ return String(value)
93
+ }
94
+ })
95
+ .filter(Boolean)
96
+
97
+ return parts.join('\n')
98
+ }
99
+
100
+ function statusGlyph(status: ToolStatus, copy: ToolStatusCopy): ReactNode {
101
+ if (status === 'running') {
102
+ return (
103
+ <BrailleSpinner
104
+ ariaLabel={copy.statusRunning}
105
+ className="size-3.5 shrink-0 text-[0.95rem] text-(--ui-text-tertiary)"
106
+ spinner="breathe"
107
+ />
108
+ )
109
+ }
110
+
111
+ if (status === 'error') {
112
+ return <AlertCircle aria-label={copy.statusError} className="size-3.5 shrink-0 text-destructive" />
113
+ }
114
+
115
+ if (status === 'warning') {
116
+ return (
117
+ <AlertCircle
118
+ aria-label={copy.statusRecovered}
119
+ className="size-3.5 shrink-0 text-amber-600 dark:text-amber-400"
120
+ />
121
+ )
122
+ }
123
+
124
+ return (
125
+ <CheckCircle2
126
+ aria-label={copy.statusDone}
127
+ className="size-3.5 shrink-0 text-emerald-600/85 dark:text-emerald-400/85"
128
+ />
129
+ )
130
+ }
131
+
132
+ // Leading glyph for any tool-row header. Status (running/error/warning)
133
+ // takes precedence; otherwise falls back to the tool's codicon. Returns
134
+ // null when neither applies so callers can render unconditionally.
135
+ function ToolGlyph({ copy, icon, status }: { copy: ToolStatusCopy; icon?: string; status?: ToolStatus }) {
136
+ const node = status ? (
137
+ statusGlyph(status, copy)
138
+ ) : icon ? (
139
+ <Codicon className="text-(--ui-text-tertiary)" name={icon} size="0.875rem" />
140
+ ) : null
141
+
142
+ return node ? <span className={TOOL_HEADER_GLYPH_WRAP_CLASS}>{node}</span> : null
143
+ }
144
+
145
+ // Which status (if any) should pre-empt the tool's icon in the leading
146
+ // slot. Success is silent — the row reads as "done" without a checkmark.
147
+ function leadingStatus(isPending: boolean, status: ToolStatus): ToolStatus | undefined {
148
+ if (isPending) {
149
+ return 'running'
150
+ }
151
+
152
+ return status === 'success' ? undefined : status
153
+ }
154
+
155
+ function SearchResultsList({ hits }: { hits: SearchResultRow[] }) {
156
+ return (
157
+ <ol className="m-0 grid list-none gap-2.5 p-0">
158
+ {hits.map((hit, index) => {
159
+ const key = `${hit.url || hit.title}-${index}`
160
+ const trimmedTitle = hit.title.trim()
161
+
162
+ return (
163
+ <li className="grid min-w-0 gap-0.5" key={key}>
164
+ {hit.url ? (
165
+ <PrettyLink
166
+ className={cn(TOOL_HEADER_TITLE_CLASS, 'block max-w-full')}
167
+ fallbackLabel={trimmedTitle || urlSlugTitleLabel(hit.url)}
168
+ href={hit.url}
169
+ label={trimmedTitle || undefined}
170
+ />
171
+ ) : (
172
+ <span className={TOOL_HEADER_TITLE_CLASS}>{trimmedTitle}</span>
173
+ )}
174
+ {hit.snippet && <p className={cn(TOOL_HEADER_SUBTITLE_CLASS, 'm-0 line-clamp-3')}>{hit.snippet}</p>}
175
+ </li>
176
+ )
177
+ })}
178
+ </ol>
179
+ )
180
+ }
181
+
182
+ function LinkifiedText({ className, text }: { className?: string; text: string }) {
183
+ return <SharedLinkifiedText className={className} pretty text={cleanVisibleText(text)} />
184
+ }
185
+
186
+ interface ToolEntryProps {
187
+ part: ToolPart
188
+ }
189
+
190
+ function useDisclosureOpen(disclosureId: string, fallbackOpen = false): boolean {
191
+ const persistedOpen = useStore($toolDisclosureOpen(disclosureId))
192
+
193
+ return persistedOpen ?? fallbackOpen
194
+ }
195
+
196
+ function ToolEntry({ part }: ToolEntryProps) {
197
+ const { t } = useI18n()
198
+ const copy = t.assistant.tool
199
+ const messageId = useAuiState(s => s.message.id)
200
+ const messageRunning = useAuiState(selectMessageRunning)
201
+ const embedded = useContext(ToolEmbedContext)
202
+ const toolViewMode = useStore($toolViewMode)
203
+ const disclosureId = `tool-entry:${messageId}:${toolPartDisclosureId(part)}`
204
+ const open = useDisclosureOpen(disclosureId)
205
+ const isPending = messageRunning && part.result === undefined
206
+ // Only animate entries that mount while their message is actively
207
+ // streaming — historical sessions mount with `messageRunning === false`,
208
+ // so they paint statically without a settle cascade. The wrapping group
209
+ // handles its own enter animation, so embedded children skip it.
210
+ const enterRef = useEnterAnimation(messageRunning && !embedded, `tool-entry:${disclosureId}`)
211
+ const elapsed = useElapsedSeconds(isPending, `tool:${disclosureId}`)
212
+ const liveDiffs = useStore($toolInlineDiffs)
213
+ const sideDiff = part.toolCallId ? liveDiffs[part.toolCallId] || '' : ''
214
+ const inlineDiff = stripInlineDiffChrome(sideDiff) || inlineDiffFromResult(part.result)
215
+
216
+ // Stale parts (no result, but message stopped running) get a synthetic
217
+ // empty result so buildToolView treats them as completed-no-output.
218
+ const view = useMemo(() => {
219
+ const p = !isPending && part.result === undefined ? { ...part, result: {} } : part
220
+
221
+ return buildToolView(p, inlineDiff)
222
+ }, [inlineDiff, isPending, part])
223
+
224
+ const detailSections = useMemo(() => {
225
+ if (!view.detail) {
226
+ return { body: '', summary: '' }
227
+ }
228
+
229
+ if (view.status !== 'error') {
230
+ return { body: view.detail, summary: '' }
231
+ }
232
+
233
+ const chunks = view.detail
234
+ .split(/\n\s*\n+/)
235
+ .map(chunk => chunk.trim())
236
+ .filter(Boolean)
237
+
238
+ const [summary = '', ...rest] = chunks
239
+ const subtitleNorm = view.subtitle.trim().toLowerCase()
240
+ const summaryDuplicatesSubtitle = summary && summary.toLowerCase() === subtitleNorm
241
+
242
+ if (summaryDuplicatesSubtitle) {
243
+ return { body: rest.join('\n\n').trim(), summary: '' }
244
+ }
245
+
246
+ return { body: rest.join('\n\n').trim(), summary }
247
+ }, [view.detail, view.status, view.subtitle])
248
+
249
+ const detailMatchesSubtitle = looksRedundant(view.subtitle, view.detail)
250
+
251
+ const showDetail =
252
+ (view.status === 'error' && Boolean(detailSections.summary || detailSections.body)) ||
253
+ (view.status !== 'error' &&
254
+ Boolean(view.detail) &&
255
+ !looksRedundant(view.title, view.detail) &&
256
+ !detailMatchesSubtitle)
257
+
258
+ const renderDetailAsCode =
259
+ view.status !== 'error' &&
260
+ (part.toolName === 'terminal' || part.toolName === 'execute_code' || part.toolName === 'read_file')
261
+
262
+ const hasSearchHits = Boolean(view.searchHits?.length)
263
+ const searchResultsLabel = part.toolName === 'web_search' ? 'Search results' : view.detailLabel
264
+
265
+ const showRawSearchDrilldown =
266
+ part.toolName === 'web_search' &&
267
+ part.result !== undefined &&
268
+ toolViewMode !== 'technical' &&
269
+ Boolean(view.rawResult.trim())
270
+
271
+ const hasExpandableContent = Boolean(
272
+ (view.previewTarget && isPreviewableTarget(view.previewTarget)) ||
273
+ view.imageUrl ||
274
+ view.inlineDiff ||
275
+ showDetail ||
276
+ hasSearchHits ||
277
+ toolViewMode === 'technical'
278
+ )
279
+
280
+ const copyAction = useMemo(() => toolCopyPayload(part, view), [part, view])
281
+
282
+ const trailing =
283
+ isPending && !embedded ? (
284
+ <ActivityTimerText className={TOOL_HEADER_DURATION_CLASS} seconds={elapsed} />
285
+ ) : !isPending && copyAction.text ? (
286
+ <CopyButton appearance="tool-row" label={copyAction.label} stopPropagation text={copyAction.text} />
287
+ ) : undefined
288
+
289
+ return (
290
+ <div
291
+ className={cn(
292
+ 'min-w-0 max-w-full overflow-hidden text-[length:var(--conversation-tool-font-size)] text-(--ui-text-tertiary)',
293
+ open && 'rounded-[0.625rem] border border-(--ui-stroke-tertiary)'
294
+ )}
295
+ data-slot="tool-block"
296
+ ref={enterRef}
297
+ >
298
+ <div className={cn(open && 'border-b border-(--ui-stroke-tertiary) px-2 py-1.5')}>
299
+ <DisclosureRow
300
+ onToggle={hasExpandableContent ? () => setToolDisclosureOpen(disclosureId, !open) : undefined}
301
+ open={open}
302
+ trailing={trailing}
303
+ >
304
+ <span className="flex min-w-0 items-center gap-1.5">
305
+ <ToolGlyph copy={copy} icon={view.icon} status={leadingStatus(isPending, view.status)} />
306
+ <FadeText
307
+ className={cn(
308
+ TOOL_HEADER_TITLE_CLASS,
309
+ isPending && 'shimmer text-(--ui-text-tertiary)',
310
+ view.status === 'error' && 'text-destructive',
311
+ view.status === 'warning' && 'text-amber-700 dark:text-amber-300'
312
+ )}
313
+ >
314
+ {view.title}
315
+ </FadeText>
316
+ {!isPending && view.countLabel && <span className={TOOL_HEADER_DURATION_CLASS}>{view.countLabel}</span>}
317
+ {!isPending && view.durationLabel && (
318
+ <span className={TOOL_HEADER_DURATION_CLASS}>{view.durationLabel}</span>
319
+ )}
320
+ </span>
321
+ </DisclosureRow>
322
+ </div>
323
+ {isPending && <PendingToolApproval part={part} />}
324
+ {open && (
325
+ <div className="grid w-full min-w-0 max-w-full gap-1.5 overflow-hidden p-1.5">
326
+ {!embedded && view.previewTarget && isPreviewableTarget(view.previewTarget) && (
327
+ <PreviewAttachment source="tool-result" target={view.previewTarget} />
328
+ )}
329
+ {view.imageUrl && (
330
+ <div className="max-w-72 overflow-hidden rounded-[0.25rem] border border-(--ui-stroke-tertiary)">
331
+ <ZoomableImage alt={copy.outputAlt} className="h-auto w-full object-cover" src={view.imageUrl} />
332
+ </div>
333
+ )}
334
+ {hasSearchHits && view.searchHits && (
335
+ <div className="max-w-full text-xs leading-relaxed text-(--ui-text-secondary)">
336
+ {searchResultsLabel && <p className={TOOL_SECTION_LABEL_CLASS}>{searchResultsLabel}</p>}
337
+ <SearchResultsList hits={view.searchHits} />
338
+ </div>
339
+ )}
340
+ {showDetail &&
341
+ toolViewMode !== 'technical' &&
342
+ (view.status === 'error' ? (
343
+ detailSections.summary || detailSections.body ? (
344
+ <div className="max-w-full text-xs leading-relaxed text-destructive">
345
+ {detailSections.summary && (
346
+ <LinkifiedText className="block font-medium" text={detailSections.summary} />
347
+ )}
348
+ {detailSections.body && (
349
+ <pre
350
+ className={cn(
351
+ 'max-h-56 overflow-auto whitespace-pre-wrap wrap-anywhere font-mono text-[0.7rem] leading-[1.55] text-destructive/90',
352
+ detailSections.summary && 'mt-1.5'
353
+ )}
354
+ >
355
+ {detailSections.body}
356
+ </pre>
357
+ )}
358
+ </div>
359
+ ) : null
360
+ ) : view.stdout || view.stderr ? (
361
+ // Stdout + stderr split: render both as labeled blocks. stderr
362
+ // is intentionally NOT painted destructive — many CLIs log
363
+ // informational output there.
364
+ <div className="max-w-full text-xs leading-relaxed text-(--ui-text-secondary)">
365
+ {view.detailLabel && <p className={TOOL_SECTION_LABEL_CLASS}>{view.detailLabel}</p>}
366
+ {view.stdout && (
367
+ <div className="space-y-0.5">
368
+ {view.stderr && <p className={TOOL_SECTION_LABEL_CLASS}>stdout</p>}
369
+ <pre className={cn(TOOL_SECTION_PRE_CLASS, 'whitespace-pre-wrap wrap-anywhere')}>
370
+ {view.rendersAnsi ? <AnsiText text={view.stdout} /> : view.stdout}
371
+ </pre>
372
+ </div>
373
+ )}
374
+ {view.stderr && (
375
+ <div className={cn('space-y-0.5', view.stdout && 'mt-1.5')}>
376
+ <p className={TOOL_SECTION_LABEL_CLASS}>stderr</p>
377
+ <pre
378
+ className={cn(
379
+ TOOL_SECTION_PRE_CLASS,
380
+ 'whitespace-pre-wrap wrap-anywhere text-(--ui-text-tertiary)'
381
+ )}
382
+ >
383
+ {view.rendersAnsi ? <AnsiText text={view.stderr} /> : view.stderr}
384
+ </pre>
385
+ </div>
386
+ )}
387
+ </div>
388
+ ) : (
389
+ <div className="max-w-full text-xs leading-relaxed text-(--ui-text-secondary)">
390
+ {view.detailLabel && <p className={TOOL_SECTION_LABEL_CLASS}>{view.detailLabel}</p>}
391
+ {renderDetailAsCode ? (
392
+ <pre className={cn(TOOL_SECTION_PRE_CLASS, 'whitespace-pre-wrap wrap-anywhere')}>
393
+ {view.rendersAnsi ? <AnsiText text={view.detail} /> : view.detail}
394
+ </pre>
395
+ ) : (
396
+ <CompactMarkdown className={cn(TOOL_SECTION_SURFACE_CLASS, 'wrap-anywhere')} text={view.detail} />
397
+ )}
398
+ </div>
399
+ ))}
400
+ {showRawSearchDrilldown && (
401
+ <details className="max-w-full">
402
+ <summary className={cn(TOOL_SECTION_LABEL_CLASS, 'mb-0')}>{copy.rawResponse}</summary>
403
+ <pre className={cn(TOOL_SECTION_PRE_CLASS, 'mt-1 whitespace-pre-wrap wrap-anywhere')}>
404
+ {view.rawResult}
405
+ </pre>
406
+ </details>
407
+ )}
408
+ {toolViewMode === 'technical' && (
409
+ <pre className={cn(TOOL_SECTION_PRE_CLASS, 'whitespace-pre-wrap wrap-anywhere')}>
410
+ {rawTechnicalTrace(part.args, part.result)}
411
+ </pre>
412
+ )}
413
+ </div>
414
+ )}
415
+ {open && view.inlineDiff && <DiffLines text={view.inlineDiff} />}
416
+ </div>
417
+ )
418
+ }
419
+
420
+ /**
421
+ * Flat, Cursor-style tool list. assistant-ui hands us a *range* of
422
+ * consecutive tool-call parts, but how that range is sliced is unstable: a
423
+ * live stream interleaves narration/reasoning between calls (many tiny
424
+ * ranges), while the settled message reconstructs every tool_call back-to-back
425
+ * (one big range). Rendering a "Tool actions · N steps" group off that range
426
+ * therefore reshuffled the whole turn the instant it settled.
427
+ *
428
+ * So we never group: each tool is a standalone row, and the wrapper just lays
429
+ * its children out on the tight `--tool-row-gap` rhythm. One range or ten,
430
+ * fragmented or consecutive, the result is pixel-identical — a tight, stable
431
+ * stack. The wrapper stays a single `<div>` of stable identity so children
432
+ * never remount as the range grows mid-stream. `ToolEmbedContext` is false so
433
+ * every row owns its own chrome (timer / preview / copy / inline approval).
434
+ */
435
+ export const ToolGroupSlot: FC<PropsWithChildren<{ endIndex: number; startIndex: number }>> = ({
436
+ children,
437
+ startIndex
438
+ }) => {
439
+ const messageId = useAuiState(s => s.message.id)
440
+ const messageRunning = useAuiState(selectMessageRunning)
441
+ const enterRef = useEnterAnimation(messageRunning, `tool-group:${messageId}:${startIndex}`)
442
+
443
+ return (
444
+ <ToolEmbedContext.Provider value={false}>
445
+ <div
446
+ className="grid min-w-0 max-w-full gap-(--tool-row-gap) overflow-hidden"
447
+ data-slot="tool-block"
448
+ ref={enterRef}
449
+ >
450
+ {children}
451
+ </div>
452
+ </ToolEmbedContext.Provider>
453
+ )
454
+ }
455
+
456
+ /**
457
+ * Per-tool fallback. Now strictly returns a single ToolEntry — the
458
+ * grouping decision lives in ToolGroupSlot above, so this never swaps
459
+ * its return type and the underlying ToolEntry stays mounted across
460
+ * group-shape changes.
461
+ */
462
+ export const ToolFallback = ({ toolCallId, toolName, args, isError, result }: ToolCallMessagePartProps) => {
463
+ const part: ToolPart = { args, isError, result, toolCallId, toolName, type: 'tool-call' }
464
+
465
+ return <ToolEntry part={part} />
466
+ }
@@ -0,0 +1,33 @@
1
+ 'use client'
2
+
3
+ import { type ComponentPropsWithRef, forwardRef } from 'react'
4
+
5
+ import { Button } from '@/components/ui/button'
6
+ import { Tip } from '@/components/ui/tooltip'
7
+ import { cn } from '@/lib/utils'
8
+
9
+ export interface TooltipIconButtonProps extends ComponentPropsWithRef<typeof Button> {
10
+ tooltip: string
11
+ side?: 'top' | 'bottom' | 'left' | 'right'
12
+ }
13
+
14
+ export const TooltipIconButton = forwardRef<HTMLButtonElement, TooltipIconButtonProps>(
15
+ ({ children, tooltip, side = 'bottom', className, ...rest }, ref) => {
16
+ return (
17
+ <Tip label={tooltip} side={side}>
18
+ <Button
19
+ size="icon-xs"
20
+ variant="ghost"
21
+ {...rest}
22
+ aria-label={tooltip}
23
+ className={cn('aui-button-icon', className)}
24
+ ref={ref}
25
+ >
26
+ {children}
27
+ </Button>
28
+ </Tip>
29
+ )
30
+ }
31
+ )
32
+
33
+ TooltipIconButton.displayName = 'TooltipIconButton'
@@ -0,0 +1,141 @@
1
+ import { ExportedMessageRepository } from '@assistant-ui/core/internal'
2
+ // Clicking a user bubble must open the inline edit composer — through the
3
+ // app's incremental external-store runtime (which reimplements capability
4
+ // resolution, incl. `edit: onEdit !== undefined`) and the stock runtime.
5
+ //
6
+ // Note: this covers the React/runtime wiring only. The Electron-level failure
7
+ // mode (titlebar -webkit-app-region:drag swallowing clicks on *stuck* sticky
8
+ // bubbles) is not reproducible in jsdom — see USER_BUBBLE_BASE_CLASS's no-drag
9
+ // carve-out in thread.tsx.
10
+ import { AssistantRuntimeProvider, type ThreadMessage, useExternalStoreRuntime } from '@assistant-ui/react'
11
+ import { fireEvent, render, screen, waitFor } from '@testing-library/react'
12
+ import { describe, expect, it, vi } from 'vitest'
13
+
14
+ import { useIncrementalExternalStoreRuntime } from '@/lib/incremental-external-store-runtime'
15
+
16
+ import { Thread } from './thread'
17
+
18
+ const createdAt = new Date('2026-05-01T00:00:00.000Z')
19
+
20
+ class TestResizeObserver {
21
+ observe() {}
22
+ unobserve() {}
23
+ disconnect() {}
24
+ }
25
+
26
+ vi.stubGlobal('ResizeObserver', TestResizeObserver)
27
+ vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) =>
28
+ window.setTimeout(() => callback(performance.now()), 0)
29
+ )
30
+ vi.stubGlobal('cancelAnimationFrame', (id: number) => window.clearTimeout(id))
31
+
32
+ Element.prototype.scrollTo = function scrollTo() {}
33
+
34
+ function stubOffsetDimension(
35
+ prop: 'offsetHeight' | 'offsetWidth',
36
+ clientProp: 'clientHeight' | 'clientWidth',
37
+ fallback: number
38
+ ) {
39
+ const previous = Object.getOwnPropertyDescriptor(HTMLElement.prototype, prop)
40
+
41
+ Object.defineProperty(HTMLElement.prototype, prop, {
42
+ configurable: true,
43
+ get() {
44
+ return previous?.get?.call(this) || (this as HTMLElement)[clientProp] || fallback
45
+ }
46
+ })
47
+ }
48
+
49
+ stubOffsetDimension('offsetWidth', 'clientWidth', 800)
50
+ stubOffsetDimension('offsetHeight', 'clientHeight', 600)
51
+
52
+ function userMessage(): ThreadMessage {
53
+ return {
54
+ id: 'user-1',
55
+ role: 'user',
56
+ content: [{ type: 'text', text: 'edit me please' }],
57
+ attachments: [],
58
+ createdAt,
59
+ metadata: { custom: {} }
60
+ } as ThreadMessage
61
+ }
62
+
63
+ function assistantMessage(): ThreadMessage {
64
+ return {
65
+ id: 'assistant-1',
66
+ role: 'assistant',
67
+ content: [{ type: 'text', text: 'done' }],
68
+ status: { type: 'complete', reason: 'stop' },
69
+ createdAt,
70
+ metadata: {
71
+ unstable_state: null,
72
+ unstable_annotations: [],
73
+ unstable_data: [],
74
+ steps: [],
75
+ custom: {}
76
+ }
77
+ } as ThreadMessage
78
+ }
79
+
80
+ // Mirrors chat/index.tsx: incremental runtime + messageRepository + onEdit.
81
+ function IncrementalHarness({ onEdit }: { onEdit: () => Promise<void> }) {
82
+ const repository = ExportedMessageRepository.fromArray([userMessage(), assistantMessage()])
83
+
84
+ const runtime = useIncrementalExternalStoreRuntime<ThreadMessage>({
85
+ messageRepository: repository,
86
+ isRunning: false,
87
+ setMessages: () => {},
88
+ onNew: async () => {},
89
+ onEdit,
90
+ onCancel: async () => {},
91
+ onReload: async () => {}
92
+ })
93
+
94
+ return (
95
+ <AssistantRuntimeProvider runtime={runtime}>
96
+ <Thread />
97
+ </AssistantRuntimeProvider>
98
+ )
99
+ }
100
+
101
+ // Control: stock external store runtime.
102
+ function StockHarness({ onEdit }: { onEdit: () => Promise<void> }) {
103
+ const runtime = useExternalStoreRuntime<ThreadMessage>({
104
+ messages: [userMessage(), assistantMessage()],
105
+ isRunning: false,
106
+ onNew: async () => {},
107
+ onEdit
108
+ })
109
+
110
+ return (
111
+ <AssistantRuntimeProvider runtime={runtime}>
112
+ <Thread />
113
+ </AssistantRuntimeProvider>
114
+ )
115
+ }
116
+
117
+ describe('click-to-edit user message', () => {
118
+ it('opens the edit composer with the incremental runtime', async () => {
119
+ const { container } = render(<IncrementalHarness onEdit={async () => {}} />)
120
+
121
+ const bubble = await screen.findByRole('button', { name: 'Edit message' })
122
+
123
+ fireEvent.click(bubble)
124
+
125
+ await waitFor(() => {
126
+ expect(container.querySelector('[data-slot="aui_edit-composer-root"]')).toBeTruthy()
127
+ })
128
+ })
129
+
130
+ it('opens the edit composer with the stock runtime', async () => {
131
+ const { container } = render(<StockHarness onEdit={async () => {}} />)
132
+
133
+ const bubble = await screen.findByRole('button', { name: 'Edit message' })
134
+
135
+ fireEvent.click(bubble)
136
+
137
+ await waitFor(() => {
138
+ expect(container.querySelector('[data-slot="aui_edit-composer-root"]')).toBeTruthy()
139
+ })
140
+ })
141
+ })