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,84 @@
1
+ import { cleanup, fireEvent, render, screen } from '@testing-library/react'
2
+ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
3
+
4
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuSub, DropdownMenuSubTrigger } from '@/components/ui/dropdown-menu'
5
+ import { $modelPresets, getModelPreset } from '@/store/model-presets'
6
+ import { $activeSessionId } from '@/store/session'
7
+
8
+ import { type FastControl, ModelEditSubmenu } from './model-edit-submenu'
9
+
10
+ // Radix calls these on open; jsdom doesn't implement them.
11
+ beforeAll(() => {
12
+ Element.prototype.scrollIntoView = vi.fn()
13
+ Element.prototype.hasPointerCapture = vi.fn(() => false)
14
+ Element.prototype.releasePointerCapture = vi.fn()
15
+ })
16
+
17
+ beforeEach(() => {
18
+ $modelPresets.set({})
19
+ $activeSessionId.set(null)
20
+ })
21
+
22
+ afterEach(() => {
23
+ cleanup()
24
+ vi.clearAllMocks()
25
+ })
26
+
27
+ // Render the submenu inside an open menu/sub so its content (switches) mounts.
28
+ function renderSubmenu(opts: { fastControl: FastControl; reasoning: boolean; requestGateway: () => Promise<unknown> }) {
29
+ return render(
30
+ <DropdownMenu open>
31
+ <DropdownMenuContent>
32
+ <DropdownMenuSub open>
33
+ <DropdownMenuSubTrigger>edit</DropdownMenuSubTrigger>
34
+ <ModelEditSubmenu
35
+ effort="medium"
36
+ fastControl={opts.fastControl}
37
+ isActive
38
+ model="m1"
39
+ onSelectModel={vi.fn()}
40
+ provider="p1"
41
+ reasoning={opts.reasoning}
42
+ requestGateway={opts.requestGateway as never}
43
+ />
44
+ </DropdownMenuSub>
45
+ </DropdownMenuContent>
46
+ </DropdownMenu>
47
+ )
48
+ }
49
+
50
+ // Regression: editing the active row before a live session exists must stay
51
+ // preset-only — the gateway's config.set falls back to global config when no
52
+ // session matches, so it must not be called. (Caught in the second review.)
53
+ describe('ModelEditSubmenu no-session guard', () => {
54
+ it('param fast: records the preset but skips the gateway without a session', () => {
55
+ const requestGateway = vi.fn().mockResolvedValue({})
56
+ renderSubmenu({ fastControl: { kind: 'param', on: false }, reasoning: false, requestGateway })
57
+
58
+ fireEvent.click(screen.getByRole('switch'))
59
+
60
+ expect(getModelPreset('p1', 'm1').fast).toBe(true)
61
+ expect(requestGateway).not.toHaveBeenCalled()
62
+ })
63
+
64
+ it('reasoning: records the preset but skips the gateway without a session', () => {
65
+ const requestGateway = vi.fn().mockResolvedValue({})
66
+ renderSubmenu({ fastControl: { kind: 'none' }, reasoning: true, requestGateway })
67
+
68
+ // Thinking starts on (medium); toggling it off routes through patchReasoning.
69
+ fireEvent.click(screen.getByRole('switch'))
70
+
71
+ expect(getModelPreset('p1', 'm1').effort).toBe('none')
72
+ expect(requestGateway).not.toHaveBeenCalled()
73
+ })
74
+
75
+ it('param fast: pushes to the gateway once a session is active', async () => {
76
+ const requestGateway = vi.fn().mockResolvedValue({})
77
+ $activeSessionId.set('sess1')
78
+ renderSubmenu({ fastControl: { kind: 'param', on: false }, reasoning: false, requestGateway })
79
+
80
+ fireEvent.click(screen.getByRole('switch'))
81
+
82
+ expect(requestGateway).toHaveBeenCalledWith('config.set', { key: 'fast', session_id: 'sess1', value: 'fast' })
83
+ })
84
+ })
@@ -0,0 +1,245 @@
1
+ import { useStore } from '@nanostores/react'
2
+
3
+ import {
4
+ DropdownMenuItem,
5
+ DropdownMenuLabel,
6
+ DropdownMenuRadioGroup,
7
+ DropdownMenuRadioItem,
8
+ dropdownMenuRow,
9
+ dropdownMenuSectionLabel,
10
+ DropdownMenuSeparator,
11
+ DropdownMenuSubContent
12
+ } from '@/components/ui/dropdown-menu'
13
+ import { Switch } from '@/components/ui/switch'
14
+ import { useI18n } from '@/i18n'
15
+ import { notifyError } from '@/store/notifications'
16
+ import {
17
+ $activeSessionId,
18
+ $currentReasoningEffort,
19
+ setCurrentFastMode,
20
+ setCurrentReasoningEffort
21
+ } from '@/store/session'
22
+
23
+ // NasTech' real reasoning levels (see VALID_REASONING_EFFORTS); `none` is owned
24
+ // by the Thinking toggle, not the radio.
25
+ const EFFORT_OPTIONS = [
26
+ { value: 'minimal', labelKey: 'minimal' },
27
+ { value: 'low', labelKey: 'low' },
28
+ { value: 'medium', labelKey: 'medium' },
29
+ { value: 'high', labelKey: 'high' },
30
+ { value: 'xhigh', labelKey: 'max' }
31
+ ] as const
32
+
33
+ /** How "fast" is achieved for a given model — two different mechanisms:
34
+ * - `param`: the Anthropic/OpenAI `speed=fast` request parameter.
35
+ * - `variant`: a separate `…-fast` sibling model selected via the model field.
36
+ */
37
+ export type FastControl =
38
+ | { kind: 'none' }
39
+ | { kind: 'param'; on: boolean }
40
+ | { kind: 'variant'; baseId: string; fastId: string; on: boolean }
41
+
42
+ /** Resolve the fast mechanism for a model: prefer the speed=fast parameter
43
+ * when the backend supports it, else fall back to a `…-fast` sibling model. */
44
+ export function resolveFastControl(
45
+ model: string,
46
+ providerModels: readonly string[],
47
+ paramSupported: boolean,
48
+ currentFastMode: boolean
49
+ ): FastControl {
50
+ if (paramSupported) {
51
+ return { kind: 'param', on: currentFastMode }
52
+ }
53
+
54
+ if (/-fast$/i.test(model)) {
55
+ const baseId = model.replace(/-fast$/i, '')
56
+
57
+ // Only a toggle if there's a base to switch back to; otherwise it's a
58
+ // standalone fast model with no "off" state.
59
+ return providerModels.includes(baseId) ? { kind: 'variant', baseId, fastId: model, on: true } : { kind: 'none' }
60
+ }
61
+
62
+ const fastId = `${model}-fast`
63
+
64
+ if (providerModels.includes(fastId)) {
65
+ return { kind: 'variant', baseId: model, fastId, on: false }
66
+ }
67
+
68
+ // Fast isn't natively offered here, but if the session still has the speed
69
+ // param on (carried over from a previous model), expose the toggle so it can
70
+ // be turned off rather than stranded.
71
+ if (currentFastMode) {
72
+ return { kind: 'param', on: true }
73
+ }
74
+
75
+ return { kind: 'none' }
76
+ }
77
+
78
+ interface ModelEditSubmenuProps {
79
+ /** How fast mode is offered for this model (param toggle vs. variant swap). */
80
+ fastControl: FastControl
81
+ /** Whether this row's model is the active one. */
82
+ isActive: boolean
83
+ /** Switch to this model (resolves false on failure). Awaited before applying
84
+ * edits when not active so a failed switch doesn't write to the old model. */
85
+ onActivate: () => Promise<boolean> | void
86
+ /** Switch to a specific model id (used to swap base ⇄ -fast variant). */
87
+ onSelectModel: (model: string) => Promise<boolean> | void
88
+ /** Whether this model supports reasoning effort. */
89
+ reasoning: boolean
90
+ requestGateway: <T>(method: string, params?: Record<string, unknown>) => Promise<T>
91
+ }
92
+
93
+ export function ModelEditSubmenu({
94
+ fastControl,
95
+ isActive,
96
+ onActivate,
97
+ onSelectModel,
98
+ reasoning,
99
+ requestGateway
100
+ }: ModelEditSubmenuProps) {
101
+ const { t } = useI18n()
102
+ const copy = t.shell.modelOptions
103
+ // Reactive session state comes straight from the stores rather than being
104
+ // drilled through the panel, so editing it re-renders only this submenu.
105
+ const activeSessionId = useStore($activeSessionId)
106
+ const currentReasoningEffort = useStore($currentReasoningEffort)
107
+
108
+ const effort = normalizeEffort(currentReasoningEffort)
109
+ const thinkingOn = isThinkingEnabled(currentReasoningEffort)
110
+
111
+ // Reasoning/fast are session-scoped (they apply to the active model), so
112
+ // editing a non-active model first switches to it. Returns false if the
113
+ // switch failed, so callers skip applying to the wrong (previous) model.
114
+ const ensureActive = async (): Promise<boolean> => {
115
+ if (isActive) {
116
+ return true
117
+ }
118
+
119
+ return (await onActivate()) !== false
120
+ }
121
+
122
+ const patchReasoning = async (next: string, rollback: string) => {
123
+ setCurrentReasoningEffort(next)
124
+
125
+ try {
126
+ if (!(await ensureActive())) {
127
+ setCurrentReasoningEffort(rollback)
128
+
129
+ return
130
+ }
131
+
132
+ await requestGateway('config.set', {
133
+ key: 'reasoning',
134
+ session_id: activeSessionId ?? '',
135
+ value: next
136
+ })
137
+ } catch (err) {
138
+ setCurrentReasoningEffort(rollback)
139
+ notifyError(err, copy.updateFailed)
140
+ }
141
+ }
142
+
143
+ const toggleFast = (enabled: boolean) => {
144
+ if (fastControl.kind === 'variant') {
145
+ // Fast is a separate model id — swap to it (or back to the base).
146
+ void onSelectModel(enabled ? fastControl.fastId : fastControl.baseId)
147
+
148
+ return
149
+ }
150
+
151
+ if (fastControl.kind === 'param') {
152
+ setCurrentFastMode(enabled)
153
+
154
+ void (async () => {
155
+ try {
156
+ if (!(await ensureActive())) {
157
+ setCurrentFastMode(!enabled)
158
+
159
+ return
160
+ }
161
+
162
+ await requestGateway('config.set', {
163
+ key: 'fast',
164
+ session_id: activeSessionId ?? '',
165
+ value: enabled ? 'fast' : 'normal'
166
+ })
167
+ } catch (err) {
168
+ setCurrentFastMode(!enabled)
169
+ notifyError(err, copy.fastFailed)
170
+ }
171
+ })()
172
+ }
173
+ }
174
+
175
+ const hasFast = fastControl.kind !== 'none'
176
+ const fastOn = fastControl.kind === 'none' ? false : fastControl.on
177
+
178
+ return (
179
+ <DropdownMenuSubContent className="w-52 p-0" sideOffset={4}>
180
+ {!hasFast && !reasoning ? (
181
+ <div className="px-2.5 py-3 text-xs text-(--ui-text-tertiary)">{copy.noOptions}</div>
182
+ ) : (
183
+ <>
184
+ <DropdownMenuLabel className={dropdownMenuSectionLabel}>{copy.options}</DropdownMenuLabel>
185
+ {reasoning ? (
186
+ <DropdownMenuItem className={dropdownMenuRow} onSelect={event => event.preventDefault()}>
187
+ {copy.thinking}
188
+ <Switch
189
+ checked={thinkingOn}
190
+ className="ml-auto"
191
+ onCheckedChange={checked =>
192
+ void patchReasoning(checked ? effort || 'medium' : 'none', currentReasoningEffort)
193
+ }
194
+ size="xs"
195
+ />
196
+ </DropdownMenuItem>
197
+ ) : null}
198
+ {hasFast ? (
199
+ <DropdownMenuItem className={dropdownMenuRow} onSelect={event => event.preventDefault()}>
200
+ {copy.fast}
201
+ <Switch checked={fastOn} className="ml-auto" onCheckedChange={toggleFast} size="xs" />
202
+ </DropdownMenuItem>
203
+ ) : null}
204
+ {reasoning ? (
205
+ <>
206
+ <DropdownMenuSeparator className="mx-0" />
207
+ <DropdownMenuLabel className={dropdownMenuSectionLabel}>{copy.effort}</DropdownMenuLabel>
208
+ <DropdownMenuRadioGroup
209
+ onValueChange={value => void patchReasoning(value, currentReasoningEffort)}
210
+ value={effort}
211
+ >
212
+ {EFFORT_OPTIONS.map(option => (
213
+ <DropdownMenuRadioItem
214
+ className={dropdownMenuRow}
215
+ key={option.value}
216
+ onSelect={event => event.preventDefault()}
217
+ value={option.value}
218
+ >
219
+ {copy[option.labelKey]}
220
+ </DropdownMenuRadioItem>
221
+ ))}
222
+ </DropdownMenuRadioGroup>
223
+ </>
224
+ ) : null}
225
+ </>
226
+ )}
227
+ </DropdownMenuSubContent>
228
+ )
229
+ }
230
+
231
+ function isThinkingEnabled(effort: string): boolean {
232
+ // Empty = NasTech default (medium) = on; only an explicit "none" is off.
233
+ return (effort || 'medium').trim().toLowerCase() !== 'none'
234
+ }
235
+
236
+ function normalizeEffort(effort: string): string {
237
+ const value = (effort || 'medium').trim().toLowerCase()
238
+
239
+ // Thinking off → no effort selected in the radio group.
240
+ if (value === 'none') {
241
+ return ''
242
+ }
243
+
244
+ return EFFORT_OPTIONS.some(option => option.value === value) ? value : 'medium'
245
+ }
@@ -0,0 +1,295 @@
1
+ import { useStore } from '@nanostores/react'
2
+ import { useQuery } from '@tanstack/react-query'
3
+ import { useMemo, useState } from 'react'
4
+
5
+ import { Codicon } from '@/components/ui/codicon'
6
+ import {
7
+ DropdownMenuGroup,
8
+ DropdownMenuItem,
9
+ DropdownMenuLabel,
10
+ dropdownMenuRow,
11
+ DropdownMenuSearch,
12
+ dropdownMenuSectionLabel,
13
+ DropdownMenuSeparator,
14
+ DropdownMenuSub,
15
+ DropdownMenuSubTrigger
16
+ } from '@/components/ui/dropdown-menu'
17
+ import { Skeleton } from '@/components/ui/skeleton'
18
+ import type { NasTechGateway } from '@/nastech'
19
+ import { getGlobalModelOptions } from '@/nastech'
20
+ import { useI18n } from '@/i18n'
21
+ import { displayModelName, modelDisplayParts, reasoningEffortLabel } from '@/lib/model-status-label'
22
+ import { cn } from '@/lib/utils'
23
+ import {
24
+ $visibleModels,
25
+ collapseModelFamilies,
26
+ DEFAULT_VISIBLE_PER_PROVIDER,
27
+ type ModelFamily,
28
+ modelVisibilityKey,
29
+ setModelVisibilityOpen
30
+ } from '@/store/model-visibility'
31
+ import {
32
+ $activeSessionId,
33
+ $currentFastMode,
34
+ $currentModel,
35
+ $currentProvider,
36
+ $currentReasoningEffort
37
+ } from '@/store/session'
38
+ import type { ModelOptionProvider, ModelOptionsResponse } from '@/types/nastech'
39
+
40
+ import { ModelEditSubmenu, resolveFastControl } from './model-edit-submenu'
41
+
42
+ interface ModelMenuPanelProps {
43
+ gateway?: NasTechGateway
44
+ onSelectModel: (selection: { model: string; persistGlobal: boolean; provider: string }) => Promise<boolean> | void
45
+ requestGateway: <T>(method: string, params?: Record<string, unknown>) => Promise<T>
46
+ }
47
+
48
+ interface ProviderGroup {
49
+ families: ModelFamily[]
50
+ provider: ModelOptionProvider
51
+ }
52
+
53
+ export function ModelMenuPanel({ gateway, onSelectModel, requestGateway }: ModelMenuPanelProps) {
54
+ const { t } = useI18n()
55
+ const copy = t.shell.modelMenu
56
+ const [search, setSearch] = useState('')
57
+ // Reactive session state is read from the stores here (not drilled in), so
58
+ // toggling effort/fast/model re-renders this panel in place without forcing
59
+ // the parent to rebuild the menu content (which would close the dropdown).
60
+ const activeSessionId = useStore($activeSessionId)
61
+ const currentFastMode = useStore($currentFastMode)
62
+ const currentModel = useStore($currentModel)
63
+ const currentProvider = useStore($currentProvider)
64
+ const currentReasoningEffort = useStore($currentReasoningEffort)
65
+ const visibleModels = useStore($visibleModels)
66
+
67
+ const modelOptions = useQuery({
68
+ queryKey: ['model-options', activeSessionId || 'global'],
69
+ queryFn: (): Promise<ModelOptionsResponse> => {
70
+ if (gateway && activeSessionId) {
71
+ return gateway.request<ModelOptionsResponse>('model.options', { session_id: activeSessionId })
72
+ }
73
+
74
+ return getGlobalModelOptions()
75
+ }
76
+ })
77
+
78
+ const optionsModel = String(modelOptions.data?.model ?? currentModel ?? '')
79
+ const optionsProvider = String(modelOptions.data?.provider ?? currentProvider ?? '')
80
+ const loading = modelOptions.isPending && !modelOptions.data
81
+
82
+ const error = modelOptions.error
83
+ ? modelOptions.error instanceof Error
84
+ ? modelOptions.error.message
85
+ : String(modelOptions.error)
86
+ : null
87
+
88
+ const providers = modelOptions.data?.providers
89
+
90
+ const switchTo = (model: string, provider: string) =>
91
+ onSelectModel({ model, persistGlobal: !activeSessionId, provider })
92
+
93
+ const groups = useMemo(
94
+ () => groupModels(providers ?? [], search, { model: optionsModel, provider: optionsProvider }, visibleModels),
95
+ [providers, search, optionsModel, optionsProvider, visibleModels]
96
+ )
97
+
98
+ return (
99
+ <>
100
+ <DropdownMenuSearch
101
+ aria-label={copy.search}
102
+ onValueChange={setSearch}
103
+ placeholder={copy.search}
104
+ value={search}
105
+ />
106
+
107
+ <DropdownMenuSeparator className="mx-0" />
108
+
109
+ {loading ? (
110
+ <DropdownMenuGroup className="py-1">
111
+ {Array.from({ length: 4 }, (_, index) => (
112
+ <DropdownMenuItem
113
+ className={dropdownMenuRow}
114
+ disabled
115
+ key={index}
116
+ onSelect={event => event.preventDefault()}
117
+ >
118
+ <Skeleton className="h-4 w-full" />
119
+ </DropdownMenuItem>
120
+ ))}
121
+ </DropdownMenuGroup>
122
+ ) : error ? (
123
+ <DropdownMenuItem className={dropdownMenuRow} disabled>
124
+ {error}
125
+ </DropdownMenuItem>
126
+ ) : groups.length === 0 ? (
127
+ <DropdownMenuItem className={dropdownMenuRow} disabled>
128
+ {copy.noModels}
129
+ </DropdownMenuItem>
130
+ ) : (
131
+ <div className="max-h-80 overflow-y-auto py-0.5">
132
+ {groups.map(group => (
133
+ <DropdownMenuGroup className="py-0.5" key={group.provider.slug}>
134
+ <DropdownMenuLabel className={dropdownMenuSectionLabel}>{group.provider.name}</DropdownMenuLabel>
135
+ {group.families.map(family => {
136
+ // The active id may be the base or its -fast sibling; either
137
+ // way this one family row represents both.
138
+ const activeId =
139
+ group.provider.slug === optionsProvider &&
140
+ (optionsModel === family.id || optionsModel === family.fastId)
141
+ ? optionsModel
142
+ : null
143
+
144
+ const isCurrent = activeId !== null
145
+ const name = modelDisplayParts(family.id).name
146
+ // Capabilities are looked up against the active/base id; the
147
+ // -fast variant carries the same param support as its base.
148
+ const caps = group.provider.capabilities?.[family.id]
149
+
150
+ // Single source of truth for the active row's fast state — keeps
151
+ // the row label in lock-step with the submenu's Fast toggle and
152
+ // handles the standalone `-fast` id case.
153
+ const fastControl = resolveFastControl(
154
+ activeId ?? family.id,
155
+ group.provider.models ?? [],
156
+ caps?.fast ?? false,
157
+ currentFastMode
158
+ )
159
+
160
+ // Grayed text: active row shows live state (Fast + effort);
161
+ // others show a fast-capability hint.
162
+ const meta = isCurrent
163
+ ? [
164
+ fastControl.kind !== 'none' && fastControl.on ? copy.fast : null,
165
+ reasoningEffortLabel(currentReasoningEffort) || copy.medium
166
+ ]
167
+ .filter(Boolean)
168
+ .join(' ')
169
+ : caps?.fast || family.fastId
170
+ ? copy.fast
171
+ : ''
172
+
173
+ // Every row is a hover-Edit submenu trigger. Activating it
174
+ // (pointer or keyboard) switches to the family's base model;
175
+ // the Fast toggle inside swaps to the -fast sibling (or flips
176
+ // the speed param). The sub-trigger has no `onSelect`, so wire
177
+ // both click and Enter/Space for keyboard parity.
178
+ const activate = () => {
179
+ if (!isCurrent) {
180
+ void switchTo(family.id, group.provider.slug)
181
+ }
182
+ }
183
+
184
+ return (
185
+ <DropdownMenuSub key={`${group.provider.slug}:${family.id}`}>
186
+ <DropdownMenuSubTrigger
187
+ className={dropdownMenuRow}
188
+ hideChevron
189
+ onClick={activate}
190
+ onKeyDown={event => {
191
+ if (event.key === 'Enter' || event.key === ' ') {
192
+ activate()
193
+ }
194
+ }}
195
+ >
196
+ <span className="min-w-0 flex-1 truncate">
197
+ {name}
198
+ {meta ? <span className="text-(--ui-text-tertiary)"> {meta}</span> : null}
199
+ </span>
200
+ {isCurrent ? <Codicon className="ml-auto text-foreground" name="check" size="0.75rem" /> : null}
201
+ </DropdownMenuSubTrigger>
202
+ <ModelEditSubmenu
203
+ fastControl={fastControl}
204
+ isActive={isCurrent}
205
+ onActivate={() => switchTo(family.id, group.provider.slug)}
206
+ onSelectModel={nextModel => switchTo(nextModel, group.provider.slug)}
207
+ reasoning={caps?.reasoning ?? true}
208
+ requestGateway={requestGateway}
209
+ />
210
+ </DropdownMenuSub>
211
+ )
212
+ })}
213
+ </DropdownMenuGroup>
214
+ ))}
215
+ </div>
216
+ )}
217
+
218
+ <DropdownMenuSeparator className="mx-0" />
219
+
220
+ <DropdownMenuItem
221
+ className={cn(dropdownMenuRow, 'text-(--ui-text-tertiary)')}
222
+ onSelect={() => setModelVisibilityOpen(true)}
223
+ >
224
+ {copy.editModels}
225
+ </DropdownMenuItem>
226
+ </>
227
+ )
228
+ }
229
+
230
+ // Collapsed we show the user's chosen models (or the curated default); typing
231
+ // spans every available model so anything is reachable past the cut.
232
+ const PER_PROVIDER_SEARCH = 12
233
+
234
+ function groupModels(
235
+ providers: ModelOptionProvider[],
236
+ search: string,
237
+ current: { model: string; provider: string },
238
+ visible: Set<string> | null
239
+ ): ProviderGroup[] {
240
+ const q = search.trim().toLowerCase()
241
+ const groups: ProviderGroup[] = []
242
+
243
+ for (const provider of providers) {
244
+ const allFamilies = collapseModelFamilies(provider.models ?? [])
245
+
246
+ if (allFamilies.length === 0) {
247
+ continue
248
+ }
249
+
250
+ const matches = (family: ModelFamily) =>
251
+ `${family.id} ${family.fastId ?? ''} ${provider.name} ${provider.slug} ${displayModelName(family.id)}`
252
+ .toLowerCase()
253
+ .includes(q)
254
+
255
+ // Which model ids to show (the active one is always added on top of this).
256
+ let shown: Set<string>
257
+
258
+ if (q) {
259
+ // Search spans every family, regardless of visibility.
260
+ shown = new Set(allFamilies.filter(matches).map(family => family.id))
261
+ } else if (visible) {
262
+ // User has customized which models show — honor their selection exactly.
263
+ shown = new Set(
264
+ allFamilies.filter(family => visible.has(modelVisibilityKey(provider.slug, family.id))).map(family => family.id)
265
+ )
266
+ } else {
267
+ // Default: curated top-N families per provider.
268
+ shown = new Set(allFamilies.slice(0, DEFAULT_VISIBLE_PER_PROVIDER).map(family => family.id))
269
+ }
270
+
271
+ // Always include the active model — but keep every row in the provider's
272
+ // stable curated order (filter `allFamilies`, never reorder), so selecting
273
+ // a model can't shuffle the list.
274
+ const activeId =
275
+ provider.slug === current.provider && current.model
276
+ ? allFamilies.find(family => family.id === current.model || family.fastId === current.model)?.id
277
+ : undefined
278
+
279
+ let families = allFamilies.filter(family => shown.has(family.id) || family.id === activeId)
280
+
281
+ if (q) {
282
+ families = families.slice(0, PER_PROVIDER_SEARCH)
283
+ }
284
+
285
+ if (families.length > 0) {
286
+ groups.push({ families, provider })
287
+ }
288
+ }
289
+
290
+ // Stable, logical group order: alphabetical by provider name. (The backend
291
+ // floats the current provider first, which would reshuffle on every switch.)
292
+ groups.sort((a, b) => a.provider.name.localeCompare(b.provider.name))
293
+
294
+ return groups
295
+ }
@@ -0,0 +1,22 @@
1
+ import type * as React from 'react'
2
+
3
+ import { cn } from '@/lib/utils'
4
+
5
+ interface SidebarPanelLabelProps extends React.ComponentProps<'span'> {
6
+ dotClassName?: string
7
+ }
8
+
9
+ export function SidebarPanelLabel({ children, className, dotClassName, ...props }: SidebarPanelLabelProps) {
10
+ return (
11
+ <span
12
+ className={cn(
13
+ 'flex min-w-0 items-center gap-2 pl-2 text-[0.64rem] font-semibold uppercase tracking-[0.16em] text-(--theme-primary)',
14
+ className
15
+ )}
16
+ {...props}
17
+ >
18
+ <span aria-hidden="true" className={cn('dither inline-block size-2 shrink-0 rounded-[1px]', dotClassName)} />
19
+ <span className="min-w-0 truncate leading-none">{children}</span>
20
+ </span>
21
+ )
22
+ }