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,277 @@
1
+ import { atom, computed } from 'nanostores'
2
+
3
+ import { translateNow } from '@/i18n'
4
+ import type { TodoItem, TodoStatus } from '@/lib/todos'
5
+
6
+ import { $gateway } from './gateway'
7
+ import { dispatchNativeNotification } from './native-notifications'
8
+ import { $subagentsBySession, type SubagentProgress } from './subagents'
9
+ import { $todosBySession } from './todos'
10
+
11
+ /** Composer status stack feed — merged todos, subagents, background per session. */
12
+ export type StatusItemState = 'done' | 'failed' | 'running'
13
+ export type StatusItemType = 'background' | 'subagent' | 'todo'
14
+
15
+ export interface ComposerStatusItem {
16
+ /** background: non-zero exit shown inline when failed. */
17
+ exitCode?: number
18
+ /** subagent: active tool label shown on the right. */
19
+ currentTool?: string
20
+ id: string
21
+ /** background process: captured stdout/stderr tail for the inline viewer. */
22
+ output?: string
23
+ /** subagent: its own stored session id — row click opens that session window
24
+ * (livestreamed by the gateway's child-session mirror). */
25
+ sessionId?: string
26
+ state: StatusItemState
27
+ title: string
28
+ /** todo: the full four-state status driving the row's checkmark glyph. */
29
+ todoStatus?: TodoStatus
30
+ type: StatusItemType
31
+ }
32
+
33
+ // Writable source for background work, synced from the gateway's process
34
+ // registry (`terminal(background=true)` spawns) via `process.list`.
35
+ export const $backgroundStatusBySession = atom<Record<string, ComposerStatusItem[]>>({})
36
+
37
+ // Rows the user X-ed away. The registry keeps finished processes around for a
38
+ // while, so without this every refresh would resurrect a dismissed row.
39
+ const dismissedBySession = new Map<string, Set<string>>()
40
+
41
+ const subToItem = (s: SubagentProgress): ComposerStatusItem => ({
42
+ currentTool: s.currentTool,
43
+ id: s.id,
44
+ sessionId: s.sessionId,
45
+ state: 'running',
46
+ title: s.goal,
47
+ type: 'subagent'
48
+ })
49
+
50
+ const todoToItem = (t: TodoItem): ComposerStatusItem => ({
51
+ id: `todo:${t.id}`,
52
+ state: t.status === 'in_progress' ? 'running' : 'done',
53
+ title: t.content,
54
+ todoStatus: t.status,
55
+ type: 'todo'
56
+ })
57
+
58
+ // The single thing the stack reads: a typed, merged item list per session.
59
+ export const $statusItemsBySession = computed(
60
+ [$subagentsBySession, $backgroundStatusBySession, $todosBySession],
61
+ (subs, background, todos) => {
62
+ const out: Record<string, ComposerStatusItem[]> = {}
63
+
64
+ const push = (sid: string, items: ComposerStatusItem[]) => {
65
+ if (items.length > 0) {
66
+ out[sid] = out[sid] ? [...out[sid], ...items] : items
67
+ }
68
+ }
69
+
70
+ for (const [sid, list] of Object.entries(todos)) {
71
+ push(sid, list.map(todoToItem))
72
+ }
73
+
74
+ for (const [sid, list] of Object.entries(subs)) {
75
+ push(sid, list.filter(s => s.status === 'running' || s.status === 'queued').map(subToItem))
76
+ }
77
+
78
+ for (const [sid, list] of Object.entries(background)) {
79
+ push(sid, list)
80
+ }
81
+
82
+ return out
83
+ }
84
+ )
85
+
86
+ // Fixed render order for the groups in the stack (top → bottom, above queue).
87
+ const TYPE_ORDER: readonly StatusItemType[] = ['todo', 'subagent', 'background']
88
+
89
+ export interface StatusGroup {
90
+ items: ComposerStatusItem[]
91
+ type: StatusItemType
92
+ }
93
+
94
+ export function groupStatusItems(items: readonly ComposerStatusItem[]): StatusGroup[] {
95
+ const byType = new Map<StatusItemType, ComposerStatusItem[]>()
96
+
97
+ for (const item of items) {
98
+ const list = byType.get(item.type)
99
+
100
+ if (list) {
101
+ list.push(item)
102
+ } else {
103
+ byType.set(item.type, [item])
104
+ }
105
+ }
106
+
107
+ return TYPE_ORDER.filter(type => byType.has(type)).map(type => ({ items: byType.get(type)!, type }))
108
+ }
109
+
110
+ const writeBackground = (sid: string, items: ComposerStatusItem[]) => {
111
+ const current = $backgroundStatusBySession.get()
112
+ const next = { ...current }
113
+
114
+ if (items.length > 0) {
115
+ next[sid] = items
116
+ } else {
117
+ delete next[sid]
118
+ }
119
+
120
+ $backgroundStatusBySession.set(next)
121
+ }
122
+
123
+ // `tui_gateway` process.list entry (tools/process_registry.list_sessions + output_tail).
124
+ interface GatewayProcessEntry {
125
+ command?: string
126
+ exit_code?: number
127
+ output_tail?: string
128
+ session_id?: string
129
+ status?: string
130
+ }
131
+
132
+ const toBackgroundItem = (proc: GatewayProcessEntry): ComposerStatusItem => {
133
+ const exited = proc.status === 'exited'
134
+ const exitCode = typeof proc.exit_code === 'number' ? proc.exit_code : undefined
135
+
136
+ return {
137
+ exitCode,
138
+ id: proc.session_id ?? '',
139
+ output: proc.output_tail || undefined,
140
+ state: exited ? (exitCode ? 'failed' : 'done') : 'running',
141
+ title: (proc.command ?? '').split('\n')[0]!.trim() || 'background process',
142
+ type: 'background'
143
+ }
144
+ }
145
+
146
+ const sameItem = (a: ComposerStatusItem, b: ComposerStatusItem) =>
147
+ a.state === b.state && a.title === b.title && a.output === b.output && a.exitCode === b.exitCode
148
+
149
+ /**
150
+ * Layout-stable sync of the registry snapshot into the store: existing rows
151
+ * keep their position (status flips happen in place, never reorder), new
152
+ * processes append, dismissed ids stay gone, and unchanged rows keep their
153
+ * object identity so memoised rows skip re-rendering.
154
+ */
155
+ export function reconcileBackgroundProcesses(sid: string, procs: GatewayProcessEntry[]) {
156
+ const dismissed = dismissedBySession.get(sid)
157
+
158
+ const fresh = new Map(
159
+ procs
160
+ .filter(proc => proc.session_id && !dismissed?.has(proc.session_id))
161
+ .map(proc => [proc.session_id!, toBackgroundItem(proc)])
162
+ )
163
+
164
+ const prev = $backgroundStatusBySession.get()[sid] ?? []
165
+
166
+ // running → exited since the last snapshot = a background process just finished.
167
+ const prevState = new Map(prev.map(item => [item.id, item.state]))
168
+
169
+ for (const [id, item] of fresh) {
170
+ if (item.state !== 'running' && prevState.get(id) === 'running') {
171
+ dispatchNativeNotification({
172
+ body: item.title,
173
+ kind: 'backgroundDone',
174
+ sessionId: sid,
175
+ title: translateNow(
176
+ item.state === 'failed'
177
+ ? 'notifications.native.backgroundFailedTitle'
178
+ : 'notifications.native.backgroundDoneTitle'
179
+ )
180
+ })
181
+ }
182
+ }
183
+
184
+ const kept = prev.flatMap(old => {
185
+ const next = fresh.get(old.id)
186
+ fresh.delete(old.id)
187
+
188
+ return next ? [sameItem(old, next) ? old : next] : []
189
+ })
190
+
191
+ const next = [...kept, ...fresh.values()]
192
+
193
+ // Dismissals only need remembering while the registry still reports the id.
194
+ if (dismissed) {
195
+ const reported = new Set(procs.map(proc => proc.session_id))
196
+
197
+ for (const id of dismissed) {
198
+ if (!reported.has(id)) {
199
+ dismissed.delete(id)
200
+ }
201
+ }
202
+ }
203
+
204
+ if (next.length === prev.length && next.every((item, i) => item === prev[i])) {
205
+ return
206
+ }
207
+
208
+ writeBackground(sid, next)
209
+ }
210
+
211
+ /** Pull the session's live process snapshot from the gateway. */
212
+ export async function refreshBackgroundProcesses(sid: string): Promise<void> {
213
+ const gateway = $gateway.get()
214
+
215
+ if (!sid || !gateway) {
216
+ return
217
+ }
218
+
219
+ try {
220
+ const result = await gateway.request<{ processes?: GatewayProcessEntry[] }>('process.list', { session_id: sid })
221
+
222
+ reconcileBackgroundProcesses(sid, result?.processes ?? [])
223
+ } catch {
224
+ // Transient socket loss — the next trigger (event or poll) retries.
225
+ }
226
+ }
227
+
228
+ /** X on a finished row: drop it now and keep it dropped across refreshes. */
229
+ export function dismissBackgroundProcess(sid: string, id: string) {
230
+ const dismissed = dismissedBySession.get(sid) ?? new Set<string>()
231
+ dismissed.add(id)
232
+ dismissedBySession.set(sid, dismissed)
233
+
234
+ const list = $backgroundStatusBySession.get()[sid] ?? []
235
+
236
+ writeBackground(
237
+ sid,
238
+ list.filter(item => item.id !== id)
239
+ )
240
+ }
241
+
242
+ /** X on a running row: kill the process for real, then drop the row. */
243
+ export function stopBackgroundProcess(sid: string, id: string) {
244
+ void $gateway
245
+ .get()
246
+ ?.request('process.kill', { process_id: id, session_id: sid })
247
+ .catch(() => undefined)
248
+ dismissBackgroundProcess(sid, id)
249
+ }
250
+
251
+ /**
252
+ * Rewind cleanup: a restore/edit discards the turns that spawned these
253
+ * processes, so they belong to an abandoned timeline. Kill the live ones and
254
+ * drop every row. Ids are marked dismissed so an in-flight `process.list` poll
255
+ * (kill is async) can't resurrect them; reconcile garbage-collects those once
256
+ * the registry stops reporting them.
257
+ */
258
+ export function resetSessionBackground(sid: string) {
259
+ if (!sid) {
260
+ return
261
+ }
262
+
263
+ const gateway = $gateway.get()
264
+ const list = $backgroundStatusBySession.get()[sid] ?? []
265
+ const dismissed = dismissedBySession.get(sid) ?? new Set<string>()
266
+
267
+ for (const item of list) {
268
+ dismissed.add(item.id)
269
+
270
+ if (item.state === 'running') {
271
+ void gateway?.request('process.kill', { process_id: item.id, session_id: sid }).catch(() => undefined)
272
+ }
273
+ }
274
+
275
+ dismissedBySession.set(sid, dismissed)
276
+ writeBackground(sid, [])
277
+ }
@@ -0,0 +1,106 @@
1
+ import { afterEach, describe, expect, it } from 'vitest'
2
+
3
+ import {
4
+ $composerAttachments,
5
+ addComposerAttachment,
6
+ clearSessionDraft,
7
+ type ComposerAttachment,
8
+ removeComposerAttachment,
9
+ SESSION_DRAFTS_STORAGE_KEY,
10
+ stashSessionDraft,
11
+ takeSessionDraft,
12
+ updateComposerAttachment
13
+ } from './composer'
14
+
15
+ function attachment(overrides: Partial<ComposerAttachment> & Pick<ComposerAttachment, 'id'>): ComposerAttachment {
16
+ return { kind: 'file', label: 'doc.pdf', ...overrides }
17
+ }
18
+
19
+ describe('updateComposerAttachment', () => {
20
+ afterEach(() => {
21
+ $composerAttachments.set([])
22
+ })
23
+
24
+ it('replaces an existing attachment in place', () => {
25
+ addComposerAttachment(attachment({ id: 'file:a', uploadState: 'uploading' }))
26
+
27
+ const updated = updateComposerAttachment(attachment({ id: 'file:a', attachedSessionId: 'sess-1' }))
28
+
29
+ expect(updated).toBe(true)
30
+ const current = $composerAttachments.get()
31
+ expect(current).toHaveLength(1)
32
+ expect(current[0]?.attachedSessionId).toBe('sess-1')
33
+ expect(current[0]?.uploadState).toBeUndefined()
34
+ })
35
+
36
+ it('does NOT resurrect an attachment the user removed mid-upload', () => {
37
+ // Drop → eager upload starts → user removes the chip → upload resolves.
38
+ // The late success must not re-add the removed attachment.
39
+ addComposerAttachment(attachment({ id: 'file:a', uploadState: 'uploading' }))
40
+ removeComposerAttachment('file:a')
41
+
42
+ const updated = updateComposerAttachment(attachment({ id: 'file:a', attachedSessionId: 'sess-1' }))
43
+
44
+ expect(updated).toBe(false)
45
+ expect($composerAttachments.get()).toHaveLength(0)
46
+ })
47
+ })
48
+
49
+ describe('session drafts', () => {
50
+ afterEach(() => {
51
+ for (const scope of ['session-a', 'session-b', null]) {
52
+ clearSessionDraft(scope)
53
+ }
54
+
55
+ window.localStorage.clear()
56
+ })
57
+
58
+ it('keeps drafts isolated per session scope', () => {
59
+ stashSessionDraft('session-a', 'draft a', [])
60
+ stashSessionDraft('session-b', 'draft b', [attachment({ id: 'image:b', kind: 'image' })])
61
+
62
+ expect(takeSessionDraft('session-a')).toEqual({ attachments: [], text: 'draft a' })
63
+ expect(takeSessionDraft('session-b').text).toBe('draft b')
64
+ expect(takeSessionDraft('session-b').attachments.map(a => a.id)).toEqual(['image:b'])
65
+ })
66
+
67
+ it('scopes the unsaved new-session draft separately from real sessions', () => {
68
+ stashSessionDraft(null, 'new chat draft', [])
69
+ stashSessionDraft('session-a', 'session draft', [])
70
+
71
+ expect(takeSessionDraft(null).text).toBe('new chat draft')
72
+ expect(takeSessionDraft(undefined).text).toBe('new chat draft')
73
+ expect(takeSessionDraft('session-a').text).toBe('session draft')
74
+ })
75
+
76
+ it('persists draft text (not attachments) to localStorage', () => {
77
+ stashSessionDraft('session-a', 'survives reload', [attachment({ id: 'file:a' })])
78
+
79
+ const persisted = JSON.parse(window.localStorage.getItem(SESSION_DRAFTS_STORAGE_KEY) ?? '{}') as Record<string, string>
80
+
81
+ expect(persisted['session-a']).toBe('survives reload')
82
+ })
83
+
84
+ it('evicts empty drafts instead of leaving stale entries behind', () => {
85
+ stashSessionDraft('session-a', 'saved', [])
86
+ stashSessionDraft('session-a', ' ', [])
87
+
88
+ expect(takeSessionDraft('session-a')).toEqual({ attachments: [], text: '' })
89
+ })
90
+
91
+ it('clears a stashed draft after an accepted submit', () => {
92
+ stashSessionDraft('session-a', 'sent prompt', [attachment({ id: 'file:a' })])
93
+ clearSessionDraft('session-a')
94
+
95
+ expect(takeSessionDraft('session-a')).toEqual({ attachments: [], text: '' })
96
+ })
97
+
98
+ it('returns clones so callers cannot mutate the stash', () => {
99
+ stashSessionDraft('session-a', 'draft', [attachment({ id: 'file:a' })])
100
+
101
+ const taken = takeSessionDraft('session-a')
102
+ taken.attachments[0]!.label = 'mutated'
103
+
104
+ expect(takeSessionDraft('session-a').attachments[0]?.label).toBe('doc.pdf')
105
+ })
106
+ })
@@ -0,0 +1,184 @@
1
+ import { atom } from 'nanostores'
2
+
3
+ import { triggerHaptic } from '@/lib/haptics'
4
+
5
+ export interface ComposerAttachment {
6
+ id: string
7
+ kind: 'image' | 'file' | 'folder' | 'terminal' | 'url'
8
+ label: string
9
+ detail?: string
10
+ refText?: string
11
+ previewUrl?: string
12
+ path?: string
13
+ attachedSessionId?: string
14
+ }
15
+
16
+ export const $composerDraft = atom('')
17
+ export const $composerAttachments = atom<ComposerAttachment[]>([])
18
+ export const $composerTerminalSelections = atom<Record<string, string>>({})
19
+
20
+ export function setComposerDraft(value: string) {
21
+ $composerDraft.set(value)
22
+ }
23
+
24
+ export function appendComposerDraft(value: string) {
25
+ const text = value.trim()
26
+
27
+ if (!text) {
28
+ return
29
+ }
30
+
31
+ const current = $composerDraft.get()
32
+ const separator = current && !current.endsWith('\n') ? '\n\n' : ''
33
+
34
+ $composerDraft.set(`${current}${separator}${text}`)
35
+ }
36
+
37
+ export function appendComposerInline(value: string) {
38
+ const text = value.trim()
39
+
40
+ if (!text) {
41
+ return
42
+ }
43
+
44
+ const current = $composerDraft.get().trimEnd()
45
+ const separator = current ? ' ' : ''
46
+
47
+ $composerDraft.set(`${current}${separator}${text}`)
48
+ }
49
+
50
+ export function clearComposerDraft() {
51
+ $composerDraft.set('')
52
+ }
53
+
54
+ export function addComposerAttachment(attachment: ComposerAttachment) {
55
+ const previous = $composerAttachments.get()
56
+ const next = upsertAttachment(previous, attachment)
57
+ $composerAttachments.set(next)
58
+
59
+ if (next.length > previous.length && attachment.kind !== 'url') {
60
+ triggerHaptic('selection')
61
+ }
62
+ }
63
+
64
+ export function removeComposerAttachment(id: string): ComposerAttachment | null {
65
+ const current = $composerAttachments.get()
66
+ const removed = current.find(attachment => attachment.id === id) || null
67
+ $composerAttachments.set(current.filter(attachment => attachment.id !== id))
68
+
69
+ return removed
70
+ }
71
+
72
+ export function clearComposerAttachments() {
73
+ $composerAttachments.set([])
74
+ }
75
+
76
+ const TERMINAL_REF_RE = /@terminal:(`[^`\n]+`|"[^"\n]+"|'[^'\n]+'|\S+)/g
77
+
78
+ function unquoteRefValue(raw: string) {
79
+ const head = raw[0]
80
+ const tail = raw[raw.length - 1]
81
+ const quoted = (head === '`' && tail === '`') || (head === '"' && tail === '"') || (head === "'" && tail === "'")
82
+
83
+ return (quoted ? raw.slice(1, -1) : raw).replace(/[,.;!?]+$/, '').trim()
84
+ }
85
+
86
+ function terminalLabelsFromDraft(draft: string) {
87
+ const labels: string[] = []
88
+ const seen = new Set<string>()
89
+
90
+ for (const match of draft.matchAll(TERMINAL_REF_RE)) {
91
+ const label = unquoteRefValue(match[1] || '')
92
+
93
+ if (!label || seen.has(label)) {
94
+ continue
95
+ }
96
+
97
+ seen.add(label)
98
+ labels.push(label)
99
+ }
100
+
101
+ return labels
102
+ }
103
+
104
+ export function setComposerTerminalSelection(label: string, text: string) {
105
+ const nextLabel = label.trim()
106
+ const nextText = text.trim()
107
+
108
+ if (!nextLabel || !nextText) {
109
+ return
110
+ }
111
+
112
+ const current = $composerTerminalSelections.get()
113
+
114
+ if (current[nextLabel] === nextText) {
115
+ return
116
+ }
117
+
118
+ $composerTerminalSelections.set({
119
+ ...current,
120
+ [nextLabel]: nextText
121
+ })
122
+ }
123
+
124
+ export function reconcileComposerTerminalSelections(draft: string) {
125
+ const current = $composerTerminalSelections.get()
126
+ const labels = new Set(terminalLabelsFromDraft(draft))
127
+ let changed = false
128
+ const next: Record<string, string> = {}
129
+
130
+ for (const [label, text] of Object.entries(current)) {
131
+ if (!labels.has(label)) {
132
+ changed = true
133
+
134
+ continue
135
+ }
136
+
137
+ next[label] = text
138
+ }
139
+
140
+ if (changed) {
141
+ $composerTerminalSelections.set(next)
142
+ }
143
+ }
144
+
145
+ export function terminalContextBlocksFromDraft(draft: string) {
146
+ const labels = terminalLabelsFromDraft(draft)
147
+
148
+ if (labels.length === 0) {
149
+ return []
150
+ }
151
+
152
+ const selections = $composerTerminalSelections.get()
153
+
154
+ return labels.flatMap(label => {
155
+ const text = selections[label]?.trim()
156
+
157
+ if (!text) {
158
+ return []
159
+ }
160
+
161
+ return `\`\`\`terminal\n${text}\n\`\`\``
162
+ })
163
+ }
164
+
165
+ export function clearComposerTerminalSelections() {
166
+ if (Object.keys($composerTerminalSelections.get()).length === 0) {
167
+ return
168
+ }
169
+
170
+ $composerTerminalSelections.set({})
171
+ }
172
+
173
+ function upsertAttachment(attachments: ComposerAttachment[], attachment: ComposerAttachment) {
174
+ const index = attachments.findIndex(item => item.id === attachment.id)
175
+
176
+ if (index < 0) {
177
+ return [...attachments, attachment]
178
+ }
179
+
180
+ const next = [...attachments]
181
+ next[index] = attachment
182
+
183
+ return next
184
+ }
@@ -0,0 +1,19 @@
1
+ import { atom } from 'nanostores'
2
+
3
+ import type { CronJob } from '@/types/nastech'
4
+
5
+ // Cron *jobs* (not run sessions) power the sidebar "Cron jobs" section. Listing
6
+ // the job — schedule, state, live next-run countdown — makes the job the
7
+ // first-class entity; its runs (sessions) resolve under it in the cron detail.
8
+ export const $cronJobs = atom<CronJob[]>([])
9
+ export const setCronJobs = (jobs: CronJob[]) => $cronJobs.set(jobs)
10
+
11
+ // In-place edit so the cron overlay's mutations (create/edit/delete/pause/…)
12
+ // land in the same atom the sidebar renders — no stale list until the next poll.
13
+ export const updateCronJobs = (fn: (jobs: CronJob[]) => CronJob[]) => $cronJobs.set(fn($cronJobs.get()))
14
+
15
+ // One-shot focus target: clicking "Manage" on a job sets this, then opens the
16
+ // cron overlay, which reads it once to select + scroll to that job. Cleared
17
+ // after consumption so re-opening cron normally doesn't re-focus a stale job.
18
+ export const $cronFocusJobId = atom<null | string>(null)
19
+ export const setCronFocusJobId = (id: null | string) => $cronFocusJobId.set(id)