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,957 @@
1
+ import type { QueryClient } from '@tanstack/react-query'
2
+ import { type MutableRefObject, useCallback, useEffect, useRef } from 'react'
3
+
4
+ import {
5
+ appendAssistantTextPart,
6
+ appendReasoningPart,
7
+ assistantTextPart,
8
+ type ChatMessage,
9
+ type ChatMessagePart,
10
+ chatMessageText,
11
+ type GatewayEventPayload,
12
+ reasoningPart,
13
+ renderMediaTags,
14
+ upsertToolPart
15
+ } from '@/lib/chat-messages'
16
+ import { coerceGatewayText, coerceThinkingText, normalizePersonalityValue } from '@/lib/chat-runtime'
17
+ import { triggerHaptic } from '@/lib/haptics'
18
+ import { isProviderSetupErrorMessage } from '@/lib/provider-setup-errors'
19
+ import { setClarifyRequest } from '@/store/clarify'
20
+ import { notify } from '@/store/notifications'
21
+ import { requestDesktopOnboarding } from '@/store/onboarding'
22
+ import { clearAllPrompts, setApprovalRequest, setSecretRequest, setSudoRequest } from '@/store/prompts'
23
+ import {
24
+ setCurrentBranch,
25
+ setCurrentCwd,
26
+ setCurrentFastMode,
27
+ setCurrentModel,
28
+ setCurrentPersonality,
29
+ setCurrentProvider,
30
+ setCurrentReasoningEffort,
31
+ setCurrentServiceTier,
32
+ setCurrentUsage,
33
+ setTurnStartedAt,
34
+ setYoloActive
35
+ } from '@/store/session'
36
+ import { clearSessionSubagents, pruneDelegateFallbackSubagents, upsertSubagent } from '@/store/subagents'
37
+ import { recordToolDiff } from '@/store/tool-diffs'
38
+ import type { RpcEvent } from '@/types/nastech'
39
+
40
+ import type { ClientSessionState } from '../../types'
41
+
42
+ interface MessageStreamOptions {
43
+ activeSessionIdRef: MutableRefObject<string | null>
44
+ hydrateFromStoredSession: (
45
+ attempts?: number,
46
+ storedSessionId?: string | null,
47
+ runtimeSessionId?: string | null
48
+ ) => Promise<void>
49
+ queryClient: QueryClient
50
+ refreshNasTechConfig: () => Promise<void>
51
+ refreshSessions: () => Promise<void>
52
+ updateSessionState: (
53
+ sessionId: string,
54
+ updater: (state: ClientSessionState) => ClientSessionState,
55
+ storedSessionId?: string | null
56
+ ) => ClientSessionState
57
+ }
58
+
59
+ interface QueuedStreamDeltas {
60
+ assistant: string
61
+ reasoning: string
62
+ }
63
+
64
+ // Minimum gap between two assistant-text flushes during a stream. Was 16ms
65
+ // (rAF only), which at typical LLM token rates of ~30-80 tok/sec meant every
66
+ // token got its own React commit + Streamdown markdown re-parse, scaling
67
+ // linearly with the growing last-block length. Bumping to 33ms lets ~2 tokens
68
+ // batch into one commit at 60 tok/sec without introducing visible lag on the
69
+ // streaming text (still 30 fps of visible text growth). Big perceived
70
+ // smoothness win on long messages with big trailing paragraphs; see
71
+ // `scripts/profile-typing-lag.md` for the measurement work behind this.
72
+ const STREAM_DELTA_FLUSH_MS = 33
73
+
74
+ // Gateway/provider failures sometimes arrive as message.complete text instead
75
+ // of an explicit error event. Treat matches as inline assistant errors so they
76
+ // persist like real error events and don't get erased by hydrate fallback.
77
+ const COMPLETION_ERROR_PATTERNS = [
78
+ /^API call failed after \d+ retries:/i,
79
+ /^HTTP\s+\d{3}\b/i,
80
+ /^(Provider|Gateway)\s+error:/i
81
+ ]
82
+
83
+ function completionErrorText(finalText: string): string | null {
84
+ const text = finalText.trim()
85
+
86
+ return text && COMPLETION_ERROR_PATTERNS.some(re => re.test(text)) ? text : null
87
+ }
88
+
89
+ const SUBAGENT_EVENT_TYPES = new Set([
90
+ 'subagent.spawn_requested',
91
+ 'subagent.start',
92
+ 'subagent.thinking',
93
+ 'subagent.tool',
94
+ 'subagent.progress',
95
+ 'subagent.complete'
96
+ ])
97
+
98
+ // Anonymous progress events that carry todos but no name still belong to the
99
+ // todo stream; named todo events are obviously routed there too.
100
+ function toTodoPayload(payload: GatewayEventPayload | undefined): GatewayEventPayload | undefined {
101
+ if (!payload) {
102
+ return undefined
103
+ }
104
+
105
+ const isTodo = payload.name === 'todo' || (!payload.name && Object.hasOwn(payload, 'todos'))
106
+
107
+ return isTodo ? { ...payload, name: 'todo', tool_id: payload.tool_id || 'todo-live' } : undefined
108
+ }
109
+
110
+ function asRecord(value: unknown): Record<string, unknown> {
111
+ return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : {}
112
+ }
113
+
114
+ function parseMaybeRecord(value: unknown): Record<string, unknown> {
115
+ if (typeof value === 'string') {
116
+ try {
117
+ return asRecord(JSON.parse(value))
118
+ } catch {
119
+ return {}
120
+ }
121
+ }
122
+
123
+ return asRecord(value)
124
+ }
125
+
126
+ const firstString = (...candidates: unknown[]): string => {
127
+ for (const v of candidates) {
128
+ if (typeof v === 'string' && v) {
129
+ return v
130
+ }
131
+ }
132
+
133
+ return ''
134
+ }
135
+
136
+ function delegateTaskPayloads(
137
+ payload: GatewayEventPayload | undefined,
138
+ phase: 'running' | 'complete',
139
+ sourceEventType?: string
140
+ ): Record<string, unknown>[] {
141
+ if (payload?.name !== 'delegate_task') {
142
+ return []
143
+ }
144
+
145
+ const args = parseMaybeRecord(payload.args ?? payload.input)
146
+ const result = parseMaybeRecord(payload.result)
147
+ const rawTasks = Array.isArray(args.tasks) ? args.tasks : []
148
+ const tasks = rawTasks.length ? rawTasks.map(parseMaybeRecord) : [args]
149
+ const status = phase === 'complete' ? (payload.error ? 'failed' : 'completed') : 'running'
150
+ const toolId = payload.tool_id || payload.tool_call_id || payload.id || 'delegate_task'
151
+ const progressText = firstString(payload.preview, payload.message, payload.context)
152
+
153
+ const eventType =
154
+ phase === 'complete'
155
+ ? 'subagent.complete'
156
+ : sourceEventType === 'tool.start'
157
+ ? 'subagent.start'
158
+ : 'subagent.progress'
159
+
160
+ return tasks.map((task, index) => {
161
+ const goal = firstString(task.goal, args.goal, payload.context) || 'Delegated task'
162
+ const summary = firstString(result.summary, payload.summary, payload.message)
163
+
164
+ return {
165
+ depth: 0,
166
+ duration_seconds: payload.duration_s,
167
+ goal,
168
+ status,
169
+ subagent_id: `delegate-tool:${toolId}:${index}`,
170
+ summary: summary || undefined,
171
+ task_count: tasks.length,
172
+ task_index: index,
173
+ text: eventType === 'subagent.progress' ? progressText || goal : undefined,
174
+ tool_name: eventType === 'subagent.start' ? 'delegate_task' : undefined,
175
+ tool_preview: eventType === 'subagent.start' ? progressText : undefined,
176
+ toolsets: Array.isArray(task.toolsets) ? task.toolsets : Array.isArray(args.toolsets) ? args.toolsets : [],
177
+ event_type: eventType,
178
+ output_tail:
179
+ phase === 'complete' && summary
180
+ ? [{ is_error: Boolean(payload.error), preview: summary, tool: 'delegate_task' }]
181
+ : undefined
182
+ }
183
+ })
184
+ }
185
+
186
+ export function useMessageStream({
187
+ activeSessionIdRef,
188
+ hydrateFromStoredSession,
189
+ queryClient,
190
+ refreshNasTechConfig,
191
+ refreshSessions,
192
+ updateSessionState
193
+ }: MessageStreamOptions) {
194
+ // Patch the in-flight assistant message (or seed it). Centralises the
195
+ // streamId/groupId bookkeeping every event callback would otherwise repeat.
196
+ const mutateStream = useCallback(
197
+ (
198
+ sessionId: string,
199
+ transform: (parts: ChatMessagePart[], message: ChatMessage) => ChatMessagePart[],
200
+ seed: () => ChatMessagePart[],
201
+ opts: {
202
+ pending?: (message: ChatMessage) => boolean
203
+ } = {}
204
+ ) => {
205
+ const apply = () => {
206
+ updateSessionState(sessionId, state => {
207
+ // After a stop, drop any late deltas / tool events for the
208
+ // cancelled turn so they don't keep growing the (now finalized)
209
+ // assistant bubble or, worse, seed a brand-new bubble that
210
+ // appears to belong to the next user message.
211
+ if (state.interrupted) {
212
+ return state
213
+ }
214
+
215
+ const streamId = state.streamId ?? `assistant-stream-${Date.now()}`
216
+ const groupId = state.pendingBranchGroup ?? undefined
217
+ const prev = state.messages
218
+ let nextMessages: ChatMessage[]
219
+
220
+ if (!prev.some(m => m.id === streamId)) {
221
+ nextMessages = [
222
+ ...prev,
223
+ {
224
+ id: streamId,
225
+ role: 'assistant',
226
+ parts: seed(),
227
+ pending: true,
228
+ branchGroupId: groupId
229
+ }
230
+ ]
231
+ } else {
232
+ nextMessages = prev.map(m =>
233
+ m.id === streamId
234
+ ? {
235
+ ...m,
236
+ parts: transform(m.parts, m),
237
+ pending: opts.pending ? opts.pending(m) : true
238
+ }
239
+ : m
240
+ )
241
+ }
242
+
243
+ return {
244
+ ...state,
245
+ messages: nextMessages,
246
+ streamId,
247
+ sawAssistantPayload: true,
248
+ awaitingResponse: false
249
+ }
250
+ })
251
+ }
252
+
253
+ apply()
254
+ },
255
+ [updateSessionState]
256
+ )
257
+
258
+ const queuedDeltasRef = useRef<Map<string, QueuedStreamDeltas>>(new Map())
259
+ const flushHandleRef = useRef<number | null>(null)
260
+ const lastFlushAtRef = useRef<number>(0)
261
+ const nativeSubagentSessionsRef = useRef<Set<string>>(new Set())
262
+
263
+ const flushQueuedDeltas = useCallback(
264
+ (sessionId?: string) => {
265
+ const queue = queuedDeltasRef.current
266
+ const ids = sessionId ? [sessionId] : [...queue.keys()]
267
+
268
+ for (const id of ids) {
269
+ const queued = queue.get(id)
270
+
271
+ if (!queued) {
272
+ continue
273
+ }
274
+
275
+ queue.delete(id)
276
+
277
+ if (queued.assistant) {
278
+ mutateStream(
279
+ id,
280
+ parts => appendAssistantTextPart(parts, queued.assistant),
281
+ () => [assistantTextPart(queued.assistant)]
282
+ )
283
+ }
284
+
285
+ if (queued.reasoning) {
286
+ mutateStream(
287
+ id,
288
+ parts => appendReasoningPart(parts, queued.reasoning),
289
+ () => [reasoningPart(queued.reasoning)]
290
+ )
291
+ }
292
+ }
293
+ },
294
+ [mutateStream]
295
+ )
296
+
297
+ const scheduleDeltaFlush = useCallback(() => {
298
+ if (flushHandleRef.current !== null) {
299
+ return
300
+ }
301
+
302
+ if (typeof window === 'undefined') {
303
+ flushQueuedDeltas()
304
+
305
+ return
306
+ }
307
+
308
+ // Enforce a floor on the gap between two flushes. Without it, an LLM
309
+ // emitting tokens slower than the rAF cadence (~30-80 tok/sec is typical)
310
+ // forces one React commit + Streamdown re-parse per token, and the
311
+ // last-block markdown re-parse cost is roughly linear in current block
312
+ // length. With this floor, slower streams still coalesce ~2 tokens per
313
+ // commit and the synthetic harness shows longtask counts drop from ~5/5s
314
+ // to ~1/5s on big sessions (see scripts/profile-typing-lag.md).
315
+ const sinceLast = performance.now() - lastFlushAtRef.current
316
+
317
+ const runFlush = () => {
318
+ flushHandleRef.current = null
319
+ lastFlushAtRef.current = performance.now()
320
+ flushQueuedDeltas()
321
+ }
322
+
323
+ if (sinceLast >= STREAM_DELTA_FLUSH_MS && typeof window.requestAnimationFrame === 'function') {
324
+ flushHandleRef.current = window.requestAnimationFrame(runFlush)
325
+
326
+ return
327
+ }
328
+
329
+ flushHandleRef.current = window.setTimeout(runFlush, Math.max(0, STREAM_DELTA_FLUSH_MS - sinceLast))
330
+ }, [flushQueuedDeltas])
331
+
332
+ const queueDelta = useCallback(
333
+ (sessionId: string, key: keyof QueuedStreamDeltas, delta: string) => {
334
+ if (!delta) {
335
+ return
336
+ }
337
+
338
+ const queued = queuedDeltasRef.current.get(sessionId) ?? { assistant: '', reasoning: '' }
339
+ queued[key] += delta
340
+ queuedDeltasRef.current.set(sessionId, queued)
341
+ scheduleDeltaFlush()
342
+ },
343
+ [scheduleDeltaFlush]
344
+ )
345
+
346
+ useEffect(
347
+ () => () => {
348
+ if (flushHandleRef.current !== null && typeof window !== 'undefined') {
349
+ if (typeof window.cancelAnimationFrame === 'function') {
350
+ window.cancelAnimationFrame(flushHandleRef.current)
351
+ } else {
352
+ window.clearTimeout(flushHandleRef.current)
353
+ }
354
+ }
355
+
356
+ flushHandleRef.current = null
357
+ flushQueuedDeltas()
358
+ },
359
+ [flushQueuedDeltas]
360
+ )
361
+
362
+ const appendAssistantDelta = useCallback(
363
+ (sessionId: string, delta: string) => {
364
+ if (!delta) {
365
+ return
366
+ }
367
+
368
+ queueDelta(sessionId, 'assistant', delta)
369
+ },
370
+ [queueDelta]
371
+ )
372
+
373
+ const appendReasoningDelta = useCallback(
374
+ (sessionId: string, delta: string, replace = false) => {
375
+ if (!delta) {
376
+ return
377
+ }
378
+
379
+ if (!replace) {
380
+ queueDelta(sessionId, 'reasoning', delta)
381
+
382
+ return
383
+ }
384
+
385
+ flushQueuedDeltas(sessionId)
386
+
387
+ mutateStream(
388
+ sessionId,
389
+ (parts, message) => {
390
+ if (replace && chatMessageText(message).trim()) {
391
+ return parts
392
+ }
393
+
394
+ if (replace) {
395
+ return [...parts.filter(part => part.type !== 'reasoning'), reasoningPart(delta)]
396
+ }
397
+
398
+ return appendReasoningPart(parts, delta)
399
+ },
400
+ () => [reasoningPart(delta)]
401
+ )
402
+ },
403
+ [flushQueuedDeltas, mutateStream, queueDelta]
404
+ )
405
+
406
+ const upsertToolCall = useCallback(
407
+ (
408
+ sessionId: string,
409
+ payload: GatewayEventPayload | undefined,
410
+ phase: 'running' | 'complete',
411
+ sourceEventType?: string
412
+ ) => {
413
+ // Text deltas flush on a timer but tool events apply now; flush first so
414
+ // a tool part can't jump ahead of the text that preceded it.
415
+ flushQueuedDeltas(sessionId)
416
+
417
+ if (!nativeSubagentSessionsRef.current.has(sessionId)) {
418
+ for (const subagentPayload of delegateTaskPayloads(payload, phase, sourceEventType)) {
419
+ upsertSubagent(
420
+ sessionId,
421
+ subagentPayload,
422
+ true,
423
+ phase === 'complete' ? 'delegate.complete' : 'delegate.running'
424
+ )
425
+ }
426
+ }
427
+
428
+ mutateStream(
429
+ sessionId,
430
+ parts => upsertToolPart(parts, payload, phase),
431
+ () => upsertToolPart([], payload, phase),
432
+ { pending: m => phase !== 'complete' || (m.pending ?? false) }
433
+ )
434
+ },
435
+ [flushQueuedDeltas, mutateStream]
436
+ )
437
+
438
+ const completeAssistantMessage = useCallback(
439
+ (sessionId: string, text: string) => {
440
+ let shouldHydrate = false
441
+
442
+ const completedState = updateSessionState(sessionId, state => {
443
+ // Late completion from an already-cancelled turn: cancelRun has
444
+ // already finalized the bubble (kept the partial text, dropped it if
445
+ // empty). Re-running the dedupe below would replace the partial with
446
+ // the just-cancelled full text, so we settle and bail instead.
447
+ if (state.interrupted) {
448
+ return {
449
+ ...state,
450
+ awaitingResponse: false,
451
+ busy: false,
452
+ needsInput: false,
453
+ pendingBranchGroup: null,
454
+ streamId: null,
455
+ turnStartedAt: null
456
+ }
457
+ }
458
+
459
+ const streamId = state.streamId
460
+ const finalText = renderMediaTags(text).trim()
461
+ const completionError = completionErrorText(finalText)
462
+ const normalize = (value: string) => value.replace(/\s+/g, ' ').trim()
463
+ const dedupeReference = normalize(finalText)
464
+
465
+ const replaceTextPart = (parts: ChatMessagePart[]) => {
466
+ const kept = parts.filter(part => {
467
+ if (part.type === 'text') {
468
+ return false
469
+ }
470
+
471
+ if (part.type !== 'reasoning' || !dedupeReference) {
472
+ return true
473
+ }
474
+
475
+ const r = normalize(part.text)
476
+
477
+ return !(r && (dedupeReference.startsWith(r) || r.startsWith(dedupeReference)))
478
+ })
479
+
480
+ return finalText ? [...kept, assistantTextPart(finalText)] : kept
481
+ }
482
+
483
+ const completeMessage = (message: ChatMessage): ChatMessage =>
484
+ completionError
485
+ ? {
486
+ ...message,
487
+ error: completionError,
488
+ parts: message.parts.filter(part => part.type !== 'text'),
489
+ pending: false
490
+ }
491
+ : {
492
+ ...message,
493
+ parts: replaceTextPart(message.parts),
494
+ pending: false
495
+ }
496
+
497
+ const newAssistantFromCompletion = (): ChatMessage => ({
498
+ id: `assistant-${Date.now()}`,
499
+ role: 'assistant',
500
+ parts: completionError ? [] : [assistantTextPart(finalText)],
501
+ branchGroupId: state.pendingBranchGroup ?? undefined,
502
+ ...(completionError && { error: completionError })
503
+ })
504
+
505
+ const prev = state.messages
506
+ let nextMessages = prev
507
+
508
+ if (streamId && prev.some(m => m.id === streamId)) {
509
+ nextMessages = prev.map(m => (m.id === streamId ? completeMessage(m) : m))
510
+ } else {
511
+ const fallbackIndex = [...prev]
512
+ .reverse()
513
+ .findIndex(message => message.role === 'assistant' && !message.hidden)
514
+
515
+ if (fallbackIndex >= 0) {
516
+ const index = prev.length - 1 - fallbackIndex
517
+ const existing = prev[index]
518
+ const existingText = chatMessageText(existing).trim()
519
+
520
+ if (existing.pending || (finalText && existingText === finalText)) {
521
+ nextMessages = prev.map((message, messageIndex) =>
522
+ messageIndex === index ? completeMessage(message) : message
523
+ )
524
+ } else if (finalText) {
525
+ nextMessages = [...prev, newAssistantFromCompletion()]
526
+ }
527
+ } else if (finalText) {
528
+ nextMessages = [...prev, newAssistantFromCompletion()]
529
+ }
530
+ }
531
+
532
+ const hasInlineError = nextMessages.some(m => m.role === 'assistant' && m.error && !m.hidden)
533
+ const lastVisible = [...nextMessages].reverse().find(m => !m.hidden)
534
+ const unresolvedUserTail = lastVisible?.role === 'user'
535
+ shouldHydrate =
536
+ !completionError && !hasInlineError && !unresolvedUserTail && (!state.sawAssistantPayload || !finalText)
537
+
538
+ return {
539
+ ...state,
540
+ messages: nextMessages,
541
+ streamId: null,
542
+ pendingBranchGroup: null,
543
+ awaitingResponse: false,
544
+ busy: false,
545
+ needsInput: false,
546
+ turnStartedAt: null
547
+ }
548
+ })
549
+
550
+ void refreshSessions().catch(() => undefined)
551
+
552
+ if (shouldHydrate) {
553
+ void hydrateFromStoredSession(3, completedState.storedSessionId, sessionId)
554
+ }
555
+
556
+ if (document.hidden && sessionId === activeSessionIdRef.current) {
557
+ void window.NASTECHDesktop?.notify({
558
+ title: 'NasTech finished',
559
+ body: text.slice(0, 140) || 'The response is ready.'
560
+ })
561
+ }
562
+ },
563
+ [activeSessionIdRef, hydrateFromStoredSession, refreshSessions, updateSessionState]
564
+ )
565
+
566
+ const failAssistantMessage = useCallback(
567
+ (sessionId: string, errorMessage: string) => {
568
+ updateSessionState(sessionId, state => {
569
+ const streamId = state.streamId ?? `assistant-error-${Date.now()}`
570
+ const groupId = state.pendingBranchGroup ?? undefined
571
+ const prev = state.messages
572
+ const error = errorMessage.trim() || 'NasTech reported an error'
573
+
574
+ const nextMessages = prev.some(m => m.id === streamId)
575
+ ? prev.map(message =>
576
+ message.id === streamId
577
+ ? {
578
+ ...message,
579
+ error,
580
+ pending: false
581
+ }
582
+ : message
583
+ )
584
+ : [
585
+ ...prev,
586
+ {
587
+ id: streamId,
588
+ role: 'assistant' as const,
589
+ parts: [],
590
+ error,
591
+ pending: false,
592
+ branchGroupId: groupId
593
+ }
594
+ ]
595
+
596
+ return {
597
+ ...state,
598
+ messages: nextMessages,
599
+ streamId: null,
600
+ pendingBranchGroup: null,
601
+ sawAssistantPayload: true,
602
+ awaitingResponse: false,
603
+ busy: false,
604
+ needsInput: false,
605
+ turnStartedAt: null
606
+ }
607
+ })
608
+ },
609
+ [updateSessionState]
610
+ )
611
+
612
+ const handleGatewayEvent = useCallback(
613
+ (event: RpcEvent) => {
614
+ const payload = event.payload as GatewayEventPayload | undefined
615
+ const explicitSid = event.session_id || ''
616
+ const sessionId = explicitSid || activeSessionIdRef.current
617
+ const isActiveEvent = !!sessionId && sessionId === activeSessionIdRef.current
618
+
619
+ if (event.type === 'gateway.ready') {
620
+ return
621
+ } else if (event.type === 'session.info') {
622
+ // Apply session-scoped fields when the event targets the active
623
+ // session, OR when it's a global broadcast and we have no session.
624
+ const apply = explicitSid ? isActiveEvent : !activeSessionIdRef.current
625
+ const modelChanged = typeof payload?.model === 'string'
626
+ const providerChanged = typeof payload?.provider === 'string'
627
+ const runningChanged = typeof payload?.running === 'boolean'
628
+
629
+ if (apply) {
630
+ const runtimeInfo: { branch?: string; cwd?: string } = {}
631
+
632
+ if (modelChanged) {
633
+ setCurrentModel(payload!.model || '')
634
+ }
635
+
636
+ if (providerChanged) {
637
+ setCurrentProvider(payload!.provider || '')
638
+ }
639
+
640
+ if (typeof payload?.cwd === 'string') {
641
+ setCurrentCwd(payload.cwd)
642
+ runtimeInfo.cwd = payload.cwd
643
+ }
644
+
645
+ if (typeof payload?.branch === 'string') {
646
+ setCurrentBranch(payload.branch)
647
+ runtimeInfo.branch = payload.branch
648
+ }
649
+
650
+ if (sessionId && (runtimeInfo.cwd !== undefined || runtimeInfo.branch !== undefined)) {
651
+ updateSessionState(sessionId, state => ({
652
+ ...state,
653
+ branch: runtimeInfo.branch ?? state.branch,
654
+ cwd: runtimeInfo.cwd ?? state.cwd
655
+ }))
656
+ }
657
+
658
+ if (typeof payload?.personality === 'string') {
659
+ setCurrentPersonality(normalizePersonalityValue(payload.personality))
660
+ }
661
+
662
+ if (typeof payload?.reasoning_effort === 'string') {
663
+ setCurrentReasoningEffort(payload.reasoning_effort)
664
+ }
665
+
666
+ if (typeof payload?.service_tier === 'string') {
667
+ setCurrentServiceTier(payload.service_tier)
668
+ }
669
+
670
+ if (typeof payload?.fast === 'boolean') {
671
+ setCurrentFastMode(payload.fast)
672
+ }
673
+
674
+ if (typeof payload?.yolo === 'boolean') {
675
+ setYoloActive(payload.yolo)
676
+ }
677
+
678
+ if (runningChanged && sessionId) {
679
+ updateSessionState(sessionId, state => {
680
+ const busy = Boolean(payload!.running)
681
+
682
+ if (state.busy === busy && (busy || !state.awaitingResponse)) {
683
+ return state
684
+ }
685
+
686
+ if (busy) {
687
+ return {
688
+ ...state,
689
+ busy,
690
+ turnStartedAt: state.turnStartedAt ?? Date.now()
691
+ }
692
+ }
693
+
694
+ if (state.awaitingResponse && !state.sawAssistantPayload) {
695
+ return state
696
+ }
697
+
698
+ return {
699
+ ...state,
700
+ awaitingResponse: false,
701
+ busy,
702
+ pendingBranchGroup: null,
703
+ streamId: null,
704
+ turnStartedAt: null
705
+ }
706
+ })
707
+ }
708
+ }
709
+
710
+ if (payload?.usage && (!explicitSid || isActiveEvent)) {
711
+ setCurrentUsage(current => ({ ...current, ...payload.usage }))
712
+ }
713
+
714
+ if (typeof payload?.credential_warning === 'string' && payload.credential_warning) {
715
+ requestDesktopOnboarding(payload.credential_warning)
716
+ }
717
+
718
+ void refreshNasTechConfig()
719
+
720
+ if (modelChanged || providerChanged) {
721
+ void queryClient.invalidateQueries({
722
+ queryKey: explicitSid && sessionId ? ['model-options', sessionId] : ['model-options']
723
+ })
724
+ }
725
+ } else if (event.type === 'message.start') {
726
+ if (!sessionId) {
727
+ return
728
+ }
729
+
730
+ flushQueuedDeltas(sessionId)
731
+ clearSessionSubagents(sessionId)
732
+ nativeSubagentSessionsRef.current.delete(sessionId)
733
+
734
+ if (isActiveEvent) {
735
+ triggerHaptic('streamStart')
736
+ }
737
+
738
+ updateSessionState(sessionId, state => ({
739
+ ...state,
740
+ busy: true,
741
+ awaitingResponse: true,
742
+ sawAssistantPayload: false,
743
+ interrupted: false,
744
+ turnStartedAt: Date.now()
745
+ }))
746
+
747
+ if (isActiveEvent) {
748
+ setTurnStartedAt(Date.now())
749
+ }
750
+ } else if (event.type === 'message.delta') {
751
+ if (sessionId) {
752
+ appendAssistantDelta(sessionId, coerceGatewayText(payload?.text))
753
+ }
754
+ } else if (event.type === 'thinking.delta') {
755
+ // thinking.delta carries the kawaii spinner status (face + verb from
756
+ // KawaiiSpinner), not real reasoning. The bottom-of-thread loading
757
+ // indicator already covers that UX, so we ignore these events to
758
+ // avoid a duplicative "Thinking" disclosure showing spinner text.
759
+ } else if (event.type === 'reasoning.delta') {
760
+ if (sessionId) {
761
+ appendReasoningDelta(sessionId, coerceThinkingText(payload?.text))
762
+ }
763
+ } else if (event.type === 'reasoning.available') {
764
+ if (sessionId) {
765
+ appendReasoningDelta(sessionId, coerceThinkingText(payload?.text), true)
766
+ }
767
+ } else if (event.type === 'message.complete') {
768
+ if (!sessionId) {
769
+ return
770
+ }
771
+
772
+ // Turn ended — drop any blocking prompt still open for THIS session
773
+ // (e.g. interrupted, or the approval already resolved). Scoped to the
774
+ // session so a background turn finishing can't wipe the active chat's
775
+ // prompt, and vice versa.
776
+ clearAllPrompts(sessionId)
777
+
778
+ flushQueuedDeltas(sessionId)
779
+
780
+ if (isActiveEvent) {
781
+ triggerHaptic('streamDone')
782
+ }
783
+
784
+ const finalText = coerceGatewayText(payload?.text) || coerceGatewayText(payload?.rendered)
785
+ completeAssistantMessage(sessionId, finalText)
786
+
787
+ if (isActiveEvent) {
788
+ setTurnStartedAt(null)
789
+ }
790
+
791
+ if (payload?.usage) {
792
+ setCurrentUsage(current => ({ ...current, ...payload.usage }))
793
+ }
794
+ } else if (event.type === 'tool.start' || event.type === 'tool.progress' || event.type === 'tool.generating') {
795
+ if (!sessionId) {
796
+ return
797
+ }
798
+
799
+ flushQueuedDeltas(sessionId)
800
+ upsertToolCall(sessionId, toTodoPayload(payload) ?? payload, 'running', event.type)
801
+ } else if (event.type === 'tool.complete') {
802
+ if (sessionId) {
803
+ flushQueuedDeltas(sessionId)
804
+ upsertToolCall(sessionId, toTodoPayload(payload) ?? payload, 'complete', event.type)
805
+ // A pending clarify blocks the turn, so the first tool.complete after
806
+ // one is the clarify resolving — drop the "needs input" flag here so
807
+ // the sidebar indicator clears as soon as it's answered, not only at
808
+ // message.complete.
809
+ updateSessionState(sessionId, state => (state.needsInput ? { ...state, needsInput: false } : state))
810
+ }
811
+
812
+ if (typeof payload?.inline_diff === 'string' && payload.inline_diff.trim()) {
813
+ recordToolDiff(payload.tool_id || payload.name || '', payload.inline_diff)
814
+ }
815
+ } else if (SUBAGENT_EVENT_TYPES.has(event.type)) {
816
+ if (sessionId && payload) {
817
+ if (!nativeSubagentSessionsRef.current.has(sessionId)) {
818
+ pruneDelegateFallbackSubagents(sessionId)
819
+ }
820
+
821
+ nativeSubagentSessionsRef.current.add(sessionId)
822
+ upsertSubagent(
823
+ sessionId,
824
+ payload as Record<string, unknown>,
825
+ event.type === 'subagent.spawn_requested' || event.type === 'subagent.start',
826
+ event.type
827
+ )
828
+ }
829
+ } else if (event.type === 'clarify.request') {
830
+ // Surface the clarify tool's overlay. The Python side is blocked on
831
+ // `clarify.respond`, so without this handler the agent would hang
832
+ // forever (see tools/clarify_tool.py + tui_gateway/server.py:_block).
833
+ //
834
+ // Store the request for whichever session raised it — even a background
835
+ // one. clarify.request is a one-shot event; if we dropped it for an
836
+ // unfocused session, that session would block on `clarify.respond`
837
+ // indefinitely and re-focusing it could never recover (the event is
838
+ // gone). Parking it per-session lets the user answer once they switch
839
+ // over; the inline ClarifyTool reads the active session's entry.
840
+ const requestId = typeof payload?.request_id === 'string' ? payload.request_id : ''
841
+ const question = typeof payload?.question === 'string' ? payload.question : ''
842
+
843
+ if (requestId && question) {
844
+ setClarifyRequest({
845
+ requestId,
846
+ question,
847
+ choices: Array.isArray(payload?.choices) ? payload!.choices!.filter(c => typeof c === 'string') : null,
848
+ sessionId: sessionId ?? null
849
+ })
850
+
851
+ // The transcript only renders the active session, so a background
852
+ // clarify is otherwise invisible (the row just keeps spinning like
853
+ // it's working). Flag the session so the sidebar shows a persistent
854
+ // "needs input" indicator on its row — works for the active session
855
+ // too, and survives alt-tab / window blur (unlike a toast).
856
+ if (sessionId) {
857
+ updateSessionState(sessionId, state => ({ ...state, needsInput: true }))
858
+ }
859
+ }
860
+ } else if (event.type === 'approval.request') {
861
+ // Dangerous-command / execute_code approval. The Python side is blocked
862
+ // in _await_gateway_decision() until approval.respond lands; without
863
+ // this the agent stalls until its 5-min timeout and the tool is BLOCKED.
864
+ // Park it per-session (like clarify) so a *background* profile's turn can
865
+ // raise it and wait — the sidebar flags "needs input" and the inline bar
866
+ // surfaces once the user focuses that chat.
867
+ setApprovalRequest({
868
+ command: typeof payload?.command === 'string' ? payload.command : '',
869
+ description: typeof payload?.description === 'string' ? payload.description : 'dangerous command',
870
+ sessionId: sessionId ?? null
871
+ })
872
+
873
+ if (sessionId) {
874
+ updateSessionState(sessionId, state => ({ ...state, needsInput: true }))
875
+ }
876
+ } else if (event.type === 'sudo.request') {
877
+ // Sudo password capture (tools/terminal_tool.py). Blocked on
878
+ // sudo.respond {request_id, password}.
879
+ const requestId = typeof payload?.request_id === 'string' ? payload.request_id : ''
880
+
881
+ if (requestId) {
882
+ setSudoRequest({ requestId, sessionId: sessionId ?? null })
883
+
884
+ if (sessionId) {
885
+ updateSessionState(sessionId, state => ({ ...state, needsInput: true }))
886
+ }
887
+ }
888
+ } else if (event.type === 'secret.request') {
889
+ // Skill credential capture (tools/skills_tool.py). Blocked on
890
+ // secret.respond {request_id, value}.
891
+ const requestId = typeof payload?.request_id === 'string' ? payload.request_id : ''
892
+
893
+ if (requestId) {
894
+ setSecretRequest({
895
+ requestId,
896
+ envVar: typeof payload?.env_var === 'string' ? payload.env_var : '',
897
+ prompt: typeof payload?.prompt === 'string' ? payload.prompt : '',
898
+ sessionId: sessionId ?? null
899
+ })
900
+
901
+ if (sessionId) {
902
+ updateSessionState(sessionId, state => ({ ...state, needsInput: true }))
903
+ }
904
+ }
905
+ } else if (event.type === 'error') {
906
+ const errorMessage = payload?.message || 'NasTech reported an error'
907
+ const looksLikeProviderSetup = isProviderSetupErrorMessage(errorMessage)
908
+
909
+ // A turn that errors out has also ended — drop any open blocking prompt
910
+ // for this session so an approval/sudo/secret overlay can't linger past
911
+ // the failed turn (same intent as the message.complete clear).
912
+ if (sessionId) {
913
+ clearAllPrompts(sessionId)
914
+ }
915
+
916
+ if (looksLikeProviderSetup) {
917
+ requestDesktopOnboarding(errorMessage)
918
+ } else if (isActiveEvent) {
919
+ notify({
920
+ kind: 'error',
921
+ title: 'NasTech error',
922
+ message: errorMessage
923
+ })
924
+ }
925
+
926
+ if (sessionId) {
927
+ flushQueuedDeltas(sessionId)
928
+ failAssistantMessage(sessionId, errorMessage)
929
+ }
930
+
931
+ if (isActiveEvent) {
932
+ setTurnStartedAt(null)
933
+ }
934
+ }
935
+ },
936
+ [
937
+ appendAssistantDelta,
938
+ appendReasoningDelta,
939
+ activeSessionIdRef,
940
+ completeAssistantMessage,
941
+ failAssistantMessage,
942
+ flushQueuedDeltas,
943
+ queryClient,
944
+ refreshNasTechConfig,
945
+ updateSessionState,
946
+ upsertToolCall
947
+ ]
948
+ )
949
+
950
+ return {
951
+ appendAssistantDelta,
952
+ appendReasoningDelta,
953
+ completeAssistantMessage,
954
+ handleGatewayEvent,
955
+ upsertToolCall
956
+ }
957
+ }