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,1030 @@
1
+ import type { AppendMessage, ThreadMessage } from '@assistant-ui/react'
2
+ import { type MutableRefObject, useCallback } from 'react'
3
+
4
+ import { getProfiles, transcribeAudio } from '@/nastech'
5
+ import { translateNow, type Translations, useI18n } from '@/i18n'
6
+ import { branchGroupForUser, type ChatMessage, chatMessageText, textPart } from '@/lib/chat-messages'
7
+ import {
8
+ attachmentDisplayText,
9
+ parseCommandDispatch,
10
+ parseSlashCommand,
11
+ pathLabel,
12
+ SLASH_COMMAND_RE
13
+ } from '@/lib/chat-runtime'
14
+ import {
15
+ type CommandsCatalogLike,
16
+ desktopSlashUnavailableMessage,
17
+ filterDesktopCommandsCatalog,
18
+ isDesktopSlashCommand,
19
+ isModelPickerCommand
20
+ } from '@/lib/desktop-slash-commands'
21
+ import { triggerHaptic } from '@/lib/haptics'
22
+ import { setMutableRef } from '@/lib/mutable-ref'
23
+ import { isProviderSetupErrorMessage } from '@/lib/provider-setup-errors'
24
+ import { setSessionYolo } from '@/lib/yolo-session'
25
+ import {
26
+ $composerAttachments,
27
+ addComposerAttachment,
28
+ clearComposerAttachments,
29
+ type ComposerAttachment,
30
+ terminalContextBlocksFromDraft
31
+ } from '@/store/composer'
32
+ import { clearNotifications, notify, notifyError } from '@/store/notifications'
33
+ import { requestDesktopOnboarding } from '@/store/onboarding'
34
+ import { $activeGatewayProfile, $newChatProfile, ensureGatewayProfile, normalizeProfileKey } from '@/store/profile'
35
+ import {
36
+ $busy,
37
+ $messages,
38
+ $yoloActive,
39
+ setAwaitingResponse,
40
+ setBusy,
41
+ setMessages,
42
+ setModelPickerOpen,
43
+ setSessions,
44
+ setYoloActive
45
+ } from '@/store/session'
46
+
47
+ import type {
48
+ ClientSessionState,
49
+ ImageAttachResponse,
50
+ SessionSteerResponse,
51
+ SessionTitleResponse,
52
+ SlashExecResponse
53
+ } from '../../types'
54
+
55
+ function blobToDataUrl(blob: Blob): Promise<string> {
56
+ return new Promise((resolve, reject) => {
57
+ const reader = new FileReader()
58
+
59
+ reader.addEventListener('load', () => {
60
+ if (typeof reader.result === 'string') {
61
+ resolve(reader.result)
62
+ } else {
63
+ reject(new Error(translateNow('desktop.audioReadFailed')))
64
+ }
65
+ })
66
+ reader.addEventListener('error', () => reject(reader.error || new Error(translateNow('desktop.audioReadFailed'))))
67
+ reader.readAsDataURL(blob)
68
+ })
69
+ }
70
+
71
+ function isProviderSetupError(error: unknown) {
72
+ const message = error instanceof Error ? error.message : String(error)
73
+
74
+ return isProviderSetupErrorMessage(message)
75
+ }
76
+
77
+ function inlineErrorMessage(error: unknown, fallback: string): string {
78
+ const raw = error instanceof Error ? error.message : typeof error === 'string' ? error : fallback
79
+
80
+ return (raw.match(/Error invoking remote method '[^']+': Error: (.+)$/)?.[1] ?? raw).replace(/^Error:\s*/, '').trim()
81
+ }
82
+
83
+ interface PromptActionsOptions {
84
+ activeSessionId: string | null
85
+ activeSessionIdRef: MutableRefObject<string | null>
86
+ busyRef: MutableRefObject<boolean>
87
+ branchCurrentSession: () => Promise<boolean>
88
+ createBackendSessionForSend: (preview?: string | null) => Promise<string | null>
89
+ handleSkinCommand: (arg: string) => string
90
+ refreshSessions: () => Promise<void>
91
+ requestGateway: <T>(method: string, params?: Record<string, unknown>) => Promise<T>
92
+ selectedStoredSessionIdRef: MutableRefObject<string | null>
93
+ startFreshSessionDraft: () => void
94
+ sttEnabled: boolean
95
+ updateSessionState: (
96
+ sessionId: string,
97
+ updater: (state: ClientSessionState) => ClientSessionState,
98
+ storedSessionId?: string | null
99
+ ) => ClientSessionState
100
+ }
101
+
102
+ interface SubmitTextOptions {
103
+ attachments?: ComposerAttachment[]
104
+ fromQueue?: boolean
105
+ }
106
+
107
+ function renderCommandsCatalog(catalog: CommandsCatalogLike, copy: Translations['desktop']): string {
108
+ const desktopCatalog = filterDesktopCommandsCatalog(catalog)
109
+
110
+ const sections = desktopCatalog.categories?.length
111
+ ? desktopCatalog.categories
112
+ : [{ name: copy.desktopCommands, pairs: desktopCatalog.pairs ?? [] }]
113
+
114
+ const body = sections
115
+ .filter(section => section.pairs.length > 0)
116
+ .map(section => {
117
+ const rows = section.pairs.map(([cmd, desc]) => `${cmd.padEnd(18)} ${desc}`)
118
+
119
+ return [`${section.name}:`, ...rows].join('\n')
120
+ })
121
+ .join('\n\n')
122
+
123
+ const tail = [
124
+ desktopCatalog.skill_count ? copy.skillCommandsAvailable(desktopCatalog.skill_count) : '',
125
+ desktopCatalog.warning ? copy.warningLine(desktopCatalog.warning) : ''
126
+ ]
127
+ .filter(Boolean)
128
+ .join('\n')
129
+
130
+ return [body || 'No desktop commands available.', tail].filter(Boolean).join('\n\n')
131
+ }
132
+
133
+ function slashStatusText(command: string, output: string): string {
134
+ return [`slash:${command}`, output.trim()].filter(Boolean).join('\n')
135
+ }
136
+
137
+ function appendText(message: AppendMessage): string {
138
+ return message.content
139
+ .map(part => ('text' in part ? part.text : ''))
140
+ .join('')
141
+ .trim()
142
+ }
143
+
144
+ function visibleUserOrdinal(messages: readonly ChatMessage[], end: number): number {
145
+ return messages.slice(0, end).filter(m => m.role === 'user' && !m.hidden).length
146
+ }
147
+
148
+ export function usePromptActions({
149
+ activeSessionId,
150
+ activeSessionIdRef,
151
+ busyRef,
152
+ branchCurrentSession,
153
+ createBackendSessionForSend,
154
+ handleSkinCommand,
155
+ refreshSessions,
156
+ requestGateway,
157
+ selectedStoredSessionIdRef,
158
+ startFreshSessionDraft,
159
+ sttEnabled,
160
+ updateSessionState
161
+ }: PromptActionsOptions) {
162
+ const { t } = useI18n()
163
+ const copy = t.desktop
164
+
165
+ const appendSessionTextMessage = useCallback(
166
+ (sessionId: string, role: ChatMessage['role'], text: string) => {
167
+ const body = text.trim()
168
+
169
+ if (!body) {
170
+ return
171
+ }
172
+
173
+ updateSessionState(
174
+ sessionId,
175
+ state => ({
176
+ ...state,
177
+ messages: [
178
+ ...state.messages,
179
+ {
180
+ id: `${role}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
181
+ role,
182
+ parts: [textPart(body)]
183
+ }
184
+ ]
185
+ }),
186
+ selectedStoredSessionIdRef.current
187
+ )
188
+ },
189
+ [selectedStoredSessionIdRef, updateSessionState]
190
+ )
191
+
192
+ const syncImageAttachmentsForSubmit = useCallback(
193
+ async (
194
+ sessionId: string,
195
+ attachments: ComposerAttachment[],
196
+ options: { updateComposerAttachments?: boolean } = {}
197
+ ) => {
198
+ const updateComposerAttachments = options.updateComposerAttachments ?? true
199
+ const images = attachments.filter(attachment => attachment.kind === 'image' && attachment.path)
200
+
201
+ for (const attachment of images) {
202
+ if (attachment.attachedSessionId === sessionId) {
203
+ continue
204
+ }
205
+
206
+ const result = await requestGateway<ImageAttachResponse>('image.attach', {
207
+ session_id: sessionId,
208
+ path: attachment.path
209
+ })
210
+
211
+ if (!result.attached) {
212
+ const label = attachment.label || (attachment.path ? pathLabel(attachment.path) : 'image')
213
+ throw new Error(result.message || `Could not attach ${label}`)
214
+ }
215
+
216
+ const attachedPath = result.path || attachment.path
217
+
218
+ if (updateComposerAttachments) {
219
+ addComposerAttachment({
220
+ ...attachment,
221
+ id: attachment.id,
222
+ label: attachedPath ? pathLabel(attachedPath) : attachment.label,
223
+ path: attachedPath,
224
+ attachedSessionId: sessionId
225
+ })
226
+ }
227
+ }
228
+ },
229
+ [requestGateway]
230
+ )
231
+
232
+ const submitPromptText = useCallback(
233
+ async (rawText: string, options?: SubmitTextOptions) => {
234
+ const visibleText = rawText.trim()
235
+ const usingComposerAttachments = !options?.attachments
236
+ const attachments = options?.attachments ?? $composerAttachments.get()
237
+
238
+ const contextRefs = attachments
239
+ .map(a => a.refText)
240
+ .filter(Boolean)
241
+ .join('\n')
242
+
243
+ const terminalContextBlocks = terminalContextBlocksFromDraft(rawText).join('\n\n')
244
+ const hasImage = attachments.some(a => a.kind === 'image')
245
+ const attachmentRefs = attachments.map(attachmentDisplayText).filter((r): r is string => Boolean(r))
246
+
247
+ const text =
248
+ [contextRefs, terminalContextBlocks, visibleText].filter(Boolean).join('\n\n') ||
249
+ (hasImage ? 'What do you see in this image?' : '')
250
+
251
+ // Queue drains fire on the busy→false settle edge, where busyRef (synced
252
+ // from $busy by a separate effect) may still read true — honoring it would
253
+ // bounce the drained send. The drain lock serializes them; the user path
254
+ // keeps the guard so a stray Enter mid-turn can't double-submit.
255
+ if (!text || (!options?.fromQueue && busyRef.current)) {
256
+ return false
257
+ }
258
+
259
+ const optimisticId = `user-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
260
+
261
+ const userMessage: ChatMessage = {
262
+ id: optimisticId,
263
+ role: 'user',
264
+ parts: [textPart(visibleText || (attachmentRefs.length ? '' : attachments.map(a => a.label).join(', ')))],
265
+ attachmentRefs
266
+ }
267
+
268
+ const releaseBusy = () => {
269
+ setMutableRef(busyRef, false)
270
+ setBusy(false)
271
+ setAwaitingResponse(false)
272
+ }
273
+
274
+ // Idempotent optimistic insert — re-running with the resolved sessionId
275
+ // after createBackendSessionForSend just overwrites with the same id.
276
+ const seedOptimistic = (sid: string) =>
277
+ updateSessionState(
278
+ sid,
279
+ state => ({
280
+ ...state,
281
+ messages: state.messages.some(m => m.id === optimisticId)
282
+ ? state.messages
283
+ : [...state.messages, userMessage],
284
+ busy: true,
285
+ awaitingResponse: true,
286
+ pendingBranchGroup: null,
287
+ sawAssistantPayload: false,
288
+ // Fresh submit = new turn — clear any leftover interrupt flag, else
289
+ // mutateStream/completeAssistantMessage drop every delta of this turn
290
+ // (what made drained-after-interrupt sends go silent).
291
+ interrupted: false
292
+ }),
293
+ selectedStoredSessionIdRef.current
294
+ )
295
+
296
+ const dropOptimistic = (sid: null | string) => {
297
+ if (!sid) {
298
+ setMessages(current => current.filter(m => m.id !== optimisticId))
299
+
300
+ return
301
+ }
302
+
303
+ updateSessionState(
304
+ sid,
305
+ state => ({
306
+ ...state,
307
+ messages: state.messages.filter(m => m.id !== optimisticId),
308
+ busy: false,
309
+ awaitingResponse: false,
310
+ pendingBranchGroup: null
311
+ }),
312
+ selectedStoredSessionIdRef.current
313
+ )
314
+ }
315
+
316
+ setMutableRef(busyRef, true)
317
+ setBusy(true)
318
+ setAwaitingResponse(true)
319
+ clearNotifications()
320
+
321
+ let sessionId: null | string = activeSessionId
322
+
323
+ if (sessionId) {
324
+ seedOptimistic(sessionId)
325
+ } else {
326
+ setMessages(current => [...current, userMessage])
327
+ }
328
+
329
+ if (!sessionId) {
330
+ try {
331
+ sessionId = await createBackendSessionForSend(visibleText)
332
+ } catch (err) {
333
+ dropOptimistic(null)
334
+ releaseBusy()
335
+ notifyError(err, copy.sessionUnavailable)
336
+
337
+ return false
338
+ }
339
+
340
+ if (!sessionId) {
341
+ dropOptimistic(null)
342
+ releaseBusy()
343
+ notify({ kind: 'error', title: copy.sessionUnavailable, message: copy.createSessionFailed })
344
+
345
+ return false
346
+ }
347
+
348
+ seedOptimistic(sessionId)
349
+ }
350
+
351
+ try {
352
+ await syncImageAttachmentsForSubmit(sessionId, attachments, {
353
+ updateComposerAttachments: usingComposerAttachments
354
+ })
355
+ await requestGateway('prompt.submit', { session_id: sessionId, text })
356
+
357
+ if (usingComposerAttachments) {
358
+ clearComposerAttachments()
359
+ }
360
+
361
+ return true
362
+ } catch (err) {
363
+ const message = inlineErrorMessage(err, copy.promptFailed)
364
+
365
+ releaseBusy()
366
+ updateSessionState(sessionId, state => ({
367
+ ...state,
368
+ messages: [
369
+ ...state.messages,
370
+ {
371
+ id: `assistant-error-${Date.now()}`,
372
+ role: 'assistant',
373
+ parts: [],
374
+ error: message || copy.promptFailed,
375
+ branchGroupId: state.pendingBranchGroup ?? undefined
376
+ }
377
+ ],
378
+ busy: false,
379
+ awaitingResponse: false,
380
+ pendingBranchGroup: null,
381
+ sawAssistantPayload: true
382
+ }))
383
+
384
+ if (isProviderSetupError(err)) {
385
+ requestDesktopOnboarding(copy.providerCredentialRequired)
386
+
387
+ return false
388
+ }
389
+
390
+ notifyError(err, copy.promptFailed)
391
+
392
+ return false
393
+ }
394
+ },
395
+ [
396
+ activeSessionId,
397
+ busyRef,
398
+ copy,
399
+ createBackendSessionForSend,
400
+ requestGateway,
401
+ selectedStoredSessionIdRef,
402
+ syncImageAttachmentsForSubmit,
403
+ updateSessionState
404
+ ]
405
+ )
406
+
407
+ const executeSlashCommand = useCallback(
408
+ async (rawCommand: string, options?: { sessionId?: string; recordInput?: boolean }) => {
409
+ const runSlash = async (commandText: string, sessionHint?: string, recordInput = true): Promise<void> => {
410
+ const command = commandText.trim()
411
+ const { name, arg } = parseSlashCommand(command)
412
+ const normalizedName = name.toLowerCase()
413
+
414
+ if (!name) {
415
+ const sessionId = sessionHint || activeSessionIdRef.current || (await createBackendSessionForSend())
416
+
417
+ if (sessionId) {
418
+ appendSessionTextMessage(sessionId, 'system', copy.emptySlashCommand)
419
+ }
420
+
421
+ return
422
+ }
423
+
424
+ if (normalizedName === 'new' || normalizedName === 'reset') {
425
+ startFreshSessionDraft()
426
+
427
+ return
428
+ }
429
+
430
+ if (normalizedName === 'branch' || normalizedName === 'fork') {
431
+ await branchCurrentSession()
432
+
433
+ return
434
+ }
435
+
436
+ // /yolo maps to the status-bar YOLO control — a per-session approval
437
+ // bypass, same scope as the TUI's Shift+Tab. With no session yet we arm
438
+ // it locally; the session-create path applies it on the first message.
439
+ if (normalizedName === 'yolo') {
440
+ const sid = sessionHint || activeSessionIdRef.current
441
+ const next = !$yoloActive.get()
442
+
443
+ if (!sid) {
444
+ setYoloActive(next)
445
+ notify({ kind: 'success', message: next ? copy.yoloArmed : copy.yoloOff })
446
+
447
+ return
448
+ }
449
+
450
+ try {
451
+ const active = await setSessionYolo(requestGateway, sid, next)
452
+ appendSessionTextMessage(sid, 'system', copy.yoloSystem(active))
453
+ } catch {
454
+ notify({ kind: 'error', title: copy.yoloTitle, message: copy.yoloToggleFailed })
455
+ }
456
+
457
+ return
458
+ }
459
+
460
+ // /model opens the desktop model picker overlay — the same full
461
+ // provider+model picker reachable from the status-bar model button —
462
+ // instead of the headless prompt_toolkit modal the slash worker can't
463
+ // render. With explicit args (`/model <name> [--provider ...]`) run the
464
+ // switch directly through slash.exec so power users can still type it.
465
+ if (isModelPickerCommand(`/${normalizedName}`)) {
466
+ if (!arg.trim()) {
467
+ setModelPickerOpen(true)
468
+
469
+ return
470
+ }
471
+
472
+ const sid = sessionHint || activeSessionIdRef.current || (await createBackendSessionForSend())
473
+
474
+ if (!sid) {
475
+ notify({ kind: 'error', title: 'Session unavailable', message: 'Could not create a new session' })
476
+
477
+ return
478
+ }
479
+
480
+ try {
481
+ const result = await requestGateway<SlashExecResponse>('slash.exec', {
482
+ session_id: sid,
483
+ command: command.replace(/^\/+/, '')
484
+ })
485
+
486
+ const body = result?.output || `/${name}: model switched`
487
+ appendSessionTextMessage(
488
+ sid,
489
+ 'system',
490
+ recordInput ? slashStatusText(command, body) : body
491
+ )
492
+ } catch (err) {
493
+ appendSessionTextMessage(
494
+ sid,
495
+ 'system',
496
+ `error: ${err instanceof Error ? err.message : String(err)}`
497
+ )
498
+ }
499
+
500
+ return
501
+ }
502
+
503
+ if (normalizedName === 'skin' && !sessionHint && !activeSessionIdRef.current) {
504
+ notify({ kind: 'success', message: handleSkinCommand(arg) })
505
+
506
+ return
507
+ }
508
+
509
+ // /profile selects which profile new chats open in — no app relaunch.
510
+ // A profile is per-session now, so an existing thread can't change its
511
+ // profile mid-stream; `/profile <name>` instead points the next new chat
512
+ // (and the current empty draft) at that profile's backend.
513
+ if (normalizedName === 'profile') {
514
+ const target = arg.trim()
515
+ const current = normalizeProfileKey($activeGatewayProfile.get())
516
+
517
+ if (!target) {
518
+ notify({
519
+ kind: 'success',
520
+ message: copy.profileStatus(current)
521
+ })
522
+
523
+ return
524
+ }
525
+
526
+ try {
527
+ const { profiles } = await getProfiles()
528
+ const match = profiles.find(profile => profile.name === target)
529
+
530
+ if (!match) {
531
+ notify({
532
+ kind: 'error',
533
+ title: copy.unknownProfile,
534
+ message: copy.noProfileNamed(target, profiles.map(profile => profile.name).join(', '))
535
+ })
536
+
537
+ return
538
+ }
539
+
540
+ const key = normalizeProfileKey(match.name)
541
+
542
+ $newChatProfile.set(key)
543
+ // Swap the live gateway now so an empty draft sends into this
544
+ // profile immediately; an existing thread keeps its own profile.
545
+ await ensureGatewayProfile(key)
546
+ notify({ kind: 'success', message: copy.newChatsProfile(match.name) })
547
+ } catch (err) {
548
+ notifyError(err, copy.setProfileFailed)
549
+ }
550
+
551
+ return
552
+ }
553
+
554
+ const sessionId = sessionHint || activeSessionIdRef.current || (await createBackendSessionForSend())
555
+
556
+ if (!sessionId) {
557
+ notify({
558
+ kind: 'error',
559
+ title: copy.sessionUnavailable,
560
+ message: copy.createSessionFailed
561
+ })
562
+
563
+ return
564
+ }
565
+
566
+ const renderSlashOutput = (text: string) =>
567
+ appendSessionTextMessage(sessionId, 'system', recordInput ? slashStatusText(command, text) : text)
568
+
569
+ // /title <name> renames the session. Route through the gateway's
570
+ // `session.title` RPC — the same path the TUI uses — NOT the REST
571
+ // renameSession endpoint and NOT the slash worker.
572
+ //
573
+ // Why not the slash worker: it's a separate NasTechCLI subprocess whose
574
+ // SQLite write to the shared state.db can silently fail (notably on
575
+ // Windows), and it never refreshes the sidebar.
576
+ //
577
+ // Why not REST renameSession: `sessionId` here is the *runtime* session
578
+ // id returned by session.create — it is NOT the stored DB `sessions.id`,
579
+ // and session.create deliberately does not persist a DB row until the
580
+ // first turn. The REST PATCH endpoint resolves against the sessions
581
+ // table, so a runtime id (or a brand-new, not-yet-persisted session)
582
+ // 404s with "Session not found" on every platform. See #38508 / #38576.
583
+ //
584
+ // session.title maps the runtime id to the in-memory session, writes
585
+ // through the gateway's own DB connection, and QUEUES the title
586
+ // (`pending: true`) when the row isn't persisted yet — so it works for a
587
+ // fresh chat too. refreshSessions() then pulls the authoritative title
588
+ // back into the sidebar. A bare `/title` (no arg) still falls through to
589
+ // the worker to display the current title.
590
+ if (normalizedName === 'title' && arg) {
591
+ try {
592
+ const result = await requestGateway<SessionTitleResponse>('session.title', {
593
+ session_id: sessionId,
594
+ title: arg
595
+ })
596
+
597
+ const finalTitle = (result?.title || arg).trim()
598
+ const queued = result?.pending === true
599
+
600
+ setSessions(prev => prev.map(s => (s.id === sessionId ? { ...s, title: finalTitle || null } : s)))
601
+ await refreshSessions().catch(() => undefined)
602
+ renderSlashOutput(
603
+ finalTitle
604
+ ? `Session title set: ${finalTitle}${queued ? ' (queued while session initializes)' : ''}`
605
+ : 'Session title cleared.'
606
+ )
607
+ } catch (err) {
608
+ renderSlashOutput(`error: ${err instanceof Error ? err.message : String(err)}`)
609
+ }
610
+
611
+ return
612
+ }
613
+
614
+ if (normalizedName === 'skin') {
615
+ renderSlashOutput(handleSkinCommand(arg))
616
+
617
+ return
618
+ }
619
+
620
+ if (name === 'help' || name === 'commands') {
621
+ try {
622
+ const catalog = await requestGateway<CommandsCatalogLike>('commands.catalog', { session_id: sessionId })
623
+
624
+ renderSlashOutput(renderCommandsCatalog(catalog, copy))
625
+ } catch (err) {
626
+ renderSlashOutput(`error: ${err instanceof Error ? err.message : String(err)}`)
627
+ }
628
+
629
+ return
630
+ }
631
+
632
+ if (!isDesktopSlashCommand(name)) {
633
+ renderSlashOutput(desktopSlashUnavailableMessage(name) || `/${name} is not available in the desktop app.`)
634
+
635
+ return
636
+ }
637
+
638
+ try {
639
+ const result = await requestGateway<SlashExecResponse>('slash.exec', {
640
+ session_id: sessionId,
641
+ command: command.replace(/^\/+/, '')
642
+ })
643
+
644
+ const body = result?.output || `/${name}: no output`
645
+ renderSlashOutput(result?.warning ? `warning: ${result.warning}\n${body}` : body)
646
+
647
+ return
648
+ } catch {
649
+ // Fall back to command.dispatch for skill/send/alias directives.
650
+ }
651
+
652
+ try {
653
+ const dispatch = parseCommandDispatch(
654
+ await requestGateway<unknown>('command.dispatch', {
655
+ session_id: sessionId,
656
+ name,
657
+ arg
658
+ })
659
+ )
660
+
661
+ if (!dispatch) {
662
+ renderSlashOutput('error: invalid response: command.dispatch')
663
+
664
+ return
665
+ }
666
+
667
+ if (dispatch.type === 'exec' || dispatch.type === 'plugin') {
668
+ renderSlashOutput(dispatch.output ?? '(no output)')
669
+
670
+ return
671
+ }
672
+
673
+ if (dispatch.type === 'alias') {
674
+ await runSlash(`/${dispatch.target}${arg ? ` ${arg}` : ''}`, sessionId, false)
675
+
676
+ return
677
+ }
678
+
679
+ const message = ('message' in dispatch ? dispatch.message : '')?.trim() ?? ''
680
+
681
+ if (!message) {
682
+ renderSlashOutput(
683
+ `/${name}: ${dispatch.type === 'skill' ? 'skill payload missing message' : 'empty message'}`
684
+ )
685
+
686
+ return
687
+ }
688
+
689
+ if (dispatch.type === 'skill') {
690
+ renderSlashOutput(`⚡ loading skill: ${dispatch.name}`)
691
+ }
692
+
693
+ if (busyRef.current) {
694
+ renderSlashOutput('session busy — /interrupt the current turn before sending this command')
695
+
696
+ return
697
+ }
698
+
699
+ await submitPromptText(message)
700
+ } catch (err) {
701
+ renderSlashOutput(`error: ${err instanceof Error ? err.message : String(err)}`)
702
+ }
703
+ }
704
+
705
+ await runSlash(rawCommand, options?.sessionId, options?.recordInput ?? true)
706
+ },
707
+ [
708
+ activeSessionIdRef,
709
+ appendSessionTextMessage,
710
+ branchCurrentSession,
711
+ busyRef,
712
+ copy,
713
+ createBackendSessionForSend,
714
+ handleSkinCommand,
715
+ refreshSessions,
716
+ requestGateway,
717
+ startFreshSessionDraft,
718
+ submitPromptText
719
+ ]
720
+ )
721
+
722
+ const submitText = useCallback(
723
+ async (rawText: string, options?: SubmitTextOptions) => {
724
+ const visibleText = rawText.trim()
725
+ const attachments = options?.attachments ?? $composerAttachments.get()
726
+
727
+ if (!attachments.length && SLASH_COMMAND_RE.test(visibleText)) {
728
+ triggerHaptic('selection')
729
+ await executeSlashCommand(visibleText)
730
+
731
+ return true
732
+ }
733
+
734
+ return await submitPromptText(rawText, options)
735
+ },
736
+ [executeSlashCommand, submitPromptText]
737
+ )
738
+
739
+ const transcribeVoiceAudio = useCallback(
740
+ async (audio: Blob) => {
741
+ if (!sttEnabled) {
742
+ throw new Error(copy.sttDisabled)
743
+ }
744
+
745
+ const dataUrl = await blobToDataUrl(audio)
746
+ const result = await transcribeAudio(dataUrl, audio.type)
747
+
748
+ return result.transcript
749
+ },
750
+ [copy.sttDisabled, sttEnabled]
751
+ )
752
+
753
+ const cancelRun = useCallback(async () => {
754
+ const sessionId = activeSessionId || activeSessionIdRef.current
755
+
756
+ setAwaitingResponse(false)
757
+
758
+ // Interrupting keeps whatever was already generated and just
759
+ // stops — no "[interrupted]" marker. A pending/streaming message with no
760
+ // body text is dropped entirely so we never leave an empty bubble behind.
761
+ const finalizeMessages = (messages: ChatMessage[], streamId?: string | null) =>
762
+ messages
763
+ .filter(
764
+ message =>
765
+ !((message.pending || message.id === streamId) && !chatMessageText(message).trim())
766
+ )
767
+ .map(message =>
768
+ message.pending || message.id === streamId ? { ...message, pending: false } : message
769
+ )
770
+
771
+ if (!sessionId) {
772
+ setMutableRef(busyRef, false)
773
+ setBusy(false)
774
+ setMessages(finalizeMessages($messages.get()))
775
+
776
+ return
777
+ }
778
+
779
+ updateSessionState(sessionId, state => {
780
+ const streamId = state.streamId
781
+
782
+ const messages = finalizeMessages(state.messages, streamId)
783
+
784
+ return {
785
+ ...state,
786
+ messages,
787
+ busy: true,
788
+ awaitingResponse: false,
789
+ streamId: null,
790
+ pendingBranchGroup: null,
791
+ interrupted: true
792
+ }
793
+ })
794
+
795
+ try {
796
+ await requestGateway('session.interrupt', { session_id: sessionId })
797
+ } catch (err) {
798
+ setMutableRef(busyRef, false)
799
+ setBusy(false)
800
+ notifyError(err, copy.stopFailed)
801
+ }
802
+ }, [activeSessionId, activeSessionIdRef, busyRef, copy.stopFailed, requestGateway, updateSessionState])
803
+
804
+ // Steer = nudge the live turn without interrupting: the gateway appends the
805
+ // text to the next tool result so the model reads it on its next iteration
806
+ // (desktop parity with `/steer`). Returns false on reject (no live tool
807
+ // window) so the caller can fall back to queueing the words for the next turn.
808
+ const steerPrompt = useCallback(
809
+ async (rawText: string): Promise<boolean> => {
810
+ const text = rawText.trim()
811
+ const sessionId = activeSessionId || activeSessionIdRef.current
812
+
813
+ if (!text || !sessionId) {
814
+ return false
815
+ }
816
+
817
+ try {
818
+ const result = await requestGateway<SessionSteerResponse>('session.steer', { session_id: sessionId, text })
819
+
820
+ if (result?.status === 'queued') {
821
+ triggerHaptic('submit')
822
+ // Inline note (not a toast) so the nudge lives in the transcript next
823
+ // to the turn it steered. The `steer:` prefix is rendered as a codicon
824
+ // row by SystemMessage (see STEER_NOTE_RE), same style as slash output.
825
+ appendSessionTextMessage(sessionId, 'system', `steer:${text}`)
826
+
827
+ return true
828
+ }
829
+ } catch {
830
+ // Swallow — caller queues the text so nothing is lost.
831
+ }
832
+
833
+ return false
834
+ },
835
+ [activeSessionId, activeSessionIdRef, appendSessionTextMessage, requestGateway]
836
+ )
837
+
838
+ const reloadFromMessage = useCallback(
839
+ async (parentId: string | null) => {
840
+ if (!activeSessionId || $busy.get()) {
841
+ return
842
+ }
843
+
844
+ const messages = $messages.get()
845
+ const parentIndex = parentId ? messages.findIndex(message => message.id === parentId) : messages.length - 1
846
+
847
+ const userIndex =
848
+ parentIndex >= 0
849
+ ? [...messages.slice(0, parentIndex + 1)].reverse().findIndex(message => message.role === 'user')
850
+ : -1
851
+
852
+ if (userIndex < 0) {
853
+ return
854
+ }
855
+
856
+ const absoluteUserIndex = parentIndex - userIndex
857
+ const userMessage = messages[absoluteUserIndex]
858
+ const userText = userMessage ? chatMessageText(userMessage).trim() : ''
859
+
860
+ if (!userText) {
861
+ return
862
+ }
863
+
864
+ const targetAssistant =
865
+ parentId && messages[parentIndex]?.role === 'assistant'
866
+ ? messages[parentIndex]
867
+ : messages.slice(absoluteUserIndex + 1).find(message => message.role === 'assistant')
868
+
869
+ const branchGroupId = targetAssistant?.branchGroupId ?? branchGroupForUser(userMessage)
870
+ const truncateBeforeUserOrdinal = visibleUserOrdinal(messages, absoluteUserIndex)
871
+
872
+ clearNotifications()
873
+ updateSessionState(activeSessionId, state => {
874
+ const nextUserIndex = state.messages.findIndex(
875
+ (message, index) => index > absoluteUserIndex && message.role === 'user'
876
+ )
877
+
878
+ const end = nextUserIndex < 0 ? state.messages.length : nextUserIndex
879
+
880
+ return {
881
+ ...state,
882
+ busy: true,
883
+ awaitingResponse: true,
884
+ pendingBranchGroup: branchGroupId,
885
+ sawAssistantPayload: false,
886
+ interrupted: false,
887
+ messages: [
888
+ ...state.messages.slice(0, absoluteUserIndex + 1),
889
+ ...state.messages
890
+ .slice(absoluteUserIndex + 1, end)
891
+ .map(message => (message.role === 'assistant' ? { ...message, branchGroupId, hidden: true } : message))
892
+ ]
893
+ }
894
+ })
895
+
896
+ try {
897
+ await requestGateway('prompt.submit', {
898
+ session_id: activeSessionId,
899
+ text: userText,
900
+ truncate_before_user_ordinal: truncateBeforeUserOrdinal
901
+ })
902
+ } catch (err) {
903
+ updateSessionState(activeSessionId, state => ({
904
+ ...state,
905
+ busy: false,
906
+ awaitingResponse: false
907
+ }))
908
+ notifyError(err, copy.regenerateFailed)
909
+ }
910
+ },
911
+ [activeSessionId, copy.regenerateFailed, requestGateway, updateSessionState]
912
+ )
913
+
914
+ const editMessage = useCallback(
915
+ async (edited: AppendMessage) => {
916
+ const sessionId = activeSessionId || activeSessionIdRef.current
917
+ const sourceId = edited.sourceId || edited.parentId
918
+ const text = appendText(edited)
919
+
920
+ if (!sessionId || !sourceId || !text || edited.role !== 'user' || $busy.get()) {
921
+ return
922
+ }
923
+
924
+ const messages = $messages.get()
925
+ const sourceIndex = messages.findIndex(m => m.id === sourceId)
926
+ const source = messages[sourceIndex]
927
+
928
+ if (!source || source.role !== 'user' || chatMessageText(source).trim() === text) {
929
+ return
930
+ }
931
+
932
+ // Failed turn: optimistic user msg never reached the gateway, so truncating
933
+ // by ordinal would 422. Submit as a plain resend instead.
934
+ const nextMessage = messages[sourceIndex + 1]
935
+ const isFailedTurn = nextMessage?.role === 'assistant' && Boolean(nextMessage.error)
936
+ const editedMessage: ChatMessage = { ...source, parts: [textPart(text)] }
937
+
938
+ clearNotifications()
939
+ setMutableRef(busyRef, true)
940
+ setBusy(true)
941
+ setAwaitingResponse(true)
942
+ updateSessionState(sessionId, state => ({
943
+ ...state,
944
+ busy: true,
945
+ awaitingResponse: true,
946
+ pendingBranchGroup: null,
947
+ sawAssistantPayload: false,
948
+ interrupted: false,
949
+ messages: [...state.messages.slice(0, sourceIndex), editedMessage]
950
+ }))
951
+
952
+ const submit = (truncateOrdinal?: number) =>
953
+ requestGateway('prompt.submit', {
954
+ session_id: sessionId,
955
+ text,
956
+ ...(truncateOrdinal !== undefined && { truncate_before_user_ordinal: truncateOrdinal })
957
+ })
958
+
959
+ const isStaleTargetError = (err: unknown) =>
960
+ /no longer in session history|not in session history/i.test(err instanceof Error ? err.message : String(err))
961
+
962
+ try {
963
+ await submit(isFailedTurn ? undefined : visibleUserOrdinal(messages, sourceIndex))
964
+ } catch (err) {
965
+ let surfaced = err
966
+
967
+ if (!isFailedTurn && isStaleTargetError(err)) {
968
+ try {
969
+ await submit()
970
+
971
+ return
972
+ } catch (retryErr) {
973
+ surfaced = retryErr
974
+ }
975
+ }
976
+
977
+ setMutableRef(busyRef, false)
978
+ setBusy(false)
979
+ setAwaitingResponse(false)
980
+ updateSessionState(sessionId, state => ({ ...state, busy: false, awaitingResponse: false }))
981
+ notifyError(surfaced, copy.editFailed)
982
+ }
983
+ },
984
+ [activeSessionId, activeSessionIdRef, busyRef, copy.editFailed, requestGateway, updateSessionState]
985
+ )
986
+
987
+ const handleThreadMessagesChange = useCallback(
988
+ (nextMessages: readonly ThreadMessage[]) => {
989
+ const visibleIds = new Set(nextMessages.map(m => m.id))
990
+ const sessionId = activeSessionIdRef.current
991
+
992
+ if (!sessionId) {
993
+ return
994
+ }
995
+
996
+ updateSessionState(sessionId, state => {
997
+ let changed = false
998
+
999
+ const messages = state.messages.map(message => {
1000
+ if (message.role !== 'assistant' || !message.branchGroupId) {
1001
+ return message
1002
+ }
1003
+
1004
+ const hidden = !visibleIds.has(message.id)
1005
+
1006
+ if (message.hidden === hidden) {
1007
+ return message
1008
+ }
1009
+
1010
+ changed = true
1011
+
1012
+ return { ...message, hidden }
1013
+ })
1014
+
1015
+ return changed ? { ...state, messages } : state
1016
+ })
1017
+ },
1018
+ [activeSessionIdRef, updateSessionState]
1019
+ )
1020
+
1021
+ return {
1022
+ cancelRun,
1023
+ editMessage,
1024
+ handleThreadMessagesChange,
1025
+ reloadFromMessage,
1026
+ steerPrompt,
1027
+ submitText,
1028
+ transcribeVoiceAudio
1029
+ }
1030
+ }