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,595 @@
1
+ import { useEffect, useMemo, useRef, useState } from 'react'
2
+
3
+ import { Button } from '@/components/ui/button'
4
+ import { Loader } from '@/components/ui/loader'
5
+ import { LogView } from '@/components/ui/log-view'
6
+ import type {
7
+ DesktopBootstrapEvent,
8
+ DesktopBootstrapStageDescriptor,
9
+ DesktopBootstrapStageResult,
10
+ DesktopBootstrapStageState,
11
+ DesktopBootstrapState
12
+ } from '@/global'
13
+ import { useI18n } from '@/i18n'
14
+ import { AlertTriangle, Check, ChevronDown, ChevronRight, Loader2 } from '@/lib/icons'
15
+ import { cn } from '@/lib/utils'
16
+
17
+ /**
18
+ * DesktopInstallOverlay
19
+ *
20
+ * Renders the first-launch install progress for NasTech Agent. Mounted always;
21
+ * shows itself only when main.cjs reports an in-flight bootstrap (state.active)
22
+ * OR an error from a completed-failed bootstrap (state.error). When the
23
+ * bootstrap finishes successfully the overlay fades out and the rest of the
24
+ * app (existing onboarding overlay -> main UI) takes over.
25
+ *
26
+ * Subscribes to two channels:
27
+ * - getBootstrapState() -- initial snapshot on mount
28
+ * - onBootstrapEvent(callback) -- live event stream
29
+ *
30
+ * The reducer is intentionally simple: every event mutates an in-component
31
+ * snapshot the same way main.cjs mutates its server-side snapshot. We don't
32
+ * try to reconcile -- if we miss an event (shouldn't happen) the initial
33
+ * getBootstrapState() call will resync the picture on the next render.
34
+ *
35
+ * Stages flagged needs_user_input render with a deliberately subdued style:
36
+ * they're expected to come back as skipped=true (install.ps1 short-circuits
37
+ * them under -NonInteractive). The post-install configuration flow that
38
+ * those stages cover (API key, model, persona, gateway autostart) is handled
39
+ * by the existing DesktopOnboardingOverlay, NOT by the install overlay.
40
+ */
41
+
42
+ interface DesktopInstallOverlayProps {
43
+ /** When false, the overlay never renders -- useful for dev when we want
44
+ * to suppress it entirely. */
45
+ enabled?: boolean
46
+ }
47
+
48
+ interface StageRowProps {
49
+ descriptor: DesktopBootstrapStageDescriptor
50
+ result: DesktopBootstrapStageResult | undefined
51
+ isCurrent: boolean
52
+ now: number
53
+ }
54
+
55
+ function formatStageName(name: string): string {
56
+ // 'system-packages' -> 'System packages'; 'uv' stays 'uv'
57
+ if (name.length <= 3) {
58
+ return name
59
+ }
60
+
61
+ return name
62
+ .split('-')
63
+ .map((word, i) => (i === 0 ? word.charAt(0).toUpperCase() + word.slice(1) : word))
64
+ .join(' ')
65
+ }
66
+
67
+ function formatDuration(ms: number | null | undefined): string {
68
+ if (typeof ms !== 'number' || !Number.isFinite(ms)) {
69
+ return ''
70
+ }
71
+
72
+ if (ms < 1000) {
73
+ return `${ms} ms`
74
+ }
75
+
76
+ const s = ms / 1000
77
+
78
+ if (s < 60) {
79
+ return `${s.toFixed(1)}s`
80
+ }
81
+
82
+ const m = Math.floor(s / 60)
83
+ const rs = Math.round(s - m * 60)
84
+
85
+ return `${m}m ${rs}s`
86
+ }
87
+
88
+ // Live elapsed for a running stage, as m:ss (or s for sub-minute).
89
+ function formatElapsed(ms: number): string {
90
+ const s = Math.max(0, Math.floor(ms / 1000))
91
+
92
+ if (s < 60) {
93
+ return `${s}s`
94
+ }
95
+
96
+ const m = Math.floor(s / 60)
97
+
98
+ return `${m}:${String(s - m * 60).padStart(2, '0')}`
99
+ }
100
+
101
+ function StageRow({ descriptor, result, isCurrent, now }: StageRowProps) {
102
+ const { t } = useI18n()
103
+ const copy = t.install
104
+ const state: DesktopBootstrapStageState = result?.state || 'pending'
105
+
106
+ const elapsed =
107
+ state === 'running' && typeof result?.startedAt === 'number' ? formatElapsed(now - result.startedAt) : ''
108
+
109
+ const icon = useMemo(() => {
110
+ switch (state) {
111
+ case 'running':
112
+ return <Loader2 className="h-4 w-4 animate-spin text-primary" />
113
+
114
+ case 'succeeded':
115
+ return <Check className="h-4 w-4 text-emerald-600" />
116
+
117
+ case 'skipped':
118
+ return <Check className="h-4 w-4 text-muted-foreground" />
119
+
120
+ case 'failed':
121
+ return <AlertTriangle className="h-4 w-4 text-destructive" />
122
+
123
+ case 'pending':
124
+
125
+ default:
126
+ return <div className="h-2 w-2 rounded-full border border-muted-foreground/40" />
127
+ }
128
+ }, [state])
129
+
130
+ const reason = result?.json?.reason || result?.error || null
131
+
132
+ return (
133
+ <li
134
+ className={cn(
135
+ 'flex items-start gap-3 rounded-md px-3 py-2 transition-colors',
136
+ isCurrent && 'bg-muted/60',
137
+ state === 'failed' && 'bg-destructive/10'
138
+ )}
139
+ >
140
+ <div className="flex h-5 w-5 flex-shrink-0 items-center justify-center">{icon}</div>
141
+ <div className="min-w-0 flex-1">
142
+ <div className="flex items-baseline justify-between gap-2">
143
+ <span className={cn('truncate text-sm font-medium', state === 'pending' && 'text-muted-foreground')}>
144
+ {formatStageName(descriptor.name)}
145
+ </span>
146
+ <span className="flex-shrink-0 text-xs tabular-nums text-muted-foreground">
147
+ {state === 'running'
148
+ ? elapsed
149
+ ? `${copy.stageStates[state]} · ${elapsed}`
150
+ : copy.stageStates[state]
151
+ : null}
152
+ {state === 'succeeded' || state === 'skipped' ? formatDuration(result?.durationMs) : null}
153
+ {state === 'failed' ? copy.stageStates[state] : null}
154
+ </span>
155
+ </div>
156
+ {reason && state !== 'pending' && <p className="mt-0.5 truncate text-xs text-muted-foreground">{reason}</p>}
157
+ </div>
158
+ </li>
159
+ )
160
+ }
161
+
162
+ const EMPTY_STATE: DesktopBootstrapState = {
163
+ active: false,
164
+ manifest: null,
165
+ stages: {},
166
+ error: null,
167
+ log: [],
168
+ startedAt: null,
169
+ completedAt: null,
170
+ unsupportedPlatform: null
171
+ }
172
+
173
+ function applyEvent(state: DesktopBootstrapState, ev: DesktopBootstrapEvent): DesktopBootstrapState {
174
+ if (ev.type === 'manifest') {
175
+ const stages: Record<string, DesktopBootstrapStageResult> = {}
176
+
177
+ for (const stage of ev.stages) {
178
+ stages[stage.name] = { state: 'pending', durationMs: null, startedAt: null, json: null, error: null }
179
+ }
180
+
181
+ return {
182
+ ...state,
183
+ active: true,
184
+ manifest: { type: 'manifest', stages: ev.stages, protocolVersion: ev.protocolVersion },
185
+ stages,
186
+ error: null,
187
+ startedAt: state.startedAt || Date.now()
188
+ }
189
+ }
190
+
191
+ if (ev.type === 'stage') {
192
+ const prev = state.stages[ev.name]
193
+
194
+ return {
195
+ ...state,
196
+ stages: {
197
+ ...state.stages,
198
+ [ev.name]: {
199
+ state: ev.state,
200
+ durationMs: ev.durationMs ?? null,
201
+ // Stamp the start time on the running transition so the UI can show
202
+ // a live elapsed timer; preserve it across repeated running events.
203
+ startedAt: ev.state === 'running' ? (prev?.startedAt ?? Date.now()) : (prev?.startedAt ?? null),
204
+ json: ev.json ?? null,
205
+ error: ev.error ?? null
206
+ }
207
+ }
208
+ }
209
+ }
210
+
211
+ if (ev.type === 'log') {
212
+ const next = state.log.concat({ ts: Date.now(), stage: ev.stage ?? null, line: ev.line, stream: ev.stream })
213
+
214
+ while (next.length > 500) {
215
+ next.shift()
216
+ }
217
+
218
+ return { ...state, log: next }
219
+ }
220
+
221
+ if (ev.type === 'complete') {
222
+ return { ...state, active: false, completedAt: Date.now(), error: null }
223
+ }
224
+
225
+ if (ev.type === 'failed') {
226
+ return { ...state, active: false, error: ev.error || 'unknown error' }
227
+ }
228
+
229
+ if (ev.type === 'unsupported-platform') {
230
+ return {
231
+ ...state,
232
+ active: false,
233
+ unsupportedPlatform: {
234
+ platform: ev.platform,
235
+ activeRoot: ev.activeRoot,
236
+ installCommand: ev.installCommand,
237
+ docsUrl: ev.docsUrl
238
+ }
239
+ }
240
+ }
241
+
242
+ return state
243
+ }
244
+
245
+ export function DesktopInstallOverlay({ enabled = true }: DesktopInstallOverlayProps) {
246
+ const { t } = useI18n()
247
+ const copy = t.install
248
+ const [state, setState] = useState<DesktopBootstrapState>(EMPTY_STATE)
249
+ const [logOpen, setLogOpen] = useState(false)
250
+ const [copied, setCopied] = useState(false)
251
+ const [cancelling, setCancelling] = useState(false)
252
+ const [now, setNow] = useState(() => Date.now())
253
+ const logEndRef = useRef<HTMLDivElement | null>(null)
254
+
255
+ // Tick once a second while a bootstrap is in flight so running steps show a
256
+ // live elapsed timer. Stops when nothing is active to avoid idle renders.
257
+ useEffect(() => {
258
+ if (!state.active) {
259
+ return
260
+ }
261
+
262
+ const id = window.setInterval(() => setNow(Date.now()), 1000)
263
+
264
+ return () => window.clearInterval(id)
265
+ }, [state.active])
266
+
267
+ // Subscribe to bootstrap events + load initial snapshot
268
+ useEffect(() => {
269
+ if (!enabled) {
270
+ return
271
+ }
272
+
273
+ const desktop = window.NASTECHDesktop
274
+
275
+ if (!desktop || typeof desktop.onBootstrapEvent !== 'function') {
276
+ return
277
+ }
278
+
279
+ let cancelled = false
280
+
281
+ desktop
282
+ .getBootstrapState()
283
+ .then(snapshot => {
284
+ if (!cancelled && snapshot) {
285
+ setState(snapshot)
286
+ }
287
+ })
288
+ .catch(() => {
289
+ // Older Electron build without the IPC handler -- bootstrap UI just
290
+ // stays empty, app falls through to existing onboarding flow.
291
+ })
292
+
293
+ const off = desktop.onBootstrapEvent(ev => setState(prev => applyEvent(prev, ev)))
294
+
295
+ return () => {
296
+ cancelled = true
297
+ off?.()
298
+ }
299
+ }, [enabled])
300
+
301
+ // Autoscroll log to bottom when new lines arrive AND the log is open
302
+ useEffect(() => {
303
+ if (logOpen && logEndRef.current) {
304
+ logEndRef.current.scrollIntoView({ behavior: 'auto', block: 'end' })
305
+ }
306
+ }, [state.log.length, logOpen])
307
+
308
+ // Auto-expand the log panel when a bootstrap fails so the user immediately
309
+ // sees the install.ps1 output. Without this, the failure block shows just
310
+ // the top-level error message and the user has to click "Show installer
311
+ // output" to see WHY the stage failed.
312
+ useEffect(() => {
313
+ if (state.error) {
314
+ setLogOpen(true)
315
+ }
316
+ }, [state.error])
317
+
318
+ // Mount logic: show whenever a bootstrap is in flight, completed-with-error,
319
+ // or actively running with a manifest. Hide entirely after a successful
320
+ // completion so the rest of the UI can take over.
321
+ const shouldShow = useMemo(() => {
322
+ if (!enabled) {
323
+ return false
324
+ }
325
+
326
+ if (state.active) {
327
+ return true
328
+ }
329
+
330
+ if (state.error) {
331
+ return true
332
+ }
333
+
334
+ if (state.unsupportedPlatform) {
335
+ return true
336
+ }
337
+
338
+ return false
339
+ }, [enabled, state.active, state.error, state.unsupportedPlatform])
340
+
341
+ if (!shouldShow) {
342
+ return null
343
+ }
344
+
345
+ // Unsupported-platform branch: macOS/Linux packaged builds hit this when
346
+ // there's no NasTech Agent installed yet and we can't drive install.sh
347
+ // (no stage protocol equivalent yet). Show a copy-paste install command
348
+ // and the docs URL; user runs it from Terminal and relaunches the app.
349
+ if (state.unsupportedPlatform) {
350
+ const ups = state.unsupportedPlatform
351
+ const platformLabel = ups.platform === 'darwin' ? 'macOS' : ups.platform === 'linux' ? 'Linux' : ups.platform
352
+
353
+ return (
354
+ <div className="fixed inset-0 z-[1400] flex items-center justify-center bg-background/90 backdrop-blur-md">
355
+ <div className="w-full max-w-xl rounded-xl border border-(--stroke-nastech) bg-card p-8 shadow-nastech">
356
+ <h2 className="text-2xl font-semibold tracking-tight">{copy.oneTimeTitle}</h2>
357
+ <p className="mt-2 text-sm text-muted-foreground">
358
+ {copy.unsupportedDesc(platformLabel)}
359
+ </p>
360
+
361
+ <div className="mt-4">
362
+ <div className="mb-1.5 text-xs font-medium text-muted-foreground">{copy.installCommand}</div>
363
+ <pre className="overflow-x-auto rounded-md border bg-muted/50 px-3 py-2.5 font-mono text-[12px]">
364
+ <code>{ups.installCommand}</code>
365
+ </pre>
366
+ <div className="mt-2 flex items-center gap-2">
367
+ <Button
368
+ onClick={() => {
369
+ void navigator.clipboard?.writeText(ups.installCommand).catch(() => {})
370
+ }}
371
+ size="sm"
372
+ variant="secondary"
373
+ >
374
+ {copy.copyCommand}
375
+ </Button>
376
+ <Button
377
+ onClick={() => {
378
+ window.NASTECHDesktop?.openExternal?.(ups.docsUrl)
379
+ }}
380
+ size="sm"
381
+ variant="ghost"
382
+ >
383
+ {copy.viewDocs}
384
+ </Button>
385
+ </div>
386
+ </div>
387
+
388
+ <div className="mt-6 flex items-center justify-between border-t pt-4">
389
+ <span className="text-xs text-muted-foreground">
390
+ {copy.installTo} <code className="rounded bg-muted/50 px-1 py-0.5 font-mono">{ups.activeRoot}</code>
391
+ </span>
392
+ <Button onClick={() => window.location.reload()} size="sm" variant="default">
393
+ {copy.retryAfterRun}
394
+ </Button>
395
+ </div>
396
+ </div>
397
+ </div>
398
+ )
399
+ }
400
+
401
+ const stages = state.manifest?.stages || []
402
+ const currentStage = stages.find(s => state.stages[s.name]?.state === 'running')?.name
403
+
404
+ const completedCount = stages.filter(
405
+ s => state.stages[s.name]?.state === 'succeeded' || state.stages[s.name]?.state === 'skipped'
406
+ ).length
407
+
408
+ const totalCount = stages.length
409
+ const failed = Boolean(state.error)
410
+ const progressPct = totalCount > 0 ? Math.round((completedCount / totalCount) * 100) : 0
411
+ const currentStartedAt = currentStage ? state.stages[currentStage]?.startedAt : null
412
+ const currentElapsed = typeof currentStartedAt === 'number' ? formatElapsed(now - currentStartedAt) : ''
413
+
414
+ return (
415
+ <div className="fixed inset-0 z-[1400] flex items-center justify-center bg-background/90 backdrop-blur-md p-4">
416
+ <div className="flex w-full max-w-2xl max-h-[90vh] flex-col rounded-xl border border-(--stroke-nastech) bg-card shadow-nastech">
417
+ {/* Header -- always visible, never scrolls */}
418
+ <div className="flex-shrink-0 p-8 pb-4">
419
+ <h2 className="text-2xl font-semibold tracking-tight">
420
+ {failed ? copy.failedTitle : state.active ? copy.settingUpTitle : copy.finishingTitle}
421
+ </h2>
422
+ <p className="mt-1.5 text-sm text-muted-foreground">
423
+ {failed ? copy.failedDesc : copy.activeDesc}
424
+ </p>
425
+ </div>
426
+
427
+ {/* Scrollable middle: progress, stages, error block, log */}
428
+ <div className="min-h-0 flex-1 overflow-y-auto px-8 pb-2">
429
+ {totalCount > 0 && (
430
+ <div className="mb-4">
431
+ <div className="mb-1 flex items-center justify-between text-xs text-muted-foreground">
432
+ <span>
433
+ {copy.progress(completedCount, totalCount)}
434
+ {currentStage && copy.currentStage(formatStageName(currentStage))}
435
+ {currentElapsed && ` (${currentElapsed})`}
436
+ </span>
437
+ <span className="tabular-nums">{progressPct}%</span>
438
+ </div>
439
+ <div className="h-1.5 w-full overflow-hidden rounded-full bg-muted">
440
+ <div
441
+ className={cn('h-full transition-all duration-300', failed ? 'bg-destructive' : 'bg-primary')}
442
+ style={{ width: `${progressPct}%` }}
443
+ />
444
+ </div>
445
+ </div>
446
+ )}
447
+
448
+ {totalCount === 0 && state.active && (
449
+ <div className="mb-4 flex items-center gap-2.5 text-sm text-muted-foreground">
450
+ <Loader className="size-5" type="lemniscate-bloom" />
451
+ <span>{copy.fetchingManifest}</span>
452
+ </div>
453
+ )}
454
+
455
+ {failed && state.error && (
456
+ <div className="mb-4 rounded-md border border-destructive/30 bg-destructive/10 p-3 text-sm">
457
+ <div className="mb-1 flex items-center gap-1.5 font-medium text-destructive">
458
+ <AlertTriangle className="h-4 w-4" />
459
+ <span>{copy.error}</span>
460
+ </div>
461
+ <p className="whitespace-pre-wrap break-words text-foreground/90">{state.error}</p>
462
+ </div>
463
+ )}
464
+
465
+ {stages.length > 0 && (
466
+ <ol className="mb-4 space-y-1">
467
+ {stages.map(stage => (
468
+ <StageRow
469
+ descriptor={stage}
470
+ isCurrent={stage.name === currentStage}
471
+ key={stage.name}
472
+ now={now}
473
+ result={state.stages[stage.name]}
474
+ />
475
+ ))}
476
+ </ol>
477
+ )}
478
+
479
+ <div className="pt-3">
480
+ <Button
481
+ className="-ml-2 text-muted-foreground hover:text-foreground"
482
+ onClick={() => setLogOpen(v => !v)}
483
+ size="xs"
484
+ type="button"
485
+ variant="ghost"
486
+ >
487
+ {logOpen ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />}
488
+ <span>{logOpen ? copy.hideOutput : copy.showOutput}</span>
489
+ <span className="ml-1 tabular-nums">
490
+ ({copy.lines(state.log.length)})
491
+ </span>
492
+ </Button>
493
+
494
+ {logOpen && (
495
+ <LogView className={cn('mt-2', failed ? 'max-h-96' : 'max-h-64')}>
496
+ {state.log.length === 0 ? (
497
+ <div>{copy.noOutput}</div>
498
+ ) : (
499
+ <>
500
+ {state.log.map((entry, i) => (
501
+ <div className={cn(entry.stream === 'stderr' && 'text-muted-foreground/70')} key={i}>
502
+ {entry.stage ? <span className="text-muted-foreground/60">[{entry.stage}] </span> : null}
503
+ <span>{entry.line}</span>
504
+ </div>
505
+ ))}
506
+ <div ref={logEndRef} />
507
+ </>
508
+ )}
509
+ </LogView>
510
+ )}
511
+ </div>
512
+ </div>
513
+
514
+ {/* Active footer: let the user actually cancel a running install. */}
515
+ {state.active && !failed && (
516
+ <div className="flex-shrink-0 bg-card p-4">
517
+ <div className="flex items-center justify-end">
518
+ <Button
519
+ disabled={cancelling}
520
+ onClick={async () => {
521
+ setCancelling(true)
522
+
523
+ try {
524
+ await window.NASTECHDesktop?.cancelBootstrap?.()
525
+ } catch {
526
+ // ignore -- the failed/cancelled event will surface the result
527
+ }
528
+ }}
529
+ size="sm"
530
+ variant="ghost"
531
+ >
532
+ {cancelling ? <Loader2 className="h-4 w-4 animate-spin" /> : null}
533
+ {cancelling ? copy.cancelling : copy.cancelInstall}
534
+ </Button>
535
+ </div>
536
+ </div>
537
+ )}
538
+
539
+ {/* Footer -- always visible, never scrolls; only renders on failure */}
540
+ {failed && (
541
+ <div className="flex-shrink-0 bg-card p-4">
542
+ <div className="flex items-center justify-between gap-2">
543
+ <span className="text-xs text-muted-foreground">
544
+ {copy.transcriptSaved}{' '}
545
+ <code className="rounded bg-muted/50 px-1 py-0.5 font-mono">%LOCALAPPDATA%\NASTECH\logs\</code>
546
+ </span>
547
+ <div className="flex gap-2">
548
+ <Button
549
+ onClick={async () => {
550
+ const text = state.log
551
+ .map(entry => (entry.stage ? `[${entry.stage}] ${entry.line}` : entry.line))
552
+ .join('\n')
553
+
554
+ const fullText = state.error ? `Error: ${state.error}\n\n${text}` : text
555
+
556
+ try {
557
+ await navigator.clipboard.writeText(fullText)
558
+ setCopied(true)
559
+ window.setTimeout(() => setCopied(false), 1500)
560
+ } catch {
561
+ // ignore -- some environments forbid clipboard writes
562
+ }
563
+ }}
564
+ size="sm"
565
+ variant="secondary"
566
+ >
567
+ {copied ? copy.copiedOutput : copy.copyOutput}
568
+ </Button>
569
+ <Button
570
+ onClick={async () => {
571
+ // Tell main.cjs to clear its latched failure BEFORE we
572
+ // reload. Otherwise the renderer reload calls getConnection
573
+ // and main short-circuits to the latched error without
574
+ // re-running install.ps1.
575
+ try {
576
+ await window.NASTECHDesktop?.resetBootstrap?.()
577
+ } catch {
578
+ // best-effort -- continue with reload regardless
579
+ }
580
+
581
+ window.location.reload()
582
+ }}
583
+ size="sm"
584
+ variant="default"
585
+ >
586
+ {copy.reloadRetry}
587
+ </Button>
588
+ </div>
589
+ </div>
590
+ </div>
591
+ )}
592
+ </div>
593
+ </div>
594
+ )
595
+ }
@@ -0,0 +1,100 @@
1
+ import { cleanup, fireEvent, render, screen } from '@testing-library/react'
2
+ import { afterEach, describe, expect, it } from 'vitest'
3
+
4
+ import { $desktopOnboarding, type DesktopOnboardingState, type OnboardingContext } from '@/store/onboarding'
5
+ import type { OAuthProvider } from '@/types/nastech'
6
+
7
+ import { Picker } from './desktop-onboarding-overlay'
8
+
9
+ function provider(id: string, name = id): OAuthProvider {
10
+ return {
11
+ cli_command: `NASTECH login ${id}`,
12
+ docs_url: `https://example.com/${id}`,
13
+ flow: 'pkce',
14
+ id,
15
+ name,
16
+ status: { logged_in: false }
17
+ }
18
+ }
19
+
20
+ function setProviders(providers: OAuthProvider[]) {
21
+ $desktopOnboarding.set({
22
+ configured: false,
23
+ flow: { status: 'idle' },
24
+ mode: 'oauth',
25
+ providers,
26
+ reason: null,
27
+ requested: false,
28
+ firstRunSkipped: false,
29
+ manual: false
30
+ } satisfies DesktopOnboardingState)
31
+ }
32
+
33
+ const ctx: OnboardingContext = { requestGateway: async () => undefined as never }
34
+
35
+ afterEach(() => {
36
+ cleanup()
37
+
38
+ try {
39
+ window.localStorage.clear()
40
+ } catch {
41
+ // jsdom localStorage should always be present; ignore if not.
42
+ }
43
+
44
+ $desktopOnboarding.set({
45
+ configured: null,
46
+ flow: { status: 'idle' },
47
+ mode: 'oauth',
48
+ providers: null,
49
+ reason: null,
50
+ requested: false,
51
+ firstRunSkipped: false,
52
+ manual: false
53
+ })
54
+ })
55
+
56
+ describe('onboarding Picker', () => {
57
+ it('features NasTech Portal and hides other providers behind a disclosure', () => {
58
+ setProviders([provider('anthropic', 'Anthropic Claude'), provider('nastech', 'NasTech Portal')])
59
+ render(<Picker ctx={ctx} />)
60
+
61
+ expect(screen.getByText('NasTech Portal')).toBeTruthy()
62
+ expect(screen.getByText('Recommended')).toBeTruthy()
63
+ expect(screen.queryByText('Anthropic API Key')).toBeNull()
64
+
65
+ fireEvent.click(screen.getByRole('button', { name: 'Other providers' }))
66
+
67
+ expect(screen.getByText('Anthropic API Key')).toBeTruthy()
68
+ expect(screen.getByRole('button', { name: 'Collapse' })).toBeTruthy()
69
+ })
70
+
71
+ it('shows every provider directly when NasTech Portal is absent', () => {
72
+ setProviders([provider('anthropic', 'Anthropic Claude'), provider('openai-codex', 'OpenAI Codex / ChatGPT')])
73
+ render(<Picker ctx={ctx} />)
74
+
75
+ expect(screen.getByText('Anthropic API Key')).toBeTruthy()
76
+ expect(screen.getByText('OpenAI OAuth (ChatGPT)')).toBeTruthy()
77
+ expect(screen.queryByText('Other sign-in options')).toBeNull()
78
+ expect(screen.queryByText('Recommended')).toBeNull()
79
+ })
80
+
81
+ it('offers "choose later" on first run and persists the skip', () => {
82
+ setProviders([provider('nastech', 'NasTech Portal')])
83
+ render(<Picker ctx={ctx} />)
84
+
85
+ const skip = screen.getByRole('button', { name: "I'll choose a provider later" })
86
+
87
+ fireEvent.click(skip)
88
+
89
+ expect($desktopOnboarding.get().firstRunSkipped).toBe(true)
90
+ expect(window.localStorage.getItem('NASTECH-onboarding-skipped-v1')).toBe('1')
91
+ })
92
+
93
+ it('hides "choose later" in manual (add-provider) mode', () => {
94
+ setProviders([provider('nastech', 'NasTech Portal')])
95
+ $desktopOnboarding.set({ ...$desktopOnboarding.get(), manual: true })
96
+ render(<Picker ctx={ctx} />)
97
+
98
+ expect(screen.queryByRole('button', { name: "I'll choose a provider later" })).toBeNull()
99
+ })
100
+ })