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,497 @@
1
+ 'use client'
2
+
3
+ import { TextMessagePartProvider, useMessagePartText } from '@assistant-ui/react'
4
+ import {
5
+ type StreamdownTextComponents,
6
+ StreamdownTextPrimitive,
7
+ type SyntaxHighlighterProps
8
+ } from '@assistant-ui/react-streamdown'
9
+ import { code } from '@streamdown/code'
10
+ import { type ComponentProps, memo, type ReactNode, useDeferredValue, useEffect, useMemo, useRef, useState } from 'react'
11
+
12
+ import { PreviewAttachment } from '@/components/chat/preview-attachment'
13
+ import { SyntaxHighlighter } from '@/components/chat/shiki-highlighter'
14
+ import { ZoomableImage } from '@/components/chat/zoomable-image'
15
+ import { normalizeExternalUrl, openExternalLink, PrettyLink } from '@/lib/external-link'
16
+ import { createMemoizedMathPlugin } from '@/lib/katex-memo'
17
+ import { preprocessMarkdown } from '@/lib/markdown-preprocess'
18
+ import {
19
+ filePathFromMediaPath,
20
+ mediaExternalUrl,
21
+ mediaKind,
22
+ mediaName,
23
+ mediaPathFromMarkdownHref,
24
+ mediaStreamUrl
25
+ } from '@/lib/media'
26
+ import { previewTargetFromMarkdownHref } from '@/lib/preview-targets'
27
+ import { cn } from '@/lib/utils'
28
+
29
+ // Math rendering plugin (KaTeX). Configured once at module scope — the
30
+ // plugin is stateless beyond its internal cache so re-creating per-render
31
+ // would needlessly thrash. We use a memoizing wrapper around rehype-katex
32
+ // (see lib/katex-memo.ts) so that during streaming we re-katex only the
33
+ // equations whose source actually changed since the last token. With the
34
+ // stock @streamdown/math plugin every equation re-renders on every token,
35
+ // which throttles UI updates badly for math-heavy responses; the memoized
36
+ // plugin keeps the steady-state work proportional to "new equations
37
+ // arriving" rather than "equations × tokens-per-second".
38
+ //
39
+ // `singleDollarTextMath: true` enables `$x^2$` for inline math (de-facto
40
+ // LLM convention). The default false-setting only accepts `$$...$$`.
41
+ const mathPlugin = createMemoizedMathPlugin({ singleDollarTextMath: true })
42
+
43
+ async function mediaSrc(path: string): Promise<string> {
44
+ if (/^(?:https?|data):/i.test(path)) {
45
+ return path
46
+ }
47
+
48
+ // Stream audio/video through the custom protocol: data URLs are capped and
49
+ // load the whole file into memory, which broke playback for larger videos.
50
+ if (window.NASTECHDesktop && ['audio', 'video'].includes(mediaKind(path))) {
51
+ return mediaStreamUrl(path)
52
+ }
53
+
54
+ if (!window.NASTECHDesktop?.readFileDataUrl) {
55
+ return mediaExternalUrl(path)
56
+ }
57
+
58
+ return window.NASTECHDesktop.readFileDataUrl(filePathFromMediaPath(path))
59
+ }
60
+
61
+ function OpenMediaButton({ kind, path }: { kind: 'audio' | 'video'; path: string }) {
62
+ return (
63
+ <button
64
+ className="mt-2 bg-transparent text-xs font-medium text-muted-foreground underline underline-offset-4 decoration-current/20 hover:text-foreground"
65
+ onClick={() => void window.NASTECHDesktop?.openExternal(mediaExternalUrl(path))}
66
+ type="button"
67
+ >
68
+ Open {kind} file
69
+ </button>
70
+ )
71
+ }
72
+
73
+ function MediaAttachment({ path }: { path: string }) {
74
+ const [src, setSrc] = useState('')
75
+ const [failed, setFailed] = useState(false)
76
+ const kind = mediaKind(path)
77
+ const name = mediaName(path)
78
+
79
+ useEffect(() => {
80
+ let cancelled = false
81
+ let objectUrl = ''
82
+
83
+ setFailed(false)
84
+ setSrc('')
85
+ void mediaSrc(path)
86
+ .then(value => {
87
+ if (value.startsWith('blob:')) {
88
+ objectUrl = value
89
+ }
90
+
91
+ if (!cancelled) {
92
+ setSrc(value)
93
+ } else if (objectUrl) {
94
+ URL.revokeObjectURL(objectUrl)
95
+ }
96
+ })
97
+ .catch(() => {
98
+ if (!cancelled) {
99
+ setFailed(true)
100
+ }
101
+ })
102
+
103
+ return () => {
104
+ cancelled = true
105
+
106
+ if (objectUrl) {
107
+ URL.revokeObjectURL(objectUrl)
108
+ }
109
+ }
110
+ }, [path])
111
+
112
+ if (kind === 'image' && src) {
113
+ return (
114
+ <span className="block">
115
+ <MarkdownImage alt={name} src={src} />
116
+ </span>
117
+ )
118
+ }
119
+
120
+ if (kind === 'audio' && src) {
121
+ return (
122
+ <span className="my-3 block max-w-md rounded-xl border border-border bg-muted/35 p-3">
123
+ <span className="mb-2 block truncate text-xs font-medium text-muted-foreground">{name}</span>
124
+ <audio className="block w-full" controls onError={() => setFailed(true)} preload="metadata" src={src} />
125
+ {failed && <OpenMediaButton kind="audio" path={path} />}
126
+ </span>
127
+ )
128
+ }
129
+
130
+ if (kind === 'video' && src) {
131
+ return (
132
+ <span className="my-3 block max-w-2xl rounded-xl border border-border bg-muted/35 p-3">
133
+ <span className="mb-2 block truncate text-xs font-medium text-muted-foreground">{name}</span>
134
+ <video
135
+ className="block max-h-112 w-full rounded-lg bg-black"
136
+ controls
137
+ onError={() => setFailed(true)}
138
+ src={src}
139
+ />
140
+ {failed && <OpenMediaButton kind="video" path={path} />}
141
+ </span>
142
+ )
143
+ }
144
+
145
+ return (
146
+ <a
147
+ className="font-semibold text-foreground underline underline-offset-4 decoration-current/20 wrap-anywhere"
148
+ href="#"
149
+ onClick={event => {
150
+ event.preventDefault()
151
+ openExternalLink(mediaExternalUrl(path))
152
+ }}
153
+ >
154
+ {failed ? `Open ${name}` : `Loading ${name}...`}
155
+ </a>
156
+ )
157
+ }
158
+
159
+ function childrenToText(children: unknown): string {
160
+ if (typeof children === 'string' || typeof children === 'number') {
161
+ return String(children).trim()
162
+ }
163
+
164
+ if (Array.isArray(children) && children.every(c => typeof c === 'string' || typeof c === 'number')) {
165
+ return children.join('').trim()
166
+ }
167
+
168
+ return ''
169
+ }
170
+
171
+ function MarkdownLink({ children, className, href, ...props }: ComponentProps<'a'>) {
172
+ const mediaPath = mediaPathFromMarkdownHref(href)
173
+
174
+ if (mediaPath) {
175
+ return <MediaAttachment path={mediaPath} />
176
+ }
177
+
178
+ const previewTarget = previewTargetFromMarkdownHref(href)
179
+
180
+ if (previewTarget) {
181
+ return <PreviewAttachment source="explicit-link" target={previewTarget} />
182
+ }
183
+
184
+ const target = href ? normalizeExternalUrl(href) : href
185
+
186
+ if (!target || !/^https?:\/\//i.test(target)) {
187
+ return (
188
+ <a
189
+ className={cn(
190
+ 'font-semibold text-foreground underline underline-offset-4 decoration-current/20 wrap-anywhere',
191
+ className
192
+ )}
193
+ href={href}
194
+ rel="noopener noreferrer"
195
+ target="_blank"
196
+ {...props}
197
+ >
198
+ {children}
199
+ </a>
200
+ )
201
+ }
202
+
203
+ const text = childrenToText(children)
204
+ const fallbackLabel = text && normalizeExternalUrl(text) !== target ? text : undefined
205
+
206
+ return (
207
+ <PrettyLink className={cn('wrap-anywhere', className)} fallbackLabel={fallbackLabel} href={target} {...props} />
208
+ )
209
+ }
210
+
211
+ function MarkdownImage({ className, src, alt, ...props }: ComponentProps<'img'>) {
212
+ return (
213
+ <ZoomableImage
214
+ alt={alt}
215
+ className={cn(
216
+ 'm-0 block h-auto w-auto max-h-(--image-preview-height) max-w-[min(100%,var(--image-preview-max-width))] rounded-lg object-contain shadow-[0_0.0625rem_0.125rem_color-mix(in_srgb,#000_4%,transparent),0_0.625rem_1.5rem_color-mix(in_srgb,#000_5%,transparent)]',
217
+ className
218
+ )}
219
+ containerClassName="my-2 block w-fit max-w-full"
220
+ slot="aui_markdown-image"
221
+ src={src}
222
+ {...props}
223
+ />
224
+ )
225
+ }
226
+
227
+ // Steady character-reveal for streaming text: decouples visible cadence from
228
+ // bursty arrival so text flows instead of popping (cf. assistant-ui's useSmooth,
229
+ // reimplemented for a tunable rate). Proportional drain — each frame reveals a
230
+ // slice of the backlog so the reveal converges within ~REVEAL_DRAIN_MS whatever
231
+ // the size; the per-frame cap stops a huge dump rendering as one slab. The loop
232
+ // is gated on backlog, not isRunning, so a stream that completes mid-reveal
233
+ // keeps draining its tail instead of snapping.
234
+ const REVEAL_DRAIN_MS = 500
235
+ const REVEAL_MAX_CHARS_PER_FRAME = 30
236
+
237
+ function useSmoothReveal(text: string, isRunning: boolean): string {
238
+ const [displayed, setDisplayed] = useState(isRunning ? '' : text)
239
+ const targetRef = useRef(text)
240
+ const shownRef = useRef(displayed)
241
+ const frameRef = useRef<number | null>(null)
242
+ const lastTickRef = useRef(0)
243
+
244
+ shownRef.current = displayed
245
+ targetRef.current = text
246
+
247
+ useEffect(() => {
248
+ if (typeof window === 'undefined') {
249
+ return
250
+ }
251
+
252
+ // Non-extending change (regenerate / branch / history swap): restart from
253
+ // empty while streaming, else snap to the replacement.
254
+ if (!text.startsWith(shownRef.current)) {
255
+ shownRef.current = isRunning ? '' : text
256
+ setDisplayed(shownRef.current)
257
+ }
258
+
259
+ if (shownRef.current.length >= text.length || frameRef.current !== null) {
260
+ return
261
+ }
262
+
263
+ lastTickRef.current = performance.now()
264
+
265
+ const tick = () => {
266
+ const now = performance.now()
267
+ const dt = now - lastTickRef.current
268
+ lastTickRef.current = now
269
+
270
+ const remaining = targetRef.current.length - shownRef.current.length
271
+ const add = Math.min(remaining, REVEAL_MAX_CHARS_PER_FRAME, Math.max(1, Math.ceil((remaining * dt) / REVEAL_DRAIN_MS)))
272
+ shownRef.current = targetRef.current.slice(0, shownRef.current.length + add)
273
+ setDisplayed(shownRef.current)
274
+
275
+ frameRef.current = shownRef.current.length < targetRef.current.length ? requestAnimationFrame(tick) : null
276
+ }
277
+
278
+ frameRef.current = requestAnimationFrame(tick)
279
+ }, [text, isRunning])
280
+
281
+ useEffect(
282
+ () => () => {
283
+ if (frameRef.current !== null && typeof window !== 'undefined') {
284
+ cancelAnimationFrame(frameRef.current)
285
+ }
286
+ },
287
+ []
288
+ )
289
+
290
+ return displayed
291
+ }
292
+
293
+ // Re-publish the part context with a smooth character-reveal, above
294
+ // DeferStreamingText so the reveal feeds the deferred markdown pipeline. Status
295
+ // stays running while revealing so the caret persists past the underlying part
296
+ // settling.
297
+ function SmoothStreamingText({ children }: { children: ReactNode }) {
298
+ const { text, status } = useMessagePartText()
299
+ const isRunning = status.type === 'running'
300
+ const revealed = useSmoothReveal(text, isRunning)
301
+
302
+ return (
303
+ <TextMessagePartProvider isRunning={isRunning || revealed !== text} text={revealed}>
304
+ {children}
305
+ </TextMessagePartProvider>
306
+ )
307
+ }
308
+
309
+ /**
310
+ * Re-publish the active message-part context with React's `useDeferredValue`
311
+ * applied to the streaming text and status. The outer wrapper still re-renders
312
+ * on every token, but the work it does is trivial (one hook, one provider).
313
+ *
314
+ * The expensive subtree (Streamdown → micromark → mdast → hast → React) lives
315
+ * inside `<TextMessagePartProvider>` and reads the deferred text via the
316
+ * normal `useMessagePartText` hook. React's concurrent scheduler then has
317
+ * permission to:
318
+ * - skip intermediate token states when the next token arrives mid-render
319
+ * (it abandons the in-flight deferred render and starts over)
320
+ * - deprioritize the markdown render when the main thread is busy with an
321
+ * urgent task (typing, scrolling, layout work elsewhere)
322
+ *
323
+ * Net effect: per-token CPU is unchanged but the *blocking* part of that work
324
+ * goes away — typing-while-streaming stays a single-frame paint, scroll
325
+ * stutter disappears, and the longtask histogram tightens because long
326
+ * commits can be interrupted and discarded.
327
+ *
328
+ * Industry standard (Streamdown's own block-array setState already uses
329
+ * `useTransition`); this just lifts the deferral up to the consumer text
330
+ * boundary so it covers the whole pipeline, not just the inner setState.
331
+ */
332
+ function DeferStreamingText({ children }: { children: ReactNode }) {
333
+ const { text, status } = useMessagePartText()
334
+ const deferredText = useDeferredValue(text)
335
+ const isRunning = status.type === 'running'
336
+
337
+ return (
338
+ <TextMessagePartProvider isRunning={isRunning} text={deferredText}>
339
+ {children}
340
+ </TextMessagePartProvider>
341
+ )
342
+ }
343
+
344
+ interface MarkdownTextSurfaceProps {
345
+ containerClassName?: string
346
+ containerProps?: ComponentProps<'div'>
347
+ }
348
+
349
+ // Headings shrink to chat scale rather than the prose default (h1≈xl). Kept
350
+ // table-driven so adding/tweaking levels is one row.
351
+ const HEADING_SIZES: Record<'h1' | 'h2' | 'h3' | 'h4', string> = {
352
+ h1: 'text-[1rem] tracking-tight',
353
+ h2: 'text-[0.9375rem] tracking-tight',
354
+ h3: 'text-[0.875rem]',
355
+ h4: 'text-[0.8125rem]'
356
+ }
357
+
358
+ const MARKDOWN_CONTAINER_CLASS_NAME = cn(
359
+ 'aui-md prose w-full max-w-none overflow-hidden text-[length:var(--conversation-text-font-size)] leading-(--dt-line-height) text-foreground',
360
+ 'prose-p:leading-(--dt-line-height) prose-li:leading-(--dt-line-height)',
361
+ 'prose-headings:text-foreground prose-strong:text-foreground',
362
+ 'prose-a:break-words prose-p:[overflow-wrap:anywhere]',
363
+ 'prose-li:marker:text-muted-foreground/70',
364
+ 'prose-code:rounded-[0.25rem] prose-code:px-[0.1875rem] prose-code:py-px prose-code:font-mono prose-code:text-[0.9em] prose-code:font-normal prose-code:before:content-none prose-code:after:content-none',
365
+ '[&>*:first-child]:mt-0 [&>*:last-child]:mb-0 [&>*+*]:mt-(--paragraph-gap)'
366
+ )
367
+
368
+ function MarkdownTextSurface({ containerClassName, containerProps }: MarkdownTextSurfaceProps) {
369
+ const { status } = useMessagePartText()
370
+ const isStreaming = status.type === 'running'
371
+
372
+ // Keep code parsing enabled while streaming so incomplete fenced blocks still
373
+ // render as code cards. The expensive Shiki pass is deferred by
374
+ // `SyntaxHighlighter` below when `isStreaming` is true.
375
+ const plugins = useMemo(() => ({ math: mathPlugin, code }), [])
376
+
377
+ const components = useMemo(
378
+ () =>
379
+ ({
380
+ h1: ({ className, ...props }: ComponentProps<'h1'>) => (
381
+ <h1 className={cn('my-1 font-semibold', HEADING_SIZES.h1, className)} {...props} />
382
+ ),
383
+ h2: ({ className, ...props }: ComponentProps<'h2'>) => (
384
+ <h2 className={cn('my-1 font-semibold', HEADING_SIZES.h2, className)} {...props} />
385
+ ),
386
+ h3: ({ className, ...props }: ComponentProps<'h3'>) => (
387
+ <h3 className={cn('my-1 font-semibold', HEADING_SIZES.h3, className)} {...props} />
388
+ ),
389
+ h4: ({ className, ...props }: ComponentProps<'h4'>) => (
390
+ <h4 className={cn('my-1 font-semibold', HEADING_SIZES.h4, className)} {...props} />
391
+ ),
392
+ p: ({ className, ...props }: ComponentProps<'p'>) => (
393
+ // Vertical rhythm is owned by styles.css (`--paragraph-gap`), which
394
+ // must out-specify Tailwind Typography's `prose` margins — so no
395
+ // `my-*` here on purpose.
396
+ <p className={cn('wrap-anywhere leading-(--dt-line-height)', className)} {...props} />
397
+ ),
398
+ a: MarkdownLink,
399
+ // `---` as quiet spacing, not a heavy full-width rule.
400
+ hr: (_props: ComponentProps<'hr'>) => <div aria-hidden className="my-3" />,
401
+ blockquote: ({ className, ...props }: ComponentProps<'blockquote'>) => (
402
+ <blockquote
403
+ className={cn('border-l-2 border-border pl-3 text-muted-foreground italic', className)}
404
+ {...props}
405
+ />
406
+ ),
407
+ ul: ({ className, ...props }: ComponentProps<'ul'>) => (
408
+ <ul className={cn('my-1 gap-0', className)} {...props} />
409
+ ),
410
+ ol: ({ className, ...props }: ComponentProps<'ol'>) => (
411
+ <ol className={cn('my-1 gap-0', className)} {...props} />
412
+ ),
413
+ li: ({ className, ...props }: ComponentProps<'li'>) => (
414
+ <li className={cn('leading-(--dt-line-height)', className)} {...props} />
415
+ ),
416
+ table: ({ className, ...props }: ComponentProps<'table'>) => (
417
+ <div className="aui-md-table my-2 max-w-full overflow-x-auto rounded-[0.375rem] border border-border">
418
+ <table
419
+ className={cn(
420
+ 'm-0 w-full border-collapse text-[0.8125rem] [&_tr]:border-b [&_tr]:border-border last:[&_tr]:border-0',
421
+ className
422
+ )}
423
+ {...props}
424
+ />
425
+ </div>
426
+ ),
427
+ thead: ({ className, ...props }: ComponentProps<'thead'>) => (
428
+ <thead className={cn('m-0 bg-muted/35 text-muted-foreground', className)} {...props} />
429
+ ),
430
+ th: ({ className, ...props }: ComponentProps<'th'>) => (
431
+ <th
432
+ className={cn(
433
+ 'px-2.5 py-1.5 text-left align-middle text-[0.75rem] font-medium text-muted-foreground',
434
+ className
435
+ )}
436
+ {...props}
437
+ />
438
+ ),
439
+ td: ({ className, ...props }: ComponentProps<'td'>) => (
440
+ <td className={cn('px-2.5 py-1.5 align-top text-[0.8125rem] leading-snug', className)} {...props} />
441
+ ),
442
+ img: MarkdownImage,
443
+ SyntaxHighlighter: (props: SyntaxHighlighterProps) => <SyntaxHighlighter {...props} defer={isStreaming} />
444
+ }) as StreamdownTextComponents,
445
+ [isStreaming]
446
+ )
447
+
448
+ return (
449
+ <StreamdownTextPrimitive
450
+ components={components}
451
+ containerClassName={cn(MARKDOWN_CONTAINER_CLASS_NAME, containerClassName)}
452
+ containerProps={containerProps}
453
+ lineNumbers={false}
454
+ mode="streaming"
455
+ // Always auto-close incomplete fences — even during streaming.
456
+ // Without this, an unclosed ```python ... ``` whose body contains
457
+ // `$` (very common: shell snippets, JS template strings, dollar
458
+ // amounts) leaks those dollars out to the math parser and they
459
+ // get rendered as broken inline math until the closing fence
460
+ // arrives. Shiki is independently deferred via `defer={isStreaming}`
461
+ // on the SyntaxHighlighter component, so we don't pay code-block
462
+ // tokenization on every token even with this set.
463
+ parseIncompleteMarkdown
464
+ plugins={plugins}
465
+ preprocess={preprocessMarkdown}
466
+ />
467
+ )
468
+ }
469
+
470
+ interface MarkdownTextContentProps extends MarkdownTextSurfaceProps {
471
+ isRunning: boolean
472
+ text: string
473
+ }
474
+
475
+ export function MarkdownTextContent({ isRunning, text, ...surfaceProps }: MarkdownTextContentProps) {
476
+ return (
477
+ <TextMessagePartProvider isRunning={isRunning} text={text}>
478
+ <SmoothStreamingText>
479
+ <DeferStreamingText>
480
+ <MarkdownTextSurface {...surfaceProps} />
481
+ </DeferStreamingText>
482
+ </SmoothStreamingText>
483
+ </TextMessagePartProvider>
484
+ )
485
+ }
486
+
487
+ const MarkdownTextImpl = () => {
488
+ return (
489
+ <SmoothStreamingText>
490
+ <DeferStreamingText>
491
+ <MarkdownTextSurface />
492
+ </DeferStreamingText>
493
+ </SmoothStreamingText>
494
+ )
495
+ }
496
+
497
+ export const MarkdownText = memo(MarkdownTextImpl)
@@ -0,0 +1,80 @@
1
+ import { cleanup, render, screen } from '@testing-library/react'
2
+ import { afterEach, describe, expect, it, vi } from 'vitest'
3
+
4
+ import { MessageRenderBoundary } from './message-render-boundary'
5
+
6
+ afterEach(cleanup)
7
+
8
+ function Boom({ error }: { error: Error | null }): null {
9
+ if (error) {
10
+ throw error
11
+ }
12
+
13
+ return null
14
+ }
15
+
16
+ const lookupError = new Error('tapClientLookup: Index 2 out of bounds (length: 2)')
17
+
18
+ describe('MessageRenderBoundary', () => {
19
+ it('renders children when nothing throws', () => {
20
+ render(
21
+ <MessageRenderBoundary resetKey="a">
22
+ <div>content</div>
23
+ </MessageRenderBoundary>
24
+ )
25
+
26
+ expect(screen.getByText('content')).toBeTruthy()
27
+ })
28
+
29
+ it('swallows the transient tapClientLookup out-of-bounds store race', () => {
30
+ const spy = vi.spyOn(console, 'error').mockImplementation(() => undefined)
31
+
32
+ const { container } = render(
33
+ <MessageRenderBoundary resetKey="a">
34
+ <Boom error={lookupError} />
35
+ </MessageRenderBoundary>
36
+ )
37
+
38
+ expect(container.innerHTML).toBe('')
39
+ spy.mockRestore()
40
+ })
41
+
42
+ it('recovers on the next consistent snapshot when resetKey changes', () => {
43
+ const spy = vi.spyOn(console, 'error').mockImplementation(() => undefined)
44
+
45
+ const { rerender } = render(
46
+ <MessageRenderBoundary resetKey="a">
47
+ <Boom error={lookupError} />
48
+ </MessageRenderBoundary>
49
+ )
50
+
51
+ rerender(
52
+ <MessageRenderBoundary resetKey="b">
53
+ <Boom error={null} />
54
+ </MessageRenderBoundary>
55
+ )
56
+
57
+ rerender(
58
+ <MessageRenderBoundary resetKey="b">
59
+ <div>recovered</div>
60
+ </MessageRenderBoundary>
61
+ )
62
+
63
+ expect(screen.getByText('recovered')).toBeTruthy()
64
+ spy.mockRestore()
65
+ })
66
+
67
+ it('re-throws unrelated errors so real bugs still surface', () => {
68
+ const spy = vi.spyOn(console, 'error').mockImplementation(() => undefined)
69
+
70
+ expect(() =>
71
+ render(
72
+ <MessageRenderBoundary resetKey="a">
73
+ <Boom error={new Error('genuine render bug')} />
74
+ </MessageRenderBoundary>
75
+ )
76
+ ).toThrow('genuine render bug')
77
+
78
+ spy.mockRestore()
79
+ })
80
+ })
@@ -0,0 +1,48 @@
1
+ import { Component, type ReactNode } from 'react'
2
+
3
+ // `@assistant-ui/store`'s index-keyed child-scope lookup (`tapClientLookup`)
4
+ // throws — rather than returning undefined — when a subscriber reads an index
5
+ // that the message/parts list no longer has. This races during high-frequency
6
+ // store replacement (session switch mid-stream, gateway reconnect replay): a
7
+ // subscriber from the previous, longer list is still in React's notification
8
+ // queue and reads one slot past the new, shorter array before it can unmount.
9
+ // The throw is transient and self-heals on the next consistent snapshot, but
10
+ // without a local boundary it unwinds to the root and blanks the whole app.
11
+ // Upstream-tracked: assistant-ui/assistant-ui#4051, #3652.
12
+ const isTransientLookupError = (error: unknown): boolean =>
13
+ error instanceof Error && /tapClient(Lookup|Resource).*out of bounds/.test(error.message)
14
+
15
+ interface Props {
16
+ // Changes whenever the message list mutates; remounting clears the caught
17
+ // error so the next consistent render recovers silently.
18
+ resetKey: string
19
+ children: ReactNode
20
+ }
21
+
22
+ export class MessageRenderBoundary extends Component<Props, { error: Error | null }> {
23
+ state: { error: Error | null } = { error: null }
24
+
25
+ static getDerivedStateFromError(error: Error) {
26
+ return { error }
27
+ }
28
+
29
+ componentDidUpdate(prev: Props) {
30
+ if (this.state.error && prev.resetKey !== this.props.resetKey) {
31
+ this.setState({ error: null })
32
+ }
33
+ }
34
+
35
+ render() {
36
+ if (this.state.error) {
37
+ // Only swallow the transient store race; re-throw anything else so real
38
+ // bugs still reach the root error boundary.
39
+ if (!isTransientLookupError(this.state.error)) {
40
+ throw this.state.error
41
+ }
42
+
43
+ return null
44
+ }
45
+
46
+ return this.props.children
47
+ }
48
+ }