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,168 @@
1
+ import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
2
+ import { afterEach, describe, expect, it, vi } from 'vitest'
3
+
4
+ import {
5
+ __resetLinkTitleCache,
6
+ ExternalLink,
7
+ fetchLinkTitle,
8
+ hostPathLabel,
9
+ isTitleFetchable,
10
+ LinkifiedText,
11
+ PrettyLink,
12
+ urlSlugTitleLabel
13
+ } from './external-link'
14
+
15
+ const desktopWindow = window as unknown as { NASTECHDesktop?: Window['NASTECHDesktop'] }
16
+ const initialNasTechDesktop = desktopWindow.NASTECHDesktop
17
+
18
+ function installDesktopBridge(partial: Partial<Window['NASTECHDesktop']> = {}) {
19
+ desktopWindow.NASTECHDesktop = {
20
+ fetchLinkTitle: vi.fn().mockResolvedValue(''),
21
+ openExternal: vi.fn().mockResolvedValue(undefined),
22
+ ...partial
23
+ } as unknown as Window['NASTECHDesktop']
24
+ }
25
+
26
+ afterEach(() => {
27
+ __resetLinkTitleCache()
28
+ vi.restoreAllMocks()
29
+ cleanup()
30
+
31
+ if (initialNasTechDesktop) {
32
+ desktopWindow.NASTECHDesktop = initialNasTechDesktop
33
+ } else {
34
+ delete desktopWindow.NASTECHDesktop
35
+ }
36
+ })
37
+
38
+ describe('external link helpers', () => {
39
+ it('formats URL fallbacks as host + path', () => {
40
+ expect(
41
+ hostPathLabel(
42
+ 'https://www.getyourguide.com/culebra-island-l145468/from-fajardo-full-day-cordillera-islands-catamaran-tour-t19894/'
43
+ )
44
+ ).toBe('getyourguide.com/culebra-island-l145468/from-fajardo-full-day-cordillera-islands-catamaran-tour-t19894')
45
+ })
46
+
47
+ it('derives readable title fallbacks from URL slugs', () => {
48
+ expect(
49
+ urlSlugTitleLabel(
50
+ 'https://www.getyourguide.com/fajardo-l882/from-fajardo-icacos-island-full-day-catamaran-trip-t19891/'
51
+ )
52
+ ).toBe('From Fajardo Icacos Island Full Day Catamaran Trip')
53
+ })
54
+
55
+ it('filters out local/non-http targets for title fetches', () => {
56
+ expect(isTitleFetchable('https://www.expedia.com/things-to-do/foo')).toBe(true)
57
+ expect(isTitleFetchable('http://localhost:5174')).toBe(false)
58
+ expect(isTitleFetchable('file:///tmp/demo.html')).toBe(false)
59
+ expect(isTitleFetchable('mailto:hello@example.com')).toBe(false)
60
+ })
61
+
62
+ it('deduplicates in-flight title fetches and caches results', async () => {
63
+ const bridge = vi.fn().mockResolvedValue('El Yunque Tour Water Slide, Rope Swing & Pickup')
64
+ installDesktopBridge({ fetchLinkTitle: bridge as unknown as Window['NASTECHDesktop']['fetchLinkTitle'] })
65
+
66
+ const url =
67
+ 'https://www.expedia.com/things-to-do/puerto-rico-el-yunque-rainforest-adventure-with-transport.a46272756.activity-details'
68
+
69
+ const [first, second] = await Promise.all([fetchLinkTitle(url), fetchLinkTitle(url)])
70
+
71
+ expect(first).toBe('El Yunque Tour Water Slide, Rope Swing & Pickup')
72
+ expect(second).toBe('El Yunque Tour Water Slide, Rope Swing & Pickup')
73
+ expect(bridge).toHaveBeenCalledTimes(1)
74
+
75
+ const third = await fetchLinkTitle(url)
76
+
77
+ expect(third).toBe('El Yunque Tour Water Slide, Rope Swing & Pickup')
78
+ expect(bridge).toHaveBeenCalledTimes(1)
79
+ })
80
+
81
+ it('shares cache across protocol/www URL variants', async () => {
82
+ const bridge = vi.fn().mockResolvedValue('Shared Canonical Title')
83
+ installDesktopBridge({ fetchLinkTitle: bridge as unknown as Window['NASTECHDesktop']['fetchLinkTitle'] })
84
+
85
+ const first = 'https://www.getyourguide.com/san-juan-puerto-rico-l355/sunset-tours-tc306/'
86
+ const second = 'http://getyourguide.com/san-juan-puerto-rico-l355/sunset-tours-tc306/'
87
+
88
+ const [a, b] = await Promise.all([fetchLinkTitle(first), fetchLinkTitle(second)])
89
+
90
+ expect(a).toBe('Shared Canonical Title')
91
+ expect(b).toBe('Shared Canonical Title')
92
+ expect(bridge).toHaveBeenCalledTimes(1)
93
+ })
94
+
95
+ it('opens links via the desktop bridge', () => {
96
+ const openExternal = vi.fn().mockResolvedValue(undefined)
97
+ installDesktopBridge({ openExternal: openExternal as unknown as Window['NASTECHDesktop']['openExternal'] })
98
+
99
+ render(<ExternalLink href="https://example.com/path/to/resource">Example link</ExternalLink>)
100
+
101
+ fireEvent.click(screen.getByRole('link', { name: 'Example link' }))
102
+ expect(openExternal).toHaveBeenCalledWith('https://example.com/path/to/resource')
103
+ })
104
+
105
+ it('shows a trailing external-link icon', () => {
106
+ installDesktopBridge()
107
+
108
+ render(<ExternalLink href="https://example.com/path/to/resource">Example link</ExternalLink>)
109
+
110
+ const link = screen.getByRole('link', { name: 'Example link' })
111
+ expect(link.querySelector('svg')).toBeTruthy()
112
+ })
113
+
114
+ it('renders pretty links with fetched titles and no host suffix', async () => {
115
+ const bridge = vi.fn().mockResolvedValue('From Fajardo: Full-Day Culebra Islands Catamaran Tour')
116
+ installDesktopBridge({ fetchLinkTitle: bridge as unknown as Window['NASTECHDesktop']['fetchLinkTitle'] })
117
+
118
+ const url =
119
+ 'https://www.getyourguide.com/culebra-island-l145468/from-fajardo-full-day-cordillera-islands-catamaran-tour-t19894/'
120
+
121
+ render(<LinkifiedText text={`Read ${url}`} />)
122
+
123
+ const link = screen.getByTitle(url)
124
+ expect(link.textContent).toContain('From Fajardo Full Day Cordillera Islands Catamaran Tour')
125
+
126
+ await waitFor(() => {
127
+ expect(link.textContent).toContain('From Fajardo: Full-Day Culebra Islands Catamaran Tour')
128
+ })
129
+ expect(link.textContent).not.toContain('getyourguide.com')
130
+ })
131
+
132
+ it('shows host/path fallback when title is unavailable', () => {
133
+ installDesktopBridge()
134
+ const url = 'https://www.expedia.com/things-to-do/puerto-rico-el-yunque'
135
+
136
+ render(<PrettyLink href={url} />)
137
+
138
+ const link = screen.getByTitle(url)
139
+
140
+ expect(link.textContent).toBe('Puerto Rico El Yunque')
141
+ })
142
+
143
+ it('ignores error-like fetched titles and falls back to slug label', async () => {
144
+ const bridge = vi.fn().mockResolvedValue('GetYourGuide – Error')
145
+ installDesktopBridge({ fetchLinkTitle: bridge as unknown as Window['NASTECHDesktop']['fetchLinkTitle'] })
146
+
147
+ const url =
148
+ 'https://www.getyourguide.com/culebra-island-l145468/from-fajardo-full-day-cordillera-islands-catamaran-tour-t19894/'
149
+
150
+ render(<PrettyLink href={url} />)
151
+
152
+ const link = screen.getByTitle(url)
153
+ await waitFor(() => {
154
+ expect(link.textContent).toBe('From Fajardo Full Day Cordillera Islands Catamaran Tour')
155
+ })
156
+ })
157
+
158
+ it('normalizes scheme-less links before opening', () => {
159
+ installDesktopBridge()
160
+
161
+ render(<LinkifiedText text="Source expedia.com/things-to-do/puerto-rico-el-yunque-rainforest-adventure" />)
162
+
163
+ const link = screen.getByRole('link')
164
+ expect(link.getAttribute('href')).toBe(
165
+ 'https://expedia.com/things-to-do/puerto-rico-el-yunque-rainforest-adventure'
166
+ )
167
+ })
168
+ })
@@ -0,0 +1,303 @@
1
+ import type { ComponentProps, ReactNode } from 'react'
2
+ import { useEffect, useMemo, useState } from 'react'
3
+
4
+ import { ArrowUpRight } from '@/lib/icons'
5
+
6
+ import { cn } from './utils'
7
+
8
+ const titleCache = new Map<string, string>()
9
+ const titleInflight = new Map<string, Promise<string>>()
10
+ const titleSubs = new Map<string, Set<(value: string) => void>>()
11
+
12
+ const URL_RE =
13
+ /(?:https?:\/\/|www\.)[^\s<>"'`]+[^\s<>"'`.,;:!?)]|[a-z0-9](?:[a-z0-9-]*\.)+[a-z]{2,}(?:\/[^\s<>"'`.,;:!?)]*)?/gi
14
+
15
+ const DOMAIN_RE = /^(?:www\.)?[a-z0-9](?:[a-z0-9-]*\.)+[a-z]{2,}(?::\d+)?(?:[/?#][^\s]*)?$/i
16
+ const SKIP_PROTO_RE = /^(?:file|data|mailto|javascript|blob|chrome|about|NASTECH):/i
17
+ const LOCAL_HOST_RE = /^(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])(?::\d+)?$/i
18
+
19
+ const ERROR_TITLE_RE =
20
+ /\b(?:access denied|attention required|captcha|error|forbidden|just a moment|request blocked|too many requests)\b/i
21
+
22
+ export function normalizeExternalUrl(value: string): string {
23
+ const trimmed = value.trim()
24
+
25
+ if (!trimmed || /^https?:\/\//i.test(trimmed)) {
26
+ return trimmed
27
+ }
28
+
29
+ return DOMAIN_RE.test(trimmed) ? `https://${trimmed}` : trimmed
30
+ }
31
+
32
+ function parseUrl(value: string): null | URL {
33
+ try {
34
+ return new URL(normalizeExternalUrl(value))
35
+ } catch {
36
+ return null
37
+ }
38
+ }
39
+
40
+ function titleCacheKey(value: string): string {
41
+ const url = parseUrl(value)
42
+
43
+ if (!url) {
44
+ return normalizeExternalUrl(value)
45
+ }
46
+
47
+ const host = url.hostname.replace(/^www\./i, '').toLowerCase()
48
+ const pathname = url.pathname === '/' ? '/' : url.pathname.replace(/\/+$/, '') || '/'
49
+
50
+ return `${host}${pathname}${url.search || ''}`
51
+ }
52
+
53
+ export function shortHostLabel(value: string): string {
54
+ return parseUrl(value)?.hostname.replace(/^www\./, '') ?? value
55
+ }
56
+
57
+ export function hostPathLabel(value: string): string {
58
+ const url = parseUrl(value)
59
+
60
+ if (!url) {
61
+ return value
62
+ }
63
+
64
+ const host = url.hostname.replace(/^www\./, '')
65
+ const path = url.pathname && url.pathname !== '/' ? url.pathname.replace(/\/$/, '') : ''
66
+
67
+ return `${host}${path}`
68
+ }
69
+
70
+ function cleanSlug(segment: string): string {
71
+ try {
72
+ return decodeURIComponent(segment)
73
+ .replace(/\.a\d+\..*$/i, '')
74
+ .replace(/\.(?:html?|php|aspx?)$/i, '')
75
+ .replace(/(?:[-_.](?:[a-z]{1,3}\d{2,}|i\d{2,}))+$/i, '')
76
+ .replace(/[_-]+/g, ' ')
77
+ .replace(/\s+/g, ' ')
78
+ .trim()
79
+ } catch {
80
+ return ''
81
+ }
82
+ }
83
+
84
+ export function urlSlugTitleLabel(value: string): string {
85
+ const url = parseUrl(value)
86
+
87
+ for (const segment of url?.pathname.split('/').filter(Boolean).reverse() ?? []) {
88
+ const cleaned = cleanSlug(segment)
89
+
90
+ if (!cleaned || !/[a-z]/i.test(cleaned)) {
91
+ continue
92
+ }
93
+
94
+ if (/^(?:[a-z]{1,3}\d+|\d+)$/i.test(cleaned.replace(/\s+/g, ''))) {
95
+ continue
96
+ }
97
+
98
+ const titled = cleaned.replace(/\b[a-z]/g, c => c.toUpperCase())
99
+
100
+ if (titled.length >= 4) {
101
+ return titled
102
+ }
103
+ }
104
+
105
+ return hostPathLabel(value)
106
+ }
107
+
108
+ export function isTitleFetchable(value: string): boolean {
109
+ if (!value || SKIP_PROTO_RE.test(value)) {
110
+ return false
111
+ }
112
+
113
+ const url = parseUrl(value)
114
+
115
+ return Boolean(url && /^https?:$/.test(url.protocol) && !LOCAL_HOST_RE.test(url.host))
116
+ }
117
+
118
+ export function fetchLinkTitle(url: string): Promise<string> {
119
+ const normalizedUrl = normalizeExternalUrl(url)
120
+ const key = titleCacheKey(normalizedUrl)
121
+
122
+ if (!isTitleFetchable(normalizedUrl)) {
123
+ return Promise.resolve('')
124
+ }
125
+
126
+ if (titleCache.has(key)) {
127
+ return Promise.resolve(titleCache.get(key) ?? '')
128
+ }
129
+
130
+ const pending = titleInflight.get(key)
131
+
132
+ if (pending) {
133
+ return pending
134
+ }
135
+
136
+ const bridge = typeof window === 'undefined' ? undefined : window.NASTECHDesktop?.fetchLinkTitle
137
+
138
+ if (!bridge) {
139
+ titleCache.set(key, '')
140
+
141
+ return Promise.resolve('')
142
+ }
143
+
144
+ const promise = bridge(normalizedUrl)
145
+ .then(value => (value || '').replace(/\s+/g, ' ').trim())
146
+ .then(clean => (clean && !ERROR_TITLE_RE.test(clean) ? clean : ''))
147
+ .catch(() => '')
148
+ .then(safe => {
149
+ titleCache.set(key, safe)
150
+ titleInflight.delete(key)
151
+ titleSubs.get(key)?.forEach(sub => sub(safe))
152
+
153
+ return safe
154
+ })
155
+
156
+ titleInflight.set(key, promise)
157
+
158
+ return promise
159
+ }
160
+
161
+ export function useLinkTitle(url?: null | string): string {
162
+ const normalizedUrl = useMemo(() => (url ? normalizeExternalUrl(url) : ''), [url])
163
+ const key = useMemo(() => (normalizedUrl ? titleCacheKey(normalizedUrl) : ''), [normalizedUrl])
164
+ const [title, setTitle] = useState(() => (key ? (titleCache.get(key) ?? '') : ''))
165
+
166
+ useEffect(() => {
167
+ setTitle(key ? (titleCache.get(key) ?? '') : '')
168
+
169
+ if (!key || !isTitleFetchable(normalizedUrl)) {
170
+ return
171
+ }
172
+
173
+ const subs = titleSubs.get(key) ?? new Set<(value: string) => void>()
174
+
175
+ subs.add(setTitle)
176
+ titleSubs.set(key, subs)
177
+ void fetchLinkTitle(normalizedUrl)
178
+
179
+ return () => {
180
+ subs.delete(setTitle)
181
+
182
+ if (!subs.size) {
183
+ titleSubs.delete(key)
184
+ }
185
+ }
186
+ }, [key, normalizedUrl])
187
+
188
+ return title
189
+ }
190
+
191
+ export function openExternalLink(href: string): void {
192
+ if (href) {
193
+ void window.NASTECHDesktop?.openExternal?.(href)
194
+ }
195
+ }
196
+
197
+ interface ExternalLinkProps extends Omit<ComponentProps<'a'>, 'href' | 'target'> {
198
+ href: string
199
+ children?: ReactNode
200
+ showExternalIcon?: boolean
201
+ }
202
+
203
+ export function ExternalLinkIcon({ className }: { className?: string }) {
204
+ return <ArrowUpRight aria-hidden className={cn('ml-1 inline size-[0.78em] align-[-0.08em] opacity-70', className)} />
205
+ }
206
+
207
+ export function ExternalLink({
208
+ children,
209
+ className,
210
+ href,
211
+ onClick,
212
+ showExternalIcon = true,
213
+ ...rest
214
+ }: ExternalLinkProps) {
215
+ const target = normalizeExternalUrl(href)
216
+
217
+ return (
218
+ <a
219
+ className={cn('font-semibold text-foreground underline underline-offset-4 decoration-current/20', className)}
220
+ href={target}
221
+ onClick={event => {
222
+ event.stopPropagation()
223
+ onClick?.(event)
224
+
225
+ if (event.defaultPrevented) {
226
+ return
227
+ }
228
+
229
+ event.preventDefault()
230
+ openExternalLink(target)
231
+ }}
232
+ rel="noopener noreferrer"
233
+ target="_blank"
234
+ {...rest}
235
+ >
236
+ {children ?? urlSlugTitleLabel(target)}
237
+ {showExternalIcon && <ExternalLinkIcon />}
238
+ </a>
239
+ )
240
+ }
241
+
242
+ interface PrettyLinkProps extends Omit<ComponentProps<'a'>, 'href' | 'target'> {
243
+ href: string
244
+ label?: string
245
+ fallbackLabel?: string
246
+ }
247
+
248
+ export function PrettyLink({ className, fallbackLabel, href, label, ...rest }: PrettyLinkProps) {
249
+ const target = useMemo(() => normalizeExternalUrl(href), [href])
250
+ const fetched = useLinkTitle(label ? null : target)
251
+ const display = fetched || label?.trim() || fallbackLabel?.trim() || urlSlugTitleLabel(target)
252
+
253
+ return (
254
+ <ExternalLink className={cn('wrap-break-word', className)} href={target} title={target} {...rest}>
255
+ <span className="font-medium">{display}</span>
256
+ </ExternalLink>
257
+ )
258
+ }
259
+
260
+ interface LinkifiedTextProps {
261
+ className?: string
262
+ text: string
263
+ pretty?: boolean
264
+ }
265
+
266
+ export function LinkifiedText({ className, pretty = true, text }: LinkifiedTextProps) {
267
+ const nodes: ReactNode[] = []
268
+ let cursor = 0
269
+
270
+ for (const match of text.matchAll(URL_RE)) {
271
+ const raw = match[0]
272
+ const url = normalizeExternalUrl(raw)
273
+ const index = match.index ?? 0
274
+
275
+ if (index > cursor) {
276
+ nodes.push(text.slice(cursor, index))
277
+ }
278
+
279
+ nodes.push(
280
+ pretty ? (
281
+ <PrettyLink href={url} key={`${url}-${index}`} />
282
+ ) : (
283
+ <ExternalLink href={url} key={`${url}-${index}`}>
284
+ {raw}
285
+ </ExternalLink>
286
+ )
287
+ )
288
+
289
+ cursor = index + raw.length
290
+ }
291
+
292
+ if (cursor < text.length) {
293
+ nodes.push(text.slice(cursor))
294
+ }
295
+
296
+ return <span className={className}>{nodes.length ? nodes : text}</span>
297
+ }
298
+
299
+ export function __resetLinkTitleCache(): void {
300
+ titleCache.clear()
301
+ titleInflight.clear()
302
+ titleSubs.clear()
303
+ }
@@ -0,0 +1,27 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { gatewayEventRequiresSessionId } from './gateway-events'
4
+
5
+ describe('gateway event routing', () => {
6
+ it('drops only unscoped subagent events (genuinely background work)', () => {
7
+ expect(gatewayEventRequiresSessionId('subagent.progress')).toBe(true)
8
+ expect(gatewayEventRequiresSessionId('subagent.start')).toBe(true)
9
+ })
10
+
11
+ it('attributes unscoped foreground turn events to the active chat', () => {
12
+ // These must NOT be dropped when unscoped — they are the focused turn's own
13
+ // output, and dropping them loses the live response until a refetch (#42178).
14
+ expect(gatewayEventRequiresSessionId('message.delta')).toBe(false)
15
+ expect(gatewayEventRequiresSessionId('message.complete')).toBe(false)
16
+ expect(gatewayEventRequiresSessionId('reasoning.delta')).toBe(false)
17
+ expect(gatewayEventRequiresSessionId('tool.start')).toBe(false)
18
+ expect(gatewayEventRequiresSessionId('approval.request')).toBe(false)
19
+ })
20
+
21
+ it('allows global events to remain unscoped', () => {
22
+ expect(gatewayEventRequiresSessionId('gateway.ready')).toBe(false)
23
+ expect(gatewayEventRequiresSessionId('preview.restart.progress')).toBe(false)
24
+ expect(gatewayEventRequiresSessionId('session.info')).toBe(false)
25
+ expect(gatewayEventRequiresSessionId(undefined)).toBe(false)
26
+ })
27
+ })
@@ -0,0 +1,49 @@
1
+ import type { StatusbarMenuItem } from '@/app/shell/statusbar-controls'
2
+
3
+ const LOG_TAIL = 5
4
+
5
+ interface RpcEventLike {
6
+ payload?: unknown
7
+ type?: string
8
+ }
9
+
10
+ function asRecord(payload: unknown): Record<string, unknown> {
11
+ return payload && typeof payload === 'object' ? (payload as Record<string, unknown>) : {}
12
+ }
13
+
14
+ const SCOPED_EVENT_PREFIXES = ['subagent.']
15
+
16
+ export function gatewayEventRequiresSessionId(type: string | undefined): boolean {
17
+ if (type == null) return false
18
+ return SCOPED_EVENT_PREFIXES.some(prefix => type.startsWith(prefix))
19
+ }
20
+
21
+ export function gatewayEventCompletedFileDiff(event: RpcEventLike): boolean {
22
+ if (event.type !== 'tool.complete') {
23
+ return false
24
+ }
25
+
26
+ const diff = asRecord(event.payload).inline_diff
27
+
28
+ return typeof diff === 'string' && diff.trim().length > 0
29
+ }
30
+
31
+ export function buildGatewayLogItems(lines: readonly string[]): readonly StatusbarMenuItem[] {
32
+ if (lines.length === 0) {
33
+ return [
34
+ {
35
+ className: 'text-muted-foreground',
36
+ disabled: true,
37
+ id: 'gateway-log-empty',
38
+ label: 'No recent gateway log lines'
39
+ }
40
+ ]
41
+ }
42
+
43
+ return lines.slice(-LOG_TAIL).map((line, index) => ({
44
+ className: 'font-mono text-[0.68rem] text-muted-foreground',
45
+ disabled: true,
46
+ id: `gateway-log:${index}`,
47
+ label: line.trim().slice(0, 120) || '(blank log line)'
48
+ }))
49
+ }
@@ -0,0 +1,78 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+
3
+ import { GatewayReauthRequiredError, isGatewayReauthRequired, resolveGatewayWsUrl } from './gateway-ws-url'
4
+
5
+ const oauthConn = { authMode: 'oauth' as const, wsUrl: 'ws://host/api/ws?ticket=stale' }
6
+ const tokenConn = { authMode: 'token' as const, wsUrl: 'ws://host/api/ws?token=abc' }
7
+
8
+ describe('resolveGatewayWsUrl', () => {
9
+ describe('oauth mode', () => {
10
+ it('uses the freshly minted URL', async () => {
11
+ const getGatewayWsUrl = vi.fn().mockResolvedValue('ws://host/api/ws?ticket=fresh')
12
+ await expect(resolveGatewayWsUrl({ getGatewayWsUrl }, oauthConn)).resolves.toBe('ws://host/api/ws?ticket=fresh')
13
+ expect(getGatewayWsUrl).toHaveBeenCalledOnce()
14
+ })
15
+
16
+ it('throws a reauth error instead of falling back to the stale cached ticket', async () => {
17
+ const getGatewayWsUrl = vi.fn().mockRejectedValue(new Error('401 cookie expired'))
18
+ await expect(resolveGatewayWsUrl({ getGatewayWsUrl }, oauthConn)).rejects.toBeInstanceOf(
19
+ GatewayReauthRequiredError
20
+ )
21
+ })
22
+
23
+ it('preserves the underlying mint failure as the cause', async () => {
24
+ const cause = new Error('401 cookie expired')
25
+ const getGatewayWsUrl = vi.fn().mockRejectedValue(cause)
26
+ const error = await resolveGatewayWsUrl({ getGatewayWsUrl }, oauthConn).catch(e => e)
27
+ expect(error).toBeInstanceOf(GatewayReauthRequiredError)
28
+ expect((error as GatewayReauthRequiredError).cause).toBe(cause)
29
+ })
30
+
31
+ it('throws a reauth error when the preload cannot mint (no method)', async () => {
32
+ await expect(resolveGatewayWsUrl({}, oauthConn)).rejects.toBeInstanceOf(GatewayReauthRequiredError)
33
+ })
34
+
35
+ it('never returns the stale cached ticket on failure', async () => {
36
+ const getGatewayWsUrl = vi.fn().mockRejectedValue(new Error('boom'))
37
+ const result = await resolveGatewayWsUrl({ getGatewayWsUrl }, oauthConn).catch(() => 'threw')
38
+ expect(result).toBe('threw')
39
+ expect(result).not.toBe(oauthConn.wsUrl)
40
+ })
41
+ })
42
+
43
+ describe('token / local mode', () => {
44
+ it('uses the minted URL when available', async () => {
45
+ const getGatewayWsUrl = vi.fn().mockResolvedValue('ws://host/api/ws?token=fresh')
46
+ await expect(resolveGatewayWsUrl({ getGatewayWsUrl }, tokenConn)).resolves.toBe('ws://host/api/ws?token=fresh')
47
+ })
48
+
49
+ it('falls back to the cached URL when minting fails (token is long-lived)', async () => {
50
+ const getGatewayWsUrl = vi.fn().mockRejectedValue(new Error('transient'))
51
+ await expect(resolveGatewayWsUrl({ getGatewayWsUrl }, tokenConn)).resolves.toBe(tokenConn.wsUrl)
52
+ })
53
+
54
+ it('falls back to the cached URL when the preload method is absent', async () => {
55
+ await expect(resolveGatewayWsUrl({}, tokenConn)).resolves.toBe(tokenConn.wsUrl)
56
+ })
57
+
58
+ it('treats a missing authMode as non-oauth (falls back safely)', async () => {
59
+ await expect(resolveGatewayWsUrl({}, { wsUrl: tokenConn.wsUrl })).resolves.toBe(tokenConn.wsUrl)
60
+ })
61
+ })
62
+ })
63
+
64
+ describe('isGatewayReauthRequired', () => {
65
+ it('detects the dedicated error class', () => {
66
+ expect(isGatewayReauthRequired(new GatewayReauthRequiredError('x'))).toBe(true)
67
+ })
68
+
69
+ it('detects plain objects tagged with needsOauthLogin (from the main process)', () => {
70
+ expect(isGatewayReauthRequired({ needsOauthLogin: true })).toBe(true)
71
+ })
72
+
73
+ it('rejects generic errors', () => {
74
+ expect(isGatewayReauthRequired(new Error('connection closed'))).toBe(false)
75
+ expect(isGatewayReauthRequired(null)).toBe(false)
76
+ expect(isGatewayReauthRequired('string')).toBe(false)
77
+ })
78
+ })