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,559 @@
1
+ import { useCallback, useEffect, useMemo, useState } from 'react'
2
+
3
+ import { Button } from '@/components/ui/button'
4
+ import { Input } from '@/components/ui/input'
5
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
6
+ import {
7
+ getAuxiliaryModels,
8
+ getGlobalModelInfo,
9
+ getGlobalModelOptions,
10
+ getRecommendedDefaultModel,
11
+ setEnvVar,
12
+ setModelAssignment
13
+ } from '@/nastech'
14
+ import type { AuxiliaryModelsResponse, ModelOptionProvider, StaleAuxAssignment } from '@/nastech'
15
+ import { useI18n } from '@/i18n'
16
+ import { AlertTriangle, Cpu, Loader2 } from '@/lib/icons'
17
+ import { cn } from '@/lib/utils'
18
+ import { startManualProviderOAuth } from '@/store/onboarding'
19
+
20
+ import { CONTROL_TEXT } from './constants'
21
+ import { ListRow, LoadingState, Pill, SectionHeading } from './primitives'
22
+
23
+ // A provider row is "ready" to pick a model from when it reports models. The
24
+ // backend now surfaces the full `NASTECH model` universe (every canonical
25
+ // provider), so unconfigured providers come back with `authenticated:false`
26
+ // and an empty `models` list — those need a setup step before a model exists.
27
+ function isProviderReady(p?: ModelOptionProvider): boolean {
28
+ return !!p && (p.authenticated !== false || (p.models?.length ?? 0) > 0)
29
+ }
30
+
31
+ // Mirrors `_AUX_TASK_SLOTS` in nastech_cli/web_server.py. Friendly labels and
32
+ // hints make the assignments readable; raw task keys (vision, mcp, …) are
33
+ // opaque to most users.
34
+ interface AuxTaskMeta {
35
+ key: string
36
+ }
37
+
38
+ const AUX_TASKS: readonly AuxTaskMeta[] = [
39
+ { key: 'vision' },
40
+ { key: 'web_extract' },
41
+ { key: 'compression' },
42
+ { key: 'skills_hub' },
43
+ { key: 'approval' },
44
+ { key: 'mcp' },
45
+ { key: 'title_generation' },
46
+ { key: 'curator' }
47
+ ]
48
+
49
+ const NO_PROVIDERS: readonly ModelOptionProvider[] = [{ name: '—', slug: '', models: [] }]
50
+
51
+ interface StaleAuxWarningProps {
52
+ applying: boolean
53
+ onReset: () => void
54
+ slots: readonly StaleAuxAssignment[]
55
+ taskLabel: (key: string) => string
56
+ }
57
+
58
+ // Shared notice: auxiliary tasks still pinned to a provider that isn't the
59
+ // current main. Surfaces the silent credit-burn path (e.g. aux pinned to a
60
+ // $0-balance provider after switching main away from it) and offers the
61
+ // existing one-click reset rather than auto-clearing legitimate pins.
62
+ function StaleAuxWarning({ applying, onReset, slots, taskLabel }: StaleAuxWarningProps) {
63
+ if (!slots.length) {
64
+ return null
65
+ }
66
+
67
+ const provider = slots[0].provider
68
+ const allSameProvider = slots.every(slot => slot.provider === provider)
69
+ const names = slots.map(slot => taskLabel(slot.task)).join(', ')
70
+
71
+ return (
72
+ <div className="flex flex-wrap items-center gap-2 rounded-md border border-amber-500/40 bg-amber-500/10 px-3 py-2 text-xs text-amber-200">
73
+ <AlertTriangle className="size-3.5 shrink-0" />
74
+ <span className="grow">
75
+ {slots.length} auxiliary task{slots.length === 1 ? '' : 's'} ({names}) still run on{' '}
76
+ <span className="font-mono">{allSameProvider ? provider : 'other providers'}</span>, not your main model.
77
+ </span>
78
+ <Button disabled={applying} onClick={onReset} size="sm" variant="textStrong">
79
+ Reset all to main
80
+ </Button>
81
+ </div>
82
+ )
83
+ }
84
+
85
+ interface ModelSettingsProps {
86
+ /** Notified after the main model is applied, so live UI stores can sync. */
87
+ onMainModelChanged?: (provider: string, model: string) => void
88
+ }
89
+
90
+ export function ModelSettings({ onMainModelChanged }: ModelSettingsProps) {
91
+ const { t } = useI18n()
92
+ const m = t.settings.model
93
+ const [loading, setLoading] = useState(true)
94
+ const [error, setError] = useState('')
95
+ const [mainModel, setMainModel] = useState<{ model: string; provider: string } | null>(null)
96
+ const [providers, setProviders] = useState<ModelOptionProvider[]>([])
97
+ const [selectedProvider, setSelectedProvider] = useState('')
98
+ const [selectedModel, setSelectedModel] = useState('')
99
+ const [auxiliary, setAuxiliary] = useState<AuxiliaryModelsResponse | null>(null)
100
+ const [applying, setApplying] = useState(false)
101
+ const [editingAuxTask, setEditingAuxTask] = useState<null | string>(null)
102
+ const [auxDraft, setAuxDraft] = useState<{ model: string; provider: string }>({ model: '', provider: '' })
103
+ // Aux slots reported stale by the backend immediately after a main-model
104
+ // switch (provider differs from the new main). Cleared on next switch/reset.
105
+ const [switchStaleAux, setSwitchStaleAux] = useState<StaleAuxAssignment[]>([])
106
+ // Inline API-key entry for picking an unconfigured `api_key` provider in
107
+ // place — mirrors the onboarding ApiKeyForm but scoped to the model picker.
108
+ const [apiKeyDraft, setApiKeyDraft] = useState('')
109
+ const [activating, setActivating] = useState(false)
110
+
111
+ const refresh = useCallback(async () => {
112
+ setLoading(true)
113
+ setError('')
114
+
115
+ try {
116
+ const [modelInfo, modelOptions, auxiliaryModels] = await Promise.all([
117
+ getGlobalModelInfo(),
118
+ getGlobalModelOptions(),
119
+ getAuxiliaryModels()
120
+ ])
121
+
122
+ setMainModel({ model: modelInfo.model, provider: modelInfo.provider })
123
+ setProviders(modelOptions.providers || [])
124
+ setSelectedProvider(prev => prev || modelInfo.provider)
125
+ setSelectedModel(prev => prev || modelInfo.model)
126
+ setAuxiliary(auxiliaryModels)
127
+ } catch (err) {
128
+ setError(err instanceof Error ? err.message : String(err))
129
+ } finally {
130
+ setLoading(false)
131
+ }
132
+ }, [])
133
+
134
+ useEffect(() => {
135
+ void refresh()
136
+ }, [refresh])
137
+
138
+ const providerOptions = providers.length ? providers : NO_PROVIDERS
139
+
140
+ const selectedProviderRow = useMemo(
141
+ () => providers.find(provider => provider.slug === selectedProvider),
142
+ [providers, selectedProvider]
143
+ )
144
+
145
+ const selectedProviderModels = selectedProviderRow?.models ?? []
146
+
147
+ // An unconfigured provider was picked: no credentials yet, so there are no
148
+ // models to choose. `api_key` providers can be activated inline (paste key);
149
+ // OAuth / external flows hand off to the onboarding sign-in.
150
+ const needsSetup = !!selectedProvider && !isProviderReady(selectedProviderRow)
151
+ const setupIsApiKey = needsSetup && selectedProviderRow?.auth_type === 'api_key' && !!selectedProviderRow?.key_env
152
+
153
+ // Clear any half-typed key when switching provider so it can't leak across.
154
+ useEffect(() => {
155
+ setApiKeyDraft('')
156
+ }, [selectedProvider])
157
+
158
+ const auxDraftProviderModels = useMemo(
159
+ () => providers.find(provider => provider.slug === auxDraft.provider)?.models ?? [],
160
+ [auxDraft.provider, providers]
161
+ )
162
+
163
+ const auxiliaryTaskLabel = useCallback((key: string) => m.tasks[key]?.label ?? key, [m.tasks])
164
+
165
+ // Persistent mismatch: any aux slot pinned to a provider different from the
166
+ // current main, regardless of whether the user just switched. Catches the
167
+ // "I pinned aux months ago and forgot, now it bills a dead provider" case.
168
+ const persistentStaleAux = useMemo<StaleAuxAssignment[]>(() => {
169
+ const mainProvider = (mainModel?.provider ?? '').toLowerCase()
170
+
171
+ if (!mainProvider || !auxiliary) {
172
+ return []
173
+ }
174
+
175
+ return auxiliary.tasks
176
+ .filter(entry => {
177
+ const p = (entry.provider ?? '').toLowerCase()
178
+
179
+ return p && p !== 'auto' && p !== mainProvider
180
+ })
181
+ .map(entry => ({ task: entry.task, provider: entry.provider, model: entry.model }))
182
+ }, [auxiliary, mainModel])
183
+
184
+ // Paste an API key for the selected `api_key` provider, persist it, then
185
+ // refresh so the now-authenticated provider's models populate. Auto-selects
186
+ // the recommended default model so the user can Apply in one more click.
187
+ const activateApiKeyProvider = useCallback(async () => {
188
+ const keyEnv = selectedProviderRow?.key_env
189
+ const slug = selectedProviderRow?.slug
190
+
191
+ if (!keyEnv || !slug || !apiKeyDraft.trim()) {
192
+ return
193
+ }
194
+
195
+ setActivating(true)
196
+ setError('')
197
+
198
+ try {
199
+ await setEnvVar(keyEnv, apiKeyDraft.trim())
200
+ setApiKeyDraft('')
201
+
202
+ // Pick a sensible default for the freshly-activated provider (mirrors
203
+ // `NASTECH model` curation). Best-effort — fall through to the refreshed
204
+ // model list if it fails.
205
+ let nextModel = ''
206
+
207
+ try {
208
+ const rec = await getRecommendedDefaultModel(slug)
209
+ nextModel = rec.model || ''
210
+ } catch {
211
+ nextModel = ''
212
+ }
213
+
214
+ const options = await getGlobalModelOptions()
215
+ setProviders(options.providers || [])
216
+ const refreshedRow = options.providers?.find(p => p.slug === slug)
217
+ const fallbackModel = refreshedRow?.models?.[0] ?? ''
218
+ setSelectedModel(nextModel || fallbackModel)
219
+ } catch (err) {
220
+ setError(err instanceof Error ? err.message : String(err))
221
+ } finally {
222
+ setActivating(false)
223
+ }
224
+ }, [apiKeyDraft, selectedProviderRow])
225
+
226
+ // OAuth / external providers can't be activated with a pasted key — hand off
227
+ // to the shared onboarding flow scoped to this provider's real sign-in.
228
+ const startProviderSetup = useCallback(() => {
229
+ if (selectedProviderRow?.slug) {
230
+ startManualProviderOAuth(selectedProviderRow.slug)
231
+ }
232
+ }, [selectedProviderRow])
233
+
234
+ const applyMainModel = useCallback(async () => {
235
+ if (!selectedProvider || !selectedModel) {
236
+ return
237
+ }
238
+
239
+ setApplying(true)
240
+ setError('')
241
+
242
+ try {
243
+ const result = await setModelAssignment({ model: selectedModel, provider: selectedProvider, scope: 'main' })
244
+ const provider = result.provider || selectedProvider
245
+ const model = result.model || selectedModel
246
+ setMainModel({ provider, model })
247
+ setSwitchStaleAux(result.stale_aux ?? [])
248
+ onMainModelChanged?.(provider, model)
249
+ await refresh()
250
+ } catch (err) {
251
+ setError(err instanceof Error ? err.message : String(err))
252
+ } finally {
253
+ setApplying(false)
254
+ }
255
+ }, [onMainModelChanged, refresh, selectedModel, selectedProvider])
256
+
257
+ const setAuxiliaryToMain = useCallback(
258
+ async (task: string) => {
259
+ if (!mainModel) {
260
+ return
261
+ }
262
+
263
+ setApplying(true)
264
+ setError('')
265
+
266
+ try {
267
+ await setModelAssignment({ model: mainModel.model, provider: mainModel.provider, scope: 'auxiliary', task })
268
+ await refresh()
269
+ } catch (err) {
270
+ setError(err instanceof Error ? err.message : String(err))
271
+ } finally {
272
+ setApplying(false)
273
+ }
274
+ },
275
+ [mainModel, refresh]
276
+ )
277
+
278
+ const applyAuxiliaryDraft = useCallback(
279
+ async (task: string) => {
280
+ if (!auxDraft.provider || !auxDraft.model) {
281
+ return
282
+ }
283
+
284
+ setApplying(true)
285
+ setError('')
286
+
287
+ try {
288
+ await setModelAssignment({ model: auxDraft.model, provider: auxDraft.provider, scope: 'auxiliary', task })
289
+ setEditingAuxTask(null)
290
+ await refresh()
291
+ } catch (err) {
292
+ setError(err instanceof Error ? err.message : String(err))
293
+ } finally {
294
+ setApplying(false)
295
+ }
296
+ },
297
+ [auxDraft, refresh]
298
+ )
299
+
300
+ const beginAuxiliaryEdit = useCallback(
301
+ (task: string) => {
302
+ const current = auxiliary?.tasks.find(entry => entry.task === task)
303
+
304
+ const initialProvider =
305
+ current?.provider && current.provider !== 'auto' ? current.provider : (mainModel?.provider ?? '')
306
+
307
+ const initialModel = current?.model || mainModel?.model || ''
308
+ setAuxDraft({ provider: initialProvider, model: initialModel })
309
+ setEditingAuxTask(task)
310
+ },
311
+ [auxiliary, mainModel]
312
+ )
313
+
314
+ const resetAuxiliaryModels = useCallback(async () => {
315
+ if (!mainModel) {
316
+ return
317
+ }
318
+
319
+ setApplying(true)
320
+ setError('')
321
+
322
+ try {
323
+ await setModelAssignment({
324
+ model: mainModel.model,
325
+ provider: mainModel.provider,
326
+ scope: 'auxiliary',
327
+ task: '__reset__'
328
+ })
329
+ setSwitchStaleAux([])
330
+ await refresh()
331
+ } catch (err) {
332
+ setError(err instanceof Error ? err.message : String(err))
333
+ } finally {
334
+ setApplying(false)
335
+ }
336
+ }, [mainModel, refresh])
337
+
338
+ if (loading && !mainModel) {
339
+ return <LoadingState label={m.loading} />
340
+ }
341
+
342
+ return (
343
+ <div className="grid gap-6">
344
+ <section>
345
+ <p className="mb-3 text-xs text-muted-foreground">
346
+ {m.appliesDesc}
347
+ </p>
348
+ <div className="flex flex-wrap items-center gap-2">
349
+ <Select onValueChange={setSelectedProvider} value={selectedProvider}>
350
+ <SelectTrigger className={cn('min-w-40', CONTROL_TEXT)}>
351
+ <SelectValue placeholder={m.provider} />
352
+ </SelectTrigger>
353
+ <SelectContent>
354
+ {providerOptions.map(provider => (
355
+ <SelectItem key={provider.slug || 'none'} value={provider.slug || 'none'}>
356
+ {provider.name}
357
+ </SelectItem>
358
+ ))}
359
+ </SelectContent>
360
+ </Select>
361
+ {needsSetup ? (
362
+ setupIsApiKey ? (
363
+ <>
364
+ <Input
365
+ autoComplete="off"
366
+ className={cn('min-w-60 flex-1', CONTROL_TEXT)}
367
+ onChange={event => setApiKeyDraft(event.target.value)}
368
+ onKeyDown={event => {
369
+ if (event.key === 'Enter') {
370
+ void activateApiKeyProvider()
371
+ }
372
+ }}
373
+ placeholder={`Paste ${selectedProviderRow?.key_env ?? 'API key'}`}
374
+ type="password"
375
+ value={apiKeyDraft}
376
+ />
377
+ <Button
378
+ disabled={!apiKeyDraft.trim() || activating}
379
+ onClick={() => void activateApiKeyProvider()}
380
+ size="sm"
381
+ >
382
+ {activating && <Loader2 className="size-3.5 animate-spin" />}
383
+ {activating ? 'Activating...' : 'Activate'}
384
+ </Button>
385
+ </>
386
+ ) : (
387
+ <Button onClick={startProviderSetup} size="sm" variant="textStrong">
388
+ Set up {selectedProviderRow?.name ?? 'provider'}
389
+ </Button>
390
+ )
391
+ ) : (
392
+ <>
393
+ <Select onValueChange={setSelectedModel} value={selectedModel}>
394
+ <SelectTrigger className={cn('min-w-60', CONTROL_TEXT)}>
395
+ <SelectValue placeholder={m.model} />
396
+ </SelectTrigger>
397
+ <SelectContent>
398
+ {(selectedProviderModels.length ? selectedProviderModels : []).map(model => (
399
+ <SelectItem key={model} value={model}>
400
+ {model}
401
+ </SelectItem>
402
+ ))}
403
+ </SelectContent>
404
+ </Select>
405
+ <Button
406
+ disabled={!selectedProvider || !selectedModel || applying}
407
+ onClick={() => void applyMainModel()}
408
+ size="sm"
409
+ >
410
+ {applying && <Loader2 className="size-3.5 animate-spin" />}
411
+ {applying ? m.applying : t.common.apply}
412
+ </Button>
413
+ </>
414
+ )}
415
+ </div>
416
+ {needsSetup && !setupIsApiKey && (
417
+ <p className="mt-2 text-xs text-muted-foreground">
418
+ {selectedProviderRow?.auth_type === 'api_key'
419
+ ? `${selectedProviderRow?.name} needs an API key — set it up to choose a model.`
420
+ : `${selectedProviderRow?.name} signs in through your browser — NasTech runs the flow for you.`}
421
+ </p>
422
+ )}
423
+ {error && <div className="mt-2 text-xs text-destructive">{error}</div>}
424
+ {switchStaleAux.length > 0 && (
425
+ <div className="mt-2">
426
+ <StaleAuxWarning
427
+ applying={applying}
428
+ onReset={() => void resetAuxiliaryModels()}
429
+ slots={switchStaleAux}
430
+ taskLabel={auxiliaryTaskLabel}
431
+ />
432
+ </div>
433
+ )}
434
+ </section>
435
+
436
+ <section>
437
+ <div className="mb-2.5 flex items-center justify-between">
438
+ <SectionHeading icon={Cpu} title={m.auxiliaryTitle} />
439
+ <Button
440
+ disabled={!mainModel || applying}
441
+ onClick={() => void resetAuxiliaryModels()}
442
+ size="sm"
443
+ variant="textStrong"
444
+ >
445
+ {m.resetAllToMain}
446
+ </Button>
447
+ </div>
448
+ <p className="mb-2 text-xs text-muted-foreground">
449
+ {m.auxiliaryDesc}
450
+ </p>
451
+ {switchStaleAux.length === 0 && persistentStaleAux.length > 0 && (
452
+ <div className="mb-2.5">
453
+ <StaleAuxWarning
454
+ applying={applying}
455
+ onReset={() => void resetAuxiliaryModels()}
456
+ slots={persistentStaleAux}
457
+ taskLabel={auxiliaryTaskLabel}
458
+ />
459
+ </div>
460
+ )}
461
+ <div className="grid gap-1">
462
+ {AUX_TASKS.map(meta => {
463
+ const copy = m.tasks[meta.key] ?? { label: meta.key, hint: meta.key }
464
+ const current = auxiliary?.tasks.find(entry => entry.task === meta.key)
465
+ const isAuto = !current || !current.provider || current.provider === 'auto'
466
+ const isEditing = editingAuxTask === meta.key
467
+
468
+ return (
469
+ <ListRow
470
+ action={
471
+ !isEditing && (
472
+ <div className="flex shrink-0 items-center gap-1.5">
473
+ <Button
474
+ disabled={!mainModel || applying}
475
+ onClick={() => void setAuxiliaryToMain(meta.key)}
476
+ size="sm"
477
+ variant="text"
478
+ >
479
+ {m.setToMain}
480
+ </Button>
481
+ <Button
482
+ disabled={!providers.length || applying}
483
+ onClick={() => beginAuxiliaryEdit(meta.key)}
484
+ size="sm"
485
+ variant="textStrong"
486
+ >
487
+ {m.change}
488
+ </Button>
489
+ </div>
490
+ )
491
+ }
492
+ below={
493
+ isEditing && (
494
+ <div className="mt-2 flex flex-wrap items-center gap-2 pt-1">
495
+ <Select
496
+ onValueChange={value => setAuxDraft(prev => ({ ...prev, provider: value, model: '' }))}
497
+ value={auxDraft.provider}
498
+ >
499
+ <SelectTrigger className={cn('min-w-32', CONTROL_TEXT)}>
500
+ <SelectValue placeholder={m.provider} />
501
+ </SelectTrigger>
502
+ <SelectContent>
503
+ {providerOptions.map(provider => (
504
+ <SelectItem key={provider.slug || 'none'} value={provider.slug || 'none'}>
505
+ {provider.name}
506
+ </SelectItem>
507
+ ))}
508
+ </SelectContent>
509
+ </Select>
510
+ <Select
511
+ onValueChange={value => setAuxDraft(prev => ({ ...prev, model: value }))}
512
+ value={auxDraft.model}
513
+ >
514
+ <SelectTrigger className={cn('min-w-48', CONTROL_TEXT)}>
515
+ <SelectValue placeholder={m.model} />
516
+ </SelectTrigger>
517
+ <SelectContent>
518
+ {(auxDraftProviderModels.length ? auxDraftProviderModels : []).map(model => (
519
+ <SelectItem key={model} value={model}>
520
+ {model}
521
+ </SelectItem>
522
+ ))}
523
+ </SelectContent>
524
+ </Select>
525
+ <Button
526
+ disabled={!auxDraft.provider || !auxDraft.model || applying}
527
+ onClick={() => void applyAuxiliaryDraft(meta.key)}
528
+ size="sm"
529
+ >
530
+ {applying ? m.applying : t.common.apply}
531
+ </Button>
532
+ <Button onClick={() => setEditingAuxTask(null)} size="sm" variant="ghost">
533
+ {t.common.cancel}
534
+ </Button>
535
+ </div>
536
+ )
537
+ }
538
+ description={
539
+ <span className="font-mono text-[0.68rem]">
540
+ {isAuto
541
+ ? m.autoUseMain
542
+ : `${current.provider} · ${current.model || m.providerDefault}`}
543
+ </span>
544
+ }
545
+ key={meta.key}
546
+ title={
547
+ <span className="flex items-baseline gap-2">
548
+ {copy.label}
549
+ <Pill>{copy.hint}</Pill>
550
+ </span>
551
+ }
552
+ />
553
+ )
554
+ })}
555
+ </div>
556
+ </section>
557
+ </div>
558
+ )
559
+ }