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,56 @@
1
+ export interface FieldCopyTree {
2
+ [key: string]: string | FieldCopyTree
3
+ }
4
+
5
+ function schemaSegmentToFieldCopySegment(segment: string): string {
6
+ return segment.replace(/_([a-z0-9])/g, (_, char: string) => char.toUpperCase())
7
+ }
8
+
9
+ function isFieldCopyTree(value: unknown): value is FieldCopyTree {
10
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
11
+ }
12
+
13
+ export function schemaKeyToFieldCopyKey(schemaKey: string): string {
14
+ return schemaKey.split('.').map(schemaSegmentToFieldCopySegment).join('.')
15
+ }
16
+
17
+ export function fieldCopyForSchemaKey(copy: Record<string, string>, schemaKey: string): string | undefined {
18
+ return copy[schemaKeyToFieldCopyKey(schemaKey)] ?? copy[schemaKey]
19
+ }
20
+
21
+ export function defineFieldCopy(copy: FieldCopyTree): Record<string, string> {
22
+ const result: Record<string, string> = {}
23
+
24
+ const visit = (node: FieldCopyTree, prefix: string[] = []) => {
25
+ for (const [key, value] of Object.entries(node)) {
26
+ const parts = key.split('.')
27
+
28
+ if (parts.some(part => part.length === 0)) {
29
+ throw new Error(`Invalid field copy key: ${[...prefix, key].join('.')}`)
30
+ }
31
+
32
+ const path = [...prefix, ...parts]
33
+
34
+ if (typeof value === 'string') {
35
+ const flatKey = path.join('.')
36
+
37
+ if (Object.prototype.hasOwnProperty.call(result, flatKey)) {
38
+ throw new Error(`Duplicate field copy key: ${flatKey}`)
39
+ }
40
+
41
+ result[flatKey] = value
42
+ continue
43
+ }
44
+
45
+ if (!isFieldCopyTree(value)) {
46
+ throw new Error(`Invalid field copy value for key: ${path.join('.')}`)
47
+ }
48
+
49
+ visit(value, path)
50
+ }
51
+ }
52
+
53
+ visit(copy)
54
+
55
+ return result
56
+ }
@@ -0,0 +1,620 @@
1
+ import { useStore } from '@nanostores/react'
2
+ import { useEffect, useMemo, useRef, useState } from 'react'
3
+
4
+ import { Button } from '@/components/ui/button'
5
+ import { Input } from '@/components/ui/input'
6
+ import type { DesktopAuthProvider, DesktopConnectionProbeResult } from '@/global'
7
+ import { useI18n } from '@/i18n'
8
+ import { AlertCircle, Check, FileText, Globe, Loader2, LogIn, Monitor } from '@/lib/icons'
9
+ import { cn } from '@/lib/utils'
10
+ import { notify, notifyError } from '@/store/notifications'
11
+ import { $profiles, refreshActiveProfile } from '@/store/profile'
12
+
13
+ import { CONTROL_TEXT } from './constants'
14
+ import { EmptyState, ListRow, LoadingState, Pill, SettingsContent } from './primitives'
15
+
16
+ type Mode = 'local' | 'remote'
17
+ type AuthMode = 'oauth' | 'token'
18
+ type ProbeStatus = 'idle' | 'probing' | 'done' | 'error'
19
+
20
+ interface GatewaySettingsState {
21
+ envOverride: boolean
22
+ mode: Mode
23
+ remoteAuthMode: AuthMode
24
+ remoteOauthConnected: boolean
25
+ remoteTokenPreview: string | null
26
+ remoteTokenSet: boolean
27
+ remoteUrl: string
28
+ }
29
+
30
+ const EMPTY_STATE: GatewaySettingsState = {
31
+ envOverride: false,
32
+ mode: 'local',
33
+ remoteAuthMode: 'token',
34
+ remoteOauthConnected: false,
35
+ remoteTokenPreview: null,
36
+ remoteTokenSet: false,
37
+ remoteUrl: ''
38
+ }
39
+
40
+ function ModeCard({
41
+ active,
42
+ description,
43
+ disabled,
44
+ icon: Icon,
45
+ onSelect,
46
+ title
47
+ }: {
48
+ active: boolean
49
+ description: string
50
+ disabled?: boolean
51
+ icon: typeof Monitor
52
+ onSelect: () => void
53
+ title: string
54
+ }) {
55
+ return (
56
+ <button
57
+ className={cn(
58
+ 'rounded-xl border p-3 text-left transition',
59
+ active
60
+ ? 'border-(--ui-stroke-secondary) bg-(--ui-bg-tertiary)'
61
+ : 'border-(--ui-stroke-tertiary) bg-(--ui-bg-quinary) hover:bg-(--chrome-action-hover)',
62
+ disabled && 'cursor-not-allowed opacity-50'
63
+ )}
64
+ disabled={disabled}
65
+ onClick={onSelect}
66
+ type="button"
67
+ >
68
+ <div className="flex items-center gap-2 text-[length:var(--conversation-text-font-size)] font-medium">
69
+ <Icon className="size-4 text-muted-foreground" />
70
+ <span>{title}</span>
71
+ {active ? <Check className="ml-auto size-4 text-primary" /> : null}
72
+ </div>
73
+ <p className="mt-1.5 text-[length:var(--conversation-caption-font-size)] leading-(--conversation-caption-line-height) text-(--ui-text-tertiary)">
74
+ {description}
75
+ </p>
76
+ </button>
77
+ )
78
+ }
79
+
80
+ function ScopeChip({ active, label, onSelect }: { active: boolean; label: string; onSelect: () => void }) {
81
+ return (
82
+ <button
83
+ className={cn(
84
+ 'rounded-full border px-3 py-1 text-[length:var(--conversation-caption-font-size)] transition',
85
+ active
86
+ ? 'border-(--ui-stroke-secondary) bg-(--ui-bg-tertiary) text-(--ui-text-primary)'
87
+ : 'border-(--ui-stroke-tertiary) bg-(--ui-bg-quinary) text-(--ui-text-tertiary) hover:bg-(--chrome-action-hover)'
88
+ )}
89
+ onClick={onSelect}
90
+ type="button"
91
+ >
92
+ {label}
93
+ </button>
94
+ )
95
+ }
96
+
97
+ export function GatewaySettings() {
98
+ const { t } = useI18n()
99
+ const g = t.settings.gateway
100
+ const [loading, setLoading] = useState(true)
101
+ const [saving, setSaving] = useState(false)
102
+ const [testing, setTesting] = useState(false)
103
+ const [signingIn, setSigningIn] = useState(false)
104
+ const [state, setState] = useState<GatewaySettingsState>(EMPTY_STATE)
105
+ const [remoteToken, setRemoteToken] = useState('')
106
+ const [lastTest, setLastTest] = useState<null | string>(null)
107
+
108
+ // Connection scope: null = the global/default connection (the original
109
+ // behavior); a profile name = that profile's per-profile remote override, so
110
+ // each profile can point at its own backend.
111
+ const [scope, setScope] = useState<null | string>(null)
112
+ const profiles = useStore($profiles)
113
+
114
+ useEffect(() => {
115
+ void refreshActiveProfile()
116
+ }, [])
117
+
118
+ // Auth-mode probe: as the user types a remote URL we ask the gateway (via
119
+ // its public /api/status) whether it gates with OAuth or a static session
120
+ // token, so we can show the right control (login button vs token box).
121
+ const [probeStatus, setProbeStatus] = useState<ProbeStatus>('idle')
122
+ const [probe, setProbe] = useState<DesktopConnectionProbeResult | null>(null)
123
+ const probeSeq = useRef(0)
124
+
125
+ useEffect(() => {
126
+ let cancelled = false
127
+ const desktop = window.NASTECHDesktop
128
+
129
+ if (!desktop?.getConnectionConfig) {
130
+ setLoading(false)
131
+
132
+ return () => void (cancelled = true)
133
+ }
134
+
135
+ setLoading(true)
136
+ // Clear scope-local entry state so a token from one scope can't leak into
137
+ // the next when switching profiles.
138
+ setRemoteToken('')
139
+ setLastTest(null)
140
+
141
+ desktop
142
+ .getConnectionConfig(scope)
143
+ .then(config => {
144
+ if (cancelled) {
145
+ return
146
+ }
147
+
148
+ setState(config)
149
+ })
150
+ .catch(err => notifyError(err, g.failedLoad))
151
+ .finally(() => {
152
+ if (!cancelled) {
153
+ setLoading(false)
154
+ }
155
+ })
156
+
157
+ return () => void (cancelled = true)
158
+ }, [scope])
159
+
160
+ // Debounced probe of the entered remote URL. Only runs in remote mode with a
161
+ // syntactically plausible URL. The probe result drives whether we render the
162
+ // OAuth login button or the session-token entry box. The effective auth mode
163
+ // prefers a fresh probe result over the saved value.
164
+ const trimmedUrl = state.remoteUrl.trim()
165
+ useEffect(() => {
166
+ if (state.mode !== 'remote' || !trimmedUrl || !/^https?:\/\//i.test(trimmedUrl)) {
167
+ setProbeStatus('idle')
168
+ setProbe(null)
169
+
170
+ return
171
+ }
172
+
173
+ const desktop = window.NASTECHDesktop
174
+
175
+ if (!desktop?.probeConnectionConfig) {
176
+ return
177
+ }
178
+
179
+ const seq = ++probeSeq.current
180
+ setProbeStatus('probing')
181
+
182
+ const timer = setTimeout(() => {
183
+ desktop
184
+ .probeConnectionConfig(trimmedUrl)
185
+ .then(result => {
186
+ if (seq !== probeSeq.current) {
187
+ return
188
+ }
189
+
190
+ setProbe(result)
191
+ setProbeStatus(result.reachable ? 'done' : 'error')
192
+ })
193
+ .catch(() => {
194
+ if (seq !== probeSeq.current) {
195
+ return
196
+ }
197
+
198
+ setProbe(null)
199
+ setProbeStatus('error')
200
+ })
201
+ }, 500)
202
+
203
+ return () => clearTimeout(timer)
204
+ }, [state.mode, trimmedUrl])
205
+
206
+ // Effective auth mode: a reachable probe wins; otherwise fall back to the
207
+ // saved config's mode so a re-open of settings doesn't flicker.
208
+ const authMode: AuthMode = useMemo(() => {
209
+ if (probeStatus === 'done' && probe && probe.authMode !== 'unknown') {
210
+ return probe.authMode
211
+ }
212
+
213
+ return state.remoteAuthMode
214
+ }, [probe, probeStatus, state.remoteAuthMode])
215
+
216
+ // Whether we actually KNOW how this gateway authenticates yet. Until we do,
217
+ // neither the OAuth button nor the session-token box should render —
218
+ // `authMode` defaults to 'token', so without this gate the token box flashes
219
+ // for every gateway (including OAuth ones) during the idle/probing window
220
+ // before the first probe lands. The scheme is known when either:
221
+ // * the live probe finished (probeStatus 'done'), or
222
+ // * we're idle but showing a previously-saved remote config (re-opening
223
+ // settings for a gateway already signed-in or with a saved token), so
224
+ // its control appears immediately with no flicker.
225
+ // While probing (or after a probe error), the scheme is unknown and we show
226
+ // the probe status row instead of a control.
227
+ const hasSavedRemote = state.remoteTokenSet || state.remoteOauthConnected
228
+
229
+ const authResolved = useMemo(() => {
230
+ if (probeStatus === 'done') {
231
+ return true
232
+ }
233
+
234
+ return probeStatus === 'idle' && hasSavedRemote
235
+ }, [probeStatus, hasSavedRemote])
236
+
237
+ const providerLabel = useMemo(() => {
238
+ const providers: DesktopAuthProvider[] = probe?.providers ?? []
239
+
240
+ if (providers.length === 1) {
241
+ return providers[0].displayName || providers[0].name
242
+ }
243
+
244
+ if (providers.length > 1) {
245
+ return providers.map(p => p.displayName || p.name).join(' / ')
246
+ }
247
+
248
+ return t.boot.failure.identityProvider
249
+ }, [probe, t.boot.failure.identityProvider])
250
+
251
+ // A username/password gateway authenticates through a credential form on the
252
+ // gateway's /login page (POST /auth/password-login) rather than an OAuth
253
+ // redirect. Everything downstream — the session cookie, the ws-ticket mint,
254
+ // the persistent partition — is identical, so the desktop drives it through
255
+ // the same sign-in window; only the button copy changes. We treat the
256
+ // gateway as password-style only when EVERY advertised provider supports
257
+ // password, so a mixed deployment keeps the generic OAuth copy.
258
+ const isPasswordProvider = useMemo(() => {
259
+ const providers: DesktopAuthProvider[] = probe?.providers ?? []
260
+
261
+ return providers.length > 0 && providers.every(p => p.supportsPassword)
262
+ }, [probe])
263
+
264
+ // The 'default' profile uses the global ("All profiles") connection, so the
265
+ // per-profile scopes are the named, non-default profiles.
266
+ const namedProfiles = useMemo(() => profiles.filter(profile => profile.name !== 'default'), [profiles])
267
+
268
+ const oauthConnected = state.remoteOauthConnected
269
+
270
+ const canUseRemote = useMemo(() => {
271
+ if (!trimmedUrl) {
272
+ return false
273
+ }
274
+
275
+ if (authMode === 'oauth') {
276
+ return oauthConnected
277
+ }
278
+
279
+ return Boolean(remoteToken.trim()) || state.remoteTokenSet
280
+ }, [authMode, oauthConnected, remoteToken, state.remoteTokenSet, trimmedUrl])
281
+
282
+ const payload = () => ({
283
+ mode: state.mode,
284
+ profile: scope ?? undefined,
285
+ remoteAuthMode: authMode,
286
+ remoteToken: authMode === 'token' ? remoteToken.trim() || undefined : undefined,
287
+ remoteUrl: trimmedUrl
288
+ })
289
+
290
+ const save = async (apply: boolean) => {
291
+ if (state.mode === 'remote' && !canUseRemote) {
292
+ notify({
293
+ kind: 'warning',
294
+ title: g.incompleteTitle,
295
+ message:
296
+ authMode === 'oauth'
297
+ ? g.incompleteSignIn
298
+ : g.incompleteToken
299
+ })
300
+
301
+ return
302
+ }
303
+
304
+ setSaving(true)
305
+
306
+ try {
307
+ const next = apply
308
+ ? await window.NASTECHDesktop.applyConnectionConfig(payload())
309
+ : await window.NASTECHDesktop.saveConnectionConfig(payload())
310
+
311
+ setState(next)
312
+ setRemoteToken('')
313
+ notify({
314
+ kind: 'success',
315
+ title: apply ? g.restartingTitle : g.savedTitle,
316
+ message: apply ? g.restartingMessage : g.savedMessage
317
+ })
318
+ } catch (err) {
319
+ notifyError(err, apply ? g.applyFailed : g.saveFailed)
320
+ } finally {
321
+ setSaving(false)
322
+ }
323
+ }
324
+
325
+ // OAuth sign-in: persist the URL + oauth mode first (so the saved config has
326
+ // the URL the login window needs), then open the gateway login window and
327
+ // refresh the connection status from the saved config once it completes.
328
+ const signIn = async () => {
329
+ if (!trimmedUrl) {
330
+ notify({ kind: 'warning', title: g.incompleteTitle, message: g.enterUrlFirst })
331
+
332
+ return
333
+ }
334
+
335
+ setSigningIn(true)
336
+
337
+ try {
338
+ // Save (don't apply/restart) so the login window has a URL to use and the
339
+ // oauth mode is persisted, without yet flipping the live connection.
340
+ const saved = await window.NASTECHDesktop.saveConnectionConfig({
341
+ mode: state.mode,
342
+ profile: scope ?? undefined,
343
+ remoteAuthMode: 'oauth',
344
+ remoteUrl: trimmedUrl
345
+ })
346
+
347
+ setState(saved)
348
+
349
+ const result = await window.NASTECHDesktop.oauthLoginConnectionConfig(trimmedUrl)
350
+
351
+ if (result.connected) {
352
+ const refreshed = await window.NASTECHDesktop.getConnectionConfig(scope)
353
+ setState(refreshed)
354
+ notify({ kind: 'success', title: g.signedIn, message: g.connectedTo(providerLabel) })
355
+ } else {
356
+ notify({
357
+ kind: 'warning',
358
+ title: t.boot.failure.signInIncompleteTitle,
359
+ message: t.boot.failure.signInIncompleteMessage
360
+ })
361
+ }
362
+ } catch (err) {
363
+ notifyError(err, g.signInFailed)
364
+ } finally {
365
+ setSigningIn(false)
366
+ }
367
+ }
368
+
369
+ const signOut = async () => {
370
+ setSigningIn(true)
371
+
372
+ try {
373
+ await window.NASTECHDesktop.oauthLogoutConnectionConfig(trimmedUrl || undefined)
374
+ const refreshed = await window.NASTECHDesktop.getConnectionConfig(scope)
375
+ setState(refreshed)
376
+ notify({ kind: 'success', title: g.signedOutTitle, message: g.signedOutMessage })
377
+ } catch (err) {
378
+ notifyError(err, g.signOutFailed)
379
+ } finally {
380
+ setSigningIn(false)
381
+ }
382
+ }
383
+
384
+ const testRemote = async () => {
385
+ if (!canUseRemote) {
386
+ notify({
387
+ kind: 'warning',
388
+ title: g.incompleteTitle,
389
+ message:
390
+ authMode === 'oauth'
391
+ ? g.incompleteSignInTest
392
+ : g.incompleteTokenTest
393
+ })
394
+
395
+ return
396
+ }
397
+
398
+ setTesting(true)
399
+ setLastTest(null)
400
+
401
+ try {
402
+ const result = await window.NASTECHDesktop.testConnectionConfig({
403
+ mode: 'remote',
404
+ profile: scope ?? undefined,
405
+ remoteAuthMode: authMode,
406
+ remoteToken: authMode === 'token' ? remoteToken.trim() || undefined : undefined,
407
+ remoteUrl: trimmedUrl
408
+ })
409
+
410
+ const message = g.connectedTo(result.baseUrl, result.version ?? undefined)
411
+ setLastTest(message)
412
+ notify({ kind: 'success', title: g.reachableTitle, message })
413
+ } catch (err) {
414
+ notifyError(err, g.testFailed)
415
+ } finally {
416
+ setTesting(false)
417
+ }
418
+ }
419
+
420
+ if (loading) {
421
+ return <LoadingState label={g.loading} />
422
+ }
423
+
424
+ if (!window.NASTECHDesktop?.getConnectionConfig) {
425
+ return (
426
+ <EmptyState
427
+ description={g.unavailableDesc}
428
+ title={g.unavailableTitle}
429
+ />
430
+ )
431
+ }
432
+
433
+ return (
434
+ <SettingsContent>
435
+ <div className="mb-5">
436
+ <div className="flex items-center gap-2 text-[length:var(--conversation-text-font-size)] font-medium">
437
+ <Globe className="size-4 text-muted-foreground" />
438
+ {g.title}
439
+ {state.envOverride ? <Pill tone="primary">{g.envOverride}</Pill> : null}
440
+ </div>
441
+ <p className="mt-2 max-w-2xl text-[length:var(--conversation-caption-font-size)] leading-(--conversation-caption-line-height) text-(--ui-text-tertiary)">
442
+ {g.intro}
443
+ </p>
444
+ </div>
445
+
446
+ {namedProfiles.length > 0 ? (
447
+ <div className="mb-5 grid gap-2">
448
+ <div className="text-[length:var(--conversation-caption-font-size)] font-medium text-(--ui-text-secondary)">
449
+ {g.appliesTo}
450
+ </div>
451
+ <div className="flex flex-wrap gap-1.5">
452
+ <ScopeChip active={scope === null} label={g.allProfiles} onSelect={() => setScope(null)} />
453
+ {namedProfiles.map(profile => (
454
+ <ScopeChip
455
+ active={scope === profile.name}
456
+ key={profile.name}
457
+ label={profile.name}
458
+ onSelect={() => setScope(profile.name)}
459
+ />
460
+ ))}
461
+ </div>
462
+ <p className="text-[length:var(--conversation-caption-font-size)] leading-(--conversation-caption-line-height) text-(--ui-text-tertiary)">
463
+ {scope === null ? g.defaultConnection : g.profileConnection(scope)}
464
+ </p>
465
+ </div>
466
+ ) : null}
467
+
468
+ {state.envOverride ? (
469
+ <div className="mb-5 flex items-start gap-2 rounded-xl border border-destructive/30 bg-destructive/10 px-3 py-2.5 text-[length:var(--conversation-caption-font-size)] text-destructive">
470
+ <AlertCircle className="mt-0.5 size-4 shrink-0" />
471
+ <div>
472
+ <div className="font-medium">{g.envOverrideTitle}</div>
473
+ <div className="mt-1 leading-5">
474
+ {g.envOverrideDesc}
475
+ </div>
476
+ </div>
477
+ </div>
478
+ ) : null}
479
+
480
+ <div className="grid gap-3 sm:grid-cols-2">
481
+ <ModeCard
482
+ active={state.mode === 'local'}
483
+ description={g.localDesc}
484
+ disabled={state.envOverride}
485
+ icon={Monitor}
486
+ onSelect={() => setState(current => ({ ...current, mode: 'local' }))}
487
+ title={g.localTitle}
488
+ />
489
+ <ModeCard
490
+ active={state.mode === 'remote'}
491
+ description={g.remoteDesc}
492
+ disabled={state.envOverride}
493
+ icon={Globe}
494
+ onSelect={() => setState(current => ({ ...current, mode: 'remote' }))}
495
+ title={g.remoteTitle}
496
+ />
497
+ </div>
498
+
499
+ <div className="mt-5 grid gap-1">
500
+ <ListRow
501
+ action={
502
+ <Input
503
+ className={cn('h-8', CONTROL_TEXT)}
504
+ disabled={state.envOverride}
505
+ onChange={event => setState(current => ({ ...current, remoteUrl: event.target.value }))}
506
+ placeholder="https://gateway.example.com/NASTECH"
507
+ value={state.remoteUrl}
508
+ />
509
+ }
510
+ description={g.remoteUrlDesc}
511
+ title={g.remoteUrlTitle}
512
+ />
513
+
514
+ {state.mode === 'remote' && probeStatus === 'probing' ? (
515
+ <div className="flex items-center gap-2 py-3 text-[length:var(--conversation-caption-font-size)] text-(--ui-text-tertiary)">
516
+ <Loader2 className="size-4 animate-spin" />
517
+ {g.probing}
518
+ </div>
519
+ ) : null}
520
+
521
+ {state.mode === 'remote' && probeStatus === 'error' ? (
522
+ <div className="flex items-start gap-2 py-3 text-[length:var(--conversation-caption-font-size)] text-(--ui-text-tertiary)">
523
+ <AlertCircle className="mt-0.5 size-4 shrink-0" />
524
+ {g.probeError}
525
+ </div>
526
+ ) : null}
527
+
528
+ {/* OAuth / password gateways: present a sign-in button + connection status. */}
529
+ {state.mode === 'remote' && authResolved && authMode === 'oauth' ? (
530
+ <ListRow
531
+ action={
532
+ oauthConnected ? (
533
+ <div className="flex items-center gap-2">
534
+ <Pill tone="primary">
535
+ <Check className="size-3" /> {g.signedIn}
536
+ </Pill>
537
+ <Button disabled={signingIn || state.envOverride} onClick={() => void signOut()} variant="outline">
538
+ {signingIn ? <Loader2 className="animate-spin" /> : null}
539
+ {g.signOut}
540
+ </Button>
541
+ </div>
542
+ ) : (
543
+ <Button disabled={signingIn || state.envOverride || !trimmedUrl} onClick={() => void signIn()}>
544
+ {signingIn ? <Loader2 className="animate-spin" /> : <LogIn />}
545
+ {isPasswordProvider ? g.signIn : g.signInWith(providerLabel)}
546
+ </Button>
547
+ )
548
+ }
549
+ description={
550
+ oauthConnected
551
+ ? isPasswordProvider
552
+ ? g.authSignedInPassword
553
+ : g.authSignedInOauth
554
+ : isPasswordProvider
555
+ ? g.authNeedsPassword
556
+ : g.authNeedsOauth(providerLabel)
557
+ }
558
+ title={g.authTitle}
559
+ />
560
+ ) : null}
561
+
562
+ {/* Session-token gateways: keep the existing token entry box. */}
563
+ {state.mode === 'remote' && authResolved && authMode === 'token' ? (
564
+ <ListRow
565
+ action={
566
+ <Input
567
+ autoComplete="off"
568
+ className={cn('h-8 font-mono', CONTROL_TEXT)}
569
+ disabled={state.envOverride}
570
+ onChange={event => setRemoteToken(event.target.value)}
571
+ placeholder={
572
+ state.remoteTokenSet ? g.existingToken(state.remoteTokenPreview ?? g.savedToken) : g.pasteSessionToken
573
+ }
574
+ type="password"
575
+ value={remoteToken}
576
+ />
577
+ }
578
+ description={g.tokenDesc}
579
+ title={g.tokenTitle}
580
+ />
581
+ ) : null}
582
+ </div>
583
+
584
+ {lastTest ? <div className="mt-4 text-xs text-primary">{lastTest}</div> : null}
585
+
586
+ <div className="mt-6 flex flex-wrap items-center justify-end gap-4">
587
+ <Button
588
+ className="mr-auto"
589
+ disabled={state.envOverride || testing || !canUseRemote}
590
+ onClick={() => void testRemote()}
591
+ size="sm"
592
+ variant="text"
593
+ >
594
+ {testing ? <Loader2 className="animate-spin" /> : null}
595
+ {g.testRemote}
596
+ </Button>
597
+ <Button disabled={state.envOverride || saving} onClick={() => void save(false)} size="sm" variant="textStrong">
598
+ {g.saveForRestart}
599
+ </Button>
600
+ <Button disabled={state.envOverride || saving} onClick={() => void save(true)} size="sm">
601
+ {saving ? <Loader2 className="animate-spin" /> : null}
602
+ {g.saveAndReconnect}
603
+ </Button>
604
+ </div>
605
+
606
+ <div className="mt-6 grid gap-1">
607
+ <ListRow
608
+ action={
609
+ <Button onClick={() => void window.NASTECHDesktop?.revealLogs()} size="sm" variant="textStrong">
610
+ <FileText />
611
+ {g.openLogs}
612
+ </Button>
613
+ }
614
+ description={g.diagnosticsDesc}
615
+ title={g.diagnostics}
616
+ />
617
+ </div>
618
+ </SettingsContent>
619
+ )
620
+ }