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,77 @@
1
+ import { Component, type ErrorInfo, type ReactNode } from 'react'
2
+
3
+ import { Button } from '@/components/ui/button'
4
+ import { ErrorState } from '@/components/ui/error-state'
5
+ import { useI18n } from '@/i18n'
6
+
7
+ export interface ErrorBoundaryFallbackProps {
8
+ error: Error
9
+ reset: () => void
10
+ }
11
+
12
+ interface ErrorBoundaryProps {
13
+ children: ReactNode
14
+ fallback?: (props: ErrorBoundaryFallbackProps) => ReactNode
15
+ label?: string
16
+ onError?: (error: Error, info: ErrorInfo) => void
17
+ }
18
+
19
+ interface ErrorBoundaryState {
20
+ error: Error | null
21
+ }
22
+
23
+ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
24
+ state: ErrorBoundaryState = { error: null }
25
+
26
+ static getDerivedStateFromError(error: Error): ErrorBoundaryState {
27
+ return { error }
28
+ }
29
+
30
+ componentDidCatch(error: Error, info: ErrorInfo) {
31
+ const tag = this.props.label ? `[error-boundary:${this.props.label}]` : '[error-boundary]'
32
+ console.error(tag, error, info.componentStack)
33
+ this.props.onError?.(error, info)
34
+ }
35
+
36
+ reset = () => {
37
+ this.setState({ error: null })
38
+ }
39
+
40
+ render() {
41
+ const { error } = this.state
42
+
43
+ if (!error) {
44
+ return this.props.children
45
+ }
46
+
47
+ if (this.props.fallback) {
48
+ return this.props.fallback({ error, reset: this.reset })
49
+ }
50
+
51
+ return <RootErrorFallback error={error} reset={this.reset} />
52
+ }
53
+ }
54
+
55
+ function RootErrorFallback({ error, reset }: ErrorBoundaryFallbackProps) {
56
+ const { t } = useI18n()
57
+
58
+ return (
59
+ <div className="fixed inset-0 z-[1500] grid place-items-center bg-(--ui-chat-surface-background) p-6">
60
+ <ErrorState
61
+ className="w-full max-w-[28rem]"
62
+ description={error.message || t.errors.boundaryDesc}
63
+ title={t.errors.boundaryTitle}
64
+ >
65
+ <Button className="font-semibold" onClick={reset} size="lg">
66
+ {t.common.retry}
67
+ </Button>
68
+ <Button onClick={() => window.location.reload()} variant="text">
69
+ {t.errors.reloadWindow}
70
+ </Button>
71
+ <Button onClick={() => void window.NASTECHDesktop?.revealLogs()?.catch(() => undefined)} variant="text">
72
+ {t.errors.openLogs}
73
+ </Button>
74
+ </ErrorState>
75
+ </div>
76
+ )
77
+ }
@@ -0,0 +1,143 @@
1
+ import { cleanup, render, screen } from '@testing-library/react'
2
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
3
+
4
+ import { $desktopBoot } from '@/store/boot'
5
+ import { $desktopOnboarding } from '@/store/onboarding'
6
+ import { $gatewayState, setGatewayState } from '@/store/session'
7
+
8
+ import { BootFailureOverlay } from './boot-failure-overlay'
9
+ import { GatewayConnectingOverlay } from './gateway-connecting-overlay'
10
+
11
+ // Repro for the "remote gateway → stuck on CONNECTING, no way to settings"
12
+ // report. The connecting overlay (z-1200, full-screen, pointer-events on) is
13
+ // shown whenever `gatewayState !== 'open' && !boot.error`. The ONLY escape
14
+ // hatch — BootFailureOverlay, which has "Use local gateway" / "Sign in" /
15
+ // "Retry" — only renders when `boot.error` is set.
16
+ //
17
+ // useGatewayBoot only calls failDesktopBoot() (which sets boot.error) when the
18
+ // INITIAL boot() throws. After the first successful connect (bootCompleted),
19
+ // any later socket drop goes through scheduleReconnect(), which loops FOREVER
20
+ // against the dead remote and never sets boot.error. So gatewayState sits at
21
+ // 'closed'/'error' with boot.error null → CONNECTING forever, recovery overlay
22
+ // never appears, settings unreachable.
23
+
24
+ function resetStores() {
25
+ setGatewayState('idle')
26
+ $desktopBoot.set({
27
+ error: null,
28
+ fakeMode: false,
29
+ message: 'ready',
30
+ phase: 'renderer.ready',
31
+ progress: 100,
32
+ running: false,
33
+ timestamp: Date.now(),
34
+ visible: false
35
+ })
36
+ $desktopOnboarding.set({
37
+ configured: true,
38
+ flow: { status: 'idle' },
39
+ mode: 'oauth',
40
+ providers: null,
41
+ reason: null,
42
+ requested: false,
43
+ firstRunSkipped: false,
44
+ manual: false
45
+ })
46
+ }
47
+
48
+ beforeEach(resetStores)
49
+ afterEach(cleanup)
50
+
51
+ // The connecting overlay renders "CONN" + a scrambled tail inside one
52
+ // uppercase span; match that node specifically so the recovery overlay's
53
+ // "Lost connection…" copy doesn't read as a false positive.
54
+ const isConnectingShown = () =>
55
+ screen.queryAllByText((_, el) => /^CONN[/\\|\-_=+<>~:*A-Z]*$/.test(el?.textContent?.trim() ?? '')).length > 0
56
+ const isRecoveryShown = () =>
57
+ Boolean(screen.queryByText(/use local gateway/i) || screen.queryByText(/retry/i) || screen.queryByText(/sign in/i))
58
+
59
+ describe('connecting overlay vs recovery surface', () => {
60
+ it('hard initial-boot failure surfaces the recovery overlay (the working path)', () => {
61
+ // failDesktopBoot() ran: error set, gateway never opened.
62
+ $desktopBoot.set({ ...$desktopBoot.get(), error: 'NasTech backend did not become ready', running: false, visible: true })
63
+ setGatewayState('error')
64
+
65
+ render(
66
+ <>
67
+ <GatewayConnectingOverlay />
68
+ <BootFailureOverlay />
69
+ </>
70
+ )
71
+
72
+ expect(isRecoveryShown()).toBe(true)
73
+ // Connecting overlay bows out when boot.error is set.
74
+ expect(isConnectingShown()).toBe(false)
75
+ })
76
+
77
+ it('REPRO: remote socket drops AFTER a successful boot → stuck on CONNECTING, no recovery, no settings', () => {
78
+ // 1. Initial boot succeeded: gateway opened, boot completed (no error).
79
+ setGatewayState('open')
80
+ const { rerender } = render(
81
+ <>
82
+ <GatewayConnectingOverlay />
83
+ <BootFailureOverlay />
84
+ </>
85
+ )
86
+ expect(isConnectingShown()).toBe(false)
87
+
88
+ // 2. The remote VPS socket drops (sleep/wake, remote restart, network).
89
+ // bootCompleted is true, so useGatewayBoot routes this through
90
+ // scheduleReconnect() — boot.error stays NULL.
91
+ setGatewayState('closed')
92
+ rerender(
93
+ <>
94
+ <GatewayConnectingOverlay />
95
+ <BootFailureOverlay />
96
+ </>
97
+ )
98
+
99
+ // The connecting overlay reappears and latches...
100
+ expect(isConnectingShown()).toBe(true)
101
+ // ...with NO recovery surface, because boot.error was never set.
102
+ expect(isRecoveryShown()).toBe(false)
103
+
104
+ // 3. Reconnect loops forever against the dead remote: gatewayState bounces
105
+ // closed → error → closed, boot.error never gets set. The user is
106
+ // pinned on CONNECTING with no path to Settings indefinitely.
107
+ setGatewayState('error')
108
+ rerender(
109
+ <>
110
+ <GatewayConnectingOverlay />
111
+ <BootFailureOverlay />
112
+ </>
113
+ )
114
+ expect($desktopBoot.get().error).toBeNull()
115
+ expect(isConnectingShown()).toBe(true)
116
+ expect(isRecoveryShown()).toBe(false)
117
+ })
118
+
119
+ it('FIX: once the prolonged reconnect raises a recoverable boot error, the recovery overlay takes over', () => {
120
+ // Mirrors what useGatewayBoot.scheduleReconnect() now does after ~45s of
121
+ // failed post-boot reconnects: it calls failDesktopBoot(), flipping the UI
122
+ // from the dead-end CONNECTING overlay to the recovery surface.
123
+ setGatewayState('error')
124
+ $desktopBoot.set({
125
+ ...$desktopBoot.get(),
126
+ error: 'Lost connection to the NasTech gateway and could not reconnect.',
127
+ running: false,
128
+ visible: true
129
+ })
130
+
131
+ render(
132
+ <>
133
+ <GatewayConnectingOverlay />
134
+ <BootFailureOverlay />
135
+ </>
136
+ )
137
+
138
+ // Escape hatch is now reachable; the connecting overlay bows out.
139
+ expect(isRecoveryShown()).toBe(true)
140
+ expect(screen.getByText(/use local gateway/i)).toBeTruthy()
141
+ expect(isConnectingShown()).toBe(false)
142
+ })
143
+ })
@@ -0,0 +1,183 @@
1
+ import { useStore } from '@nanostores/react'
2
+ import { useEffect, useRef, useState } from 'react'
3
+
4
+ import { cn } from '@/lib/utils'
5
+ import { $desktopBoot } from '@/store/boot'
6
+ import { $gatewayState } from '@/store/session'
7
+
8
+ // Static, always-legible prefix; only TAIL ever scrambles. Splitting them at
9
+ // the render level means no timer logic (even a stale HMR one) can ever
10
+ // scramble "CONN".
11
+ const PREFIX = 'CONN'
12
+ const TAIL = 'ECTING'
13
+ // Even-weight mono ascii so cycling glyphs don't jump width (matches the
14
+ // nousnet-web download-button decode effect).
15
+ const SCRAMBLE_CHARS = '/\\|-_=+<>~:*'
16
+ const TICK_MS = 45
17
+
18
+ // Exit choreography (ms): text fades down + out, hold, then the overlay fades.
19
+ const TEXT_OUT_MS = 360
20
+ const POST_TEXT_HOLD_MS = 300
21
+ const OVERLAY_OUT_MS = 520
22
+ // Preview-only: how long to "connect" for, and the pause before replaying.
23
+ const PREVIEW_CONNECT_MS = 2600
24
+ const PREVIEW_REPLAY_MS = 1100
25
+
26
+ type Phase = 'live' | 'text-out' | 'overlay-out' | 'gone'
27
+
28
+ // Dev affordance: a warm Cmd+R reconnects almost instantly, so the overlay
29
+ // only flashes. Load with `?connecting=1` to force a looping preview.
30
+ function forcedPreview(): boolean {
31
+ if (!import.meta.env.DEV || typeof window === 'undefined') {
32
+ return false
33
+ }
34
+
35
+ try {
36
+ return new URLSearchParams(window.location.search).get('connecting') === '1'
37
+ } catch {
38
+ return false
39
+ }
40
+ }
41
+
42
+ function scrambledTail(resolvedCount: number): string {
43
+ return Array.from(TAIL, (ch, i) =>
44
+ i < resolvedCount ? ch : SCRAMBLE_CHARS[(Math.random() * SCRAMBLE_CHARS.length) | 0]
45
+ ).join('')
46
+ }
47
+
48
+ export function GatewayConnectingOverlay() {
49
+ const gatewayState = useStore($gatewayState)
50
+ const boot = useStore($desktopBoot)
51
+ const [previewing] = useState(forcedPreview)
52
+ const [tail, setTail] = useState(TAIL)
53
+ const [phase, setPhase] = useState<Phase>('live')
54
+
55
+ const connecting = gatewayState !== 'open' && !boot.error
56
+ // Latches once we've actually shown the overlay, so the brief frame where
57
+ // gatewayState flips to "open" (connecting -> false) before the exit phase
58
+ // kicks in doesn't unmount us and cause a flash.
59
+ const shownRef = useRef(false)
60
+
61
+ if (previewing || connecting) {
62
+ shownRef.current = true
63
+ }
64
+
65
+ // Decode loop — only while live (freeze the resolved word during the exit).
66
+ useEffect(() => {
67
+ if (phase !== 'live' || (!previewing && !connecting)) {
68
+ return
69
+ }
70
+
71
+ let resolved = 0
72
+ let hold = 0
73
+
74
+ const id = window.setInterval(() => {
75
+ if (resolved >= TAIL.length) {
76
+ hold += 1
77
+
78
+ if (hold > 16) {
79
+ resolved = 0
80
+ hold = 0
81
+ }
82
+
83
+ setTail(TAIL)
84
+
85
+ return
86
+ }
87
+
88
+ resolved += 0.5
89
+ setTail(scrambledTail(Math.floor(resolved)))
90
+ }, TICK_MS)
91
+
92
+ return () => window.clearInterval(id)
93
+ }, [phase, previewing, connecting])
94
+
95
+ // Kick off the exit when connected: real connect, or a faked timer in preview.
96
+ useEffect(() => {
97
+ if (phase !== 'live') {
98
+ return
99
+ }
100
+
101
+ if (previewing) {
102
+ const id = window.setTimeout(() => {
103
+ setTail(TAIL)
104
+ setPhase('text-out')
105
+ }, PREVIEW_CONNECT_MS)
106
+
107
+ return () => window.clearTimeout(id)
108
+ }
109
+
110
+ if (gatewayState === 'open' && shownRef.current) {
111
+ setTail(TAIL)
112
+ setPhase('text-out')
113
+ }
114
+ }, [phase, previewing, gatewayState])
115
+
116
+ // Advance the exit choreography: text-out -> overlay-out -> gone.
117
+ useEffect(() => {
118
+ if (phase === 'text-out') {
119
+ const id = window.setTimeout(() => setPhase('overlay-out'), TEXT_OUT_MS + POST_TEXT_HOLD_MS)
120
+
121
+ return () => window.clearTimeout(id)
122
+ }
123
+
124
+ if (phase === 'overlay-out') {
125
+ const id = window.setTimeout(() => setPhase('gone'), OVERLAY_OUT_MS)
126
+
127
+ return () => window.clearTimeout(id)
128
+ }
129
+
130
+ // Preview replays so we can keep watching the transition.
131
+ if (phase === 'gone' && previewing) {
132
+ const id = window.setTimeout(() => {
133
+ setTail(TAIL)
134
+ setPhase('live')
135
+ }, PREVIEW_REPLAY_MS)
136
+
137
+ return () => window.clearTimeout(id)
138
+ }
139
+ }, [phase, previewing])
140
+
141
+ // Boot failed — BootFailureOverlay owns the screen; don't linger behind it.
142
+ if (boot.error && !previewing) {
143
+ return null
144
+ }
145
+
146
+ // Real connect: once the fade finishes, get out of the way for good.
147
+ if (phase === 'gone' && !previewing) {
148
+ return null
149
+ }
150
+
151
+ // Never showed (e.g. gateway already up on a warm reload) — stay out.
152
+ if (!previewing && !connecting && !shownRef.current) {
153
+ return null
154
+ }
155
+
156
+ const leaving = phase !== 'live'
157
+ const overlayHidden = phase === 'overlay-out' || phase === 'gone'
158
+
159
+ return (
160
+ <div
161
+ className={cn(
162
+ 'fixed inset-0 z-[1200] grid place-items-center bg-(--ui-chat-surface-background) transition-opacity duration-500 ease-out',
163
+ overlayHidden ? 'pointer-events-none opacity-0' : 'opacity-100'
164
+ )}
165
+ >
166
+ <style>{'@keyframes gco-cursor { 0%, 49% { opacity: 1 } 50%, 100% { opacity: 0 } }'}</style>
167
+ <span
168
+ className={cn(
169
+ 'inline-flex items-center pl-[0.4em] font-mono text-[0.64rem] font-semibold uppercase tracking-[0.4em] tabular-nums text-(--theme-primary) transition duration-300 ease-out',
170
+ leaving ? 'translate-y-2 opacity-0 saturate-0' : 'translate-y-0 opacity-100 saturate-100'
171
+ )}
172
+ >
173
+ {PREFIX}
174
+ {tail}
175
+ <span
176
+ aria-hidden="true"
177
+ className="dither ml-0.5 inline-block size-2 shrink-0 -translate-y-px rounded-[1px]"
178
+ style={{ animation: 'gco-cursor 1s step-end infinite' }}
179
+ />
180
+ </span>
181
+ </div>
182
+ )
183
+ }
@@ -0,0 +1,19 @@
1
+ import { useStore } from '@nanostores/react'
2
+ import { type ReactNode, useEffect } from 'react'
3
+ import { useWebHaptics } from 'web-haptics/react'
4
+
5
+ import { registerHapticTrigger } from '@/lib/haptics'
6
+ import { $hapticsMuted } from '@/store/haptics'
7
+
8
+ export function HapticsProvider({ children }: { children: ReactNode }) {
9
+ const muted = useStore($hapticsMuted)
10
+ const { trigger } = useWebHaptics({ debug: true, showSwitch: false })
11
+
12
+ useEffect(() => {
13
+ registerHapticTrigger(muted ? null : trigger)
14
+
15
+ return () => registerHapticTrigger(null)
16
+ }, [muted, trigger])
17
+
18
+ return <>{children}</>
19
+ }
@@ -0,0 +1,53 @@
1
+ import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
2
+ import { afterEach, describe, expect, it, vi } from 'vitest'
3
+
4
+ import type { NasTechConfigRecord } from '@/nastech'
5
+ import { type I18nConfigClient, I18nProvider } from '@/i18n'
6
+
7
+ import { LanguageSwitcher } from './language-switcher'
8
+
9
+ // cmdk (the searchable list) wires a ResizeObserver and scrolls the active
10
+ // item into view — neither exists in jsdom. Stub them, matching the polyfill
11
+ // idiom in tool-approval-group.test.tsx.
12
+ class TestResizeObserver {
13
+ observe() {}
14
+ unobserve() {}
15
+ disconnect() {}
16
+ }
17
+
18
+ vi.stubGlobal('ResizeObserver', TestResizeObserver)
19
+
20
+ Element.prototype.scrollIntoView = function scrollIntoView() {}
21
+
22
+ describe('LanguageSwitcher', () => {
23
+ afterEach(() => {
24
+ cleanup()
25
+ vi.restoreAllMocks()
26
+ })
27
+
28
+ it('persists language changes through display.language config', async () => {
29
+ const saveConfig = vi.fn().mockResolvedValue({ ok: true })
30
+ const latestConfig: NasTechConfigRecord = { display: { language: 'en', skin: 'slate' } }
31
+
32
+ const configClient: I18nConfigClient = {
33
+ getConfig: vi.fn().mockResolvedValue(latestConfig),
34
+ saveConfig
35
+ }
36
+
37
+ render(
38
+ <I18nProvider configClient={configClient}>
39
+ <LanguageSwitcher />
40
+ </I18nProvider>
41
+ )
42
+
43
+ await waitFor(() => {
44
+ expect(screen.getByRole('button', { name: 'Switch language' }).hasAttribute('disabled')).toBe(false)
45
+ })
46
+
47
+ fireEvent.click(screen.getByRole('button', { name: 'Switch language' }))
48
+ fireEvent.click(screen.getByRole('option', { name: /日本語/i }))
49
+
50
+ await waitFor(() => expect(saveConfig).toHaveBeenCalledTimes(1))
51
+ expect(saveConfig).toHaveBeenCalledWith({ display: { language: 'ja', skin: 'slate' } })
52
+ })
53
+ })
@@ -0,0 +1,175 @@
1
+ import { useState } from 'react'
2
+
3
+ import { Button } from '@/components/ui/button'
4
+ import { Command, CommandInput, CommandItem, CommandList } from '@/components/ui/command'
5
+ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
6
+ import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet'
7
+ import { useIsMobile } from '@/hooks/use-mobile'
8
+ import { type Locale, LOCALE_META, useI18n } from '@/i18n'
9
+ import { triggerHaptic } from '@/lib/haptics'
10
+ import { Check, ChevronDown, Globe } from '@/lib/icons'
11
+ import { cn } from '@/lib/utils'
12
+ import { notifyError } from '@/store/notifications'
13
+
14
+ export interface LanguageSwitcherProps {
15
+ className?: string
16
+ collapsed?: boolean
17
+ dropUp?: boolean
18
+ }
19
+
20
+ interface LanguageCommandProps {
21
+ allLocales: Array<[Locale, (typeof LOCALE_META)[Locale]]>
22
+ autoFocus?: boolean
23
+ disabled?: boolean
24
+ locale: Locale
25
+ noResults: string
26
+ onSelect: (code: Locale) => void
27
+ searchPlaceholder: string
28
+ }
29
+
30
+ export function LanguageSwitcher({ className, collapsed = false, dropUp = false }: LanguageSwitcherProps) {
31
+ const { isSavingLocale, locale, setLocale, t } = useI18n()
32
+ const [open, setOpen] = useState(false)
33
+ const isMobile = useIsMobile()
34
+ const useMobileSheet = Boolean(dropUp && isMobile)
35
+ const current = LOCALE_META[locale]
36
+ const allLocales = Object.entries(LOCALE_META) as Array<[Locale, typeof current]>
37
+ const title = t.language.switchTo
38
+
39
+ const selectLocale = async (code: Locale) => {
40
+ if (code === locale || isSavingLocale) {
41
+ setOpen(false)
42
+
43
+ return
44
+ }
45
+
46
+ triggerHaptic('selection')
47
+
48
+ try {
49
+ await setLocale(code)
50
+ setOpen(false)
51
+ triggerHaptic('success')
52
+ } catch (error) {
53
+ notifyError(error, t.language.saveError)
54
+ }
55
+ }
56
+
57
+ const trigger = (
58
+ <Button
59
+ aria-expanded={open}
60
+ aria-label={title}
61
+ className={cn(
62
+ 'min-w-32 justify-between gap-2 border-(--ui-stroke-tertiary) bg-(--ui-bg-quinary) px-2.5 text-left text-muted-foreground hover:text-foreground',
63
+ collapsed && 'min-w-0 px-2',
64
+ className
65
+ )}
66
+ disabled={isSavingLocale}
67
+ size="sm"
68
+ title={title}
69
+ type="button"
70
+ variant="outline"
71
+ >
72
+ <span className="inline-flex min-w-0 items-center gap-2">
73
+ <Globe className="size-3.5 shrink-0" />
74
+ {!collapsed && <span className="truncate">{current.name}</span>}
75
+ </span>
76
+ {!collapsed && <ChevronDown className="size-3 shrink-0 opacity-70" />}
77
+ </Button>
78
+ )
79
+
80
+ if (useMobileSheet) {
81
+ return (
82
+ <Sheet onOpenChange={setOpen} open={open}>
83
+ <SheetTrigger asChild>{trigger}</SheetTrigger>
84
+ <SheetContent className="max-h-[min(28rem,80vh)] rounded-t-xl" side="bottom">
85
+ <SheetHeader>
86
+ <SheetTitle>{title}</SheetTitle>
87
+ <SheetDescription>{t.language.description}</SheetDescription>
88
+ </SheetHeader>
89
+ <LanguageCommand
90
+ allLocales={allLocales}
91
+ disabled={isSavingLocale}
92
+ locale={locale}
93
+ noResults={t.language.noResults}
94
+ onSelect={code => void selectLocale(code)}
95
+ searchPlaceholder={t.language.searchPlaceholder}
96
+ />
97
+ </SheetContent>
98
+ </Sheet>
99
+ )
100
+ }
101
+
102
+ return (
103
+ <Popover onOpenChange={setOpen} open={open}>
104
+ <PopoverTrigger asChild>{trigger}</PopoverTrigger>
105
+ <PopoverContent align="end" className="w-56 p-0" side={dropUp ? 'top' : 'bottom'}>
106
+ <LanguageCommand
107
+ allLocales={allLocales}
108
+ autoFocus
109
+ disabled={isSavingLocale}
110
+ locale={locale}
111
+ noResults={t.language.noResults}
112
+ onSelect={code => void selectLocale(code)}
113
+ searchPlaceholder={t.language.searchPlaceholder}
114
+ />
115
+ </PopoverContent>
116
+ </Popover>
117
+ )
118
+ }
119
+
120
+ function LanguageCommand({
121
+ allLocales,
122
+ autoFocus,
123
+ disabled,
124
+ locale,
125
+ noResults,
126
+ onSelect,
127
+ searchPlaceholder
128
+ }: LanguageCommandProps) {
129
+ const [search, setSearch] = useState('')
130
+
131
+ // Own the search term and filter manually. cmdk's built-in shouldFilter
132
+ // reorders items by its fuzzy-match score (≈alphabetical with an empty
133
+ // query), which destroys the curated en→zh→zh-hant→ja order. We disable it
134
+ // and do a plain substring filter that preserves array order — matching
135
+ // model-picker.tsx. Match against the endonym, the (hidden) English name,
136
+ // and the locale code so "日本"/"japanese"/"ja" all find Japanese.
137
+ const q = search.trim().toLowerCase()
138
+
139
+ const filtered = allLocales.filter(
140
+ ([code, meta]) =>
141
+ !q ||
142
+ meta.name.toLowerCase().includes(q) ||
143
+ meta.englishName.toLowerCase().includes(q) ||
144
+ code.toLowerCase().includes(q)
145
+ )
146
+
147
+ return (
148
+ <Command className="bg-transparent" shouldFilter={false}>
149
+ <CommandInput autoFocus={autoFocus} onValueChange={setSearch} placeholder={searchPlaceholder} value={search} />
150
+ <CommandList className="max-h-80 p-1">
151
+ {filtered.length === 0 ? (
152
+ <div className="py-6 text-center text-sm text-muted-foreground">{noResults}</div>
153
+ ) : (
154
+ filtered.map(([code, meta]) => {
155
+ const selected = code === locale
156
+
157
+ return (
158
+ <CommandItem
159
+ className={cn(selected ? 'font-medium text-foreground' : 'text-muted-foreground')}
160
+ disabled={disabled}
161
+ key={code}
162
+ onSelect={() => onSelect(code)}
163
+ value={code}
164
+ >
165
+ <Check className={cn('size-3.5 shrink-0 text-primary', !selected && 'invisible')} />
166
+ <span className="min-w-0 flex-1 truncate">{meta.name}</span>
167
+ <span className="font-mono text-[0.65rem] uppercase text-(--ui-text-tertiary)">{code}</span>
168
+ </CommandItem>
169
+ )
170
+ })
171
+ )}
172
+ </CommandList>
173
+ </Command>
174
+ )
175
+ }