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,109 @@
1
+ import { type FC } from 'react'
2
+
3
+ import { Checkbox } from '@/components/ui/checkbox'
4
+ import { Loader2Icon } from '@/lib/icons'
5
+ import { parseTodos, type TodoItem, type TodoStatus } from '@/lib/todos'
6
+ import { cn } from '@/lib/utils'
7
+
8
+ export function todosFromMessageContent(content: unknown): TodoItem[] {
9
+ if (!Array.isArray(content)) {
10
+ return []
11
+ }
12
+
13
+ let latest: null | TodoItem[] = null
14
+
15
+ for (const part of content) {
16
+ if (!part || typeof part !== 'object') {
17
+ continue
18
+ }
19
+
20
+ const row = part as Record<string, unknown>
21
+
22
+ if (row.type !== 'tool-call' || row.toolName !== 'todo') {
23
+ continue
24
+ }
25
+
26
+ const parsed = parseTodos(row.result) ?? parseTodos(row.args)
27
+
28
+ if (parsed !== null) {
29
+ latest = parsed
30
+ }
31
+ }
32
+
33
+ return latest ?? []
34
+ }
35
+
36
+ const headerLabel = (todos: readonly TodoItem[]): string =>
37
+ todos.find(t => t.status === 'in_progress')?.content ??
38
+ todos.find(t => t.status === 'pending')?.content ??
39
+ todos.at(-1)?.content ??
40
+ 'Tasks'
41
+
42
+ const Checkmark: FC<{ status: TodoStatus; label: string }> = ({ status, label }) => {
43
+ if (status === 'in_progress') {
44
+ return (
45
+ <span
46
+ aria-label={`In progress: ${label}`}
47
+ className="grid size-[1.1rem] shrink-0 place-items-center rounded-full border border-ring/65 bg-[color-mix(in_srgb,var(--dt-ring)_14%,transparent)]"
48
+ >
49
+ <Loader2Icon className="size-3 animate-spin text-ring" />
50
+ </span>
51
+ )
52
+ }
53
+
54
+ const checked = status === 'completed'
55
+
56
+ return (
57
+ <Checkbox
58
+ aria-label={label}
59
+ checked={checked}
60
+ className={cn(
61
+ 'size-[1.1rem] shrink-0 rounded-full border-border/80 pointer-events-none disabled:cursor-default disabled:opacity-100',
62
+ checked &&
63
+ 'data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground [&_[data-slot=checkbox-indicator]_svg]:size-3',
64
+ status === 'cancelled' && 'border-muted-foreground/40'
65
+ )}
66
+ disabled
67
+ />
68
+ )
69
+ }
70
+
71
+ export const HoistedTodoPanel: FC<{ todos: TodoItem[] }> = ({ todos }) => {
72
+ if (!todos.length) {
73
+ return null
74
+ }
75
+
76
+ const label = headerLabel(todos)
77
+
78
+ return (
79
+ <section
80
+ className="mt-1 mb-3 inline-block w-fit max-w-full overflow-hidden rounded-2xl border border-border/70 bg-card align-top shadow-[0_1px_2px_0_hsl(var(--foreground)/0.04),0_1px_4px_-1px_hsl(var(--foreground)/0.06)]"
81
+ data-slot="aui_todo-hoisted"
82
+ >
83
+ <header className="px-3 pt-3 pb-2">
84
+ <span
85
+ className="block max-w-full truncate text-[0.85rem] font-semibold leading-tight tracking-tight text-foreground"
86
+ title={label}
87
+ >
88
+ {label}
89
+ </span>
90
+ </header>
91
+ <ul className="grid min-w-0 gap-0.5 px-3 pb-3">
92
+ {todos.map(todo => (
93
+ <li
94
+ // Active row at full presence; everything else fades. Opacity on
95
+ // the row so the checkbox glyph dims with the text.
96
+ className={cn(
97
+ 'flex min-w-0 items-center gap-3 py-1.5 transition-opacity',
98
+ todo.status === 'in_progress' ? 'opacity-100' : 'opacity-45'
99
+ )}
100
+ key={todo.id}
101
+ >
102
+ <Checkmark label={todo.content} status={todo.status} />
103
+ <span className="min-w-0 wrap-anywhere text-[0.8rem] leading-[1.2rem] text-foreground">{todo.content}</span>
104
+ </li>
105
+ ))}
106
+ </ul>
107
+ </section>
108
+ )
109
+ }
@@ -0,0 +1,158 @@
1
+ import { AssistantRuntimeProvider, type ThreadMessage, useExternalStoreRuntime } from '@assistant-ui/react'
2
+ import { cleanup, render, waitFor } from '@testing-library/react'
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
4
+
5
+ import { clearAllPrompts, setApprovalRequest } from '@/store/prompts'
6
+ import { $activeSessionId } from '@/store/session'
7
+ import { $toolDisclosureStates } from '@/store/tool-view'
8
+
9
+ import { Thread } from './thread'
10
+
11
+ // Regression coverage for the "approval must never be buried" bug. Tools now
12
+ // render as a flat list (no collapsible "N steps" group), so a pending tool's
13
+ // inline ApprovalBar is always in the visual flow — never inside a `hidden`
14
+ // body. These assert the bar shows only when an approval is live and is never
15
+ // trapped under a `hidden` ancestor.
16
+
17
+ const createdAt = new Date('2026-06-03T00:00:00.000Z')
18
+
19
+ const resizeObservers = new Set<TestResizeObserver>()
20
+
21
+ class TestResizeObserver {
22
+ private target: Element | null = null
23
+
24
+ constructor(private readonly callback: ResizeObserverCallback) {
25
+ resizeObservers.add(this)
26
+ }
27
+
28
+ observe(target: Element) {
29
+ this.target = target
30
+ }
31
+
32
+ unobserve() {}
33
+
34
+ disconnect() {
35
+ resizeObservers.delete(this)
36
+ }
37
+ }
38
+
39
+ vi.stubGlobal('ResizeObserver', TestResizeObserver)
40
+ vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) =>
41
+ window.setTimeout(() => callback(performance.now()), 0)
42
+ )
43
+ vi.stubGlobal('cancelAnimationFrame', (id: number) => window.clearTimeout(id))
44
+
45
+ Element.prototype.scrollTo = function scrollTo() {}
46
+
47
+ Element.prototype.animate = function animate() {
48
+ return {
49
+ cancel: () => {},
50
+ finished: Promise.resolve()
51
+ } as unknown as Animation
52
+ }
53
+
54
+ function stubOffsetDimension(
55
+ prop: 'offsetHeight' | 'offsetWidth',
56
+ clientProp: 'clientHeight' | 'clientWidth',
57
+ fallback: number
58
+ ) {
59
+ const previous = Object.getOwnPropertyDescriptor(HTMLElement.prototype, prop)
60
+
61
+ Object.defineProperty(HTMLElement.prototype, prop, {
62
+ configurable: true,
63
+ get() {
64
+ return previous?.get?.call(this) || (this as HTMLElement)[clientProp] || fallback
65
+ }
66
+ })
67
+ }
68
+
69
+ stubOffsetDimension('offsetWidth', 'clientWidth', 800)
70
+ stubOffsetDimension('offsetHeight', 'clientHeight', 600)
71
+
72
+ // A running assistant message with two tools: a completed read_file plus a
73
+ // pending terminal (no result), rendered as a flat two-row list.
74
+ function groupedPendingMessage(): ThreadMessage {
75
+ return {
76
+ id: 'assistant-group-1',
77
+ role: 'assistant',
78
+ content: [
79
+ {
80
+ type: 'tool-call',
81
+ toolCallId: 'read-1',
82
+ toolName: 'read_file',
83
+ args: { path: '/etc/hosts' },
84
+ argsText: JSON.stringify({ path: '/etc/hosts' }),
85
+ result: { content: '127.0.0.1 localhost' }
86
+ },
87
+ {
88
+ type: 'tool-call',
89
+ toolCallId: 'term-1',
90
+ toolName: 'terminal',
91
+ args: { command: 'rm -rf /tmp/x' },
92
+ argsText: JSON.stringify({ command: 'rm -rf /tmp/x' })
93
+ }
94
+ ],
95
+ status: { type: 'running' },
96
+ createdAt,
97
+ metadata: {
98
+ unstable_state: null,
99
+ unstable_annotations: [],
100
+ unstable_data: [],
101
+ steps: [],
102
+ custom: {}
103
+ }
104
+ } as ThreadMessage
105
+ }
106
+
107
+ function GroupHarness({ message }: { message: ThreadMessage }) {
108
+ const runtime = useExternalStoreRuntime<ThreadMessage>({
109
+ messages: [message],
110
+ isRunning: message.status?.type === 'running',
111
+ onNew: async () => {}
112
+ })
113
+
114
+ return (
115
+ <AssistantRuntimeProvider runtime={runtime}>
116
+ <Thread />
117
+ </AssistantRuntimeProvider>
118
+ )
119
+ }
120
+
121
+ beforeEach(() => {
122
+ clearAllPrompts()
123
+ $activeSessionId.set('sess-1')
124
+ $toolDisclosureStates.set({})
125
+ })
126
+
127
+ afterEach(() => {
128
+ cleanup()
129
+ clearAllPrompts()
130
+ $activeSessionId.set(null)
131
+ })
132
+
133
+ describe('flat tool list approval surfacing', () => {
134
+ it('renders no inline approval bar when there is no live approval', async () => {
135
+ const { container } = render(<GroupHarness message={groupedPendingMessage()} />)
136
+
137
+ // The pending terminal row mounts immediately, but its inline ApprovalBar
138
+ // returns null while $approvalRequest is empty.
139
+ await waitFor(() => {
140
+ expect(container.querySelectorAll('[data-slot="tool-block"]').length).toBeGreaterThan(0)
141
+ })
142
+ expect(container.querySelector('[data-slot="tool-approval-inline"]')).toBeNull()
143
+ })
144
+
145
+ it('surfaces the approval inline and never under a hidden ancestor', async () => {
146
+ setApprovalRequest({ command: 'rm -rf /tmp/x', description: 'dangerous command', sessionId: 'sess-1' })
147
+
148
+ const { container } = render(<GroupHarness message={groupedPendingMessage()} />)
149
+
150
+ await waitFor(() => {
151
+ const bar = container.querySelector('[data-slot="tool-approval-inline"]')
152
+ expect(bar).not.toBeNull()
153
+ // Flat rows live directly in the flow — nothing should ever wrap the bar
154
+ // in a `hidden` subtree.
155
+ expect(bar?.closest('[hidden]')).toBeNull()
156
+ })
157
+ })
158
+ })
@@ -0,0 +1,81 @@
1
+ import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
2
+ import { afterEach, describe, expect, it, vi } from 'vitest'
3
+
4
+ import type { NasTechGateway } from '@/nastech'
5
+ import { $gateway } from '@/store/gateway'
6
+ import { $approvalRequest, clearAllPrompts, setApprovalRequest } from '@/store/prompts'
7
+ import { $activeSessionId } from '@/store/session'
8
+
9
+ import { PendingToolApproval } from './tool-approval'
10
+ import type { ToolPart } from './tool-fallback-model'
11
+
12
+ function part(toolName: string): ToolPart {
13
+ return { toolName, type: `tool-${toolName}` } as unknown as ToolPart
14
+ }
15
+
16
+ function setRequest(command = 'rm -rf /tmp/x') {
17
+ $activeSessionId.set('sess-1')
18
+ setApprovalRequest({ command, description: 'dangerous command', sessionId: 'sess-1' })
19
+ }
20
+
21
+ function mockGateway() {
22
+ const request = vi.fn().mockResolvedValue({ resolved: true })
23
+ $gateway.set({ request } as unknown as NasTechGateway)
24
+
25
+ return request
26
+ }
27
+
28
+ afterEach(() => {
29
+ cleanup()
30
+ clearAllPrompts()
31
+ $activeSessionId.set(null)
32
+ $gateway.set(null)
33
+ })
34
+
35
+ describe('PendingToolApproval', () => {
36
+ it('renders nothing when there is no pending approval', () => {
37
+ const { container } = render(<PendingToolApproval part={part('terminal')} />)
38
+
39
+ expect(container.innerHTML).toBe('')
40
+ })
41
+
42
+ it('renders nothing for tools that never raise approval', () => {
43
+ setRequest()
44
+ const { container } = render(<PendingToolApproval part={part('read_file')} />)
45
+
46
+ expect(container.innerHTML).toBe('')
47
+ })
48
+
49
+ it('renders the inline run/reject controls on the pending terminal row', () => {
50
+ setRequest('chmod -R 777 /tmp/x')
51
+ render(<PendingToolApproval part={part('terminal')} />)
52
+
53
+ expect(screen.getByRole('button', { name: /Run/ })).toBeTruthy()
54
+ expect(screen.getByRole('button', { name: /Reject/ })).toBeTruthy()
55
+ })
56
+
57
+ it('sends approval.respond {choice: "once"} and clears the request on Run', async () => {
58
+ const request = mockGateway()
59
+ setRequest()
60
+ render(<PendingToolApproval part={part('terminal')} />)
61
+
62
+ fireEvent.click(screen.getByRole('button', { name: /Run/ }))
63
+
64
+ await waitFor(() => {
65
+ expect(request).toHaveBeenCalledWith('approval.respond', { choice: 'once', session_id: 'sess-1' })
66
+ })
67
+ expect($approvalRequest.get()).toBeNull()
68
+ })
69
+
70
+ it('sends choice "deny" on Reject', async () => {
71
+ const request = mockGateway()
72
+ setRequest()
73
+ render(<PendingToolApproval part={part('terminal')} />)
74
+
75
+ fireEvent.click(screen.getByRole('button', { name: /Reject/ }))
76
+
77
+ await waitFor(() => {
78
+ expect(request).toHaveBeenCalledWith('approval.respond', { choice: 'deny', session_id: 'sess-1' })
79
+ })
80
+ })
81
+ })
@@ -0,0 +1,209 @@
1
+ 'use client'
2
+
3
+ import { useStore } from '@nanostores/react'
4
+ import { type FC, useCallback, useEffect, useState } from 'react'
5
+
6
+ import { Button } from '@/components/ui/button'
7
+ import {
8
+ Dialog,
9
+ DialogContent,
10
+ DialogDescription,
11
+ DialogFooter,
12
+ DialogHeader,
13
+ DialogTitle
14
+ } from '@/components/ui/dialog'
15
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
16
+ import { useI18n } from '@/i18n'
17
+ import { triggerHaptic } from '@/lib/haptics'
18
+ import { ChevronDown, Loader2 } from '@/lib/icons'
19
+ import { $gateway } from '@/store/gateway'
20
+ import { notifyError } from '@/store/notifications'
21
+ import { $approvalRequest, type ApprovalRequest, clearApprovalRequest } from '@/store/prompts'
22
+
23
+ import type { ToolPart } from './tool-fallback-model'
24
+
25
+ // Inline approval control. Rendered as a compact button strip
26
+ // under the pending tool row that raised the approval (the row already shows
27
+ // the command, so the strip deliberately doesn't repeat it) instead of as a
28
+ // modal overlay.
29
+ //
30
+ // Binding is POSITIONAL, not command-matched: the desktop `tool.start` payload
31
+ // carries no structured args (only tool_id/name/context — see
32
+ // tui_gateway/server.py::_on_tool_start), so we cannot join the approval to the
33
+ // row by command string. But `approval.request` only ever fires from the
34
+ // `terminal` / `execute_code` guards and the agent thread blocks on exactly one
35
+ // approval at a time, so the single pending row of those tools IS the row that
36
+ // raised it. The command/description text comes from `$approvalRequest` (the
37
+ // event payload), which is the only place that data reliably exists.
38
+ export const APPROVAL_TOOLS = new Set(['terminal', 'execute_code'])
39
+
40
+ // Canonical gateway choices (ui-tui/src/components/prompts.tsx).
41
+ type ApprovalChoice = 'once' | 'session' | 'always' | 'deny'
42
+
43
+ export const PendingToolApproval: FC<{ part: ToolPart }> = ({ part }) => {
44
+ const request = useStore($approvalRequest)
45
+
46
+ if (!request || !APPROVAL_TOOLS.has(part.toolName)) {
47
+ return null
48
+ }
49
+
50
+ return <ApprovalBar request={request} />
51
+ }
52
+
53
+ const isMac = typeof navigator !== 'undefined' && /Mac|iP(hone|ad|od)/.test(navigator.platform)
54
+
55
+ const ApprovalBar: FC<{ request: ApprovalRequest }> = ({ request }) => {
56
+ const { t } = useI18n()
57
+ const copy = t.assistant.approval
58
+ const gateway = useStore($gateway)
59
+ const [submitting, setSubmitting] = useState<ApprovalChoice | null>(null)
60
+ // "Always allow" persists the pattern to ~/.NASTECH/config.yaml permanently, so
61
+ // it goes through a confirm step rather than firing straight from the menu.
62
+ const [confirmAlways, setConfirmAlways] = useState(false)
63
+ const busy = submitting !== null
64
+
65
+ const respond = useCallback(
66
+ async (choice: ApprovalChoice) => {
67
+ // Another bar (or the keyboard path) may have already resolved this
68
+ // approval; the atom is the single source of truth, so bail if it's gone.
69
+ if (busy || !$approvalRequest.get()) {
70
+ return
71
+ }
72
+
73
+ if (!gateway) {
74
+ notifyError(new Error(copy.gatewayDisconnected), copy.sendFailed)
75
+
76
+ return
77
+ }
78
+
79
+ setSubmitting(choice)
80
+
81
+ try {
82
+ await gateway.request<{ resolved?: boolean }>('approval.respond', {
83
+ choice,
84
+ session_id: request.sessionId ?? undefined
85
+ })
86
+ triggerHaptic(choice === 'deny' ? 'cancel' : 'submit')
87
+ clearApprovalRequest(request.sessionId)
88
+ } catch (error) {
89
+ notifyError(error, copy.sendFailed)
90
+ setSubmitting(null)
91
+ }
92
+ },
93
+ [busy, gateway, request.sessionId]
94
+ )
95
+
96
+ // ⌘/Ctrl+Enter → Run, Esc → Reject.
97
+ // While the confirm dialog is open it owns the keyboard (Esc closes it), so
98
+ // the strip-level shortcuts stand down to avoid denying the whole approval.
99
+ useEffect(() => {
100
+ if (confirmAlways) {
101
+ return
102
+ }
103
+
104
+ const onKeyDown = (event: KeyboardEvent) => {
105
+ if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {
106
+ event.preventDefault()
107
+ void respond('once')
108
+ } else if (event.key === 'Escape') {
109
+ event.preventDefault()
110
+ void respond('deny')
111
+ }
112
+ }
113
+
114
+ window.addEventListener('keydown', onKeyDown, true)
115
+
116
+ return () => window.removeEventListener('keydown', onKeyDown, true)
117
+ }, [confirmAlways, respond])
118
+
119
+ return (
120
+ <div className="mt-1 flex items-center gap-2.5 ps-5" data-slot="tool-approval-inline">
121
+ <div className="inline-flex h-6 items-stretch overflow-hidden rounded-md border border-primary/25 bg-primary/10 text-primary">
122
+ <Button
123
+ className="h-full gap-1 rounded-none px-2 text-xs font-medium text-primary hover:bg-primary/15 hover:text-primary"
124
+ disabled={busy}
125
+ onClick={() => void respond('once')}
126
+ size="xs"
127
+ variant="ghost"
128
+ >
129
+ {submitting === 'once' ? <Loader2 className="size-3 animate-spin" /> : copy.run}
130
+ {submitting !== 'once' && <span className="text-[0.625rem] text-primary/60">{isMac ? '⌘⏎' : 'Ctrl⏎'}</span>}
131
+ </Button>
132
+ <span aria-hidden className="w-px self-stretch bg-primary/20" />
133
+ <DropdownMenu>
134
+ <DropdownMenuTrigger asChild>
135
+ <Button
136
+ aria-label={copy.moreOptions}
137
+ className="h-full w-5 rounded-none px-0 text-primary hover:bg-primary/15 hover:text-primary"
138
+ disabled={busy}
139
+ size="xs"
140
+ variant="ghost"
141
+ >
142
+ <ChevronDown className="size-3" />
143
+ </Button>
144
+ </DropdownMenuTrigger>
145
+ <DropdownMenuContent align="start" className="min-w-44">
146
+ <DropdownMenuItem onSelect={() => void respond('session')}>{copy.allowSession}</DropdownMenuItem>
147
+ <DropdownMenuItem
148
+ onSelect={() => {
149
+ // Defer one tick so the menu fully unmounts before the dialog
150
+ // mounts — otherwise Radix's focus-return races the dialog and
151
+ // dismisses it via onInteractOutside.
152
+ setTimeout(() => setConfirmAlways(true), 0)
153
+ }}
154
+ >
155
+ {copy.alwaysAllowMenu}
156
+ </DropdownMenuItem>
157
+ <DropdownMenuItem onSelect={() => void respond('deny')} variant="destructive">
158
+ {copy.reject}
159
+ </DropdownMenuItem>
160
+ </DropdownMenuContent>
161
+ </DropdownMenu>
162
+ </div>
163
+
164
+ <Button
165
+ className="h-6 gap-1.5 rounded-md px-1.5 text-xs font-normal text-(--ui-text-tertiary) hover:text-foreground"
166
+ disabled={busy}
167
+ onClick={() => void respond('deny')}
168
+ size="xs"
169
+ variant="ghost"
170
+ >
171
+ {submitting === 'deny' ? <Loader2 className="size-3 animate-spin" /> : copy.reject}
172
+ {submitting !== 'deny' && <span className="text-[0.625rem] opacity-55">Esc</span>}
173
+ </Button>
174
+
175
+ <Dialog onOpenChange={setConfirmAlways} open={confirmAlways}>
176
+ <DialogContent className="max-w-md">
177
+ <DialogHeader>
178
+ <DialogTitle>{copy.alwaysTitle}</DialogTitle>
179
+ <DialogDescription>
180
+ {copy.alwaysDescription(request.description)}
181
+ </DialogDescription>
182
+ </DialogHeader>
183
+
184
+ {request.command.trim() && (
185
+ <pre className="max-h-32 overflow-auto whitespace-pre-wrap break-words rounded-md border border-(--ui-stroke-tertiary) bg-(--ui-chat-surface-background) px-2.5 py-1.5 font-mono text-xs leading-snug text-foreground">
186
+ {request.command.trim()}
187
+ </pre>
188
+ )}
189
+
190
+ <DialogFooter>
191
+ <Button onClick={() => setConfirmAlways(false)} size="sm" variant="ghost">
192
+ {t.common.cancel}
193
+ </Button>
194
+ <Button
195
+ onClick={() => {
196
+ setConfirmAlways(false)
197
+ void respond('always')
198
+ }}
199
+ size="sm"
200
+ variant="destructive"
201
+ >
202
+ {copy.alwaysAllow}
203
+ </Button>
204
+ </DialogFooter>
205
+ </DialogContent>
206
+ </Dialog>
207
+ </div>
208
+ )
209
+ }
@@ -0,0 +1,66 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { buildToolView, type ToolPart } from './tool-fallback-model'
4
+
5
+ const part = (overrides: Partial<ToolPart>): ToolPart => ({
6
+ args: {},
7
+ isError: false,
8
+ result: {},
9
+ toolCallId: 'call_1',
10
+ toolName: 'vision_analyze',
11
+ type: 'tool-call',
12
+ ...overrides
13
+ })
14
+
15
+ describe('buildToolView image handling', () => {
16
+ // vision_analyze reports the input image as a local path; an <img> pointed at
17
+ // a bare path resolves against the renderer origin and 404s, so we render the
18
+ // tool codicon instead of a broken image.
19
+ it('drops bare filesystem paths', () => {
20
+ expect(buildToolView(part({ args: { path: '/Users/me/shot.png' } }), '').imageUrl).toBe('')
21
+ expect(buildToolView(part({ result: { image_path: '/tmp/out.jpg' } }), '').imageUrl).toBe('')
22
+ })
23
+
24
+ it('keeps fetchable data URLs', () => {
25
+ const dataUrl = 'data:image/png;base64,AAAA'
26
+
27
+ expect(buildToolView(part({ result: { image_url: dataUrl } }), '').imageUrl).toBe(dataUrl)
28
+ })
29
+
30
+ it('keeps remote http(s) image URLs', () => {
31
+ const url = 'https://example.com/pic.webp'
32
+
33
+ expect(buildToolView(part({ result: { url } }), '').imageUrl).toBe(url)
34
+ })
35
+ })
36
+
37
+ describe('buildToolView terminal exit-code status', () => {
38
+ const terminal = (result: Record<string, unknown>) =>
39
+ buildToolView(part({ result, toolName: 'terminal' }), '')
40
+
41
+ // A non-zero exit code with real output is not a failure (grep no-match,
42
+ // diff differences, piped commands surfacing the last stage's code, etc.) —
43
+ // it should render as success so the card isn't painted red.
44
+ it('treats non-zero exit with output as success', () => {
45
+ expect(terminal({ exit_code: 7, output: 'node ... 5174 (LISTEN)' }).status).toBe('success')
46
+ expect(terminal({ exit_code: 1, stdout: 'partial results' }).status).toBe('success')
47
+ })
48
+
49
+ // No output + non-zero exit is a genuine failure worth flagging.
50
+ it('treats non-zero exit with no output as error', () => {
51
+ expect(terminal({ exit_code: 127, output: '' }).status).toBe('error')
52
+ expect(terminal({ exit_code: 1 }).status).toBe('error')
53
+ })
54
+
55
+ it('treats zero exit as success', () => {
56
+ expect(terminal({ exit_code: 0, output: 'done' }).status).toBe('success')
57
+ })
58
+
59
+ // Explicit error signals still win regardless of output presence.
60
+ it('keeps explicit error signals red even with output', () => {
61
+ expect(terminal({ error: 'boom', exit_code: 0, output: 'partial' }).status).toBe('error')
62
+ expect(buildToolView(part({ isError: true, result: { output: 'x' }, toolName: 'terminal' }), '').status).toBe(
63
+ 'error'
64
+ )
65
+ })
66
+ })