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,184 @@
1
+ #!/usr/bin/env node
2
+ // Measure end-to-end keystroke→paint latency in the Electron renderer.
3
+ //
4
+ // For each synthetic keystroke we record:
5
+ // t0 = Input.dispatchKeyEvent send time
6
+ // t1 = first observed mutation of [data-slot="composer-rich-input"] childList/character data
7
+ // t2 = first requestAnimationFrame callback after t1 (proxy for next paint)
8
+ //
9
+ // We use Page.startScreencast briefly to also get frame-presentation timestamps;
10
+ // alternatively rely on rAF timing which is close enough for typing UX.
11
+ //
12
+ // Output: per-char latency histogram (min/p50/p95/p99/max) + samples > 16ms.
13
+ //
14
+ // Usage:
15
+ // node apps/desktop/scripts/measure-latency.mjs [--chars=100] [--cps=15] [--port=9222]
16
+
17
+ import { writeFileSync } from 'node:fs'
18
+
19
+ const args = Object.fromEntries(
20
+ process.argv.slice(2).flatMap(s => {
21
+ const m = s.match(/^--([^=]+)(?:=(.*))?$/)
22
+ return m ? [[m[1], m[2] ?? true]] : []
23
+ })
24
+ )
25
+ const PORT = Number(args.port ?? 9222)
26
+ const CHARS = Number(args.chars ?? 100)
27
+ const CPS = Number(args.cps ?? 15)
28
+
29
+ const log = (...m) => console.log('[latency]', ...m)
30
+
31
+ async function pickRenderer() {
32
+ const list = await (await fetch(`http://127.0.0.1:${PORT}/json/list`)).json()
33
+ return list.find(t => t.type === 'page' && t.url.startsWith('http'))
34
+ }
35
+
36
+ function connect(url) {
37
+ return new Promise((resolve, reject) => {
38
+ const ws = new WebSocket(url)
39
+ let id = 0
40
+ const pending = new Map()
41
+ const events = new Map()
42
+ ws.addEventListener('open', () =>
43
+ resolve({
44
+ send(method, params = {}) {
45
+ const myId = ++id
46
+ ws.send(JSON.stringify({ id: myId, method, params }))
47
+ return new Promise((res, rej) => pending.set(myId, { res, rej }))
48
+ },
49
+ on(method, h) {
50
+ if (!events.has(method)) events.set(method, [])
51
+ events.get(method).push(h)
52
+ },
53
+ close: () => ws.close()
54
+ })
55
+ )
56
+ ws.addEventListener('error', reject)
57
+ ws.addEventListener('message', ev => {
58
+ const m = JSON.parse(typeof ev.data === 'string' ? ev.data : ev.data.toString('utf8'))
59
+ if (m.id != null) {
60
+ const p = pending.get(m.id)
61
+ if (!p) return
62
+ pending.delete(m.id)
63
+ m.error ? p.rej(new Error(m.error.message)) : p.res(m.result)
64
+ } else if (m.method) {
65
+ ;(events.get(m.method) ?? []).forEach(h => h(m.params))
66
+ }
67
+ })
68
+ })
69
+ }
70
+
71
+ async function evalInPage(cdp, expr) {
72
+ const r = await cdp.send('Runtime.evaluate', { expression: expr, returnByValue: true })
73
+ if (r.exceptionDetails) throw new Error(r.exceptionDetails.text)
74
+ return r.result.value
75
+ }
76
+
77
+ async function main() {
78
+ const tgt = await pickRenderer()
79
+ log(`target ${tgt.url}`)
80
+ const cdp = await connect(tgt.webSocketDebuggerUrl)
81
+ await cdp.send('Runtime.enable')
82
+
83
+ 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
+ window.__keypressTimings = []
96
+ window.__pendingKey = null
97
+ // Observe the composer for content/text changes; record the time relative
98
+ // to the most recent simulated keypress timestamp set on window.__pendingKey.
99
+ const obs = new MutationObserver(() => {
100
+ const start = window.__pendingKey
101
+ if (start === null) return
102
+ const mutationT = performance.now()
103
+ window.__pendingKey = null
104
+ requestAnimationFrame(() => {
105
+ const paintT = performance.now()
106
+ window.__keypressTimings.push({
107
+ start, mutationT, paintT,
108
+ mutationLatency: mutationT - start,
109
+ paintLatency: paintT - start
110
+ })
111
+ })
112
+ })
113
+ obs.observe(el, { childList: true, subtree: true, characterData: true })
114
+ window.__keystrokeObserver = obs
115
+ return true
116
+ })()`
117
+ )
118
+
119
+ const lorem =
120
+ 'the quick brown fox jumps over the lazy dog while typing into this composer feels like wading through molasses on a hot afternoon. '
121
+ let text = ''
122
+ while (text.length < CHARS) text += lorem
123
+ text = text.slice(0, CHARS)
124
+
125
+ const intervalMs = Math.max(1, Math.round(1000 / CPS))
126
+ const start = Date.now()
127
+ for (let i = 0; i < text.length; i++) {
128
+ // Mark the keypress time inside the page so it's measured from the same clock.
129
+ await evalInPage(cdp, `window.__pendingKey = performance.now()`)
130
+ await cdp.send('Input.dispatchKeyEvent', { type: 'char', text: text[i], unmodifiedText: text[i] })
131
+ const expected = start + (i + 1) * intervalMs
132
+ const wait = expected - Date.now()
133
+ if (wait > 0) await new Promise(r => setTimeout(r, wait))
134
+ }
135
+
136
+ await new Promise(r => setTimeout(r, 500))
137
+ const samples = await evalInPage(cdp, `window.__keypressTimings`)
138
+ log(`${samples.length} keystroke samples measured out of ${text.length} typed`)
139
+
140
+ // Clear composer for next run
141
+ await evalInPage(cdp, `
142
+ (() => {
143
+ const el = document.querySelector('[data-slot="composer-rich-input"]')
144
+ if (el) { el.innerHTML = ''; el.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'deleteContentBackward' })) }
145
+ window.__keystrokeObserver?.disconnect()
146
+ })()
147
+ `)
148
+
149
+ const mutLat = samples.map(s => s.mutationLatency).sort((a, b) => a - b)
150
+ const paintLat = samples.map(s => s.paintLatency).sort((a, b) => a - b)
151
+ const stat = arr => ({
152
+ n: arr.length,
153
+ min: arr[0]?.toFixed(2),
154
+ p50: arr[Math.floor(arr.length * 0.5)]?.toFixed(2),
155
+ p90: arr[Math.floor(arr.length * 0.9)]?.toFixed(2),
156
+ p95: arr[Math.floor(arr.length * 0.95)]?.toFixed(2),
157
+ p99: arr[Math.floor(arr.length * 0.99)]?.toFixed(2),
158
+ max: arr[arr.length - 1]?.toFixed(2),
159
+ mean: arr.length ? (arr.reduce((s, x) => s + x, 0) / arr.length).toFixed(2) : 0
160
+ })
161
+
162
+ console.log('\n=== keypress → mutation latency (ms) ===')
163
+ console.log(' ', stat(mutLat))
164
+ console.log('\n=== keypress → next rAF (≈paint) latency (ms) ===')
165
+ console.log(' ', stat(paintLat))
166
+
167
+ const slow = samples.filter(s => s.paintLatency > 16)
168
+ console.log(`\n=== ${slow.length}/${samples.length} keystrokes >16ms (one frame) ===`)
169
+ if (slow.length) {
170
+ const slowSorted = [...slow].sort((a, b) => b.paintLatency - a.paintLatency).slice(0, 10)
171
+ for (const s of slowSorted) {
172
+ console.log(` paint=${s.paintLatency.toFixed(1)}ms mut=${s.mutationLatency.toFixed(1)}ms at t=${s.start.toFixed(0)}`)
173
+ }
174
+ }
175
+
176
+ writeFileSync('/tmp/nastech-latency-samples.json', JSON.stringify(samples, null, 2))
177
+
178
+ cdp.close()
179
+ }
180
+
181
+ main().catch(e => {
182
+ console.error('[latency] fatal:', e.stack ?? e.message)
183
+ process.exit(1)
184
+ })
@@ -0,0 +1,252 @@
1
+ // REAL streaming measurement — no React internals.
2
+ //
3
+ // Measures:
4
+ // 1) rAF frame intervals during a verified live stream (long-frame histogram)
5
+ // 2) MutationObserver: how often does the live assistant message mutate, what's the budget per mutation
6
+ // 3) Text length growth rate (chars/sec)
7
+ // 4) PerformanceObserver `longtask` entries (any task > 50ms blocks input)
8
+ //
9
+ // Detects REAL stream by waiting for assistant-message DOM count to grow past baseline.
10
+ // Does NOT cancel — lets the stream run to completion or hits TIMEOUT_MS.
11
+
12
+ const CDP_HTTP = 'http://127.0.0.1:9222'
13
+ const PROMPT = process.env.PROMPT || 'count from 1 to 80, one number per line'
14
+ const TIMEOUT_MS = Number(process.env.TIMEOUT_MS || 60000)
15
+
16
+ async function getTarget() {
17
+ const list = await (await fetch(`${CDP_HTTP}/json`)).json()
18
+ const t = list.find((t) => t.type === 'page' && /5174/.test(t.url))
19
+ if (!t) throw new Error('renderer not found')
20
+ return t
21
+ }
22
+
23
+ class CDP {
24
+ constructor(ws) { this.ws = ws; this.id = 0; this.pending = new Map() }
25
+ static async open(url) {
26
+ const ws = new WebSocket(url)
27
+ await new Promise((r, j) => {
28
+ ws.addEventListener('open', r, { once: true })
29
+ ws.addEventListener('error', (e) => j(e), { once: true })
30
+ })
31
+ const cdp = new CDP(ws)
32
+ ws.addEventListener('message', (event) => {
33
+ const m = JSON.parse(event.data.toString())
34
+ if (m.id != null && cdp.pending.has(m.id)) {
35
+ const { resolve, reject } = cdp.pending.get(m.id)
36
+ cdp.pending.delete(m.id)
37
+ if (m.error) reject(new Error(m.error.message))
38
+ else resolve(m.result)
39
+ }
40
+ })
41
+ return cdp
42
+ }
43
+ send(method, params) {
44
+ const id = ++this.id
45
+ return new Promise((res, rej) => {
46
+ this.pending.set(id, { resolve: res, reject: rej })
47
+ this.ws.send(JSON.stringify({ id, method, params }))
48
+ })
49
+ }
50
+ async eval(expr) {
51
+ const r = await this.send('Runtime.evaluate', { expression: expr, returnByValue: true, awaitPromise: true })
52
+ if (r.exceptionDetails) throw new Error(r.exceptionDetails.exception?.description || 'eval')
53
+ return r.result.value
54
+ }
55
+ close() { this.ws.close() }
56
+ }
57
+
58
+ async function main() {
59
+ const target = await getTarget()
60
+ const cdp = await CDP.open(target.webSocketDebuggerUrl)
61
+
62
+ // Install recorders.
63
+ await cdp.eval(`
64
+ (() => {
65
+ // rAF frame intervals
66
+ window.__FT__ = { times: [], stop: false }
67
+ let last = performance.now()
68
+ const tick = () => {
69
+ if (window.__FT__.stop) return
70
+ const now = performance.now()
71
+ window.__FT__.times.push(now - last)
72
+ last = now
73
+ requestAnimationFrame(tick)
74
+ }
75
+ requestAnimationFrame(tick)
76
+
77
+ // longtask observer
78
+ window.__LT__ = { entries: [], stop: false }
79
+ try {
80
+ const po = new PerformanceObserver((list) => {
81
+ if (window.__LT__.stop) return
82
+ for (const e of list.getEntries()) {
83
+ window.__LT__.entries.push({ name: e.name, duration: e.duration, startTime: e.startTime })
84
+ }
85
+ })
86
+ po.observe({ entryTypes: ['longtask'] })
87
+ window.__LT__.po = po
88
+ } catch {}
89
+
90
+ // mutation observer on streaming message
91
+ window.__MO__ = { mutations: [], stop: false, currentMsg: null }
92
+ const tryArm = () => {
93
+ const all = document.querySelectorAll('[data-slot="aui_assistant-message-root"]')
94
+ const last = all[all.length - 1]
95
+ if (!last || last === window.__MO__.currentMsg) return
96
+ window.__MO__.currentMsg = last
97
+ if (window.__MO__.obs) window.__MO__.obs.disconnect()
98
+ const obs = new MutationObserver((muts) => {
99
+ if (window.__MO__.stop) return
100
+ const t = performance.now()
101
+ window.__MO__.mutations.push({ t, count: muts.length, len: last.textContent.length })
102
+ })
103
+ obs.observe(last, { childList: true, subtree: true, characterData: true })
104
+ window.__MO__.obs = obs
105
+ }
106
+ window.__MO__.arm = tryArm
107
+ return 'recorders armed'
108
+ })()
109
+ `)
110
+
111
+ // Baseline
112
+ const base = JSON.parse(await cdp.eval(`
113
+ JSON.stringify({
114
+ assistantCount: document.querySelectorAll('[data-slot="aui_assistant-message-root"]').length,
115
+ busy: !!document.querySelector('[data-status="running"], [data-busy="true"]'),
116
+ hasComposer: !!document.querySelector('[contenteditable="true"]'),
117
+ })
118
+ `))
119
+ console.log('baseline:', base)
120
+ if (!base.hasComposer) { console.error('no composer'); cdp.close(); return }
121
+
122
+ // Type + submit
123
+ await cdp.eval(`
124
+ (() => {
125
+ const ed = document.querySelector('[contenteditable="true"]')
126
+ ed.focus()
127
+ document.execCommand('insertText', false, ${JSON.stringify(PROMPT)})
128
+ return 'typed'
129
+ })()
130
+ `)
131
+ const submitT0 = Date.now()
132
+ await cdp.eval(`
133
+ (() => {
134
+ const ed = document.querySelector('[contenteditable="true"]')
135
+ ed.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', bubbles: true, cancelable: true }))
136
+ return 'submitted'
137
+ })()
138
+ `)
139
+
140
+ // Poll for REAL stream (assistant count > baseline). 30 seconds — accommodates
141
+ // slow first-token latencies on big providers.
142
+ let realStreamT = null
143
+ for (let i = 0; i < 600; i++) {
144
+ await new Promise((r) => setTimeout(r, 50))
145
+ const s = JSON.parse(await cdp.eval(`
146
+ JSON.stringify({
147
+ n: document.querySelectorAll('[data-slot="aui_assistant-message-root"]').length,
148
+ busy: !!document.querySelector('[data-status="running"], [data-busy="true"]'),
149
+ text: (() => { const a = document.querySelectorAll('[data-slot="aui_assistant-message-root"]'); return a.length ? a[a.length-1].textContent.length : 0 })()
150
+ })
151
+ `))
152
+ if (s.n > base.assistantCount) {
153
+ realStreamT = Date.now()
154
+ console.log('REAL stream started after', realStreamT - submitT0, 'ms — busy=', s.busy, 'text=', s.text)
155
+ // Arm mutation observer on the new message
156
+ await cdp.eval('window.__MO__.arm()')
157
+ break
158
+ }
159
+ }
160
+ if (!realStreamT) {
161
+ console.error('REAL STREAM NEVER STARTED')
162
+ cdp.close()
163
+ return
164
+ }
165
+
166
+ // Sample length growth, wait for completion or timeout
167
+ const samples = []
168
+ const start = Date.now()
169
+ while (Date.now() - start < TIMEOUT_MS) {
170
+ await new Promise((r) => setTimeout(r, 250))
171
+ const s = JSON.parse(await cdp.eval(`
172
+ JSON.stringify({
173
+ t: performance.now(),
174
+ len: (() => { const a = document.querySelectorAll('[data-slot="aui_assistant-message-root"]'); return a.length ? a[a.length-1].textContent.length : 0 })(),
175
+ busy: !!document.querySelector('[data-status="running"], [data-busy="true"]')
176
+ })
177
+ `))
178
+ samples.push(s)
179
+ if (!s.busy && samples.length > 4) {
180
+ await new Promise((r) => setTimeout(r, 300))
181
+ break
182
+ }
183
+ }
184
+
185
+ // Pull recordings
186
+ const data = JSON.parse(await cdp.eval(`
187
+ (() => {
188
+ window.__FT__.stop = true
189
+ window.__LT__.stop = true
190
+ window.__MO__.stop = true
191
+ try { window.__LT__.po && window.__LT__.po.disconnect() } catch {}
192
+ try { window.__MO__.obs && window.__MO__.obs.disconnect() } catch {}
193
+ return JSON.stringify({
194
+ frames: window.__FT__.times,
195
+ longtasks: window.__LT__.entries,
196
+ mutations: window.__MO__.mutations,
197
+ })
198
+ })()
199
+ `))
200
+
201
+ const { frames, longtasks, mutations } = data
202
+
203
+ // Frame histogram (filter to stream window)
204
+ const buckets = { '<=16.7': 0, '16.7-33': 0, '33-50': 0, '50-100': 0, '100-200': 0, '>200': 0 }
205
+ let frameTotal = 0
206
+ let maxFrame = 0
207
+ for (const f of frames) {
208
+ frameTotal += f
209
+ if (f > maxFrame) maxFrame = f
210
+ if (f <= 16.7) buckets['<=16.7']++
211
+ else if (f <= 33) buckets['16.7-33']++
212
+ else if (f <= 50) buckets['33-50']++
213
+ else if (f <= 100) buckets['50-100']++
214
+ else if (f <= 200) buckets['100-200']++
215
+ else buckets['>200']++
216
+ }
217
+ const avgFps = frames.length ? (frames.length / (frameTotal / 1000)).toFixed(1) : 'n/a'
218
+ const slowFrames = frames.filter((f) => f > 33).length
219
+ const veryslowFrames = frames.filter((f) => f > 100).length
220
+
221
+ // Longtask summary
222
+ const ltMs = longtasks.reduce((a, b) => a + b.duration, 0)
223
+ const ltMax = longtasks.length ? Math.max(...longtasks.map((e) => e.duration)) : 0
224
+
225
+ // Mutation rate
226
+ let mutTotal = mutations.length
227
+ let mutDurs = []
228
+ for (let i = 1; i < mutations.length; i++) {
229
+ mutDurs.push(mutations[i].t - mutations[i - 1].t)
230
+ }
231
+ mutDurs.sort((a, b) => a - b)
232
+ const mutP50 = mutDurs[Math.floor(mutDurs.length * 0.5)] ?? 0
233
+ const mutP95 = mutDurs[Math.floor(mutDurs.length * 0.95)] ?? 0
234
+
235
+ // Growth rate
236
+ const firstLen = samples[0]?.len ?? 0
237
+ const lastLen = samples[samples.length - 1]?.len ?? 0
238
+ const elapsedS = samples.length ? (samples[samples.length - 1].t - samples[0].t) / 1000 : 0
239
+ const charsPerSec = elapsedS ? ((lastLen - firstLen) / elapsedS).toFixed(1) : 'n/a'
240
+
241
+ console.log('\n=== STREAM RESULTS ===')
242
+ console.log('window:', (frameTotal / 1000).toFixed(1), 's | frames:', frames.length, '| avgFps:', avgFps, '| maxFrame:', maxFrame.toFixed(1), 'ms')
243
+ console.log('frame histogram:', buckets)
244
+ console.log('slow frames (>33ms):', slowFrames, '| very slow (>100ms):', veryslowFrames)
245
+ console.log('longtasks:', longtasks.length, 'total', ltMs.toFixed(0), 'ms — max', ltMax.toFixed(1), 'ms')
246
+ console.log('text grew', firstLen, '→', lastLen, 'chars (', charsPerSec, 'char/s )')
247
+ console.log('mutations on streaming msg:', mutTotal, '| inter-mutation p50:', mutP50.toFixed(1), 'ms', 'p95:', mutP95.toFixed(1), 'ms')
248
+
249
+ cdp.close()
250
+ }
251
+
252
+ main().catch((e) => { console.error(e); process.exit(1) })
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env node
2
+ // Measure submit (Enter) latency in the composer.
3
+ //
4
+ // For each round:
5
+ // 1. Focus composer, type N chars of stub text
6
+ // 2. Mark a timestamp, fire Enter via Input.dispatchKeyEvent
7
+ // 3. Observe: time until the composer becomes empty (submit accepted),
8
+ // time until the user message renders in the thread viewport,
9
+ // time until the optional "running…" indicator appears,
10
+ // time until the next frame is painted after the message renders.
11
+ //
12
+ // Pre-condition: a session is loaded (load via click-session.mjs first).
13
+ // Note: this DOES talk to the real gateway/agent, so each round triggers
14
+ // a real prompt submission. Don't run this on a live conversation
15
+ // you care about — use a throwaway session.
16
+
17
+ import { writeFileSync } from 'node:fs'
18
+
19
+ const args = Object.fromEntries(
20
+ process.argv.slice(2).flatMap(s => {
21
+ const m = s.match(/^--([^=]+)(?:=(.*))?$/)
22
+ return m ? [[m[1], m[2] ?? true]] : []
23
+ })
24
+ )
25
+ const PORT = Number(args.port ?? 9222)
26
+ const ROUNDS = Number(args.rounds ?? 3)
27
+
28
+ async function pickRenderer() {
29
+ const list = await (await fetch(`http://127.0.0.1:${PORT}/json/list`)).json()
30
+ return list.find(t => t.type === 'page' && t.url.startsWith('http'))
31
+ }
32
+
33
+ function connect(url) {
34
+ return new Promise((resolve, reject) => {
35
+ const ws = new WebSocket(url)
36
+ let id = 0
37
+ const pending = new Map()
38
+ ws.addEventListener('open', () =>
39
+ resolve({
40
+ send(method, params = {}) {
41
+ const myId = ++id
42
+ ws.send(JSON.stringify({ id: myId, method, params }))
43
+ return new Promise((res, rej) => pending.set(myId, { res, rej }))
44
+ },
45
+ close: () => ws.close()
46
+ })
47
+ )
48
+ ws.addEventListener('error', reject)
49
+ ws.addEventListener('message', ev => {
50
+ const m = JSON.parse(typeof ev.data === 'string' ? ev.data : ev.data.toString('utf8'))
51
+ if (m.id != null) {
52
+ const p = pending.get(m.id)
53
+ if (!p) return
54
+ pending.delete(m.id)
55
+ m.error ? p.rej(new Error(m.error.message)) : p.res(m.result)
56
+ }
57
+ })
58
+ })
59
+ }
60
+
61
+ async function evalP(cdp, expr) {
62
+ const r = await cdp.send('Runtime.evaluate', { expression: expr, returnByValue: true, awaitPromise: true })
63
+ if (r.exceptionDetails) throw new Error(r.exceptionDetails.text)
64
+ return r.result.value
65
+ }
66
+
67
+ async function focusAndType(cdp, text) {
68
+ await evalP(cdp, `
69
+ (() => {
70
+ const el = document.querySelector('[data-slot="composer-rich-input"]')
71
+ if (!el) return
72
+ el.focus()
73
+ const range = document.createRange()
74
+ range.selectNodeContents(el)
75
+ range.collapse(false)
76
+ const sel = window.getSelection()
77
+ sel.removeAllRanges()
78
+ sel.addRange(range)
79
+ })()
80
+ `)
81
+ for (const c of text) {
82
+ await cdp.send('Input.dispatchKeyEvent', { type: 'char', text: c, unmodifiedText: c })
83
+ await new Promise(r => setTimeout(r, 8))
84
+ }
85
+ }
86
+
87
+ async function submitAndMeasure(cdp, timeoutMs = 5000) {
88
+ // Install observers, record submit time as performance.now() inside the page,
89
+ // and wait for all milestones.
90
+ return await evalP(cdp, `
91
+ new Promise((resolve) => {
92
+ const composer = document.querySelector('[data-slot="composer-rich-input"]')
93
+ const threadRoot = document.querySelector('[data-slot="aui_thread-content"]') ||
94
+ document.querySelector('[data-slot="aui_thread-viewport"]')
95
+ const startMessageCount = threadRoot ? threadRoot.querySelectorAll('[data-slot="aui_turn-pair"], [data-slot="aui_message"]').length : 0
96
+ const startComposerText = composer ? composer.innerText : ''
97
+
98
+ const milestones = { start: performance.now() }
99
+ let done = false
100
+ const finish = (reason) => {
101
+ if (done) return
102
+ done = true
103
+ clearInterval(poll); clearTimeout(timer)
104
+ composerObs.disconnect()
105
+ threadObs?.disconnect()
106
+ milestones.reason = reason
107
+ milestones.end = performance.now()
108
+ milestones.totalMs = milestones.end - milestones.start
109
+ resolve(milestones)
110
+ }
111
+
112
+ const composerObs = new MutationObserver(() => {
113
+ if (!milestones.composerClearedMs && composer && composer.innerText.length === 0) {
114
+ milestones.composerClearedMs = performance.now() - milestones.start
115
+ }
116
+ })
117
+ composer && composerObs.observe(composer, { childList: true, subtree: true, characterData: true })
118
+
119
+ let threadObs = null
120
+ if (threadRoot) {
121
+ threadObs = new MutationObserver(() => {
122
+ const c = threadRoot.querySelectorAll('[data-slot="aui_turn-pair"], [data-slot="aui_message"]').length
123
+ if (!milestones.userMessageRenderedMs && c > startMessageCount) {
124
+ milestones.userMessageRenderedMs = performance.now() - milestones.start
125
+ requestAnimationFrame(() => {
126
+ milestones.userMessagePaintMs = performance.now() - milestones.start
127
+ finish('paint')
128
+ })
129
+ }
130
+ })
131
+ threadObs.observe(threadRoot, { childList: true, subtree: true })
132
+ }
133
+
134
+ const poll = setInterval(() => {
135
+ if (milestones.composerClearedMs && !milestones.userMessageRenderedMs &&
136
+ performance.now() - milestones.start > 2000) {
137
+ finish('timeout-after-clear')
138
+ }
139
+ }, 100)
140
+ const timer = setTimeout(() => finish('timeout-overall'), ${timeoutMs})
141
+
142
+ // Send Enter immediately
143
+ window.dispatchEvent(new KeyboardEvent('keydown')) // no-op marker
144
+ const enterEv = new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', bubbles: true, cancelable: true })
145
+ composer?.dispatchEvent(enterEv)
146
+ })
147
+ `)
148
+ }
149
+
150
+ async function main() {
151
+ const tgt = await pickRenderer()
152
+ console.log('target', tgt.url)
153
+ const cdp = await connect(tgt.webSocketDebuggerUrl)
154
+ await cdp.send('Runtime.enable')
155
+
156
+ const samples = []
157
+ for (let i = 1; i <= ROUNDS; i++) {
158
+ await focusAndType(cdp, `latency test ${i} ${'x'.repeat(40)}`)
159
+ await new Promise(r => setTimeout(r, 300))
160
+ const result = await submitAndMeasure(cdp, 4000)
161
+ samples.push({ round: i, ...result })
162
+ console.log(
163
+ `r${i}: clear=${(result.composerClearedMs ?? -1).toFixed?.(0) ?? '?'}ms ` +
164
+ `userMsg=${(result.userMessageRenderedMs ?? -1).toFixed?.(0) ?? '?'}ms ` +
165
+ `paint=${(result.userMessagePaintMs ?? -1).toFixed?.(0) ?? '?'}ms ` +
166
+ `reason=${result.reason}`
167
+ )
168
+ // wait for any agent activity to finish before next round so we're not piling up
169
+ await new Promise(r => setTimeout(r, 4000))
170
+ }
171
+ writeFileSync('/tmp/nastech-submit-latency.json', JSON.stringify(samples, null, 2))
172
+ console.log('\nwrote /tmp/nastech-submit-latency.json')
173
+ cdp.close()
174
+ }
175
+
176
+ main().catch(e => {
177
+ console.error('fatal:', e.stack ?? e.message)
178
+ process.exit(1)
179
+ })