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,38 @@
1
+ /**
2
+ * Window translucency (see-through window).
3
+ *
4
+ * One lever, 0–100. 0 = off (fully opaque, the default). Higher = more of the
5
+ * desktop shows through the whole window — the main process maps it to the
6
+ * native window opacity (`setOpacity`), the same effect as the Windows
7
+ * shift-scroll trick. macOS + Windows only; Linux has no runtime window
8
+ * opacity, so it's a no-op there.
9
+ *
10
+ * The renderer owns the value and mirrors it to the main process over IPC.
11
+ */
12
+
13
+ import { atom } from 'nanostores'
14
+
15
+ import { persistString, storedString } from '@/lib/storage'
16
+
17
+ const KEY = 'nastech.desktop.translucency.v1'
18
+
19
+ const clamp = (n: number): number => Math.min(100, Math.max(0, Math.round(n)))
20
+
21
+ const read = (): number => {
22
+ const n = Number(storedString(KEY))
23
+
24
+ return Number.isFinite(n) ? clamp(n) : 0
25
+ }
26
+
27
+ export const $translucency = atom<number>(typeof window === 'undefined' ? 0 : read())
28
+
29
+ export function setTranslucency(intensity: number): void {
30
+ $translucency.set(clamp(intensity))
31
+ }
32
+
33
+ if (typeof window !== 'undefined') {
34
+ $translucency.subscribe(intensity => {
35
+ persistString(KEY, String(intensity))
36
+ window.NASTECHDesktop?.setTranslucency?.({ intensity })
37
+ })
38
+ }
@@ -0,0 +1,77 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+
3
+ import type { DesktopUpdateStatus } from '@/global'
4
+
5
+ const storage = new Map<string, string>()
6
+
7
+ vi.mock('@/lib/storage', () => ({
8
+ persistString: (key: string, value: null | string) => {
9
+ if (value === null) {
10
+ storage.delete(key)
11
+ } else {
12
+ storage.set(key, value)
13
+ }
14
+ },
15
+ storedString: (key: string) => storage.get(key) ?? null
16
+ }))
17
+
18
+ const notifySpy = vi.fn()
19
+ const dismissSpy = vi.fn()
20
+
21
+ vi.mock('@/store/notifications', () => ({
22
+ notify: (...args: unknown[]) => notifySpy(...args),
23
+ dismissNotification: (...args: unknown[]) => dismissSpy(...args)
24
+ }))
25
+
26
+ const { maybeNotifyUpdateAvailable } = await import('./updates')
27
+
28
+ const status = (over: Partial<DesktopUpdateStatus> = {}): DesktopUpdateStatus => ({
29
+ supported: true,
30
+ behind: 3,
31
+ targetSha: 'sha-a',
32
+ fetchedAt: 0,
33
+ ...over
34
+ })
35
+
36
+ const lastToast = () => notifySpy.mock.calls.at(-1)?.[0] as { onDismiss: () => void }
37
+
38
+ describe('maybeNotifyUpdateAvailable', () => {
39
+ beforeEach(() => {
40
+ storage.clear()
41
+ notifySpy.mockClear()
42
+ vi.useRealTimers()
43
+ })
44
+
45
+ it('shows when an update is available and not snoozed', () => {
46
+ maybeNotifyUpdateAvailable(status())
47
+ expect(notifySpy).toHaveBeenCalledTimes(1)
48
+ })
49
+
50
+ it('stays quiet for new commits once the toast was closed', () => {
51
+ maybeNotifyUpdateAvailable(status())
52
+ lastToast().onDismiss() // user closes it → cooldown starts
53
+ notifySpy.mockClear()
54
+
55
+ // A different commit lands while still within the cooldown window.
56
+ maybeNotifyUpdateAvailable(status({ targetSha: 'sha-b', behind: 9 }))
57
+ expect(notifySpy).not.toHaveBeenCalled()
58
+ })
59
+
60
+ it('re-shows once the cooldown elapses', () => {
61
+ vi.useFakeTimers()
62
+ vi.setSystemTime(0)
63
+
64
+ maybeNotifyUpdateAvailable(status())
65
+ lastToast().onDismiss()
66
+ notifySpy.mockClear()
67
+
68
+ vi.setSystemTime(25 * 60 * 60 * 1000) // > 24h cooldown
69
+ maybeNotifyUpdateAvailable(status({ targetSha: 'sha-b' }))
70
+ expect(notifySpy).toHaveBeenCalledTimes(1)
71
+ })
72
+
73
+ it('does nothing when already up to date', () => {
74
+ maybeNotifyUpdateAvailable(status({ behind: 0 }))
75
+ expect(notifySpy).not.toHaveBeenCalled()
76
+ })
77
+ })
@@ -0,0 +1,315 @@
1
+ /**
2
+ * Desktop self-update store. Tracks distance from the configured branch,
3
+ * surfaces it as an ambient pill, and orchestrates the apply flow.
4
+ */
5
+
6
+ import { atom } from 'nanostores'
7
+
8
+ import type {
9
+ DesktopUpdateApplyOptions,
10
+ DesktopUpdateApplyResult,
11
+ DesktopUpdateProgress,
12
+ DesktopUpdateStage,
13
+ DesktopUpdateStatus,
14
+ DesktopVersionInfo
15
+ } from '@/global'
16
+ import { translateNow } from '@/i18n'
17
+ import { persistString, storedString } from '@/lib/storage'
18
+ import { dismissNotification, notify } from '@/store/notifications'
19
+
20
+ export interface UpdateApplyState {
21
+ applying: boolean
22
+ stage: DesktopUpdateStage
23
+ message: string
24
+ percent: number | null
25
+ error: string | null
26
+ /** When the stage is 'manual': the exact command the user should run
27
+ * (CLI install with no staged updater). */
28
+ command: string | null
29
+ log: readonly { stage: DesktopUpdateStage; message: string; at: number }[]
30
+ }
31
+
32
+ const IDLE: UpdateApplyState = {
33
+ applying: false,
34
+ stage: 'idle',
35
+ message: '',
36
+ percent: null,
37
+ error: null,
38
+ command: null,
39
+ log: []
40
+ }
41
+
42
+ export const $desktopVersion = atom<DesktopVersionInfo | null>(null)
43
+ export const $updateApply = atom<UpdateApplyState>(IDLE)
44
+ export const $updateChecking = atom<boolean>(false)
45
+ export const $updateOverlayOpen = atom<boolean>(false)
46
+ export const $updateStatus = atom<DesktopUpdateStatus | null>(null)
47
+
48
+ export const setUpdateOverlayOpen = (open: boolean) => $updateOverlayOpen.set(open)
49
+ export const resetUpdateApplyState = () => $updateApply.set(IDLE)
50
+
51
+ const UPDATE_TOAST_ID = 'desktop-update-available'
52
+ // Time-based snooze instead of per-sha dismissal: this repo lands ~100 commits
53
+ // a day, so a "don't show this exact sha again" guard re-popped the toast on
54
+ // every new commit. We instead suppress the toast for a cooldown window that
55
+ // (re)starts whenever the user closes it.
56
+ const UPDATE_TOAST_SNOOZE_KEY = 'NASTECH:update-toast-snooze-until'
57
+ const UPDATE_TOAST_COOLDOWN_MS = 24 * 60 * 60 * 1000
58
+
59
+ function snoozeUpdateToast(): void {
60
+ persistString(UPDATE_TOAST_SNOOZE_KEY, String(Date.now() + UPDATE_TOAST_COOLDOWN_MS))
61
+ }
62
+
63
+ function isUpdateToastSnoozed(): boolean {
64
+ const until = Number(storedString(UPDATE_TOAST_SNOOZE_KEY) || 0)
65
+
66
+ return Number.isFinite(until) && Date.now() < until
67
+ }
68
+
69
+ // Must match tui_gateway's DESKTOP_BACKEND_CONTRACT that this build was written
70
+ // against. The backend reports its own value in session runtime info; a lower
71
+ // value (or none — a pre-GUI checkout) means GUI<->backend skew.
72
+ const REQUIRED_BACKEND_CONTRACT = 1
73
+ const SKEW_TOAST_ID = 'backend-contract-skew'
74
+
75
+ /**
76
+ * Guard against a desktop GUI talking to a backend that predates its contract
77
+ * (e.g. a bb/gui-built app pointed at a `main` checkout). Rather than failing
78
+ * cryptically downstream, surface a persistent warning with a one-click align
79
+ * that runs the normal update flow (which self-heals to the right branch).
80
+ */
81
+ export function reportBackendContract(contract: number | undefined): void {
82
+ if ((contract ?? 0) >= REQUIRED_BACKEND_CONTRACT) {
83
+ dismissNotification(SKEW_TOAST_ID)
84
+
85
+ return
86
+ }
87
+
88
+ notify({
89
+ action: { label: translateNow('notifications.updateNasTech'), onClick: () => void applyUpdates() },
90
+ durationMs: 0,
91
+ id: SKEW_TOAST_ID,
92
+ kind: 'warning',
93
+ message: translateNow('notifications.backendOutOfDateMessage'),
94
+ title: translateNow('notifications.backendOutOfDateTitle')
95
+ })
96
+ }
97
+
98
+ /**
99
+ * Fire a toast when an update is available, at most once per cooldown window.
100
+ * Closing the toast — dismissing it or opening the updates window from it —
101
+ * (re)starts the cooldown, so a busy upstream branch doesn't re-spam the user
102
+ * on every new commit. The snooze is persisted, so it survives relaunches too.
103
+ */
104
+ export function maybeNotifyUpdateAvailable(status: DesktopUpdateStatus | null) {
105
+ if (!status || status.supported === false || status.error || !status.targetSha) {
106
+ return
107
+ }
108
+
109
+ if ((status.behind ?? 0) <= 0) {
110
+ return
111
+ }
112
+
113
+ if (isUpdateToastSnoozed()) {
114
+ return
115
+ }
116
+
117
+ if ($updateApply.get().applying) {
118
+ return
119
+ }
120
+
121
+ const behind = status.behind ?? 0
122
+
123
+ notify({
124
+ action: {
125
+ label: translateNow('notifications.seeWhatsNew'),
126
+ onClick: () => {
127
+ snoozeUpdateToast()
128
+ openUpdatesWindow()
129
+ }
130
+ },
131
+ durationMs: 0,
132
+ id: UPDATE_TOAST_ID,
133
+ kind: 'info',
134
+ message: translateNow('notifications.updateReadyMessage', behind),
135
+ onDismiss: () => snoozeUpdateToast(),
136
+ title: translateNow('notifications.updateReadyTitle')
137
+ })
138
+ }
139
+
140
+ /**
141
+ * Opens the updates dialog and kicks off a fresh check so the user always
142
+ * sees current state, even if a stale status is cached from earlier.
143
+ */
144
+ export function openUpdatesWindow(): void {
145
+ $updateOverlayOpen.set(true)
146
+ void checkUpdates()
147
+ }
148
+
149
+ /** Re-read the running app's version from the Electron main process and
150
+ * publish it on `$desktopVersion`. Called when the About panel mounts, the
151
+ * update flow finishes, and the window regains focus, so the About text
152
+ * stays in sync with the just-installed binary instead of frozen at the
153
+ * value captured at first-load. */
154
+ export async function refreshDesktopVersion(): Promise<DesktopVersionInfo | null> {
155
+ if (typeof window === 'undefined') {
156
+ return null
157
+ }
158
+
159
+ // Best-effort UI sync: callers (checkUpdates, startUpdatePoller, window
160
+ // focus handler) all kick this off with `void refreshDesktopVersion()`,
161
+ // so any rejection from the IPC bridge (e.g. main process shutting down
162
+ // mid-reload, or the bridge not yet ready on first paint) would surface
163
+ // as an unhandled promise rejection in the renderer. Swallow it.
164
+ try {
165
+ const next = await window.NASTECHDesktop?.getVersion?.()
166
+
167
+ if (next) {
168
+ $desktopVersion.set(next)
169
+ }
170
+
171
+ return next ?? null
172
+ } catch {
173
+ return null
174
+ }
175
+ }
176
+
177
+ export async function checkUpdates(): Promise<DesktopUpdateStatus | null> {
178
+ const bridge = window.NASTECHDesktop?.updates
179
+
180
+ if (!bridge || $updateChecking.get()) {
181
+ return $updateStatus.get()
182
+ }
183
+
184
+ $updateChecking.set(true)
185
+
186
+ try {
187
+ const status = await bridge.check()
188
+ $updateStatus.set(status)
189
+ maybeNotifyUpdateAvailable(status)
190
+ // The update check pulls the latest nastech_cli + bundled package metadata
191
+ // into place. Re-read the running version so About reflects the now-fresh
192
+ // checkout rather than the one captured at process start.
193
+ void refreshDesktopVersion()
194
+
195
+ return status
196
+ } catch (error) {
197
+ const previous = $updateStatus.get()
198
+
199
+ const fallback: DesktopUpdateStatus = {
200
+ supported: previous?.supported ?? true,
201
+ branch: previous?.branch,
202
+ error: 'check-failed',
203
+ message: error instanceof Error ? error.message : String(error),
204
+ fetchedAt: Date.now()
205
+ }
206
+
207
+ $updateStatus.set(fallback)
208
+
209
+ return fallback
210
+ } finally {
211
+ $updateChecking.set(false)
212
+ }
213
+ }
214
+
215
+ export async function applyUpdates(opts: DesktopUpdateApplyOptions = {}): Promise<DesktopUpdateApplyResult> {
216
+ const bridge = window.NASTECHDesktop?.updates
217
+
218
+ if (!bridge) {
219
+ return { ok: false, error: 'unavailable', message: 'Desktop bridge unavailable.' }
220
+ }
221
+
222
+ dismissNotification(UPDATE_TOAST_ID)
223
+ $updateApply.set({ ...IDLE, applying: true, stage: 'prepare', message: 'Starting update…' })
224
+
225
+ try {
226
+ const result = await bridge.apply(opts)
227
+
228
+ // CLI install with no staged updater: not an error — the user just runs
229
+ // `NASTECH update` themselves. Land on a dedicated manual state so the
230
+ // overlay shows the command + copy button instead of a dead retry loop.
231
+ if (result?.manual) {
232
+ $updateApply.set({
233
+ ...IDLE,
234
+ applying: false,
235
+ stage: 'manual',
236
+ message: result.command ?? 'NASTECH update',
237
+ command: result.command ?? 'NASTECH update'
238
+ })
239
+ }
240
+
241
+ return result
242
+ } catch (error) {
243
+ const message = error instanceof Error ? error.message : String(error)
244
+ $updateApply.set({ ...$updateApply.get(), applying: false, stage: 'error', error: 'apply-failed', message })
245
+
246
+ return { ok: false, error: 'apply-failed', message }
247
+ }
248
+ }
249
+
250
+ function ingestProgress(payload: DesktopUpdateProgress): void {
251
+ const current = $updateApply.get()
252
+ const log = [...current.log, { stage: payload.stage, message: payload.message, at: payload.at }].slice(-50)
253
+ const terminal = payload.stage === 'error' || payload.stage === 'restart' || payload.stage === 'manual'
254
+
255
+ $updateApply.set({
256
+ applying: !terminal,
257
+ stage: payload.stage,
258
+ message: payload.message,
259
+ percent: payload.percent,
260
+ error: payload.error,
261
+ // 'manual' carries the command to run in its message field.
262
+ command: payload.stage === 'manual' ? payload.message : current.command,
263
+ log
264
+ })
265
+ }
266
+
267
+ let pollerStarted = false
268
+ let backgroundTimer: ReturnType<typeof setInterval> | null = null
269
+ let lastFocusAt = 0
270
+
271
+ /** Wire up background polling + progress streaming. Idempotent. */
272
+ export function startUpdatePoller(): void {
273
+ if (pollerStarted || typeof window === 'undefined') {
274
+ return
275
+ }
276
+
277
+ const bridge = window.NASTECHDesktop?.updates
278
+
279
+ if (!bridge) {
280
+ return
281
+ }
282
+
283
+ pollerStarted = true
284
+ void checkUpdates()
285
+ void refreshDesktopVersion()
286
+ bridge.onProgress(ingestProgress)
287
+
288
+ window.addEventListener('focus', onFocus)
289
+ backgroundTimer = setInterval(() => void checkUpdates(), 30 * 60 * 1000)
290
+ }
291
+
292
+ export function stopUpdatePoller(): void {
293
+ if (backgroundTimer !== null) {
294
+ clearInterval(backgroundTimer)
295
+ backgroundTimer = null
296
+ }
297
+
298
+ window.removeEventListener('focus', onFocus)
299
+ pollerStarted = false
300
+ }
301
+
302
+ function onFocus() {
303
+ const now = Date.now()
304
+
305
+ if (now - lastFocusAt < 5 * 60 * 1000) {
306
+ return
307
+ }
308
+
309
+ lastFocusAt = now
310
+ void checkUpdates()
311
+ // Cheap and safe to re-read on every (throttled) focus: the user may have
312
+ // updated NasTech from another window/CLI between focuses, and About should
313
+ // catch up without forcing a restart.
314
+ void refreshDesktopVersion()
315
+ }
@@ -0,0 +1,24 @@
1
+ import { atom } from 'nanostores'
2
+
3
+ export type VoicePlaybackSource = 'read-aloud' | 'voice-conversation'
4
+ export type VoicePlaybackStatus = 'idle' | 'preparing' | 'speaking'
5
+
6
+ export interface VoicePlaybackState {
7
+ audioElement: HTMLAudioElement | null
8
+ messageId: string | null
9
+ sequence: number
10
+ source: VoicePlaybackSource | null
11
+ status: VoicePlaybackStatus
12
+ }
13
+
14
+ export const $voicePlayback = atom<VoicePlaybackState>({
15
+ audioElement: null,
16
+ messageId: null,
17
+ sequence: 0,
18
+ source: null,
19
+ status: 'idle'
20
+ })
21
+
22
+ export function setVoicePlaybackState(next: VoicePlaybackState) {
23
+ $voicePlayback.set(next)
24
+ }
@@ -0,0 +1,143 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2
+
3
+ import { canOpenSessionWindow, openNewSessionInNewWindow, openSessionInNewWindow } from './windows'
4
+
5
+ const desktopWindow = window as unknown as { nastechDesktop?: Window['nastechDesktop'] }
6
+ const initialNasTechDesktop = desktopWindow.nastechDesktop
7
+
8
+ const notifyError = vi.fn()
9
+
10
+ vi.mock('./notifications', () => ({
11
+ notifyError: (...args: unknown[]) => notifyError(...args)
12
+ }))
13
+
14
+ function installBridge(
15
+ openSessionWindow?: Window['nastechDesktop']['openSessionWindow'],
16
+ openNewSessionWindow?: Window['nastechDesktop']['openNewSessionWindow']
17
+ ) {
18
+ desktopWindow.nastechDesktop = {
19
+ ...(openSessionWindow ? { openSessionWindow } : {}),
20
+ ...(openNewSessionWindow ? { openNewSessionWindow } : {})
21
+ } as unknown as Window['nastechDesktop']
22
+ }
23
+
24
+ beforeEach(() => {
25
+ notifyError.mockClear()
26
+ })
27
+
28
+ afterEach(() => {
29
+ if (initialNasTechDesktop) {
30
+ desktopWindow.nastechDesktop = initialNasTechDesktop
31
+ } else {
32
+ delete desktopWindow.nastechDesktop
33
+ }
34
+ })
35
+
36
+ describe('canOpenSessionWindow', () => {
37
+ it('is false when the desktop bridge is absent', () => {
38
+ delete desktopWindow.nastechDesktop
39
+ expect(canOpenSessionWindow()).toBe(false)
40
+ })
41
+
42
+ it('is false when the bridge lacks openSessionWindow', () => {
43
+ installBridge(undefined)
44
+ expect(canOpenSessionWindow()).toBe(false)
45
+ })
46
+
47
+ it('is true when the bridge exposes openSessionWindow', () => {
48
+ installBridge(vi.fn().mockResolvedValue({ ok: true }))
49
+ expect(canOpenSessionWindow()).toBe(true)
50
+ })
51
+ })
52
+
53
+ describe('openSessionInNewWindow', () => {
54
+ it('no-ops without a session id', async () => {
55
+ const open = vi.fn().mockResolvedValue({ ok: true })
56
+ installBridge(open)
57
+
58
+ await openSessionInNewWindow('')
59
+
60
+ expect(open).not.toHaveBeenCalled()
61
+ expect(notifyError).not.toHaveBeenCalled()
62
+ })
63
+
64
+ it('no-ops gracefully when the bridge is absent (web fallback)', async () => {
65
+ delete desktopWindow.nastechDesktop
66
+
67
+ await openSessionInNewWindow('s1')
68
+
69
+ expect(notifyError).not.toHaveBeenCalled()
70
+ })
71
+
72
+ it('invokes the bridge with the session id', async () => {
73
+ const open = vi.fn().mockResolvedValue({ ok: true })
74
+ installBridge(open)
75
+
76
+ await openSessionInNewWindow('s1')
77
+
78
+ expect(open).toHaveBeenCalledWith('s1', undefined)
79
+ expect(notifyError).not.toHaveBeenCalled()
80
+ })
81
+
82
+ it('forwards the watch flag for spectator (subagent) windows', async () => {
83
+ const open = vi.fn().mockResolvedValue({ ok: true })
84
+ installBridge(open)
85
+
86
+ await openSessionInNewWindow('s1', { watch: true })
87
+
88
+ expect(open).toHaveBeenCalledWith('s1', { watch: true })
89
+ expect(notifyError).not.toHaveBeenCalled()
90
+ })
91
+
92
+ it('notifies on an ok:false result', async () => {
93
+ installBridge(vi.fn().mockResolvedValue({ ok: false, error: 'invalid-session-id' }))
94
+
95
+ await openSessionInNewWindow('s1')
96
+
97
+ expect(notifyError).toHaveBeenCalledTimes(1)
98
+ })
99
+
100
+ it('notifies when the bridge throws', async () => {
101
+ installBridge(vi.fn().mockRejectedValue(new Error('boom')))
102
+
103
+ await openSessionInNewWindow('s1')
104
+
105
+ expect(notifyError).toHaveBeenCalledTimes(1)
106
+ })
107
+ })
108
+
109
+ describe('openNewSessionInNewWindow', () => {
110
+ it('no-ops gracefully when the bridge is absent (web fallback)', async () => {
111
+ delete desktopWindow.nastechDesktop
112
+
113
+ await openNewSessionInNewWindow()
114
+
115
+ expect(notifyError).not.toHaveBeenCalled()
116
+ })
117
+
118
+ it('no-ops when openNewSessionWindow is missing', async () => {
119
+ installBridge(vi.fn().mockResolvedValue({ ok: true }))
120
+
121
+ await openNewSessionInNewWindow()
122
+
123
+ expect(notifyError).not.toHaveBeenCalled()
124
+ })
125
+
126
+ it('invokes the bridge', async () => {
127
+ const openNew = vi.fn().mockResolvedValue({ ok: true })
128
+ installBridge(vi.fn().mockResolvedValue({ ok: true }), openNew)
129
+
130
+ await openNewSessionInNewWindow()
131
+
132
+ expect(openNew).toHaveBeenCalledTimes(1)
133
+ expect(notifyError).not.toHaveBeenCalled()
134
+ })
135
+
136
+ it('notifies on an ok:false result', async () => {
137
+ installBridge(vi.fn().mockResolvedValue({ ok: true }), vi.fn().mockResolvedValue({ ok: false, error: 'nope' }))
138
+
139
+ await openNewSessionInNewWindow()
140
+
141
+ expect(notifyError).toHaveBeenCalledTimes(1)
142
+ })
143
+ })
@@ -0,0 +1,77 @@
1
+ import { notifyError } from './notifications'
2
+
3
+ // Window flag set by the Electron main process when it opens a standalone
4
+ // session window (see electron/main.cjs buildSessionWindowUrl). It rides in the
5
+ // query string BEFORE the HashRouter '#', so we read it from location.search,
6
+ // never from the router. A "secondary" window renders a single chat without the
7
+ // global session sidebar or the install / onboarding overlays.
8
+ const SECONDARY_WINDOW_FLAG = 'secondary'
9
+
10
+ let secondaryWindowCache: boolean | null = null
11
+
12
+ export function isSecondaryWindow(): boolean {
13
+ if (secondaryWindowCache !== null) {
14
+ return secondaryWindowCache
15
+ }
16
+
17
+ let result = false
18
+
19
+ try {
20
+ result = new URLSearchParams(window.location.search).get('win') === SECONDARY_WINDOW_FLAG
21
+ } catch {
22
+ result = false
23
+ }
24
+
25
+ secondaryWindowCache = result
26
+
27
+ return result
28
+ }
29
+
30
+ let watchWindowCache: boolean | null = null
31
+
32
+ // A "watch" window spectates a session that is being driven elsewhere (a
33
+ // running subagent). It resumes lazily — the gateway registers history + a
34
+ // transport for the live mirror without building an agent, so opening it is
35
+ // cheap even while the backend is busy running the delegation.
36
+ export function isWatchWindow(): boolean {
37
+ if (watchWindowCache !== null) {
38
+ return watchWindowCache
39
+ }
40
+
41
+ let result = false
42
+
43
+ try {
44
+ result = new URLSearchParams(window.location.search).get('watch') === '1'
45
+ } catch {
46
+ result = false
47
+ }
48
+
49
+ watchWindowCache = result
50
+
51
+ return result
52
+ }
53
+
54
+ // True when running inside the Electron desktop shell (the preload bridge is
55
+ // present). The "open in new window" affordance is desktop-only.
56
+ export function canOpenSessionWindow(): boolean {
57
+ return typeof window !== 'undefined' && typeof window.NASTECHDesktop?.openSessionWindow === 'function'
58
+ }
59
+
60
+ // Open (or focus) a standalone OS window for a single chat session. No-ops
61
+ // gracefully outside Electron so callers can wire it unconditionally.
62
+ // `watch: true` opens a spectator window (lazy resume, live-mirror stream).
63
+ export async function openSessionInNewWindow(sessionId: string, opts?: { watch?: boolean }): Promise<void> {
64
+ if (!sessionId || !canOpenSessionWindow()) {
65
+ return
66
+ }
67
+
68
+ try {
69
+ const result = await window.NASTECHDesktop.openSessionWindow(sessionId, opts)
70
+
71
+ if (!result?.ok) {
72
+ notifyError(new Error(result?.error || 'unknown error'), 'Could not open chat in a new window')
73
+ }
74
+ } catch (err) {
75
+ notifyError(err, 'Could not open chat in a new window')
76
+ }
77
+ }