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,150 @@
1
+ import type { FC } from 'react'
2
+ import { Fragment, useMemo } from 'react'
3
+
4
+ import { DirectiveContent } from '@/components/assistant-ui/directive-text'
5
+ import { cn } from '@/lib/utils'
6
+
7
+ // User messages should render the bare-minimum of markdown: backtick `code`
8
+ // spans and ``` fenced blocks. We deliberately don't pull in the full
9
+ // assistant Markdown pipeline (Streamdown + KaTeX + syntax highlighter)
10
+ // because user input rarely contains structured docs and the heavy pipeline
11
+ // adds a lot of runtime cost per bubble.
12
+ //
13
+ // Directive chips (`@file:`, `@image:`, ...) still resolve via DirectiveContent
14
+ // inside the plain-text segments.
15
+
16
+ interface FenceSegment {
17
+ kind: 'fence'
18
+ code: string
19
+ lang: string | null
20
+ }
21
+
22
+ interface InlineSegment {
23
+ kind: 'inline'
24
+ text: string
25
+ }
26
+
27
+ interface InlineCodeSegment {
28
+ kind: 'inline-code'
29
+ code: string
30
+ }
31
+
32
+ interface InlineTextSegment {
33
+ kind: 'inline-text'
34
+ text: string
35
+ }
36
+
37
+ type TopSegment = FenceSegment | InlineSegment
38
+ type InlineNode = InlineCodeSegment | InlineTextSegment
39
+
40
+ const FENCE_RE = /```([^\n`]*)\n([\s\S]*?)```/g
41
+
42
+ // Greedy backtick run length so ``code with `backticks` inside`` works.
43
+ const INLINE_CODE_RE = /(`+)([^`\n][\s\S]*?)\1/g
44
+
45
+ function splitFences(text: string): TopSegment[] {
46
+ const segments: TopSegment[] = []
47
+ let cursor = 0
48
+
49
+ for (const match of text.matchAll(FENCE_RE)) {
50
+ const start = match.index ?? 0
51
+
52
+ if (start > cursor) {
53
+ segments.push({ kind: 'inline', text: text.slice(cursor, start) })
54
+ }
55
+
56
+ segments.push({
57
+ kind: 'fence',
58
+ lang: (match[1] || '').trim() || null,
59
+ code: match[2] ?? ''
60
+ })
61
+ cursor = start + match[0].length
62
+ }
63
+
64
+ if (cursor < text.length) {
65
+ segments.push({ kind: 'inline', text: text.slice(cursor) })
66
+ }
67
+
68
+ return segments
69
+ }
70
+
71
+ function splitInlineCode(text: string): InlineNode[] {
72
+ const nodes: InlineNode[] = []
73
+ let cursor = 0
74
+
75
+ for (const match of text.matchAll(INLINE_CODE_RE)) {
76
+ const start = match.index ?? 0
77
+
78
+ if (start > cursor) {
79
+ nodes.push({ kind: 'inline-text', text: text.slice(cursor, start) })
80
+ }
81
+
82
+ nodes.push({ kind: 'inline-code', code: match[2] })
83
+ cursor = start + match[0].length
84
+ }
85
+
86
+ if (cursor < text.length) {
87
+ nodes.push({ kind: 'inline-text', text: text.slice(cursor) })
88
+ }
89
+
90
+ return nodes
91
+ }
92
+
93
+ interface UserMessageTextProps {
94
+ text: string
95
+ className?: string
96
+ }
97
+
98
+ export const UserMessageText: FC<UserMessageTextProps> = ({ className, text }) => {
99
+ const top = useMemo(() => splitFences(text), [text])
100
+
101
+ return (
102
+ <span className={cn('block', className)} data-slot="aui_user-message-text">
103
+ {top.map((segment, segmentIndex) => {
104
+ if (segment.kind === 'fence') {
105
+ return (
106
+ <pre
107
+ className="my-1.5 max-w-full overflow-x-auto rounded-md border border-border/45 bg-[color-mix(in_srgb,currentColor_5%,transparent)] px-2.5 py-2 font-mono text-[0.86em] leading-snug"
108
+ data-slot="aui_user-fence"
109
+ key={`fence-${segmentIndex}`}
110
+ >
111
+ <code className="block whitespace-pre">{segment.code}</code>
112
+ </pre>
113
+ )
114
+ }
115
+
116
+ return (
117
+ <Fragment key={`inline-${segmentIndex}`}>
118
+ <InlineSegmentView text={segment.text} />
119
+ </Fragment>
120
+ )
121
+ })}
122
+ </span>
123
+ )
124
+ }
125
+
126
+ const InlineSegmentView: FC<{ text: string }> = ({ text }) => {
127
+ const nodes = useMemo(() => splitInlineCode(text), [text])
128
+
129
+ return (
130
+ <span className="wrap-anywhere block whitespace-pre-line">
131
+ {nodes.map((node, nodeIndex) =>
132
+ node.kind === 'inline-code' ? (
133
+ <code
134
+ className="mx-px rounded bg-[color-mix(in_srgb,currentColor_8%,transparent)] px-1 py-px font-mono text-[0.92em]"
135
+ data-slot="aui_user-inline-code"
136
+ key={`code-${nodeIndex}`}
137
+ >
138
+ {node.code}
139
+ </code>
140
+ ) : (
141
+ // Pass plain-text bits through DirectiveContent so @file:/@url: chips
142
+ // still render. DirectiveContent already preserves whitespace.
143
+ <Fragment key={`text-${nodeIndex}`}>
144
+ <DirectiveContent text={node.text} />
145
+ </Fragment>
146
+ )
147
+ )}
148
+ </span>
149
+ )
150
+ }
@@ -0,0 +1,246 @@
1
+ import { useStore } from '@nanostores/react'
2
+ import { useEffect, useState } from 'react'
3
+
4
+ import { Button } from '@/components/ui/button'
5
+ import { ErrorIcon } from '@/components/ui/error-state'
6
+ import { LogView } from '@/components/ui/log-view'
7
+ import type { DesktopConnectionConfig } from '@/global'
8
+ import { useI18n } from '@/i18n'
9
+ import { FileText, Loader2, LogIn, RefreshCw, Wrench } from '@/lib/icons'
10
+ import { $desktopBoot } from '@/store/boot'
11
+ import { notify, notifyError } from '@/store/notifications'
12
+ import { $desktopOnboarding } from '@/store/onboarding'
13
+
14
+ import type { RemoteReauth } from './boot-failure-reauth'
15
+ import { deriveProviderShape, isRemoteReauthFailure, signInLabel } from './boot-failure-reauth'
16
+
17
+ type BusyAction = 'local' | 'repair' | 'retry' | 'signin' | null
18
+
19
+ // A remote gateway whose access cookie has lapsed (e.g. the dashboard
20
+ // restarted on the remote box) boots into this overlay with a reauth-shaped
21
+ // error. The local-recovery buttons (Retry resets the local bootstrap latch;
22
+ // Repair re-runs the installer) are no-ops for that case — the only fix is to
23
+ // re-establish the remote session. The detection + copy helpers live in
24
+ // ./boot-failure-reauth so they're unit-testable without a React render.
25
+
26
+ // Recovery surface for a hard boot failure (gateway never came up, backend
27
+ // exited during startup, bootstrap latched, …). Without this the app shell
28
+ // renders dead — "gateway offline", no composer, only a toast — with no way
29
+ // to retry, repair the install, switch the gateway, or find the logs.
30
+ export function BootFailureOverlay() {
31
+ const boot = useStore($desktopBoot)
32
+ const onboarding = useStore($desktopOnboarding)
33
+ const { t } = useI18n()
34
+ const [busy, setBusy] = useState<BusyAction>(null)
35
+ const [logs, setLogs] = useState<string[]>([])
36
+ const [showLogs, setShowLogs] = useState(false)
37
+ const [remoteReauth, setRemoteReauth] = useState<RemoteReauth | null>(null)
38
+
39
+ const visible = Boolean(boot.error) && !boot.running
40
+ // While first-run onboarding owns the picker/flow we let it surface its own
41
+ // progress; the recovery overlay is for hard failures, which it covers via a
42
+ // higher z-index regardless of onboarding state.
43
+ const suppressed = onboarding.flow.status !== 'idle' && onboarding.flow.status !== 'error'
44
+
45
+ useEffect(() => {
46
+ if (!visible) {
47
+ return
48
+ }
49
+
50
+ void window.NASTECHDesktop
51
+ ?.getRecentLogs()
52
+ .then(res => setLogs(res.lines ?? []))
53
+ .catch(() => undefined)
54
+ }, [visible])
55
+
56
+ // Resolve whether this boot failure is a remote-gateway reauth so we can
57
+ // offer the actionable "Sign in" path instead of the local-only recovery
58
+ // buttons. Runs whenever the overlay becomes visible.
59
+ useEffect(() => {
60
+ if (!visible) {
61
+ setRemoteReauth(null)
62
+
63
+ return
64
+ }
65
+
66
+ let cancelled = false
67
+
68
+ void (async () => {
69
+ const desktop = window.NASTECHDesktop
70
+
71
+ if (!desktop?.getConnectionConfig) {
72
+ return
73
+ }
74
+
75
+ let config: DesktopConnectionConfig
76
+
77
+ try {
78
+ config = await desktop.getConnectionConfig()
79
+ } catch {
80
+ return
81
+ }
82
+
83
+ if (cancelled || !isRemoteReauthFailure(config)) {
84
+ return
85
+ }
86
+
87
+ // Best-effort probe for the provider shape so the button copy matches
88
+ // what the user will see in the login window (password form vs OAuth
89
+ // redirect). Probe failure just keeps the generic copy.
90
+ let shape = deriveProviderShape(null)
91
+
92
+ try {
93
+ const probe = await desktop.probeConnectionConfig(config.remoteUrl)
94
+ shape = deriveProviderShape(probe?.providers)
95
+ } catch {
96
+ // Generic copy is fine.
97
+ }
98
+
99
+ if (!cancelled) {
100
+ setRemoteReauth({ url: config.remoteUrl, ...shape })
101
+ }
102
+ })()
103
+
104
+ return () => {
105
+ cancelled = true
106
+ }
107
+ }, [visible])
108
+
109
+ if (!visible || suppressed) {
110
+ return null
111
+ }
112
+
113
+ const retry = async () => {
114
+ setBusy('retry')
115
+ await window.NASTECHDesktop?.resetBootstrap().catch(() => undefined)
116
+ window.location.reload()
117
+ }
118
+
119
+ const repair = async () => {
120
+ setBusy('repair')
121
+ await window.NASTECHDesktop?.repairBootstrap().catch(() => undefined)
122
+ window.location.reload()
123
+ }
124
+
125
+ const switchToLocalGateway = async () => {
126
+ setBusy('local')
127
+ // applyConnectionConfig reloads the window from the main process.
128
+ await window.NASTECHDesktop?.applyConnectionConfig({ mode: 'local' }).catch(() => undefined)
129
+ setBusy(null)
130
+ }
131
+
132
+ // Open the gateway's login window (renders the username/password form for a
133
+ // basic gateway, or the OAuth redirect otherwise — the desktop drives both
134
+ // through the same window). On a successful sign-in the session cookie is
135
+ // re-established in the persistent partition; reload so boot re-runs and the
136
+ // reconnect now mints a ticket against a live session.
137
+ const signInRemote = async () => {
138
+ if (!remoteReauth) {
139
+ return
140
+ }
141
+
142
+ setBusy('signin')
143
+
144
+ try {
145
+ const result = await window.NASTECHDesktop?.oauthLoginConnectionConfig(remoteReauth.url)
146
+
147
+ if (result?.connected) {
148
+ notify({ kind: 'success', title: t.boot.failure.signedInTitle, message: t.boot.failure.signedInMessage })
149
+ window.location.reload()
150
+
151
+ return
152
+ }
153
+
154
+ notify({
155
+ kind: 'warning',
156
+ title: t.boot.failure.signInIncompleteTitle,
157
+ message: t.boot.failure.signInIncompleteMessage
158
+ })
159
+ } catch (err) {
160
+ notifyError(err, t.boot.failure.signInFailed)
161
+ } finally {
162
+ setBusy(null)
163
+ }
164
+ }
165
+
166
+ const openLogs = () => void window.NASTECHDesktop?.revealLogs().catch(() => undefined)
167
+ const copy = t.boot.failure
168
+
169
+ const label = signInLabel(remoteReauth, {
170
+ identityProvider: copy.identityProvider,
171
+ remoteGateway: copy.signInToRemoteGateway,
172
+ withProvider: copy.signInWithProvider
173
+ })
174
+
175
+ return (
176
+ <div className="fixed inset-0 z-[1400] flex items-center justify-center bg-(--ui-chat-surface-background) p-6">
177
+ <div className="w-full max-w-[40rem] overflow-hidden rounded-xl border border-(--stroke-nastech) bg-(--ui-chat-bubble-background) shadow-nastech">
178
+ <div className="flex items-start gap-3 px-5 py-4">
179
+ <ErrorIcon className="mt-0.5" size="1.25rem" />
180
+ <div>
181
+ <h2 className="text-[0.9375rem] font-semibold tracking-tight">
182
+ {remoteReauth ? copy.remoteTitle : copy.title}
183
+ </h2>
184
+ <p className="mt-1 text-[0.8125rem] leading-5 text-(--ui-text-tertiary)">
185
+ {remoteReauth ? copy.remoteDescription : copy.description}
186
+ </p>
187
+ </div>
188
+ </div>
189
+
190
+ <div className="grid gap-4 p-5">
191
+ <div className="rounded-2xl border border-destructive/30 bg-destructive/10 px-4 py-3 text-xs text-destructive">
192
+ {boot.error}
193
+ </div>
194
+
195
+ <div className="grid gap-2">
196
+ <div className="flex flex-wrap gap-2">
197
+ {remoteReauth ? (
198
+ <Button disabled={Boolean(busy)} onClick={() => void signInRemote()}>
199
+ {busy === 'signin' ? <Loader2 className="animate-spin" /> : <LogIn />}
200
+ {label}
201
+ </Button>
202
+ ) : (
203
+ <Button disabled={Boolean(busy)} onClick={() => void retry()}>
204
+ {busy === 'retry' ? <Loader2 className="animate-spin" /> : <RefreshCw />}
205
+ {copy.retry}
206
+ </Button>
207
+ )}
208
+ {!remoteReauth ? (
209
+ <Button disabled={Boolean(busy)} onClick={() => void repair()} variant="secondary">
210
+ {busy === 'repair' ? <Loader2 className="animate-spin" /> : <Wrench />}
211
+ {copy.repairInstall}
212
+ </Button>
213
+ ) : null}
214
+ <Button disabled={Boolean(busy)} onClick={() => void switchToLocalGateway()} variant="secondary">
215
+ {busy === 'local' ? <Loader2 className="animate-spin" /> : null}
216
+ {copy.useLocalGateway}
217
+ </Button>
218
+ <Button onClick={openLogs} variant="ghost">
219
+ <FileText />
220
+ {copy.openLogs}
221
+ </Button>
222
+ </div>
223
+ <p className="text-xs text-muted-foreground">
224
+ {remoteReauth ? copy.remoteSignInHint : copy.repairHint}
225
+ </p>
226
+ </div>
227
+
228
+ {logs.length > 0 ? (
229
+ <div className="grid gap-2">
230
+ <Button
231
+ className="-ml-2 self-start font-medium"
232
+ onClick={() => setShowLogs(v => !v)}
233
+ size="xs"
234
+ type="button"
235
+ variant="text"
236
+ >
237
+ {showLogs ? copy.hideRecentLogs : copy.showRecentLogs}
238
+ </Button>
239
+ {showLogs ? <LogView className="max-h-48">{logs.slice(-40).join('')}</LogView> : null}
240
+ </div>
241
+ ) : null}
242
+ </div>
243
+ </div>
244
+ </div>
245
+ )
246
+ }
@@ -0,0 +1,100 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import type { DesktopConnectionConfig } from '@/global'
4
+
5
+ import { deriveProviderShape, isRemoteReauthFailure, signInLabel } from './boot-failure-reauth'
6
+
7
+ function config(overrides: Partial<DesktopConnectionConfig> = {}): DesktopConnectionConfig {
8
+ return {
9
+ envOverride: false,
10
+ mode: 'remote',
11
+ profile: null,
12
+ remoteAuthMode: 'oauth',
13
+ remoteOauthConnected: false,
14
+ remoteTokenPreview: null,
15
+ remoteTokenSet: false,
16
+ remoteUrl: 'https://box:9119',
17
+ ...overrides
18
+ }
19
+ }
20
+
21
+ describe('isRemoteReauthFailure', () => {
22
+ it('true for a remote, gated, disconnected gateway with a URL', () => {
23
+ expect(isRemoteReauthFailure(config())).toBe(true)
24
+ })
25
+
26
+ it('false when the oauth session is still connected', () => {
27
+ expect(isRemoteReauthFailure(config({ remoteOauthConnected: true }))).toBe(false)
28
+ })
29
+
30
+ it('false for a local gateway', () => {
31
+ expect(isRemoteReauthFailure(config({ mode: 'local' }))).toBe(false)
32
+ })
33
+
34
+ it('false for a token (non-gated) remote gateway', () => {
35
+ expect(isRemoteReauthFailure(config({ remoteAuthMode: 'token' }))).toBe(false)
36
+ })
37
+
38
+ it('false when there is no remote URL to sign in against', () => {
39
+ expect(isRemoteReauthFailure(config({ remoteUrl: '' }))).toBe(false)
40
+ })
41
+
42
+ it('false for null/undefined config', () => {
43
+ expect(isRemoteReauthFailure(null)).toBe(false)
44
+ expect(isRemoteReauthFailure(undefined)).toBe(false)
45
+ })
46
+ })
47
+
48
+ describe('deriveProviderShape', () => {
49
+ it('generic copy when there are no providers', () => {
50
+ expect(deriveProviderShape([])).toEqual({ isPassword: false, providerLabel: 'your identity provider' })
51
+ expect(deriveProviderShape(null)).toEqual({ isPassword: false, providerLabel: 'your identity provider' })
52
+ })
53
+
54
+ it('password shape when the sole provider supports password', () => {
55
+ expect(
56
+ deriveProviderShape([{ name: 'basic', displayName: 'Username & Password', supportsPassword: true }])
57
+ ).toEqual({ isPassword: true, providerLabel: 'Username & Password' })
58
+ })
59
+
60
+ it('OAuth shape when the provider is a redirect IDP', () => {
61
+ expect(deriveProviderShape([{ name: 'nastech', displayName: 'NasTech', supportsPassword: false }])).toEqual({
62
+ isPassword: false,
63
+ providerLabel: 'NasTech'
64
+ })
65
+ })
66
+
67
+ it('mixed deployment keeps generic OAuth copy (not every provider is password)', () => {
68
+ const shape = deriveProviderShape([
69
+ { name: 'basic', displayName: 'Username & Password', supportsPassword: true },
70
+ { name: 'nastech', displayName: 'NasTech', supportsPassword: false }
71
+ ])
72
+
73
+ expect(shape.isPassword).toBe(false)
74
+ expect(shape.providerLabel).toBe('Username & Password / NasTech')
75
+ })
76
+
77
+ it('falls back to name when displayName is empty', () => {
78
+ expect(deriveProviderShape([{ name: 'basic', displayName: '', supportsPassword: true }]).providerLabel).toBe(
79
+ 'basic'
80
+ )
81
+ })
82
+ })
83
+
84
+ describe('signInLabel', () => {
85
+ it('password gateway gets the plain "Sign in to remote gateway" copy', () => {
86
+ expect(signInLabel({ url: 'x', isPassword: true, providerLabel: 'Username & Password' })).toBe(
87
+ 'Sign in to remote gateway'
88
+ )
89
+ })
90
+
91
+ it('OAuth gateway names the provider', () => {
92
+ expect(signInLabel({ url: 'x', isPassword: false, providerLabel: 'NasTech' })).toBe(
93
+ 'Sign in with NasTech'
94
+ )
95
+ })
96
+
97
+ it('null reauth falls back to the generic provider phrase', () => {
98
+ expect(signInLabel(null)).toBe('Sign in with your identity provider')
99
+ })
100
+ })
@@ -0,0 +1,81 @@
1
+ import type { DesktopAuthProvider, DesktopConnectionConfig } from '@/global'
2
+
3
+ // Pure helpers for the boot-failure overlay's remote-reauth branch. Kept out
4
+ // of the .tsx so they can be unit-tested without a React/jsdom render (the
5
+ // jsx-dev-runtime resolution in this repo's vitest setup is flaky for
6
+ // component renders, but these are plain functions).
7
+
8
+ export interface RemoteReauth {
9
+ url: string
10
+ // True when every advertised provider is username/password — drives the
11
+ // button copy ("Sign in to remote gateway" vs "Sign in with <provider>"),
12
+ // mirroring the gateway-settings page. Probe is best-effort.
13
+ isPassword: boolean
14
+ providerLabel: string
15
+ }
16
+
17
+ interface SignInCopy {
18
+ identityProvider: string
19
+ remoteGateway: string
20
+ withProvider: (provider: string) => string
21
+ }
22
+
23
+ const DEFAULT_SIGN_IN_COPY: SignInCopy = {
24
+ identityProvider: 'your identity provider',
25
+ remoteGateway: 'Sign in to remote gateway',
26
+ withProvider: provider => `Sign in with ${provider}`
27
+ }
28
+
29
+ // A remote, gated (oauth-bucket), not-currently-connected gateway is a
30
+ // remote-reauth boot failure: the access cookie lapsed (e.g. the remote
31
+ // dashboard restarted) and the local-recovery buttons (Retry/Repair) can't
32
+ // fix it — only re-establishing the remote session can. A connected oauth
33
+ // session, or a token/local gateway, boots for some other reason the
34
+ // local-recovery buttons address, so those return false here.
35
+ export function isRemoteReauthFailure(config: DesktopConnectionConfig | null | undefined): boolean {
36
+ if (!config) {
37
+ return false
38
+ }
39
+
40
+ return (
41
+ config.mode === 'remote' &&
42
+ config.remoteAuthMode === 'oauth' &&
43
+ !config.remoteOauthConnected &&
44
+ Boolean(config.remoteUrl)
45
+ )
46
+ }
47
+
48
+ // Derive the password flag + display label from the probed providers. A
49
+ // gateway is treated as password-style only when EVERY advertised provider
50
+ // supports password (a mixed deployment keeps the generic OAuth copy), so the
51
+ // button copy matches the login window the user is about to see.
52
+ export function deriveProviderShape(providers: DesktopAuthProvider[] | null | undefined): {
53
+ isPassword: boolean
54
+ providerLabel: string
55
+ } {
56
+ const list = providers ?? []
57
+
58
+ if (list.length === 0) {
59
+ return { isPassword: false, providerLabel: 'your identity provider' }
60
+ }
61
+
62
+ const isPassword = list.every(p => Boolean(p.supportsPassword))
63
+
64
+ const providerLabel =
65
+ list.length === 1
66
+ ? list[0].displayName || list[0].name
67
+ : list.map(p => p.displayName || p.name).join(' / ')
68
+
69
+ return { isPassword, providerLabel }
70
+ }
71
+
72
+ // Button copy for the remote sign-in action.
73
+ export function signInLabel(reauth: RemoteReauth | null, copy: SignInCopy = DEFAULT_SIGN_IN_COPY): string {
74
+ if (reauth?.isPassword) {
75
+ return copy.remoteGateway
76
+ }
77
+
78
+ const provider = reauth?.providerLabel === DEFAULT_SIGN_IN_COPY.identityProvider ? copy.identityProvider : reauth?.providerLabel
79
+
80
+ return copy.withProvider(provider ?? copy.identityProvider)
81
+ }
@@ -0,0 +1,19 @@
1
+ import { cn } from '@/lib/utils'
2
+
3
+ const assetPath = (path: string) => `${import.meta.env.BASE_URL}${path.replace(/^\/+/, '')}`
4
+
5
+ // Brand badge: NasTech logo mark on a white tile, identical in light/dark.
6
+ // Fills the tile (softly rounded); size via className (default size-14).
7
+ export function BrandMark({ className, ...props }: React.ComponentProps<'span'>) {
8
+ return (
9
+ <span
10
+ className={cn(
11
+ 'inline-flex size-14 shrink-0 items-center justify-center overflow-hidden rounded-md bg-white',
12
+ className
13
+ )}
14
+ {...props}
15
+ >
16
+ <img alt="" className="size-full object-contain" src={assetPath('nastech-logo.png')} />
17
+ </span>
18
+ )
19
+ }
@@ -0,0 +1,24 @@
1
+ import { cn } from '@/lib/utils'
2
+
3
+ import { formatElapsed } from './activity-timer'
4
+
5
+ interface ActivityTimerTextProps {
6
+ seconds: number
7
+ className?: string
8
+ }
9
+
10
+ export function ActivityTimerText({ seconds, className }: ActivityTimerTextProps) {
11
+ return (
12
+ <span
13
+ className={cn(
14
+ // Tinted with --dt-midground (very low alpha) so the timer reads
15
+ // as part of the same "live signal" cluster as the dither block /
16
+ // arc-border / working-session dot, instead of being neutral chrome.
17
+ 'shrink-0 font-mono text-[0.56rem] leading-none tracking-[0.02em] text-midground/55 tabular-nums',
18
+ className
19
+ )}
20
+ >
21
+ {formatElapsed(seconds)}
22
+ </span>
23
+ )
24
+ }
@@ -0,0 +1,43 @@
1
+ import { act, render, screen } from '@testing-library/react'
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
3
+
4
+ import { __resetElapsedTimerRegistryForTests, useElapsedSeconds } from './activity-timer'
5
+
6
+ function Probe({ active, timerKey }: { active: boolean; timerKey?: string }) {
7
+ const elapsed = useElapsedSeconds(active, timerKey)
8
+
9
+ return <span data-testid="elapsed">{elapsed}</span>
10
+ }
11
+
12
+ describe('useElapsedSeconds', () => {
13
+ beforeEach(() => {
14
+ vi.useFakeTimers()
15
+ vi.setSystemTime(new Date('2026-01-01T00:00:00.000Z'))
16
+ __resetElapsedTimerRegistryForTests()
17
+ })
18
+
19
+ afterEach(() => {
20
+ vi.useRealTimers()
21
+ __resetElapsedTimerRegistryForTests()
22
+ })
23
+
24
+ it('keeps elapsed time stable across remounts for the same key', () => {
25
+ const first = render(<Probe active timerKey="tool:abc" />)
26
+
27
+ act(() => {
28
+ vi.advanceTimersByTime(5_000)
29
+ })
30
+
31
+ expect(screen.getByTestId('elapsed').textContent).toBe('5')
32
+
33
+ first.unmount()
34
+
35
+ act(() => {
36
+ vi.advanceTimersByTime(3_000)
37
+ })
38
+
39
+ render(<Probe active timerKey="tool:abc" />)
40
+
41
+ expect(screen.getByTestId('elapsed').textContent).toBe('8')
42
+ })
43
+ })