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,364 @@
1
+ 'use strict'
2
+
3
+ const assert = require('node:assert/strict')
4
+ const fs = require('node:fs')
5
+ const os = require('node:os')
6
+ const path = require('node:path')
7
+ const test = require('node:test')
8
+ const { pathToFileURL } = require('node:url')
9
+
10
+ const { readDirForIpc } = require('./fs-read-dir.cjs')
11
+
12
+ function mkTmpDir() {
13
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'nastech-fs-read-dir-'))
14
+ }
15
+
16
+ function fakeDirent(name, flags = {}) {
17
+ return {
18
+ name,
19
+ isDirectory: () => Boolean(flags.directory),
20
+ isFile: () => Boolean(flags.file),
21
+ isSymbolicLink: () => Boolean(flags.symlink)
22
+ }
23
+ }
24
+
25
+ test('readDirForIpc hides noisy directories and files from the project tree', async () => {
26
+ const root = mkTmpDir()
27
+
28
+ try {
29
+ fs.mkdirSync(path.join(root, 'node_modules'))
30
+ fs.mkdirSync(path.join(root, 'src'))
31
+ fs.writeFileSync(path.join(root, 'target'), 'hidden file')
32
+ fs.writeFileSync(path.join(root, 'README.md'), 'visible file')
33
+
34
+ const result = await readDirForIpc(root)
35
+
36
+ assert.equal(result.error, undefined)
37
+ assert.deepEqual(
38
+ result.entries.map(entry => entry.name),
39
+ ['src', 'README.md']
40
+ )
41
+ } finally {
42
+ fs.rmSync(root, { recursive: true, force: true })
43
+ }
44
+ })
45
+
46
+ test('readDirForIpc filters a hidden basename whether it is a file or directory', async () => {
47
+ const dirRoot = mkTmpDir()
48
+ const fileRoot = mkTmpDir()
49
+
50
+ try {
51
+ fs.mkdirSync(path.join(dirRoot, 'node_modules'))
52
+ fs.writeFileSync(path.join(dirRoot, 'visible.txt'), 'visible')
53
+ fs.writeFileSync(path.join(fileRoot, 'node_modules'), 'hidden file')
54
+ fs.writeFileSync(path.join(fileRoot, 'visible.txt'), 'visible')
55
+
56
+ assert.deepEqual(
57
+ (await readDirForIpc(dirRoot)).entries.map(entry => entry.name),
58
+ ['visible.txt']
59
+ )
60
+ assert.deepEqual(
61
+ (await readDirForIpc(fileRoot)).entries.map(entry => entry.name),
62
+ ['visible.txt']
63
+ )
64
+ } finally {
65
+ fs.rmSync(dirRoot, { recursive: true, force: true })
66
+ fs.rmSync(fileRoot, { recursive: true, force: true })
67
+ }
68
+ })
69
+
70
+ test('readDirForIpc returns directories before files and sorts by name within groups', async () => {
71
+ const root = mkTmpDir()
72
+
73
+ try {
74
+ fs.writeFileSync(path.join(root, 'z.txt'), 'z')
75
+ fs.mkdirSync(path.join(root, 'src'))
76
+ fs.writeFileSync(path.join(root, 'a.txt'), 'a')
77
+ fs.mkdirSync(path.join(root, 'lib'))
78
+
79
+ const result = await readDirForIpc(root)
80
+
81
+ assert.equal(result.error, undefined)
82
+ assert.deepEqual(
83
+ result.entries.map(entry => entry.name),
84
+ ['lib', 'src', 'a.txt', 'z.txt']
85
+ )
86
+ } finally {
87
+ fs.rmSync(root, { recursive: true, force: true })
88
+ }
89
+ })
90
+
91
+ test('readDirForIpc accepts file URLs for directories', async () => {
92
+ const root = mkTmpDir()
93
+
94
+ try {
95
+ fs.mkdirSync(path.join(root, 'src'))
96
+ fs.writeFileSync(path.join(root, 'README.md'), 'visible file')
97
+
98
+ const result = await readDirForIpc(pathToFileURL(root).toString())
99
+
100
+ assert.equal(result.error, undefined)
101
+ assert.deepEqual(
102
+ result.entries.map(entry => entry.name),
103
+ ['src', 'README.md']
104
+ )
105
+ } finally {
106
+ fs.rmSync(root, { recursive: true, force: true })
107
+ }
108
+ })
109
+
110
+ test('readDirForIpc returns invalid-path for blank or non-string input', async () => {
111
+ let readdirCalls = 0
112
+ const fsImpl = {
113
+ promises: {
114
+ readdir: async () => {
115
+ readdirCalls += 1
116
+ return []
117
+ }
118
+ }
119
+ }
120
+
121
+ assert.deepEqual(await readDirForIpc('', { fs: fsImpl }), { entries: [], error: 'invalid-path' })
122
+ assert.deepEqual(await readDirForIpc(' ', { fs: fsImpl }), { entries: [], error: 'invalid-path' })
123
+ assert.deepEqual(await readDirForIpc(null, { fs: fsImpl }), { entries: [], error: 'invalid-path' })
124
+ assert.equal(readdirCalls, 0)
125
+ })
126
+
127
+ test('readDirForIpc rejects Windows device paths before readdir', async () => {
128
+ let readdirCalls = 0
129
+ const fsImpl = {
130
+ promises: {
131
+ readdir: async () => {
132
+ readdirCalls += 1
133
+ return []
134
+ }
135
+ }
136
+ }
137
+
138
+ assert.deepEqual(await readDirForIpc('\\\\?\\C:\\secret', { fs: fsImpl }), {
139
+ entries: [],
140
+ error: 'device-path'
141
+ })
142
+ assert.equal(readdirCalls, 0)
143
+ })
144
+
145
+ test('readDirForIpc returns filesystem error codes instead of throwing', async () => {
146
+ const root = mkTmpDir()
147
+
148
+ try {
149
+ const result = await readDirForIpc(path.join(root, 'missing'))
150
+
151
+ assert.deepEqual(result, { entries: [], error: 'ENOENT' })
152
+ } finally {
153
+ fs.rmSync(root, { recursive: true, force: true })
154
+ }
155
+ })
156
+
157
+ test('readDirForIpc marks a symlink to a directory as a directory', async t => {
158
+ const root = mkTmpDir()
159
+
160
+ try {
161
+ fs.mkdirSync(path.join(root, 'actual-dir'))
162
+
163
+ try {
164
+ fs.symlinkSync(path.join(root, 'actual-dir'), path.join(root, 'linked-dir'), 'dir')
165
+ } catch (error) {
166
+ if (error?.code === 'EPERM' || error?.code === 'EACCES') {
167
+ t.skip(`symlink creation is not permitted on this platform (${error.code})`)
168
+
169
+ return
170
+ }
171
+
172
+ throw error
173
+ }
174
+
175
+ const result = await readDirForIpc(root)
176
+ const linked = result.entries.find(entry => entry.name === 'linked-dir')
177
+
178
+ assert.equal(result.error, undefined)
179
+ assert.equal(linked?.isDirectory, true)
180
+ } finally {
181
+ fs.rmSync(root, { recursive: true, force: true })
182
+ }
183
+ })
184
+
185
+ test('readDirForIpc marks a Windows junction to a directory as a directory', async t => {
186
+ if (process.platform !== 'win32') {
187
+ t.skip('junctions are a Windows-specific symlink type')
188
+
189
+ return
190
+ }
191
+
192
+ const root = mkTmpDir()
193
+
194
+ try {
195
+ fs.mkdirSync(path.join(root, 'actual-dir'))
196
+
197
+ try {
198
+ fs.symlinkSync(path.join(root, 'actual-dir'), path.join(root, 'junction-dir'), 'junction')
199
+ } catch (error) {
200
+ if (error?.code === 'EPERM' || error?.code === 'EACCES') {
201
+ t.skip(`junction creation is not permitted on this platform (${error.code})`)
202
+
203
+ return
204
+ }
205
+
206
+ throw error
207
+ }
208
+
209
+ const result = await readDirForIpc(root)
210
+ const junction = result.entries.find(entry => entry.name === 'junction-dir')
211
+
212
+ assert.equal(result.error, undefined)
213
+ assert.equal(junction?.isDirectory, true)
214
+ } finally {
215
+ fs.rmSync(root, { recursive: true, force: true })
216
+ }
217
+ })
218
+
219
+ test('readDirForIpc allows expanding symlink or junction directories outside the project root', async t => {
220
+ const root = mkTmpDir()
221
+ const outside = mkTmpDir()
222
+
223
+ try {
224
+ fs.writeFileSync(path.join(outside, 'outside.txt'), 'ok')
225
+
226
+ const linkPath = path.join(root, 'outside-link')
227
+ try {
228
+ fs.symlinkSync(outside, linkPath, process.platform === 'win32' ? 'junction' : 'dir')
229
+ } catch (error) {
230
+ if (error?.code === 'EPERM' || error?.code === 'EACCES') {
231
+ t.skip(`directory symlink creation is not permitted on this platform (${error.code})`)
232
+
233
+ return
234
+ }
235
+
236
+ throw error
237
+ }
238
+
239
+ const result = await readDirForIpc(linkPath)
240
+
241
+ assert.equal(result.error, undefined)
242
+ assert.deepEqual(result.entries, [
243
+ { name: 'outside.txt', path: path.join(linkPath, 'outside.txt'), isDirectory: false }
244
+ ])
245
+ } finally {
246
+ fs.rmSync(root, { recursive: true, force: true })
247
+ fs.rmSync(outside, { recursive: true, force: true })
248
+ }
249
+ })
250
+
251
+ test('readDirForIpc stats symbolic links and unknown entries without dropping the whole listing', async () => {
252
+ const input = path.join('virtual-root')
253
+ const resolved = path.resolve(input)
254
+ const statCalls = []
255
+ const fsImpl = {
256
+ promises: {
257
+ readdir: async () => [
258
+ fakeDirent('unknown-entry'),
259
+ fakeDirent('linked-dir', { symlink: true }),
260
+ fakeDirent('broken-link', { symlink: true }),
261
+ fakeDirent('plain.txt', { file: true })
262
+ ],
263
+ stat: async fullPath => {
264
+ if (fullPath === resolved) {
265
+ return { isDirectory: () => true }
266
+ }
267
+
268
+ statCalls.push(fullPath)
269
+ if (fullPath.endsWith(`${path.sep}linked-dir`)) {
270
+ return { isDirectory: () => true }
271
+ }
272
+ throw Object.assign(new Error('gone'), { code: 'ENOENT' })
273
+ }
274
+ }
275
+ }
276
+
277
+ const result = await readDirForIpc(input, { fs: fsImpl })
278
+
279
+ assert.equal(result.error, undefined)
280
+ assert.deepEqual(
281
+ statCalls.sort(),
282
+ [path.join(resolved, 'broken-link'), path.join(resolved, 'linked-dir'), path.join(resolved, 'unknown-entry')].sort()
283
+ )
284
+ assert.deepEqual(result.entries, [
285
+ { name: 'linked-dir', path: path.join(resolved, 'linked-dir'), isDirectory: true },
286
+ { name: 'broken-link', path: path.join(resolved, 'broken-link'), isDirectory: false },
287
+ { name: 'plain.txt', path: path.join(resolved, 'plain.txt'), isDirectory: false },
288
+ { name: 'unknown-entry', path: path.join(resolved, 'unknown-entry'), isDirectory: false }
289
+ ])
290
+ })
291
+
292
+ test('readDirForIpc bounds concurrent stats while preserving complete sorted output', async () => {
293
+ const input = path.join('virtual-root')
294
+ const resolved = path.resolve(input)
295
+ const names = Array.from({ length: 105 }, (_, index) => `entry-${String(104 - index).padStart(3, '0')}`)
296
+ const failedName = 'entry-100'
297
+ const directoryNames = new Set(names.filter((_, index) => index % 10 === 4))
298
+ const successfulDirectoryNames = new Set([...directoryNames].filter(name => name !== failedName))
299
+ const statCalls = []
300
+ let active = 0
301
+ let peak = 0
302
+ let releaseStats
303
+ let markFirstStatStarted
304
+ const statsReleased = new Promise(resolve => {
305
+ releaseStats = resolve
306
+ })
307
+ const firstStatStarted = new Promise(resolve => {
308
+ markFirstStatStarted = resolve
309
+ })
310
+ const fsImpl = {
311
+ promises: {
312
+ readdir: async () => [
313
+ fakeDirent('node_modules', { symlink: true }),
314
+ ...names.map((name, index) => fakeDirent(name, { symlink: index % 2 === 0 }))
315
+ ],
316
+ stat: async fullPath => {
317
+ if (fullPath === resolved) {
318
+ return { isDirectory: () => true }
319
+ }
320
+
321
+ statCalls.push(fullPath)
322
+ active += 1
323
+ peak = Math.max(peak, active)
324
+ markFirstStatStarted()
325
+ await statsReleased
326
+ active -= 1
327
+
328
+ const name = path.basename(fullPath)
329
+ if (name === failedName) {
330
+ throw Object.assign(new Error('gone'), { code: 'ENOENT' })
331
+ }
332
+
333
+ return { isDirectory: () => successfulDirectoryNames.has(name) }
334
+ }
335
+ }
336
+ }
337
+
338
+ const resultPromise = readDirForIpc(input, { fs: fsImpl })
339
+ await firstStatStarted
340
+ await new Promise(resolve => setImmediate(resolve))
341
+ releaseStats()
342
+ const result = await resultPromise
343
+
344
+ const expectedNames = [
345
+ ...names.filter(name => successfulDirectoryNames.has(name)).sort(),
346
+ ...names.filter(name => !successfulDirectoryNames.has(name)).sort()
347
+ ]
348
+
349
+ assert.equal(result.error, undefined)
350
+ assert.equal(result.entries.length, names.length)
351
+ assert.equal(statCalls.length, names.length)
352
+ assert.equal(statCalls.some(fullPath => fullPath.endsWith(`${path.sep}node_modules`)), false)
353
+ assert.ok(peak > 1, `expected concurrent stats, observed peak ${peak}`)
354
+ assert.ok(peak <= 16, `expected at most 16 concurrent stats, observed peak ${peak}`)
355
+ assert.deepEqual(
356
+ result.entries.map(entry => entry.name),
357
+ expectedNames
358
+ )
359
+ assert.equal(result.entries.find(entry => entry.name === failedName)?.isDirectory, false)
360
+ assert.equal(
361
+ result.entries.filter(entry => entry.isDirectory).length,
362
+ successfulDirectoryNames.size
363
+ )
364
+ })
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Live WebSocket validation for the remote-gateway "Test remote" button.
3
+ *
4
+ * Background: the desktop boot does two independent things to a remote gateway:
5
+ *
6
+ * 1. The MAIN process hits ``GET /api/status`` over HTTP (token in a header)
7
+ * to confirm the backend is up. This is what "Test remote" historically
8
+ * checked, and what the boot logs print as "Remote NasTech backend is
9
+ * ready".
10
+ * 2. The RENDERER then opens a live WebSocket to ``/api/ws`` (credential in a
11
+ * query param) via ``gateway.connect()``. The chat surface only works once
12
+ * THIS succeeds.
13
+ *
14
+ * Those two paths use different processes, transports, and credentials, and the
15
+ * server applies extra guards to the WS upgrade that the HTTP status route never
16
+ * sees (Host/Origin checks, ws-ticket/token auth, peer-IP checks). So a gateway
17
+ * can pass the HTTP status check yet reject the WebSocket — which surfaces to
18
+ * the user as a green "Test remote" followed by an opaque "Could not connect to
19
+ * NasTech gateway" on the boot overlay.
20
+ *
21
+ * This module performs the second half of the check: it actually opens the WS
22
+ * URL and confirms the upgrade is accepted (and isn't immediately torn down by
23
+ * a post-upgrade auth rejection). The ``WebSocketImpl`` is injectable so the
24
+ * unit tests can drive the handshake without a real socket; in production the
25
+ * caller passes the Node/Electron global ``WebSocket``.
26
+ */
27
+
28
+ const DEFAULT_CONNECT_TIMEOUT_MS = 10_000
29
+ // After the upgrade is accepted, a gateway that rejects the credential
30
+ // post-handshake closes the socket almost immediately. Wait a short grace
31
+ // window: a frame (gateway.ready) or a still-open socket means success; an
32
+ // early close means the upgrade was accepted but the session was refused.
33
+ const DEFAULT_READY_GRACE_MS = 750
34
+
35
+ /**
36
+ * Attempt a live WebSocket connection and classify the outcome.
37
+ *
38
+ * @param {string} wsUrl - Fully-formed ws(s):// URL including the credential.
39
+ * @param {object} [options]
40
+ * @param {new (url: string) => any} [options.WebSocketImpl] - WebSocket ctor.
41
+ * @param {number} [options.connectTimeoutMs]
42
+ * @param {number} [options.readyGraceMs]
43
+ * @returns {Promise<{ ok: boolean, reason?: string }>}
44
+ */
45
+ function probeGatewayWebSocket(wsUrl, options = {}) {
46
+ const WebSocketImpl = options.WebSocketImpl
47
+ const connectTimeoutMs = options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS
48
+ const readyGraceMs = options.readyGraceMs ?? DEFAULT_READY_GRACE_MS
49
+
50
+ if (typeof WebSocketImpl !== 'function') {
51
+ return Promise.resolve({
52
+ ok: false,
53
+ reason: 'WebSocket is not available in this runtime.'
54
+ })
55
+ }
56
+
57
+ return new Promise(resolve => {
58
+ let settled = false
59
+ let opened = false
60
+ let connectTimer = null
61
+ let graceTimer = null
62
+ let socket
63
+
64
+ const clearTimers = () => {
65
+ if (connectTimer !== null) {
66
+ clearTimeout(connectTimer)
67
+ connectTimer = null
68
+ }
69
+ if (graceTimer !== null) {
70
+ clearTimeout(graceTimer)
71
+ graceTimer = null
72
+ }
73
+ }
74
+
75
+ const finish = result => {
76
+ if (settled) return
77
+ settled = true
78
+ clearTimers()
79
+ try {
80
+ socket?.close?.()
81
+ } catch {
82
+ // ignore — best effort teardown
83
+ }
84
+ resolve(result)
85
+ }
86
+
87
+ try {
88
+ socket = new WebSocketImpl(wsUrl)
89
+ } catch (error) {
90
+ finish({
91
+ ok: false,
92
+ reason: error instanceof Error ? error.message : String(error)
93
+ })
94
+ return
95
+ }
96
+
97
+ const onOpen = () => {
98
+ if (settled) return
99
+ opened = true
100
+ // Upgrade accepted. Give the server a brief window to reject the
101
+ // credential post-handshake (early close) before declaring success.
102
+ graceTimer = setTimeout(() => {
103
+ finish({ ok: true })
104
+ }, readyGraceMs)
105
+ }
106
+
107
+ const onMessage = () => {
108
+ // Any frame means the gateway accepted us and is talking — unambiguous
109
+ // success, no need to wait out the grace window.
110
+ finish({ ok: true })
111
+ }
112
+
113
+ const onError = event => {
114
+ finish({
115
+ ok: false,
116
+ reason: extractErrorReason(event) || 'WebSocket connection failed.'
117
+ })
118
+ }
119
+
120
+ const onClose = event => {
121
+ if (settled) return
122
+ if (opened) {
123
+ // Opened, then closed inside the grace window: the upgrade was accepted
124
+ // but the session was refused (e.g. ws-ticket/token rejected, or a
125
+ // server-side Host/Origin guard tripped after accept).
126
+ finish({
127
+ ok: false,
128
+ reason: closeReason(event, 'The gateway accepted the connection then closed it (credential rejected?).')
129
+ })
130
+ return
131
+ }
132
+ finish({
133
+ ok: false,
134
+ reason: closeReason(event, 'The gateway closed the WebSocket before it opened.')
135
+ })
136
+ }
137
+
138
+ addListener(socket, 'open', onOpen)
139
+ addListener(socket, 'message', onMessage)
140
+ addListener(socket, 'error', onError)
141
+ addListener(socket, 'close', onClose)
142
+
143
+ if (connectTimeoutMs > 0) {
144
+ connectTimer = setTimeout(() => {
145
+ finish({
146
+ ok: false,
147
+ reason: `Timed out after ${connectTimeoutMs}ms waiting for the WebSocket to open.`
148
+ })
149
+ }, connectTimeoutMs)
150
+ }
151
+ })
152
+ }
153
+
154
+ function addListener(socket, type, handler) {
155
+ if (typeof socket.addEventListener === 'function') {
156
+ socket.addEventListener(type, handler)
157
+ return
158
+ }
159
+ // Node's global WebSocket implements addEventListener; this fallback keeps the
160
+ // helper usable with the `ws` package's EventEmitter shape too.
161
+ if (typeof socket.on === 'function') {
162
+ socket.on(type, handler)
163
+ }
164
+ }
165
+
166
+ function extractErrorReason(event) {
167
+ if (!event) return ''
168
+ if (event instanceof Error) return event.message
169
+ const err = event.error || event.message
170
+ if (err instanceof Error) return err.message
171
+ if (typeof err === 'string') return err
172
+ return ''
173
+ }
174
+
175
+ function closeReason(event, fallback) {
176
+ const code = event && typeof event.code === 'number' ? event.code : null
177
+ const reason = event && typeof event.reason === 'string' ? event.reason.trim() : ''
178
+ if (code && reason) return `${fallback} (code ${code}: ${reason})`
179
+ if (code) return `${fallback} (code ${code})`
180
+ if (reason) return `${fallback} (${reason})`
181
+ return fallback
182
+ }
183
+
184
+ module.exports = {
185
+ DEFAULT_CONNECT_TIMEOUT_MS,
186
+ DEFAULT_READY_GRACE_MS,
187
+ probeGatewayWebSocket
188
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Tests for electron/gateway-ws-probe.cjs.
3
+ *
4
+ * Run with: node --test electron/gateway-ws-probe.test.cjs
5
+ * (Wired into npm test:desktop:platforms in package.json.)
6
+ *
7
+ * The probe drives a real WebSocket handshake for the "Test remote" button.
8
+ * Here we inject a fake socket so we can deterministically replay each handshake
9
+ * outcome (open, frame, error, early close, never-opens) without a network.
10
+ */
11
+
12
+ const test = require('node:test')
13
+ const assert = require('node:assert/strict')
14
+
15
+ const { probeGatewayWebSocket } = require('./gateway-ws-probe.cjs')
16
+
17
+ // Minimal WebSocket double: records listeners synchronously (the probe attaches
18
+ // them in its executor) and exposes emit() so the test can replay events.
19
+ function makeFakeWs() {
20
+ const instances = []
21
+ class FakeWs {
22
+ constructor(url) {
23
+ this.url = url
24
+ this.listeners = {}
25
+ this.closed = false
26
+ instances.push(this)
27
+ }
28
+ addEventListener(type, fn) {
29
+ ;(this.listeners[type] ||= []).push(fn)
30
+ }
31
+ close() {
32
+ this.closed = true
33
+ }
34
+ emit(type, event) {
35
+ for (const fn of this.listeners[type] || []) fn(event)
36
+ }
37
+ }
38
+ return { FakeWs, instances }
39
+ }
40
+
41
+ const FAST = { connectTimeoutMs: 1_000, readyGraceMs: 10 }
42
+
43
+ test('probe resolves ok when the socket opens and stays open', async () => {
44
+ const { FakeWs, instances } = makeFakeWs()
45
+ const promise = probeGatewayWebSocket('ws://host/api/ws?token=t', { WebSocketImpl: FakeWs, ...FAST })
46
+ instances[0].emit('open')
47
+ const result = await promise
48
+ assert.deepEqual(result, { ok: true })
49
+ assert.equal(instances[0].closed, true)
50
+ })
51
+
52
+ test('probe resolves ok immediately when a frame arrives', async () => {
53
+ const { FakeWs, instances } = makeFakeWs()
54
+ const promise = probeGatewayWebSocket('ws://host/api/ws?token=t', {
55
+ WebSocketImpl: FakeWs,
56
+ connectTimeoutMs: 1_000,
57
+ readyGraceMs: 10_000 // long grace: success must come from the frame, not the timer
58
+ })
59
+ instances[0].emit('open')
60
+ instances[0].emit('message', { data: '{"jsonrpc":"2.0"}' })
61
+ const result = await promise
62
+ assert.deepEqual(result, { ok: true })
63
+ })
64
+
65
+ test('probe fails when the socket errors before opening', async () => {
66
+ const { FakeWs, instances } = makeFakeWs()
67
+ const promise = probeGatewayWebSocket('ws://host/api/ws?token=t', { WebSocketImpl: FakeWs, ...FAST })
68
+ instances[0].emit('error', { message: 'ECONNREFUSED' })
69
+ const result = await promise
70
+ assert.equal(result.ok, false)
71
+ assert.match(result.reason, /ECONNREFUSED/)
72
+ })
73
+
74
+ test('probe fails when the gateway closes before opening', async () => {
75
+ const { FakeWs, instances } = makeFakeWs()
76
+ const promise = probeGatewayWebSocket('ws://host/api/ws?token=t', { WebSocketImpl: FakeWs, ...FAST })
77
+ instances[0].emit('close', { code: 1006 })
78
+ const result = await promise
79
+ assert.equal(result.ok, false)
80
+ assert.match(result.reason, /before it opened/)
81
+ assert.match(result.reason, /1006/)
82
+ })
83
+
84
+ test('probe fails when the gateway accepts then immediately closes (auth rejected)', async () => {
85
+ const { FakeWs, instances } = makeFakeWs()
86
+ const promise = probeGatewayWebSocket('ws://host/api/ws?token=t', { WebSocketImpl: FakeWs, ...FAST })
87
+ instances[0].emit('open')
88
+ instances[0].emit('close', { code: 4403, reason: 'forbidden' })
89
+ const result = await promise
90
+ assert.equal(result.ok, false)
91
+ assert.match(result.reason, /credential rejected/)
92
+ assert.match(result.reason, /4403/)
93
+ assert.match(result.reason, /forbidden/)
94
+ })
95
+
96
+ test('probe times out when the socket never opens', async () => {
97
+ const { FakeWs } = makeFakeWs()
98
+ const result = await probeGatewayWebSocket('ws://host/api/ws?token=t', {
99
+ WebSocketImpl: FakeWs,
100
+ connectTimeoutMs: 20,
101
+ readyGraceMs: 10
102
+ })
103
+ assert.equal(result.ok, false)
104
+ assert.match(result.reason, /Timed out/)
105
+ })
106
+
107
+ test('probe fails gracefully when the constructor throws', async () => {
108
+ class ThrowingWs {
109
+ constructor() {
110
+ throw new Error('bad url')
111
+ }
112
+ }
113
+ const result = await probeGatewayWebSocket('ws://host/api/ws', { WebSocketImpl: ThrowingWs, ...FAST })
114
+ assert.equal(result.ok, false)
115
+ assert.match(result.reason, /bad url/)
116
+ })
117
+
118
+ test('probe reports unavailable when no WebSocket implementation is provided', async () => {
119
+ const result = await probeGatewayWebSocket('ws://host/api/ws', { WebSocketImpl: undefined })
120
+ assert.equal(result.ok, false)
121
+ assert.match(result.reason, /not available/)
122
+ })