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,329 @@
1
+ /**
2
+ * Tests for electron/connection-config.cjs.
3
+ *
4
+ * Run with: node --test electron/connection-config.test.cjs
5
+ * (Wire into npm test:desktop:platforms in package.json.)
6
+ *
7
+ * These are the pure helpers behind the remote-gateway connection settings:
8
+ * URL normalization, WS-URL construction (token vs OAuth ticket), auth-mode
9
+ * classification from /api/status, the coerce-time auth-mode resolution rules,
10
+ * and the OAuth session-cookie detector.
11
+ */
12
+
13
+ const test = require('node:test')
14
+ const assert = require('node:assert/strict')
15
+
16
+ const {
17
+ AT_COOKIE_VARIANTS,
18
+ RT_COOKIE_VARIANTS,
19
+ authModeFromStatus,
20
+ buildGatewayWsUrl,
21
+ buildGatewayWsUrlWithTicket,
22
+ connectionScopeKey,
23
+ cookiesHaveSession,
24
+ cookiesHaveLiveSession,
25
+ normAuthMode,
26
+ normalizeRemoteBaseUrl,
27
+ profileRemoteOverride,
28
+ resolveAuthMode,
29
+ resolveTestWsUrl,
30
+ tokenPreview
31
+ } = require('./connection-config.cjs')
32
+
33
+ // --- connectionScopeKey / normAuthMode ---
34
+
35
+ test('connectionScopeKey trims to a name or null for the global scope', () => {
36
+ assert.equal(connectionScopeKey(' coder '), 'coder')
37
+ assert.equal(connectionScopeKey(''), null)
38
+ assert.equal(connectionScopeKey(null), null)
39
+ assert.equal(connectionScopeKey(undefined), null)
40
+ })
41
+
42
+ test('normAuthMode coerces to token unless explicitly oauth', () => {
43
+ assert.equal(normAuthMode('oauth'), 'oauth')
44
+ assert.equal(normAuthMode('token'), 'token')
45
+ assert.equal(normAuthMode(undefined), 'token')
46
+ assert.equal(normAuthMode('weird'), 'token')
47
+ })
48
+
49
+ // --- profileRemoteOverride ---
50
+
51
+ test('profileRemoteOverride returns null when no profile is given', () => {
52
+ const config = { profiles: { coder: { mode: 'remote', url: 'https://x' } } }
53
+ assert.equal(profileRemoteOverride(config, ''), null)
54
+ assert.equal(profileRemoteOverride(config, null), null)
55
+ assert.equal(profileRemoteOverride(config, undefined), null)
56
+ })
57
+
58
+ test('profileRemoteOverride returns null when the profile has no entry', () => {
59
+ const config = { profiles: { coder: { mode: 'remote', url: 'https://x' } } }
60
+ assert.equal(profileRemoteOverride(config, 'writer'), null)
61
+ })
62
+
63
+ test('profileRemoteOverride ignores local or url-less profile entries', () => {
64
+ assert.equal(profileRemoteOverride({ profiles: { p: { mode: 'local', url: 'https://x' } } }, 'p'), null)
65
+ assert.equal(profileRemoteOverride({ profiles: { p: { mode: 'remote', url: '' } } }, 'p'), null)
66
+ assert.equal(profileRemoteOverride({ profiles: { p: { mode: 'remote' } } }, 'p'), null)
67
+ })
68
+
69
+ test('profileRemoteOverride returns the per-profile remote with defaulted auth mode', () => {
70
+ const config = {
71
+ profiles: {
72
+ coder: { mode: 'remote', url: ' https://coder.example.com/nastech ', token: { value: 'sek' } }
73
+ }
74
+ }
75
+ assert.deepEqual(profileRemoteOverride(config, 'coder'), {
76
+ url: 'https://coder.example.com/nastech',
77
+ authMode: 'token',
78
+ token: { value: 'sek' }
79
+ })
80
+ })
81
+
82
+ test('profileRemoteOverride preserves an explicit oauth auth mode', () => {
83
+ const config = { profiles: { coder: { mode: 'remote', url: 'https://x', authMode: 'oauth' } } }
84
+ assert.equal(profileRemoteOverride(config, 'coder').authMode, 'oauth')
85
+ })
86
+
87
+ test('profileRemoteOverride tolerates a missing/!object profiles map', () => {
88
+ assert.equal(profileRemoteOverride({}, 'coder'), null)
89
+ assert.equal(profileRemoteOverride({ profiles: null }, 'coder'), null)
90
+ assert.equal(profileRemoteOverride(null, 'coder'), null)
91
+ })
92
+
93
+ // --- normalizeRemoteBaseUrl ---
94
+
95
+ test('normalizeRemoteBaseUrl strips trailing slashes, hash, and query', () => {
96
+ assert.equal(normalizeRemoteBaseUrl('https://gw.example.com/'), 'https://gw.example.com')
97
+ assert.equal(normalizeRemoteBaseUrl('https://gw.example.com/nastech/'), 'https://gw.example.com/nastech')
98
+ assert.equal(normalizeRemoteBaseUrl('https://gw.example.com/nastech?x=1#frag'), 'https://gw.example.com/nastech')
99
+ })
100
+
101
+ test('normalizeRemoteBaseUrl preserves a path prefix', () => {
102
+ assert.equal(normalizeRemoteBaseUrl('https://host/nastech'), 'https://host/nastech')
103
+ })
104
+
105
+ test('normalizeRemoteBaseUrl rejects empty input', () => {
106
+ assert.throws(() => normalizeRemoteBaseUrl(''), /required/)
107
+ assert.throws(() => normalizeRemoteBaseUrl(' '), /required/)
108
+ })
109
+
110
+ test('normalizeRemoteBaseUrl rejects non-http(s) protocols', () => {
111
+ assert.throws(() => normalizeRemoteBaseUrl('ftp://host'), /http:\/\/ or https:\/\//)
112
+ assert.throws(() => normalizeRemoteBaseUrl('file:///etc/passwd'), /http:\/\/ or https:\/\//)
113
+ })
114
+
115
+ test('normalizeRemoteBaseUrl rejects garbage', () => {
116
+ assert.throws(() => normalizeRemoteBaseUrl('not a url'), /not valid/)
117
+ })
118
+
119
+ // --- buildGatewayWsUrl (token) ---
120
+
121
+ test('buildGatewayWsUrl uses wss for https and bakes the token', () => {
122
+ assert.equal(buildGatewayWsUrl('https://gw.example.com', 'tok123'), 'wss://gw.example.com/api/ws?token=tok123')
123
+ })
124
+
125
+ test('buildGatewayWsUrl uses ws for http', () => {
126
+ assert.equal(buildGatewayWsUrl('http://127.0.0.1:9119', 'abc'), 'ws://127.0.0.1:9119/api/ws?token=abc')
127
+ })
128
+
129
+ test('buildGatewayWsUrl honors a path prefix', () => {
130
+ assert.equal(buildGatewayWsUrl('https://host/nastech', 't'), 'wss://host/nastech/api/ws?token=t')
131
+ })
132
+
133
+ test('buildGatewayWsUrl url-encodes the token', () => {
134
+ assert.equal(buildGatewayWsUrl('https://host', 'a/b c+d'), 'wss://host/api/ws?token=a%2Fb%20c%2Bd')
135
+ })
136
+
137
+ // --- buildGatewayWsUrlWithTicket (oauth) ---
138
+
139
+ test('buildGatewayWsUrlWithTicket uses ?ticket= not ?token=', () => {
140
+ const url = buildGatewayWsUrlWithTicket('https://gw.example.com/nastech', 'tkt-9')
141
+ assert.equal(url, 'wss://gw.example.com/nastech/api/ws?ticket=tkt-9')
142
+ assert.ok(!url.includes('token='))
143
+ })
144
+
145
+ test('buildGatewayWsUrlWithTicket url-encodes the ticket', () => {
146
+ assert.equal(buildGatewayWsUrlWithTicket('https://host', 'a+b/c'), 'wss://host/api/ws?ticket=a%2Bb%2Fc')
147
+ })
148
+
149
+ // --- authModeFromStatus ---
150
+
151
+ test('authModeFromStatus returns oauth when auth_required is true', () => {
152
+ assert.equal(authModeFromStatus({ auth_required: true, auth_providers: ['nastech'] }), 'oauth')
153
+ })
154
+
155
+ test('authModeFromStatus returns token when auth_required is false/missing', () => {
156
+ assert.equal(authModeFromStatus({ auth_required: false }), 'token')
157
+ assert.equal(authModeFromStatus({}), 'token')
158
+ assert.equal(authModeFromStatus(null), 'token')
159
+ assert.equal(authModeFromStatus(undefined), 'token')
160
+ })
161
+
162
+ // --- resolveAuthMode ---
163
+
164
+ test('resolveAuthMode: explicit input wins over existing', () => {
165
+ assert.equal(resolveAuthMode('oauth', 'token'), 'oauth')
166
+ assert.equal(resolveAuthMode('token', 'oauth'), 'token')
167
+ })
168
+
169
+ test('resolveAuthMode: falls back to existing when input absent', () => {
170
+ assert.equal(resolveAuthMode(undefined, 'oauth'), 'oauth')
171
+ assert.equal(resolveAuthMode(undefined, 'token'), 'token')
172
+ assert.equal(resolveAuthMode('', 'oauth'), 'oauth')
173
+ })
174
+
175
+ test('resolveAuthMode: defaults to token when nothing is set', () => {
176
+ assert.equal(resolveAuthMode(undefined, undefined), 'token')
177
+ assert.equal(resolveAuthMode(null, null), 'token')
178
+ })
179
+
180
+ test('resolveAuthMode: ignores unknown values, defaults to token', () => {
181
+ assert.equal(resolveAuthMode('bogus', 'also-bogus'), 'token')
182
+ })
183
+
184
+ // --- cookiesHaveSession ---
185
+
186
+ test('cookiesHaveSession detects the bare access-token cookie', () => {
187
+ assert.equal(cookiesHaveSession([{ name: 'nastech_session_at', value: 'x' }]), true)
188
+ })
189
+
190
+ test('cookiesHaveSession detects the __Host- and __Secure- prefixed variants', () => {
191
+ assert.equal(cookiesHaveSession([{ name: '__Host-nastech_session_at', value: 'x' }]), true)
192
+ assert.equal(cookiesHaveSession([{ name: '__Secure-nastech_session_at', value: 'x' }]), true)
193
+ })
194
+
195
+ test('cookiesHaveSession is false for an empty value', () => {
196
+ assert.equal(cookiesHaveSession([{ name: 'nastech_session_at', value: '' }]), false)
197
+ })
198
+
199
+ test('cookiesHaveSession ignores unrelated cookies (AT-only by design)', () => {
200
+ // cookiesHaveSession is deliberately access-token-only — a lone RT cookie
201
+ // is NOT an access token, so this returns false. Connectivity callers must
202
+ // use cookiesHaveLiveSession instead (see below).
203
+ assert.equal(cookiesHaveSession([{ name: 'nastech_session_rt', value: 'x' }]), false)
204
+ assert.equal(cookiesHaveSession([{ name: 'other', value: 'x' }]), false)
205
+ })
206
+
207
+ test('cookiesHaveSession handles non-arrays', () => {
208
+ assert.equal(cookiesHaveSession(null), false)
209
+ assert.equal(cookiesHaveSession(undefined), false)
210
+ assert.equal(cookiesHaveSession([]), false)
211
+ })
212
+
213
+ test('AT_COOKIE_VARIANTS covers all three deploy shapes', () => {
214
+ assert.deepEqual(AT_COOKIE_VARIANTS, ['__Host-nastech_session_at', '__Secure-nastech_session_at', 'nastech_session_at'])
215
+ })
216
+
217
+ test('RT_COOKIE_VARIANTS covers all three deploy shapes', () => {
218
+ assert.deepEqual(RT_COOKIE_VARIANTS, ['__Host-nastech_session_rt', '__Secure-nastech_session_rt', 'nastech_session_rt'])
219
+ })
220
+
221
+ // --- cookiesHaveLiveSession (AT or RT — the connectivity check) ---
222
+
223
+ test('cookiesHaveLiveSession is true for a live access-token cookie', () => {
224
+ assert.equal(cookiesHaveLiveSession([{ name: 'nastech_session_at', value: 'x' }]), true)
225
+ assert.equal(cookiesHaveLiveSession([{ name: '__Host-nastech_session_at', value: 'x' }]), true)
226
+ assert.equal(cookiesHaveLiveSession([{ name: '__Secure-nastech_session_at', value: 'x' }]), true)
227
+ })
228
+
229
+ test('cookiesHaveLiveSession is true for an RT cookie even with NO access-token cookie', () => {
230
+ // This is the bug-fix case: the AT cookie has lapsed (dropped from the jar)
231
+ // but the 24h RT cookie is still alive. The session is still connectable —
232
+ // the gateway rotates a fresh AT from the RT on the next request.
233
+ assert.equal(cookiesHaveLiveSession([{ name: 'nastech_session_rt', value: 'x' }]), true)
234
+ assert.equal(cookiesHaveLiveSession([{ name: '__Host-nastech_session_rt', value: 'x' }]), true)
235
+ assert.equal(cookiesHaveLiveSession([{ name: '__Secure-nastech_session_rt', value: 'x' }]), true)
236
+ })
237
+
238
+ test('cookiesHaveLiveSession is true when both AT and RT are present', () => {
239
+ assert.equal(
240
+ cookiesHaveLiveSession([
241
+ { name: 'nastech_session_at', value: 'a' },
242
+ { name: 'nastech_session_rt', value: 'r' }
243
+ ]),
244
+ true
245
+ )
246
+ })
247
+
248
+ test('cookiesHaveLiveSession is false for empty values', () => {
249
+ assert.equal(cookiesHaveLiveSession([{ name: 'nastech_session_at', value: '' }]), false)
250
+ assert.equal(cookiesHaveLiveSession([{ name: 'nastech_session_rt', value: '' }]), false)
251
+ assert.equal(
252
+ cookiesHaveLiveSession([
253
+ { name: 'nastech_session_at', value: '' },
254
+ { name: 'nastech_session_rt', value: '' }
255
+ ]),
256
+ false
257
+ )
258
+ })
259
+
260
+ test('cookiesHaveLiveSession is false for unrelated cookies and non-arrays', () => {
261
+ assert.equal(cookiesHaveLiveSession([{ name: 'other', value: 'x' }]), false)
262
+ assert.equal(cookiesHaveLiveSession(null), false)
263
+ assert.equal(cookiesHaveLiveSession(undefined), false)
264
+ assert.equal(cookiesHaveLiveSession([]), false)
265
+ })
266
+
267
+ // --- tokenPreview ---
268
+
269
+ test('tokenPreview returns null for empty', () => {
270
+ assert.equal(tokenPreview(''), null)
271
+ assert.equal(tokenPreview(null), null)
272
+ })
273
+
274
+ test('tokenPreview returns set for short tokens', () => {
275
+ assert.equal(tokenPreview('12345678'), 'set')
276
+ })
277
+
278
+ test('tokenPreview returns a masked suffix for long tokens', () => {
279
+ assert.equal(tokenPreview('abcdefghijklmnop'), '...klmnop')
280
+ })
281
+
282
+ // --- resolveTestWsUrl ---
283
+ //
284
+ // The "Test remote" button must exercise the same WS transport the app uses,
285
+ // and must FAIL (not skip) when an OAuth session can't mint a ws-ticket — that
286
+ // is the exact false-positive PR #39098 set out to eliminate.
287
+
288
+ test('resolveTestWsUrl (token mode) builds a ?token= URL the WS probe can use', async () => {
289
+ const url = await resolveTestWsUrl('https://gw.example.com', 'token', 'tok123')
290
+ assert.equal(url, 'wss://gw.example.com/api/ws?token=tok123')
291
+ })
292
+
293
+ test('resolveTestWsUrl (token mode, no token) returns null — genuine skip', async () => {
294
+ assert.equal(await resolveTestWsUrl('https://gw.example.com', 'token', null), null)
295
+ })
296
+
297
+ test('resolveTestWsUrl (oauth, mint ok) builds a ?ticket= URL', async () => {
298
+ const url = await resolveTestWsUrl('https://gw.example.com', 'oauth', null, {
299
+ mintTicket: async () => 'tkt-9'
300
+ })
301
+ assert.equal(url, 'wss://gw.example.com/api/ws?ticket=tkt-9')
302
+ })
303
+
304
+ test('resolveTestWsUrl (oauth, mint FAILS) throws — must NOT skip WS validation', async () => {
305
+ await assert.rejects(
306
+ () =>
307
+ resolveTestWsUrl('https://gw.example.com', 'oauth', null, {
308
+ mintTicket: async () => {
309
+ throw new Error('401 ticket mint failed')
310
+ }
311
+ }),
312
+ err => {
313
+ // Actionable, points the user at re-auth, and preserves the cause + flag
314
+ // the boot overlay uses to offer a sign-in prompt.
315
+ assert.match(err.message, /WebSocket ticket/i)
316
+ assert.match(err.message, /sign in again/i)
317
+ assert.equal(err.needsOauthLogin, true)
318
+ assert.ok(err.cause instanceof Error)
319
+ return true
320
+ }
321
+ )
322
+ })
323
+
324
+ test('resolveTestWsUrl (oauth) requires a mintTicket function', async () => {
325
+ await assert.rejects(
326
+ () => resolveTestWsUrl('https://gw.example.com', 'oauth', null),
327
+ /mintTicket function is required/
328
+ )
329
+ })
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Helpers for local dashboard session-token discovery.
3
+ *
4
+ * The desktop main process can pass NASTECH_DASHBOARD_SESSION_TOKEN when it
5
+ * spawns the local dashboard, but the dashboard is the source of truth for the
6
+ * token it actually serves to the renderer. If those drift, HTTP readiness
7
+ * probes still pass while /api/ws rejects the renderer's token.
8
+ */
9
+
10
+ const DEFAULT_TOKEN_FETCH_TIMEOUT_MS = 3_000
11
+
12
+ async function fetchPublicText(url, options = {}) {
13
+ const { protocol } = new URL(url)
14
+ if (protocol !== 'http:' && protocol !== 'https:') {
15
+ throw new Error(`Unsupported NasTech backend URL protocol: ${protocol}`)
16
+ }
17
+
18
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TOKEN_FETCH_TIMEOUT_MS
19
+ const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) }).catch(error => {
20
+ if (error.name === 'TimeoutError') {
21
+ throw new Error(`Timed out connecting to NasTech backend after ${timeoutMs}ms`)
22
+ }
23
+ throw error
24
+ })
25
+ const text = await res.text()
26
+
27
+ if (!res.ok) throw new Error(`${res.status}: ${text || res.statusText}`)
28
+
29
+ return text
30
+ }
31
+
32
+ function extractInjectedDashboardToken(html) {
33
+ const match = /window\.__NASTECH_SESSION_TOKEN__\s*=\s*("(?:\\.|[^"\\])*")/.exec(String(html || ''))
34
+ if (!match) return null
35
+ try {
36
+ return JSON.parse(match[1])
37
+ } catch {
38
+ return null
39
+ }
40
+ }
41
+
42
+ function dashboardIndexUrl(baseUrl) {
43
+ return `${String(baseUrl || '').replace(/\/+$/, '')}/`
44
+ }
45
+
46
+ async function resolveServedDashboardToken(baseUrl, fallbackToken, options = {}) {
47
+ const fetchText = options.fetchText || fetchPublicText
48
+ const html = await fetchText(dashboardIndexUrl(baseUrl), {
49
+ timeoutMs: options.timeoutMs ?? DEFAULT_TOKEN_FETCH_TIMEOUT_MS
50
+ })
51
+ const servedToken = extractInjectedDashboardToken(html)
52
+
53
+ if (servedToken && servedToken !== fallbackToken && typeof options.rememberLog === 'function') {
54
+ options.rememberLog('[boot] dashboard served a different session token; using served token for WebSocket auth')
55
+ }
56
+
57
+ return servedToken || fallbackToken
58
+ }
59
+
60
+ /**
61
+ * A served token that differs from our spawn token while our child is DEAD
62
+ * came from a process we did not spawn (orphan/port squatter that satisfied
63
+ * the public /api/status readiness probe). With a live child the mismatch is
64
+ * benign: our own backend regenerated the token because the env pin did not
65
+ * survive the spawn.
66
+ */
67
+ function isForeignBackendToken({ servedToken, spawnToken, childAlive }) {
68
+ return Boolean(servedToken) && servedToken !== spawnToken && !childAlive
69
+ }
70
+
71
+ /**
72
+ * Resolve the token the backend actually serves, adopting benign drift and
73
+ * failing loudly on a foreign backend. `childAlive` is a thunk so liveness is
74
+ * sampled after the fetch, not before.
75
+ */
76
+ async function adoptServedDashboardToken(baseUrl, spawnToken, { childAlive, label = 'NasTech backend', ...options }) {
77
+ const servedToken = await resolveServedDashboardToken(baseUrl, spawnToken, options).catch(error => {
78
+ options.rememberLog?.(`[boot] could not read served dashboard token (${label}): ${error.message}`)
79
+ return spawnToken
80
+ })
81
+
82
+ if (isForeignBackendToken({ servedToken, spawnToken, childAlive: childAlive() })) {
83
+ throw new Error(
84
+ `${label} exited and ${dashboardIndexUrl(baseUrl)} is served by a process we did not spawn; refusing its session token.`
85
+ )
86
+ }
87
+
88
+ return servedToken
89
+ }
90
+
91
+ module.exports = {
92
+ DEFAULT_TOKEN_FETCH_TIMEOUT_MS,
93
+ adoptServedDashboardToken,
94
+ dashboardIndexUrl,
95
+ extractInjectedDashboardToken,
96
+ fetchPublicText,
97
+ isForeignBackendToken,
98
+ resolveServedDashboardToken
99
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Tests for electron/dashboard-token.cjs.
3
+ *
4
+ * Run with: node --test electron/dashboard-token.test.cjs
5
+ * (Wired into npm test:desktop:platforms in package.json.)
6
+ */
7
+
8
+ const test = require('node:test')
9
+ const assert = require('node:assert/strict')
10
+
11
+ const {
12
+ adoptServedDashboardToken,
13
+ dashboardIndexUrl,
14
+ extractInjectedDashboardToken,
15
+ fetchPublicText,
16
+ isForeignBackendToken,
17
+ resolveServedDashboardToken
18
+ } = require('./dashboard-token.cjs')
19
+
20
+ test('extractInjectedDashboardToken reads the JSON-encoded dashboard token', () => {
21
+ const html = '<script>window.__NASTECH_SESSION_TOKEN__="served-token";window.__NASTECH_BASE_PATH__=""</script>'
22
+ assert.equal(extractInjectedDashboardToken(html), 'served-token')
23
+ })
24
+
25
+ test('extractInjectedDashboardToken handles escaped token strings', () => {
26
+ const html = '<script>window.__NASTECH_SESSION_TOKEN__="served\\\\token\\"quoted";</script>'
27
+ assert.equal(extractInjectedDashboardToken(html), 'served\\token"quoted')
28
+ })
29
+
30
+ test('extractInjectedDashboardToken returns null for missing or malformed values', () => {
31
+ assert.equal(extractInjectedDashboardToken('<html></html>'), null)
32
+ assert.equal(extractInjectedDashboardToken('<script>window.__NASTECH_SESSION_TOKEN__={bad}</script>'), null)
33
+ })
34
+
35
+ test('dashboardIndexUrl preserves dashboard path prefixes', () => {
36
+ assert.equal(dashboardIndexUrl('http://127.0.0.1:9120'), 'http://127.0.0.1:9120/')
37
+ assert.equal(dashboardIndexUrl('https://host.example/nastech/'), 'https://host.example/nastech/')
38
+ })
39
+
40
+ test('resolveServedDashboardToken uses the served token and logs when it differs', async () => {
41
+ const logs = []
42
+ const token = await resolveServedDashboardToken('http://127.0.0.1:9120', 'spawn-token', {
43
+ fetchText: async url => {
44
+ assert.equal(url, 'http://127.0.0.1:9120/')
45
+ return '<script>window.__NASTECH_SESSION_TOKEN__="served-token";</script>'
46
+ },
47
+ rememberLog: line => logs.push(line)
48
+ })
49
+
50
+ assert.equal(token, 'served-token')
51
+ assert.equal(logs.length, 1)
52
+ assert.match(logs[0], /served a different session token/)
53
+ })
54
+
55
+ test('resolveServedDashboardToken falls back when the served HTML has no token', async () => {
56
+ const token = await resolveServedDashboardToken('http://127.0.0.1:9120', 'spawn-token', {
57
+ fetchText: async () => '<html></html>',
58
+ rememberLog: () => {
59
+ throw new Error('should not log when no served token is present')
60
+ }
61
+ })
62
+
63
+ assert.equal(token, 'spawn-token')
64
+ })
65
+
66
+ test('resolveServedDashboardToken does not log when served token matches fallback', async () => {
67
+ const token = await resolveServedDashboardToken('http://127.0.0.1:9120', 'same-token', {
68
+ fetchText: async () => '<script>window.__NASTECH_SESSION_TOKEN__="same-token";</script>',
69
+ rememberLog: () => {
70
+ throw new Error('should not log when token already matches')
71
+ }
72
+ })
73
+
74
+ assert.equal(token, 'same-token')
75
+ })
76
+
77
+ test('resolveServedDashboardToken propagates fetch errors so callers can fall back explicitly', async () => {
78
+ await assert.rejects(
79
+ () =>
80
+ resolveServedDashboardToken('http://127.0.0.1:9120', 'spawn-token', {
81
+ fetchText: async () => {
82
+ throw new Error('boom')
83
+ }
84
+ }),
85
+ /boom/
86
+ )
87
+ })
88
+
89
+ test('fetchPublicText rejects unsupported protocols', async () => {
90
+ await assert.rejects(() => fetchPublicText('file:///tmp/index.html'), /Unsupported NasTech backend URL protocol/)
91
+ })
92
+
93
+ test('isForeignBackendToken only flags a mismatched token from a dead child', () => {
94
+ const cases = [
95
+ [{ servedToken: 'other', spawnToken: 'mine', childAlive: false }, true],
96
+ // Live child + drift = our backend regenerated the token (env pin lost).
97
+ [{ servedToken: 'other', spawnToken: 'mine', childAlive: true }, false],
98
+ [{ servedToken: 'mine', spawnToken: 'mine', childAlive: false }, false],
99
+ [{ servedToken: 'mine', spawnToken: 'mine', childAlive: true }, false],
100
+ [{ servedToken: null, spawnToken: 'mine', childAlive: false }, false],
101
+ [{ servedToken: '', spawnToken: 'mine', childAlive: false }, false]
102
+ ]
103
+ for (const [input, expected] of cases) {
104
+ assert.equal(isForeignBackendToken(input), expected, JSON.stringify(input))
105
+ }
106
+ })
107
+
108
+ test('adoptServedDashboardToken adopts drift from a live child', async () => {
109
+ const token = await adoptServedDashboardToken('http://127.0.0.1:9120', 'spawn-token', {
110
+ childAlive: () => true,
111
+ fetchText: async () => '<script>window.__NASTECH_SESSION_TOKEN__="served-token";</script>'
112
+ })
113
+
114
+ assert.equal(token, 'served-token')
115
+ })
116
+
117
+ test('adoptServedDashboardToken refuses a foreign token when our child is dead', async () => {
118
+ await assert.rejects(
119
+ () =>
120
+ adoptServedDashboardToken('http://127.0.0.1:9120', 'spawn-token', {
121
+ childAlive: () => false,
122
+ fetchText: async () => '<script>window.__NASTECH_SESSION_TOKEN__="squatter-token";</script>',
123
+ label: 'NasTech backend for profile "work"'
124
+ }),
125
+ /profile "work".*process we did not spawn/
126
+ )
127
+ })
128
+
129
+ test('adoptServedDashboardToken falls back to the spawn token when the fetch fails', async () => {
130
+ const logs = []
131
+ const token = await adoptServedDashboardToken('http://127.0.0.1:9120', 'spawn-token', {
132
+ childAlive: () => true,
133
+ fetchText: async () => {
134
+ throw new Error('boom')
135
+ },
136
+ rememberLog: line => logs.push(line)
137
+ })
138
+
139
+ assert.equal(token, 'spawn-token')
140
+ assert.equal(logs.length, 1)
141
+ assert.match(logs[0], /could not read served dashboard token \(NasTech backend\): boom/)
142
+ })