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,57 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { type DroppedFile, partitionDroppedFiles } from './use-composer-actions'
4
+
5
+ // A Finder/Explorer drop carries a native File handle; an in-app drag (project
6
+ // tree, gutter line ref) is path-only. The split decides whether a drop becomes
7
+ // an inline @file: ref (in-app, workspace-relative, gateway-resolvable) or goes
8
+ // through the upload pipeline (OS drop — absolute local path a remote gateway
9
+ // can't read, plus image bytes for vision).
10
+ const osDrop = (path: string): DroppedFile => ({ file: new File(['x'], path.split('/').pop() || 'f'), path })
11
+ const inAppRef = (path: string, extra: Partial<DroppedFile> = {}): DroppedFile => ({ path, ...extra })
12
+
13
+ describe('partitionDroppedFiles', () => {
14
+ it('routes File-bearing OS drops to osDrops and path-only in-app drags to inAppRefs', () => {
15
+ const finderPdf = osDrop('/Users/mahmoud/Downloads/DEVIS_signed.pdf')
16
+ const projectFile = inAppRef('src/index.ts')
17
+
18
+ const { inAppRefs, osDrops } = partitionDroppedFiles([finderPdf, projectFile])
19
+
20
+ expect(osDrops).toEqual([finderPdf])
21
+ expect(inAppRefs).toEqual([projectFile])
22
+ })
23
+
24
+ it('treats an OS screenshot drop as an upload target (so it gets byte upload + vision)', () => {
25
+ const screenshot = osDrop('/var/folders/tmp/Screenshot 2026-06-09.png')
26
+
27
+ const { inAppRefs, osDrops } = partitionDroppedFiles([screenshot])
28
+
29
+ expect(osDrops).toEqual([screenshot])
30
+ expect(inAppRefs).toEqual([])
31
+ })
32
+
33
+ it('keeps gutter line-range drags inline (no File handle)', () => {
34
+ const lineRef = inAppRef('src/app.ts', { line: 10, lineEnd: 20 })
35
+
36
+ const { inAppRefs, osDrops } = partitionDroppedFiles([lineRef])
37
+
38
+ expect(osDrops).toEqual([])
39
+ expect(inAppRefs).toEqual([lineRef])
40
+ })
41
+
42
+ it('splits a mixed drop and preserves order within each group', () => {
43
+ const a = inAppRef('a.ts')
44
+ const b = osDrop('/abs/b.pdf')
45
+ const c = inAppRef('c.ts')
46
+ const d = osDrop('/abs/d.png')
47
+
48
+ const { inAppRefs, osDrops } = partitionDroppedFiles([a, b, c, d])
49
+
50
+ expect(inAppRefs).toEqual([a, c])
51
+ expect(osDrops).toEqual([b, d])
52
+ })
53
+
54
+ it('returns empty groups for an empty drop', () => {
55
+ expect(partitionDroppedFiles([])).toEqual({ inAppRefs: [], osDrops: [] })
56
+ })
57
+ })
@@ -0,0 +1,525 @@
1
+ import { useCallback } from 'react'
2
+
3
+ import { requestComposerFocus, requestComposerInsert } from '@/app/chat/composer/focus'
4
+ import { formatRefValue } from '@/components/assistant-ui/directive-text'
5
+ import { useI18n } from '@/i18n'
6
+ import { attachmentId, contextPath, pathLabel } from '@/lib/chat-runtime'
7
+ import {
8
+ addComposerAttachment,
9
+ type ComposerAttachment,
10
+ removeComposerAttachment,
11
+ setComposerTerminalSelection
12
+ } from '@/store/composer'
13
+ import { notify, notifyError } from '@/store/notifications'
14
+
15
+ import type { ImageDetachResponse } from '../../types'
16
+
17
+ const IMAGE_EXTENSION_PATTERN = /\.(png|jpe?g|gif|webp|bmp|tiff?|svg|ico)$/i
18
+
19
+ const BLOB_MIME_EXTENSION: Record<string, string> = {
20
+ 'image/bmp': '.bmp',
21
+ 'image/gif': '.gif',
22
+ 'image/jpeg': '.jpg',
23
+ 'image/png': '.png',
24
+ 'image/svg+xml': '.svg',
25
+ 'image/tiff': '.tiff',
26
+ 'image/webp': '.webp',
27
+ 'image/x-icon': '.ico'
28
+ }
29
+
30
+ function blobExtension(blob: Blob): string {
31
+ const mime = blob.type.split(';')[0]?.trim().toLowerCase()
32
+
33
+ return (mime && BLOB_MIME_EXTENSION[mime]) || '.png'
34
+ }
35
+
36
+ function isImagePath(filePath: string): boolean {
37
+ return IMAGE_EXTENSION_PATTERN.test(filePath)
38
+ }
39
+
40
+ export interface DroppedFile {
41
+ /** Browser-native File handle. Absent for in-app drags (e.g. project tree). */
42
+ file?: File
43
+ /** Absolute filesystem path. Empty when an OS drop didn't carry one. */
44
+ path: string
45
+ /** True if the entry is a directory. Currently only set by in-app drags. */
46
+ isDirectory?: boolean
47
+ /** First line number for in-app line-ref drags (source view gutter). */
48
+ line?: number
49
+ /** Last line number for line-range drags (`line..lineEnd` inclusive). */
50
+ lineEnd?: number
51
+ }
52
+
53
+ /** MIME emitted by in-app drag sources (project tree, gutter line numbers).
54
+ * Payload is JSON `{ path; isDirectory?; line?; lineEnd? }[]`. */
55
+ export const NASTECH_PATHS_MIME = 'application/x-NASTECH-paths'
56
+
57
+ /**
58
+ * Eagerly resolve files from a drop event into [File?, path, isDirectory?]
59
+ * triples. Internal NasTech sources (e.g. the project tree) ride on a custom
60
+ * MIME and produce path-only entries; OS drops produce File-bearing entries.
61
+ *
62
+ * Must be called synchronously from inside the drop handler — `DataTransfer`
63
+ * items are detached as soon as the handler returns, and `webUtils.getPathForFile`
64
+ * also requires the original (non-cloned) File reference.
65
+ */
66
+ export function extractDroppedFiles(transfer: DataTransfer): DroppedFile[] {
67
+ const result: DroppedFile[] = []
68
+ const seenPaths = new Set<string>()
69
+ const seenFiles = new Set<File>()
70
+ const getPath = window.NASTECHDesktop?.getPathForFile
71
+
72
+ // In-app drags first — they carry richer metadata (isDirectory) than the
73
+ // File-based fallback can provide, and produce no overlapping native files.
74
+ try {
75
+ const internalRaw = transfer.getData(NASTECH_PATHS_MIME)
76
+
77
+ if (internalRaw) {
78
+ const parsed = JSON.parse(internalRaw) as {
79
+ path?: unknown
80
+ isDirectory?: unknown
81
+ line?: unknown
82
+ lineEnd?: unknown
83
+ }[]
84
+
85
+ const positiveInt = (value: unknown) => (typeof value === 'number' && value > 0 ? Math.floor(value) : undefined)
86
+
87
+ for (const entry of parsed) {
88
+ if (!entry || typeof entry.path !== 'string' || !entry.path) {
89
+ continue
90
+ }
91
+
92
+ const line = positiveInt(entry.line)
93
+ const rawEnd = positiveInt(entry.lineEnd)
94
+ const lineEnd = line && rawEnd && rawEnd > line ? rawEnd : undefined
95
+ const dedupKey = line ? `${entry.path}:${line}-${lineEnd ?? line}` : entry.path
96
+
97
+ if (seenPaths.has(dedupKey)) {
98
+ continue
99
+ }
100
+
101
+ seenPaths.add(dedupKey)
102
+ result.push({ isDirectory: entry.isDirectory === true, line, lineEnd, path: entry.path })
103
+ }
104
+ }
105
+ } catch {
106
+ // Malformed payload — fall through to native files.
107
+ }
108
+
109
+ const fileList = transfer.files
110
+
111
+ if (fileList) {
112
+ for (let i = 0; i < fileList.length; i += 1) {
113
+ const file = fileList.item(i)
114
+
115
+ if (!file || seenFiles.has(file)) {
116
+ continue
117
+ }
118
+
119
+ seenFiles.add(file)
120
+ let path = ''
121
+
122
+ if (getPath) {
123
+ try {
124
+ path = getPath(file) || ''
125
+ } catch {
126
+ path = ''
127
+ }
128
+ }
129
+
130
+ if (path && seenPaths.has(path)) {
131
+ continue
132
+ }
133
+
134
+ if (path) {
135
+ seenPaths.add(path)
136
+ }
137
+
138
+ result.push({ file, path })
139
+ }
140
+ }
141
+
142
+ const items = transfer.items
143
+
144
+ if (items) {
145
+ for (let i = 0; i < items.length; i += 1) {
146
+ const item = items[i]
147
+
148
+ if (!item || item.kind !== 'file') {
149
+ continue
150
+ }
151
+
152
+ const file = item.getAsFile()
153
+
154
+ if (!file || seenFiles.has(file)) {
155
+ continue
156
+ }
157
+
158
+ seenFiles.add(file)
159
+ let path = ''
160
+
161
+ if (getPath) {
162
+ try {
163
+ path = getPath(file) || ''
164
+ } catch {
165
+ path = ''
166
+ }
167
+ }
168
+
169
+ if (path && seenPaths.has(path)) {
170
+ continue
171
+ }
172
+
173
+ if (path) {
174
+ seenPaths.add(path)
175
+ }
176
+
177
+ result.push({ file, path })
178
+ }
179
+ }
180
+
181
+ return result
182
+ }
183
+
184
+ interface ComposerActionsOptions {
185
+ activeSessionId: string | null
186
+ currentCwd: string
187
+ requestGateway: <T>(method: string, params?: Record<string, unknown>) => Promise<T>
188
+ }
189
+
190
+ /** Add to the main composer and focus it. All sidebar/picker/drop attach paths funnel through here. */
191
+ const attachToMain = (attachment: ComposerAttachment) => {
192
+ addComposerAttachment(attachment)
193
+ requestComposerFocus('main')
194
+ }
195
+
196
+ export function useComposerActions({ activeSessionId, currentCwd, requestGateway }: ComposerActionsOptions) {
197
+ const { t } = useI18n()
198
+ const copy = t.desktop
199
+ const addTextToDraft = useCallback((text: string) => {
200
+ requestComposerInsert(text, { mode: 'block' })
201
+ }, [copy.imagePreviewFailed])
202
+
203
+ const addTerminalSelectionAttachment = useCallback((text: string, label = 'selection') => {
204
+ const trimmed = text.trim()
205
+ const normalizedLabel = label.trim() || 'selection'
206
+ const refText = `@terminal:${formatRefValue(normalizedLabel)}`
207
+
208
+ if (!trimmed) {
209
+ return
210
+ }
211
+
212
+ setComposerTerminalSelection(normalizedLabel, trimmed)
213
+ requestComposerInsert(refText, { mode: 'inline' })
214
+ }, [])
215
+
216
+ const addContextRefAttachment = useCallback((refText: string, label?: string, detail?: string) => {
217
+ const kind: ComposerAttachment['kind'] = refText.startsWith('@folder:')
218
+ ? 'folder'
219
+ : refText.startsWith('@url:')
220
+ ? 'url'
221
+ : 'file'
222
+
223
+ attachToMain({
224
+ id: attachmentId(kind, refText),
225
+ kind,
226
+ label: label || refText.replace(/^@(file|folder|url):/, ''),
227
+ detail,
228
+ refText
229
+ })
230
+ }, [])
231
+
232
+ const pickContextPaths = useCallback(
233
+ async (kind: 'file' | 'folder') => {
234
+ const paths = await window.NASTECHDesktop?.selectPaths({
235
+ title: kind === 'file' ? 'Add files as context' : 'Add folders as context',
236
+ defaultPath: currentCwd || undefined,
237
+ directories: kind === 'folder'
238
+ })
239
+
240
+ if (!paths?.length) {
241
+ return
242
+ }
243
+
244
+ for (const path of paths) {
245
+ const rel = contextPath(path, currentCwd)
246
+
247
+ attachToMain({
248
+ id: attachmentId(kind, rel),
249
+ kind,
250
+ label: pathLabel(path),
251
+ detail: rel,
252
+ refText: `@${kind}:${formatRefValue(rel)}`,
253
+ path
254
+ })
255
+ }
256
+ },
257
+ [currentCwd]
258
+ )
259
+
260
+ const attachContextFilePath = useCallback(
261
+ (filePath: string) => {
262
+ if (!filePath) {
263
+ return false
264
+ }
265
+
266
+ const rel = contextPath(filePath, currentCwd)
267
+
268
+ attachToMain({
269
+ id: attachmentId('file', rel),
270
+ kind: 'file',
271
+ label: pathLabel(filePath),
272
+ detail: rel,
273
+ refText: `@file:${formatRefValue(rel)}`,
274
+ path: filePath
275
+ })
276
+
277
+ return true
278
+ },
279
+ [currentCwd]
280
+ )
281
+
282
+ const attachImagePath = useCallback(async (filePath: string) => {
283
+ if (!filePath) {
284
+ return false
285
+ }
286
+
287
+ const baseAttachment: ComposerAttachment = {
288
+ id: attachmentId('image', filePath),
289
+ kind: 'image',
290
+ label: pathLabel(filePath),
291
+ detail: filePath,
292
+ path: filePath
293
+ }
294
+
295
+ attachToMain(baseAttachment)
296
+
297
+ try {
298
+ const previewUrl = await window.NASTECHDesktop?.readFileDataUrl(filePath)
299
+
300
+ if (previewUrl) {
301
+ addComposerAttachment({ ...baseAttachment, previewUrl })
302
+ }
303
+
304
+ return true
305
+ } catch (err) {
306
+ notifyError(err, copy.imagePreviewFailed)
307
+
308
+ return true
309
+ }
310
+ }, [])
311
+
312
+ const attachImageBlob = useCallback(
313
+ async (blob: Blob) => {
314
+ if (blob.size === 0) {
315
+ return false
316
+ }
317
+
318
+ if (blob.type && !blob.type.startsWith('image/')) {
319
+ return false
320
+ }
321
+
322
+ try {
323
+ const buffer = await blob.arrayBuffer()
324
+ const data = new Uint8Array(buffer)
325
+ const savedPath = await window.NASTECHDesktop?.saveImageBuffer(data, blobExtension(blob))
326
+
327
+ if (!savedPath) {
328
+ notify({ kind: 'error', title: copy.imageAttach, message: copy.imageWriteFailed })
329
+
330
+ return false
331
+ }
332
+
333
+ return attachImagePath(savedPath)
334
+ } catch (err) {
335
+ notifyError(err, copy.imageAttachFailed)
336
+
337
+ return false
338
+ }
339
+ },
340
+ [attachImagePath, copy.imageAttach, copy.imageAttachFailed, copy.imageWriteFailed]
341
+ )
342
+
343
+ const pickImages = useCallback(async () => {
344
+ const paths = await window.NASTECHDesktop?.selectPaths({
345
+ title: copy.attachImages,
346
+ defaultPath: currentCwd || undefined,
347
+ filters: [
348
+ {
349
+ name: t.composer.images,
350
+ extensions: ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'tiff']
351
+ }
352
+ ]
353
+ })
354
+
355
+ if (!paths?.length) {
356
+ return
357
+ }
358
+
359
+ for (const path of paths) {
360
+ await attachImagePath(path)
361
+ }
362
+ }, [attachImagePath, copy.attachImages, currentCwd, t.composer.images])
363
+
364
+ const pasteClipboardImage = useCallback(async () => {
365
+ try {
366
+ const path = await window.NASTECHDesktop?.saveClipboardImage()
367
+
368
+ if (!path) {
369
+ notify({
370
+ kind: 'warning',
371
+ title: copy.clipboard,
372
+ message: copy.noClipboardImage
373
+ })
374
+
375
+ return
376
+ }
377
+
378
+ await attachImagePath(path)
379
+ } catch (err) {
380
+ notifyError(err, copy.clipboardPasteFailed)
381
+ }
382
+ }, [attachImagePath, copy.clipboard, copy.clipboardPasteFailed, copy.noClipboardImage])
383
+
384
+ const attachContextFolderPath = useCallback(
385
+ (folderPath: string) => {
386
+ if (!folderPath) {
387
+ return false
388
+ }
389
+
390
+ const rel = contextPath(folderPath, currentCwd)
391
+
392
+ attachToMain({
393
+ id: attachmentId('folder', rel),
394
+ kind: 'folder',
395
+ label: pathLabel(folderPath),
396
+ detail: rel,
397
+ refText: `@folder:${formatRefValue(rel)}`,
398
+ path: folderPath
399
+ })
400
+
401
+ return true
402
+ },
403
+ [currentCwd]
404
+ )
405
+
406
+ const attachDroppedItems = useCallback(
407
+ async (candidates: DroppedFile[]) => {
408
+ if (candidates.length === 0) {
409
+ return false
410
+ }
411
+
412
+ let attached = false
413
+ let lastFailure: string | null = null
414
+
415
+ for (const candidate of candidates) {
416
+ const { file, isDirectory, path: knownPath } = candidate
417
+
418
+ // Path-only entry (in-app drag from the file browser tree, etc.).
419
+ if (!file) {
420
+ if (isDirectory) {
421
+ if (knownPath && attachContextFolderPath(knownPath)) {
422
+ attached = true
423
+
424
+ continue
425
+ }
426
+
427
+ lastFailure = `Could not attach folder ${knownPath || ''}`
428
+
429
+ continue
430
+ }
431
+
432
+ if (knownPath && isImagePath(knownPath)) {
433
+ if (await attachImagePath(knownPath)) {
434
+ attached = true
435
+
436
+ continue
437
+ }
438
+
439
+ lastFailure = `Could not attach ${knownPath}`
440
+
441
+ continue
442
+ }
443
+
444
+ if (knownPath && attachContextFilePath(knownPath)) {
445
+ attached = true
446
+
447
+ continue
448
+ }
449
+
450
+ lastFailure = `Could not attach ${knownPath || 'file'}`
451
+
452
+ continue
453
+ }
454
+
455
+ const fallbackPath =
456
+ !knownPath && window.NASTECHDesktop?.getPathForFile ? window.NASTECHDesktop.getPathForFile(file) : ''
457
+
458
+ const filePath = knownPath || fallbackPath || ''
459
+ const isImage = file.type.startsWith('image/') || isImagePath(file.name) || (filePath && isImagePath(filePath))
460
+
461
+ if (isImage) {
462
+ if ((filePath && (await attachImagePath(filePath))) || (await attachImageBlob(file))) {
463
+ attached = true
464
+
465
+ continue
466
+ }
467
+
468
+ lastFailure = `Could not attach ${file.name || 'image'}`
469
+
470
+ continue
471
+ }
472
+
473
+ if (filePath && attachContextFilePath(filePath)) {
474
+ attached = true
475
+
476
+ continue
477
+ }
478
+
479
+ lastFailure = `Could not attach ${file.name || 'file'}`
480
+ }
481
+
482
+ if (!attached && lastFailure) {
483
+ notify({ kind: 'warning', title: copy.dropFiles, message: lastFailure })
484
+ }
485
+
486
+ return attached
487
+ },
488
+ [attachContextFilePath, attachContextFolderPath, attachImageBlob, attachImagePath, copy.dropFiles]
489
+ )
490
+
491
+ const removeAttachment = useCallback(
492
+ async (id: string) => {
493
+ const removed = removeComposerAttachment(id)
494
+
495
+ if (
496
+ removed?.kind === 'image' &&
497
+ removed.path &&
498
+ activeSessionId &&
499
+ removed.attachedSessionId &&
500
+ removed.attachedSessionId === activeSessionId
501
+ ) {
502
+ await requestGateway<ImageDetachResponse>('image.detach', {
503
+ session_id: activeSessionId,
504
+ path: removed.path
505
+ }).catch(() => undefined)
506
+ }
507
+ },
508
+ [activeSessionId, requestGateway]
509
+ )
510
+
511
+ return {
512
+ addContextRefAttachment,
513
+ addTerminalSelectionAttachment,
514
+ addTextToDraft,
515
+ attachContextFilePath,
516
+ attachContextFolderPath,
517
+ attachDroppedItems,
518
+ attachImageBlob,
519
+ attachImagePath,
520
+ pasteClipboardImage,
521
+ pickContextPaths,
522
+ pickImages,
523
+ removeAttachment
524
+ }
525
+ }
@@ -0,0 +1,118 @@
1
+ import { type DragEvent as ReactDragEvent, useCallback, useRef, useState } from 'react'
2
+
3
+ import {
4
+ dragHasAttachments,
5
+ dragHasSession,
6
+ readSessionDrag,
7
+ type SessionDragPayload
8
+ } from '@/app/chat/composer/inline-refs'
9
+
10
+ import { type DroppedFile, extractDroppedFiles, NASTECH_PATHS_MIME } from './use-composer-actions'
11
+
12
+ export type DragKind = 'files' | 'session' | null
13
+
14
+ const dragKindOf = (event: ReactDragEvent): DragKind => {
15
+ if (dragHasSession(event.dataTransfer)) {
16
+ return 'session'
17
+ }
18
+
19
+ if (dragHasAttachments(event.dataTransfer, NASTECH_PATHS_MIME)) {
20
+ return 'files'
21
+ }
22
+
23
+ return null
24
+ }
25
+
26
+ interface FileDropZoneOptions {
27
+ /** When false the zone ignores drags entirely. */
28
+ enabled?: boolean
29
+ onDropFiles: (files: DroppedFile[]) => void
30
+ onDropSession?: (session: SessionDragPayload) => void
31
+ }
32
+
33
+ /**
34
+ * "Drop anywhere in this region" affordance for files *and* in-app session
35
+ * links. An enter/leave depth counter keeps nested children from flickering the
36
+ * active state; `onDropCapture` clears it even when a nested target (the
37
+ * composer) handles the drop and stops propagation before our bubble-phase
38
+ * `onDrop` would fire.
39
+ *
40
+ * Spread `dropHandlers` onto the container; render an overlay off `dragKind`.
41
+ */
42
+ export function useFileDropZone({ enabled = true, onDropFiles, onDropSession }: FileDropZoneOptions) {
43
+ const [dragKind, setDragKind] = useState<DragKind>(null)
44
+ const depth = useRef(0)
45
+
46
+ const reset = useCallback(() => {
47
+ depth.current = 0
48
+ setDragKind(null)
49
+ }, [])
50
+
51
+ const onDragEnter = useCallback(
52
+ (event: ReactDragEvent) => {
53
+ const kind = enabled ? dragKindOf(event) : null
54
+
55
+ if (!kind) {
56
+ return
57
+ }
58
+
59
+ event.preventDefault()
60
+ depth.current += 1
61
+ setDragKind(kind)
62
+ },
63
+ [enabled]
64
+ )
65
+
66
+ const onDragOver = useCallback(
67
+ (event: ReactDragEvent) => {
68
+ if (!enabled || !dragKindOf(event)) {
69
+ return
70
+ }
71
+
72
+ event.preventDefault()
73
+ event.dataTransfer.dropEffect = 'copy'
74
+ },
75
+ [enabled]
76
+ )
77
+
78
+ const onDragLeave = useCallback(() => {
79
+ if (enabled && --depth.current <= 0) {
80
+ reset()
81
+ }
82
+ }, [enabled, reset])
83
+
84
+ const onDrop = useCallback(
85
+ (event: ReactDragEvent) => {
86
+ const kind = enabled ? dragKindOf(event) : null
87
+
88
+ if (!kind) {
89
+ return
90
+ }
91
+
92
+ event.preventDefault()
93
+ reset()
94
+
95
+ if (kind === 'session') {
96
+ const session = readSessionDrag(event.dataTransfer)
97
+
98
+ if (session) {
99
+ onDropSession?.(session)
100
+ }
101
+
102
+ return
103
+ }
104
+
105
+ const files = extractDroppedFiles(event.dataTransfer)
106
+
107
+ if (files.length) {
108
+ onDropFiles(files)
109
+ }
110
+ },
111
+ [enabled, onDropFiles, onDropSession, reset]
112
+ )
113
+
114
+ return {
115
+ dragKind,
116
+ dropHandlers: { onDragEnter, onDragLeave, onDragOver, onDrop, onDropCapture: reset }
117
+ }
118
+ }