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,340 @@
1
+ import { useQuery } from '@tanstack/react-query'
2
+ import { useState } from 'react'
3
+
4
+ import { useI18n } from '@/i18n'
5
+ import type { ModelOptionProvider, ModelOptionsResponse, ModelPricing } from '@/types/nastech'
6
+
7
+ import type { NasTechGateway } from '../nastech'
8
+ import { getGlobalModelOptions } from '../nastech'
9
+ import { cn } from '../lib/utils'
10
+ import { startManualOnboarding } from '../store/onboarding'
11
+
12
+ import { InlineNotice } from './notifications'
13
+ import { Button } from './ui/button'
14
+ import { Checkbox } from './ui/checkbox'
15
+ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from './ui/command'
16
+ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from './ui/dialog'
17
+ import { Skeleton } from './ui/skeleton'
18
+
19
+ interface ModelPickerDialogProps {
20
+ open: boolean
21
+ onOpenChange: (open: boolean) => void
22
+ gw?: NasTechGateway
23
+ sessionId?: string | null
24
+ currentModel: string
25
+ currentProvider: string
26
+ onSelect: (selection: { provider: string; model: string; persistGlobal: boolean }) => void
27
+ /**
28
+ * Optional class to apply to DialogContent. Use to override z-index when
29
+ * stacking the picker on top of another fixed overlay (e.g. the desktop
30
+ * onboarding overlay, which sits at z-1300; the default Dialog z-130 ends
31
+ * up rendering underneath and blocks pointer events).
32
+ */
33
+ contentClassName?: string
34
+ }
35
+
36
+ export function ModelPickerDialog({
37
+ open,
38
+ onOpenChange,
39
+ gw,
40
+ sessionId,
41
+ currentModel,
42
+ currentProvider,
43
+ onSelect,
44
+ contentClassName
45
+ }: ModelPickerDialogProps) {
46
+ const { t } = useI18n()
47
+ const copy = t.modelPicker
48
+ const [persistGlobal, setPersistGlobal] = useState(!sessionId)
49
+ // Own the search term so we can filter manually. cmdk's built-in
50
+ // shouldFilter reorders items by its fuzzy-match score (≈alphabetical with
51
+ // an empty query), which destroys the backend's curated order. We disable
52
+ // it and do a plain substring filter that preserves array order — matching
53
+ // the `NASTECH model` CLI picker, which shows the curated list verbatim.
54
+ const [search, setSearch] = useState('')
55
+
56
+ const modelOptions = useQuery({
57
+ queryKey: ['model-options', sessionId || 'global'],
58
+ queryFn: () => {
59
+ if (gw && sessionId) {
60
+ return gw.request<ModelOptionsResponse>('model.options', {
61
+ session_id: sessionId
62
+ })
63
+ }
64
+
65
+ return getGlobalModelOptions()
66
+ },
67
+ enabled: open
68
+ })
69
+
70
+ const providers = modelOptions.data?.providers ?? []
71
+ const optionsModel = String(modelOptions.data?.model ?? currentModel ?? '')
72
+ const optionsProvider = String(modelOptions.data?.provider ?? currentProvider ?? '')
73
+ const loading = modelOptions.isPending && !modelOptions.data
74
+
75
+ const error = modelOptions.error
76
+ ? modelOptions.error instanceof Error
77
+ ? modelOptions.error.message
78
+ : String(modelOptions.error)
79
+ : null
80
+
81
+ const selectModel = (provider: ModelOptionProvider, model: string) => {
82
+ onSelect({
83
+ provider: provider.slug,
84
+ model,
85
+ persistGlobal: persistGlobal || !sessionId
86
+ })
87
+ onOpenChange(false)
88
+ }
89
+
90
+ // Open the full onboarding provider selector to add/switch a provider.
91
+ // Reuses the entire onboarding flow (OAuth rows, API-key form, device-code,
92
+ // model-confirm) instead of duplicating provider UI here. Closes the picker
93
+ // so the onboarding overlay (z-1300) isn't rendered underneath it.
94
+ const addProvider = () => {
95
+ startManualOnboarding()
96
+ onOpenChange(false)
97
+ }
98
+
99
+ return (
100
+ <Dialog onOpenChange={onOpenChange} open={open}>
101
+ <DialogContent className={cn('max-h-[85vh] max-w-2xl gap-0 overflow-hidden p-0', contentClassName)}>
102
+ <DialogHeader className="border-b border-border px-4 py-3">
103
+ <DialogTitle>{copy.title}</DialogTitle>
104
+ <DialogDescription className="font-mono text-xs leading-relaxed">
105
+ {copy.current} {optionsModel || currentModel || copy.unknown}
106
+ {optionsProvider || currentProvider ? ` · ${optionsProvider || currentProvider}` : ''}
107
+ </DialogDescription>
108
+ </DialogHeader>
109
+
110
+ <Command className="rounded-none bg-card" shouldFilter={false}>
111
+ <CommandInput
112
+ autoFocus
113
+ onValueChange={setSearch}
114
+ placeholder={copy.search}
115
+ value={search}
116
+ />
117
+ <CommandList className="max-h-96">
118
+ {!loading && !error && <CommandEmpty>{copy.noModels}</CommandEmpty>}
119
+ <ModelResults
120
+ currentModel={optionsModel || currentModel}
121
+ currentProvider={optionsProvider || currentProvider}
122
+ error={error}
123
+ loading={loading}
124
+ onSelectModel={selectModel}
125
+ providers={providers}
126
+ search={search}
127
+ />
128
+ </CommandList>
129
+ </Command>
130
+
131
+ <DialogFooter className="flex-row items-center justify-between gap-3 bg-card p-3 sm:justify-between">
132
+ <label className="flex cursor-pointer select-none items-center gap-2 text-xs text-muted-foreground">
133
+ <Checkbox
134
+ checked={persistGlobal || !sessionId}
135
+ disabled={!sessionId}
136
+ onCheckedChange={checked => setPersistGlobal(checked === true)}
137
+ />
138
+ {sessionId ? copy.persistGlobalSession : copy.persistGlobal}
139
+ </label>
140
+
141
+ <div className="flex items-center gap-2">
142
+ <Button onClick={addProvider} variant="ghost">
143
+ {copy.addProvider}
144
+ </Button>
145
+ <Button onClick={() => onOpenChange(false)} variant="outline">
146
+ {t.common.cancel}
147
+ </Button>
148
+ </div>
149
+ </DialogFooter>
150
+ </DialogContent>
151
+ </Dialog>
152
+ )
153
+ }
154
+
155
+ function ModelResults({
156
+ loading,
157
+ error,
158
+ providers,
159
+ currentModel,
160
+ currentProvider,
161
+ onSelectModel,
162
+ search
163
+ }: {
164
+ loading: boolean
165
+ error: string | null
166
+ providers: ModelOptionProvider[]
167
+ currentModel: string
168
+ currentProvider: string
169
+ onSelectModel: (provider: ModelOptionProvider, model: string) => void
170
+ search: string
171
+ }) {
172
+ const { t } = useI18n()
173
+ const copy = t.modelPicker
174
+
175
+ if (loading) {
176
+ return <LoadingResults />
177
+ }
178
+
179
+ if (error) {
180
+ return (
181
+ <div className="px-3 py-3">
182
+ <InlineNotice kind="error" title={copy.loadFailed}>
183
+ {error}
184
+ </InlineNotice>
185
+ </div>
186
+ )
187
+ }
188
+
189
+ if (providers.length === 0) {
190
+ return <div className="px-4 py-6 text-sm text-muted-foreground">{copy.noAuthenticatedProviders}</div>
191
+ }
192
+
193
+ const q = search.trim().toLowerCase()
194
+
195
+ const matches = (provider: ModelOptionProvider, model: string) =>
196
+ !q ||
197
+ model.toLowerCase().includes(q) ||
198
+ provider.name.toLowerCase().includes(q) ||
199
+ provider.slug.toLowerCase().includes(q)
200
+
201
+ // Only configured providers (those with curated models) are selectable
202
+ // here. Switching to a NOT-yet-configured provider goes through the
203
+ // "Add provider" footer button, which opens the full onboarding selector.
204
+ const configured = providers.filter(p => (p.models ?? []).length > 0)
205
+
206
+ return (
207
+ <>
208
+ {configured.map(provider => {
209
+ // Preserve the backend's curated order — filter in place, no re-sort.
210
+ const models = (provider.models ?? []).filter(m => matches(provider, m))
211
+
212
+ if (models.length === 0) {
213
+ return null
214
+ }
215
+
216
+ const unavailable = new Set(provider.unavailable_models ?? [])
217
+
218
+ return (
219
+ <CommandGroup heading={<ProviderHeading provider={provider} />} key={provider.slug}>
220
+ {provider.warning && (
221
+ <div className="px-2 pb-2">
222
+ <InlineNotice className="px-2.5 py-1.5 text-xs" kind="warning">
223
+ {provider.warning}
224
+ </InlineNotice>
225
+ </div>
226
+ )}
227
+ {models.map(model => {
228
+ const isCurrent = model === currentModel && provider.slug === currentProvider
229
+ const price = provider.pricing?.[model]
230
+ const locked = unavailable.has(model)
231
+
232
+ return (
233
+ <CommandItem
234
+ className={cn(
235
+ 'flex items-center gap-2 pl-6 font-mono',
236
+ isCurrent &&
237
+ 'bg-primary text-primary-foreground data-[selected=true]:bg-primary data-[selected=true]:text-primary-foreground',
238
+ locked && 'cursor-not-allowed opacity-45'
239
+ )}
240
+ disabled={locked}
241
+ key={`${provider.slug}:${model}`}
242
+ onSelect={() => {
243
+ if (!locked) {
244
+ onSelectModel(provider, model)
245
+ }
246
+ }}
247
+ value={`${provider.slug}:${model}`}
248
+ >
249
+ <span className="min-w-0 flex-1 truncate">{model}</span>
250
+ {locked && <span className="shrink-0 text-[0.62rem] uppercase tracking-wide opacity-80">{copy.pro}</span>}
251
+ <ModelPrice isCurrent={isCurrent} price={price} />
252
+ </CommandItem>
253
+ )
254
+ })}
255
+ {unavailable.size > 0 && (
256
+ <div className="px-6 pb-2 pt-1 text-[0.62rem] leading-relaxed text-muted-foreground">
257
+ {copy.proNeedsSubscription}
258
+ </div>
259
+ )}
260
+ </CommandGroup>
261
+ )
262
+ })}
263
+ </>
264
+ )
265
+ }
266
+
267
+ // Compact In/Out $/Mtok price tag, mirroring the CLI picker's price columns.
268
+ // Renders nothing when pricing is unavailable for the model.
269
+ function ModelPrice({ price, isCurrent }: { price?: ModelPricing; isCurrent: boolean }) {
270
+ const { t } = useI18n()
271
+ const copy = t.modelPicker
272
+
273
+ if (!price || (!price.input && !price.output)) {
274
+ return null
275
+ }
276
+
277
+ if (price.free) {
278
+ return (
279
+ <span
280
+ className={cn(
281
+ 'shrink-0 rounded-sm px-1 py-0.5 text-[0.62rem] font-semibold uppercase tracking-wide',
282
+ isCurrent ? 'bg-primary-foreground/20' : 'bg-emerald-500/15 text-emerald-600 dark:text-emerald-400'
283
+ )}
284
+ >
285
+ {copy.free}
286
+ </span>
287
+ )
288
+ }
289
+
290
+ return (
291
+ <span
292
+ className={cn(
293
+ 'shrink-0 text-[0.66rem] tabular-nums',
294
+ isCurrent ? 'text-primary-foreground/80' : 'text-muted-foreground'
295
+ )}
296
+ title={copy.priceTitle}
297
+ >
298
+ {price.input || '?'} / {price.output || '?'}
299
+ </span>
300
+ )
301
+ }
302
+
303
+ function LoadingResults() {
304
+ return (
305
+ <CommandGroup heading={<Skeleton className="h-3 w-32" />}>
306
+ {Array.from({ length: 4 }, (_, rowIndex) => (
307
+ <div className="rounded-sm py-1.5 pl-6 pr-2" key={rowIndex}>
308
+ <Skeleton className={cn('h-5', rowIndex % 3 === 0 ? 'w-3/5' : rowIndex % 3 === 1 ? 'w-4/5' : 'w-1/2')} />
309
+ </div>
310
+ ))}
311
+ </CommandGroup>
312
+ )
313
+ }
314
+
315
+ function ProviderHeading({ provider }: { provider: ModelOptionProvider }) {
316
+ const { t } = useI18n()
317
+ const copy = t.modelPicker
318
+
319
+ // free_tier is only set for NasTech Portal. true → "Free tier", false → "Pro".
320
+ const tierBadge =
321
+ provider.free_tier === true ? (
322
+ <span className="rounded-sm bg-emerald-500/15 px-1 py-0.5 text-[0.6rem] font-semibold uppercase tracking-wide text-emerald-600 dark:text-emerald-400">
323
+ {copy.freeTier}
324
+ </span>
325
+ ) : provider.free_tier === false ? (
326
+ <span className="rounded-sm bg-primary/15 px-1 py-0.5 text-[0.6rem] font-semibold uppercase tracking-wide text-primary">
327
+ {copy.pro}
328
+ </span>
329
+ ) : null
330
+
331
+ return (
332
+ <span className="flex min-w-0 items-center gap-2">
333
+ <span className="truncate">{provider.name}</span>
334
+ <span className="font-mono text-xs font-normal normal-case tracking-normal text-muted-foreground">
335
+ {provider.slug} · {provider.total_models ?? provider.models?.length ?? 0}
336
+ </span>
337
+ {tierBadge}
338
+ </span>
339
+ )
340
+ }
@@ -0,0 +1,155 @@
1
+ import { useStore } from '@nanostores/react'
2
+ import { useQuery } from '@tanstack/react-query'
3
+ import { useMemo, useState } from 'react'
4
+
5
+ import { BrailleSpinner } from '@/components/ui/braille-spinner'
6
+ import { Button } from '@/components/ui/button'
7
+ import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
8
+ import { Switch } from '@/components/ui/switch'
9
+ import type { NasTechGateway } from '@/nastech'
10
+ import { getGlobalModelOptions } from '@/nastech'
11
+ import { useI18n } from '@/i18n'
12
+ import { displayModelName, modelDisplayParts } from '@/lib/model-status-label'
13
+ import {
14
+ $visibleModels,
15
+ collapseModelFamilies,
16
+ effectiveVisibleKeys,
17
+ modelVisibilityKey,
18
+ setVisibleModels
19
+ } from '@/store/model-visibility'
20
+ import type { ModelOptionProvider, ModelOptionsResponse } from '@/types/nastech'
21
+
22
+ interface ModelVisibilityDialogProps {
23
+ gw?: NasTechGateway
24
+ onOpenChange: (open: boolean) => void
25
+ onOpenProviders: () => void
26
+ open: boolean
27
+ sessionId?: string | null
28
+ }
29
+
30
+ export function ModelVisibilityDialog({
31
+ gw,
32
+ onOpenChange,
33
+ onOpenProviders,
34
+ open,
35
+ sessionId
36
+ }: ModelVisibilityDialogProps) {
37
+ const { t } = useI18n()
38
+ const copy = t.modelVisibility
39
+ const [search, setSearch] = useState('')
40
+ const stored = useStore($visibleModels)
41
+
42
+ const modelOptions = useQuery({
43
+ queryKey: ['model-options', sessionId || 'global'],
44
+ queryFn: (): Promise<ModelOptionsResponse> => {
45
+ if (gw && sessionId) {
46
+ return gw.request<ModelOptionsResponse>('model.options', { session_id: sessionId })
47
+ }
48
+
49
+ return getGlobalModelOptions()
50
+ },
51
+ enabled: open
52
+ })
53
+
54
+ const providers = useMemo(
55
+ () => (modelOptions.data?.providers ?? []).filter(provider => (provider.models ?? []).length > 0),
56
+ [modelOptions.data]
57
+ )
58
+
59
+ const visible = effectiveVisibleKeys(stored, providers)
60
+
61
+ const toggle = (provider: ModelOptionProvider, model: string) => {
62
+ const next = new Set(effectiveVisibleKeys($visibleModels.get(), providers))
63
+ const key = modelVisibilityKey(provider.slug, model)
64
+
65
+ if (next.has(key)) {
66
+ next.delete(key)
67
+ } else {
68
+ next.add(key)
69
+ }
70
+
71
+ setVisibleModels(next)
72
+ }
73
+
74
+ const q = search.trim().toLowerCase()
75
+
76
+ const matches = (provider: ModelOptionProvider, model: string) =>
77
+ !q || `${model} ${provider.name} ${provider.slug} ${displayModelName(model)}`.toLowerCase().includes(q)
78
+
79
+ return (
80
+ <Dialog onOpenChange={onOpenChange} open={open}>
81
+ <DialogContent className="max-w-xs gap-0 overflow-hidden p-0">
82
+ <DialogHeader className="px-3 pb-1 pt-3">
83
+ <DialogTitle className="text-[0.8125rem]">{copy.title}</DialogTitle>
84
+ </DialogHeader>
85
+
86
+ <div className="px-3 py-1.5">
87
+ <input
88
+ autoFocus
89
+ className="h-5 w-full bg-transparent text-xs text-foreground placeholder:text-(--ui-text-tertiary) focus:outline-none"
90
+ onChange={event => setSearch(event.target.value)}
91
+ placeholder={copy.search}
92
+ type="text"
93
+ value={search}
94
+ />
95
+ </div>
96
+
97
+ <div className="max-h-[55vh] overflow-y-auto pb-1">
98
+ {providers.length === 0 ? (
99
+ <div className="px-3 py-5 text-center text-xs text-muted-foreground">
100
+ {modelOptions.isPending ? <BrailleSpinner className="mx-auto text-sm" /> : copy.noAuthenticatedProviders}
101
+ </div>
102
+ ) : (
103
+ providers.map(provider => {
104
+ const models = collapseModelFamilies(provider.models ?? []).filter(family => matches(provider, family.id))
105
+
106
+ if (models.length === 0) {
107
+ return null
108
+ }
109
+
110
+ return (
111
+ <div className="py-0.5" key={provider.slug}>
112
+ <div className="px-3 pb-0.5 pt-1 text-[0.625rem] font-medium uppercase tracking-wide text-(--ui-text-tertiary)">
113
+ {provider.name}
114
+ </div>
115
+ {models.map(family => {
116
+ const { name, tag } = modelDisplayParts(family.id)
117
+ const key = modelVisibilityKey(provider.slug, family.id)
118
+
119
+ return (
120
+ <label
121
+ className="flex cursor-pointer items-center gap-2 px-3 py-1 text-xs hover:bg-accent/50"
122
+ key={key}
123
+ >
124
+ <span className="min-w-0 flex-1 truncate">
125
+ {name}
126
+ {tag ? <span className="text-(--ui-text-tertiary)"> {tag}</span> : null}
127
+ </span>
128
+ <Switch checked={visible.has(key)} onCheckedChange={() => toggle(provider, family.id)} />
129
+ </label>
130
+ )
131
+ })}
132
+ </div>
133
+ )
134
+ })
135
+ )}
136
+ </div>
137
+
138
+ <div className="px-3 py-2">
139
+ <Button
140
+ className="-ml-2 text-(--ui-text-tertiary)"
141
+ onClick={() => {
142
+ onOpenChange(false)
143
+ onOpenProviders()
144
+ }}
145
+ size="xs"
146
+ type="button"
147
+ variant="text"
148
+ >
149
+ {copy.addProvider}
150
+ </Button>
151
+ </div>
152
+ </DialogContent>
153
+ </Dialog>
154
+ )
155
+ }
@@ -0,0 +1,196 @@
1
+ import { useStore } from '@nanostores/react'
2
+ import { type ReactNode, useEffect, useRef, useState } from 'react'
3
+ import { createPortal } from 'react-dom'
4
+
5
+ import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
6
+ import { Button } from '@/components/ui/button'
7
+ import { Codicon } from '@/components/ui/codicon'
8
+ import { CopyButton } from '@/components/ui/copy-button'
9
+ import { useI18n } from '@/i18n'
10
+ import { triggerHaptic } from '@/lib/haptics'
11
+ import { AlertCircle, AlertTriangle, CheckCircle2, type IconComponent, Info } from '@/lib/icons'
12
+ import { cn } from '@/lib/utils'
13
+ import {
14
+ $notifications,
15
+ type AppNotification,
16
+ clearNotifications,
17
+ dismissNotification,
18
+ type NotificationKind
19
+ } from '@/store/notifications'
20
+
21
+ type ToneVariant = 'default' | 'destructive' | 'warning' | 'success'
22
+
23
+ const tone: Record<NotificationKind, { icon: IconComponent; iconClass: string; variant: ToneVariant }> = {
24
+ error: { icon: AlertCircle, iconClass: 'text-destructive', variant: 'destructive' },
25
+ warning: { icon: AlertTriangle, iconClass: 'text-primary', variant: 'warning' },
26
+ info: { icon: Info, iconClass: 'text-muted-foreground', variant: 'default' },
27
+ success: { icon: CheckCircle2, iconClass: 'text-primary', variant: 'success' }
28
+ }
29
+
30
+ const STACK_SURFACE = 'pointer-events-auto border border-(--stroke-nastech) bg-popover/95 shadow-nastech backdrop-blur-md'
31
+
32
+ export function NotificationStack() {
33
+ const notifications = useStore($notifications)
34
+ const { t } = useI18n()
35
+ const lastNotificationIdRef = useRef<string | null>(null)
36
+ const [expanded, setExpanded] = useState(false)
37
+ const copy = t.notifications
38
+
39
+ useEffect(() => {
40
+ if (notifications.length <= 1) {
41
+ setExpanded(false)
42
+ }
43
+ }, [notifications.length])
44
+
45
+ useEffect(() => {
46
+ const latest = notifications[0]
47
+
48
+ if (!latest || latest.id === lastNotificationIdRef.current) {
49
+ return
50
+ }
51
+
52
+ lastNotificationIdRef.current = latest.id
53
+
54
+ if (latest.kind === 'success') {
55
+ triggerHaptic('success')
56
+ } else if (latest.kind === 'error') {
57
+ triggerHaptic('error')
58
+ } else if (latest.kind === 'warning') {
59
+ triggerHaptic('warning')
60
+ }
61
+ }, [notifications])
62
+
63
+ if (notifications.length === 0) {
64
+ return null
65
+ }
66
+
67
+ const [latest, ...olderNotifications] = notifications
68
+ const overflowCount = olderNotifications.length
69
+
70
+ // Portaled to <body> with a z above the Radix dialog layer (overlay z-[120],
71
+ // content z-[130]). Without the portal the stack lives inside the React root
72
+ // subtree, which any body-level dialog/overlay portal paints over — so a
73
+ // success toast fired while a dialog is open (or over an OverlayView page)
74
+ // was invisible. The titlebar-height var only exists inside the app shell
75
+ // scope, so fall back to its constant (34px) when mounted on <body>.
76
+ return createPortal(
77
+ <div
78
+ aria-label={copy.region}
79
+ className="pointer-events-none fixed left-1/2 top-[calc(var(--titlebar-height,34px)+0.75rem)] z-[200] flex w-[min(32rem,calc(100%-2rem))] -translate-x-1/2 flex-col gap-2"
80
+ role="region"
81
+ >
82
+ <NotificationItem notification={latest} />
83
+ {expanded && olderNotifications.map(n => <NotificationItem key={n.id} notification={n} />)}
84
+ {overflowCount > 0 && (
85
+ <div className={cn(STACK_SURFACE, 'flex min-h-8 items-center justify-between rounded-lg px-3 text-xs')}>
86
+ <Button className="-ml-2 font-medium" onClick={() => setExpanded(v => !v)} size="xs" type="button" variant="text">
87
+ {expanded ? copy.hide : copy.show} {copy.more(overflowCount)}
88
+ </Button>
89
+ <Button className="-mr-2" onClick={clearNotifications} size="xs" type="button" variant="text">
90
+ {copy.clearAll}
91
+ </Button>
92
+ </div>
93
+ )}
94
+ </div>,
95
+ document.body
96
+ )
97
+ }
98
+
99
+ function NotificationItem({ notification }: { notification: AppNotification }) {
100
+ const styles = tone[notification.kind]
101
+ const Icon = styles.icon
102
+ const hasDetail = Boolean(notification.detail && notification.detail !== notification.message)
103
+ const { t } = useI18n()
104
+ const copy = t.notifications
105
+
106
+ return (
107
+ <Alert
108
+ aria-live={notification.kind === 'error' ? 'assertive' : 'polite'}
109
+ className={cn(STACK_SURFACE, 'grid-cols-[auto_minmax(0,1fr)_auto] pr-2.5')}
110
+ role={notification.kind === 'error' ? 'alert' : 'status'}
111
+ variant="default"
112
+ >
113
+ <Icon className={styles.iconClass} />
114
+ <div className="col-start-2 min-w-0">
115
+ {notification.title && <AlertTitle className="col-start-auto">{notification.title}</AlertTitle>}
116
+ <AlertDescription className="col-start-auto">
117
+ <p className="m-0">{notification.message}</p>
118
+ {hasDetail && <NotificationDetail detail={notification.detail || ''} />}
119
+ {notification.action && (
120
+ <Button
121
+ className="mt-1.5 bg-primary/15 font-medium text-primary hover:bg-primary/25 hover:text-primary"
122
+ onClick={() => {
123
+ notification.action?.onClick()
124
+ dismissNotification(notification.id)
125
+ }}
126
+ size="xs"
127
+ type="button"
128
+ variant="ghost"
129
+ >
130
+ {notification.action.label}
131
+ </Button>
132
+ )}
133
+ </AlertDescription>
134
+ </div>
135
+ <Button
136
+ aria-label={copy.dismiss}
137
+ className="col-start-3 -mr-1 text-muted-foreground"
138
+ onClick={() => dismissNotification(notification.id)}
139
+ size="icon-xs"
140
+ type="button"
141
+ variant="ghost"
142
+ >
143
+ <Codicon name="close" size="0.875rem" />
144
+ </Button>
145
+ </Alert>
146
+ )
147
+ }
148
+
149
+ function NotificationDetail({ detail }: { detail: string }) {
150
+ const { t } = useI18n()
151
+ const copy = t.notifications
152
+
153
+ return (
154
+ <details className="mt-2 text-xs text-muted-foreground">
155
+ <summary className="select-none font-medium text-muted-foreground hover:text-foreground">{copy.details}</summary>
156
+ <div className="mt-1 rounded-md bg-background/65 p-2">
157
+ <pre className="max-h-32 whitespace-pre-wrap wrap-break-word font-mono text-[0.6875rem] leading-relaxed">
158
+ {detail}
159
+ </pre>
160
+ <CopyButton
161
+ appearance="inline"
162
+ className="mt-1 inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-[0.6875rem] text-muted-foreground hover:bg-accent hover:text-foreground"
163
+ errorMessage={copy.copyDetailFailed}
164
+ iconClassName="size-3"
165
+ label={copy.copyDetail}
166
+ text={detail}
167
+ >
168
+ {copy.copyDetail}
169
+ </CopyButton>
170
+ </div>
171
+ </details>
172
+ )
173
+ }
174
+
175
+ export function InlineNotice({
176
+ kind = 'info',
177
+ title,
178
+ children,
179
+ className
180
+ }: {
181
+ kind?: NotificationKind
182
+ title?: string
183
+ children: ReactNode
184
+ className?: string
185
+ }) {
186
+ const styles = tone[kind]
187
+ const Icon = styles.icon
188
+
189
+ return (
190
+ <Alert className={cn('min-w-0', className)} role={kind === 'error' ? 'alert' : 'status'} variant={styles.variant}>
191
+ <Icon />
192
+ {title && <AlertTitle>{title}</AlertTitle>}
193
+ <AlertDescription className={cn(!title && 'row-start-1')}>{children}</AlertDescription>
194
+ </Alert>
195
+ )
196
+ }