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,20 @@
1
+ /**
2
+ * Helpers for Electron net.request calls that ride the OAuth session partition.
3
+ *
4
+ * Electron's ClientRequest forbids app-set restricted headers such as
5
+ * Content-Length. Let Chromium frame the body itself; only set the JSON content
6
+ * type here.
7
+ */
8
+
9
+ function serializeJsonBody(body) {
10
+ return body === undefined ? undefined : Buffer.from(JSON.stringify(body))
11
+ }
12
+
13
+ function setJsonRequestHeaders(request) {
14
+ request.setHeader('Content-Type', 'application/json')
15
+ }
16
+
17
+ module.exports = {
18
+ serializeJsonBody,
19
+ setJsonRequestHeaders
20
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Tests for OAuth-session Electron net.request helpers.
3
+ *
4
+ * Run with: node --test electron/oauth-net-request.test.cjs
5
+ */
6
+
7
+ const test = require('node:test')
8
+ const assert = require('node:assert/strict')
9
+
10
+ const { serializeJsonBody, setJsonRequestHeaders } = require('./oauth-net-request.cjs')
11
+
12
+ test('serializeJsonBody returns undefined for absent bodies', () => {
13
+ assert.equal(serializeJsonBody(undefined), undefined)
14
+ })
15
+
16
+ test('serializeJsonBody JSON-encodes request bodies', () => {
17
+ const body = serializeJsonBody({ archived: true })
18
+ assert.ok(Buffer.isBuffer(body))
19
+ assert.equal(body.toString('utf8'), '{"archived":true}')
20
+ })
21
+
22
+ test('setJsonRequestHeaders does not set Electron-restricted Content-Length', () => {
23
+ const headers = []
24
+ const request = {
25
+ setHeader(name, value) {
26
+ headers.push([name, value])
27
+ }
28
+ }
29
+
30
+ setJsonRequestHeaders(request)
31
+
32
+ assert.deepEqual(headers, [['Content-Type', 'application/json']])
33
+ assert.equal(headers.some(([name]) => name.toLowerCase() === 'content-length'), false)
34
+ })
@@ -0,0 +1,135 @@
1
+ const { contextBridge, ipcRenderer, webUtils } = require('electron')
2
+
3
+ contextBridge.exposeInMainWorld('nastechDesktop', {
4
+ getConnection: profile => ipcRenderer.invoke('nastech:connection', profile),
5
+ touchBackend: profile => ipcRenderer.invoke('nastech:backend:touch', profile),
6
+ getGatewayWsUrl: profile => ipcRenderer.invoke('nastech:gateway:ws-url', profile),
7
+ getBootProgress: () => ipcRenderer.invoke('nastech:boot-progress:get'),
8
+ getConnectionConfig: profile => ipcRenderer.invoke('nastech:connection-config:get', profile),
9
+ saveConnectionConfig: payload => ipcRenderer.invoke('nastech:connection-config:save', payload),
10
+ applyConnectionConfig: payload => ipcRenderer.invoke('nastech:connection-config:apply', payload),
11
+ testConnectionConfig: payload => ipcRenderer.invoke('nastech:connection-config:test', payload),
12
+ probeConnectionConfig: remoteUrl => ipcRenderer.invoke('nastech:connection-config:probe', remoteUrl),
13
+ oauthLoginConnectionConfig: remoteUrl => ipcRenderer.invoke('nastech:connection-config:oauth-login', remoteUrl),
14
+ oauthLogoutConnectionConfig: remoteUrl => ipcRenderer.invoke('nastech:connection-config:oauth-logout', remoteUrl),
15
+ profile: {
16
+ get: () => ipcRenderer.invoke('nastech:profile:get'),
17
+ set: name => ipcRenderer.invoke('nastech:profile:set', name)
18
+ },
19
+ api: request => ipcRenderer.invoke('nastech:api', request),
20
+ notify: payload => ipcRenderer.invoke('nastech:notify', payload),
21
+ requestMicrophoneAccess: () => ipcRenderer.invoke('nastech:requestMicrophoneAccess'),
22
+ readFileDataUrl: filePath => ipcRenderer.invoke('nastech:readFileDataUrl', filePath),
23
+ readFileText: filePath => ipcRenderer.invoke('nastech:readFileText', filePath),
24
+ selectPaths: options => ipcRenderer.invoke('nastech:selectPaths', options),
25
+ writeClipboard: text => ipcRenderer.invoke('nastech:writeClipboard', text),
26
+ saveImageFromUrl: url => ipcRenderer.invoke('nastech:saveImageFromUrl', url),
27
+ saveImageBuffer: (data, ext) => ipcRenderer.invoke('nastech:saveImageBuffer', { data, ext }),
28
+ saveClipboardImage: () => ipcRenderer.invoke('nastech:saveClipboardImage'),
29
+ getPathForFile: file => {
30
+ try {
31
+ return webUtils.getPathForFile(file) || ''
32
+ } catch {
33
+ return ''
34
+ }
35
+ },
36
+ normalizePreviewTarget: (target, baseDir) => ipcRenderer.invoke('nastech:normalizePreviewTarget', target, baseDir),
37
+ watchPreviewFile: url => ipcRenderer.invoke('nastech:watchPreviewFile', url),
38
+ stopPreviewFileWatch: id => ipcRenderer.invoke('nastech:stopPreviewFileWatch', id),
39
+ setTitleBarTheme: payload => ipcRenderer.send('nastech:titlebar-theme', payload),
40
+ setPreviewShortcutActive: active => ipcRenderer.send('nastech:previewShortcutActive', Boolean(active)),
41
+ openExternal: url => ipcRenderer.invoke('nastech:openExternal', url),
42
+ fetchLinkTitle: url => ipcRenderer.invoke('nastech:fetchLinkTitle', url),
43
+ settings: {
44
+ getDefaultProjectDir: () => ipcRenderer.invoke('nastech:setting:defaultProjectDir:get'),
45
+ setDefaultProjectDir: dir => ipcRenderer.invoke('nastech:setting:defaultProjectDir:set', dir),
46
+ pickDefaultProjectDir: () => ipcRenderer.invoke('nastech:setting:defaultProjectDir:pick')
47
+ },
48
+ revealLogs: () => ipcRenderer.invoke('nastech:logs:reveal'),
49
+ getRecentLogs: () => ipcRenderer.invoke('nastech:logs:recent'),
50
+ readDir: dirPath => ipcRenderer.invoke('nastech:fs:readDir', dirPath),
51
+ gitRoot: startPath => ipcRenderer.invoke('nastech:fs:gitRoot', startPath),
52
+ terminal: {
53
+ dispose: id => ipcRenderer.invoke('nastech:terminal:dispose', id),
54
+ resize: (id, size) => ipcRenderer.invoke('nastech:terminal:resize', id, size),
55
+ start: options => ipcRenderer.invoke('nastech:terminal:start', options),
56
+ write: (id, data) => ipcRenderer.invoke('nastech:terminal:write', id, data),
57
+ onData: (id, callback) => {
58
+ const channel = `nastech:terminal:${id}:data`
59
+ const listener = (_event, payload) => callback(payload)
60
+ ipcRenderer.on(channel, listener)
61
+ return () => ipcRenderer.removeListener(channel, listener)
62
+ },
63
+ onExit: (id, callback) => {
64
+ const channel = `nastech:terminal:${id}:exit`
65
+ const listener = (_event, payload) => callback(payload)
66
+ ipcRenderer.on(channel, listener)
67
+ return () => ipcRenderer.removeListener(channel, listener)
68
+ }
69
+ },
70
+ onClosePreviewRequested: callback => {
71
+ const listener = () => callback()
72
+ ipcRenderer.on('nastech:close-preview-requested', listener)
73
+ return () => ipcRenderer.removeListener('nastech:close-preview-requested', listener)
74
+ },
75
+ onOpenUpdatesRequested: callback => {
76
+ const listener = () => callback()
77
+ ipcRenderer.on('nastech:open-updates', listener)
78
+ return () => ipcRenderer.removeListener('nastech:open-updates', listener)
79
+ },
80
+ onWindowStateChanged: callback => {
81
+ const listener = (_event, payload) => callback(payload)
82
+ ipcRenderer.on('nastech:window-state-changed', listener)
83
+ return () => ipcRenderer.removeListener('nastech:window-state-changed', listener)
84
+ },
85
+ onPreviewFileChanged: callback => {
86
+ const listener = (_event, payload) => callback(payload)
87
+ ipcRenderer.on('nastech:preview-file-changed', listener)
88
+ return () => ipcRenderer.removeListener('nastech:preview-file-changed', listener)
89
+ },
90
+ onBackendExit: callback => {
91
+ const listener = (_event, payload) => callback(payload)
92
+ ipcRenderer.on('nastech:backend-exit', listener)
93
+ return () => ipcRenderer.removeListener('nastech:backend-exit', listener)
94
+ },
95
+ onPowerResume: callback => {
96
+ const listener = () => callback()
97
+ ipcRenderer.on('nastech:power-resume', listener)
98
+ return () => ipcRenderer.removeListener('nastech:power-resume', listener)
99
+ },
100
+ onBootProgress: callback => {
101
+ const listener = (_event, payload) => callback(payload)
102
+ ipcRenderer.on('nastech:boot-progress', listener)
103
+ return () => ipcRenderer.removeListener('nastech:boot-progress', listener)
104
+ },
105
+ // First-launch bootstrap progress -- emitted by the install.ps1 stage
106
+ // runner in main.cjs (apps/desktop/electron/bootstrap-runner.cjs).
107
+ // Renderer's install overlay subscribes to live events and queries the
108
+ // current snapshot via getBootstrapState() to recover after a devtools
109
+ // reload mid-bootstrap.
110
+ getBootstrapState: () => ipcRenderer.invoke('nastech:bootstrap:get'),
111
+ resetBootstrap: () => ipcRenderer.invoke('nastech:bootstrap:reset'),
112
+ repairBootstrap: () => ipcRenderer.invoke('nastech:bootstrap:repair'),
113
+ cancelBootstrap: () => ipcRenderer.invoke('nastech:bootstrap:cancel'),
114
+ onBootstrapEvent: callback => {
115
+ const listener = (_event, payload) => callback(payload)
116
+ ipcRenderer.on('nastech:bootstrap:event', listener)
117
+ return () => ipcRenderer.removeListener('nastech:bootstrap:event', listener)
118
+ },
119
+ getVersion: () => ipcRenderer.invoke('nastech:version'),
120
+ uninstall: {
121
+ summary: () => ipcRenderer.invoke('nastech:uninstall:summary'),
122
+ run: mode => ipcRenderer.invoke('nastech:uninstall:run', { mode })
123
+ },
124
+ updates: {
125
+ check: () => ipcRenderer.invoke('nastech:updates:check'),
126
+ apply: opts => ipcRenderer.invoke('nastech:updates:apply', opts),
127
+ getBranch: () => ipcRenderer.invoke('nastech:updates:branch:get'),
128
+ setBranch: name => ipcRenderer.invoke('nastech:updates:branch:set', name),
129
+ onProgress: callback => {
130
+ const listener = (_event, payload) => callback(payload)
131
+ ipcRenderer.on('nastech:updates:progress', listener)
132
+ return () => ipcRenderer.removeListener('nastech:updates:progress', listener)
133
+ }
134
+ }
135
+ })
@@ -0,0 +1,99 @@
1
+ // Secondary "session windows" — one extra OS window per chat so a user can
2
+ // work with multiple chats side by side. The pure, Electron-free pieces live
3
+ // here so they can be unit-tested with node --test (mirroring how the rest of
4
+ // electron/*.cjs splits testable logic out of the main.cjs monolith).
5
+
6
+ const { pathToFileURL } = require('node:url')
7
+
8
+ // Secondary windows open at the minimum usable size — a compact side panel for
9
+ // subagent watch / cmd-click session pop-out, not a second full desktop.
10
+ const SESSION_WINDOW_MIN_WIDTH = 420
11
+ const SESSION_WINDOW_MIN_HEIGHT = 620
12
+
13
+ // Build the renderer URL for a secondary window. The renderer uses a
14
+ // HashRouter, so the session route lives after the '#'. The `?win=secondary`
15
+ // flag MUST sit in the query string BEFORE the '#': anything after the '#' is
16
+ // treated as the route by HashRouter and would break routeSessionId(). The
17
+ // renderer reads the flag from window.location.search to suppress the install /
18
+ // onboarding overlays and the global session sidebar. `watch=1` marks a
19
+ // spectator window (e.g. a running subagent's session): the renderer resumes
20
+ // it lazily so the gateway never builds an agent just to stream into it.
21
+ function buildSessionWindowUrl(sessionId, { devServer, rendererIndexPath, watch } = {}) {
22
+ const query = `?win=secondary${watch ? '&watch=1' : ''}`
23
+ const route = `#/${encodeURIComponent(sessionId)}`
24
+
25
+ if (devServer) {
26
+ const base = devServer.endsWith('/') ? devServer.slice(0, -1) : devServer
27
+
28
+ return `${base}/${query}${route}`
29
+ }
30
+
31
+ return `${pathToFileURL(rendererIndexPath).toString()}${query}${route}`
32
+ }
33
+
34
+ // A small registry keyed by sessionId that guarantees one window per chat:
35
+ // opening a session that already has a live window focuses it instead of
36
+ // spawning a duplicate, and a window removes itself from the registry when it
37
+ // closes. The actual BrowserWindow construction is injected (the `factory`) so
38
+ // this module stays free of Electron and is unit-testable.
39
+ function createSessionWindowRegistry() {
40
+ const windows = new Map()
41
+
42
+ function openOrFocus(sessionId, factory) {
43
+ const key = typeof sessionId === 'string' ? sessionId.trim() : ''
44
+
45
+ if (!key) {
46
+ return null
47
+ }
48
+
49
+ const existing = windows.get(key)
50
+
51
+ if (existing && !existing.isDestroyed()) {
52
+ // Focus-or-create: never duplicate a window for the same chat.
53
+ if (typeof existing.isMinimized === 'function' && existing.isMinimized()) {
54
+ existing.restore?.()
55
+ }
56
+
57
+ if (typeof existing.isVisible === 'function' && !existing.isVisible()) {
58
+ existing.show?.()
59
+ }
60
+
61
+ existing.focus?.()
62
+
63
+ return existing
64
+ }
65
+
66
+ const win = factory(key)
67
+
68
+ if (!win) {
69
+ return null
70
+ }
71
+
72
+ windows.set(key, win)
73
+
74
+ // Self-cleanup on close so the registry never holds a destroyed window.
75
+ win.on?.('closed', () => {
76
+ if (windows.get(key) === win) {
77
+ windows.delete(key)
78
+ }
79
+ })
80
+
81
+ return win
82
+ }
83
+
84
+ return {
85
+ openOrFocus,
86
+ get: key => windows.get(key),
87
+ has: key => windows.has(key),
88
+ get size() {
89
+ return windows.size
90
+ }
91
+ }
92
+ }
93
+
94
+ module.exports = {
95
+ buildSessionWindowUrl,
96
+ createSessionWindowRegistry,
97
+ SESSION_WINDOW_MIN_HEIGHT,
98
+ SESSION_WINDOW_MIN_WIDTH
99
+ }
@@ -0,0 +1,177 @@
1
+ const assert = require('node:assert/strict')
2
+ const test = require('node:test')
3
+
4
+ const { buildSessionWindowUrl, createSessionWindowRegistry } = require('./session-windows.cjs')
5
+
6
+ // A minimal fake BrowserWindow: tracks listeners + destroyed state and lets a
7
+ // test fire the 'closed' event, mirroring the slice of the Electron API the
8
+ // registry actually touches.
9
+ function makeFakeWindow() {
10
+ const listeners = {}
11
+ const calls = { focus: 0, show: 0, restore: 0 }
12
+ let destroyed = false
13
+ let minimized = false
14
+ let visible = true
15
+
16
+ return {
17
+ on(event, handler) {
18
+ listeners[event] = handler
19
+
20
+ return this
21
+ },
22
+ emit(event) {
23
+ listeners[event]?.()
24
+ },
25
+ isDestroyed: () => destroyed,
26
+ destroy() {
27
+ destroyed = true
28
+ },
29
+ isMinimized: () => minimized,
30
+ setMinimized(value) {
31
+ minimized = value
32
+ },
33
+ isVisible: () => visible,
34
+ setVisible(value) {
35
+ visible = value
36
+ },
37
+ restore() {
38
+ calls.restore += 1
39
+ minimized = false
40
+ },
41
+ show() {
42
+ calls.show += 1
43
+ visible = true
44
+ },
45
+ focus() {
46
+ calls.focus += 1
47
+ },
48
+ calls
49
+ }
50
+ }
51
+
52
+ test('buildSessionWindowUrl puts the secondary flag before the hash route (dev server)', () => {
53
+ const url = buildSessionWindowUrl('abc123', { devServer: 'http://localhost:5173' })
54
+
55
+ assert.equal(url, 'http://localhost:5173/?win=secondary#/abc123')
56
+ })
57
+
58
+ test('buildSessionWindowUrl avoids a double slash when the dev server has a trailing slash', () => {
59
+ const url = buildSessionWindowUrl('abc123', { devServer: 'http://localhost:5173/' })
60
+
61
+ assert.equal(url, 'http://localhost:5173/?win=secondary#/abc123')
62
+ })
63
+
64
+ test('buildSessionWindowUrl encodes the session id in the hash route', () => {
65
+ const url = buildSessionWindowUrl('a b/c', { devServer: 'http://localhost:5173' })
66
+
67
+ // The query flag must precede the '#' or HashRouter would swallow it as the
68
+ // route; the id is URL-encoded so slashes/spaces survive routeSessionId().
69
+ assert.equal(url, 'http://localhost:5173/?win=secondary#/a%20b%2Fc')
70
+ assert.ok(url.indexOf('?win=secondary') < url.indexOf('#'))
71
+ })
72
+
73
+ test('buildSessionWindowUrl builds a packaged file URL with the flag before the hash', () => {
74
+ const url = buildSessionWindowUrl('abc', { rendererIndexPath: '/opt/app/index.html' })
75
+
76
+ assert.match(url, /^file:\/\/.*index\.html\?win=secondary#\/abc$/)
77
+ })
78
+
79
+ test('buildSessionWindowUrl adds the watch flag for spectator windows, before the hash', () => {
80
+ const url = buildSessionWindowUrl('abc', { devServer: 'http://localhost:5173', watch: true })
81
+
82
+ assert.equal(url, 'http://localhost:5173/?win=secondary&watch=1#/abc')
83
+ })
84
+
85
+ test('buildSessionWindowUrl routes new-session windows to the draft (#/)', () => {
86
+ const url = buildSessionWindowUrl(null, { devServer: 'http://localhost:5173', newSession: true })
87
+
88
+ assert.equal(url, 'http://localhost:5173/?win=secondary&new=1#/')
89
+ })
90
+
91
+ test('registry opens one window per session and focuses on re-open', () => {
92
+ const registry = createSessionWindowRegistry()
93
+ let built = 0
94
+ const win = makeFakeWindow()
95
+ const factory = () => {
96
+ built += 1
97
+
98
+ return win
99
+ }
100
+
101
+ const first = registry.openOrFocus('s1', factory)
102
+ const second = registry.openOrFocus('s1', factory)
103
+
104
+ assert.equal(built, 1, 'factory runs once for the same session')
105
+ assert.equal(first, second)
106
+ assert.equal(registry.size, 1)
107
+ assert.equal(win.calls.focus, 1, 'second open focuses the existing window')
108
+ })
109
+
110
+ test('registry restores + shows a minimized/hidden window on re-open', () => {
111
+ const registry = createSessionWindowRegistry()
112
+ const win = makeFakeWindow()
113
+ registry.openOrFocus('s1', () => win)
114
+
115
+ win.setMinimized(true)
116
+ win.setVisible(false)
117
+ registry.openOrFocus('s1', () => win)
118
+
119
+ assert.equal(win.calls.restore, 1)
120
+ assert.equal(win.calls.show, 1)
121
+ assert.equal(win.calls.focus, 1)
122
+ })
123
+
124
+ test('registry drops the entry when the window closes', () => {
125
+ const registry = createSessionWindowRegistry()
126
+ const win = makeFakeWindow()
127
+ registry.openOrFocus('s1', () => win)
128
+ assert.equal(registry.size, 1)
129
+
130
+ win.emit('closed')
131
+
132
+ assert.equal(registry.size, 0)
133
+ assert.equal(registry.has('s1'), false)
134
+ })
135
+
136
+ test('registry rebuilds a fresh window after the previous one was destroyed', () => {
137
+ const registry = createSessionWindowRegistry()
138
+ const first = makeFakeWindow()
139
+ registry.openOrFocus('s1', () => first)
140
+ first.destroy()
141
+
142
+ let built = 0
143
+ const second = makeFakeWindow()
144
+ const result = registry.openOrFocus('s1', () => {
145
+ built += 1
146
+
147
+ return second
148
+ })
149
+
150
+ assert.equal(built, 1, 'a destroyed window is replaced, not focused')
151
+ assert.equal(result, second)
152
+ })
153
+
154
+ test('registry ignores empty / non-string session ids', () => {
155
+ const registry = createSessionWindowRegistry()
156
+ let built = 0
157
+ const factory = () => {
158
+ built += 1
159
+
160
+ return makeFakeWindow()
161
+ }
162
+
163
+ assert.equal(registry.openOrFocus('', factory), null)
164
+ assert.equal(registry.openOrFocus(' ', factory), null)
165
+ assert.equal(registry.openOrFocus(null, factory), null)
166
+ assert.equal(registry.openOrFocus(42, factory), null)
167
+ assert.equal(built, 0)
168
+ assert.equal(registry.size, 0)
169
+ })
170
+
171
+ test('registry trims the session id before keying', () => {
172
+ const registry = createSessionWindowRegistry()
173
+ const win = makeFakeWindow()
174
+ registry.openOrFocus(' s1 ', () => win)
175
+
176
+ assert.equal(registry.has('s1'), true)
177
+ })
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Pure helpers for choosing a remote URL during passive update checks.
3
+ *
4
+ * A public install can end up with `origin=git@github.com:nastechai/nastech-agent.git`.
5
+ * If the user's GitHub SSH key is FIDO2/passkey-backed, a background `git fetch
6
+ * origin` triggers an unexplained hardware-touch prompt. For passive checks
7
+ * against the official repo we substitute the public HTTPS `ls-remote` path,
8
+ * which needs no auth and cannot prompt. Active update/apply flows are left
9
+ * unchanged.
10
+ *
11
+ * Extracted from main.cjs so the security-critical remote detection is unit
12
+ * testable without booting Electron (main.cjs requires('electron') at load).
13
+ */
14
+
15
+ const OFFICIAL_REPO_HTTPS_URL = 'https://github.com/nastechai/nastech-agent.git'
16
+ const OFFICIAL_REPO_CANONICAL = 'github.com/nastechai/nastech-agent'
17
+
18
+ // Normalize common GitHub remote URL forms to `host/owner/repo` (lowercased,
19
+ // no trailing slash, no .git suffix) so SSH and HTTPS forms of the same repo
20
+ // compare equal.
21
+ function canonicalGitHubRemote(url) {
22
+ if (!url) return ''
23
+ let value = String(url).trim()
24
+ if (value.startsWith('git@github.com:')) {
25
+ value = `github.com/${value.slice('git@github.com:'.length)}`
26
+ } else if (value.startsWith('ssh://git@github.com/')) {
27
+ value = `github.com/${value.slice('ssh://git@github.com/'.length)}`
28
+ } else {
29
+ try {
30
+ const parsed = new URL(value)
31
+ if (parsed.hostname && parsed.pathname) value = `${parsed.hostname}${parsed.pathname}`
32
+ } catch {
33
+ // Leave non-URL forms unchanged.
34
+ }
35
+ }
36
+ value = value.trim().replace(/\/+$/, '')
37
+ if (value.endsWith('.git')) value = value.slice(0, -4)
38
+ return value.toLowerCase()
39
+ }
40
+
41
+ function isSshRemote(url) {
42
+ const value = String(url || '').trim().toLowerCase()
43
+ return value.startsWith('git@') || value.startsWith('ssh://')
44
+ }
45
+
46
+ function isOfficialSshRemote(url) {
47
+ return isSshRemote(url) && canonicalGitHubRemote(url) === OFFICIAL_REPO_CANONICAL
48
+ }
49
+
50
+ module.exports = {
51
+ OFFICIAL_REPO_HTTPS_URL,
52
+ OFFICIAL_REPO_CANONICAL,
53
+ canonicalGitHubRemote,
54
+ isSshRemote,
55
+ isOfficialSshRemote
56
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Tests for electron/update-remote.cjs — the remote-detection helpers that
3
+ * keep passive update checks off the SSH origin for official installs.
4
+ *
5
+ * Run with: node --test electron/update-remote.test.cjs
6
+ * (Wired into npm test:desktop:platforms in package.json.)
7
+ *
8
+ * Why this matters: a public install can carry
9
+ * origin=git@github.com:nastech-ai/NasTech-Agent.git. A background
10
+ * `git fetch origin` then authenticates over SSH and, with a FIDO2/passkey
11
+ * key, triggers an unexplained hardware-touch prompt. isOfficialSshRemote
12
+ * must reliably recognize the official SSH remote (in every URL form,
13
+ * case-insensitively) so the caller can swap in the anonymous HTTPS path —
14
+ * while NOT misclassifying forks, other hosts, or the HTTPS remote (which
15
+ * never prompts and should keep the normal fetch path).
16
+ */
17
+
18
+ const test = require('node:test')
19
+ const assert = require('node:assert/strict')
20
+
21
+ const {
22
+ OFFICIAL_REPO_HTTPS_URL,
23
+ OFFICIAL_REPO_CANONICAL,
24
+ canonicalGitHubRemote,
25
+ isSshRemote,
26
+ isOfficialSshRemote
27
+ } = require('./update-remote.cjs')
28
+
29
+ test('canonicalGitHubRemote normalizes SSH and HTTPS forms to the same value', () => {
30
+ assert.equal(canonicalGitHubRemote('git@github.com:nastech-ai/NasTech-Agent.git'), OFFICIAL_REPO_CANONICAL)
31
+ assert.equal(canonicalGitHubRemote('git@github.com:nastech-ai/NasTech-Agent'), OFFICIAL_REPO_CANONICAL)
32
+ assert.equal(canonicalGitHubRemote('ssh://git@github.com/nastech-ai/NasTech-Agent.git'), OFFICIAL_REPO_CANONICAL)
33
+ assert.equal(canonicalGitHubRemote('https://github.com/nastech-ai/NasTech-Agent.git'), OFFICIAL_REPO_CANONICAL)
34
+ // Case-insensitive: an uppercased owner still canonicalizes to the same repo.
35
+ assert.equal(canonicalGitHubRemote('git@github.com:NASTECHAI/nastech-agent.git'), OFFICIAL_REPO_CANONICAL)
36
+ // Trailing slashes are stripped.
37
+ assert.equal(canonicalGitHubRemote('https://github.com/nastech-ai/NasTech-Agent/'), OFFICIAL_REPO_CANONICAL)
38
+ })
39
+
40
+ test('canonicalGitHubRemote is empty for falsy input', () => {
41
+ assert.equal(canonicalGitHubRemote(''), '')
42
+ assert.equal(canonicalGitHubRemote(null), '')
43
+ assert.equal(canonicalGitHubRemote(undefined), '')
44
+ })
45
+
46
+ test('isSshRemote detects scp-like and ssh:// forms only', () => {
47
+ assert.equal(isSshRemote('git@github.com:nastech-ai/NasTech-Agent.git'), true)
48
+ assert.equal(isSshRemote('ssh://git@github.com/nastech-ai/NasTech-Agent.git'), true)
49
+ assert.equal(isSshRemote('https://github.com/nastech-ai/NasTech-Agent.git'), false)
50
+ assert.equal(isSshRemote(''), false)
51
+ assert.equal(isSshRemote(null), false)
52
+ })
53
+
54
+ test('isOfficialSshRemote is true only for the official repo over SSH', () => {
55
+ assert.equal(isOfficialSshRemote('git@github.com:nastech-ai/NasTech-Agent.git'), true)
56
+ assert.equal(isOfficialSshRemote('git@github.com:nastech-ai/NasTech-Agent'), true)
57
+ assert.equal(isOfficialSshRemote('ssh://git@github.com/nastech-ai/NasTech-Agent.git'), true)
58
+ // Case-insensitive owner/repo match.
59
+ assert.equal(isOfficialSshRemote('git@github.com:NASTECHAI/nastech-agent.git'), true)
60
+ })
61
+
62
+ test('isOfficialSshRemote does NOT match forks, other hosts, or HTTPS', () => {
63
+ // A fork over SSH belongs to the user — fetching it is their own remote,
64
+ // not the official upstream, so the SSH-avoidance swap must not apply.
65
+ assert.equal(isOfficialSshRemote('git@github.com:someuser/nastech-agent.git'), false)
66
+ // Same repo name on a different host is not the official repo.
67
+ assert.equal(isOfficialSshRemote('git@gitlab.com:nastech-ai/NasTech-Agent.git'), false)
68
+ // HTTPS to the official repo never prompts for SSH/FIDO2, so it keeps the
69
+ // normal fetch path — must not be flagged as an official SSH remote.
70
+ assert.equal(isOfficialSshRemote('https://github.com/nastech-ai/NasTech-Agent.git'), false)
71
+ assert.equal(isOfficialSshRemote(''), false)
72
+ assert.equal(isOfficialSshRemote(null), false)
73
+ })
74
+
75
+ test('OFFICIAL_REPO_HTTPS_URL canonicalizes to OFFICIAL_REPO_CANONICAL', () => {
76
+ // Invariant: the URL we substitute in must be the same repo we detect.
77
+ assert.equal(canonicalGitHubRemote(OFFICIAL_REPO_HTTPS_URL), OFFICIAL_REPO_CANONICAL)
78
+ })