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,107 @@
1
+ 'use client'
2
+
3
+ import type { SyntaxHighlighterProps } from '@assistant-ui/react-streamdown'
4
+ import type { FC } from 'react'
5
+ import ShikiHighlighter from 'react-shiki'
6
+
7
+ import {
8
+ CodeCard,
9
+ CodeCardBody,
10
+ CodeCardHeader,
11
+ CodeCardIcon,
12
+ CodeCardSubtitle,
13
+ CodeCardTitle
14
+ } from '@/components/chat/code-card'
15
+ import { CopyButton } from '@/components/ui/copy-button'
16
+ import { useI18n } from '@/i18n'
17
+ import { codiconForLanguage, isLikelyProseCodeBlock, sanitizeLanguageTag } from '@/lib/markdown-code'
18
+
19
+ /**
20
+ * Streamdown's code adapter renders header + body as inline siblings, so we
21
+ * own the wrapping `<CodeCard>` here and neutralize the upstream
22
+ * `data-streamdown="code-block"` chrome from styles.css. Anything that wants
23
+ * a card-shaped code surface should compose `CodeCard*` directly.
24
+ *
25
+ * `react-shiki` full bundle so all `bundledLanguages` work; theme switches
26
+ * follow the document `color-scheme` via `defaultColor="light-dark()"`.
27
+ */
28
+ interface NasTechSyntaxHighlighterProps extends SyntaxHighlighterProps {
29
+ defer?: boolean
30
+ }
31
+
32
+ const SHIKI_THEME = { dark: 'github-dark-default', light: 'github-light-default' } as const
33
+
34
+ /**
35
+ * `github-light-default` colors comments `#6e7781` (~4.2:1 against the code
36
+ * card background) — borderline unreadable at our 11px code size, and worst of
37
+ * all for shell snippets where a single `#` turns the rest of the line into one
38
+ * long comment span. Remap light-mode comments to GitHub's darker muted gray
39
+ * (`#57606a`, ~6.4:1). Dark mode (`#8b949e`, ~6.1:1) already reads fine, so we
40
+ * leave it untouched. Keyed per theme name so the bump only applies in light.
41
+ */
42
+ const SHIKI_COLOR_REPLACEMENTS: Record<string, Record<string, string>> = {
43
+ 'github-light-default': { '#6e7781': '#57606a' }
44
+ }
45
+
46
+ export const SyntaxHighlighter: FC<NasTechSyntaxHighlighterProps> = ({
47
+ components: { Pre },
48
+ language,
49
+ code,
50
+ defer = false
51
+ }) => {
52
+ const { t } = useI18n()
53
+ const trimmed = (code ?? '').replace(/^\n+/, '').trimEnd()
54
+
55
+ // Streaming may hand us empty/incomplete fences — render nothing rather
56
+ // than a transient empty card.
57
+ if (!trimmed.trim()) {
58
+ return null
59
+ }
60
+
61
+ if (isLikelyProseCodeBlock(language, trimmed)) {
62
+ return <div className="aui-prose-fence whitespace-pre-wrap wrap-anywhere text-foreground">{trimmed}</div>
63
+ }
64
+
65
+ const cleanLanguage = sanitizeLanguageTag(language || '')
66
+ const label = cleanLanguage && cleanLanguage !== 'unknown' ? cleanLanguage : ''
67
+
68
+ return (
69
+ <CodeCard data-streaming={defer ? 'true' : undefined}>
70
+ <CodeCardHeader>
71
+ <CodeCardTitle>
72
+ <CodeCardIcon name={codiconForLanguage(label)} />
73
+ {t.assistant.tool.code}
74
+ {label && <CodeCardSubtitle> · {label}</CodeCardSubtitle>}
75
+ </CodeCardTitle>
76
+ <CopyButton
77
+ appearance="inline"
78
+ className="-my-1 -mr-1 h-5 px-1 opacity-55 hover:opacity-100"
79
+ iconClassName="size-2.5"
80
+ label={t.assistant.tool.copyCode}
81
+ showLabel={false}
82
+ text={trimmed}
83
+ />
84
+ </CodeCardHeader>
85
+ <CodeCardBody>
86
+ <Pre className="aui-shiki m-0 overflow-hidden bg-transparent p-0">
87
+ {defer ? (
88
+ <code className="block whitespace-pre">{trimmed}</code>
89
+ ) : (
90
+ <ShikiHighlighter
91
+ addDefaultStyles={false}
92
+ as="div"
93
+ colorReplacements={SHIKI_COLOR_REPLACEMENTS}
94
+ defaultColor="light-dark()"
95
+ delay={120}
96
+ language={language || 'text'}
97
+ showLanguage={false}
98
+ theme={SHIKI_THEME}
99
+ >
100
+ {trimmed}
101
+ </ShikiHighlighter>
102
+ )}
103
+ </Pre>
104
+ </CodeCardBody>
105
+ </CodeCard>
106
+ )
107
+ }
@@ -0,0 +1,70 @@
1
+ import { type ReactNode } from 'react'
2
+
3
+ import { cn } from '@/lib/utils'
4
+
5
+ interface StatusRowProps {
6
+ children: ReactNode
7
+ className?: string
8
+ /** Leading glyph slot (spinner / status dot / selection circle). */
9
+ leading?: ReactNode
10
+ /** Makes the whole row activatable (adds `cursor-pointer` + keyboard a11y).
11
+ * Trailing-slot buttons should `stopPropagation` so they don't also fire it. */
12
+ onActivate?: () => void
13
+ /** Right-aligned actions. Revealed on row hover/focus unless `trailingVisible`. */
14
+ trailing?: ReactNode
15
+ trailingVisible?: boolean
16
+ }
17
+
18
+ /**
19
+ * Shared row chrome for everything in the composer status stack — status items
20
+ * (subagents, background) AND queued prompts. Fixed height, a leading glyph
21
+ * slot, flexible content, and a trailing actions slot that reveals on hover.
22
+ * Hover background matches the session sidebar. Consumers fill the three slots;
23
+ * they never re-implement the row container.
24
+ */
25
+ export function StatusRow({
26
+ children,
27
+ className,
28
+ leading,
29
+ onActivate,
30
+ trailing,
31
+ trailingVisible = false
32
+ }: StatusRowProps) {
33
+ return (
34
+ <div
35
+ className={cn(
36
+ 'group/status-row flex min-h-6 items-center gap-2 rounded-md px-1.5 py-1 hover:bg-(--ui-row-hover-background)',
37
+ onActivate && 'cursor-pointer',
38
+ className
39
+ )}
40
+ onClick={onActivate}
41
+ onKeyDown={
42
+ onActivate
43
+ ? event => {
44
+ if (event.key === 'Enter' || event.key === ' ') {
45
+ event.preventDefault()
46
+ onActivate()
47
+ }
48
+ }
49
+ : undefined
50
+ }
51
+ role={onActivate ? 'button' : undefined}
52
+ tabIndex={onActivate ? 0 : undefined}
53
+ >
54
+ {leading !== undefined && (
55
+ <span className="flex size-3.5 shrink-0 items-center justify-center">{leading}</span>
56
+ )}
57
+ <div className="flex min-w-0 flex-1 items-center gap-2">{children}</div>
58
+ {trailing && (
59
+ <div
60
+ className={cn(
61
+ 'flex shrink-0 items-center gap-0.5',
62
+ !trailingVisible && 'opacity-0 group-hover/status-row:opacity-100 group-focus-within/status-row:opacity-100'
63
+ )}
64
+ >
65
+ {trailing}
66
+ </div>
67
+ )}
68
+ </div>
69
+ )
70
+ }
@@ -0,0 +1,42 @@
1
+ import { type ReactNode, useState } from 'react'
2
+
3
+ import { DisclosureCaret } from '@/components/ui/disclosure-caret'
4
+
5
+ interface StatusSectionProps {
6
+ /** Optional right-aligned actions (text links / micro buttons). Pass
7
+ * `Button` with `size="micro"` + `variant="text"` or `"link"`. */
8
+ accessory?: ReactNode
9
+ children: ReactNode
10
+ defaultCollapsed?: boolean
11
+ /** Optional glyph between the caret and the label (e.g. a `Codicon`). */
12
+ icon?: ReactNode
13
+ label: ReactNode
14
+ }
15
+
16
+ /**
17
+ * One collapsible group inside the composer status stack. Pure chrome — header
18
+ * (caret + label) + body — styled to match the queue exactly so every status
19
+ * (queue, subagents, background) reads as one piece. The stack supplies the
20
+ * outer card and the dividers between groups; this owns only its own collapse.
21
+ */
22
+ export function StatusSection({ accessory, children, defaultCollapsed = true, icon, label }: StatusSectionProps) {
23
+ const [collapsed, setCollapsed] = useState(defaultCollapsed)
24
+
25
+ return (
26
+ <div>
27
+ <div className="flex items-center gap-1 pr-1">
28
+ <button
29
+ className="flex min-w-0 flex-1 items-center gap-1.5 px-2 py-1 text-left text-xs font-normal text-muted-foreground/92 transition-colors hover:text-foreground/90"
30
+ onClick={() => setCollapsed(open => !open)}
31
+ type="button"
32
+ >
33
+ <DisclosureCaret className="shrink-0" open={!collapsed} size="1em" />
34
+ {icon && <span className="flex shrink-0 items-center">{icon}</span>}
35
+ <span className="truncate">{label}</span>
36
+ </button>
37
+ {accessory && <div className="flex shrink-0 items-center gap-1">{accessory}</div>}
38
+ </div>
39
+ {!collapsed && <div className="px-1 pb-0.5">{children}</div>}
40
+ </div>
41
+ )
42
+ }
@@ -0,0 +1,50 @@
1
+ import { useEffect, useLayoutEffect, useRef } from 'react'
2
+
3
+ import { cn } from '@/lib/utils'
4
+
5
+ interface TerminalOutputProps {
6
+ className?: string
7
+ text: string
8
+ }
9
+
10
+ const NEAR_BOTTOM_PX = 24
11
+
12
+ /**
13
+ * Tiny read-only terminal viewer: monospace, non-wrapping (long lines scroll
14
+ * horizontally), vertical scroll past `max-h`. Jumps to the bottom on mount,
15
+ * then tails — sticking to the bottom as `text` grows, but only when the user
16
+ * is already near the bottom so scrolling up to read earlier output isn't
17
+ * interrupted.
18
+ *
19
+ * Self-contained so any surface (status rows, tool calls, inspectors) can drop
20
+ * in a stdout/stderr box without re-implementing the scroll logic.
21
+ */
22
+ export function TerminalOutput({ className, text }: TerminalOutputProps) {
23
+ const ref = useRef<HTMLDivElement>(null)
24
+
25
+ // On open: jump straight to the latest output (no animation, before paint).
26
+ useLayoutEffect(() => {
27
+ const el = ref.current
28
+
29
+ if (el) {
30
+ el.scrollTop = el.scrollHeight
31
+ }
32
+ }, [])
33
+
34
+ // On growth: tail only when already pinned near the bottom.
35
+ useEffect(() => {
36
+ const el = ref.current
37
+
38
+ if (el && el.scrollHeight - el.scrollTop - el.clientHeight < NEAR_BOTTOM_PX) {
39
+ el.scrollTop = el.scrollHeight
40
+ }
41
+ }, [text])
42
+
43
+ return (
44
+ <div className={cn('max-h-16 overflow-auto overscroll-contain', className)} ref={ref}>
45
+ <pre className="w-max min-w-full font-mono text-[0.5625rem] leading-[0.85rem] whitespace-pre text-muted-foreground/70">
46
+ {text}
47
+ </pre>
48
+ </div>
49
+ )
50
+ }
@@ -0,0 +1,177 @@
1
+ 'use client'
2
+
3
+ import { type ComponentProps, useState } from 'react'
4
+
5
+ import { Dialog, DialogContent } from '@/components/ui/dialog'
6
+ import { useI18n } from '@/i18n'
7
+ import { Download } from '@/lib/icons'
8
+ import { cn } from '@/lib/utils'
9
+ import { notify, notifyError } from '@/store/notifications'
10
+
11
+ function imageFilename(src?: string): string {
12
+ if (!src) {
13
+ return 'image'
14
+ }
15
+
16
+ try {
17
+ const { pathname } = new URL(src, window.location.href)
18
+
19
+ return pathname.split('/').filter(Boolean).pop() || 'image'
20
+ } catch {
21
+ return src.split(/[\\/]/).filter(Boolean).pop() || 'image'
22
+ }
23
+ }
24
+
25
+ function isMissingIpcHandler(error: unknown): boolean {
26
+ const message = error instanceof Error ? error.message : typeof error === 'string' ? error : ''
27
+
28
+ return message.includes("No handler registered for 'NASTECH:saveImageFromUrl'")
29
+ }
30
+
31
+ async function startBrowserDownload(src: string) {
32
+ const response = await fetch(src)
33
+
34
+ if (!response.ok) {
35
+ throw new Error(`Could not fetch image: ${response.status}`)
36
+ }
37
+
38
+ const blobUrl = URL.createObjectURL(await response.blob())
39
+ const link = document.createElement('a')
40
+ link.href = blobUrl
41
+ link.download = imageFilename(src)
42
+ link.rel = 'noopener noreferrer'
43
+ document.body.appendChild(link)
44
+ link.click()
45
+ link.remove()
46
+ window.setTimeout(() => URL.revokeObjectURL(blobUrl), 30_000)
47
+ }
48
+
49
+ export interface ZoomableImageProps extends ComponentProps<'img'> {
50
+ containerClassName?: string
51
+ slot?: string
52
+ }
53
+
54
+ interface ImageActionCopy {
55
+ downloadImage: string
56
+ savingImage: string
57
+ }
58
+
59
+ export function ZoomableImage({ className, containerClassName, src, alt, slot, ...props }: ZoomableImageProps) {
60
+ const { t } = useI18n()
61
+ const copy = t.desktop
62
+ const [saving, setSaving] = useState(false)
63
+ const [lightboxOpen, setLightboxOpen] = useState(false)
64
+ const canOpen = Boolean(src)
65
+
66
+ async function handleDownload() {
67
+ if (!src || saving) {
68
+ return
69
+ }
70
+
71
+ setSaving(true)
72
+
73
+ try {
74
+ if (window.NASTECHDesktop?.saveImageFromUrl) {
75
+ const saved = await window.NASTECHDesktop.saveImageFromUrl(src)
76
+
77
+ if (saved) {
78
+ notify({ kind: 'success', title: copy.imageSaved, message: imageFilename(src) })
79
+ }
80
+
81
+ return
82
+ }
83
+
84
+ await startBrowserDownload(src)
85
+ } catch (error) {
86
+ if (isMissingIpcHandler(error)) {
87
+ try {
88
+ await startBrowserDownload(src)
89
+ notify({
90
+ kind: 'info',
91
+ title: copy.downloadStarted,
92
+ message: copy.restartToUseSaveImage
93
+ })
94
+ } catch (fallbackError) {
95
+ notifyError(fallbackError, copy.restartToSaveImages)
96
+ }
97
+
98
+ return
99
+ }
100
+
101
+ notifyError(error, copy.imageDownloadFailed)
102
+ } finally {
103
+ setSaving(false)
104
+ }
105
+ }
106
+
107
+ const lightbox = src ? (
108
+ <Dialog onOpenChange={setLightboxOpen} open={lightboxOpen}>
109
+ <DialogContent
110
+ className="block w-auto max-h-[calc(100vh-12rem)] max-w-[calc(100vw-12rem)] overflow-visible border-0 bg-transparent p-0 shadow-none"
111
+ showCloseButton={false}
112
+ >
113
+ <div className="group/lightbox relative inline-block">
114
+ <img
115
+ alt={alt ?? ''}
116
+ className="block max-h-[calc(100vh-12rem)] max-w-[calc(100vw-12rem)] cursor-zoom-out select-auto rounded-lg object-contain shadow-2xl"
117
+ onClick={() => setLightboxOpen(false)}
118
+ src={src}
119
+ />
120
+ <ImageActionButton copy={copy} onClick={handleDownload} saving={saving} variant="lightbox" />
121
+ </div>
122
+ </DialogContent>
123
+ </Dialog>
124
+ ) : null
125
+
126
+ return (
127
+ <>
128
+ <span
129
+ className={cn('group/image relative inline-block max-w-full align-top', containerClassName)}
130
+ data-slot={slot ?? 'aui_zoomable-image'}
131
+ >
132
+ <button
133
+ className="contents"
134
+ disabled={!canOpen}
135
+ onClick={() => canOpen && setLightboxOpen(true)}
136
+ title={canOpen ? copy.openImage : undefined}
137
+ type="button"
138
+ >
139
+ <img alt={alt ?? ''} className={className} src={src} {...props} />
140
+ </button>
141
+ {src && <ImageActionButton copy={copy} onClick={handleDownload} saving={saving} variant="inline" />}
142
+ </span>
143
+ {lightbox}
144
+ </>
145
+ )
146
+ }
147
+
148
+ function ImageActionButton({
149
+ copy,
150
+ onClick,
151
+ saving,
152
+ variant
153
+ }: {
154
+ copy: ImageActionCopy
155
+ onClick: () => void
156
+ saving: boolean
157
+ variant: 'inline' | 'lightbox'
158
+ }) {
159
+ return (
160
+ <button
161
+ aria-label={saving ? copy.savingImage : copy.downloadImage}
162
+ className={cn(
163
+ 'absolute right-2 top-2 grid size-8 place-items-center rounded-full border border-border/70 bg-background/80 text-muted-foreground opacity-0 shadow-sm backdrop-blur transition-opacity hover:bg-accent hover:text-foreground focus-visible:opacity-100 disabled:opacity-50',
164
+ variant === 'inline' ? 'group-hover/image:opacity-100' : 'group-hover/lightbox:opacity-100'
165
+ )}
166
+ disabled={saving}
167
+ onClick={event => {
168
+ event.stopPropagation()
169
+ void onClick()
170
+ }}
171
+ title={saving ? copy.savingImage : copy.downloadImage}
172
+ type="button"
173
+ >
174
+ <Download className={cn('size-4', saving && 'animate-pulse')} />
175
+ </button>
176
+ )
177
+ }