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,158 @@
1
+ import { atom } from 'nanostores'
2
+
3
+ /**
4
+ * Per-session input history browse state.
5
+ *
6
+ * The user-text ring is **derived from the live session messages** on each
7
+ * keypress — it is not mirrored anywhere. This keeps a single source of truth
8
+ * and avoids the entire class of seeding/dedup bugs that come from trying to
9
+ * keep a parallel ring in sync with submit/queue/voice paths.
10
+ *
11
+ * We only persist the cursor and the saved draft:
12
+ * - `cursor` — index into the derived user-text ring (0 = newest, larger = older).
13
+ * `-1` means "not browsing".
14
+ * - `draftSnapshot` — the composer text at the moment the user started
15
+ * browsing, so ArrowDown back to the "present" restores it.
16
+ */
17
+ export interface SessionBrowseState {
18
+ cursor: number
19
+ draftSnapshot: string
20
+ }
21
+
22
+ const $perSessionBrowse = atom<Record<string, SessionBrowseState>>({})
23
+
24
+ function ensure(sessionId: string): SessionBrowseState {
25
+ const all = { ...$perSessionBrowse.get() }
26
+ let s = all[sessionId]
27
+
28
+ if (!s) {
29
+ s = { cursor: -1, draftSnapshot: '' }
30
+ all[sessionId] = s
31
+ $perSessionBrowse.set(all)
32
+ }
33
+
34
+ return s
35
+ }
36
+
37
+ function persist() {
38
+ $perSessionBrowse.set({ ...$perSessionBrowse.get() })
39
+ }
40
+
41
+ function valid(sessionId: string | null | undefined): sessionId is string {
42
+ return typeof sessionId === 'string' && sessionId.length > 0
43
+ }
44
+
45
+ /**
46
+ * Derive the user-text ring (newest first) from session messages.
47
+ * The caller is responsible for providing already-session-scoped messages.
48
+ */
49
+ export function deriveUserHistory<T extends { role: string }>(
50
+ messages: readonly T[],
51
+ getText: (m: T) => string
52
+ ): string[] {
53
+ const out: string[] = []
54
+
55
+ for (let i = messages.length - 1; i >= 0; i--) {
56
+ const m = messages[i]!
57
+
58
+ if (m.role !== 'user') {continue}
59
+
60
+ const t = getText(m).trim()
61
+
62
+ if (t) {out.push(t)}
63
+ }
64
+
65
+ return out
66
+ }
67
+
68
+ /**
69
+ * Start browsing backward, or step to the next older entry.
70
+ * Returns the text to place in the composer, or null if already at the oldest
71
+ * entry (or the ring is empty).
72
+ */
73
+ export function browseBackward(
74
+ sessionId: string | null | undefined,
75
+ currentDraft: string,
76
+ history: readonly string[]
77
+ ): string | null {
78
+ if (!valid(sessionId) || history.length === 0) {
79
+ return null
80
+ }
81
+
82
+ const s = ensure(sessionId)
83
+
84
+ if (s.cursor === -1) {
85
+ s.draftSnapshot = currentDraft
86
+ s.cursor = 0
87
+ } else if (s.cursor < history.length - 1) {
88
+ s.cursor += 1
89
+ } else {
90
+ return null
91
+ }
92
+
93
+ persist()
94
+
95
+ return history[s.cursor]!
96
+ }
97
+
98
+ /**
99
+ * Browse forward toward the present. When reaching the "newest" entry the
100
+ * saved draft is restored and the cursor resets.
101
+ */
102
+ export function browseForward(
103
+ sessionId: string | null | undefined,
104
+ history: readonly string[]
105
+ ): { text: string; returnedToPresent: boolean } | null {
106
+ if (!valid(sessionId)) {
107
+ return null
108
+ }
109
+
110
+ const s = ensure(sessionId)
111
+
112
+ if (s.cursor === -1) {
113
+ return null
114
+ }
115
+
116
+ if (s.cursor > 0) {
117
+ s.cursor -= 1
118
+ persist()
119
+
120
+ return { text: history[s.cursor]!, returnedToPresent: false }
121
+ }
122
+
123
+ // At newest; moving forward restores the saved draft.
124
+ const text = s.draftSnapshot
125
+ s.cursor = -1
126
+ s.draftSnapshot = ''
127
+ persist()
128
+
129
+ return { text, returnedToPresent: true }
130
+ }
131
+
132
+ /** Clear browse state for a session (e.g. on session switch or new submit). */
133
+ export function resetBrowseState(sessionId: string | null | undefined) {
134
+ if (!valid(sessionId)) {
135
+ return
136
+ }
137
+
138
+ const all = { ...$perSessionBrowse.get() }
139
+ const existing = all[sessionId]
140
+
141
+ if (!existing) {return}
142
+
143
+ all[sessionId] = { cursor: -1, draftSnapshot: '' }
144
+ $perSessionBrowse.set(all)
145
+ }
146
+
147
+ /** True if the user is currently browsing history for this session. */
148
+ export function isBrowsingHistory(sessionId: string | null | undefined): boolean {
149
+ if (!valid(sessionId)) {
150
+ return false
151
+ }
152
+
153
+ const s = $perSessionBrowse.get()[sessionId]
154
+
155
+ return s ? s.cursor >= 0 : false
156
+ }
157
+
158
+ export { $perSessionBrowse }
@@ -0,0 +1,148 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest'
2
+
3
+ import type { ComposerAttachment } from './composer'
4
+ import {
5
+ $queuedPromptsBySession,
6
+ clearQueuedPrompts,
7
+ dequeueQueuedPrompt,
8
+ enqueueQueuedPrompt,
9
+ getQueuedPrompts,
10
+ promoteQueuedPrompt,
11
+ removeQueuedPrompt,
12
+ shouldAutoDrainOnSettle,
13
+ updateQueuedPrompt,
14
+ updateQueuedPromptText
15
+ } from './composer-queue'
16
+
17
+ const SESSION_KEY = 'session-abc'
18
+ const QUEUE_STORAGE_KEY = 'NASTECH.desktop.composerQueue.v1'
19
+
20
+ function attachment(id: string, kind: ComposerAttachment['kind'] = 'file'): ComposerAttachment {
21
+ return {
22
+ id,
23
+ kind,
24
+ label: id,
25
+ refText: `@file:${id}`
26
+ }
27
+ }
28
+
29
+ describe('composer queue store', () => {
30
+ beforeEach(() => {
31
+ window.localStorage.removeItem(QUEUE_STORAGE_KEY)
32
+ $queuedPromptsBySession.set({})
33
+ })
34
+
35
+ it('queues prompts in FIFO order', () => {
36
+ enqueueQueuedPrompt(SESSION_KEY, { attachments: [], text: 'first' })
37
+ enqueueQueuedPrompt(SESSION_KEY, { attachments: [], text: 'second' })
38
+
39
+ expect(dequeueQueuedPrompt(SESSION_KEY)?.text).toBe('first')
40
+ expect(dequeueQueuedPrompt(SESSION_KEY)?.text).toBe('second')
41
+ expect(dequeueQueuedPrompt(SESSION_KEY)).toBeNull()
42
+ })
43
+
44
+ it('clones attachments when queueing', () => {
45
+ const source = [attachment('a-1')]
46
+ const queued = enqueueQueuedPrompt(SESSION_KEY, { attachments: source, text: 'check clones' })
47
+
48
+ expect(queued).not.toBeNull()
49
+ expect(getQueuedPrompts(SESSION_KEY)[0]?.attachments[0]).toEqual(source[0])
50
+ expect(getQueuedPrompts(SESSION_KEY)[0]?.attachments[0]).not.toBe(source[0])
51
+ })
52
+
53
+ it('updates and removes queued entries by id', () => {
54
+ const first = enqueueQueuedPrompt(SESSION_KEY, { attachments: [], text: 'draft one' })
55
+ const second = enqueueQueuedPrompt(SESSION_KEY, { attachments: [], text: 'draft two' })
56
+
57
+ expect(first).not.toBeNull()
58
+ expect(second).not.toBeNull()
59
+
60
+ expect(updateQueuedPromptText(SESSION_KEY, first!.id, 'draft one edited')).toBe(true)
61
+ expect(getQueuedPrompts(SESSION_KEY).map(entry => entry.text)).toEqual(['draft one edited', 'draft two'])
62
+
63
+ expect(removeQueuedPrompt(SESSION_KEY, first!.id)).toBe(true)
64
+ expect(getQueuedPrompts(SESSION_KEY).map(entry => entry.text)).toEqual(['draft two'])
65
+ })
66
+
67
+ it('promotes a queued entry to the front', () => {
68
+ const first = enqueueQueuedPrompt(SESSION_KEY, { attachments: [], text: 'first' })
69
+ const second = enqueueQueuedPrompt(SESSION_KEY, { attachments: [], text: 'second' })
70
+ const third = enqueueQueuedPrompt(SESSION_KEY, { attachments: [], text: 'third' })
71
+
72
+ expect(first).not.toBeNull()
73
+ expect(second).not.toBeNull()
74
+ expect(third).not.toBeNull()
75
+
76
+ expect(promoteQueuedPrompt(SESSION_KEY, third!.id)).toBe(true)
77
+ expect(getQueuedPrompts(SESSION_KEY).map(entry => entry.text)).toEqual(['third', 'first', 'second'])
78
+ expect(promoteQueuedPrompt(SESSION_KEY, third!.id)).toBe(false)
79
+ })
80
+
81
+ it('updates queued text and attachment snapshot', () => {
82
+ const first = enqueueQueuedPrompt(SESSION_KEY, { attachments: [attachment('f-1')], text: 'draft one' })
83
+ const editedAttachments = [attachment('f-2'), attachment('f-3', 'image')]
84
+
85
+ expect(first).not.toBeNull()
86
+ expect(
87
+ updateQueuedPrompt(SESSION_KEY, first!.id, {
88
+ attachments: editedAttachments,
89
+ text: 'edited text'
90
+ })
91
+ ).toBe(true)
92
+
93
+ const queue = getQueuedPrompts(SESSION_KEY)
94
+ expect(queue[0]?.text).toBe('edited text')
95
+ expect(queue[0]?.attachments).toEqual(editedAttachments)
96
+ expect(queue[0]?.attachments[0]).not.toBe(editedAttachments[0])
97
+ })
98
+
99
+ it('clears queue state for a session', () => {
100
+ enqueueQueuedPrompt(SESSION_KEY, { attachments: [attachment('img-1', 'image')], text: 'queued' })
101
+
102
+ clearQueuedPrompts(SESSION_KEY)
103
+
104
+ expect(getQueuedPrompts(SESSION_KEY)).toEqual([])
105
+ expect($queuedPromptsBySession.get()[SESSION_KEY]).toBeUndefined()
106
+ expect(window.localStorage.getItem(QUEUE_STORAGE_KEY)).toBeNull()
107
+ })
108
+
109
+ it('persists queue entries into local storage', () => {
110
+ enqueueQueuedPrompt(SESSION_KEY, { attachments: [], text: 'persist me' })
111
+
112
+ const raw = window.localStorage.getItem(QUEUE_STORAGE_KEY)
113
+ expect(raw).toBeTruthy()
114
+
115
+ const parsed = JSON.parse(String(raw)) as Record<string, { text: string }[]>
116
+ expect(parsed[SESSION_KEY]?.[0]?.text).toBe('persist me')
117
+ })
118
+ })
119
+
120
+ describe('shouldAutoDrainOnSettle', () => {
121
+ const base = { isBusy: false, queueLength: 1, wasBusy: true }
122
+
123
+ it('drains the next queued prompt when a turn settles', () => {
124
+ expect(shouldAutoDrainOnSettle(base)).toBe(true)
125
+ })
126
+
127
+ it('drains after an interrupt — the settle edge is the same', () => {
128
+ // Interrupting to reach a queued message is the point of the queue; the
129
+ // gateway emits the same settle whether the turn finished or was stopped.
130
+ expect(shouldAutoDrainOnSettle(base)).toBe(true)
131
+ })
132
+
133
+ it('does not drain when the queue is empty', () => {
134
+ expect(shouldAutoDrainOnSettle({ ...base, queueLength: 0 })).toBe(false)
135
+ })
136
+
137
+ it('ignores steady busy state (no true → false transition)', () => {
138
+ expect(shouldAutoDrainOnSettle({ ...base, isBusy: true })).toBe(false)
139
+ })
140
+
141
+ it('ignores busy entry (false → true, not a settle)', () => {
142
+ expect(shouldAutoDrainOnSettle({ ...base, isBusy: true, wasBusy: false })).toBe(false)
143
+ })
144
+
145
+ it('ignores steady idle state (was not busy)', () => {
146
+ expect(shouldAutoDrainOnSettle({ ...base, wasBusy: false })).toBe(false)
147
+ })
148
+ })
@@ -0,0 +1,239 @@
1
+ import { atom } from 'nanostores'
2
+
3
+ import type { ComposerAttachment } from './composer'
4
+
5
+ export interface QueuedPromptEntry {
6
+ id: string
7
+ text: string
8
+ attachments: ComposerAttachment[]
9
+ queuedAt: number
10
+ }
11
+
12
+ type QueueState = Record<string, QueuedPromptEntry[]>
13
+
14
+ const STORAGE_KEY = 'NASTECH.desktop.composerQueue.v1'
15
+
16
+ const load = (): QueueState => {
17
+ if (typeof window === 'undefined') {
18
+ return {}
19
+ }
20
+
21
+ try {
22
+ const raw = window.localStorage.getItem(STORAGE_KEY)
23
+ const parsed = raw ? JSON.parse(raw) : null
24
+
25
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? (parsed as QueueState) : {}
26
+ } catch {
27
+ return {}
28
+ }
29
+ }
30
+
31
+ const save = (state: QueueState) => {
32
+ if (typeof window === 'undefined') {
33
+ return
34
+ }
35
+
36
+ try {
37
+ if (Object.keys(state).length === 0) {
38
+ window.localStorage.removeItem(STORAGE_KEY)
39
+ } else {
40
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state))
41
+ }
42
+ } catch {
43
+ // best-effort: storage may be unavailable, queue still works in-memory
44
+ }
45
+ }
46
+
47
+ export const $queuedPromptsBySession = atom<QueueState>(load())
48
+
49
+ const writeSession = (sid: string, queue: QueuedPromptEntry[]) => {
50
+ const current = $queuedPromptsBySession.get()
51
+ const next = { ...current }
52
+
53
+ if (queue.length === 0) {
54
+ delete next[sid]
55
+ } else {
56
+ next[sid] = queue
57
+ }
58
+
59
+ $queuedPromptsBySession.set(next)
60
+ save(next)
61
+ }
62
+
63
+ const sidOf = (key: string | null | undefined): null | string => {
64
+ const trimmed = key?.trim()
65
+
66
+ return trimmed ? trimmed : null
67
+ }
68
+
69
+ const queueFor = (sid: string) => $queuedPromptsBySession.get()[sid] ?? []
70
+
71
+ const nextId = () => `queued-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
72
+
73
+ const cloneAttachments = (attachments: ComposerAttachment[]) => attachments.map(a => ({ ...a }))
74
+
75
+ export const getQueuedPrompts = (key: string | null | undefined): QueuedPromptEntry[] => {
76
+ const sid = sidOf(key)
77
+
78
+ return sid ? queueFor(sid) : []
79
+ }
80
+
81
+ export const enqueueQueuedPrompt = (
82
+ key: string | null | undefined,
83
+ payload: { text: string; attachments: ComposerAttachment[] }
84
+ ): null | QueuedPromptEntry => {
85
+ const sid = sidOf(key)
86
+
87
+ if (!sid) {
88
+ return null
89
+ }
90
+
91
+ const entry: QueuedPromptEntry = {
92
+ id: nextId(),
93
+ text: payload.text,
94
+ attachments: cloneAttachments(payload.attachments),
95
+ queuedAt: Date.now()
96
+ }
97
+
98
+ writeSession(sid, [...queueFor(sid), entry])
99
+
100
+ return entry
101
+ }
102
+
103
+ export const dequeueQueuedPrompt = (key: string | null | undefined): null | QueuedPromptEntry => {
104
+ const sid = sidOf(key)
105
+
106
+ if (!sid) {
107
+ return null
108
+ }
109
+
110
+ const [head, ...rest] = queueFor(sid)
111
+
112
+ if (!head) {
113
+ return null
114
+ }
115
+
116
+ writeSession(sid, rest)
117
+
118
+ return head
119
+ }
120
+
121
+ export const removeQueuedPrompt = (key: string | null | undefined, id: string): boolean => {
122
+ const sid = sidOf(key)
123
+
124
+ if (!sid) {
125
+ return false
126
+ }
127
+
128
+ const queue = queueFor(sid)
129
+ const next = queue.filter(e => e.id !== id)
130
+
131
+ if (next.length === queue.length) {
132
+ return false
133
+ }
134
+
135
+ writeSession(sid, next)
136
+
137
+ return true
138
+ }
139
+
140
+ export const promoteQueuedPrompt = (key: string | null | undefined, id: string): boolean => {
141
+ const sid = sidOf(key)
142
+
143
+ if (!sid) {
144
+ return false
145
+ }
146
+
147
+ const queue = queueFor(sid)
148
+ const index = queue.findIndex(e => e.id === id)
149
+
150
+ if (index <= 0) {
151
+ return false
152
+ }
153
+
154
+ const entry = queue[index]!
155
+ writeSession(sid, [entry, ...queue.slice(0, index), ...queue.slice(index + 1)])
156
+
157
+ return true
158
+ }
159
+
160
+ export const updateQueuedPrompt = (
161
+ key: string | null | undefined,
162
+ id: string,
163
+ update: { text: string; attachments?: ComposerAttachment[] }
164
+ ): boolean => {
165
+ const sid = sidOf(key)
166
+
167
+ if (!sid) {
168
+ return false
169
+ }
170
+
171
+ const queue = queueFor(sid)
172
+ let changed = false
173
+
174
+ const next = queue.map(entry => {
175
+ if (entry.id !== id) {
176
+ return entry
177
+ }
178
+
179
+ const attachments = update.attachments ? cloneAttachments(update.attachments) : entry.attachments
180
+
181
+ if (entry.text === update.text && !update.attachments) {
182
+ return entry
183
+ }
184
+
185
+ changed = true
186
+
187
+ return { ...entry, text: update.text, attachments }
188
+ })
189
+
190
+ if (!changed) {
191
+ return false
192
+ }
193
+
194
+ writeSession(sid, next)
195
+
196
+ return true
197
+ }
198
+
199
+ export const updateQueuedPromptText = (key: string | null | undefined, id: string, text: string): boolean =>
200
+ updateQueuedPrompt(key, id, { text })
201
+
202
+ export const clearQueuedPrompts = (key: string | null | undefined) => {
203
+ const sid = sidOf(key)
204
+
205
+ if (!sid || !(sid in $queuedPromptsBySession.get())) {
206
+ return
207
+ }
208
+
209
+ writeSession(sid, [])
210
+ }
211
+
212
+ /** Inputs to {@link shouldAutoDrainOnSettle}, captured at a `busy` transition. */
213
+ export interface AutoDrainSettleInput {
214
+ wasBusy: boolean
215
+ isBusy: boolean
216
+ queueLength: number
217
+ }
218
+
219
+ /**
220
+ * Decide whether the composer should auto-drain the next queued prompt when a
221
+ * turn settles (busy transitions true → false).
222
+ *
223
+ * Queued turns always advance once the session is idle again, whether the turn
224
+ * finished naturally or the user interrupted it. Interrupting to reach a queued
225
+ * message is the whole point of the queue, so we never suppress the drain. The
226
+ * gateway guarantees a settle (message.complete + session.info running:false)
227
+ * even after an interrupt, so this single edge reliably advances the queue. To
228
+ * cancel queued turns the user deletes them from the panel.
229
+ */
230
+ export const shouldAutoDrainOnSettle = (params: AutoDrainSettleInput): boolean => {
231
+ const { isBusy, queueLength, wasBusy } = params
232
+
233
+ // Only react to a true → false transition; ignore steady state and entry.
234
+ if (isBusy || !wasBusy) {
235
+ return false
236
+ }
237
+
238
+ return queueLength > 0
239
+ }
@@ -0,0 +1,99 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest'
2
+
3
+ import { $backgroundStatusBySession, dismissBackgroundProcess, reconcileBackgroundProcesses } from './composer-status'
4
+
5
+ const SID = 'sess-1'
6
+
7
+ const running = (id: string, command = `cmd ${id}`) => ({ command, session_id: id, status: 'running' })
8
+
9
+ const exited = (id: string, exit_code = 0, command = `cmd ${id}`) => ({
10
+ command,
11
+ exit_code,
12
+ session_id: id,
13
+ status: 'exited'
14
+ })
15
+
16
+ const items = () => $backgroundStatusBySession.get()[SID] ?? []
17
+
18
+ describe('reconcileBackgroundProcesses', () => {
19
+ beforeEach(() => {
20
+ $backgroundStatusBySession.set({})
21
+ })
22
+
23
+ it('maps registry entries to status items', () => {
24
+ reconcileBackgroundProcesses(SID, [running('a'), exited('b', 0), exited('c', 1)])
25
+
26
+ expect(items().map(i => [i.id, i.state])).toEqual([
27
+ ['a', 'running'],
28
+ ['b', 'done'],
29
+ ['c', 'failed']
30
+ ])
31
+ expect(items()[2]!.exitCode).toBe(1)
32
+ })
33
+
34
+ it('keeps row order stable when a process flips state or the snapshot reorders', () => {
35
+ reconcileBackgroundProcesses(SID, [running('a'), running('b')])
36
+ // Snapshot arrives reordered AND `a` has exited — rows must not move.
37
+ reconcileBackgroundProcesses(SID, [running('b'), exited('a', 0)])
38
+
39
+ expect(items().map(i => [i.id, i.state])).toEqual([
40
+ ['a', 'done'],
41
+ ['b', 'running']
42
+ ])
43
+ })
44
+
45
+ it('appends new processes after existing rows', () => {
46
+ reconcileBackgroundProcesses(SID, [running('a')])
47
+ reconcileBackgroundProcesses(SID, [running('b'), running('a')])
48
+
49
+ expect(items().map(i => i.id)).toEqual(['a', 'b'])
50
+ })
51
+
52
+ it('preserves object identity for unchanged rows (memo stability)', () => {
53
+ reconcileBackgroundProcesses(SID, [running('a'), running('b')])
54
+ const [a1] = items()
55
+
56
+ reconcileBackgroundProcesses(SID, [running('a'), exited('b', 0)])
57
+ const [a2, b2] = items()
58
+
59
+ expect(a2).toBe(a1)
60
+ expect(b2!.state).toBe('done')
61
+ })
62
+
63
+ it('is a no-op store write when nothing changed', () => {
64
+ reconcileBackgroundProcesses(SID, [running('a')])
65
+ const before = $backgroundStatusBySession.get()
66
+
67
+ reconcileBackgroundProcesses(SID, [running('a')])
68
+
69
+ expect($backgroundStatusBySession.get()).toBe(before)
70
+ })
71
+
72
+ it('never resurrects a dismissed process while the registry still reports it', () => {
73
+ reconcileBackgroundProcesses(SID, [exited('a', 0), running('b')])
74
+ dismissBackgroundProcess(SID, 'a')
75
+
76
+ reconcileBackgroundProcesses(SID, [exited('a', 0), running('b')])
77
+
78
+ expect(items().map(i => i.id)).toEqual(['b'])
79
+ })
80
+
81
+ it('forgets a dismissal once the registry prunes the process', () => {
82
+ reconcileBackgroundProcesses(SID, [exited('a', 0)])
83
+ dismissBackgroundProcess(SID, 'a')
84
+
85
+ // Registry pruned it…
86
+ reconcileBackgroundProcesses(SID, [])
87
+ // …so a future process reusing the id (new spawn) shows again.
88
+ reconcileBackgroundProcesses(SID, [running('a')])
89
+
90
+ expect(items().map(i => i.id)).toEqual(['a'])
91
+ })
92
+
93
+ it('drops the session key entirely when the last row goes away', () => {
94
+ reconcileBackgroundProcesses(SID, [running('a')])
95
+ reconcileBackgroundProcesses(SID, [])
96
+
97
+ expect($backgroundStatusBySession.get()).toEqual({})
98
+ })
99
+ })