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,223 @@
1
+ import { useStore } from '@nanostores/react'
2
+ import { type MutableRefObject, useCallback, useEffect } from 'react'
3
+
4
+ import { gatewayEventCompletedFileDiff } from '@/lib/gateway-events'
5
+ import {
6
+ $previewTarget,
7
+ $sessionPreviewRegistry,
8
+ beginPreviewServerRestart,
9
+ completePreviewServerRestart,
10
+ getSessionPreviewRecord,
11
+ progressPreviewServerRestart,
12
+ requestPreviewReload,
13
+ setPreviewTarget,
14
+ setSessionPreviewTarget
15
+ } from '@/store/preview'
16
+ import { $currentCwd } from '@/store/session'
17
+ import type { RpcEvent } from '@/types/nastech'
18
+
19
+ type EventHandler = (event: RpcEvent) => void
20
+
21
+ interface PreviewRoutingOptions {
22
+ activeSessionIdRef: MutableRefObject<string | null>
23
+ baseHandleGatewayEvent: EventHandler
24
+ currentCwd: string
25
+ currentView: string
26
+ requestGateway: <T = unknown>(method: string, params?: Record<string, unknown>) => Promise<T>
27
+ routedSessionId: string | null
28
+ selectedStoredSessionId: string | null
29
+ }
30
+
31
+ function asRecord(payload: unknown): Record<string, unknown> {
32
+ return payload && typeof payload === 'object' ? (payload as Record<string, unknown>) : {}
33
+ }
34
+
35
+ function activePreviewSessionId(
36
+ activeSessionIdRef: MutableRefObject<string | null>,
37
+ routedSessionId: string | null,
38
+ selectedStoredSessionId: string | null
39
+ ): string {
40
+ return selectedStoredSessionId || routedSessionId || activeSessionIdRef.current || ''
41
+ }
42
+
43
+ function looksLikePreviewTarget(value: string): boolean {
44
+ return /^https?:\/\//i.test(value) || /^file:\/\//i.test(value) || /^(?:\/|\.{1,2}\/|~\/).+/.test(value)
45
+ }
46
+
47
+ function stripAnsi(value: string): string {
48
+ return value.replace(new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g'), '')
49
+ }
50
+
51
+ function htmlPathFromInlineDiff(value: string): string {
52
+ const cleaned = stripAnsi(value).replace(/^\s*┊\s*review diff\s*\n/i, '')
53
+
54
+ for (const match of cleaned.matchAll(/(?:^|\s)(?:[ab]\/)?([^\s]+\.html?)(?=\s|$)/gi)) {
55
+ const candidate = match[1]?.trim()
56
+
57
+ if (candidate) {
58
+ return candidate
59
+ }
60
+ }
61
+
62
+ return ''
63
+ }
64
+
65
+ function structuredPreviewCandidate(payload: unknown): string {
66
+ const record = asRecord(payload)
67
+ const fields = ['url', 'target', 'path', 'file', 'filepath', 'preview']
68
+
69
+ for (const field of fields) {
70
+ const value = record[field]
71
+
72
+ if (typeof value === 'string') {
73
+ const target = value.trim()
74
+
75
+ if (target && looksLikePreviewTarget(target)) {
76
+ return target
77
+ }
78
+ }
79
+ }
80
+
81
+ const inlineDiff = record.inline_diff
82
+
83
+ if (typeof inlineDiff === 'string') {
84
+ return htmlPathFromInlineDiff(inlineDiff)
85
+ }
86
+
87
+ return ''
88
+ }
89
+
90
+ export function usePreviewRouting({
91
+ activeSessionIdRef,
92
+ baseHandleGatewayEvent,
93
+ currentCwd,
94
+ currentView,
95
+ requestGateway,
96
+ routedSessionId,
97
+ selectedStoredSessionId
98
+ }: PreviewRoutingOptions) {
99
+ const previewRegistry = useStore($sessionPreviewRegistry)
100
+ const previewSessionId = activePreviewSessionId(activeSessionIdRef, routedSessionId, selectedStoredSessionId)
101
+
102
+ useEffect(() => {
103
+ if (currentView !== 'chat' || !previewSessionId) {
104
+ setPreviewTarget(null)
105
+
106
+ return
107
+ }
108
+
109
+ const record = getSessionPreviewRecord(previewSessionId)
110
+
111
+ setPreviewTarget(record?.normalized ?? null)
112
+ }, [currentView, previewRegistry, previewSessionId])
113
+
114
+ const registerStructuredPreview = useCallback(
115
+ async (event: RpcEvent) => {
116
+ if (
117
+ event.session_id &&
118
+ event.session_id !== activeSessionIdRef.current &&
119
+ event.session_id !== previewSessionId
120
+ ) {
121
+ return
122
+ }
123
+
124
+ if (!event.type.startsWith('tool.')) {
125
+ return
126
+ }
127
+
128
+ if (!previewSessionId) {
129
+ return
130
+ }
131
+
132
+ const candidate = structuredPreviewCandidate(event.payload)
133
+
134
+ if (!candidate) {
135
+ return
136
+ }
137
+
138
+ const desktop = window.NASTECHDesktop
139
+
140
+ if (!desktop?.normalizePreviewTarget) {
141
+ return
142
+ }
143
+
144
+ const sessionId = previewSessionId
145
+ const cwd = currentCwd || ''
146
+ const target = await desktop.normalizePreviewTarget(candidate, cwd || undefined).catch(() => null)
147
+
148
+ if (
149
+ !target ||
150
+ sessionId !== activePreviewSessionId(activeSessionIdRef, routedSessionId, selectedStoredSessionId) ||
151
+ $currentCwd.get() !== cwd
152
+ ) {
153
+ return
154
+ }
155
+
156
+ setSessionPreviewTarget(sessionId, target, 'tool-result', candidate)
157
+ },
158
+ [activeSessionIdRef, currentCwd, previewSessionId, routedSessionId, selectedStoredSessionId]
159
+ )
160
+
161
+ const restartPreviewServer = useCallback(
162
+ async (url: string, context?: string) => {
163
+ const sessionId = activeSessionIdRef.current
164
+
165
+ if (!sessionId) {
166
+ throw new Error('No active session for background restart')
167
+ }
168
+
169
+ const cwd = $currentCwd.get() || currentCwd || ''
170
+
171
+ const result = await requestGateway<{ task_id?: string }>('preview.restart', {
172
+ context: context || undefined,
173
+ cwd: cwd || undefined,
174
+ session_id: sessionId,
175
+ url
176
+ })
177
+
178
+ const taskId = result.task_id || ''
179
+
180
+ if (!taskId) {
181
+ throw new Error('Background restart did not return a task id')
182
+ }
183
+
184
+ beginPreviewServerRestart(taskId, url)
185
+
186
+ return taskId
187
+ },
188
+ [activeSessionIdRef, currentCwd, requestGateway]
189
+ )
190
+
191
+ const handleDesktopGatewayEvent = useCallback<EventHandler>(
192
+ event => {
193
+ baseHandleGatewayEvent(event)
194
+
195
+ if (event.type === 'preview.restart.complete') {
196
+ const { task_id, text } = asRecord(event.payload)
197
+
198
+ if (typeof task_id === 'string' && task_id) {
199
+ completePreviewServerRestart(task_id, typeof text === 'string' ? text : '')
200
+ }
201
+ } else if (event.type === 'preview.restart.progress') {
202
+ const { task_id, text } = asRecord(event.payload)
203
+
204
+ if (typeof task_id === 'string' && task_id) {
205
+ progressPreviewServerRestart(task_id, typeof text === 'string' ? text : '')
206
+ }
207
+ }
208
+
209
+ if (event.session_id && event.session_id !== activeSessionIdRef.current) {
210
+ return
211
+ }
212
+
213
+ void registerStructuredPreview(event)
214
+
215
+ if ($previewTarget.get()?.kind === 'url' && gatewayEventCompletedFileDiff(event)) {
216
+ requestPreviewReload()
217
+ }
218
+ },
219
+ [activeSessionIdRef, baseHandleGatewayEvent, registerStructuredPreview]
220
+ )
221
+
222
+ return { handleDesktopGatewayEvent, restartPreviewServer }
223
+ }
@@ -0,0 +1,316 @@
1
+ import { cleanup, render } from '@testing-library/react'
2
+ import type { MutableRefObject } from 'react'
3
+ import { useEffect } from 'react'
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
5
+
6
+ import { $sessions, setSessions } from '@/store/session'
7
+ import type { SessionInfo } from '@/types/nastech'
8
+
9
+ import { usePromptActions } from './use-prompt-actions'
10
+
11
+ vi.mock('@/nastech', () => ({
12
+ getProfiles: vi.fn(async () => ({ profiles: [] })),
13
+ setApiRequestProfile: vi.fn(),
14
+ transcribeAudio: vi.fn()
15
+ }))
16
+
17
+ // The active id the desktop holds is the *runtime* session id from
18
+ // session.create — deliberately distinct from the stored DB id here, because
19
+ // that mismatch is the bug: the REST renameSession endpoint resolves against
20
+ // the stored sessions table and 404s on a runtime id. session.title accepts
21
+ // the runtime id directly.
22
+ const RUNTIME_SESSION_ID = 'rt-abc123'
23
+
24
+ function sessionInfo(overrides: Partial<SessionInfo> = {}): SessionInfo {
25
+ return {
26
+ ended_at: null,
27
+ id: RUNTIME_SESSION_ID,
28
+ input_tokens: 0,
29
+ is_active: true,
30
+ last_active: 0,
31
+ message_count: 3,
32
+ model: null,
33
+ output_tokens: 0,
34
+ preview: null,
35
+ source: null,
36
+ started_at: 0,
37
+ title: 'Old title',
38
+ tool_call_count: 0,
39
+ ...overrides
40
+ }
41
+ }
42
+
43
+ interface HarnessHandle {
44
+ steerPrompt: (text: string) => Promise<boolean>
45
+ submitText: (text: string, options?: { attachments?: never[]; fromQueue?: boolean }) => Promise<boolean>
46
+ }
47
+
48
+ function Harness({
49
+ busyRef,
50
+ onReady,
51
+ onSeedState,
52
+ refreshSessions,
53
+ requestGateway
54
+ }: {
55
+ busyRef?: MutableRefObject<boolean>
56
+ onReady: (handle: HarnessHandle) => void
57
+ onSeedState?: (state: Record<string, unknown>) => void
58
+ refreshSessions: () => Promise<void>
59
+ requestGateway: <T>(method: string, params?: Record<string, unknown>) => Promise<T>
60
+ }) {
61
+ const activeSessionIdRef: MutableRefObject<string | null> = { current: RUNTIME_SESSION_ID }
62
+ const selectedStoredSessionIdRef: MutableRefObject<string | null> = { current: RUNTIME_SESSION_ID }
63
+ const localBusyRef = busyRef ?? { current: false }
64
+
65
+ const actions = usePromptActions({
66
+ activeSessionId: RUNTIME_SESSION_ID,
67
+ activeSessionIdRef,
68
+ branchCurrentSession: async () => true,
69
+ busyRef: localBusyRef,
70
+ createBackendSessionForSend: async () => RUNTIME_SESSION_ID,
71
+ handleSkinCommand: () => '',
72
+ refreshSessions,
73
+ requestGateway,
74
+ selectedStoredSessionIdRef,
75
+ startFreshSessionDraft: () => undefined,
76
+ sttEnabled: false,
77
+ updateSessionState: (_sessionId, updater) => {
78
+ // Seed with interrupted:true so we can prove a fresh submit clears it.
79
+ const next = updater({
80
+ messages: [],
81
+ busy: false,
82
+ awaitingResponse: false,
83
+ interrupted: true
84
+ } as never) as unknown as Record<string, unknown>
85
+ onSeedState?.(next)
86
+
87
+ return next as never
88
+ }
89
+ })
90
+
91
+ useEffect(() => {
92
+ onReady({ steerPrompt: actions.steerPrompt, submitText: actions.submitText })
93
+ }, [actions.steerPrompt, actions.submitText, onReady])
94
+
95
+ return null
96
+ }
97
+
98
+ describe('usePromptActions /title', () => {
99
+ beforeEach(() => {
100
+ setSessions(() => [sessionInfo()])
101
+ })
102
+
103
+ afterEach(() => {
104
+ cleanup()
105
+ vi.restoreAllMocks()
106
+ })
107
+
108
+ it('renames via the session.title RPC (with the runtime id), updates the sidebar store, and refreshes', async () => {
109
+ const refreshSessions = vi.fn(async () => undefined)
110
+ const requestGateway = vi.fn(async (method: string) =>
111
+ (method === 'session.title' ? { pending: false, title: 'New title' } : {}) as never
112
+ )
113
+
114
+ let handle: HarnessHandle | null = null
115
+ render(<Harness onReady={h => (handle = h)} refreshSessions={refreshSessions} requestGateway={requestGateway} />)
116
+
117
+ await handle!.submitText('/title New title')
118
+
119
+ // Routes through session.title with the runtime session id — NOT the slash
120
+ // worker (slash.exec) and NOT the REST endpoint. This is the path that
121
+ // resolves the runtime id and persists reliably across platforms.
122
+ expect(requestGateway).toHaveBeenCalledWith('session.title', {
123
+ session_id: RUNTIME_SESSION_ID,
124
+ title: 'New title'
125
+ })
126
+ expect(requestGateway).not.toHaveBeenCalledWith('slash.exec', expect.anything())
127
+ expect(refreshSessions).toHaveBeenCalledTimes(1)
128
+ expect($sessions.get()[0]?.title).toBe('New title')
129
+ })
130
+
131
+ it('reports the queued state when the session row is not persisted yet', async () => {
132
+ const refreshSessions = vi.fn(async () => undefined)
133
+ const requestGateway = vi.fn(async (method: string) =>
134
+ (method === 'session.title' ? { pending: true, title: 'Fresh chat' } : {}) as never
135
+ )
136
+
137
+ let handle: HarnessHandle | null = null
138
+ render(<Harness onReady={h => (handle = h)} refreshSessions={refreshSessions} requestGateway={requestGateway} />)
139
+
140
+ await handle!.submitText('/title Fresh chat')
141
+
142
+ expect(requestGateway).toHaveBeenCalledWith('session.title', {
143
+ session_id: RUNTIME_SESSION_ID,
144
+ title: 'Fresh chat'
145
+ })
146
+ // Even when queued, the sidebar reflects the chosen title optimistically.
147
+ expect(refreshSessions).toHaveBeenCalledTimes(1)
148
+ expect($sessions.get()[0]?.title).toBe('Fresh chat')
149
+ })
150
+
151
+ it('falls through to the slash worker for a bare /title (show current title)', async () => {
152
+ const refreshSessions = vi.fn(async () => undefined)
153
+ const requestGateway = vi.fn(async () => ({ output: 'Title: Old title' }) as never)
154
+
155
+ let handle: HarnessHandle | null = null
156
+ render(<Harness onReady={h => (handle = h)} refreshSessions={refreshSessions} requestGateway={requestGateway} />)
157
+
158
+ await handle!.submitText('/title')
159
+
160
+ expect(requestGateway).not.toHaveBeenCalledWith('session.title', expect.anything())
161
+ expect(requestGateway).toHaveBeenCalledWith('slash.exec', expect.objectContaining({ command: 'title' }))
162
+ })
163
+
164
+ it('surfaces a rename error without touching the sidebar store', async () => {
165
+ const refreshSessions = vi.fn(async () => undefined)
166
+ const requestGateway = vi.fn(async (method: string) => {
167
+ if (method === 'session.title') {
168
+ throw new Error('Title too long')
169
+ }
170
+
171
+ return {} as never
172
+ })
173
+
174
+ let handle: HarnessHandle | null = null
175
+ render(<Harness onReady={h => (handle = h)} refreshSessions={refreshSessions} requestGateway={requestGateway} />)
176
+
177
+ await handle!.submitText('/title way too long title')
178
+
179
+ expect(requestGateway).toHaveBeenCalledWith('session.title', expect.objectContaining({ title: 'way too long title' }))
180
+ expect(refreshSessions).not.toHaveBeenCalled()
181
+ expect($sessions.get()[0]?.title).toBe('Old title')
182
+ })
183
+ })
184
+
185
+ describe('usePromptActions submit / queue drain semantics', () => {
186
+ afterEach(() => {
187
+ cleanup()
188
+ vi.restoreAllMocks()
189
+ })
190
+
191
+ it('clears a leftover interrupted flag on a fresh submit (so the new turn streams)', async () => {
192
+ const seeds: Record<string, unknown>[] = []
193
+ const requestGateway = vi.fn(async () => ({}) as never)
194
+
195
+ let handle: HarnessHandle | null = null
196
+ render(
197
+ <Harness
198
+ onReady={h => (handle = h)}
199
+ onSeedState={s => seeds.push(s)}
200
+ refreshSessions={async () => undefined}
201
+ requestGateway={requestGateway}
202
+ />
203
+ )
204
+
205
+ await handle!.submitText('hello after a stop')
206
+
207
+ // The optimistic seed must reset interrupted:false even though the prior
208
+ // session state had interrupted:true — otherwise the message stream drops
209
+ // every delta of this brand-new turn.
210
+ expect(seeds.length).toBeGreaterThan(0)
211
+ expect(seeds.every(s => s.interrupted === false)).toBe(true)
212
+ expect(requestGateway).toHaveBeenCalledWith('prompt.submit', {
213
+ session_id: RUNTIME_SESSION_ID,
214
+ text: 'hello after a stop'
215
+ })
216
+ })
217
+
218
+ it('a fromQueue drain sends even when busyRef is still true on the settle edge', async () => {
219
+ // busyRef lags $busy by one effect tick on the busy→false settle edge, so a
220
+ // drained queue send would otherwise hit the busy guard and silently no-op.
221
+ const busyRef = { current: true }
222
+ const requestGateway = vi.fn(async () => ({}) as never)
223
+
224
+ let handle: HarnessHandle | null = null
225
+ render(
226
+ <Harness
227
+ busyRef={busyRef}
228
+ onReady={h => (handle = h)}
229
+ refreshSessions={async () => undefined}
230
+ requestGateway={requestGateway}
231
+ />
232
+ )
233
+
234
+ const accepted = await handle!.submitText('queued message', { fromQueue: true })
235
+
236
+ expect(accepted).toBe(true)
237
+ expect(requestGateway).toHaveBeenCalledWith('prompt.submit', {
238
+ session_id: RUNTIME_SESSION_ID,
239
+ text: 'queued message'
240
+ })
241
+ })
242
+
243
+ it('a normal (non-queue) submit still respects the busyRef guard', async () => {
244
+ const busyRef = { current: true }
245
+ const requestGateway = vi.fn(async () => ({}) as never)
246
+
247
+ let handle: HarnessHandle | null = null
248
+ render(
249
+ <Harness
250
+ busyRef={busyRef}
251
+ onReady={h => (handle = h)}
252
+ refreshSessions={async () => undefined}
253
+ requestGateway={requestGateway}
254
+ />
255
+ )
256
+
257
+ const accepted = await handle!.submitText('should be blocked')
258
+
259
+ expect(accepted).toBe(false)
260
+ expect(requestGateway).not.toHaveBeenCalledWith('prompt.submit', expect.anything())
261
+ })
262
+ })
263
+
264
+ describe('usePromptActions steerPrompt', () => {
265
+ afterEach(() => {
266
+ cleanup()
267
+ vi.restoreAllMocks()
268
+ })
269
+
270
+ it('injects the trimmed text via session.steer and reports acceptance on a queued status', async () => {
271
+ const requestGateway = vi.fn(async () => ({ status: 'queued' }) as never)
272
+
273
+ let handle: HarnessHandle | null = null
274
+ render(<Harness onReady={h => (handle = h)} refreshSessions={async () => undefined} requestGateway={requestGateway} />)
275
+
276
+ const accepted = await handle!.steerPrompt(' nudge the run ')
277
+
278
+ expect(accepted).toBe(true)
279
+ // Steer never starts a turn — it rides the live run via session.steer only.
280
+ expect(requestGateway).toHaveBeenCalledWith('session.steer', {
281
+ session_id: RUNTIME_SESSION_ID,
282
+ text: 'nudge the run'
283
+ })
284
+ expect(requestGateway).not.toHaveBeenCalledWith('prompt.submit', expect.anything())
285
+ })
286
+
287
+ it('reports rejection (so the caller queues) when the gateway has no live tool window', async () => {
288
+ const requestGateway = vi.fn(async () => ({ status: 'rejected' }) as never)
289
+
290
+ let handle: HarnessHandle | null = null
291
+ render(<Harness onReady={h => (handle = h)} refreshSessions={async () => undefined} requestGateway={requestGateway} />)
292
+
293
+ expect(await handle!.steerPrompt('too late')).toBe(false)
294
+ })
295
+
296
+ it('reports rejection (never throws) when the steer RPC errors', async () => {
297
+ const requestGateway = vi.fn(async () => {
298
+ throw new Error('agent does not support steer')
299
+ })
300
+
301
+ let handle: HarnessHandle | null = null
302
+ render(<Harness onReady={h => (handle = h)} refreshSessions={async () => undefined} requestGateway={requestGateway} />)
303
+
304
+ expect(await handle!.steerPrompt('boom')).toBe(false)
305
+ })
306
+
307
+ it('skips the RPC entirely for empty text', async () => {
308
+ const requestGateway = vi.fn(async () => ({ status: 'queued' }) as never)
309
+
310
+ let handle: HarnessHandle | null = null
311
+ render(<Harness onReady={h => (handle = h)} refreshSessions={async () => undefined} requestGateway={requestGateway} />)
312
+
313
+ expect(await handle!.steerPrompt(' ')).toBe(false)
314
+ expect(requestGateway).not.toHaveBeenCalled()
315
+ })
316
+ })