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,467 @@
1
+ // Heuristic JSON → human summary for tool results. Default view; technical
2
+ // mode still gets the raw JSON section.
3
+
4
+ const WRAPPER_KEYS = ['data', 'result', 'output', 'response', 'payload'] as const
5
+
6
+ const PRIORITY_KEYS = [
7
+ 'title',
8
+ 'name',
9
+ 'path',
10
+ 'file',
11
+ 'filepath',
12
+ 'url',
13
+ 'href',
14
+ 'link',
15
+ 'status',
16
+ 'id',
17
+ 'message',
18
+ 'summary',
19
+ 'description'
20
+ ] as const
21
+
22
+ const ERROR_KEYS = ['error', 'errors', 'failure', 'exception'] as const
23
+ // 'stderr' deliberately excluded: many CLIs emit informational lines on
24
+ // stderr (npm progress, git's hint:, gcc's `In file included from`) that
25
+ // aren't errors. Treating those as error signal flipped tool cards into
26
+ // destructive styling for healthy commands.
27
+ const ERROR_MSG_KEYS = ['message', 'reason', 'detail'] as const
28
+ const NON_ERROR_TEXT = new Set(['', '0', 'false', 'none', 'null', 'nil', 'ok', 'success', 'n/a', 'na'])
29
+
30
+ type Json = Record<string, unknown>
31
+
32
+ const isRecord = (v: unknown): v is Json => Boolean(v && typeof v === 'object' && !Array.isArray(v))
33
+
34
+ function tryJson(value: string): unknown {
35
+ const t = value.trim()
36
+
37
+ if (!t) {
38
+ return ''
39
+ }
40
+
41
+ if (!/^[{[]|^"/.test(t)) {
42
+ return value
43
+ }
44
+
45
+ try {
46
+ return JSON.parse(t)
47
+ } catch {
48
+ return value
49
+ }
50
+ }
51
+
52
+ const norm = (v: unknown): unknown => (typeof v === 'string' ? tryJson(v) : v)
53
+
54
+ const titleCase = (k: string) =>
55
+ k
56
+ .split(/[_\-.]+/)
57
+ .filter(Boolean)
58
+ .map(p => `${p[0]?.toUpperCase() ?? ''}${p.slice(1)}`)
59
+ .join(' ')
60
+
61
+ const pluralize = (n: number, noun: string) => `${n} ${noun}${n === 1 ? '' : 's'}`
62
+
63
+ function clipInline(value: string, max = 180): string {
64
+ const c = value.replace(/\s+/g, ' ').trim()
65
+
66
+ return c.length > max ? `${c.slice(0, max - 1)}…` : c
67
+ }
68
+
69
+ function clipBlock(value: string, maxChars = 1800, maxLines = 18): string {
70
+ const t = value.trim()
71
+
72
+ if (!t) {
73
+ return ''
74
+ }
75
+
76
+ const lines = t.split('\n')
77
+ let text = lines.slice(0, maxLines).join('\n')
78
+ const clipped = lines.length > maxLines || text.length > maxChars
79
+
80
+ if (text.length > maxChars) {
81
+ text = text.slice(0, maxChars - 1).trimEnd()
82
+ }
83
+
84
+ return clipped && !text.endsWith('…') ? `${text}…` : text
85
+ }
86
+
87
+ function firstString(record: Json, keys: readonly string[]): string {
88
+ for (const k of keys) {
89
+ const v = record[k]
90
+
91
+ if (typeof v === 'string' && v.trim()) {
92
+ return v.trim()
93
+ }
94
+ }
95
+
96
+ return ''
97
+ }
98
+
99
+ function orderedKeys(keys: string[]): string[] {
100
+ const priority = PRIORITY_KEYS.filter(k => keys.includes(k))
101
+ const rest = keys.filter(k => !priority.includes(k as never))
102
+
103
+ return [...priority, ...rest]
104
+ }
105
+
106
+ const isWrapperKey = (k: string) => (WRAPPER_KEYS as readonly string[]).includes(k)
107
+ const skipField = (k: string, v: unknown) => isWrapperKey(k) || ((k === 'success' || k === 'ok') && v === true)
108
+
109
+ function summarizeScalar(v: unknown): string {
110
+ if (typeof v === 'string') {
111
+ return clipInline(v)
112
+ }
113
+
114
+ if (typeof v === 'number' || typeof v === 'boolean') {
115
+ return String(v)
116
+ }
117
+
118
+ return ''
119
+ }
120
+
121
+ function summarizeRecordInline(record: Json, depth: number): string {
122
+ if (depth > 3) {
123
+ return pluralize(Object.keys(record).length, 'field')
124
+ }
125
+
126
+ const title = firstString(record, ['title', 'name', 'path', 'file', 'filepath', 'url', 'href', 'link', 'id'])
127
+ const status = firstString(record, ['status', 'category', 'type'])
128
+ const message = firstString(record, ['snippet', 'summary', 'description', 'message'])
129
+
130
+ if (title && status) {
131
+ return `${clipInline(title, 110)} (${clipInline(status, 54)})`
132
+ }
133
+
134
+ if (title && message && title !== message) {
135
+ return `${clipInline(title, 90)} - ${clipInline(message, 84)}`
136
+ }
137
+
138
+ if (title) {
139
+ return clipInline(title, 150)
140
+ }
141
+
142
+ const pairs = orderedKeys(Object.keys(record))
143
+ .filter(k => !skipField(k, record[k]))
144
+ .map(k => {
145
+ const s = summarizeScalar(record[k])
146
+
147
+ return s ? `${titleCase(k)}: ${s}` : ''
148
+ })
149
+ .filter(Boolean)
150
+ .slice(0, 2)
151
+
152
+ return pairs.length ? pairs.join(' · ') : pluralize(Object.keys(record).length, 'field')
153
+ }
154
+
155
+ function summarizeListItem(item: unknown, depth: number): string {
156
+ const v = norm(item)
157
+
158
+ if (typeof v === 'string') {
159
+ return clipInline(v)
160
+ }
161
+
162
+ if (typeof v === 'number' || typeof v === 'boolean') {
163
+ return String(v)
164
+ }
165
+
166
+ if (v == null) {
167
+ return ''
168
+ }
169
+
170
+ if (Array.isArray(v)) {
171
+ return pluralize(v.length, 'item')
172
+ }
173
+
174
+ if (isRecord(v)) {
175
+ return summarizeRecordInline(v, depth + 1)
176
+ }
177
+
178
+ return clipInline(String(v))
179
+ }
180
+
181
+ function formatFieldValue(value: unknown, depth: number): string {
182
+ const v = norm(value)
183
+ const scalar = summarizeScalar(v)
184
+
185
+ if (scalar) {
186
+ return scalar
187
+ }
188
+
189
+ if (v == null) {
190
+ return ''
191
+ }
192
+
193
+ if (Array.isArray(v)) {
194
+ if (!v.length) {
195
+ return ''
196
+ }
197
+
198
+ const scalars = v.map(summarizeScalar).filter(Boolean)
199
+
200
+ if (scalars.length === v.length && v.length <= 4) {
201
+ return clipInline(scalars.join(', '))
202
+ }
203
+
204
+ const first = summarizeListItem(v[0], depth + 1)
205
+
206
+ return first ? `${pluralize(v.length, 'item')} (${first})` : pluralize(v.length, 'item')
207
+ }
208
+
209
+ if (isRecord(v)) {
210
+ return summarizeRecordInline(v, depth + 1)
211
+ }
212
+
213
+ return clipInline(String(v))
214
+ }
215
+
216
+ // "Returned N items" / "0 items" / "Returned an empty object" are all
217
+ // noise — better to render nothing and let the title carry the signal.
218
+ function formatArraySummary(value: unknown[], depth: number): string {
219
+ if (!value.length) {
220
+ return ''
221
+ }
222
+
223
+ const max = 6
224
+
225
+ const lines = value
226
+ .slice(0, max)
227
+ .map(item => summarizeListItem(item, depth + 1))
228
+ .filter(Boolean)
229
+ .map(l => `- ${l}`)
230
+
231
+ if (!lines.length) {
232
+ return ''
233
+ }
234
+
235
+ if (value.length > max) {
236
+ const remaining = value.length - max
237
+ lines.push(`- … ${remaining} more ${remaining === 1 ? 'item' : 'items'}`)
238
+ }
239
+
240
+ return lines.join('\n')
241
+ }
242
+
243
+ function formatRecordSummary(record: Json, depth: number): string {
244
+ const keys = Object.keys(record)
245
+
246
+ if (!keys.length) {
247
+ return ''
248
+ }
249
+
250
+ if (depth <= 2) {
251
+ const direct = firstString(record, ['message', 'summary', 'description', 'preview', 'text', 'content'])
252
+ const meaningful = keys.filter(k => !skipField(k, record[k]) && !isWrapperKey(k))
253
+
254
+ if (direct && meaningful.length <= 1) {
255
+ return clipBlock(direct)
256
+ }
257
+ }
258
+
259
+ const candidates = orderedKeys(keys).filter(k => !skipField(k, record[k]))
260
+ const max = 8
261
+ const lines: string[] = []
262
+
263
+ for (const k of candidates) {
264
+ const v = formatFieldValue(record[k], depth + 1)
265
+
266
+ if (!v) {
267
+ continue
268
+ }
269
+
270
+ lines.push(`- ${titleCase(k)}: ${v}`)
271
+
272
+ if (lines.length >= max) {
273
+ break
274
+ }
275
+ }
276
+
277
+ if (!lines.length) {
278
+ return ''
279
+ }
280
+
281
+ if (candidates.length > lines.length) {
282
+ const remaining = candidates.length - lines.length
283
+ lines.push(`- … ${remaining} more ${remaining === 1 ? 'field' : 'fields'}`)
284
+ }
285
+
286
+ return lines.join('\n')
287
+ }
288
+
289
+ function formatSummaryValue(value: unknown, depth: number): string {
290
+ if (depth > 4) {
291
+ return ''
292
+ }
293
+
294
+ const v = norm(value)
295
+
296
+ if (typeof v === 'string') {
297
+ return clipBlock(v)
298
+ }
299
+
300
+ if (typeof v === 'number' || typeof v === 'boolean') {
301
+ return String(v)
302
+ }
303
+
304
+ if (v == null) {
305
+ return ''
306
+ }
307
+
308
+ if (Array.isArray(v)) {
309
+ return formatArraySummary(v, depth + 1)
310
+ }
311
+
312
+ if (isRecord(v)) {
313
+ return formatRecordSummary(v, depth + 1)
314
+ }
315
+
316
+ return clipInline(String(v))
317
+ }
318
+
319
+ function unwrapPayload(value: unknown): unknown {
320
+ let cur: unknown = norm(value)
321
+
322
+ for (let i = 0; i < 4; i += 1) {
323
+ if (!isRecord(cur)) {
324
+ return cur
325
+ }
326
+
327
+ const record = cur
328
+ const key = WRAPPER_KEYS.find(k => record[k] != null)
329
+
330
+ if (!key) {
331
+ return record
332
+ }
333
+
334
+ cur = norm(record[key])
335
+ }
336
+
337
+ return cur
338
+ }
339
+
340
+ function hasMeaningfulErrorValue(value: unknown): boolean {
341
+ const v = norm(value)
342
+
343
+ if (v == null) {
344
+ return false
345
+ }
346
+
347
+ if (typeof v === 'string') {
348
+ return !NON_ERROR_TEXT.has(v.trim().toLowerCase())
349
+ }
350
+
351
+ if (typeof v === 'boolean') {
352
+ return v
353
+ }
354
+
355
+ if (typeof v === 'number') {
356
+ return v !== 0
357
+ }
358
+
359
+ if (Array.isArray(v)) {
360
+ return v.some(hasMeaningfulErrorValue)
361
+ }
362
+
363
+ if (isRecord(v)) {
364
+ return Object.keys(v).length > 0
365
+ }
366
+
367
+ return true
368
+ }
369
+
370
+ function hasErrorSignal(record: Json): boolean {
371
+ const status = typeof record.status === 'string' ? record.status : ''
372
+
373
+ return (
374
+ record.success === false ||
375
+ record.ok === false ||
376
+ /\b(error|failed|failure|fatal|exception)\b/i.test(status) ||
377
+ ERROR_KEYS.some(k => hasMeaningfulErrorValue(record[k]))
378
+ )
379
+ }
380
+
381
+ function valueErrorText(value: unknown): string {
382
+ const v = norm(value)
383
+
384
+ if (typeof v === 'string') {
385
+ return hasMeaningfulErrorValue(v) ? clipBlock(v, 700, 12) : ''
386
+ }
387
+
388
+ if (Array.isArray(v)) {
389
+ return clipBlock(v.map(valueErrorText).filter(Boolean).slice(0, 3).join('; '), 700, 12)
390
+ }
391
+
392
+ if (isRecord(v)) {
393
+ const direct = firstString(v, ERROR_MSG_KEYS)
394
+
395
+ if (direct) {
396
+ return clipBlock(direct, 700, 12)
397
+ }
398
+ }
399
+
400
+ return ''
401
+ }
402
+
403
+ function findNestedError(value: unknown, depth: number, seen: Set<unknown>): string {
404
+ if (depth > 5) {
405
+ return ''
406
+ }
407
+
408
+ const v = norm(value)
409
+
410
+ if (!v || typeof v !== 'object' || seen.has(v)) {
411
+ return ''
412
+ }
413
+
414
+ seen.add(v)
415
+
416
+ if (Array.isArray(v)) {
417
+ for (const item of v) {
418
+ const nested = findNestedError(item, depth + 1, seen)
419
+
420
+ if (nested) {
421
+ return nested
422
+ }
423
+ }
424
+
425
+ return ''
426
+ }
427
+
428
+ const record = v as Json
429
+
430
+ for (const k of ERROR_KEYS) {
431
+ if (!hasMeaningfulErrorValue(record[k])) {
432
+ continue
433
+ }
434
+
435
+ const text = valueErrorText(record[k])
436
+
437
+ if (text) {
438
+ return text
439
+ }
440
+ }
441
+
442
+ if (hasErrorSignal(record)) {
443
+ const direct = firstString(record, ERROR_MSG_KEYS)
444
+
445
+ if (direct) {
446
+ return clipBlock(direct, 700, 12)
447
+ }
448
+ }
449
+
450
+ for (const k of [...ERROR_KEYS, ...WRAPPER_KEYS, 'details', 'meta']) {
451
+ const nested = findNestedError(record[k], depth + 1, seen)
452
+
453
+ if (nested) {
454
+ return nested
455
+ }
456
+ }
457
+
458
+ return ''
459
+ }
460
+
461
+ export function formatToolResultSummary(value: unknown): string {
462
+ return formatSummaryValue(unwrapPayload(value), 0) || formatSummaryValue(value, 0)
463
+ }
464
+
465
+ export function extractToolErrorMessage(value: unknown): string {
466
+ return findNestedError(value, 0, new Set())
467
+ }
@@ -0,0 +1,38 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { resolveUpdateCopy } from './update-copy'
4
+
5
+ const copy = {
6
+ availableTitle: 'New update available',
7
+ availableBody: 'A new version of NasTech is ready to install.',
8
+ availableTitleBackend: 'Backend update available',
9
+ availableBodyBackend: 'A newer version of the connected NasTech backend is ready to install.',
10
+ availableBodyNoChangelog: 'A newer version is ready. Release notes aren’t available for this install type.'
11
+ }
12
+
13
+ describe('resolveUpdateCopy', () => {
14
+ it('client target with commits: client title + client body', () => {
15
+ const r = resolveUpdateCopy({ target: 'client', shownItems: 5, copy })
16
+ expect(r.title).toBe('New update available')
17
+ expect(r.body).toBe('A new version of NasTech is ready to install.')
18
+ })
19
+
20
+ it('backend target with commits: names the backend in title and body', () => {
21
+ const r = resolveUpdateCopy({ target: 'backend', shownItems: 5, copy })
22
+ expect(r.title).toBe('Backend update available')
23
+ expect(r.body).toContain('backend')
24
+ })
25
+
26
+ it('no changelog (pip/non-git backend): degrades honestly, still names backend target in title', () => {
27
+ const r = resolveUpdateCopy({ target: 'backend', shownItems: 0, copy })
28
+ expect(r.title).toBe('Backend update available')
29
+ // Body must NOT pretend there are notes — it states they're unavailable.
30
+ expect(r.body).toBe(copy.availableBodyNoChangelog)
31
+ })
32
+
33
+ it('no changelog on client: same honest degrade', () => {
34
+ const r = resolveUpdateCopy({ target: 'client', shownItems: 0, copy })
35
+ expect(r.title).toBe('New update available')
36
+ expect(r.body).toBe(copy.availableBodyNoChangelog)
37
+ })
38
+ })
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Pure copy-selection for the updates overlay's "available" state.
3
+ *
4
+ * Names the update target (client vs the connected backend in remote mode) and
5
+ * degrades honestly when there's no commit changelog to show (e.g. a pip /
6
+ * non-git backend where `git log` yields nothing) instead of generic filler.
7
+ *
8
+ * Extracted from updates-overlay.tsx so the wording logic is unit-testable.
9
+ */
10
+
11
+ export type UpdateTarget = 'client' | 'backend'
12
+
13
+ export interface UpdateCopyStrings {
14
+ availableTitle: string
15
+ availableBody: string
16
+ availableTitleBackend: string
17
+ availableBodyBackend: string
18
+ availableBodyNoChangelog: string
19
+ }
20
+
21
+ export interface ResolveUpdateCopyInput {
22
+ target: UpdateTarget
23
+ /** Number of commit rows actually shown in the changelog. 0 → no notes. */
24
+ shownItems: number
25
+ copy: UpdateCopyStrings
26
+ }
27
+
28
+ export interface UpdateCopyResult {
29
+ title: string
30
+ body: string
31
+ }
32
+
33
+ export function resolveUpdateCopy({ target, shownItems, copy }: ResolveUpdateCopyInput): UpdateCopyResult {
34
+ const title = target === 'backend' ? copy.availableTitleBackend : copy.availableTitle
35
+
36
+ const body =
37
+ shownItems === 0
38
+ ? copy.availableBodyNoChangelog
39
+ : target === 'backend'
40
+ ? copy.availableBodyBackend
41
+ : copy.availableBody
42
+
43
+ return { title, body }
44
+ }
@@ -0,0 +1,100 @@
1
+ import { useCallback, useRef } from 'react'
2
+
3
+ /**
4
+ * One-shot enter animation via the Web Animations API.
5
+ *
6
+ * Returns a callback ref. The animation fires exactly once when the element
7
+ * first attaches to the DOM and never replays for an already-mounted node —
8
+ * this is deliberate. CSS-transition + `@starting-style` is fragile here
9
+ * because:
10
+ * - Streaming deltas constantly invalidate ancestor state, which can
11
+ * re-trigger transitions on unrelated descendants.
12
+ * - `@starting-style` only covers DOM insertion / first-match, but any
13
+ * style restart during the message lifecycle replays the transition.
14
+ * - Some Chromium versions reset transitions when an attribute on an
15
+ * ancestor toggles, even if the descendant's properties never change.
16
+ *
17
+ * `el.animate(...)` runs against the element directly and is independent of
18
+ * CSS rule churn — it plays once, finishes, and is done. If the element
19
+ * unmounts and re-mounts, the callback ref runs again and replays it
20
+ * (correct behaviour).
21
+ *
22
+ * `enabled` is captured at mount-time only — flipping it later doesn't
23
+ * suddenly play the animation on existing nodes.
24
+ */
25
+ const playedAnimationKeys = new Set<string>()
26
+ const playedAnimationOrder: string[] = []
27
+ const MAX_TRACKED_KEYS = 2048
28
+
29
+ function hasPlayedAnimation(key: string): boolean {
30
+ return playedAnimationKeys.has(key)
31
+ }
32
+
33
+ function rememberPlayedAnimation(key: string): void {
34
+ if (playedAnimationKeys.has(key)) {
35
+ return
36
+ }
37
+
38
+ playedAnimationKeys.add(key)
39
+ playedAnimationOrder.push(key)
40
+
41
+ if (playedAnimationOrder.length > MAX_TRACKED_KEYS) {
42
+ const evicted = playedAnimationOrder.shift()
43
+
44
+ if (evicted) {
45
+ playedAnimationKeys.delete(evicted)
46
+ }
47
+ }
48
+ }
49
+
50
+ function scheduleMicrotask(cb: () => void): void {
51
+ if (typeof queueMicrotask === 'function') {
52
+ queueMicrotask(cb)
53
+
54
+ return
55
+ }
56
+
57
+ void Promise.resolve().then(cb)
58
+ }
59
+
60
+ export function useEnterAnimation(enabled: boolean, animationKey?: string): (el: HTMLElement | null) => void {
61
+ const enabledRef = useRef(enabled)
62
+ const keyRef = useRef(animationKey)
63
+
64
+ enabledRef.current = enabled
65
+ keyRef.current = animationKey
66
+
67
+ return useCallback((el: HTMLElement | null) => {
68
+ if (!el || !enabledRef.current || typeof window === 'undefined') {
69
+ return
70
+ }
71
+
72
+ if (window.matchMedia?.('(prefers-reduced-motion: reduce)').matches) {
73
+ return
74
+ }
75
+
76
+ const key = keyRef.current
77
+
78
+ if (key && hasPlayedAnimation(key)) {
79
+ return
80
+ }
81
+
82
+ el.animate(
83
+ [
84
+ { opacity: 0, transform: 'translateY(0.375rem)' },
85
+ { opacity: 1, transform: 'translateY(0)' }
86
+ ],
87
+ { duration: 180, easing: 'cubic-bezier(0.16, 1, 0.3, 1)', fill: 'both' }
88
+ )
89
+
90
+ if (key) {
91
+ // In React StrictMode the first mount can be immediately torn down.
92
+ // Only persist "played" once the element survives to the microtask tick.
93
+ scheduleMicrotask(() => {
94
+ if (el.isConnected) {
95
+ rememberPlayedAnimation(key)
96
+ }
97
+ })
98
+ }
99
+ }, [])
100
+ }
@@ -0,0 +1,6 @@
1
+ import { type ClassValue, clsx } from 'clsx'
2
+ import { twMerge } from 'tailwind-merge'
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }