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,100 @@
1
+ import { atom } from 'nanostores'
2
+
3
+ import { sessionTitle } from '@/lib/chat-runtime'
4
+ import type { PreviewServerRestart } from '@/store/preview'
5
+ import type { ActionStatusResponse, SessionInfo } from '@/types/nastech'
6
+
7
+ const HISTORY_LIMIT = 8
8
+ const COMPLETED_TTL_MS = 5 * 60 * 1000
9
+
10
+ export type RailTaskStatus = 'error' | 'running' | 'success'
11
+
12
+ export interface RailTask {
13
+ id: string
14
+ label: string
15
+ detail: string
16
+ status: RailTaskStatus
17
+ updatedAt: number
18
+ }
19
+
20
+ export interface DesktopActionTask {
21
+ status: ActionStatusResponse
22
+ updatedAt: number
23
+ }
24
+
25
+ export const $desktopActionTasks = atom<Record<string, DesktopActionTask>>({})
26
+
27
+ export function upsertDesktopActionTask(status: ActionStatusResponse): void {
28
+ $desktopActionTasks.set(prune({ ...$desktopActionTasks.get(), [status.name]: { status, updatedAt: Date.now() } }))
29
+ }
30
+
31
+ export function buildRailTasks(
32
+ workingSessionIds: readonly string[],
33
+ sessions: readonly SessionInfo[],
34
+ previewRestart: PreviewServerRestart | null,
35
+ actionTasks: Record<string, DesktopActionTask>
36
+ ): RailTask[] {
37
+ const sessionsById = new Map(sessions.map(session => [session.id, session]))
38
+
39
+ const sessionTasks: RailTask[] = workingSessionIds.map((id, index) => {
40
+ const session = sessionsById.get(id)
41
+
42
+ return {
43
+ id: `session:${id}`,
44
+ label: session ? sessionTitle(session) : 'Session task',
45
+ detail: 'Agent task running',
46
+ status: 'running',
47
+ updatedAt: session?.last_active || Date.now() - index
48
+ }
49
+ })
50
+
51
+ const previewTasks: RailTask[] = previewRestart
52
+ ? [
53
+ {
54
+ id: `preview:${previewRestart.taskId}`,
55
+ label: 'Preview restart',
56
+ detail: previewRestart.message || previewRestart.url,
57
+ status:
58
+ previewRestart.status === 'error' ? 'error' : previewRestart.status === 'running' ? 'running' : 'success',
59
+ updatedAt: Date.now()
60
+ }
61
+ ]
62
+ : []
63
+
64
+ const actions: RailTask[] = Object.values(actionTasks).map(({ status, updatedAt }) => ({
65
+ id: `action:${status.name}`,
66
+ label: status.name,
67
+ detail: actionDetail(status),
68
+ status: actionStatus(status),
69
+ updatedAt
70
+ }))
71
+
72
+ return [...sessionTasks, ...previewTasks, ...actions].sort((left, right) => right.updatedAt - left.updatedAt)
73
+ }
74
+
75
+ function actionStatus(status: ActionStatusResponse): RailTaskStatus {
76
+ if (status.running) {
77
+ return 'running'
78
+ }
79
+
80
+ return status.exit_code === 0 ? 'success' : 'error'
81
+ }
82
+
83
+ function actionDetail(status: ActionStatusResponse): string {
84
+ if (status.running) {
85
+ return 'Running'
86
+ }
87
+
88
+ return status.exit_code === 0 ? 'Completed' : `Failed (${status.exit_code ?? 'unknown'})`
89
+ }
90
+
91
+ function prune(tasks: Record<string, DesktopActionTask>): Record<string, DesktopActionTask> {
92
+ const now = Date.now()
93
+
94
+ return Object.fromEntries(
95
+ Object.entries(tasks)
96
+ .filter(([, task]) => task.status.running || now - task.updatedAt <= COMPLETED_TTL_MS)
97
+ .sort(([, left], [, right]) => right.updatedAt - left.updatedAt)
98
+ .slice(0, HISTORY_LIMIT)
99
+ )
100
+ }
@@ -0,0 +1,91 @@
1
+ import { atom } from 'nanostores'
2
+
3
+ import type { DesktopBootProgress } from '@/global'
4
+ import { translateNow } from '@/i18n'
5
+
6
+ export interface DesktopBootState extends DesktopBootProgress {
7
+ visible: boolean
8
+ }
9
+
10
+ const INITIAL_BOOT_STATE: DesktopBootState = {
11
+ error: null,
12
+ fakeMode: false,
13
+ message: translateNow('boot.steps.startingNasTechDesktop'),
14
+ phase: 'renderer.init',
15
+ progress: 2,
16
+ running: true,
17
+ timestamp: Date.now(),
18
+ visible: true
19
+ }
20
+
21
+ export const $desktopBoot = atom<DesktopBootState>(INITIAL_BOOT_STATE)
22
+
23
+ function clampProgress(value: number) {
24
+ if (!Number.isFinite(value)) {
25
+ return 0
26
+ }
27
+
28
+ return Math.max(0, Math.min(100, Math.round(value)))
29
+ }
30
+
31
+ export function applyDesktopBootProgress(progress: DesktopBootProgress) {
32
+ const current = $desktopBoot.get()
33
+ const nextProgress = clampProgress(progress.progress)
34
+ const mergedProgress = progress.running ? Math.max(current.progress, nextProgress) : nextProgress
35
+
36
+ $desktopBoot.set({
37
+ ...current,
38
+ ...progress,
39
+ error: progress.error ?? null,
40
+ progress: mergedProgress,
41
+ visible: progress.running || mergedProgress < 100 || Boolean(progress.error)
42
+ })
43
+ }
44
+
45
+ export function setDesktopBootStep(step: {
46
+ phase: string
47
+ message: string
48
+ progress: number
49
+ running?: boolean
50
+ fakeMode?: boolean
51
+ error?: string | null
52
+ }) {
53
+ const current = $desktopBoot.get()
54
+ applyDesktopBootProgress({
55
+ error: step.error ?? null,
56
+ fakeMode: step.fakeMode ?? current.fakeMode,
57
+ message: step.message,
58
+ phase: step.phase,
59
+ progress: step.progress,
60
+ running: step.running ?? true,
61
+ timestamp: Date.now()
62
+ })
63
+ }
64
+
65
+ export function completeDesktopBoot(message = translateNow('boot.ready')) {
66
+ const current = $desktopBoot.get()
67
+ $desktopBoot.set({
68
+ ...current,
69
+ error: null,
70
+ message,
71
+ phase: 'renderer.ready',
72
+ progress: 100,
73
+ running: false,
74
+ timestamp: Date.now(),
75
+ visible: false
76
+ })
77
+ }
78
+
79
+ export function failDesktopBoot(message: string) {
80
+ const current = $desktopBoot.get()
81
+ $desktopBoot.set({
82
+ ...current,
83
+ error: message,
84
+ message: translateNow('boot.desktopBootFailedWithMessage', message),
85
+ phase: 'renderer.error',
86
+ progress: clampProgress(current.progress),
87
+ running: false,
88
+ timestamp: Date.now(),
89
+ visible: true
90
+ })
91
+ }
@@ -0,0 +1,81 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
2
+
3
+ import {
4
+ $clarifyRequest,
5
+ $clarifyRequests,
6
+ type ClarifyRequest,
7
+ clearClarifyRequest,
8
+ setClarifyRequest
9
+ } from './clarify'
10
+ import { $activeSessionId } from './session'
11
+
12
+ function clarify(sessionId: string | null, requestId: string): ClarifyRequest {
13
+ return {
14
+ requestId,
15
+ question: `question-${requestId}`,
16
+ choices: null,
17
+ sessionId
18
+ }
19
+ }
20
+
21
+ describe('clarify store', () => {
22
+ beforeEach(() => {
23
+ $clarifyRequests.set({})
24
+ $activeSessionId.set(null)
25
+ })
26
+
27
+ afterEach(() => {
28
+ $clarifyRequests.set({})
29
+ $activeSessionId.set(null)
30
+ })
31
+
32
+ it('keeps clarify requests from concurrent sessions independent', () => {
33
+ setClarifyRequest(clarify('session-a', 'req-a'))
34
+ setClarifyRequest(clarify('session-b', 'req-b'))
35
+
36
+ expect($clarifyRequests.get()['session-a']?.requestId).toBe('req-a')
37
+ expect($clarifyRequests.get()['session-b']?.requestId).toBe('req-b')
38
+ })
39
+
40
+ it('exposes only the active session via the focus-scoped view', () => {
41
+ setClarifyRequest(clarify('session-a', 'req-a'))
42
+ setClarifyRequest(clarify('session-b', 'req-b'))
43
+
44
+ $activeSessionId.set('session-a')
45
+ expect($clarifyRequest.get()?.requestId).toBe('req-a')
46
+
47
+ $activeSessionId.set('session-b')
48
+ expect($clarifyRequest.get()?.requestId).toBe('req-b')
49
+
50
+ $activeSessionId.set('session-c')
51
+ expect($clarifyRequest.get()).toBeNull()
52
+ })
53
+
54
+ it('clears only the targeted session, leaving the other pending', () => {
55
+ setClarifyRequest(clarify('session-a', 'req-a'))
56
+ setClarifyRequest(clarify('session-b', 'req-b'))
57
+
58
+ clearClarifyRequest('req-a', 'session-a')
59
+
60
+ expect($clarifyRequests.get()['session-a']).toBeUndefined()
61
+ expect($clarifyRequests.get()['session-b']?.requestId).toBe('req-b')
62
+ })
63
+
64
+ it('ignores a stale clear whose request id no longer matches', () => {
65
+ setClarifyRequest(clarify('session-a', 'req-a2'))
66
+
67
+ clearClarifyRequest('req-a1', 'session-a')
68
+
69
+ expect($clarifyRequests.get()['session-a']?.requestId).toBe('req-a2')
70
+ })
71
+
72
+ it('clears by request id across sessions when no session hint is given', () => {
73
+ setClarifyRequest(clarify('session-a', 'shared'))
74
+ setClarifyRequest(clarify('session-b', 'other'))
75
+
76
+ clearClarifyRequest('shared')
77
+
78
+ expect($clarifyRequests.get()['session-a']).toBeUndefined()
79
+ expect($clarifyRequests.get()['session-b']?.requestId).toBe('other')
80
+ })
81
+ })
@@ -0,0 +1,69 @@
1
+ import { atom, computed } from 'nanostores'
2
+
3
+ import { $activeSessionId } from './session'
4
+
5
+ export interface ClarifyRequest {
6
+ requestId: string
7
+ question: string
8
+ choices: string[] | null
9
+ sessionId: string | null
10
+ }
11
+
12
+ // Pending clarify requests keyed by the runtime session id that raised them.
13
+ // Storing per-session (instead of one shared slot) lets a *background* session
14
+ // park its clarify request while the user is looking at a different chat, then
15
+ // resolve it once they switch over — without a second concurrent clarify
16
+ // clobbering the first. A request with no session id lands under the empty key.
17
+ const keyFor = (sessionId: string | null | undefined): string => sessionId ?? ''
18
+
19
+ export const $clarifyRequests = atom<Record<string, ClarifyRequest>>({})
20
+
21
+ // The clarify request for the currently-viewed session. The inline ClarifyTool
22
+ // only ever mounts inside the active session's transcript, so it reads this
23
+ // focus-scoped view rather than reaching into the whole map.
24
+ export const $clarifyRequest = computed(
25
+ [$clarifyRequests, $activeSessionId],
26
+ (requests, activeId) => requests[keyFor(activeId)] ?? null
27
+ )
28
+
29
+ export function setClarifyRequest(request: ClarifyRequest): void {
30
+ $clarifyRequests.set({ ...$clarifyRequests.get(), [keyFor(request.sessionId)]: request })
31
+ }
32
+
33
+ export function clearClarifyRequest(requestId?: string, sessionId?: string | null): void {
34
+ const requests = $clarifyRequests.get()
35
+
36
+ // Targeted clear when the caller knows the session (the common path from the
37
+ // inline ClarifyTool answering its own request).
38
+ if (sessionId !== undefined) {
39
+ const key = keyFor(sessionId)
40
+ const current = requests[key]
41
+
42
+ if (!current || (requestId && current.requestId !== requestId)) {
43
+ return
44
+ }
45
+
46
+ const next = { ...requests }
47
+ delete next[key]
48
+ $clarifyRequests.set(next)
49
+
50
+ return
51
+ }
52
+
53
+ // Fallback with no session hint: drop every entry matching the request id
54
+ // (or clear all when none is given).
55
+ const next: Record<string, ClarifyRequest> = {}
56
+ let changed = false
57
+
58
+ for (const [key, value] of Object.entries(requests)) {
59
+ if (requestId && value.requestId !== requestId) {
60
+ next[key] = value
61
+ } else {
62
+ changed = true
63
+ }
64
+ }
65
+
66
+ if (changed) {
67
+ $clarifyRequests.set(next)
68
+ }
69
+ }
@@ -0,0 +1,20 @@
1
+ import { atom } from 'nanostores'
2
+
3
+ /** Whether the global command palette (Cmd/Ctrl+K) is currently open. */
4
+ export const $commandPaletteOpen = atom(false)
5
+
6
+ export function openCommandPalette(): void {
7
+ $commandPaletteOpen.set(true)
8
+ }
9
+
10
+ export function closeCommandPalette(): void {
11
+ $commandPaletteOpen.set(false)
12
+ }
13
+
14
+ export function setCommandPaletteOpen(open: boolean): void {
15
+ $commandPaletteOpen.set(open)
16
+ }
17
+
18
+ export function toggleCommandPalette(): void {
19
+ $commandPaletteOpen.set(!$commandPaletteOpen.get())
20
+ }
@@ -0,0 +1,53 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
2
+
3
+ import { $compactingSessions, $compactionActive, setSessionCompacting } from './compaction'
4
+ import { $activeSessionId } from './session'
5
+
6
+ describe('compaction store', () => {
7
+ beforeEach(() => {
8
+ $compactingSessions.set({})
9
+ $activeSessionId.set(null)
10
+ })
11
+
12
+ afterEach(() => {
13
+ $compactingSessions.set({})
14
+ $activeSessionId.set(null)
15
+ })
16
+
17
+ it('tracks compaction per session independently', () => {
18
+ setSessionCompacting('session-a', true)
19
+ setSessionCompacting('session-b', true)
20
+
21
+ expect($compactingSessions.get()).toEqual({ 'session-a': true, 'session-b': true })
22
+ })
23
+
24
+ it('exposes only the active session via the focus-scoped view', () => {
25
+ setSessionCompacting('session-a', true)
26
+
27
+ expect($compactionActive.get()).toBe(false)
28
+
29
+ $activeSessionId.set('session-a')
30
+ expect($compactionActive.get()).toBe(true)
31
+
32
+ $activeSessionId.set('session-b')
33
+ expect($compactionActive.get()).toBe(false)
34
+ })
35
+
36
+ it('clears a session without disturbing the others', () => {
37
+ setSessionCompacting('session-a', true)
38
+ setSessionCompacting('session-b', true)
39
+
40
+ setSessionCompacting('session-a', false)
41
+
42
+ expect($compactingSessions.get()).toEqual({ 'session-b': true })
43
+ })
44
+
45
+ it('is a no-op when clearing an unknown session', () => {
46
+ setSessionCompacting('session-a', true)
47
+ const before = $compactingSessions.get()
48
+
49
+ setSessionCompacting('session-missing', false)
50
+
51
+ expect($compactingSessions.get()).toBe(before)
52
+ })
53
+ })
@@ -0,0 +1,38 @@
1
+ import { atom, computed } from 'nanostores'
2
+
3
+ import { $activeSessionId } from './session'
4
+
5
+ // Per-session flag while auto-compaction runs mid-turn. Without it the
6
+ // transcript looks like it reset; per-session so a background chat can't
7
+ // clobber the foreground view.
8
+ const keyFor = (sessionId: string | null | undefined): string => sessionId ?? ''
9
+
10
+ export const $compactingSessions = atom<Record<string, true>>({})
11
+
12
+ export const $compactionActive = computed(
13
+ [$compactingSessions, $activeSessionId],
14
+ (sessions, activeId) => keyFor(activeId) in sessions
15
+ )
16
+
17
+ export function setSessionCompacting(sessionId: string | null | undefined, active: boolean): void {
18
+ const key = keyFor(sessionId)
19
+ const sessions = $compactingSessions.get()
20
+
21
+ if (active) {
22
+ if (key in sessions) {
23
+ return
24
+ }
25
+
26
+ $compactingSessions.set({ ...sessions, [key]: true })
27
+
28
+ return
29
+ }
30
+
31
+ if (!(key in sessions)) {
32
+ return
33
+ }
34
+
35
+ const next = { ...sessions }
36
+ delete next[key]
37
+ $compactingSessions.set(next)
38
+ }
@@ -0,0 +1,32 @@
1
+ import { atom } from 'nanostores'
2
+
3
+ import { persistString, storedString } from '@/lib/storage'
4
+
5
+ const STORAGE_KEY = 'nastech.desktop.completionSoundVariantId'
6
+
7
+ export const DEFAULT_COMPLETION_SOUND_VARIANT_ID = 1
8
+
9
+ // Range mirrors COMPLETION_SOUND_VARIANTS in lib/completion-sound.ts. Validating
10
+ // by range (not membership) keeps this store free of a dependency on the lib,
11
+ // which imports the atom back — a membership check would close that cycle.
12
+ const VARIANT_COUNT = 14
13
+
14
+ export function resolveCompletionSoundVariantId(variantId: number): number {
15
+ return Number.isInteger(variantId) && variantId >= 1 && variantId <= VARIANT_COUNT
16
+ ? variantId
17
+ : DEFAULT_COMPLETION_SOUND_VARIANT_ID
18
+ }
19
+
20
+ function load(): number {
21
+ const stored = storedString(STORAGE_KEY)
22
+
23
+ return stored ? resolveCompletionSoundVariantId(Number.parseInt(stored, 10)) : DEFAULT_COMPLETION_SOUND_VARIANT_ID
24
+ }
25
+
26
+ export const $completionSoundVariantId = atom(load())
27
+
28
+ $completionSoundVariantId.subscribe(id => persistString(STORAGE_KEY, String(id)))
29
+
30
+ export function setCompletionSoundVariantId(variantId: number) {
31
+ $completionSoundVariantId.set(resolveCompletionSoundVariantId(variantId))
32
+ }
@@ -0,0 +1,147 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest'
2
+
3
+ import {
4
+ $perSessionBrowse,
5
+ browseBackward,
6
+ browseForward,
7
+ deriveUserHistory,
8
+ isBrowsingHistory,
9
+ resetBrowseState
10
+ } from './composer-input-history'
11
+
12
+ const SESSION_A = 'session-a'
13
+ const SESSION_B = 'session-b'
14
+
15
+ // Newest-first user text ring, what the caller passes to browse*.
16
+ const HISTORY = ['third', 'second', 'first']
17
+
18
+ const MSG = (role: string, text: string) => ({ id: '', role, text })
19
+
20
+ beforeEach(() => {
21
+ $perSessionBrowse.set({})
22
+ })
23
+
24
+ describe('deriveUserHistory', () => {
25
+ it('returns user messages newest-first with empty/whitespace skipped', () => {
26
+ const messages = [
27
+ MSG('user', ' '),
28
+ MSG('assistant', 'hi'),
29
+ MSG('user', 'first'),
30
+ MSG('user', 'second')
31
+ ]
32
+
33
+ expect(deriveUserHistory(messages, m => m.text)).toEqual(['second', 'first'])
34
+ })
35
+ })
36
+
37
+ describe('browseBackward', () => {
38
+ it('returns null when history is empty', () => {
39
+ expect(browseBackward(SESSION_A, '', [])).toBeNull()
40
+ })
41
+
42
+ it('returns the most recent entry on first press and saves the draft', () => {
43
+ const result = browseBackward(SESSION_A, 'unsent draft', HISTORY)
44
+
45
+ expect(result).toBe('third')
46
+ expect($perSessionBrowse.get()[SESSION_A]!.draftSnapshot).toBe('unsent draft')
47
+ })
48
+
49
+ it('moves to older entries on subsequent presses and stops at the oldest', () => {
50
+ expect(browseBackward(SESSION_A, '', HISTORY)).toBe('third')
51
+ expect(browseBackward(SESSION_A, '', HISTORY)).toBe('second')
52
+ expect(browseBackward(SESSION_A, '', HISTORY)).toBe('first')
53
+ expect(browseBackward(SESSION_A, '', HISTORY)).toBeNull()
54
+ })
55
+
56
+ it('uses caller-provided history, not a mirrored ring', () => {
57
+ // The store never owns the ring — the caller passes it every press.
58
+ // If the ring changes between presses (e.g. a new message was sent),
59
+ // the next press sees the updated ring and the cursor continues
60
+ // from where it was within it.
61
+ expect(browseBackward(SESSION_A, '', ['youngest', 'older'])).toBe('youngest')
62
+
63
+ // Caller added a new message; ring is now [brand-new, youngest, older].
64
+ // Cursor was at 0, next press advances to 1 -> "youngest".
65
+ expect(
66
+ browseBackward(SESSION_A, '', ['brand-new', 'youngest', 'older'])
67
+ ).toBe('youngest')
68
+
69
+ // One more press -> "older".
70
+ expect(
71
+ browseBackward(SESSION_A, '', ['brand-new', 'youngest', 'older'])
72
+ ).toBe('older')
73
+ })
74
+ })
75
+
76
+ describe('browseForward', () => {
77
+ it('returns null when not browsing', () => {
78
+ expect(browseForward(SESSION_A, HISTORY)).toBeNull()
79
+ })
80
+
81
+ it('moves toward the present', () => {
82
+ browseBackward(SESSION_A, 'draft', HISTORY) // cursor 0 -> 'third'
83
+ browseBackward(SESSION_A, '', HISTORY) // cursor 1 -> 'second'
84
+
85
+ expect(browseForward(SESSION_A, HISTORY)).toEqual({
86
+ text: 'third',
87
+ returnedToPresent: false
88
+ })
89
+ })
90
+
91
+ it('restores the saved draft and resets when reaching the present', () => {
92
+ browseBackward(SESSION_A, 'my original draft', HISTORY)
93
+
94
+ const result = browseForward(SESSION_A, HISTORY)
95
+
96
+ expect(result).toEqual({ text: 'my original draft', returnedToPresent: true })
97
+ expect(isBrowsingHistory(SESSION_A)).toBe(false)
98
+ })
99
+ })
100
+
101
+ describe('per-session isolation', () => {
102
+ it('tracks cursor and draft independently per session', () => {
103
+ browseBackward(SESSION_A, 'draft-a', HISTORY)
104
+ browseBackward(SESSION_A, '', HISTORY) // older
105
+
106
+ browseBackward(SESSION_B, 'draft-b', HISTORY)
107
+
108
+ const a = $perSessionBrowse.get()[SESSION_A]!
109
+ const b = $perSessionBrowse.get()[SESSION_B]!
110
+
111
+ expect(a.cursor).toBe(1)
112
+ expect(a.draftSnapshot).toBe('draft-a')
113
+ expect(b.cursor).toBe(0)
114
+ expect(b.draftSnapshot).toBe('draft-b')
115
+ })
116
+ })
117
+
118
+ describe('resetBrowseState', () => {
119
+ it('clears cursor and draft snapshot', () => {
120
+ browseBackward(SESSION_A, 'draft', HISTORY)
121
+ resetBrowseState(SESSION_A)
122
+
123
+ const s = $perSessionBrowse.get()[SESSION_A]!
124
+
125
+ expect(s.cursor).toBe(-1)
126
+ expect(s.draftSnapshot).toBe('')
127
+ })
128
+ })
129
+
130
+ describe('session switch behavior', () => {
131
+ it('resets the previous session cursor and lets the new session derive its own ring', () => {
132
+ // Session A: user browsed into the past
133
+ browseBackward(SESSION_A, '', HISTORY)
134
+ expect(isBrowsingHistory(SESSION_A)).toBe(true)
135
+
136
+ // Caller switches to session B; resets A's browse state
137
+ resetBrowseState(SESSION_A)
138
+
139
+ // Session B's ring is derived from B's messages, not A's
140
+ const sessionBMessages = [MSG('user', 'hello-b'), MSG('user', 'world-b')]
141
+ const sessionBHistory = deriveUserHistory(sessionBMessages, m => m.text)
142
+
143
+ expect(browseBackward(SESSION_B, '', sessionBHistory)).toBe('world-b')
144
+ expect(browseBackward(SESSION_B, '', sessionBHistory)).toBe('hello-b')
145
+ expect(isBrowsingHistory(SESSION_A)).toBe(false)
146
+ })
147
+ })