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,22 @@
1
+ // Shared chrome for the top-center floating HUDs (command palette + session
2
+ // switcher). They pin just under the title bar, centered, and lean on a crisp
3
+ // border + shadow to separate from the app — no dimming/blurring backdrop.
4
+ // Each caller layers on its own z-index, width, and overflow.
5
+ export const HUD_POSITION = 'fixed left-1/2 top-3 -translate-x-1/2'
6
+
7
+ // Matches the app's borderless-overlay surface (dialog, keybind panel, …):
8
+ // hairline `--stroke-nastech` paired with the soft `--shadow-nastech` float.
9
+ export const HUD_SURFACE = 'rounded-xl border border-(--stroke-nastech) bg-(--ui-chat-bubble-background) shadow-nastech'
10
+
11
+ // One row/text size for both HUDs (compact — two notches under `text-sm`).
12
+ export const HUD_TEXT = 'text-xs'
13
+
14
+ // Shared item layout + padding for both HUDs. Tight vertical rhythm so rows
15
+ // don't feel chunky; overrides the shadcn `CommandItem` default (`px-2 py-1.5`).
16
+ export const HUD_ITEM = 'gap-2 px-2 py-1'
17
+
18
+ // Section headings styled like the sidebar panel labels: brand-tinted, uppercase,
19
+ // tightly tracked — plain text, no sticky chrome bar. Targets the cmdk group
20
+ // heading via the universal-descendant variant.
21
+ export const HUD_HEADING =
22
+ '**:[[cmdk-group-heading]]:static **:[[cmdk-group-heading]]:bg-transparent **:[[cmdk-group-heading]]:px-2.5 **:[[cmdk-group-heading]]:pb-1 **:[[cmdk-group-heading]]:pt-2.5 **:[[cmdk-group-heading]]:text-[0.64rem] **:[[cmdk-group-heading]]:font-semibold **:[[cmdk-group-heading]]:uppercase **:[[cmdk-group-heading]]:tracking-[0.16em] **:[[cmdk-group-heading]]:text-(--theme-primary)'
@@ -0,0 +1,265 @@
1
+ import { act, cleanup, render } from '@testing-library/react'
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
3
+
4
+ import { $desktopBoot } from '@/store/boot'
5
+ import { $gatewayState } from '@/store/session'
6
+
7
+ import { useGatewayBoot } from './use-gateway-boot'
8
+
9
+ // End-to-end-ish repro of the "remote VPS → stuck on CONNECTING, no Settings"
10
+ // bug that drives the REAL useGatewayBoot hook + REAL NasTechGateway through a
11
+ // fake WebSocket we fully control. No Docker / no real port: from the desktop's
12
+ // point of view a "remote VPS" is just a WebSocket that opens once and later
13
+ // refuses to reopen, so that is exactly (and only) what we fake.
14
+ //
15
+ // The previous test (gateway-connecting-overlay.test.tsx) hand-set the stores
16
+ // and asserted the overlays; this one proves the HOOK actually PRODUCES that
17
+ // stuck store combo — closing the "inferred by reading code" gap on the
18
+ // post-boot reconnect loop.
19
+
20
+ type Listener = (ev: unknown) => void
21
+
22
+ // Minimal WebSocket stand-in implementing only what json-rpc-gateway.connect()
23
+ // touches: readyState, add/removeEventListener('open'|'error'|'close'), close().
24
+ class FakeWebSocket {
25
+ static OPEN = 1
26
+ static CLOSED = 3
27
+ // Flipped by the test: 'open' = next socket connects; 'fail' = next socket
28
+ // errors (a dead remote). Mirrors a VPS going away after the first connect.
29
+ static mode: 'open' | 'fail' = 'open'
30
+ static instances: FakeWebSocket[] = []
31
+
32
+ readyState = 0
33
+ private listeners: Record<string, Set<Listener>> = {}
34
+
35
+ constructor(public url: string) {
36
+ FakeWebSocket.instances.push(this)
37
+ const willOpen = FakeWebSocket.mode === 'open'
38
+ // Resolve on the next microtask/macrotask so connect()'s promise wiring is
39
+ // in place before open/error fires (matches real async socket handshake).
40
+ setTimeout(() => {
41
+ if (willOpen) {
42
+ this.readyState = FakeWebSocket.OPEN
43
+ this.emit('open', {})
44
+ } else {
45
+ this.readyState = FakeWebSocket.CLOSED
46
+ this.emit('error', {})
47
+ }
48
+ }, 0)
49
+ }
50
+
51
+ addEventListener(type: string, fn: Listener) {
52
+ ;(this.listeners[type] ??= new Set()).add(fn)
53
+ }
54
+
55
+ removeEventListener(type: string, fn: Listener) {
56
+ this.listeners[type]?.delete(fn)
57
+ }
58
+
59
+ close() {
60
+ this.readyState = FakeWebSocket.CLOSED
61
+ this.emit('close', {})
62
+ }
63
+
64
+ // Force-drop an open socket, as a sleeping laptop / restarted remote would.
65
+ drop() {
66
+ this.readyState = FakeWebSocket.CLOSED
67
+ this.emit('close', {})
68
+ }
69
+
70
+ private emit(type: string, ev: unknown) {
71
+ for (const fn of this.listeners[type] ?? []) fn(ev)
72
+ }
73
+ }
74
+
75
+ function fakeDesktop() {
76
+ const conn = {
77
+ authMode: 'token' as const,
78
+ baseUrl: 'https://vps.example.com',
79
+ profile: 'default',
80
+ token: 't',
81
+ wsUrl: 'wss://vps.example.com/api/ws?token=t'
82
+ }
83
+
84
+ return {
85
+ getConnection: vi.fn(async () => conn),
86
+ getGatewayWsUrl: vi.fn(async () => conn.wsUrl),
87
+ getBootProgress: vi.fn(async () => ({
88
+ error: null,
89
+ fakeMode: false,
90
+ message: '',
91
+ phase: 'init',
92
+ progress: 0,
93
+ running: true,
94
+ timestamp: Date.now()
95
+ })),
96
+ onBootProgress: vi.fn(() => () => undefined),
97
+ onBackendExit: vi.fn(() => () => undefined),
98
+ onPowerResume: vi.fn(() => () => undefined),
99
+ onWindowStateChanged: vi.fn(() => () => undefined),
100
+ touchBackend: vi.fn(async () => undefined),
101
+ profile: { get: vi.fn(async () => ({ profile: 'default' })) }
102
+ }
103
+ }
104
+
105
+ function Harness() {
106
+ useGatewayBoot({
107
+ handleGatewayEvent: () => undefined,
108
+ onConnectionReady: () => undefined,
109
+ onGatewayReady: () => undefined,
110
+ refreshNasTechConfig: async () => undefined,
111
+ refreshSessions: async () => undefined
112
+ })
113
+
114
+ return null
115
+ }
116
+
117
+ const originalWebSocket = globalThis.WebSocket
118
+
119
+ beforeEach(() => {
120
+ vi.useFakeTimers()
121
+ FakeWebSocket.mode = 'open'
122
+ FakeWebSocket.instances = []
123
+ ;(globalThis as { WebSocket: unknown }).WebSocket = FakeWebSocket
124
+ ;(window as { NASTECHDesktop?: unknown }).NASTECHDesktop = fakeDesktop()
125
+ $gatewayState.set('idle')
126
+ $desktopBoot.set({
127
+ error: null,
128
+ fakeMode: false,
129
+ message: '',
130
+ phase: 'init',
131
+ progress: 0,
132
+ running: true,
133
+ timestamp: Date.now(),
134
+ visible: true
135
+ })
136
+ })
137
+
138
+ afterEach(() => {
139
+ cleanup()
140
+ vi.useRealTimers()
141
+ ;(globalThis as { WebSocket: unknown }).WebSocket = originalWebSocket
142
+ delete (window as { NASTECHDesktop?: unknown }).NASTECHDesktop
143
+ })
144
+
145
+ // Let pending microtasks (awaits) AND the queued 0ms socket open/error fire.
146
+ async function flushAsync() {
147
+ await act(async () => {
148
+ await vi.advanceTimersByTimeAsync(0)
149
+ })
150
+ }
151
+
152
+ // Drive the exponential backoff forward by its full cap so the next scheduled
153
+ // reconnect attempt actually runs (1s,2s,4s,8s,15s,15s…). Returns after the
154
+ // attempt's async work settles.
155
+ async function advanceBackoff() {
156
+ await act(async () => {
157
+ await vi.advanceTimersByTimeAsync(15_000)
158
+ })
159
+ }
160
+
161
+ describe('useGatewayBoot remote reconnect loop (real hook, fake socket)', () => {
162
+ it('INITIAL boot against a dead VPS: getConnection hangs (waitForNasTech) → app sits in the connecting combo, then fails', async () => {
163
+ // The report's actual path: a fresh launch pointed at an unreachable VPS.
164
+ // startNasTech()'s remote branch awaits waitForNasTech() for 45s before it
165
+ // throws, so the renderer's `await desktop.getConnection()` stays pending
166
+ // that whole window. During it: gatewayState is still 'idle' (connect was
167
+ // never reached) and boot.error is null → connecting=true → the fullscreen
168
+ // CONNECTING overlay, latched, blocking Settings.
169
+ let rejectConn: (e: Error) => void = () => undefined
170
+ const desktop = fakeDesktop()
171
+ desktop.getConnection = vi.fn(
172
+ () =>
173
+ new Promise((_resolve, reject) => {
174
+ rejectConn = reject
175
+ })
176
+ )
177
+ ;(window as { NASTECHDesktop?: unknown }).NASTECHDesktop = desktop
178
+
179
+ render(<Harness />)
180
+ await flushAsync()
181
+
182
+ // getConnection is still pending — the dead-VPS wait. No socket was ever
183
+ // created, gatewayState never left idle, boot.error is null.
184
+ expect(FakeWebSocket.instances).toHaveLength(0)
185
+ expect($gatewayState.get()).not.toBe('open')
186
+ expect($desktopBoot.get().error).toBeNull()
187
+ // ^ connecting === true here → fullscreen CONNECTING, no Settings.
188
+
189
+ // After ~45s waitForNasTech gives up and getConnection rejects → boot()
190
+ // catch → failDesktopBoot → the BootFailureOverlay recovery surface.
191
+ await act(async () => {
192
+ rejectConn(new Error('NasTech backend did not become ready: timeout'))
193
+ await vi.advanceTimersByTimeAsync(0)
194
+ })
195
+
196
+ expect($desktopBoot.get().error).toBeTruthy()
197
+ })
198
+
199
+ it('a remote that drops post-boot keeps looping with NO boot.error (the dead-end CONNECTING combo)', async () => {
200
+ render(<Harness />)
201
+ await flushAsync()
202
+
203
+ // Initial boot connected.
204
+ expect($gatewayState.get()).toBe('open')
205
+ expect($desktopBoot.get().error).toBeNull()
206
+ expect(FakeWebSocket.instances).toHaveLength(1)
207
+
208
+ // The remote VPS goes away: drop the live socket, and make every reopen
209
+ // fail from here on.
210
+ FakeWebSocket.mode = 'fail'
211
+ act(() => FakeWebSocket.instances[0].drop())
212
+ await flushAsync()
213
+
214
+ // Burn a couple backoff cycles BEFORE the escalation threshold (<6 attempts,
215
+ // ~the first ~15s). This is the window where stock and fixed behave the
216
+ // same: socket down, hook retrying, gatewayState non-open, boot.error still
217
+ // null → CONNECTING covers the screen with no recovery surface. (Past ~45s
218
+ // the fix raises boot.error; that's asserted in the next test.)
219
+ await advanceBackoff()
220
+
221
+ expect($gatewayState.get()).not.toBe('open')
222
+ expect($desktopBoot.get().error).toBeNull()
223
+ // It is actively retrying, not idle — more sockets were minted.
224
+ expect(FakeWebSocket.instances.length).toBeGreaterThan(1)
225
+ })
226
+
227
+ it('FIX: after the prolonged drop the hook raises a recoverable boot error (the escape hatch)', async () => {
228
+ render(<Harness />)
229
+ await flushAsync()
230
+ expect($desktopBoot.get().error).toBeNull()
231
+
232
+ FakeWebSocket.mode = 'fail'
233
+ act(() => FakeWebSocket.instances[0].drop())
234
+ await flushAsync()
235
+
236
+ // Walk the backoff past the >=6 attempt threshold (~45s of failures).
237
+ for (let i = 0; i < 8; i += 1) {
238
+ await advanceBackoff()
239
+ }
240
+
241
+ // The hook surfaced the recoverable error → BootFailureOverlay (Use local
242
+ // gateway / Sign in / Retry) becomes reachable instead of CONNECTING.
243
+ expect($desktopBoot.get().error).toBeTruthy()
244
+ })
245
+
246
+ it('FIX: a successful reconnect clears the recoverable error', async () => {
247
+ render(<Harness />)
248
+ await flushAsync()
249
+
250
+ FakeWebSocket.mode = 'fail'
251
+ act(() => FakeWebSocket.instances[0].drop())
252
+ await flushAsync()
253
+ for (let i = 0; i < 8; i += 1) {
254
+ await advanceBackoff()
255
+ }
256
+ expect($desktopBoot.get().error).toBeTruthy()
257
+
258
+ // The remote comes back: next reconnect attempt opens.
259
+ FakeWebSocket.mode = 'open'
260
+ await advanceBackoff()
261
+
262
+ expect($gatewayState.get()).toBe('open')
263
+ expect($desktopBoot.get().error).toBeNull()
264
+ })
265
+ })
@@ -0,0 +1,387 @@
1
+ import { useEffect, useRef } from 'react'
2
+
3
+ import type { NasTechConnection } from '@/global'
4
+ import { NasTechGateway } from '@/nastech'
5
+ import { translateNow } from '@/i18n'
6
+ import { isGatewayReauthRequired, resolveGatewayWsUrl } from '@/lib/gateway-ws-url'
7
+ import {
8
+ $desktopBoot,
9
+ applyDesktopBootProgress,
10
+ completeDesktopBoot,
11
+ failDesktopBoot,
12
+ setDesktopBootStep
13
+ } from '@/store/boot'
14
+ import {
15
+ $gateway,
16
+ closeSecondaryGateways,
17
+ configureGatewayRegistry,
18
+ ensureGatewayForProfile,
19
+ pruneSecondaryGateways,
20
+ reconnectSecondaryGateways,
21
+ reportPrimaryGatewayState,
22
+ setPrimaryGateway,
23
+ touchSecondaryGateways
24
+ } from '@/store/gateway'
25
+ import { notify, notifyError } from '@/store/notifications'
26
+ import { $activeGatewayProfile, normalizeProfileKey, touchActiveGatewayBackend } from '@/store/profile'
27
+ import {
28
+ $attentionSessionIds,
29
+ $connection,
30
+ $sessions,
31
+ $workingSessionIds,
32
+ setConnection,
33
+ setSessionsLoading
34
+ } from '@/store/session'
35
+ import type { RpcEvent } from '@/types/nastech'
36
+
37
+ interface GatewayBootOptions {
38
+ handleGatewayEvent: (event: RpcEvent) => void
39
+ onConnectionReady: (
40
+ connection: Awaited<ReturnType<NonNullable<typeof window.NASTECHDesktop>['getConnection']>> | null
41
+ ) => void
42
+ onGatewayReady: (gateway: NasTechGateway | null) => void
43
+ refreshNasTechConfig: () => Promise<void>
44
+ refreshSessions: () => Promise<void>
45
+ }
46
+
47
+ export function useGatewayBoot({
48
+ handleGatewayEvent,
49
+ onConnectionReady,
50
+ onGatewayReady,
51
+ refreshNasTechConfig,
52
+ refreshSessions
53
+ }: GatewayBootOptions) {
54
+ const callbacksRef = useRef({
55
+ handleGatewayEvent,
56
+ onConnectionReady,
57
+ onGatewayReady,
58
+ refreshNasTechConfig,
59
+ refreshSessions
60
+ })
61
+
62
+ callbacksRef.current = {
63
+ handleGatewayEvent,
64
+ onConnectionReady,
65
+ onGatewayReady,
66
+ refreshNasTechConfig,
67
+ refreshSessions
68
+ }
69
+
70
+ useEffect(() => {
71
+ let cancelled = false
72
+ const desktop = window.NASTECHDesktop
73
+
74
+ const publish = (next: NasTechConnection | null) => {
75
+ callbacksRef.current.onConnectionReady(next)
76
+ setConnection(next)
77
+ }
78
+
79
+ if (!desktop) {
80
+ failDesktopBoot('Desktop IPC bridge is unavailable.')
81
+ setSessionsLoading(false)
82
+
83
+ return () => void (cancelled = true)
84
+ }
85
+
86
+ // --- Reconnect-after-sleep machinery -------------------------------------
87
+ // macOS sleep silently drops the renderer's WebSocket. The backend Python
88
+ // process keeps running, but nothing re-opened the socket on wake, so the
89
+ // composer stayed disabled forever on "Starting NasTech...". Once the
90
+ // initial boot succeeds we treat any non-open state as recoverable and
91
+ // reconnect with backoff, and we nudge a reconnect on the OS/browser
92
+ // signals that fire around wake (power resume, network online, the window
93
+ // becoming visible).
94
+ let bootCompleted = false
95
+ let reconnecting = false
96
+ let reconnectTimer: ReturnType<typeof setTimeout> | null = null
97
+ let reconnectAttempt = 0
98
+ // Surface "sign in again" once per disconnect episode, not on every backoff
99
+ // tick — a stale OAuth ticket fails every attempt and would otherwise stack
100
+ // identical error toasts (and their haptics). Reset on the next clean open.
101
+ let reauthNotified = false
102
+
103
+ // Wrap the live getter in a call so TS control-flow analysis doesn't narrow
104
+ // `connectionState` to a constant across the early-return guards (the state
105
+ // genuinely changes between reads).
106
+ const gatewayOpen = () => gateway.connectionState === 'open'
107
+
108
+ const clearReconnectTimer = () => {
109
+ if (reconnectTimer !== null) {
110
+ clearTimeout(reconnectTimer)
111
+ reconnectTimer = null
112
+ }
113
+ }
114
+
115
+ const attemptReconnect = async () => {
116
+ if (cancelled || reconnecting || gatewayOpen()) {
117
+ return
118
+ }
119
+
120
+ reconnecting = true
121
+
122
+ try {
123
+ const conn = await desktop.getConnection($activeGatewayProfile.get())
124
+
125
+ if (cancelled) {
126
+ return
127
+ }
128
+
129
+ publish(conn)
130
+ // Re-mint the WS URL before reconnecting. OAuth tickets are single-use
131
+ // with a short TTL, so the ticket baked into the cached conn.wsUrl is
132
+ // dead on every reconnect after the initial boot — reusing it surfaces
133
+ // as an opaque "Could not connect to NasTech gateway". resolveGatewayWsUrl
134
+ // mints a fresh ticket (or throws a reauth error in OAuth mode rather
135
+ // than connecting with a stale one). For local/token gateways the URL
136
+ // carries a long-lived token and the re-mint is a cheap no-op.
137
+ const wsUrl = await resolveGatewayWsUrl(desktop, conn)
138
+ await gateway.connect(wsUrl)
139
+
140
+ if (cancelled) {
141
+ return
142
+ }
143
+
144
+ reconnectAttempt = 0
145
+ // Resync state that may have moved on the backend while we were asleep.
146
+ await callbacksRef.current.refreshNasTechConfig().catch(() => undefined)
147
+ await callbacksRef.current.refreshSessions().catch(() => undefined)
148
+ } catch (err) {
149
+ // OAuth session expired mid-reconnect: surface the actionable "sign in
150
+ // again" message once instead of silently looping the backoff against a
151
+ // ticket that can never succeed. Transport failures fall through to the
152
+ // backoff in the finally block below.
153
+ if (!cancelled && isGatewayReauthRequired(err) && !reauthNotified) {
154
+ reauthNotified = true
155
+ notifyError(err, translateNow('boot.errors.gatewaySignInRequired'))
156
+ }
157
+ } finally {
158
+ reconnecting = false
159
+
160
+ if (!cancelled && !gatewayOpen()) {
161
+ scheduleReconnect()
162
+ }
163
+ }
164
+ }
165
+
166
+ function scheduleReconnect() {
167
+ if (cancelled || reconnecting || reconnectTimer !== null || gatewayOpen()) {
168
+ return
169
+ }
170
+
171
+ // 1s, 2s, 4s … capped at 15s.
172
+ const delay = Math.min(15_000, 1_000 * 2 ** Math.min(reconnectAttempt, 4))
173
+ reconnectAttempt += 1
174
+ reconnectTimer = setTimeout(() => {
175
+ reconnectTimer = null
176
+ void attemptReconnect()
177
+ }, delay)
178
+ }
179
+
180
+ const reconnectNow = () => {
181
+ if (cancelled || !bootCompleted) {
182
+ return
183
+ }
184
+
185
+ clearReconnectTimer()
186
+ reconnectAttempt = 0
187
+ reconnectSecondaryGateways()
188
+
189
+ if (!gatewayOpen()) {
190
+ void attemptReconnect()
191
+ }
192
+ }
193
+
194
+ const offBootProgress = desktop.onBootProgress(payload => applyDesktopBootProgress(payload))
195
+ void desktop
196
+ .getBootProgress()
197
+ .then(snapshot => applyDesktopBootProgress(snapshot))
198
+ .catch(() => undefined)
199
+
200
+ setDesktopBootStep({
201
+ phase: 'renderer.boot',
202
+ message: translateNow('boot.steps.startingDesktopConnection'),
203
+ progress: 6
204
+ })
205
+
206
+ const gateway = new NasTechGateway()
207
+ callbacksRef.current.onGatewayReady(gateway)
208
+ setPrimaryGateway(gateway, normalizeProfileKey($activeGatewayProfile.get()))
209
+ // Secondary (background-profile) sockets funnel into the same handler.
210
+ configureGatewayRegistry({ onEvent: event => callbacksRef.current.handleGatewayEvent(event) })
211
+
212
+ const offState = gateway.onState(st => {
213
+ // Mirror to the composer only while the primary is the active profile —
214
+ // a background secondary reconnect mustn't flip the foreground state.
215
+ reportPrimaryGatewayState(st)
216
+
217
+ if (st === 'open') {
218
+ reconnectAttempt = 0
219
+ reauthNotified = false
220
+ clearReconnectTimer()
221
+ } else if (bootCompleted && (st === 'closed' || st === 'error')) {
222
+ // The socket dropped after a healthy boot (typically sleep/wake). Try
223
+ // to bring it back instead of leaving the composer stuck disabled.
224
+ scheduleReconnect()
225
+ }
226
+ })
227
+
228
+ const offEvent = gateway.onEvent(event => callbacksRef.current.handleGatewayEvent(event))
229
+
230
+ // Wake signals: power resume (macOS/Windows), network coming back, and the
231
+ // window regaining focus/visibility. Each nudges an immediate reconnect.
232
+ const offPowerResume = desktop.onPowerResume?.(() => reconnectNow())
233
+
234
+ const onOnline = () => reconnectNow()
235
+
236
+ const onVisible = () => {
237
+ if (document.visibilityState === 'visible') {
238
+ reconnectNow()
239
+ }
240
+ }
241
+
242
+ window.addEventListener('online', onOnline)
243
+ document.addEventListener('visibilitychange', onVisible)
244
+
245
+ // Keep live pool backends alive while this window is open (the main process
246
+ // can't observe the direct renderer↔backend WS). No-op for the primary.
247
+ const keepaliveTimer = setInterval(() => {
248
+ touchActiveGatewayBackend()
249
+ touchSecondaryGateways()
250
+ }, 60_000)
251
+
252
+ // Bound concurrency cost to live work: keep a background socket only while
253
+ // its profile has a running (working) or blocked (needs-input) session.
254
+ // Once that profile goes idle its socket is dropped and its backend is free
255
+ // to idle-reap. The active profile is always spared.
256
+ const recomputeKeptGateways = () => {
257
+ const live = new Set([...$workingSessionIds.get(), ...$attentionSessionIds.get()])
258
+ const keep = new Set<string>()
259
+
260
+ for (const session of $sessions.get()) {
261
+ if (live.has(session.id)) {
262
+ keep.add(normalizeProfileKey(session.profile))
263
+ }
264
+ }
265
+
266
+ pruneSecondaryGateways(keep)
267
+ }
268
+
269
+ const offWorking = $workingSessionIds.subscribe(() => recomputeKeptGateways())
270
+ const offAttention = $attentionSessionIds.subscribe(() => recomputeKeptGateways())
271
+ const offActiveProfile = $activeGatewayProfile.subscribe(() => recomputeKeptGateways())
272
+
273
+ const offWindowState = desktop.onWindowStateChanged?.(payload => {
274
+ const current = $connection.get()
275
+
276
+ if (current) {
277
+ publish({ ...current, ...payload })
278
+ }
279
+ })
280
+
281
+ const offExit = desktop.onBackendExit(() => {
282
+ if ($desktopBoot.get().running || $desktopBoot.get().visible) {
283
+ failDesktopBoot(translateNow('boot.errors.backgroundExitedDuringStartup'))
284
+ }
285
+
286
+ notify({
287
+ kind: 'error',
288
+ title: translateNow('boot.errors.backendStopped'),
289
+ message: translateNow('boot.errors.backgroundExited'),
290
+ durationMs: 0
291
+ })
292
+ })
293
+
294
+ async function boot() {
295
+ try {
296
+ const conn = await desktop.getConnection()
297
+
298
+ if (cancelled) {
299
+ return
300
+ }
301
+
302
+ setDesktopBootStep({
303
+ phase: 'renderer.gateway.connect',
304
+ message: translateNow('boot.steps.connectingGateway'),
305
+ progress: 95
306
+ })
307
+ publish(conn)
308
+ // Mint a fresh WS URL right before connecting. For OAuth gateways the
309
+ // ticket is single-use with a short TTL, so the ticket baked into
310
+ // conn.wsUrl is stale; resolveGatewayWsUrl() re-mints it and, on
311
+ // failure, throws a reauth error rather than connecting with a dead
312
+ // ticket (which would surface as an opaque "connection closed").
313
+ const wsUrl = await resolveGatewayWsUrl(desktop, conn)
314
+ await gateway.connect(wsUrl)
315
+
316
+ if (cancelled) {
317
+ return
318
+ }
319
+
320
+ // Record which profile the primary (window) backend booted as, so
321
+ // same-profile resumes are no-op swaps and any reconnect targets the
322
+ // right backend. Best-effort: a missing preference means "default".
323
+ try {
324
+ const pref = await desktop.profile?.get?.()
325
+ const profileKey = (pref?.profile ?? '').trim() || 'default'
326
+ $activeGatewayProfile.set(profileKey)
327
+ setPrimaryGateway(gateway, profileKey)
328
+ void ensureGatewayForProfile(profileKey)
329
+ } catch {
330
+ $activeGatewayProfile.set('default')
331
+ }
332
+
333
+ setDesktopBootStep({
334
+ phase: 'renderer.config',
335
+ message: translateNow('boot.steps.loadingSettings'),
336
+ progress: 97
337
+ })
338
+ await callbacksRef.current.refreshNasTechConfig()
339
+
340
+ if (cancelled) {
341
+ return
342
+ }
343
+
344
+ setDesktopBootStep({
345
+ phase: 'renderer.sessions',
346
+ message: translateNow('boot.steps.loadingSessions'),
347
+ progress: 99
348
+ })
349
+ await callbacksRef.current.refreshSessions()
350
+ completeDesktopBoot()
351
+ bootCompleted = true
352
+ } catch (err) {
353
+ if (!cancelled) {
354
+ const message = err instanceof Error ? err.message : String(err)
355
+ failDesktopBoot(message)
356
+ notifyError(err, translateNow('boot.errors.desktopBootFailed'))
357
+ setSessionsLoading(false)
358
+ }
359
+ }
360
+ }
361
+
362
+ void boot()
363
+
364
+ return () => {
365
+ cancelled = true
366
+ clearReconnectTimer()
367
+ clearInterval(keepaliveTimer)
368
+ offWorking()
369
+ offAttention()
370
+ offActiveProfile()
371
+ window.removeEventListener('online', onOnline)
372
+ document.removeEventListener('visibilitychange', onVisible)
373
+ offPowerResume?.()
374
+ offState()
375
+ offEvent()
376
+ offExit()
377
+ offWindowState?.()
378
+ offBootProgress()
379
+ closeSecondaryGateways()
380
+ gateway.close()
381
+ publish(null)
382
+ callbacksRef.current.onGatewayReady(null)
383
+ setPrimaryGateway(null)
384
+ $gateway.set(null)
385
+ }
386
+ }, [])
387
+ }