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,229 @@
1
+ // Reproduce + diagnose the "scroll wheel resets position while reading" bug.
2
+ //
3
+ // The complaint (Windows, mouse wheel): scrolling UP through a chat to re-read
4
+ // older content randomly yanks the view to a different position, so you have to
5
+ // fight the scrollbar. Mac users on trackpads don't see it.
6
+ //
7
+ // Hypothesis: the thread scroller has the browser default `overflow-anchor:
8
+ // auto`, and the thread renders items in natural document flow (padding
9
+ // spacers, NOT transforms). When an item above the viewport is measured by
10
+ // @tanstack/react-virtual (its real height differs a lot from the 220px
11
+ // estimate) — or when Shiki/images/fonts reflow it — TWO mechanisms both
12
+ // adjust scrollTop for the same delta: TanStack's measurement compensation AND
13
+ // the browser's native scroll anchoring. The double-correction lurches the
14
+ // view. A mouse wheel's coarse, discrete notches mount/measure several
15
+ // under-estimated turns per tick, so the over-correction is large and visible;
16
+ // a trackpad's ~1-3px/frame keeps it sub-perceptual.
17
+ //
18
+ // This script drives synthetic mouse-wheel-UP scrolling on a long thread and
19
+ // measures how much a tracked on-screen turn jumps, first with
20
+ // `overflow-anchor: auto` (reproduce) then `overflow-anchor: none` (the fix).
21
+ // If the fix run shows dramatically fewer/smaller jumps, the hypothesis holds.
22
+ //
23
+ // Prereq: a running desktop app with remote debugging on 9222, on a thread
24
+ // with enough history to scroll (the longer / more code+tool blocks, the
25
+ // better the repro). Then: node apps/desktop/scripts/diag-scroll-reset.mjs
26
+
27
+ const NOTCHES = 14 // wheel-up ticks per sweep
28
+ const NOTCH_PX = 120 // Windows wheel notch ≈ 120px
29
+ const NOTCH_GAP_MS = 130 // let each smooth-scroll animation settle
30
+ const REVERSE_JUMP_PX = 6 // tracked turn moving UP while scrolling up = wrong way
31
+ const LURCH_PX = 60 // single-frame on-screen jump that reads as a "reset"
32
+
33
+ const list = await (await fetch('http://127.0.0.1:9222/json/list')).json()
34
+ const tgt = list.find(t => t.type === 'page' && t.url.startsWith('http'))
35
+ if (!tgt) {
36
+ console.error('No page target on :9222. Is the desktop app running with --remote-debugging-port=9222?')
37
+ process.exit(1)
38
+ }
39
+ const ws = new WebSocket(tgt.webSocketDebuggerUrl)
40
+ let id = 0
41
+ const pending = new Map()
42
+ ws.addEventListener('message', ev => {
43
+ const m = JSON.parse(ev.data)
44
+ if (m.id != null && pending.has(m.id)) {
45
+ pending.get(m.id)(m)
46
+ pending.delete(m.id)
47
+ }
48
+ })
49
+ await new Promise(r => ws.addEventListener('open', r))
50
+ const send = (m, p = {}) =>
51
+ new Promise(r => {
52
+ const i = ++id
53
+ pending.set(i, r)
54
+ ws.send(JSON.stringify({ id: i, method: m, params: p }))
55
+ })
56
+ const evalP = async expr => {
57
+ const r = await send('Runtime.evaluate', { expression: expr, returnByValue: true })
58
+ if (r.result?.exceptionDetails) throw new Error(r.result.exceptionDetails.text)
59
+ return r.result.result.value
60
+ }
61
+ const sleep = ms => new Promise(r => setTimeout(r, ms))
62
+
63
+ // Install per-sweep instrumentation. `mode` is the overflow-anchor value to
64
+ // force inline so we A/B the exact same thread regardless of any CSS fix.
65
+ // Starts from ~45% down the thread so there's room to scroll up into
66
+ // not-yet-measured turns, tags the turn nearest viewport-center as the anchor,
67
+ // then records (per rAF) scrollTop + that turn's on-screen top, plus every
68
+ // scrollTop *setter* write (TanStack compensation) and ResizeObserver hit.
69
+ async function arm(mode) {
70
+ await evalP(`(() => {
71
+ const v = document.querySelector('[data-slot="aui_thread-viewport"]')
72
+ if (!v) throw new Error('thread viewport not found')
73
+
74
+ // Force the overflow-anchor behavior under test (inline beats CSS).
75
+ v.style.overflowAnchor = ${JSON.stringify(mode)}
76
+
77
+ // Park ~45% down so a wheel-up sweep climbs into estimated-but-unmeasured
78
+ // turns above the fold (where the measurement correction fires).
79
+ v.scrollTop = Math.round(v.scrollHeight * 0.45)
80
+
81
+ // Tag the turn closest to viewport center; we track its on-screen top.
82
+ const vr = v.getBoundingClientRect()
83
+ const center = vr.top + v.clientHeight / 2
84
+ let best = null, bestD = Infinity
85
+ for (const el of v.querySelectorAll('[data-index]')) {
86
+ const r = el.getBoundingClientRect()
87
+ const d = Math.abs((r.top + r.height / 2) - center)
88
+ if (d < bestD) { bestD = d; best = el }
89
+ }
90
+ document.querySelectorAll('[data-se-anchor]').forEach(e => e.removeAttribute('data-se-anchor'))
91
+ if (best) best.setAttribute('data-se-anchor', '1')
92
+ const anchorIndex = best ? best.getAttribute('data-index') : null
93
+
94
+ const samples = []
95
+ const writes = []
96
+ const ros = []
97
+ const t0 = performance.now()
98
+
99
+ // Intercept scrollTop writes → these are JS (TanStack) corrections.
100
+ // Native browser scroll anchoring does NOT go through this setter, so a
101
+ // scrollTop change with no write in the same frame is a native adjust.
102
+ const desc = Object.getOwnPropertyDescriptor(Element.prototype, 'scrollTop')
103
+ Object.defineProperty(v, 'scrollTop', {
104
+ configurable: true,
105
+ get() { return desc.get.call(this) },
106
+ set(val) {
107
+ writes.push({ t: performance.now() - t0, val, sh: this.scrollHeight })
108
+ desc.set.call(this, val)
109
+ }
110
+ })
111
+ window.__restoreScrollTop = () => Object.defineProperty(v, 'scrollTop', desc)
112
+
113
+ const ro = new ResizeObserver(entries => {
114
+ for (const e of entries) {
115
+ ros.push({ t: performance.now() - t0, slot: e.target.getAttribute?.('data-slot') || e.target.tagName, h: Math.round(e.contentRect.height) })
116
+ }
117
+ })
118
+ ro.observe(v)
119
+ if (v.firstElementChild) ro.observe(v.firstElementChild)
120
+
121
+ let running = true
122
+ const tick = () => {
123
+ if (!running) return
124
+ const a = v.querySelector('[data-se-anchor]')
125
+ const ar = a ? a.getBoundingClientRect() : null
126
+ samples.push({
127
+ t: performance.now() - t0,
128
+ st: Math.round(v.scrollTop * 100) / 100,
129
+ sh: v.scrollHeight,
130
+ ch: v.clientHeight,
131
+ atop: ar ? Math.round(ar.top * 100) / 100 : null,
132
+ aconn: !!a
133
+ })
134
+ requestAnimationFrame(tick)
135
+ }
136
+ requestAnimationFrame(tick)
137
+
138
+ window.__se = { samples, writes, ros, anchorIndex, dpr: window.devicePixelRatio, stop() { running = false; ro.disconnect(); window.__restoreScrollTop?.() } }
139
+ return true
140
+ })()`)
141
+ }
142
+
143
+ async function wheelUpSweep() {
144
+ const { x, y } = await evalP(`(() => {
145
+ const v = document.querySelector('[data-slot="aui_thread-viewport"]')
146
+ const r = v.getBoundingClientRect()
147
+ return { x: Math.round(r.left + r.width / 2), y: Math.round(r.top + r.height / 2) }
148
+ })()`)
149
+
150
+ for (let i = 0; i < NOTCHES; i++) {
151
+ await send('Input.dispatchMouseEvent', { type: 'mouseWheel', x, y, deltaX: 0, deltaY: -NOTCH_PX })
152
+ await sleep(NOTCH_GAP_MS)
153
+ }
154
+ await sleep(400)
155
+ }
156
+
157
+ async function collect() {
158
+ const data = JSON.parse(await evalP(`(() => { window.__se.stop(); return JSON.stringify(window.__se) })()`))
159
+ return data
160
+ }
161
+
162
+ function analyze(label, data) {
163
+ const { samples, writes, ros, anchorIndex, dpr } = data
164
+ let reverseJumps = 0
165
+ let reverseSum = 0
166
+ let lurches = 0
167
+ let maxJump = 0
168
+ let nativeMoves = 0
169
+ let prev = null
170
+ for (const s of samples) {
171
+ if (prev && prev.aconn && s.aconn && prev.atop != null && s.atop != null) {
172
+ const dTop = s.atop - prev.atop // wheel-up should move content DOWN → dTop >= 0
173
+ const dSt = s.st - prev.st
174
+ // Native (browser-anchoring) move: scrollTop changed with no setter write in this frame window.
175
+ const wroteThisFrame = writes.some(w => w.t > prev.t && w.t <= s.t)
176
+ if (Math.abs(dSt) > 0.5 && !wroteThisFrame) nativeMoves++
177
+ if (dTop < -REVERSE_JUMP_PX) {
178
+ reverseJumps++
179
+ reverseSum += -dTop
180
+ }
181
+ if (Math.abs(dTop) > LURCH_PX) lurches++
182
+ if (Math.abs(dTop) > maxJump) maxJump = Math.abs(dTop)
183
+ }
184
+ prev = s
185
+ }
186
+ console.log(`\n── ${label} ──`)
187
+ console.log(` devicePixelRatio: ${dpr}${Number.isInteger(dpr) ? '' : ' (fractional — Windows scaling, worsens rounding jitter)'}`)
188
+ console.log(` tracked turn index: ${anchorIndex}`)
189
+ console.log(` rAF frames: ${samples.length}`)
190
+ console.log(` scrollTop writes: ${writes.length} (TanStack measurement corrections)`)
191
+ console.log(` ResizeObserver hits: ${ros.length}`)
192
+ console.log(` native scroll moves: ${nativeMoves} (scrollTop moved with NO JS write = browser anchoring)`)
193
+ console.log(` reverse jumps: ${reverseJumps} (tracked turn yanked UP while scrolling up; total ${reverseSum.toFixed(0)}px)`)
194
+ console.log(` big lurches (>${LURCH_PX}px): ${lurches}`)
195
+ console.log(` max single-frame jump: ${maxJump.toFixed(0)}px`)
196
+ return { reverseJumps, reverseSum, lurches, maxJump, nativeMoves }
197
+ }
198
+
199
+ console.log(`Wheel-up repro: ${NOTCHES} notches × ${NOTCH_PX}px, anchored mid-thread.\n`)
200
+
201
+ await arm('auto')
202
+ await sleep(150)
203
+ await wheelUpSweep()
204
+ const a = analyze('overflow-anchor: auto (current / repro)', await collect())
205
+
206
+ await sleep(300)
207
+
208
+ await arm('none')
209
+ await sleep(150)
210
+ await wheelUpSweep()
211
+ const b = analyze('overflow-anchor: none (proposed fix)', await collect())
212
+
213
+ // Clean up our tag.
214
+ await evalP(`document.querySelectorAll('[data-se-anchor]').forEach(e => e.removeAttribute('data-se-anchor'))`)
215
+
216
+ console.log('\n══ verdict ══')
217
+ const drop = (x, y) => (x === 0 ? (y === 0 ? '0' : 'n/a') : `${Math.round((1 - y / x) * 100)}% fewer`)
218
+ console.log(` reverse jumps: auto=${a.reverseJumps} none=${b.reverseJumps} (${drop(a.reverseJumps, b.reverseJumps)})`)
219
+ console.log(` big lurches: auto=${a.lurches} none=${b.lurches} (${drop(a.lurches, b.lurches)})`)
220
+ console.log(` max jump: auto=${a.maxJump.toFixed(0)}px none=${b.maxJump.toFixed(0)}px`)
221
+ console.log(` native moves: auto=${a.nativeMoves} none=${b.nativeMoves} (browser anchoring should ~vanish at none)`)
222
+ if (a.reverseJumps + a.lurches > 0 && b.reverseJumps + b.lurches < a.reverseJumps + a.lurches) {
223
+ console.log('\n → Jumps drop sharply with overflow-anchor:none → root cause confirmed.')
224
+ } else if (a.reverseJumps + a.lurches === 0) {
225
+ console.log('\n → No jumps captured this run. Use a longer thread (many code/tool blocks),')
226
+ console.log(' raise NOTCHES, and ensure you start scrolled up from the bottom.')
227
+ }
228
+
229
+ ws.close()
@@ -0,0 +1,21 @@
1
+ // Simple eval helper — runs an expression and returns the result.value.
2
+ const targets = await (await fetch('http://127.0.0.1:9222/json')).json()
3
+ const t = targets.find((t) => t.url.includes('5174'))
4
+ const ws = new WebSocket(t.webSocketDebuggerUrl)
5
+ let id = 0
6
+ const pending = new Map()
7
+ ws.addEventListener('message', (ev) => {
8
+ const m = JSON.parse(ev.data)
9
+ if (pending.has(m.id)) { pending.get(m.id)(m); pending.delete(m.id) }
10
+ })
11
+ await new Promise((r) => ws.addEventListener('open', r))
12
+ const send = (method, params) => new Promise((res) => { const i = ++id; pending.set(i, res); ws.send(JSON.stringify({ id: i, method, params })) })
13
+
14
+ const expr = process.argv[2] || '1+1'
15
+ const r = await send('Runtime.evaluate', { expression: expr, returnByValue: true, awaitPromise: true })
16
+ if (r.result.exceptionDetails) {
17
+ console.error('EXCEPTION:', r.result.exceptionDetails.exception?.description)
18
+ } else {
19
+ console.log(JSON.stringify(r.result.result.value, null, 2))
20
+ }
21
+ ws.close()
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env node
2
+ // Leak-detection harness — measure detached DOM, listener count, and FiberNode
3
+ // growth as a function of keystrokes typed.
4
+ //
5
+ // Workflow:
6
+ // 1. Open session, focus composer
7
+ // 2. forceGC; capture baseline counts
8
+ // 3. Repeat N rounds: type M chars, forceGC, capture counts, clear composer
9
+ // 4. Print growth-per-round table
10
+ //
11
+ // Usage:
12
+ // node apps/desktop/scripts/leak-typing.mjs [--rounds=6] [--chars=200] [--cps=40] [--port=9222]
13
+
14
+ import { writeFileSync } from 'node:fs'
15
+
16
+ const args = Object.fromEntries(
17
+ process.argv.slice(2).flatMap(s => {
18
+ const m = s.match(/^--([^=]+)(?:=(.*))?$/)
19
+ return m ? [[m[1], m[2] ?? true]] : []
20
+ })
21
+ )
22
+ const PORT = Number(args.port ?? 9222)
23
+ const ROUNDS = Number(args.rounds ?? 6)
24
+ const CHARS = Number(args.chars ?? 200)
25
+ const CPS = Number(args.cps ?? 40)
26
+
27
+ const log = (...m) => console.log('[leak]', ...m)
28
+
29
+ async function pickRenderer() {
30
+ const list = await (await fetch(`http://127.0.0.1:${PORT}/json/list`)).json()
31
+ return list.find(t => t.type === 'page' && t.url.startsWith('http'))
32
+ }
33
+
34
+ function connect(url) {
35
+ return new Promise((resolve, reject) => {
36
+ const ws = new WebSocket(url)
37
+ let id = 0
38
+ const pending = new Map()
39
+ const events = new Map()
40
+ ws.addEventListener('open', () =>
41
+ resolve({
42
+ send(method, params = {}) {
43
+ const myId = ++id
44
+ ws.send(JSON.stringify({ id: myId, method, params }))
45
+ return new Promise((res, rej) => pending.set(myId, { res, rej }))
46
+ },
47
+ on(method, h) {
48
+ if (!events.has(method)) events.set(method, [])
49
+ events.get(method).push(h)
50
+ },
51
+ close: () => ws.close()
52
+ })
53
+ )
54
+ ws.addEventListener('error', reject)
55
+ ws.addEventListener('message', ev => {
56
+ const m = JSON.parse(typeof ev.data === 'string' ? ev.data : ev.data.toString('utf8'))
57
+ if (m.id != null) {
58
+ const p = pending.get(m.id)
59
+ if (!p) return
60
+ pending.delete(m.id)
61
+ m.error ? p.rej(new Error(m.error.message)) : p.res(m.result)
62
+ } else if (m.method) {
63
+ ;(events.get(m.method) ?? []).forEach(h => h(m.params))
64
+ }
65
+ })
66
+ })
67
+ }
68
+
69
+ async function evalInPage(cdp, expr) {
70
+ const r = await cdp.send('Runtime.evaluate', { expression: expr, returnByValue: true })
71
+ if (r.exceptionDetails) throw new Error(r.exceptionDetails.text)
72
+ return r.result.value
73
+ }
74
+
75
+ async function forceGCAndSettle(cdp) {
76
+ for (let i = 0; i < 3; i++) {
77
+ await cdp.send('HeapProfiler.collectGarbage')
78
+ await new Promise(r => setTimeout(r, 60))
79
+ }
80
+ }
81
+
82
+ async function focusComposer(cdp) {
83
+ return await evalInPage(
84
+ cdp,
85
+ `(() => {
86
+ const el = document.querySelector('[data-slot="composer-rich-input"]')
87
+ if (!el) return false
88
+ el.focus()
89
+ const range = document.createRange()
90
+ range.selectNodeContents(el)
91
+ range.collapse(false)
92
+ const sel = window.getSelection()
93
+ sel.removeAllRanges()
94
+ sel.addRange(range)
95
+ return true
96
+ })()`
97
+ )
98
+ }
99
+
100
+ async function clearComposer(cdp) {
101
+ await evalInPage(
102
+ cdp,
103
+ `(() => {
104
+ const el = document.querySelector('[data-slot="composer-rich-input"]')
105
+ if (!el) return false
106
+ // Clear via the same path as the composer's clear flow:
107
+ // dispatch a single Backspace until empty would be N round-trips; quicker
108
+ // to directly assign empty text and fire input.
109
+ el.innerHTML = ''
110
+ el.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'deleteContentBackward' }))
111
+ el.focus()
112
+ return el.innerText.length === 0
113
+ })()`
114
+ )
115
+ }
116
+
117
+ async function snapshotCounts(cdp) {
118
+ // Counts via Runtime.evaluate using internal V8 counters where possible.
119
+ // For DOM stats we directly query the document.
120
+ // Performance metrics include JSHeapUsedSize, Nodes, JSEventListeners, etc.
121
+ const { metrics } = await cdp.send('Performance.getMetrics')
122
+ const byName = Object.fromEntries(metrics.map(m => [m.name, m.value]))
123
+ // Total nodes in document
124
+ const docNodes = await evalInPage(
125
+ cdp,
126
+ `document.getElementsByTagName('*').length + document.querySelectorAll('*').length / 2`
127
+ )
128
+ return {
129
+ heapUsedMB: (byName.JSHeapUsedSize / 1024 / 1024) || 0,
130
+ heapTotalMB: (byName.JSHeapTotalSize / 1024 / 1024) || 0,
131
+ nodes: byName.Nodes || 0,
132
+ jsListeners: byName.JSEventListeners || 0,
133
+ docNodes,
134
+ layoutCount: byName.LayoutCount || 0,
135
+ recalcStyleCount: byName.RecalcStyleCount || 0,
136
+ fps: byName.FramesPerSecond || 0
137
+ }
138
+ }
139
+
140
+ async function typeChars(cdp, text, cps) {
141
+ const intervalMs = Math.max(1, Math.round(1000 / cps))
142
+ const start = Date.now()
143
+ for (let i = 0; i < text.length; i++) {
144
+ await cdp.send('Input.dispatchKeyEvent', { type: 'char', text: text[i], unmodifiedText: text[i] })
145
+ const expected = start + (i + 1) * intervalMs
146
+ const wait = expected - Date.now()
147
+ if (wait > 0) await new Promise(r => setTimeout(r, wait))
148
+ }
149
+ }
150
+
151
+ const lorem =
152
+ 'the quick brown fox jumps over the lazy dog while the agent thinks really hard about why typing into this composer feels like wading through molasses on a hot afternoon '
153
+ function genText(n) {
154
+ let s = ''
155
+ while (s.length < n) s += lorem
156
+ return s.slice(0, n)
157
+ }
158
+
159
+ async function main() {
160
+ log(`port ${PORT} · ${ROUNDS} rounds × ${CHARS} chars @ ${CPS} cps`)
161
+ const tgt = await pickRenderer()
162
+ log(`target ${tgt.url}`)
163
+ const cdp = await connect(tgt.webSocketDebuggerUrl)
164
+ await cdp.send('Runtime.enable')
165
+ await cdp.send('Performance.enable')
166
+ await cdp.send('DOM.enable')
167
+
168
+ const focused = await focusComposer(cdp)
169
+ if (!focused) {
170
+ console.error('composer not focusable')
171
+ process.exit(2)
172
+ }
173
+
174
+ await forceGCAndSettle(cdp)
175
+ const baseline = await snapshotCounts(cdp)
176
+ log('baseline:', JSON.stringify(baseline))
177
+
178
+ const text = genText(CHARS)
179
+ const history = [{ round: 0, ...baseline, charsTyped: 0 }]
180
+
181
+ for (let r = 1; r <= ROUNDS; r++) {
182
+ await typeChars(cdp, text, CPS)
183
+ await new Promise(res => setTimeout(res, 200))
184
+ await clearComposer(cdp)
185
+ await forceGCAndSettle(cdp)
186
+ const snap = await snapshotCounts(cdp)
187
+ snap.charsTyped = r * CHARS
188
+ snap.round = r
189
+ history.push(snap)
190
+ log(
191
+ `round ${r}: heap=${snap.heapUsedMB.toFixed(1)}MB ` +
192
+ `nodes=${snap.nodes} listeners=${snap.jsListeners} ` +
193
+ `domNodes=${Math.round(snap.docNodes)} ` +
194
+ `layoutCount=${snap.layoutCount} ` +
195
+ `Δheap=+${(snap.heapUsedMB - baseline.heapUsedMB).toFixed(2)}MB ` +
196
+ `Δnodes=+${snap.nodes - baseline.nodes} ` +
197
+ `Δlisteners=+${snap.jsListeners - baseline.jsListeners}`
198
+ )
199
+ }
200
+
201
+ console.log('\n=== GROWTH PER ROUND (averaged over last 5 rounds) ===')
202
+ const tail = history.slice(-5)
203
+ const first = tail[0]
204
+ const last = tail[tail.length - 1]
205
+ const rounds = last.round - first.round
206
+ const cells = ['heapUsedMB', 'nodes', 'jsListeners', 'docNodes', 'layoutCount']
207
+ for (const c of cells) {
208
+ const delta = last[c] - first[c]
209
+ const per = delta / Math.max(1, rounds)
210
+ const perChar = delta / Math.max(1, rounds * CHARS)
211
+ console.log(` ${c.padEnd(16)} Δtotal=${delta.toFixed(2).padStart(10)} /round=${per.toFixed(2).padStart(8)} /char=${perChar.toFixed(4).padStart(8)}`)
212
+ }
213
+
214
+ writeFileSync('/tmp/nastech-leak-history.json', JSON.stringify(history, null, 2))
215
+ log('wrote /tmp/nastech-leak-history.json')
216
+ cdp.close()
217
+ }
218
+
219
+ main().catch(e => {
220
+ console.error('[leak] fatal:', e.stack ?? e.message)
221
+ process.exit(1)
222
+ })
@@ -0,0 +1,108 @@
1
+ // Measure scroll position before and after Enter on a long thread.
2
+ // The user's complaint: pressing Enter to submit makes the view "jump up".
3
+ //
4
+ // Steps:
5
+ // 1. Scroll to the bottom of the thread
6
+ // 2. Type a short message
7
+ // 3. Record scroll position
8
+ // 4. Hit Enter
9
+ // 5. Record scroll position every 10ms for 1.5s after Enter
10
+ // 6. Report deltas
11
+ //
12
+ // Usage: node apps/desktop/scripts/measure-jump.mjs
13
+
14
+ const list = await (await fetch('http://127.0.0.1:9222/json/list')).json()
15
+ const tgt = list.find(t => t.type === 'page' && t.url.startsWith('http'))
16
+ const ws = new WebSocket(tgt.webSocketDebuggerUrl)
17
+ let id = 0
18
+ const pending = new Map()
19
+ ws.addEventListener('message', ev => {
20
+ const m = JSON.parse(ev.data)
21
+ if (m.id != null && pending.has(m.id)) {
22
+ pending.get(m.id)(m)
23
+ pending.delete(m.id)
24
+ }
25
+ })
26
+ await new Promise(r => ws.addEventListener('open', r))
27
+ const send = (m, p = {}) =>
28
+ new Promise(r => {
29
+ const i = ++id
30
+ pending.set(i, r)
31
+ ws.send(JSON.stringify({ id: i, method: m, params: p }))
32
+ })
33
+ const evalP = async expr => {
34
+ const r = await send('Runtime.evaluate', { expression: expr, returnByValue: true })
35
+ if (r.result?.exceptionDetails) throw new Error(r.result.exceptionDetails.text)
36
+ return r.result.result.value
37
+ }
38
+
39
+ // Scroll to bottom
40
+ await evalP(`(() => {
41
+ const v = document.querySelector('[data-slot="aui_thread-viewport"]')
42
+ if (v) v.scrollTop = v.scrollHeight
43
+ })()`)
44
+ await new Promise(r => setTimeout(r, 300))
45
+
46
+ // Focus composer and type
47
+ await evalP(`(() => {
48
+ const el = document.querySelector('[data-slot="composer-rich-input"]')
49
+ el.focus()
50
+ const r = document.createRange(); r.selectNodeContents(el); r.collapse(false)
51
+ window.getSelection().removeAllRanges(); window.getSelection().addRange(r)
52
+ })()`)
53
+
54
+ const text = 'short follow-up message'
55
+ for (const c of text) {
56
+ await send('Input.dispatchKeyEvent', { type: 'char', text: c, unmodifiedText: c })
57
+ await new Promise(r => setTimeout(r, 10))
58
+ }
59
+ await new Promise(r => setTimeout(r, 300))
60
+
61
+ // Set up sampling — sample scroll position every animation frame
62
+ await evalP(`(() => {
63
+ const v = document.querySelector('[data-slot="aui_thread-viewport"]')
64
+ window.__jumpSamples = []
65
+ window.__jumpStart = performance.now()
66
+ const tick = () => {
67
+ if (!v) return
68
+ window.__jumpSamples.push({
69
+ t: performance.now() - window.__jumpStart,
70
+ scrollTop: v.scrollTop,
71
+ scrollHeight: v.scrollHeight,
72
+ clientHeight: v.clientHeight,
73
+ distFromBottom: v.scrollHeight - v.scrollTop - v.clientHeight
74
+ })
75
+ if (performance.now() - window.__jumpStart < 2000) {
76
+ requestAnimationFrame(tick)
77
+ }
78
+ }
79
+ requestAnimationFrame(tick)
80
+ })()`)
81
+
82
+ // Fire Enter
83
+ await send('Input.dispatchKeyEvent', {
84
+ type: 'rawKeyDown', windowsVirtualKeyCode: 13, key: 'Enter', code: 'Enter', text: '\r', unmodifiedText: '\r'
85
+ })
86
+ await send('Input.dispatchKeyEvent', { type: 'keyUp', windowsVirtualKeyCode: 13, key: 'Enter', code: 'Enter' })
87
+
88
+ await new Promise(r => setTimeout(r, 2200))
89
+
90
+ const samples = JSON.parse(await evalP(`JSON.stringify(window.__jumpSamples || [])`))
91
+ console.log(`\n${samples.length} samples over 2s`)
92
+ console.log(`\n t(ms) scrollTop scrollHeight clientHeight distFromBottom`)
93
+ let prev = null
94
+ for (const s of samples) {
95
+ const marker = prev && Math.abs(s.scrollTop - prev.scrollTop) > 5 ? ' ← jump' : ''
96
+ console.log(` ${String(s.t.toFixed(0)).padStart(5)} ${String(s.scrollTop).padStart(9)} ${String(s.scrollHeight).padStart(12)} ${String(s.clientHeight).padStart(12)} ${String(s.distFromBottom).padStart(14)}${marker}`)
97
+ prev = s
98
+ }
99
+
100
+ // Cancel any running agent
101
+ await evalP(`(() => {
102
+ for (const b of document.querySelectorAll('button')) {
103
+ if ((b.getAttribute('aria-label') || '').toLowerCase().includes('stop')) { b.click(); return 'stopped' }
104
+ }
105
+ return 'no-stop'
106
+ })()`).then(r => console.log('\ncancel:', r))
107
+
108
+ ws.close()