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,365 @@
1
+ import { atom, computed } from 'nanostores'
2
+
3
+ import { getProfiles, setApiRequestProfile } from '@/nastech'
4
+ import { queryClient } from '@/lib/query-client'
5
+ import {
6
+ arraysEqual,
7
+ persistBoolean,
8
+ persistStringArray,
9
+ persistStringRecord,
10
+ storedBoolean,
11
+ storedStringArray,
12
+ storedStringRecord
13
+ } from '@/lib/storage'
14
+ import { $gateway, ensureGatewayForProfile } from '@/store/gateway'
15
+ import type { ProfileInfo } from '@/types/nastech'
16
+
17
+ // Canonical key for a profile: trimmed, empty → "default". Used everywhere we
18
+ // compare a session's owning profile against the live gateway's profile.
19
+ export function normalizeProfileKey(name: string | null | undefined): string {
20
+ const value = (name ?? '').trim()
21
+
22
+ return value || 'default'
23
+ }
24
+
25
+ // The profile the running local backend is actually scoped to (mirrors
26
+ // /api/profiles/active `current`). "default" is the root ~/.NASTECH. This is the
27
+ // display source of truth for the statusbar pill; the desktop's *stored*
28
+ // preference (which may be unset) lives in the Electron main process.
29
+ export const $activeProfile = atom<string>('default')
30
+
31
+ // Cached profile list for the picker. Refreshed lazily; the dropdown also
32
+ // re-fetches on open so a profile created elsewhere shows up.
33
+ export const $profiles = atom<ProfileInfo[]>([])
34
+
35
+ export function setActiveProfile(name: string): void {
36
+ $activeProfile.set(name || 'default')
37
+ }
38
+
39
+ // ── Rail order ─────────────────────────────────────────────────────────────
40
+ // User-defined order for the named (non-default) profile squares in the rail.
41
+ // Names absent from the list fall back to alphabetical, appended at the tail —
42
+ // so a freshly created profile lands at the end until the user drags it.
43
+ const PROFILE_ORDER_STORAGE_KEY = 'NASTECH.desktop.profileOrder'
44
+
45
+ export const $profileOrder = atom<string[]>(storedStringArray(PROFILE_ORDER_STORAGE_KEY))
46
+
47
+ $profileOrder.subscribe(value => persistStringArray(PROFILE_ORDER_STORAGE_KEY, [...value]))
48
+
49
+ export function setProfileOrder(names: string[]): void {
50
+ if (!arraysEqual($profileOrder.get(), names)) {
51
+ $profileOrder.set(names)
52
+ }
53
+ }
54
+
55
+ // Sort items by the stored order; unordered names alphabetise at the tail.
56
+ export function sortByProfileOrder<T extends { name: string }>(items: T[], order: string[]): T[] {
57
+ const rank = new Map(order.map((name, index) => [name, index]))
58
+
59
+ return [...items].sort((a, b) => {
60
+ const ra = rank.get(a.name)
61
+ const rb = rank.get(b.name)
62
+
63
+ if (ra != null && rb != null) {
64
+ return ra - rb
65
+ }
66
+
67
+ return ra != null ? -1 : rb != null ? 1 : a.name.localeCompare(b.name)
68
+ })
69
+ }
70
+
71
+ // ── Rail colors ────────────────────────────────────────────────────────────
72
+ // Optional per-profile color override (long-press a rail square to pick). Absent
73
+ // names fall back to the deterministic hue from profileColor(); a local-only
74
+ // cosmetic preference, so single-profile users never touch it.
75
+ const PROFILE_COLORS_STORAGE_KEY = 'NASTECH.desktop.profileColors'
76
+
77
+ export const $profileColors = atom<Record<string, string>>(storedStringRecord(PROFILE_COLORS_STORAGE_KEY))
78
+
79
+ $profileColors.subscribe(value => persistStringRecord(PROFILE_COLORS_STORAGE_KEY, value))
80
+
81
+ // Set (or, with null, clear) a profile's color override.
82
+ export function setProfileColor(name: string, color: null | string): void {
83
+ const key = normalizeProfileKey(name)
84
+ const next = { ...$profileColors.get() }
85
+
86
+ if (color) {
87
+ next[key] = color
88
+ } else {
89
+ delete next[key]
90
+ }
91
+
92
+ $profileColors.set(next)
93
+ }
94
+
95
+ interface ActiveProfileResponse {
96
+ active: string
97
+ current: string
98
+ }
99
+
100
+ // Pull the running backend's current profile + the available profile list.
101
+ // Best-effort: failures (backend not up yet) leave the prior values intact.
102
+ export async function refreshActiveProfile(): Promise<void> {
103
+ try {
104
+ const res = await window.NASTECHDesktop.api<ActiveProfileResponse>({ path: '/api/profiles/active' })
105
+
106
+ setActiveProfile(res.current || 'default')
107
+ } catch {
108
+ // Backend may not be ready; keep the last known value.
109
+ }
110
+
111
+ try {
112
+ const { profiles } = await getProfiles()
113
+ $profiles.set(profiles)
114
+ } catch {
115
+ // Leave the cached list in place.
116
+ }
117
+ }
118
+
119
+ // Persist the choice and relaunch the backend under the new NASTECH_HOME. The
120
+ // main process reloads the window, so this normally never returns to the caller
121
+ // (the renderer is torn down). We optimistically reflect the selection first so
122
+ // the pill updates instantly if the reload is delayed.
123
+ export async function switchProfile(name: string): Promise<void> {
124
+ if (!name || name === $activeProfile.get()) {
125
+ return
126
+ }
127
+
128
+ setActiveProfile(name)
129
+ await window.NASTECHDesktop.profile.set(name)
130
+ }
131
+
132
+ // ── Swap-minimal gateway routing ──────────────────────────────────────────
133
+ // One live gateway at a time. When the user opens/sends a session whose profile
134
+ // differs from the gateway's current profile, we lazily reconnect the single
135
+ // gateway to that profile's backend (spawned on demand by the Electron pool).
136
+ // A single-profile user never triggers a swap, so their path is unchanged.
137
+
138
+ // The profile the live gateway WebSocket is currently connected to. Initialized
139
+ // to the primary (window) backend's profile on boot.
140
+ export const $activeGatewayProfile = atom<string>('default')
141
+
142
+ // Profile for the NEXT new chat (chosen via the new-chat picker). null = primary
143
+ // / default, so single-profile users are unaffected.
144
+ export const $newChatProfile = atom<string | null>(null)
145
+
146
+ // Bumped whenever the profile context actually changes (switch or create). The
147
+ // chat controller subscribes and drops to a fresh new-session draft, so the
148
+ // session you were in doesn't stay sticky across a profile switch.
149
+ export const $freshSessionRequest = atom(0)
150
+
151
+ function requestFreshSession(): void {
152
+ $freshSessionRequest.set($freshSessionRequest.get() + 1)
153
+ }
154
+
155
+ // Route profile-scoped REST settings (config/env/skills/tools/model/…) to the
156
+ // profile the live gateway is currently on, and drop cached settings from the
157
+ // previous profile so pages refetch against the right backend. Fires once
158
+ // immediately (no real change → no invalidation), so single-profile users just
159
+ // get "default" (→ the primary backend) with no extra fetches.
160
+ let _lastRoutedProfile: string | null = null
161
+
162
+ $activeGatewayProfile.subscribe(value => {
163
+ const key = normalizeProfileKey(value)
164
+ setApiRequestProfile(key)
165
+
166
+ if (_lastRoutedProfile !== null && _lastRoutedProfile !== key) {
167
+ // Profile-scoped settings + the unified session list are now stale.
168
+ void queryClient.invalidateQueries()
169
+ }
170
+
171
+ _lastRoutedProfile = key
172
+ })
173
+
174
+ // Target profile while a gateway swap is mid-flight (spawning/reconnecting that
175
+ // profile's backend), else null. Drives the chat's "waking up <profile>" loader
176
+ // so a lazy spawn doesn't read as a hang. Single-profile users never swap.
177
+ export const $gatewaySwapTarget = atom<string | null>(null)
178
+
179
+ let gatewaySwitch: Promise<void> | null = null
180
+
181
+ // Make `profile`'s backend the active gateway, lazily opening its socket if it
182
+ // isn't live yet. Unlike the old single-socket swap, background profiles keep
183
+ // their sockets — so their sessions keep streaming concurrently. A null/empty
184
+ // target means "no explicit profile" → keep the current gateway (a plain new
185
+ // chat stays put; single-profile users never leave the primary).
186
+ export async function ensureGatewayProfile(profile: string | null | undefined): Promise<void> {
187
+ if (profile == null || !String(profile).trim()) {
188
+ // "No explicit profile" = use the current gateway. But if an explicit swap
189
+ // (e.g. the user just picked a profile in the switcher) is still in flight,
190
+ // let it settle first so a new chat doesn't race session.create against a
191
+ // half-open socket and land on the wrong backend.
192
+ if (gatewaySwitch) {
193
+ await gatewaySwitch.catch(() => undefined)
194
+ }
195
+
196
+ return
197
+ }
198
+
199
+ const target = normalizeProfileKey(profile)
200
+
201
+ if (normalizeProfileKey($activeGatewayProfile.get()) === target && $gateway.get()) {
202
+ return
203
+ }
204
+
205
+ // Serialize concurrent activations so two rapid session switches don't race
206
+ // the active pointer.
207
+ if (gatewaySwitch) {
208
+ await gatewaySwitch.catch(() => undefined)
209
+
210
+ if (normalizeProfileKey($activeGatewayProfile.get()) === target && $gateway.get()) {
211
+ return
212
+ }
213
+ }
214
+
215
+ $gatewaySwapTarget.set(target)
216
+ gatewaySwitch = (async () => {
217
+ // ensureGatewayForProfile opens (or reuses) the target's socket and points
218
+ // the active gateway at it — without closing the profile you came from.
219
+ await ensureGatewayForProfile(target)
220
+ $activeGatewayProfile.set(target)
221
+ })()
222
+
223
+ try {
224
+ await gatewaySwitch
225
+ } finally {
226
+ gatewaySwitch = null
227
+ $gatewaySwapTarget.set(null)
228
+ }
229
+ }
230
+
231
+ // ── Sidebar profile scope (the "workspace switcher" model) ─────────────────
232
+ // Mirrors how Slack/VS Code/Linear do multi-context: you're "in" one profile at
233
+ // a time and the sidebar shows only that profile's sessions (clean rows, no
234
+ // per-row tags). The lone exception is an explicit "All profiles" mode that
235
+ // fans every profile's sessions into one grouped, browsable list.
236
+
237
+ export const ALL_PROFILES = '__all__'
238
+
239
+ const SHOW_ALL_PROFILES_STORAGE_KEY = 'NASTECH.desktop.showAllProfiles'
240
+
241
+ // Opt-in unified view. When false, scope follows the live gateway profile, so
242
+ // single-profile users (who never see the switcher) are completely unaffected.
243
+ export const $showAllProfiles = atom<boolean>(storedBoolean(SHOW_ALL_PROFILES_STORAGE_KEY, false))
244
+
245
+ $showAllProfiles.subscribe(value => persistBoolean(SHOW_ALL_PROFILES_STORAGE_KEY, value))
246
+
247
+ // The profile context the sidebar is currently showing: a concrete profile key,
248
+ // or ALL_PROFILES for the unified grouped view. Concrete scope is tied to the
249
+ // gateway so opening/selecting a profile (which swaps the gateway) moves the
250
+ // whole sidebar with it — a real context switch, not a separate filter to keep
251
+ // in sync.
252
+ export const $profileScope = computed([$showAllProfiles, $activeGatewayProfile], (showAll, gateway) =>
253
+ showAll ? ALL_PROFILES : normalizeProfileKey(gateway)
254
+ )
255
+
256
+ // Switch the active context to `name`: leave "All profiles" mode, point new
257
+ // chats at it, and swap the single live gateway onto its backend (which moves
258
+ // $activeGatewayProfile → name, so $profileScope follows).
259
+ export function selectProfile(name: string): void {
260
+ const target = normalizeProfileKey(name)
261
+ // Switching profiles (or coming back from the all-profiles browse view) starts
262
+ // fresh; re-tapping the profile you're already in leaves your session be.
263
+ const switching = $showAllProfiles.get() || target !== normalizeProfileKey($activeGatewayProfile.get())
264
+ $showAllProfiles.set(false)
265
+ $newChatProfile.set(target)
266
+
267
+ if (switching) {
268
+ requestFreshSession()
269
+ }
270
+
271
+ void ensureGatewayProfile(target)
272
+ }
273
+
274
+ // Start a fresh session in `name` WITHOUT collapsing the "All profiles" browse
275
+ // view. Unlike selectProfile, it leaves $showAllProfiles untouched, so the
276
+ // unified sidebar stays put — used by the per-profile "+" in the all-profiles
277
+ // session list, where switching scope would throw away the browse state the user
278
+ // is in. Points new chats at the profile and opens its backend so the next
279
+ // message lands in the right place.
280
+ export function newSessionInProfile(name: string): void {
281
+ const target = normalizeProfileKey(name)
282
+ $newChatProfile.set(target)
283
+ requestFreshSession()
284
+ void ensureGatewayProfile(target)
285
+ }
286
+
287
+ export function setShowAllProfiles(value: boolean): void {
288
+ $showAllProfiles.set(value)
289
+ }
290
+
291
+ export function toggleShowAllProfiles(): void {
292
+ $showAllProfiles.set(!$showAllProfiles.get())
293
+ }
294
+
295
+ // ── Hotkey-driven profile switching ────────────────────────────────────────
296
+ // Positional + relative navigation for the rail, used by the keybind runtime.
297
+ // The ordered list is [default, ...named-in-rail-order]; switching is a no-op
298
+ // when the slot is empty so unused ⌘N keys stay harmless.
299
+
300
+ function orderedProfileKeys(): string[] {
301
+ const profiles = $profiles.get()
302
+
303
+ const named = sortByProfileOrder(
304
+ profiles.filter(profile => !profile.is_default),
305
+ $profileOrder.get()
306
+ ).map(profile => normalizeProfileKey(profile.name))
307
+
308
+ const hasDefault = profiles.some(profile => profile.is_default)
309
+
310
+ return hasDefault ? ['default', ...named] : named
311
+ }
312
+
313
+ // Switch to the default (root ~/.NASTECH) profile — bound to ⌘1.
314
+ export function switchToDefaultProfile(): void {
315
+ const def = $profiles.get().find(profile => profile.is_default)
316
+
317
+ selectProfile(def ? def.name : 'default')
318
+ }
319
+
320
+ // Switch to the Nth named (non-default) profile in rail order (1-based).
321
+ export function switchProfileToSlot(slot: number): void {
322
+ const named = sortByProfileOrder(
323
+ $profiles.get().filter(profile => !profile.is_default),
324
+ $profileOrder.get()
325
+ )
326
+
327
+ const target = named[slot - 1]
328
+
329
+ if (target) {
330
+ selectProfile(target.name)
331
+ }
332
+ }
333
+
334
+ // Step to the next/previous profile in the rail, wrapping around.
335
+ export function cycleProfile(direction: 1 | -1): void {
336
+ const keys = orderedProfileKeys()
337
+
338
+ if (keys.length < 2) {
339
+ return
340
+ }
341
+
342
+ const current = $showAllProfiles.get() ? -1 : keys.indexOf(normalizeProfileKey($activeGatewayProfile.get()))
343
+ const start = current < 0 ? (direction === 1 ? -1 : 0) : current
344
+ const next = (start + direction + keys.length) % keys.length
345
+
346
+ selectProfile(keys[next])
347
+ }
348
+
349
+ // Bumped to ask the rail to open its "create profile" dialog (the dialog state
350
+ // is local to the rail component; this lets a global hotkey trigger it).
351
+ export const $profileCreateRequest = atom(0)
352
+
353
+ export function requestProfileCreate(): void {
354
+ $profileCreateRequest.set($profileCreateRequest.get() + 1)
355
+ }
356
+
357
+ // Keepalive ping for the active pool backend so the main-process idle reaper
358
+ // (which can't see the direct renderer↔backend WS) spares it. No-op for the
359
+ // primary/default backend, which is never pooled.
360
+ export function touchActiveGatewayBackend(): void {
361
+ // Always ping: the main process no-ops for non-pool (primary) backends, so we
362
+ // don't need to know which profile is primary from here.
363
+ const target = normalizeProfileKey($activeGatewayProfile.get())
364
+ void window.NASTECHDesktop?.touchBackend?.(target).catch(() => undefined)
365
+ }
@@ -0,0 +1,121 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
2
+
3
+ import {
4
+ $approvalRequest,
5
+ $secretRequest,
6
+ $sudoRequest,
7
+ clearAllPrompts,
8
+ clearApprovalRequest,
9
+ clearSecretRequest,
10
+ clearSudoRequest,
11
+ setApprovalRequest,
12
+ setSecretRequest,
13
+ setSudoRequest
14
+ } from './prompts'
15
+ import { $activeSessionId } from './session'
16
+
17
+ // Prompts are parked per-session; the exported $*Request views are scoped to the
18
+ // active session, so each test focuses the session it's asserting on.
19
+ beforeEach(() => {
20
+ $activeSessionId.set('s1')
21
+ })
22
+
23
+ afterEach(() => {
24
+ clearAllPrompts()
25
+ $activeSessionId.set(null)
26
+ })
27
+
28
+ describe('approval prompt store', () => {
29
+ it('holds the active session-keyed approval request', () => {
30
+ setApprovalRequest({ command: 'rm -rf /tmp/x', description: 'recursive delete', sessionId: 's1' })
31
+
32
+ expect($approvalRequest.get()).toEqual({
33
+ command: 'rm -rf /tmp/x',
34
+ description: 'recursive delete',
35
+ sessionId: 's1'
36
+ })
37
+ })
38
+
39
+ it('parks a background session prompt out of the active view', () => {
40
+ setApprovalRequest({ command: 'x', description: 'd', sessionId: 's2' })
41
+
42
+ // Not visible while s1 is focused …
43
+ expect($approvalRequest.get()).toBeNull()
44
+
45
+ // … but surfaces once the user switches to the session that raised it.
46
+ $activeSessionId.set('s2')
47
+ expect($approvalRequest.get()?.sessionId).toBe('s2')
48
+ })
49
+
50
+ it('clears the active session prompt', () => {
51
+ setApprovalRequest({ command: 'x', description: 'd', sessionId: 's1' })
52
+ clearApprovalRequest('s1')
53
+
54
+ expect($approvalRequest.get()).toBeNull()
55
+ })
56
+ })
57
+
58
+ describe('sudo prompt store', () => {
59
+ it('clears only when the request id matches the in-flight prompt', () => {
60
+ setSudoRequest({ requestId: 'abc', sessionId: 's1' })
61
+
62
+ // A stale clear for a different request must NOT drop the live prompt —
63
+ // otherwise a late response to a prior sudo ask would dismiss the current
64
+ // one and leave the agent blocked.
65
+ clearSudoRequest('s1', 'stale')
66
+ expect($sudoRequest.get()).toEqual({ requestId: 'abc', sessionId: 's1' })
67
+
68
+ clearSudoRequest('s1', 'abc')
69
+ expect($sudoRequest.get()).toBeNull()
70
+ })
71
+
72
+ it('clears unconditionally when no request id is given', () => {
73
+ setSudoRequest({ requestId: 'abc', sessionId: 's1' })
74
+ clearSudoRequest('s1')
75
+
76
+ expect($sudoRequest.get()).toBeNull()
77
+ })
78
+ })
79
+
80
+ describe('secret prompt store', () => {
81
+ it('carries env var and prompt, and clears on id match', () => {
82
+ setSecretRequest({ requestId: 'r1', envVar: 'OPENAI_API_KEY', prompt: 'Paste your key', sessionId: 's1' })
83
+
84
+ expect($secretRequest.get()).toEqual({
85
+ requestId: 'r1',
86
+ envVar: 'OPENAI_API_KEY',
87
+ prompt: 'Paste your key',
88
+ sessionId: 's1'
89
+ })
90
+
91
+ clearSecretRequest('s1', 'mismatch')
92
+ expect($secretRequest.get()).not.toBeNull()
93
+
94
+ clearSecretRequest('s1', 'r1')
95
+ expect($secretRequest.get()).toBeNull()
96
+ })
97
+ })
98
+
99
+ describe('clearAllPrompts', () => {
100
+ it('drops every kind for one session at once (turn end / interrupt)', () => {
101
+ setApprovalRequest({ command: 'x', description: 'd', sessionId: 's1' })
102
+ setSudoRequest({ requestId: 'abc', sessionId: 's1' })
103
+ setSecretRequest({ requestId: 'r1', envVar: 'E', prompt: 'p', sessionId: 's1' })
104
+
105
+ clearAllPrompts('s1')
106
+
107
+ expect($approvalRequest.get()).toBeNull()
108
+ expect($sudoRequest.get()).toBeNull()
109
+ expect($secretRequest.get()).toBeNull()
110
+ })
111
+
112
+ it('leaves other sessions parked prompts intact', () => {
113
+ setApprovalRequest({ command: 'x', description: 'd', sessionId: 's1' })
114
+ setApprovalRequest({ command: 'y', description: 'e', sessionId: 's2' })
115
+
116
+ clearAllPrompts('s1')
117
+
118
+ $activeSessionId.set('s2')
119
+ expect($approvalRequest.get()?.command).toBe('y')
120
+ })
121
+ })
@@ -0,0 +1,115 @@
1
+ import { atom, computed, type ReadableAtom } from 'nanostores'
2
+
3
+ import { $activeSessionId } from './session'
4
+
5
+ // Blocking interactive prompts the gateway raises mid-turn. Each maps to a
6
+ // `*.request` event the Python side emits while it blocks the agent thread
7
+ // waiting for a `*.respond` RPC. Without a renderer for these, the agent
8
+ // silently stalls until its timeout (default 5 min) and the tool is BLOCKED.
9
+ //
10
+ // Like clarify, every prompt is parked under the runtime session id that raised
11
+ // it (not one shared slot), so a *background* session running concurrently can
12
+ // raise an approval/sudo/secret prompt and have it wait — surfaced via the
13
+ // sidebar "needs input" badge — until the user switches to that chat. The
14
+ // exported $*Request view is scoped to the active session, so a background
15
+ // prompt never hijacks the foreground.
16
+
17
+ const keyFor = (sessionId: string | null | undefined): string => sessionId ?? ''
18
+
19
+ interface KeyedPrompt {
20
+ sessionId: string | null
21
+ }
22
+
23
+ interface PromptStore<T extends KeyedPrompt> {
24
+ $active: ReadableAtom<null | T>
25
+ clear: (sessionId?: string | null, requestId?: string) => void
26
+ reset: () => void
27
+ set: (request: T) => void
28
+ }
29
+
30
+ // One per-session prompt kind: a map keyed by session, plus an active-session
31
+ // view for the overlays. `clear` drops one session's entry (a request-id
32
+ // mismatch is a no-op so a stale resolve can't wipe a newer prompt); with no
33
+ // session hint it drops every entry, optionally filtered by request id.
34
+ function keyedPromptStore<T extends KeyedPrompt>(): PromptStore<T> {
35
+ const $all = atom<Record<string, T>>({})
36
+ const idOf = (value: T): string | undefined => (value as { requestId?: string }).requestId
37
+
38
+ return {
39
+ $active: computed([$all, $activeSessionId], (all, activeId) => all[keyFor(activeId)] ?? null),
40
+ reset: () => $all.set({}),
41
+ set: request => $all.set({ ...$all.get(), [keyFor(request.sessionId)]: request }),
42
+ clear(sessionId, requestId) {
43
+ const all = $all.get()
44
+
45
+ if (sessionId !== undefined) {
46
+ const key = keyFor(sessionId)
47
+ const current = all[key]
48
+
49
+ if (current && !(requestId && idOf(current) !== requestId)) {
50
+ const next = { ...all }
51
+ delete next[key]
52
+ $all.set(next)
53
+ }
54
+
55
+ return
56
+ }
57
+
58
+ const next = Object.fromEntries(Object.entries(all).filter(([, v]) => requestId && idOf(v) !== requestId))
59
+
60
+ if (Object.keys(next).length !== Object.keys(all).length) {
61
+ $all.set(next as Record<string, T>)
62
+ }
63
+ }
64
+ }
65
+ }
66
+
67
+ // Approval is session-keyed on the backend (one in-flight approval per session,
68
+ // resolved via approval.respond {choice, session_id}). It carries no request_id,
69
+ // unlike sudo/secret which are _block()-style request/response.
70
+ export interface ApprovalRequest extends KeyedPrompt {
71
+ command: string
72
+ description: string
73
+ }
74
+
75
+ export interface SudoRequest extends KeyedPrompt {
76
+ requestId: string
77
+ }
78
+
79
+ export interface SecretRequest extends KeyedPrompt {
80
+ envVar: string
81
+ prompt: string
82
+ requestId: string
83
+ }
84
+
85
+ const approval = keyedPromptStore<ApprovalRequest>()
86
+ const sudo = keyedPromptStore<SudoRequest>()
87
+ const secret = keyedPromptStore<SecretRequest>()
88
+
89
+ export const $approvalRequest = approval.$active
90
+ export const setApprovalRequest = approval.set
91
+ export const clearApprovalRequest = approval.clear
92
+
93
+ export const $sudoRequest = sudo.$active
94
+ export const setSudoRequest = sudo.set
95
+ export const clearSudoRequest = sudo.clear
96
+
97
+ export const $secretRequest = secret.$active
98
+ export const setSecretRequest = secret.set
99
+ export const clearSecretRequest = secret.clear
100
+
101
+ // Drop in-flight prompts for `sessionId` (a turn ended) across all three kinds —
102
+ // or every parked prompt when no session is given (global reset / tests).
103
+ export function clearAllPrompts(sessionId?: string | null): void {
104
+ if (sessionId === undefined) {
105
+ approval.reset()
106
+ sudo.reset()
107
+ secret.reset()
108
+
109
+ return
110
+ }
111
+
112
+ approval.clear(sessionId)
113
+ sudo.clear(sessionId)
114
+ secret.clear(sessionId)
115
+ }