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,289 @@
1
+ import { Button } from '@/components/ui/button'
2
+ import { Codicon } from '@/components/ui/codicon'
3
+ import { Tip } from '@/components/ui/tooltip'
4
+ import { useI18n } from '@/i18n'
5
+ import { triggerHaptic } from '@/lib/haptics'
6
+ import { AudioLines, Layers3, Loader2, Square, SteeringWheel } from '@/lib/icons'
7
+ import { cn } from '@/lib/utils'
8
+
9
+ import type { ConversationStatus } from './hooks/use-voice-conversation'
10
+ import type { ChatBarState, VoiceStatus } from './types'
11
+
12
+ export const ICON_BTN = 'size-(--composer-control-size) shrink-0 rounded-md'
13
+ export const GHOST_ICON_BTN = cn(
14
+ ICON_BTN,
15
+ 'text-(--ui-text-tertiary) hover:bg-(--chrome-action-hover) hover:text-foreground'
16
+ )
17
+ // Send/voice-conversation primary: solid foreground-on-background circle
18
+ // (reads as black-on-white in light mode, white-on-black in dark mode) to
19
+ // match the reference composer's high-contrast CTA. Keeps the pill itself
20
+ // neutral and lets the action visually dominate the row.
21
+ export const PRIMARY_ICON_BTN = cn(
22
+ 'size-(--composer-control-primary-size,var(--composer-control-size)) shrink-0 rounded-full p-0',
23
+ 'bg-foreground text-background hover:bg-foreground/90',
24
+ 'disabled:bg-foreground/30 disabled:text-background disabled:opacity-100'
25
+ )
26
+
27
+ interface ConversationProps {
28
+ active: boolean
29
+ level: number
30
+ muted: boolean
31
+ status: ConversationStatus
32
+ onEnd: () => void
33
+ onStart: () => void
34
+ onStopTurn: () => void
35
+ onToggleMute: () => void
36
+ }
37
+
38
+ export function ComposerControls({
39
+ busy,
40
+ busyAction,
41
+ canSteer,
42
+ canSubmit,
43
+ conversation,
44
+ disabled,
45
+ hasComposerPayload,
46
+ state,
47
+ voiceStatus,
48
+ onDictate,
49
+ onSteer
50
+ }: {
51
+ busy: boolean
52
+ busyAction: 'queue' | 'stop'
53
+ canSteer: boolean
54
+ canSubmit: boolean
55
+ conversation: ConversationProps
56
+ disabled: boolean
57
+ hasComposerPayload: boolean
58
+ state: ChatBarState
59
+ voiceStatus: VoiceStatus
60
+ onDictate: () => void
61
+ onSteer: () => void
62
+ }) {
63
+ const { t } = useI18n()
64
+ const c = t.composer
65
+
66
+ if (conversation.active) {
67
+ return <ConversationPill {...conversation} disabled={disabled} />
68
+ }
69
+
70
+ const showVoicePrimary = !busy && !hasComposerPayload
71
+
72
+ return (
73
+ <div className="ml-auto flex shrink-0 items-center gap-(--composer-control-gap)">
74
+ <DictationButton disabled={disabled} onToggle={onDictate} state={state.voice} status={voiceStatus} />
75
+ {canSteer && (
76
+ <Tip label={c.steer}>
77
+ <Button
78
+ aria-label={c.steer}
79
+ className={GHOST_ICON_BTN}
80
+ disabled={disabled}
81
+ onClick={onSteer}
82
+ size="icon"
83
+ type="button"
84
+ variant="ghost"
85
+ >
86
+ <SteeringWheel size={16} />
87
+ </Button>
88
+ </Tip>
89
+ )}
90
+ {showVoicePrimary ? (
91
+ <Tip label={c.startVoice}>
92
+ <Button
93
+ aria-label={c.startVoice}
94
+ className={PRIMARY_ICON_BTN}
95
+ disabled={disabled}
96
+ onClick={() => {
97
+ triggerHaptic('open')
98
+ conversation.onStart()
99
+ }}
100
+ size="icon"
101
+ type="button"
102
+ >
103
+ <AudioLines size={17} />
104
+ </Button>
105
+ </Tip>
106
+ ) : (
107
+ <Tip label={busy ? (busyAction === 'queue' ? c.queueMessage : c.stop) : c.send}>
108
+ <Button
109
+ aria-label={busy ? (busyAction === 'queue' ? c.queueMessage : c.stop) : c.send}
110
+ className={PRIMARY_ICON_BTN}
111
+ disabled={disabled || !canSubmit}
112
+ type="submit"
113
+ >
114
+ {busy ? (
115
+ busyAction === 'queue' ? (
116
+ <Layers3 size={16} />
117
+ ) : (
118
+ <span className="block size-3 rounded-[0.1875rem] bg-current" />
119
+ )
120
+ ) : (
121
+ <Codicon name="arrow-up" size="1rem" />
122
+ )}
123
+ </Button>
124
+ </Tip>
125
+ )}
126
+ </div>
127
+ )
128
+ }
129
+
130
+ function ConversationPill({
131
+ disabled,
132
+ level,
133
+ muted,
134
+ onEnd,
135
+ onStopTurn,
136
+ onToggleMute,
137
+ status
138
+ }: ConversationProps & { disabled: boolean }) {
139
+ const { t } = useI18n()
140
+ const c = t.composer
141
+ const speaking = status === 'speaking'
142
+ const listening = status === 'listening' && !muted
143
+
144
+ const label =
145
+ status === 'speaking'
146
+ ? c.speaking
147
+ : status === 'transcribing'
148
+ ? c.transcribing
149
+ : status === 'thinking'
150
+ ? c.thinking
151
+ : muted
152
+ ? c.muted
153
+ : c.listening
154
+
155
+ return (
156
+ <div className="ml-auto flex shrink-0 items-center gap-(--composer-control-gap)">
157
+ <Tip label={muted ? c.unmuteMic : c.muteMic}>
158
+ <Button
159
+ aria-label={muted ? c.unmuteMic : c.muteMic}
160
+ aria-pressed={muted}
161
+ className={cn(GHOST_ICON_BTN, 'p-0', muted && 'bg-muted text-muted-foreground')}
162
+ disabled={disabled}
163
+ onClick={() => {
164
+ triggerHaptic('selection')
165
+ onToggleMute()
166
+ }}
167
+ size="icon"
168
+ type="button"
169
+ variant="ghost"
170
+ >
171
+ <Codicon name={muted ? 'mic-off' : 'mic'} size="1rem" />
172
+ </Button>
173
+ </Tip>
174
+ {listening && (
175
+ <Button
176
+ aria-label={c.stopListening}
177
+ className="h-(--composer-control-size) shrink-0 gap-1.5 rounded-full px-2.5 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
178
+ disabled={disabled}
179
+ onClick={() => {
180
+ triggerHaptic('submit')
181
+ onStopTurn()
182
+ }}
183
+ title={c.stopListening}
184
+ type="button"
185
+ variant="ghost"
186
+ >
187
+ <Square className="fill-current" size={11} />
188
+ <span>{c.stopShort}</span>
189
+ </Button>
190
+ )}
191
+ <Button
192
+ aria-label={c.endConversation}
193
+ className="h-(--composer-control-size) gap-1.5 rounded-full bg-primary px-3 text-xs font-medium text-primary-foreground hover:bg-primary/90"
194
+ disabled={disabled}
195
+ onClick={() => {
196
+ triggerHaptic('close')
197
+ onEnd()
198
+ }}
199
+ title={c.endConversation}
200
+ type="button"
201
+ >
202
+ <ConversationIndicator level={level} listening={listening} speaking={speaking} />
203
+ <span>{c.endShort}</span>
204
+ </Button>
205
+ <span className="sr-only" role="status">
206
+ {label}
207
+ </span>
208
+ </div>
209
+ )
210
+ }
211
+
212
+ function ConversationIndicator({
213
+ level,
214
+ listening,
215
+ speaking
216
+ }: {
217
+ level: number
218
+ listening: boolean
219
+ speaking: boolean
220
+ }) {
221
+ if (speaking) {
222
+ return <Loader2 className="animate-spin" size={12} />
223
+ }
224
+
225
+ const bars = [0.55, 0.85, 1, 0.85, 0.55]
226
+ const normalized = Math.max(0, Math.min(level, 1))
227
+
228
+ return (
229
+ <span aria-hidden="true" className="flex h-3 items-center gap-0.5">
230
+ {bars.map((weight, index) => {
231
+ const height = listening ? 0.3 + Math.min(0.7, normalized * weight) : 0.3
232
+
233
+ return <span className="w-0.5 rounded-full bg-current" key={index} style={{ height: `${height * 100}%` }} />
234
+ })}
235
+ </span>
236
+ )
237
+ }
238
+
239
+ function DictationButton({
240
+ disabled,
241
+ state,
242
+ status,
243
+ onToggle
244
+ }: {
245
+ disabled: boolean
246
+ state: ChatBarState['voice']
247
+ status: VoiceStatus
248
+ onToggle: () => void
249
+ }) {
250
+ const { t } = useI18n()
251
+ const c = t.composer
252
+ const active = state.active || status !== 'idle'
253
+
254
+ const aria =
255
+ status === 'recording' ? c.stopDictation : status === 'transcribing' ? c.transcribingDictation : c.voiceDictation
256
+
257
+ return (
258
+ <Tip label={aria}>
259
+ <Button
260
+ aria-label={aria}
261
+ aria-pressed={active}
262
+ className={cn(
263
+ GHOST_ICON_BTN,
264
+ 'p-0',
265
+ 'data-[active=true]:bg-accent data-[active=true]:text-foreground',
266
+ status === 'recording' && 'bg-primary/10 text-primary hover:bg-primary/15 hover:text-primary',
267
+ status === 'transcribing' && 'bg-primary/10 text-primary'
268
+ )}
269
+ data-active={active}
270
+ disabled={disabled || !state.enabled || status === 'transcribing'}
271
+ onClick={() => {
272
+ triggerHaptic(active ? 'close' : 'open')
273
+ onToggle()
274
+ }}
275
+ size="icon"
276
+ type="button"
277
+ variant="ghost"
278
+ >
279
+ {status === 'recording' ? (
280
+ <Square className="fill-current" size={12} />
281
+ ) : status === 'transcribing' ? (
282
+ <Loader2 className="animate-spin" size={16} />
283
+ ) : (
284
+ <Codicon name="mic" size="1rem" />
285
+ )}
286
+ </Button>
287
+ </Tip>
288
+ )
289
+ }
@@ -0,0 +1,2 @@
1
+ export const COMPOSER_DROP_FADE_CLASS = 'transition-opacity duration-150 ease-out'
2
+ export const COMPOSER_DROP_ACTIVE_CLASS = 'opacity-60'
@@ -0,0 +1,218 @@
1
+ import { act, cleanup, fireEvent, render } from '@testing-library/react'
2
+ import { useRef, useState } from 'react'
3
+ import { afterEach, describe, expect, it, vi } from 'vitest'
4
+
5
+ // No global setupFiles registers auto-cleanup, so unmount between tests —
6
+ // otherwise a second render() leaks the first editor and getByTestId('editor')
7
+ // matches multiple nodes.
8
+ afterEach(cleanup)
9
+
10
+ // Faithful mirror of index.tsx's Enter wiring (handleEditorKeyDown's Enter
11
+ // branch + submitDraft), driven through REAL DOM keydown events on a
12
+ // contentEditable.
13
+ //
14
+ // Regression repro for #39630: pressing Enter right after typing (fast typing /
15
+ // IME) did nothing. The composer state (`draft` from useAuiState) and its
16
+ // derived `hasComposerPayload` lag the DOM by a render, so the keydown handler
17
+ // read empty state and either dropped the message, drained a queued prompt
18
+ // instead of sending, or (while busy) refused to queue. The fix reads the live
19
+ // editor text — `hasLivePayload` in the handler and a DOM re-sync at the top of
20
+ // submitDraft — so the just-typed text always wins.
21
+ //
22
+ // We model the race deterministically the way the IME repro does: mutate the
23
+ // editor's textContent WITHOUT firing an input event, so the React `draft`
24
+ // state stays stale while the DOM already holds the text.
25
+ function Harness({
26
+ busy = false,
27
+ disabled = false,
28
+ queued = [],
29
+ onSubmit,
30
+ onQueue,
31
+ onCancel,
32
+ onDrain
33
+ }: {
34
+ busy?: boolean
35
+ disabled?: boolean
36
+ queued?: readonly string[]
37
+ onSubmit: (text: string) => void
38
+ onQueue: (text: string) => void
39
+ onCancel: () => void
40
+ onDrain: () => void
41
+ }) {
42
+ const editorRef = useRef<HTMLDivElement>(null)
43
+ const draftRef = useRef('')
44
+ // Mirrors `useAuiState(s => s.composer.text)` — updated only via setText, so
45
+ // it lags the DOM until React re-renders (the source of the bug).
46
+ const [draft, setDraft] = useState('')
47
+ const attachments: unknown[] = []
48
+
49
+ const composerPlainText = (el: HTMLElement) => el.textContent ?? ''
50
+
51
+ const setText = (next: string) => {
52
+ draftRef.current = next
53
+ setDraft(next)
54
+ }
55
+
56
+ const submitDraft = () => {
57
+ if (disabled) {
58
+ return
59
+ }
60
+
61
+ const editor = editorRef.current
62
+ if (editor) {
63
+ const domText = composerPlainText(editor)
64
+ if (domText !== draftRef.current) {
65
+ draftRef.current = domText
66
+ setDraft(domText)
67
+ }
68
+ }
69
+
70
+ const text = draftRef.current
71
+ const payloadPresent = text.trim().length > 0 || attachments.length > 0
72
+
73
+ if (busy) {
74
+ if (payloadPresent) {
75
+ onQueue(text)
76
+ } else {
77
+ onCancel()
78
+ }
79
+ } else if (!payloadPresent && queued.length > 0) {
80
+ onDrain()
81
+ } else if (payloadPresent) {
82
+ onSubmit(text)
83
+ }
84
+ }
85
+
86
+ const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
87
+ if (event.key === 'Enter' && !event.shiftKey) {
88
+ event.preventDefault()
89
+
90
+ const editorText = editorRef.current ? composerPlainText(editorRef.current) : draftRef.current
91
+ const hasLivePayload = editorText.trim().length > 0 || attachments.length > 0
92
+
93
+ if (disabled) {
94
+ return
95
+ }
96
+
97
+ if (!busy && !hasLivePayload && queued.length > 0) {
98
+ onDrain()
99
+
100
+ return
101
+ }
102
+
103
+ if (busy && !hasLivePayload) {
104
+ return
105
+ }
106
+
107
+ submitDraft()
108
+ }
109
+ }
110
+
111
+ // `draft` is read so the lint/compiler treats the stale-state mirror as live;
112
+ // the assertions prove the handler never relies on it.
113
+ void draft
114
+
115
+ return (
116
+ <div
117
+ contentEditable
118
+ data-testid="editor"
119
+ onInput={event => setText(composerPlainText(event.currentTarget))}
120
+ onKeyDown={handleKeyDown}
121
+ ref={editorRef}
122
+ suppressContentEditableWarning
123
+ />
124
+ )
125
+ }
126
+
127
+ describe('composer Enter submit — live DOM vs stale composer state (#39630)', () => {
128
+ it('sends the just-typed text on Enter even when composer state has not synced', async () => {
129
+ const onSubmit = vi.fn()
130
+ const { getByTestId } = render(
131
+ <Harness onCancel={vi.fn()} onDrain={vi.fn()} onQueue={vi.fn()} onSubmit={onSubmit} />
132
+ )
133
+ const editor = getByTestId('editor')
134
+
135
+ // Fast typing: the DOM has the text but NO input event fired, so `draft`
136
+ // state is still empty (the exact stale-state race).
137
+ await act(async () => {
138
+ editor.textContent = 'hello world'
139
+ fireEvent.keyDown(editor, { key: 'Enter' })
140
+ })
141
+
142
+ expect(onSubmit).toHaveBeenCalledWith('hello world')
143
+ })
144
+
145
+ it('queues a fast-typed message while busy instead of draining the queue or cancelling', async () => {
146
+ const onQueue = vi.fn()
147
+ const onDrain = vi.fn()
148
+ const onCancel = vi.fn()
149
+ const { getByTestId } = render(
150
+ <Harness busy onCancel={onCancel} onDrain={onDrain} onQueue={onQueue} onSubmit={vi.fn()} queued={['queued-1']} />
151
+ )
152
+ const editor = getByTestId('editor')
153
+
154
+ await act(async () => {
155
+ editor.textContent = 'urgent follow-up'
156
+ fireEvent.keyDown(editor, { key: 'Enter' })
157
+ })
158
+
159
+ expect(onQueue).toHaveBeenCalledWith('urgent follow-up')
160
+ expect(onDrain).not.toHaveBeenCalled()
161
+ expect(onCancel).not.toHaveBeenCalled()
162
+ })
163
+
164
+ it('treats an empty Enter while busy as a no-op (never an accidental Stop)', async () => {
165
+ const onCancel = vi.fn()
166
+ const onSubmit = vi.fn()
167
+ const onQueue = vi.fn()
168
+ const { getByTestId } = render(
169
+ <Harness busy onCancel={onCancel} onDrain={vi.fn()} onQueue={onQueue} onSubmit={onSubmit} />
170
+ )
171
+ const editor = getByTestId('editor')
172
+
173
+ await act(async () => {
174
+ editor.textContent = ''
175
+ fireEvent.keyDown(editor, { key: 'Enter' })
176
+ })
177
+
178
+ expect(onCancel).not.toHaveBeenCalled()
179
+ expect(onSubmit).not.toHaveBeenCalled()
180
+ expect(onQueue).not.toHaveBeenCalled()
181
+ })
182
+
183
+ it('drains the next queued prompt on Enter when idle with a truly empty editor', async () => {
184
+ const onDrain = vi.fn()
185
+ const onSubmit = vi.fn()
186
+ const { getByTestId } = render(
187
+ <Harness onCancel={vi.fn()} onDrain={onDrain} onQueue={vi.fn()} onSubmit={onSubmit} queued={['queued-1']} />
188
+ )
189
+ const editor = getByTestId('editor')
190
+
191
+ await act(async () => {
192
+ editor.textContent = ''
193
+ fireEvent.keyDown(editor, { key: 'Enter' })
194
+ })
195
+
196
+ expect(onDrain).toHaveBeenCalledTimes(1)
197
+ expect(onSubmit).not.toHaveBeenCalled()
198
+ })
199
+
200
+ it('keeps reconnect drafts editable but blocks Enter submit until the gateway returns', async () => {
201
+ const onSubmit = vi.fn()
202
+ const onDrain = vi.fn()
203
+ const { getByTestId } = render(
204
+ <Harness disabled onCancel={vi.fn()} onDrain={onDrain} onQueue={vi.fn()} onSubmit={onSubmit} queued={['queued-1']} />
205
+ )
206
+ const editor = getByTestId('editor')
207
+
208
+ await act(async () => {
209
+ editor.textContent = 'draft while reconnecting'
210
+ fireEvent.input(editor)
211
+ fireEvent.keyDown(editor, { key: 'Enter' })
212
+ })
213
+
214
+ expect(editor.textContent).toBe('draft while reconnecting')
215
+ expect(onDrain).not.toHaveBeenCalled()
216
+ expect(onSubmit).not.toHaveBeenCalled()
217
+ })
218
+ })
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Composer focus + external-insert bus.
3
+ *
4
+ * Mutations from outside the composer (sidebar attach, drag drop, terminal
5
+ * Cmd+L, preview console, etc.) dispatch through here. Each composer subscribes
6
+ * and routes the work back into its own ref/state.
7
+ *
8
+ * `dispatch` defers to a macrotask so synchronous click/keydown handlers
9
+ * (react-arborist row focus, picker `node.select()`) finish first and don't
10
+ * steal focus from the composer effect.
11
+ */
12
+
13
+ import type { InlineRefInput } from './inline-refs'
14
+ import { RICH_INPUT_SLOT } from './rich-editor'
15
+
16
+ export type ComposerTarget = 'edit' | 'main'
17
+ export type ComposerInsertMode = 'block' | 'inline'
18
+
19
+ interface FocusDetail {
20
+ target: ComposerTarget
21
+ }
22
+
23
+ interface InsertDetail {
24
+ mode: ComposerInsertMode
25
+ target: ComposerTarget
26
+ text: string
27
+ }
28
+
29
+ interface InsertRefsDetail {
30
+ refs: InlineRefInput[]
31
+ target: ComposerTarget
32
+ }
33
+
34
+ const FOCUS_EVENT = 'NASTECH:composer-focus'
35
+ const INSERT_EVENT = 'NASTECH:composer-insert'
36
+ const INSERT_REFS_EVENT = 'NASTECH:composer-insert-refs'
37
+
38
+ let activeTarget: ComposerTarget = 'main'
39
+
40
+ const resolve = (target: ComposerTarget | 'active') => (target === 'active' ? activeTarget : target)
41
+
42
+ const dispatch = <T>(name: string, detail: T) => {
43
+ if (typeof window === 'undefined') {
44
+ return
45
+ }
46
+
47
+ window.setTimeout(() => window.dispatchEvent(new CustomEvent<T>(name, { detail })), 0)
48
+ }
49
+
50
+ const subscribe = <T>(name: string, handler: (detail: T) => void) => {
51
+ if (typeof window === 'undefined') {
52
+ return () => undefined
53
+ }
54
+
55
+ const listener = (event: Event) => {
56
+ const detail = (event as CustomEvent<T>).detail
57
+
58
+ if (detail) {
59
+ handler(detail)
60
+ }
61
+ }
62
+
63
+ window.addEventListener(name, listener)
64
+
65
+ return () => window.removeEventListener(name, listener)
66
+ }
67
+
68
+ export const markActiveComposer = (target: ComposerTarget) => {
69
+ activeTarget = target
70
+ }
71
+
72
+ export const requestComposerFocus = (target: ComposerTarget | 'active' = 'active') =>
73
+ dispatch<FocusDetail>(FOCUS_EVENT, { target: resolve(target) })
74
+
75
+ export const requestComposerInsert = (
76
+ text: string,
77
+ { mode = 'block', target = 'active' }: { mode?: ComposerInsertMode; target?: ComposerTarget | 'active' } = {}
78
+ ) => {
79
+ const trimmed = text.trim()
80
+
81
+ if (!trimmed) {
82
+ return
83
+ }
84
+
85
+ dispatch<InsertDetail>(INSERT_EVENT, { mode, target: resolve(target), text: trimmed })
86
+ }
87
+
88
+ export const onComposerFocusRequest = (handler: (target: ComposerTarget) => void) =>
89
+ subscribe<FocusDetail>(FOCUS_EVENT, ({ target }) => handler(target))
90
+
91
+ export const onComposerInsertRequest = (handler: (detail: InsertDetail) => void) =>
92
+ subscribe<InsertDetail>(INSERT_EVENT, handler)
93
+
94
+ /** Insert typed ref chips (carrying a display label) into a composer — the
95
+ * structured cousin of {@link requestComposerInsert}, used for session links. */
96
+ export const requestComposerInsertRefs = (
97
+ refs: InlineRefInput[],
98
+ { target = 'active' }: { target?: ComposerTarget | 'active' } = {}
99
+ ) => {
100
+ if (refs.length) {
101
+ dispatch<InsertRefsDetail>(INSERT_REFS_EVENT, { refs, target: resolve(target) })
102
+ }
103
+ }
104
+
105
+ export const onComposerInsertRefsRequest = (handler: (detail: InsertRefsDetail) => void) =>
106
+ subscribe<InsertRefsDetail>(INSERT_REFS_EVENT, handler)
107
+
108
+ /**
109
+ * Focus a composer input across React commit + browser focus restore.
110
+ *
111
+ * The triple-call survives:
112
+ * - sync: contenteditable already mounted
113
+ * - rAF: React just committed a `renderComposerContents` swap
114
+ * - 0ms: browser focus reclaim from a click target inside an external panel
115
+ */
116
+ export const focusComposerInput = (el: HTMLElement | null) => {
117
+ if (!el) {
118
+ return
119
+ }
120
+
121
+ const focus = () => el.focus({ preventScroll: true })
122
+
123
+ focus()
124
+ window.requestAnimationFrame(focus)
125
+ window.setTimeout(focus, 0)
126
+ }
127
+
128
+ export const blurComposerInput = () => {
129
+ const el = document.querySelector(`[data-slot="${RICH_INPUT_SLOT}"]`) as HTMLElement | null
130
+
131
+ if (el && document.activeElement === el) {
132
+ el.blur()
133
+ }
134
+ }
@@ -0,0 +1,59 @@
1
+ import type { ReactNode } from 'react'
2
+
3
+ import { useI18n } from '@/i18n'
4
+
5
+ import { COMPLETION_DRAWER_CLASS } from './completion-drawer'
6
+
7
+ const COMMON_COMMAND_KEYS = ['/help', '/clear', '/resume', '/details', '/copy', '/quit']
8
+ const HOTKEY_KEYS = ['@', '/', '?', 'Enter', 'Cmd/Ctrl+Shift+K', 'Cmd/Ctrl+/', 'Esc', '↑ / ↓']
9
+
10
+ export function HelpHint() {
11
+ const { t } = useI18n()
12
+ const c = t.composer
13
+
14
+ return (
15
+ <div className={COMPLETION_DRAWER_CLASS} data-slot="composer-completion-drawer" data-state="open" role="dialog">
16
+ <Section title={c.commonCommands}>
17
+ {COMMON_COMMAND_KEYS.map(key => (
18
+ <Row description={c.commandDescs[key] ?? ''} key={key} keyLabel={key} mono />
19
+ ))}
20
+ </Section>
21
+
22
+ <Section title={c.hotkeys}>
23
+ {HOTKEY_KEYS.map(key => (
24
+ <Row description={c.hotkeyDescs[key] ?? ''} key={key} keyLabel={key} />
25
+ ))}
26
+ </Section>
27
+
28
+ <p className="px-2.5 py-1 text-xs text-muted-foreground/80">
29
+ <span className="font-mono text-foreground/80">/help</span> {c.helpFooter}
30
+ </p>
31
+ </div>
32
+ )
33
+ }
34
+
35
+ function Section({ children, title }: { children: ReactNode; title: string }) {
36
+ return (
37
+ <div className="grid gap-0.5 pt-0.5">
38
+ <p className="px-2.5 pb-0.5 pt-1 text-[0.65rem] font-medium uppercase tracking-wide text-muted-foreground/75">
39
+ {title}
40
+ </p>
41
+ {children}
42
+ </div>
43
+ )
44
+ }
45
+
46
+ function Row({ description, keyLabel, mono = false }: { description: string; keyLabel: string; mono?: boolean }) {
47
+ return (
48
+ <div className="flex min-w-0 items-baseline gap-2 rounded-md px-2.5 py-1 text-xs">
49
+ <span
50
+ className={
51
+ mono ? 'shrink-0 truncate font-mono font-medium text-foreground/85' : 'shrink-0 truncate text-foreground/85'
52
+ }
53
+ >
54
+ {keyLabel}
55
+ </span>
56
+ <span className="min-w-0 truncate text-muted-foreground/80">{description}</span>
57
+ </div>
58
+ )
59
+ }