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,186 @@
1
+ import type { Unstable_TriggerAdapter, Unstable_TriggerItem } from '@assistant-ui/core'
2
+ import { act, fireEvent, render } from '@testing-library/react'
3
+ import { useCallback, useEffect, useRef, useState } from 'react'
4
+ import { describe, expect, it, vi } from 'vitest'
5
+
6
+ import { useLiveCompletionAdapter } from './hooks/use-live-completion-adapter'
7
+ import { detectTrigger, type TriggerState } from './text-utils'
8
+
9
+ // Faithful mirror of index.tsx's trigger wiring, driven through REAL DOM
10
+ // keydown+keyup events on a contentEditable. Exercises the parts a direct
11
+ // reducer-call repro misses: the keyup -> refreshTrigger path, the
12
+ // keydown-set "consumed" ref that guards it, and per-press keydown+keyup
13
+ // ordering (critical for Escape, whose keydown nulls `trigger` before keyup).
14
+ function Harness({
15
+ onState
16
+ }: {
17
+ onState: (s: { active: number; items: readonly Unstable_TriggerItem[]; open: boolean }) => void
18
+ }) {
19
+ const editorRef = useRef<HTMLDivElement>(null)
20
+ const triggerKeyConsumedRef = useRef(false)
21
+ const [trigger, setTrigger] = useState<TriggerState | null>(null)
22
+ const [triggerActive, setTriggerActive] = useState(0)
23
+ const [triggerItems, setTriggerItems] = useState<readonly Unstable_TriggerItem[]>([])
24
+
25
+ const { adapter } = useLiveCompletionAdapter({
26
+ enabled: true,
27
+ debounceMs: 0,
28
+ fetcher: async (query: string) => ({
29
+ query,
30
+ items: Array.from({ length: 5 }, (_, i) => ({ text: `/cmd${i}`, display: `/cmd${i}`, meta: '' }))
31
+ }),
32
+ toItem: (entry, index) => ({ id: `${entry.text}|${index}`, type: 'slash', label: entry.text.slice(1) })
33
+ })
34
+
35
+ const triggerAdapter: Unstable_TriggerAdapter | null = trigger?.kind === '/' ? adapter : null
36
+
37
+ const refreshTrigger = useCallback(() => {
38
+ const editor = editorRef.current
39
+
40
+ if (!editor) {
41
+ return
42
+ }
43
+
44
+ const raw = editor.textContent ?? ''
45
+
46
+ if (!raw.includes('@') && !raw.includes('/')) {
47
+ if (trigger) {
48
+ setTrigger(null)
49
+ setTriggerActive(0)
50
+ }
51
+
52
+ return
53
+ }
54
+
55
+ const detected = detectTrigger(raw)
56
+ setTrigger(detected)
57
+
58
+ if (detected?.kind !== trigger?.kind || detected?.query !== trigger?.query) {
59
+ setTriggerActive(0)
60
+ }
61
+ }, [trigger])
62
+
63
+ useEffect(() => {
64
+ if (!trigger || !triggerAdapter?.search) {
65
+ setTriggerItems([])
66
+
67
+ return
68
+ }
69
+
70
+ setTriggerItems(triggerAdapter.search(trigger.query))
71
+ }, [trigger, triggerAdapter])
72
+
73
+ useEffect(() => {
74
+ setTriggerActive(idx => Math.min(idx, Math.max(0, triggerItems.length - 1)))
75
+ }, [triggerItems.length])
76
+
77
+ onState({ active: triggerActive, items: triggerItems, open: trigger !== null })
78
+
79
+ const closeTrigger = () => {
80
+ setTrigger(null)
81
+ setTriggerItems([])
82
+ setTriggerActive(0)
83
+ }
84
+
85
+ // Exact copies of index.tsx handlers, including the keydown-set "consumed"
86
+ // ref that the keyup consults.
87
+ const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
88
+ if (trigger && triggerItems.length > 0) {
89
+ if (event.key === 'ArrowDown') {
90
+ event.preventDefault()
91
+ triggerKeyConsumedRef.current = true
92
+ setTriggerActive(idx => (idx + 1) % triggerItems.length)
93
+
94
+ return
95
+ }
96
+
97
+ if (event.key === 'ArrowUp') {
98
+ event.preventDefault()
99
+ triggerKeyConsumedRef.current = true
100
+ setTriggerActive(idx => (idx - 1 + triggerItems.length) % triggerItems.length)
101
+
102
+ return
103
+ }
104
+
105
+ if (event.key === 'Escape') {
106
+ event.preventDefault()
107
+ triggerKeyConsumedRef.current = true
108
+ closeTrigger()
109
+
110
+ return
111
+ }
112
+ }
113
+ }
114
+
115
+ const handleKeyUp = () => {
116
+ if (triggerKeyConsumedRef.current) {
117
+ triggerKeyConsumedRef.current = false
118
+
119
+ return
120
+ }
121
+
122
+ // index.tsx defers via setTimeout(refreshTrigger, 0); call synchronously
123
+ // here so the test deterministically observes the keyup-driven refresh.
124
+ refreshTrigger()
125
+ }
126
+
127
+ return (
128
+ <div
129
+ contentEditable
130
+ data-testid="editor"
131
+ onInput={() => refreshTrigger()}
132
+ onKeyDown={handleKeyDown}
133
+ onKeyUp={handleKeyUp}
134
+ ref={editorRef}
135
+ suppressContentEditableWarning
136
+ />
137
+ )
138
+ }
139
+
140
+ async function flush() {
141
+ await act(async () => {
142
+ await new Promise(r => setTimeout(r, 20))
143
+ })
144
+ }
145
+
146
+ describe('slash menu navigation — real DOM keydown+keyup', () => {
147
+ it('cycles through ALL items and Esc closes (and stays closed)', async () => {
148
+ vi.useRealTimers()
149
+ let latest = { active: 0, items: [] as readonly Unstable_TriggerItem[], open: false }
150
+ const { getByTestId } = render(<Harness onState={s => (latest = s)} />)
151
+ const editor = getByTestId('editor')
152
+
153
+ // Simulate typing '/'.
154
+ await act(async () => {
155
+ editor.textContent = '/'
156
+ fireEvent.input(editor)
157
+ })
158
+ await flush()
159
+
160
+ expect(latest.open).toBe(true)
161
+ expect(latest.items.length).toBe(5)
162
+
163
+ // ArrowDown 6x with REAL keydown+keyup pairs. Bug = stuck [0,1,0,1,...].
164
+ const seen: number[] = [latest.active]
165
+
166
+ for (let i = 0; i < 6; i++) {
167
+ await act(async () => {
168
+ fireEvent.keyDown(editor, { key: 'ArrowDown' })
169
+ fireEvent.keyUp(editor, { key: 'ArrowDown' })
170
+ await Promise.resolve()
171
+ })
172
+ seen.push(latest.active)
173
+ }
174
+
175
+ expect(seen).toEqual([0, 1, 2, 3, 4, 0, 1])
176
+
177
+ // Escape: keydown closes; keyup must NOT reopen (the '/' is still in text).
178
+ await act(async () => {
179
+ fireEvent.keyDown(editor, { key: 'Escape' })
180
+ fireEvent.keyUp(editor, { key: 'Escape' })
181
+ await Promise.resolve()
182
+ })
183
+ await flush()
184
+ expect(latest.open).toBe(false)
185
+ })
186
+ })
@@ -0,0 +1,202 @@
1
+ import { useStore } from '@nanostores/react'
2
+ import { type ReactNode, useEffect, useLayoutEffect, useMemo, useRef } from 'react'
3
+ import { useNavigate } from 'react-router-dom'
4
+
5
+ import { blurComposerInput } from '@/app/chat/composer/focus'
6
+ import { AGENTS_ROUTE } from '@/app/routes'
7
+ import { composerDockCard } from '@/components/chat/composer-dock'
8
+ import { StatusSection } from '@/components/chat/status-section'
9
+ import { Button } from '@/components/ui/button'
10
+ import { Codicon } from '@/components/ui/codicon'
11
+ import { type Translations, useI18n } from '@/i18n'
12
+ import { cn } from '@/lib/utils'
13
+ import {
14
+ $statusItemsBySession,
15
+ type ComposerStatusItem,
16
+ dismissBackgroundProcess,
17
+ groupStatusItems,
18
+ refreshBackgroundProcesses,
19
+ type StatusGroup,
20
+ stopBackgroundProcess
21
+ } from '@/store/composer-status'
22
+ import { $threadScrolledUp } from '@/store/thread-scroll'
23
+ import { openSessionInNewWindow } from '@/store/windows'
24
+
25
+ import { StatusItemRow } from './status-row'
26
+
27
+ // Slow safety-net poll for silent exits (processes without notify_on_complete
28
+ // emit no event when they die). Only armed while a running row is on screen.
29
+ const BACKGROUND_POLL_MS = 5_000
30
+
31
+ const groupLabel = (group: StatusGroup, s: Translations['statusStack']) => {
32
+ if (group.type === 'todo') {
33
+ return s.todos(group.items.filter(i => i.todoStatus === 'completed').length, group.items.length)
34
+ }
35
+
36
+ return group.type === 'subagent' ? s.subagents(group.items.length) : s.background(group.items.length)
37
+ }
38
+
39
+ interface ComposerStatusStackProps {
40
+ /** The queue, built by the composer (it owns the queue's callbacks). Rendered
41
+ * as the last group so it stays fused to the composer like before. */
42
+ queue: ReactNode
43
+ sessionId: null | string
44
+ }
45
+
46
+ /**
47
+ * The status "sink" above the composer: one card (the queue's chrome) holding
48
+ * every session-scoped status — subagents, background tasks, queue — grouped by
49
+ * type and separated by light dividers. Collapses to nothing when empty.
50
+ */
51
+ export function ComposerStatusStack({ queue, sessionId }: ComposerStatusStackProps) {
52
+ const { t } = useI18n()
53
+ const navigate = useNavigate()
54
+ const itemsBySession = useStore($statusItemsBySession)
55
+ const scrolledUp = useStore($threadScrolledUp)
56
+
57
+ const groups = useMemo(
58
+ () => groupStatusItems(sessionId ? (itemsBySession[sessionId] ?? []) : []),
59
+ [itemsBySession, sessionId]
60
+ )
61
+
62
+ // Seed from the registry on session open; event-driven refreshes (terminal /
63
+ // process tool completions) live in use-message-stream.
64
+ useEffect(() => {
65
+ if (sessionId) {
66
+ void refreshBackgroundProcesses(sessionId)
67
+ }
68
+ }, [sessionId])
69
+
70
+ const hasRunningBackground = groups.some(g => g.type === 'background' && g.items.some(i => i.state === 'running'))
71
+
72
+ useEffect(() => {
73
+ if (!sessionId || !hasRunningBackground) {
74
+ return
75
+ }
76
+
77
+ const timer = setInterval(() => void refreshBackgroundProcesses(sessionId), BACKGROUND_POLL_MS)
78
+
79
+ return () => clearInterval(timer)
80
+ }, [hasRunningBackground, sessionId])
81
+
82
+ const openAgents = () => navigate(AGENTS_ROUTE)
83
+
84
+ const openSubagent = (item: ComposerStatusItem) =>
85
+ item.sessionId ? void openSessionInNewWindow(item.sessionId, { watch: true }) : openAgents()
86
+
87
+ const sections: { key: string; node: ReactNode }[] = groups.map(group => ({
88
+ key: group.type,
89
+ node: (
90
+ <StatusSection
91
+ accessory={
92
+ group.type === 'subagent' ? (
93
+ <Button
94
+ className="text-muted-foreground/75 hover:text-foreground/90"
95
+ onClick={openAgents}
96
+ size="micro"
97
+ type="button"
98
+ variant="text"
99
+ >
100
+ {t.statusStack.agents}
101
+ </Button>
102
+ ) : undefined
103
+ }
104
+ defaultCollapsed={group.type !== 'todo'}
105
+ icon={
106
+ group.type === 'todo' ? (
107
+ <Codicon className="text-muted-foreground/70" name="checklist" size="0.8rem" />
108
+ ) : undefined
109
+ }
110
+ label={groupLabel(group, t.statusStack)}
111
+ >
112
+ {group.items.map(item => (
113
+ <StatusItemRow
114
+ item={item}
115
+ key={item.id}
116
+ onDismiss={sessionId ? id => dismissBackgroundProcess(sessionId, id) : undefined}
117
+ onOpen={() => openSubagent(item)}
118
+ onStop={sessionId ? id => stopBackgroundProcess(sessionId, id) : undefined}
119
+ />
120
+ ))}
121
+ </StatusSection>
122
+ )
123
+ }))
124
+
125
+ if (queue) {
126
+ sections.push({ key: 'queue', node: queue })
127
+ }
128
+
129
+ const visible = sections.length > 0
130
+ const stackRef = useRef<HTMLDivElement | null>(null)
131
+
132
+ // The stack is out of flow (overlays the thread), so the composer's measured
133
+ // height never sees it. Publish our own measured height — bucketed like the
134
+ // composer's, to avoid style invalidation churn — so the thread's
135
+ // last-message clearance can add it and the stack never hides messages.
136
+ useLayoutEffect(() => {
137
+ const root = document.documentElement
138
+ const el = stackRef.current
139
+
140
+ if (!visible || !el) {
141
+ root.style.removeProperty('--status-stack-measured-height')
142
+
143
+ return
144
+ }
145
+
146
+ let last = -1
147
+
148
+ const sync = () => {
149
+ const bucket = Math.round(el.getBoundingClientRect().height / 8) * 8
150
+
151
+ if (bucket !== last) {
152
+ last = bucket
153
+ root.style.setProperty('--status-stack-measured-height', `${bucket}px`)
154
+ }
155
+ }
156
+
157
+ const observer = new ResizeObserver(sync)
158
+ observer.observe(el)
159
+ sync()
160
+
161
+ return () => {
162
+ observer.disconnect()
163
+ root.style.removeProperty('--status-stack-measured-height')
164
+ }
165
+ }, [visible])
166
+
167
+ if (!visible) {
168
+ return null
169
+ }
170
+
171
+ return (
172
+ <div
173
+ // Sits above the composer (bottom-full), nudged down by the shell's 0.5rem
174
+ // top pad (pt-2 on composer-root) plus 1px so its bottom edge overlaps the
175
+ // composer surface's top border. z BELOW the surface (z-4) so the surface's
176
+ // top border paints over our transparent bottom border — one seam, no
177
+ // double line.
178
+ className="absolute inset-x-0 bottom-full z-3 max-h-[40vh] translate-y-[calc(0.5rem+1px)] overflow-y-auto"
179
+ onPointerDownCapture={() => blurComposerInput()}
180
+ ref={stackRef}
181
+ >
182
+ {/* The card paints the shared --composer-fill (rest / scrolled / focused
183
+ all match the composer surface by construction); on scroll we only
184
+ ghost the CONTENT — element opacity on the card would kill the blur.
185
+ Rounded top, square bottom; the bottom border is TRANSPARENT — the
186
+ composer surface's visible top border (which sits at a higher z) is the
187
+ single shared seam, so the two read as one fused capsule. */}
188
+ <div className={cn(composerDockCard('top'), 'mx-2 rounded-b-none border-b border-b-transparent pt-0.5 pb-1')}>
189
+ <div
190
+ className={cn(
191
+ 'transition-opacity duration-200 ease-out',
192
+ scrolledUp ? 'opacity-30 group-hover/composer:opacity-100' : 'opacity-100'
193
+ )}
194
+ >
195
+ {sections.map(section => (
196
+ <div key={section.key}>{section.node}</div>
197
+ ))}
198
+ </div>
199
+ </div>
200
+ </div>
201
+ )
202
+ }
@@ -0,0 +1,155 @@
1
+ import { Fragment, memo, type ReactNode, useState } from 'react'
2
+
3
+ import { StatusRow } from '@/components/chat/status-row'
4
+ import { TerminalOutput } from '@/components/chat/terminal-output'
5
+ import { Button } from '@/components/ui/button'
6
+ import { Codicon } from '@/components/ui/codicon'
7
+ import { DisclosureCaret } from '@/components/ui/disclosure-caret'
8
+ import { GlyphSpinner } from '@/components/ui/glyph-spinner'
9
+ import { Tip } from '@/components/ui/tooltip'
10
+ import { type Translations, useI18n } from '@/i18n'
11
+ import { ArrowUpRight, X } from '@/lib/icons'
12
+ import type { TodoStatus } from '@/lib/todos'
13
+ import { cn } from '@/lib/utils'
14
+ import type { ComposerStatusItem } from '@/store/composer-status'
15
+
16
+ const toolLabel = (name: string) =>
17
+ name
18
+ .split('_')
19
+ .filter(Boolean)
20
+ .map(part => part[0]!.toUpperCase() + part.slice(1))
21
+ .join(' ') || name
22
+
23
+ // Todo rows speak checkbox, not spinner-and-dot: a dashed ring while the item
24
+ // is still open (pending), codicons once it resolves, a live spinner only on
25
+ // the in-progress item.
26
+ const TODO_GLYPHS: Record<Exclude<TodoStatus, 'in_progress' | 'pending'>, { icon: string; tone: string }> = {
27
+ cancelled: { icon: 'circle-slash', tone: 'text-muted-foreground/45' },
28
+ completed: { icon: 'pass-filled', tone: 'text-emerald-500/80' }
29
+ }
30
+
31
+ // Left slot: braille spinner while running, otherwise a small status dot
32
+ // (green = done, red = failed) so the slot is always filled and rows align.
33
+ function leadingGlyph(item: ComposerStatusItem, s: Translations['statusStack']): ReactNode {
34
+ if (item.todoStatus === 'pending') {
35
+ return (
36
+ <span
37
+ aria-hidden
38
+ className="box-border size-[0.7rem] rounded-full border border-dashed border-muted-foreground/60"
39
+ />
40
+ )
41
+ }
42
+
43
+ if (item.todoStatus && item.todoStatus !== 'in_progress') {
44
+ const glyph = TODO_GLYPHS[item.todoStatus]
45
+
46
+ return <Codicon className={glyph.tone} name={glyph.icon} size="0.8rem" />
47
+ }
48
+
49
+ if (item.state === 'running') {
50
+ return (
51
+ <GlyphSpinner
52
+ ariaLabel={s.running}
53
+ className="text-[0.9rem] leading-none text-muted-foreground/80"
54
+ spinner="braille"
55
+ />
56
+ )
57
+ }
58
+
59
+ return (
60
+ <span
61
+ aria-hidden
62
+ className={cn('size-1.5 rounded-full', item.state === 'failed' ? 'bg-destructive/80' : 'bg-emerald-500/70')}
63
+ />
64
+ )
65
+ }
66
+
67
+ interface StatusItemRowProps {
68
+ item: ComposerStatusItem
69
+ /** Clear a finished background task from the stack. */
70
+ onDismiss?: (id: string) => void
71
+ /** Open the subagent's own session window, livestreamed by the gateway's
72
+ * child-session mirror (Agents view fallback for older gateways). */
73
+ onOpen?: () => void
74
+ /** Cancel a running background task. */
75
+ onStop?: (id: string) => void
76
+ }
77
+
78
+ /**
79
+ * Renders one {@link ComposerStatusItem} into the shared {@link StatusRow}.
80
+ * Memoised + keyed by id so parent re-renders never remount it (the spinner
81
+ * keeps ticking instead of resetting).
82
+ */
83
+ export const StatusItemRow = memo(function StatusItemRow({ item, onDismiss, onOpen, onStop }: StatusItemRowProps) {
84
+ const { t } = useI18n()
85
+ const s = t.statusStack
86
+ const [outputOpen, setOutputOpen] = useState(false)
87
+ const failed = item.state === 'failed'
88
+ const running = item.state === 'running'
89
+
90
+ const action =
91
+ item.type === 'background'
92
+ ? running
93
+ ? onStop && { label: s.stop, onClick: () => onStop(item.id) }
94
+ : onDismiss && { label: s.dismiss, onClick: () => onDismiss(item.id) }
95
+ : null
96
+
97
+ const canOpen = item.type === 'subagent' && !!onOpen
98
+ const hasOutput = item.type === 'background' && !!item.output
99
+ const onActivate = canOpen ? onOpen : hasOutput ? () => setOutputOpen(open => !open) : undefined
100
+
101
+ return (
102
+ <Fragment>
103
+ <StatusRow
104
+ leading={leadingGlyph(item, s)}
105
+ onActivate={onActivate}
106
+ trailing={
107
+ action ? (
108
+ <Tip label={action.label}>
109
+ <Button
110
+ aria-label={action.label}
111
+ className="-my-1 size-4 rounded-md text-muted-foreground/60 hover:text-foreground/90"
112
+ onClick={event => {
113
+ event.stopPropagation()
114
+ action.onClick()
115
+ }}
116
+ size="icon-xs"
117
+ type="button"
118
+ variant="ghost"
119
+ >
120
+ <X size={12} />
121
+ </Button>
122
+ </Tip>
123
+ ) : canOpen ? (
124
+ <ArrowUpRight aria-hidden className="size-3.5 text-muted-foreground/55" />
125
+ ) : undefined
126
+ }
127
+ >
128
+ <span
129
+ className={cn(
130
+ 'min-w-0 max-w-[18rem] truncate text-[0.73rem] leading-4',
131
+ failed
132
+ ? 'text-destructive/90'
133
+ : item.todoStatus && item.todoStatus !== 'in_progress'
134
+ ? 'text-muted-foreground/75'
135
+ : 'text-foreground/92'
136
+ )}
137
+ >
138
+ {item.title}
139
+ </span>
140
+ {item.type === 'subagent' && item.currentTool && (
141
+ <span className="shrink-0 truncate text-[0.62rem] leading-4 text-muted-foreground/70">
142
+ {toolLabel(item.currentTool)}
143
+ </span>
144
+ )}
145
+ {failed && typeof item.exitCode === 'number' && item.exitCode !== 0 && (
146
+ <span className="shrink-0 rounded bg-destructive/15 px-1 text-[0.58rem] font-semibold text-destructive tabular-nums">
147
+ {s.exit(item.exitCode)}
148
+ </span>
149
+ )}
150
+ {hasOutput && <DisclosureCaret className="shrink-0 text-muted-foreground/45" open={outputOpen} size="0.8em" />}
151
+ </StatusRow>
152
+ {hasOutput && outputOpen && <TerminalOutput className="mx-auto mb-1 max-w-[90%]" text={item.output!} />}
153
+ </Fragment>
154
+ )
155
+ })
@@ -0,0 +1,77 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { blobDedupeKey, detectTrigger, extractClipboardImageBlobs } from './text-utils'
4
+
5
+ describe('detectTrigger', () => {
6
+ it('detects a bare slash trigger with an empty query', () => {
7
+ expect(detectTrigger('/')).toEqual({ kind: '/', query: '', tokenLength: 1 })
8
+ })
9
+
10
+ it('detects a slash command query', () => {
11
+ expect(detectTrigger('/skill')).toEqual({ kind: '/', query: 'skill', tokenLength: 6 })
12
+ })
13
+
14
+ it('detects a bare at-mention trigger with an empty query', () => {
15
+ expect(detectTrigger('@')).toEqual({ kind: '@', query: '', tokenLength: 1 })
16
+ })
17
+
18
+ it('detects an at-mention query', () => {
19
+ expect(detectTrigger('@file')).toEqual({ kind: '@', query: 'file', tokenLength: 5 })
20
+ })
21
+
22
+ it('returns null for plain text', () => {
23
+ expect(detectTrigger('hello there')).toBeNull()
24
+ })
25
+ })
26
+
27
+ describe('extractClipboardImageBlobs', () => {
28
+ it('dedupes the same image exposed on both items and files', () => {
29
+ const image = new File([new Uint8Array([1, 2, 3])], 'paste.png', {
30
+ type: 'image/png',
31
+ lastModified: 1_700_000_000_000
32
+ })
33
+
34
+ const clipboard = {
35
+ files: {
36
+ length: 1,
37
+ item: (index: number) => (index === 0 ? image : null)
38
+ },
39
+ getData: () => '',
40
+ items: [
41
+ {
42
+ kind: 'file',
43
+ type: 'image/png',
44
+ getAsFile: () => image
45
+ }
46
+ ]
47
+ } as unknown as DataTransfer
48
+
49
+ expect(extractClipboardImageBlobs(clipboard)).toEqual([image])
50
+ })
51
+
52
+ it('falls back to files when items has no image', () => {
53
+ const image = new File([new Uint8Array([4, 5])], 'shot.jpg', {
54
+ type: 'image/jpeg',
55
+ lastModified: 1_700_000_000_001
56
+ })
57
+
58
+ const clipboard = {
59
+ files: {
60
+ length: 1,
61
+ item: (index: number) => (index === 0 ? image : null)
62
+ },
63
+ getData: () => '',
64
+ items: []
65
+ } as unknown as DataTransfer
66
+
67
+ expect(extractClipboardImageBlobs(clipboard)).toEqual([image])
68
+ })
69
+ })
70
+
71
+ describe('blobDedupeKey', () => {
72
+ it('uses file metadata for File blobs', () => {
73
+ const file = new File([], 'a.png', { type: 'image/png', lastModified: 42 })
74
+
75
+ expect(blobDedupeKey(file)).toBe('file:a.png:0:image/png:42')
76
+ })
77
+ })