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,118 @@
1
+ import { act, cleanup, render } from '@testing-library/react'
2
+ import type { MutableRefObject } from 'react'
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
4
+
5
+ import { $turnStartedAt, setTurnStartedAt } from '@/store/session'
6
+
7
+ import { useSessionStateCache } from './use-session-state-cache'
8
+
9
+ type Cache = ReturnType<typeof useSessionStateCache>
10
+
11
+ interface HarnessProps {
12
+ activeSessionId: string | null
13
+ onReady: (cache: Cache) => void
14
+ selectedStoredSessionId: string | null
15
+ }
16
+
17
+ function Harness({ activeSessionId, onReady, selectedStoredSessionId }: HarnessProps) {
18
+ const busyRef: MutableRefObject<boolean> = { current: false }
19
+ const cache = useSessionStateCache({
20
+ activeSessionId,
21
+ busyRef,
22
+ selectedStoredSessionId,
23
+ setAwaitingResponse: () => undefined,
24
+ setBusy: () => undefined,
25
+ setMessages: () => undefined
26
+ })
27
+
28
+ onReady(cache)
29
+
30
+ return null
31
+ }
32
+
33
+ describe('useSessionStateCache — per-session turn timer', () => {
34
+ beforeEach(() => {
35
+ // The view-sync flush runs on a real rAF in the browser path; in jsdom we
36
+ // want it synchronous so the global mirror is observable immediately. The
37
+ // hook closes over `window.requestAnimationFrame`, so stub that exact ref.
38
+ // Return null (not a handle) so the hook's `viewSyncRafRef.current = rAF(...)`
39
+ // assignment doesn't overwrite the null the synchronous callback just set —
40
+ // otherwise the ref reads truthy and the NEXT sync is suppressed (a real
41
+ // browser returns a handle but runs the callback async, so this race is a
42
+ // test-only artifact of firing synchronously).
43
+ vi.spyOn(window, 'requestAnimationFrame').mockImplementation((cb: FrameRequestCallback) => {
44
+ cb(0)
45
+
46
+ return null as unknown as number
47
+ })
48
+ setTurnStartedAt(null)
49
+ })
50
+
51
+ afterEach(() => {
52
+ cleanup()
53
+ vi.restoreAllMocks()
54
+ setTurnStartedAt(null)
55
+ })
56
+
57
+ it("keeps a background session's running turn clock and never mirrors it to the view", () => {
58
+ let cache!: Cache
59
+ // Active session is "fg-runtime"; the turn starts on the BACKGROUND session.
60
+ render(
61
+ <Harness activeSessionId="fg-runtime" onReady={c => (cache = c)} selectedStoredSessionId="fg-stored" />
62
+ )
63
+
64
+ const startedAt = 1_700_000_000_000
65
+
66
+ act(() => {
67
+ cache.updateSessionState(
68
+ 'bg-runtime',
69
+ state => ({ ...state, busy: true, turnStartedAt: startedAt }),
70
+ 'bg-stored'
71
+ )
72
+ })
73
+
74
+ // The background session's own cache entry holds the clock...
75
+ expect(cache.sessionStateByRuntimeIdRef.current.get('bg-runtime')?.turnStartedAt).toBe(startedAt)
76
+ // ...but the global atom (statusbar timer) is untouched — a background turn
77
+ // must not drive the foreground timer.
78
+ expect($turnStartedAt.get()).toBeNull()
79
+ })
80
+
81
+ it("mirrors the focused session's turn clock into the global atom on view-sync", () => {
82
+ let cache!: Cache
83
+ render(<Harness activeSessionId="fg-runtime" onReady={c => (cache = c)} selectedStoredSessionId="fg-stored" />)
84
+
85
+ const startedAt = 1_700_000_111_000
86
+
87
+ // A turn on the ACTIVE session stages into the view; the flush mirrors its
88
+ // turnStartedAt into the global atom the statusbar reads.
89
+ act(() => {
90
+ cache.updateSessionState(
91
+ 'fg-runtime',
92
+ state => ({ ...state, busy: true, turnStartedAt: startedAt }),
93
+ 'fg-stored'
94
+ )
95
+ })
96
+
97
+ expect($turnStartedAt.get()).toBe(startedAt)
98
+ })
99
+
100
+ it('clears the global clock when the focused turn ends', () => {
101
+ let cache!: Cache
102
+ render(<Harness activeSessionId="fg-runtime" onReady={c => (cache = c)} selectedStoredSessionId="fg-stored" />)
103
+
104
+ act(() => {
105
+ cache.updateSessionState(
106
+ 'fg-runtime',
107
+ state => ({ ...state, busy: true, turnStartedAt: 1_700_000_222_000 }),
108
+ 'fg-stored'
109
+ )
110
+ })
111
+ expect($turnStartedAt.get()).toBe(1_700_000_222_000)
112
+
113
+ act(() => {
114
+ cache.updateSessionState('fg-runtime', state => ({ ...state, busy: false, turnStartedAt: null }))
115
+ })
116
+ expect($turnStartedAt.get()).toBeNull()
117
+ })
118
+ })
@@ -0,0 +1,191 @@
1
+ import { useStore } from '@nanostores/react'
2
+ import { type MutableRefObject, useCallback, useEffect, useRef } from 'react'
3
+
4
+ import type { ChatMessage } from '@/lib/chat-messages'
5
+ import { preserveLocalAssistantErrors } from '@/lib/chat-messages'
6
+ import { createClientSessionState } from '@/lib/chat-runtime'
7
+ import { setMutableRef } from '@/lib/mutable-ref'
8
+ import { $busy, $messages, noteSessionActivity, setSessionAttention, setSessionWorking, setTurnStartedAt } from '@/store/session'
9
+
10
+ import type { ClientSessionState } from '../../types'
11
+
12
+ interface SessionStateCacheOptions {
13
+ activeSessionId: string | null
14
+ busyRef: MutableRefObject<boolean>
15
+ selectedStoredSessionId: string | null
16
+ setAwaitingResponse: (awaiting: boolean) => void
17
+ setBusy: (busy: boolean) => void
18
+ setMessages: (messages: ChatMessage[]) => void
19
+ }
20
+
21
+ export function useSessionStateCache({
22
+ activeSessionId,
23
+ busyRef,
24
+ selectedStoredSessionId,
25
+ setAwaitingResponse,
26
+ setBusy,
27
+ setMessages
28
+ }: SessionStateCacheOptions) {
29
+ const busy = useStore($busy)
30
+ const activeSessionIdRef = useRef<string | null>(null)
31
+ const selectedStoredSessionIdRef = useRef<string | null>(null)
32
+ const sessionStateByRuntimeIdRef = useRef(new Map<string, ClientSessionState>())
33
+ const runtimeIdByStoredSessionIdRef = useRef(new Map<string, string>())
34
+ const pendingViewStateRef = useRef<{ sessionId: string; state: ClientSessionState } | null>(null)
35
+ const viewSyncRafRef = useRef<number | null>(null)
36
+
37
+ useEffect(() => {
38
+ activeSessionIdRef.current = activeSessionId
39
+ }, [activeSessionId])
40
+
41
+ useEffect(() => {
42
+ setMutableRef(busyRef, busy)
43
+ }, [busy, busyRef])
44
+
45
+ useEffect(() => {
46
+ selectedStoredSessionIdRef.current = selectedStoredSessionId
47
+ }, [selectedStoredSessionId])
48
+
49
+ const ensureSessionState = useCallback((sessionId: string, storedSessionId?: string | null) => {
50
+ const existing = sessionStateByRuntimeIdRef.current.get(sessionId)
51
+
52
+ if (existing) {
53
+ if (storedSessionId !== undefined) {
54
+ const previousStoredSessionId = existing.storedSessionId
55
+ existing.storedSessionId = storedSessionId
56
+
57
+ if (storedSessionId) {
58
+ runtimeIdByStoredSessionIdRef.current.set(storedSessionId, sessionId)
59
+
60
+ if (existing.busy) {
61
+ setSessionWorking(storedSessionId, true)
62
+ }
63
+ }
64
+
65
+ if (previousStoredSessionId && previousStoredSessionId !== storedSessionId) {
66
+ setSessionWorking(previousStoredSessionId, false)
67
+ }
68
+ }
69
+
70
+ return existing
71
+ }
72
+
73
+ const created = createClientSessionState(storedSessionId ?? null)
74
+ sessionStateByRuntimeIdRef.current.set(sessionId, created)
75
+
76
+ if (storedSessionId) {
77
+ runtimeIdByStoredSessionIdRef.current.set(storedSessionId, sessionId)
78
+ }
79
+
80
+ return created
81
+ }, [])
82
+
83
+ const flushPendingViewState = useCallback(() => {
84
+ const pending = pendingViewStateRef.current
85
+ pendingViewStateRef.current = null
86
+
87
+ if (!pending || pending.sessionId !== activeSessionIdRef.current) {
88
+ return
89
+ }
90
+
91
+ setMessages(preserveLocalAssistantErrors(pending.state.messages, $messages.get()))
92
+ setBusy(pending.state.busy)
93
+ setMutableRef(busyRef, pending.state.busy)
94
+ setAwaitingResponse(pending.state.awaitingResponse)
95
+ // Mirror the focused session's per-session turn clock into the global
96
+ // atom the statusbar timer reads. Keeps a backgrounded turn's elapsed
97
+ // time intact on focus instead of zeroing it (the "timer restarts" bug).
98
+ setTurnStartedAt(pending.state.turnStartedAt)
99
+ }, [busyRef, setAwaitingResponse, setBusy, setMessages])
100
+
101
+ const syncSessionStateToView = useCallback(
102
+ (sessionId: string, state: ClientSessionState) => {
103
+ // Only the currently-viewed session may stage into the shared `$messages`
104
+ // view. A background session (e.g. one still busy and emitting stream /
105
+ // error updates after the user toggled away) must update its own cache
106
+ // entry but never the view — otherwise its messages clobber the
107
+ // foreground transcript and appear to "bleed" into every other session.
108
+ // The flush below also re-checks the active id, but staging here is what
109
+ // prevents a background write from overwriting an already-pending
110
+ // foreground write within the same animation frame (only one RAF is
111
+ // scheduled, so the last `pendingViewStateRef` writer would otherwise win).
112
+ if (sessionId !== activeSessionIdRef.current) {
113
+ return
114
+ }
115
+
116
+ pendingViewStateRef.current = { sessionId, state }
117
+
118
+ if (viewSyncRafRef.current !== null) {
119
+ return
120
+ }
121
+
122
+ if (typeof window === 'undefined') {
123
+ flushPendingViewState()
124
+
125
+ return
126
+ }
127
+
128
+ viewSyncRafRef.current = window.requestAnimationFrame(() => {
129
+ viewSyncRafRef.current = null
130
+ flushPendingViewState()
131
+ })
132
+ },
133
+ [flushPendingViewState]
134
+ )
135
+
136
+ useEffect(
137
+ () => () => {
138
+ if (viewSyncRafRef.current !== null && typeof window !== 'undefined') {
139
+ window.cancelAnimationFrame(viewSyncRafRef.current)
140
+ viewSyncRafRef.current = null
141
+ }
142
+ },
143
+ []
144
+ )
145
+
146
+ const updateSessionState = useCallback(
147
+ (
148
+ sessionId: string,
149
+ updater: (state: ClientSessionState) => ClientSessionState,
150
+ storedSessionId?: string | null
151
+ ) => {
152
+ const previous = ensureSessionState(sessionId, storedSessionId)
153
+ const next = updater({ ...previous, messages: previous.messages })
154
+ sessionStateByRuntimeIdRef.current.set(sessionId, next)
155
+
156
+ if (previous.storedSessionId !== next.storedSessionId || !next.busy) {
157
+ setSessionWorking(previous.storedSessionId, false)
158
+ }
159
+
160
+ if (previous.storedSessionId !== next.storedSessionId || !next.needsInput) {
161
+ setSessionAttention(previous.storedSessionId, false)
162
+ }
163
+
164
+ setSessionWorking(next.storedSessionId, next.busy)
165
+ setSessionAttention(next.storedSessionId, next.needsInput)
166
+
167
+ // Every state update is effectively a "still alive" heartbeat for
168
+ // streaming events. The session-store watchdog uses this to keep the
169
+ // working flag alive during long-running turns and to clear it once
170
+ // the stream goes silent.
171
+ if (next.busy) {
172
+ noteSessionActivity(next.storedSessionId)
173
+ }
174
+
175
+ syncSessionStateToView(sessionId, next)
176
+
177
+ return next
178
+ },
179
+ [ensureSessionState, syncSessionStateToView]
180
+ )
181
+
182
+ return {
183
+ activeSessionIdRef,
184
+ ensureSessionState,
185
+ runtimeIdByStoredSessionIdRef,
186
+ selectedStoredSessionIdRef,
187
+ sessionStateByRuntimeIdRef,
188
+ syncSessionStateToView,
189
+ updateSessionState
190
+ }
191
+ }
@@ -0,0 +1,32 @@
1
+ import { useStore } from '@nanostores/react'
2
+
3
+ import { SessionPickerDialog } from '@/components/session-picker'
4
+ import { $gatewayState, $selectedStoredSessionId, $sessionPickerOpen, setSessionPickerOpen } from '@/store/session'
5
+
6
+ interface SessionPickerOverlayProps {
7
+ onResume: (storedSessionId: string) => void
8
+ }
9
+
10
+ /**
11
+ * Mounts the session picker that `/resume` (and `/sessions`, `/switch`) opens —
12
+ * the desktop equivalent of the TUI's sessions overlay. Resuming runs through
13
+ * the same `resumeSession` path the sidebar uses.
14
+ */
15
+ export function SessionPickerOverlay({ onResume }: SessionPickerOverlayProps) {
16
+ const open = useStore($sessionPickerOpen)
17
+ const gatewayOpen = useStore($gatewayState) === 'open'
18
+ const activeStoredSessionId = useStore($selectedStoredSessionId)
19
+
20
+ if (!gatewayOpen) {
21
+ return null
22
+ }
23
+
24
+ return (
25
+ <SessionPickerDialog
26
+ activeStoredSessionId={activeStoredSessionId}
27
+ onOpenChange={setSessionPickerOpen}
28
+ onResume={onResume}
29
+ open={open}
30
+ />
31
+ )
32
+ }
@@ -0,0 +1,107 @@
1
+ import { useStore } from '@nanostores/react'
2
+ import { useEffect, useRef } from 'react'
3
+ import { createPortal } from 'react-dom'
4
+ import { useNavigate } from 'react-router-dom'
5
+
6
+ import { sessionTitle } from '@/lib/chat-runtime'
7
+ import { cn } from '@/lib/utils'
8
+ import { $attentionSessionIds, $workingSessionIds } from '@/store/session'
9
+ import { $switcherIndex, $switcherOpen, $switcherSessions, closeSwitcher } from '@/store/session-switcher'
10
+
11
+ import { HUD_ITEM, HUD_POSITION, HUD_SURFACE, HUD_TEXT } from './floating-hud'
12
+ import { sessionRoute } from './routes'
13
+
14
+ // Compact session-switcher HUD — keyboard-driven from `use-keybinds`, rows
15
+ // clickable via mousedown (Ctrl+click on macOS). No Dialog: Tab stays global.
16
+ export function SessionSwitcher() {
17
+ const open = useStore($switcherOpen)
18
+ const sessions = useStore($switcherSessions)
19
+ const index = useStore($switcherIndex)
20
+ const working = useStore($workingSessionIds)
21
+ const attention = useStore($attentionSessionIds)
22
+ const navigate = useNavigate()
23
+
24
+ const activeRef = useRef<HTMLDivElement>(null)
25
+
26
+ useEffect(() => {
27
+ activeRef.current?.scrollIntoView({ block: 'nearest' })
28
+ }, [index, open])
29
+
30
+ if (!open || sessions.length === 0) {
31
+ return null
32
+ }
33
+
34
+ const workingIds = new Set(working)
35
+ const attentionIds = new Set(attention)
36
+
37
+ const pick = (sessionId: string) => {
38
+ closeSwitcher()
39
+ navigate(sessionRoute(sessionId))
40
+ }
41
+
42
+ return createPortal(
43
+ <>
44
+ {/* Transparent click-catcher: click-away closes, but no dim/blur. */}
45
+ <div
46
+ className="fixed inset-0 z-[219]"
47
+ onMouseDown={e => {
48
+ e.preventDefault()
49
+ closeSwitcher()
50
+ }}
51
+ />
52
+ <div
53
+ className={cn(
54
+ HUD_POSITION,
55
+ HUD_SURFACE,
56
+ 'dt-portal-scrollbar z-[220] max-h-[min(22rem,64vh)] w-[min(19rem,calc(100vw-2rem))] select-none overflow-y-auto p-1'
57
+ )}
58
+ >
59
+ {sessions.map((session, i) => {
60
+ const selected = i === index
61
+
62
+ return (
63
+ <div
64
+ className={cn(
65
+ 'flex cursor-pointer items-center rounded leading-tight',
66
+ HUD_ITEM,
67
+ HUD_TEXT,
68
+ selected ? 'bg-accent text-accent-foreground' : 'text-(--ui-text-secondary) hover:bg-(--ui-row-hover-background)'
69
+ )}
70
+ key={session.id}
71
+ onMouseDown={e => {
72
+ e.preventDefault()
73
+ pick(session.id)
74
+ }}
75
+ ref={selected ? activeRef : undefined}
76
+ >
77
+ <SwitcherDot attention={attentionIds.has(session.id)} working={workingIds.has(session.id)} />
78
+ <span className="min-w-0 flex-1 truncate">{sessionTitle(session)}</span>
79
+ {i < 9 && (
80
+ <span
81
+ className={cn(
82
+ 'shrink-0 font-mono text-[0.625rem] tabular-nums',
83
+ selected ? 'text-accent-foreground/70' : 'text-(--ui-text-quaternary)'
84
+ )}
85
+ >
86
+ ⌃{i + 1}
87
+ </span>
88
+ )}
89
+ </div>
90
+ )
91
+ })}
92
+ </div>
93
+ </>,
94
+ document.body
95
+ )
96
+ }
97
+
98
+ function SwitcherDot({ attention, working }: { attention: boolean; working: boolean }) {
99
+ return (
100
+ <span
101
+ className={cn(
102
+ 'size-1 shrink-0 rounded-full',
103
+ attention ? 'bg-amber-400' : working ? 'animate-pulse bg-(--ui-accent)' : 'bg-(--ui-text-quaternary)/50'
104
+ )}
105
+ />
106
+ )
107
+ }
@@ -0,0 +1,173 @@
1
+ import { useStore } from '@nanostores/react'
2
+ import { useEffect, useState } from 'react'
3
+
4
+ import { BrandMark } from '@/components/brand-mark'
5
+ import { Button } from '@/components/ui/button'
6
+ import { type Translations, useI18n } from '@/i18n'
7
+ import { CheckCircle2, ExternalLink, Loader2, RefreshCw, Sparkles } from '@/lib/icons'
8
+ import { cn } from '@/lib/utils'
9
+ import {
10
+ $desktopVersion,
11
+ $updateApply,
12
+ $updateChecking,
13
+ $updateStatus,
14
+ checkUpdates,
15
+ openUpdatesWindow,
16
+ refreshDesktopVersion
17
+ } from '@/store/updates'
18
+
19
+ import { ListRow, SectionHeading, SettingsContent } from './primitives'
20
+
21
+ const RELEASE_NOTES_URL = 'https://github.com/nastech-ai/NasTech-Agent/releases'
22
+
23
+ function relativeTime(ms: number | undefined, a: Translations['settings']['about']) {
24
+ if (!ms) {
25
+ return a.never
26
+ }
27
+
28
+ const diff = Date.now() - ms
29
+
30
+ if (diff < 60_000) {
31
+ return a.justNow
32
+ }
33
+
34
+ if (diff < 3_600_000) {
35
+ return a.minAgo(Math.round(diff / 60_000))
36
+ }
37
+
38
+ if (diff < 86_400_000) {
39
+ return a.hoursAgo(Math.round(diff / 3_600_000))
40
+ }
41
+
42
+ return a.daysAgo(Math.round(diff / 86_400_000))
43
+ }
44
+
45
+ export function AboutSettings() {
46
+ const { t } = useI18n()
47
+ const a = t.settings.about
48
+ const version = useStore($desktopVersion)
49
+ const status = useStore($updateStatus)
50
+ const apply = useStore($updateApply)
51
+ const checking = useStore($updateChecking)
52
+ const [justChecked, setJustChecked] = useState(false)
53
+
54
+ // The version atom is loaded once at app boot, which makes About show a
55
+ // stale number after a self-update (the running binary is current, the
56
+ // displayed string is not). Re-read on mount so opening About always
57
+ // reflects the running build.
58
+ useEffect(() => {
59
+ void refreshDesktopVersion()
60
+ }, [])
61
+
62
+ const behind = status?.behind ?? 0
63
+ const supported = status?.supported !== false
64
+ const applying = apply.applying || apply.stage === 'restart'
65
+
66
+ const handleCheck = async () => {
67
+ setJustChecked(false)
68
+ const next = await checkUpdates()
69
+ setJustChecked(Boolean(next))
70
+ }
71
+
72
+ let statusLine: string
73
+ let statusTone: 'idle' | 'available' | 'error' = 'idle'
74
+
75
+ if (!supported) {
76
+ statusLine = status?.message ?? a.cantUpdate
77
+ statusTone = 'error'
78
+ } else if (status?.error) {
79
+ statusLine = a.cantReach
80
+ statusTone = 'error'
81
+ } else if (applying) {
82
+ statusLine = a.installing
83
+ statusTone = 'available'
84
+ } else if (behind > 0) {
85
+ statusLine = a.updateReady(behind)
86
+ statusTone = 'available'
87
+ } else if (status) {
88
+ statusLine = a.onLatest
89
+ } else {
90
+ statusLine = a.tapCheck
91
+ }
92
+
93
+ return (
94
+ <SettingsContent>
95
+ <div className="flex flex-col items-center gap-3 pt-6 pb-2 text-center">
96
+ <BrandMark className="size-16" />
97
+ <div>
98
+ <h2 className="text-lg font-semibold tracking-tight">{a.heading}</h2>
99
+ <p className="mt-1 text-xs text-muted-foreground">
100
+ {version?.appVersion ? a.version(version.appVersion) : a.versionUnavailable}
101
+ </p>
102
+ </div>
103
+ </div>
104
+
105
+ <div className="mx-auto mt-4 w-full max-w-2xl">
106
+ <SectionHeading icon={RefreshCw} title={a.updates} />
107
+
108
+ <div
109
+ className={cn(
110
+ 'rounded-xl border px-4 py-3 text-sm',
111
+ statusTone === 'available' && 'border-primary/30 bg-primary/5 text-foreground',
112
+ statusTone === 'error' && 'border-destructive/35 bg-destructive/5 text-destructive',
113
+ statusTone === 'idle' && 'border-border/70 bg-muted/20 text-foreground'
114
+ )}
115
+ >
116
+ <div className="flex items-start gap-2">
117
+ {statusTone === 'available' ? (
118
+ <Sparkles className="mt-0.5 size-4 shrink-0 text-primary" />
119
+ ) : statusTone === 'error' ? null : (
120
+ <CheckCircle2 className="mt-0.5 size-4 shrink-0 text-emerald-600 dark:text-emerald-400" />
121
+ )}
122
+ <div className="min-w-0">
123
+ <p className="font-medium">{statusLine}</p>
124
+ <p className="mt-1 text-xs text-muted-foreground">
125
+ {a.lastChecked(relativeTime(status?.fetchedAt, a))}
126
+ {justChecked && !checking ? a.justNowSuffix : ''}
127
+ </p>
128
+ </div>
129
+ </div>
130
+
131
+ <div className="mt-3 flex flex-wrap items-center gap-4">
132
+ <Button
133
+ disabled={checking || applying || !supported}
134
+ onClick={() => void handleCheck()}
135
+ size="sm"
136
+ variant="textStrong"
137
+ >
138
+ {checking ? <Loader2 className="size-3 animate-spin" /> : <RefreshCw className="size-3" />}
139
+ {checking ? a.checking : a.checkNow}
140
+ </Button>
141
+
142
+ {behind > 0 && supported && !applying && (
143
+ <Button onClick={() => openUpdatesWindow()} size="sm">
144
+ {a.seeWhatsNew}
145
+ </Button>
146
+ )}
147
+
148
+ <Button asChild className="ml-auto" size="sm" variant="text">
149
+ <a
150
+ href={RELEASE_NOTES_URL}
151
+ onClick={event => {
152
+ event.preventDefault()
153
+ void window.NASTECHDesktop?.openExternal?.(RELEASE_NOTES_URL)
154
+ }}
155
+ rel="noreferrer"
156
+ target="_blank"
157
+ >
158
+ <ExternalLink className="size-3" />
159
+ {a.releaseNotes}
160
+ </a>
161
+ </Button>
162
+ </div>
163
+ </div>
164
+
165
+ <ListRow
166
+ description={a.automaticUpdatesDesc}
167
+ hint={a.branchCommit(status?.branch ?? 'unknown', status?.currentSha?.slice(0, 7) ?? 'unknown')}
168
+ title={a.automaticUpdates}
169
+ />
170
+ </div>
171
+ </SettingsContent>
172
+ )
173
+ }