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,290 @@
1
+ import type { ConnectionState, GatewayEvent } from '@NASTECH/shared'
2
+ import { atom } from 'nanostores'
3
+
4
+ import { NasTechGateway } from '@/nastech'
5
+ import { resolveGatewayWsUrl } from '@/lib/gateway-ws-url'
6
+ import { setGatewayState } from '@/store/session'
7
+
8
+ // ── Multi-profile gateway routing ──────────────────────────────────────────
9
+ // Concurrent sessions across profiles need concurrent sockets: the renderer's
10
+ // event handler is already session-keyed, so the only thing stopping two
11
+ // profiles streaming at once was the single swapping socket. We keep that one
12
+ // socket as the PRIMARY (window) backend — owned by use-gateway-boot, with all
13
+ // its boot-progress / sleep-wake machinery — and add one persistent SECONDARY
14
+ // socket per *other* profile that has live work. Every socket feeds the same
15
+ // handleGatewayEvent, so background sessions keep painting. Single-profile users
16
+ // only ever have the primary, so their path is byte-for-byte unchanged.
17
+
18
+ const normKey = (profile: string | null | undefined): string => (profile ?? '').trim() || 'default'
19
+
20
+ // Read connection state through a call so TS control-flow analysis doesn't
21
+ // narrow the getter to a constant across guards (it genuinely changes).
22
+ const isOpen = (gateway: NasTechGateway | null): boolean => gateway?.connectionState === 'open'
23
+
24
+ // The active gateway instance, exposed for inline message-stream components
25
+ // (e.g. inline ClarifyTool, model overlays) that call gateway methods without
26
+ // the instance threaded down through props.
27
+ export const $gateway = atom<NasTechGateway | null>(null)
28
+
29
+ interface RegistryConfig {
30
+ onEvent: (event: GatewayEvent) => void
31
+ }
32
+
33
+ let config: RegistryConfig | null = null
34
+
35
+ export function configureGatewayRegistry(cfg: RegistryConfig): void {
36
+ config = cfg
37
+ }
38
+
39
+ // ── Primary (window) backend ───────────────────────────────────────────────
40
+ let primaryGateway: NasTechGateway | null = null
41
+ let primaryProfile = 'default'
42
+
43
+ export function setPrimaryGateway(gateway: NasTechGateway | null, profile = 'default'): void {
44
+ primaryGateway = gateway
45
+ primaryProfile = normKey(profile)
46
+ }
47
+
48
+ // ── Secondary (pool) backends ──────────────────────────────────────────────
49
+ interface Secondary {
50
+ profile: string
51
+ gateway: NasTechGateway
52
+ offEvent: () => void
53
+ offState: () => void
54
+ reconnectTimer: ReturnType<typeof setTimeout> | null
55
+ reconnectAttempt: number
56
+ reconnecting: boolean
57
+ // While true the entry auto-reconnects on drop; pruning flips it off so a
58
+ // deliberate close doesn't trigger the backoff loop.
59
+ wantOpen: boolean
60
+ }
61
+
62
+ const secondaries = new Map<string, Secondary>()
63
+
64
+ let activeKey = 'default'
65
+
66
+ export function isActivePrimary(): boolean {
67
+ return activeKey === primaryProfile
68
+ }
69
+
70
+ export function activeGateway(): NasTechGateway | null {
71
+ if (activeKey === primaryProfile) {
72
+ return primaryGateway
73
+ }
74
+
75
+ return secondaries.get(activeKey)?.gateway ?? primaryGateway
76
+ }
77
+
78
+ // Mirror a backend's connection state into the global composer state, but only
79
+ // when that backend is the one the user is currently looking at. Lets the
80
+ // composer reflect the active profile's socket without a background reconnect
81
+ // flipping the foreground enabled/disabled state.
82
+ function reportGatewayState(profile: string, state: ConnectionState): void {
83
+ if (normKey(profile) === activeKey) {
84
+ setGatewayState(state)
85
+ }
86
+ }
87
+
88
+ export function reportPrimaryGatewayState(state: ConnectionState): void {
89
+ reportGatewayState(primaryProfile, state)
90
+ }
91
+
92
+ function setActive(profile: string): void {
93
+ activeKey = normKey(profile)
94
+ const gateway = activeGateway()
95
+ $gateway.set(gateway)
96
+ setGatewayState(gateway?.connectionState ?? 'closed')
97
+ }
98
+
99
+ function clearTimer(entry: Secondary): void {
100
+ if (entry.reconnectTimer !== null) {
101
+ clearTimeout(entry.reconnectTimer)
102
+ entry.reconnectTimer = null
103
+ }
104
+ }
105
+
106
+ async function openSecondary(entry: Secondary): Promise<void> {
107
+ const desktop = window.NASTECHDesktop
108
+
109
+ if (!desktop) {
110
+ return
111
+ }
112
+
113
+ const conn = await desktop.getConnection(entry.profile)
114
+ const wsUrl = await resolveGatewayWsUrl(desktop, conn)
115
+ await entry.gateway.connect(wsUrl)
116
+ void desktop.touchBackend?.(entry.profile).catch(() => undefined)
117
+ }
118
+
119
+ function scheduleReconnect(entry: Secondary): void {
120
+ if (entry.reconnecting || entry.reconnectTimer !== null || !entry.wantOpen) {
121
+ return
122
+ }
123
+
124
+ // 1s, 2s, 4s … capped at 15s — same backoff shape as the primary.
125
+ const delay = Math.min(15_000, 1_000 * 2 ** Math.min(entry.reconnectAttempt, 4))
126
+ entry.reconnectAttempt += 1
127
+ entry.reconnectTimer = setTimeout(() => {
128
+ entry.reconnectTimer = null
129
+ void reconnectSecondary(entry)
130
+ }, delay)
131
+ }
132
+
133
+ async function reconnectSecondary(entry: Secondary): Promise<void> {
134
+ if (entry.reconnecting || !entry.wantOpen || isOpen(entry.gateway)) {
135
+ return
136
+ }
137
+
138
+ entry.reconnecting = true
139
+
140
+ try {
141
+ await openSecondary(entry)
142
+ entry.reconnectAttempt = 0
143
+ } catch {
144
+ // Transport failure → fall through to the backoff below.
145
+ } finally {
146
+ entry.reconnecting = false
147
+
148
+ if (entry.wantOpen && !isOpen(entry.gateway)) {
149
+ scheduleReconnect(entry)
150
+ }
151
+ }
152
+ }
153
+
154
+ function createSecondary(profile: string): Secondary {
155
+ const gateway = new NasTechGateway()
156
+
157
+ const entry: Secondary = {
158
+ profile,
159
+ gateway,
160
+ offEvent: () => {},
161
+ offState: () => {},
162
+ reconnectTimer: null,
163
+ reconnectAttempt: 0,
164
+ reconnecting: false,
165
+ wantOpen: true
166
+ }
167
+
168
+ entry.offEvent = gateway.onEvent(event => config?.onEvent(event))
169
+ entry.offState = gateway.onState(state => {
170
+ reportGatewayState(profile, state)
171
+
172
+ if (state === 'open') {
173
+ entry.reconnectAttempt = 0
174
+ clearTimer(entry)
175
+ } else if ((state === 'closed' || state === 'error') && entry.wantOpen) {
176
+ scheduleReconnect(entry)
177
+ }
178
+ })
179
+
180
+ secondaries.set(profile, entry)
181
+
182
+ return entry
183
+ }
184
+
185
+ // Make `profile` the active gateway, lazily opening its socket if needed. The
186
+ // primary is a no-op fast path. Background sockets are never closed here.
187
+ export async function ensureGatewayForProfile(profile: string): Promise<void> {
188
+ const key = normKey(profile)
189
+
190
+ if (key === primaryProfile) {
191
+ setActive(key)
192
+
193
+ return
194
+ }
195
+
196
+ let entry = secondaries.get(key)
197
+
198
+ if (!entry) {
199
+ entry = createSecondary(key)
200
+ }
201
+
202
+ entry.wantOpen = true
203
+
204
+ if (!isOpen(entry.gateway)) {
205
+ clearTimer(entry)
206
+ entry.reconnectAttempt = 0
207
+
208
+ try {
209
+ await openSecondary(entry)
210
+ } catch {
211
+ scheduleReconnect(entry)
212
+ }
213
+ }
214
+
215
+ setActive(key)
216
+ }
217
+
218
+ // Reconnect the active gateway after a transient request failure. Primary
219
+ // reconnects are owned by use-gateway-boot, so we only drive secondaries here.
220
+ export async function ensureActiveGatewayOpen(): Promise<NasTechGateway | null> {
221
+ if (activeKey === primaryProfile) {
222
+ return primaryGateway
223
+ }
224
+
225
+ const entry = secondaries.get(activeKey)
226
+
227
+ if (!entry) {
228
+ return null
229
+ }
230
+
231
+ if (!isOpen(entry.gateway)) {
232
+ await reconnectSecondary(entry)
233
+ }
234
+
235
+ return isOpen(entry.gateway) ? entry.gateway : null
236
+ }
237
+
238
+ // Wake signal (sleep/network/visibility): nudge every live secondary back open.
239
+ export function reconnectSecondaryGateways(): void {
240
+ for (const entry of secondaries.values()) {
241
+ if (!entry.wantOpen || isOpen(entry.gateway)) {
242
+ continue
243
+ }
244
+
245
+ entry.reconnectAttempt = 0
246
+ clearTimer(entry)
247
+ void reconnectSecondary(entry)
248
+ }
249
+ }
250
+
251
+ // Keep the idle reaper from killing a backend we still need: ping every live
252
+ // secondary. The active one is pinged separately (touchActiveGatewayBackend).
253
+ export function touchSecondaryGateways(): void {
254
+ const desktop = window.NASTECHDesktop
255
+
256
+ for (const entry of secondaries.values()) {
257
+ if (entry.wantOpen) {
258
+ void desktop?.touchBackend?.(entry.profile).catch(() => undefined)
259
+ }
260
+ }
261
+ }
262
+
263
+ // Close + evict secondaries whose profile is neither active nor in `keep`
264
+ // (profiles with a running / needs-input session). Bounds cost to live work.
265
+ export function pruneSecondaryGateways(keep: Set<string>): void {
266
+ for (const [key, entry] of [...secondaries]) {
267
+ if (key === activeKey || keep.has(key)) {
268
+ continue
269
+ }
270
+
271
+ entry.wantOpen = false
272
+ clearTimer(entry)
273
+ entry.offEvent()
274
+ entry.offState()
275
+ entry.gateway.close()
276
+ secondaries.delete(key)
277
+ }
278
+ }
279
+
280
+ export function closeSecondaryGateways(): void {
281
+ for (const entry of secondaries.values()) {
282
+ entry.wantOpen = false
283
+ clearTimer(entry)
284
+ entry.offEvent()
285
+ entry.offState()
286
+ entry.gateway.close()
287
+ }
288
+
289
+ secondaries.clear()
290
+ }
@@ -0,0 +1,17 @@
1
+ import { atom } from 'nanostores'
2
+
3
+ import { persistBoolean, storedBoolean } from '@/lib/storage'
4
+
5
+ const HAPTICS_MUTED_STORAGE_KEY = 'NASTECH.desktop.hapticsMuted'
6
+
7
+ export const $hapticsMuted = atom(storedBoolean(HAPTICS_MUTED_STORAGE_KEY, false))
8
+
9
+ $hapticsMuted.subscribe(muted => persistBoolean(HAPTICS_MUTED_STORAGE_KEY, muted))
10
+
11
+ export function setHapticsMuted(muted: boolean) {
12
+ $hapticsMuted.set(muted)
13
+ }
14
+
15
+ export function toggleHapticsMuted() {
16
+ $hapticsMuted.set(!$hapticsMuted.get())
17
+ }
@@ -0,0 +1,139 @@
1
+ import { atom, computed } from 'nanostores'
2
+
3
+ import {
4
+ defaultBindings,
5
+ KEYBIND_ACTION_IDS,
6
+ keybindAction,
7
+ type KeybindBindings
8
+ } from '@/lib/keybinds/actions'
9
+ import { arraysEqual, persistString, storedString } from '@/lib/storage'
10
+
11
+ const STORAGE_KEY = 'NASTECH.desktop.keybinds'
12
+
13
+ // Defaults overlaid with the user's stored overrides. Unknown / stale action ids
14
+ // are dropped; actions added in a later release pick up their shipped default.
15
+ function loadBindings(): KeybindBindings {
16
+ const base = defaultBindings()
17
+ const raw = storedString(STORAGE_KEY)
18
+
19
+ if (!raw) {
20
+ return base
21
+ }
22
+
23
+ try {
24
+ const parsed = JSON.parse(raw) as Record<string, unknown>
25
+
26
+ for (const id of KEYBIND_ACTION_IDS) {
27
+ const value = parsed[id]
28
+
29
+ if (Array.isArray(value)) {
30
+ base[id] = value.filter((combo): combo is string => typeof combo === 'string')
31
+ }
32
+ }
33
+ } catch {
34
+ // Corrupt storage falls back to defaults.
35
+ }
36
+
37
+ return base
38
+ }
39
+
40
+ // Persist only the actions whose combos differ from their shipped default, so
41
+ // changing a default never gets shadowed by a stored snapshot.
42
+ function persistBindings(bindings: KeybindBindings): void {
43
+ const defaults = defaultBindings()
44
+ const diff: KeybindBindings = {}
45
+
46
+ for (const id of KEYBIND_ACTION_IDS) {
47
+ const current = bindings[id] ?? []
48
+
49
+ if (!arraysEqual(current, defaults[id] ?? [])) {
50
+ diff[id] = current
51
+ }
52
+ }
53
+
54
+ persistString(STORAGE_KEY, JSON.stringify(diff))
55
+ }
56
+
57
+ export const $bindings = atom<KeybindBindings>(loadBindings())
58
+
59
+ $bindings.subscribe(persistBindings)
60
+
61
+ // Reverse lookup combo → actionId for dispatch. First action wins on conflict;
62
+ // the panel/edit overlay surface conflicts so users can resolve them.
63
+ export const $comboIndex = computed($bindings, bindings => {
64
+ const index = new Map<string, string>()
65
+
66
+ for (const id of KEYBIND_ACTION_IDS) {
67
+ for (const combo of bindings[id] ?? []) {
68
+ if (!index.has(combo)) {
69
+ index.set(combo, id)
70
+ }
71
+ }
72
+ }
73
+
74
+ return index
75
+ })
76
+
77
+ export function setBinding(actionId: string, combos: string[]): void {
78
+ if (!keybindAction(actionId)) {
79
+ return
80
+ }
81
+
82
+ $bindings.set({ ...$bindings.get(), [actionId]: [...combos] })
83
+ }
84
+
85
+ export function resetBinding(actionId: string): void {
86
+ const action = keybindAction(actionId)
87
+
88
+ if (!action) {
89
+ return
90
+ }
91
+
92
+ $bindings.set({ ...$bindings.get(), [actionId]: [...action.defaults] })
93
+ }
94
+
95
+ export function resetAllBindings(): void {
96
+ $bindings.set(defaultBindings())
97
+ }
98
+
99
+ // Other actions that already use `combo` (excluding `actionId` itself).
100
+ export function conflictsFor(actionId: string, combo: string): string[] {
101
+ const bindings = $bindings.get()
102
+
103
+ return KEYBIND_ACTION_IDS.filter(id => id !== actionId && (bindings[id] ?? []).includes(combo))
104
+ }
105
+
106
+ // ── Capture ─────────────────────────────────────────────────────────────────
107
+ // `$capture` is the action currently listening for its next keypress (a panel
108
+ // row armed for rebinding). Session-only — never persisted.
109
+
110
+ export const $capture = atom<string | null>(null)
111
+
112
+ export function beginCapture(actionId: string): void {
113
+ $capture.set(actionId)
114
+ }
115
+
116
+ export function endCapture(): void {
117
+ $capture.set(null)
118
+ }
119
+
120
+ // ── Panel ───────────────────────────────────────────────────────────────────
121
+
122
+ export const $keybindPanelOpen = atom(false)
123
+
124
+ export function openKeybindPanel(): void {
125
+ $keybindPanelOpen.set(true)
126
+ }
127
+
128
+ export function closeKeybindPanel(): void {
129
+ $keybindPanelOpen.set(false)
130
+ $capture.set(null)
131
+ }
132
+
133
+ export function toggleKeybindPanel(): void {
134
+ if ($keybindPanelOpen.get()) {
135
+ closeKeybindPanel()
136
+ } else {
137
+ openKeybindPanel()
138
+ }
139
+ }
@@ -0,0 +1,176 @@
1
+ import { atom, computed, type ReadableAtom } from 'nanostores'
2
+
3
+ import {
4
+ arraysEqual,
5
+ insertUniqueId,
6
+ persistBoolean,
7
+ persistStringArray,
8
+ storedBoolean,
9
+ storedStringArray
10
+ } from '@/lib/storage'
11
+
12
+ import { $paneStates, ensurePaneRegistered, setPaneOpen, setPaneWidthOverride, togglePane } from './panes'
13
+
14
+ export const SIDEBAR_DEFAULT_WIDTH = 237
15
+ export const SIDEBAR_MAX_WIDTH = 360
16
+ // Open at the same width as the sessions sidebar so the two rails match.
17
+ export const FILE_BROWSER_DEFAULT_WIDTH = `${SIDEBAR_DEFAULT_WIDTH}px`
18
+ export const FILE_BROWSER_MIN_WIDTH = '14rem'
19
+ export const FILE_BROWSER_MAX_WIDTH = '20rem'
20
+
21
+ export const SIDEBAR_SESSIONS_PAGE_SIZE = 50
22
+
23
+ const SIDEBAR_PINNED_STORAGE_KEY = 'NASTECH.desktop.pinnedSessions'
24
+ const SIDEBAR_AGENTS_GROUPED_STORAGE_KEY = 'NASTECH.desktop.agentsGroupedByWorkspace'
25
+ const SIDEBAR_CRON_OPEN_STORAGE_KEY = 'NASTECH.desktop.sidebarCronOpen'
26
+ const PANES_FLIPPED_STORAGE_KEY = 'NASTECH.desktop.panesFlipped'
27
+
28
+ export const CHAT_SIDEBAR_PANE_ID = 'chat-sidebar'
29
+ export const FILE_BROWSER_PANE_ID = 'file-browser'
30
+ export const RIGHT_RAIL_PREVIEW_TAB_ID = 'preview'
31
+
32
+ export type RightRailTabId = typeof RIGHT_RAIL_PREVIEW_TAB_ID | `file:${string}`
33
+
34
+ ensurePaneRegistered(CHAT_SIDEBAR_PANE_ID, { open: true })
35
+ ensurePaneRegistered(FILE_BROWSER_PANE_ID, { open: false })
36
+
37
+ export const $sidebarOpen: ReadableAtom<boolean> = computed(
38
+ $paneStates,
39
+ states => states[CHAT_SIDEBAR_PANE_ID]?.open ?? true
40
+ )
41
+
42
+ export const $fileBrowserOpen: ReadableAtom<boolean> = computed(
43
+ $paneStates,
44
+ states => states[FILE_BROWSER_PANE_ID]?.open ?? false
45
+ )
46
+
47
+ export const $rightRailActiveTabId = atom<RightRailTabId>(RIGHT_RAIL_PREVIEW_TAB_ID)
48
+
49
+ export const $sidebarWidth: ReadableAtom<number> = computed($paneStates, states => {
50
+ const override = states[CHAT_SIDEBAR_PANE_ID]?.widthOverride
51
+
52
+ return typeof override === 'number' ? override : SIDEBAR_DEFAULT_WIDTH
53
+ })
54
+
55
+ export const $pinnedSessionIds = atom(storedStringArray(SIDEBAR_PINNED_STORAGE_KEY))
56
+ export const $sidebarPinsOpen = atom(true)
57
+ export const $sidebarRecentsOpen = atom(true)
58
+ // Cron-job sessions live in their own section below recents, collapsed by
59
+ // default (it only renders at all when cron sessions exist) so the
60
+ // scheduler's `[IMPORTANT: …]` first-message previews don't spam recents.
61
+ export const $sidebarCronOpen = atom(storedBoolean(SIDEBAR_CRON_OPEN_STORAGE_KEY, false))
62
+ export const $sidebarAgentsGrouped = atom(storedBoolean(SIDEBAR_AGENTS_GROUPED_STORAGE_KEY, false))
63
+ // When true, the sessions sidebar moves to the right and the file browser +
64
+ // preview rail move to the left — a mirror of the default layout.
65
+ export const $panesFlipped = atom(storedBoolean(PANES_FLIPPED_STORAGE_KEY, false))
66
+ export const $isSidebarResizing = atom(false)
67
+ export const $sessionsLimit = atom(SIDEBAR_SESSIONS_PAGE_SIZE)
68
+
69
+ $pinnedSessionIds.subscribe(ids => persistStringArray(SIDEBAR_PINNED_STORAGE_KEY, [...ids]))
70
+ $sidebarCronOpen.subscribe(open => persistBoolean(SIDEBAR_CRON_OPEN_STORAGE_KEY, open))
71
+ $sidebarAgentsGrouped.subscribe(grouped => persistBoolean(SIDEBAR_AGENTS_GROUPED_STORAGE_KEY, grouped))
72
+ $panesFlipped.subscribe(flipped => persistBoolean(PANES_FLIPPED_STORAGE_KEY, flipped))
73
+
74
+ export function setSidebarWidth(width: number) {
75
+ const bounded = Math.min(SIDEBAR_MAX_WIDTH, Math.max(SIDEBAR_DEFAULT_WIDTH, width))
76
+ setPaneWidthOverride(CHAT_SIDEBAR_PANE_ID, bounded)
77
+ }
78
+
79
+ export function setSidebarOpen(open: boolean) {
80
+ setPaneOpen(CHAT_SIDEBAR_PANE_ID, open)
81
+ }
82
+
83
+ export function toggleSidebarOpen() {
84
+ togglePane(CHAT_SIDEBAR_PANE_ID)
85
+ }
86
+
87
+ export function toggleFileBrowserOpen() {
88
+ togglePane(FILE_BROWSER_PANE_ID)
89
+ }
90
+
91
+ export function setFileBrowserOpen(open: boolean) {
92
+ setPaneOpen(FILE_BROWSER_PANE_ID, open)
93
+ }
94
+
95
+ // Hotkey → focus the sessions search field. Opens the sidebar first, then lets
96
+ // the field (which only mounts when the sidebar is open) subscribe + focus.
97
+ export const SESSION_SEARCH_FOCUS_EVENT = 'NASTECH:focus-session-search'
98
+
99
+ export function requestSessionSearchFocus() {
100
+ setSidebarOpen(true)
101
+
102
+ if (typeof window !== 'undefined') {
103
+ window.setTimeout(() => window.dispatchEvent(new CustomEvent(SESSION_SEARCH_FOCUS_EVENT)), 0)
104
+ }
105
+ }
106
+
107
+ export function togglePanesFlipped() {
108
+ $panesFlipped.set(!$panesFlipped.get())
109
+ }
110
+
111
+ export function selectRightRailTab(id: RightRailTabId) {
112
+ $rightRailActiveTabId.set(id)
113
+ }
114
+
115
+ export function setSidebarPinsOpen(open: boolean) {
116
+ $sidebarPinsOpen.set(open)
117
+ }
118
+
119
+ export function setSidebarRecentsOpen(open: boolean) {
120
+ $sidebarRecentsOpen.set(open)
121
+ }
122
+
123
+ export function setSidebarCronOpen(open: boolean) {
124
+ $sidebarCronOpen.set(open)
125
+ }
126
+
127
+ export function setSidebarAgentsGrouped(grouped: boolean) {
128
+ $sidebarAgentsGrouped.set(grouped)
129
+ }
130
+
131
+ export function setSidebarResizing(resizing: boolean) {
132
+ $isSidebarResizing.set(resizing)
133
+ }
134
+
135
+ export function pinSession(sessionId: string, index?: number) {
136
+ const prev = $pinnedSessionIds.get()
137
+ const next = insertUniqueId(prev, sessionId, index ?? prev.filter(id => id !== sessionId).length)
138
+
139
+ if (!arraysEqual(prev, next)) {
140
+ $pinnedSessionIds.set(next)
141
+ }
142
+ }
143
+
144
+ export function unpinSession(sessionId: string) {
145
+ const prev = $pinnedSessionIds.get()
146
+ const next = prev.filter(id => id !== sessionId)
147
+
148
+ if (!arraysEqual(prev, next)) {
149
+ $pinnedSessionIds.set(next)
150
+ }
151
+ }
152
+
153
+ export function reorderPinnedSession(sessionId: string, targetIndex: number) {
154
+ const prev = $pinnedSessionIds.get()
155
+
156
+ if (!prev.includes(sessionId)) {
157
+ return
158
+ }
159
+
160
+ const next = insertUniqueId(prev, sessionId, targetIndex)
161
+
162
+ if (!arraysEqual(prev, next)) {
163
+ $pinnedSessionIds.set(next)
164
+ }
165
+ }
166
+
167
+ export function bumpSessionsLimit(step: number = SIDEBAR_SESSIONS_PAGE_SIZE) {
168
+ const safeStep = Math.max(1, Math.floor(step))
169
+ $sessionsLimit.set($sessionsLimit.get() + safeStep)
170
+ }
171
+
172
+ export function resetSessionsLimit() {
173
+ if ($sessionsLimit.get() !== SIDEBAR_SESSIONS_PAGE_SIZE) {
174
+ $sessionsLimit.set(SIDEBAR_SESSIONS_PAGE_SIZE)
175
+ }
176
+ }
@@ -0,0 +1,51 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest'
2
+
3
+ import { $modelPresets, applyModelPreset, getModelPreset, modelPresetKey, setModelPreset } from './model-presets'
4
+
5
+ describe('model presets', () => {
6
+ beforeEach(() => $modelPresets.set({}))
7
+
8
+ it('round-trips a preset and merges patches without dropping prior fields', () => {
9
+ setModelPreset('anthropic', 'claude-opus-4-8', { effort: 'high' })
10
+ setModelPreset('anthropic', 'claude-opus-4-8', { fast: true })
11
+
12
+ expect(getModelPreset('anthropic', 'claude-opus-4-8')).toEqual({ effort: 'high', fast: true })
13
+ })
14
+
15
+ it('returns an empty preset for unknown models', () => {
16
+ expect(getModelPreset('x', 'y')).toEqual({})
17
+ })
18
+
19
+ it('keys by provider::model', () => {
20
+ expect(modelPresetKey('openai', 'gpt-5.5')).toBe('openai::gpt-5.5')
21
+ })
22
+
23
+ it('pushes only the provided dimensions to the gateway', async () => {
24
+ const calls: { method: string; params?: Record<string, unknown> }[] = []
25
+
26
+ const request = async <T>(method: string, params?: Record<string, unknown>) => {
27
+ calls.push({ method, params })
28
+
29
+ return {} as T
30
+ }
31
+
32
+ await applyModelPreset({ effort: 'high' }, { failMessage: 'x', request, sessionId: 's1' })
33
+ await applyModelPreset({}, { failMessage: 'x', request, sessionId: 's1' })
34
+
35
+ expect(calls).toEqual([{ method: 'config.set', params: { key: 'reasoning', session_id: 's1', value: 'high' } }])
36
+ })
37
+
38
+ it('no-ops without a session so selecting a model cannot mutate global config', async () => {
39
+ const calls: { method: string; params?: Record<string, unknown> }[] = []
40
+
41
+ const request = async <T>(method: string, params?: Record<string, unknown>) => {
42
+ calls.push({ method, params })
43
+
44
+ return {} as T
45
+ }
46
+
47
+ await applyModelPreset({ effort: 'high', fast: true }, { failMessage: 'x', request, sessionId: null })
48
+
49
+ expect(calls).toEqual([])
50
+ })
51
+ })