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.
- package/.prettierrc +11 -0
- package/DESIGN.md +167 -0
- package/README.md +141 -0
- package/assets/icon.icns +0 -0
- package/assets/icon.ico +0 -0
- package/assets/icon.png +0 -0
- package/components.json +21 -0
- package/electron/backend-env.cjs +112 -0
- package/electron/backend-env.test.cjs +111 -0
- package/electron/backend-probes.cjs +106 -0
- package/electron/backend-probes.test.cjs +82 -0
- package/electron/backend-ready.cjs +66 -0
- package/electron/bootstrap-platform.cjs +91 -0
- package/electron/bootstrap-platform.test.cjs +111 -0
- package/electron/bootstrap-runner.cjs +720 -0
- package/electron/bootstrap-runner.test.cjs +138 -0
- package/electron/connection-config.cjs +254 -0
- package/electron/connection-config.test.cjs +329 -0
- package/electron/dashboard-token.cjs +99 -0
- package/electron/dashboard-token.test.cjs +142 -0
- package/electron/desktop-uninstall.cjs +232 -0
- package/electron/desktop-uninstall.test.cjs +246 -0
- package/electron/entitlements.mac.inherit.plist +14 -0
- package/electron/entitlements.mac.plist +14 -0
- package/electron/fs-read-dir.cjs +109 -0
- package/electron/fs-read-dir.test.cjs +364 -0
- package/electron/gateway-ws-probe.cjs +188 -0
- package/electron/gateway-ws-probe.test.cjs +122 -0
- package/electron/git-root.cjs +54 -0
- package/electron/git-root.test.cjs +40 -0
- package/electron/git-worktrees.cjs +174 -0
- package/electron/hardening.cjs +184 -0
- package/electron/hardening.test.cjs +116 -0
- package/electron/main.cjs +5762 -0
- package/electron/oauth-net-request.cjs +20 -0
- package/electron/oauth-net-request.test.cjs +34 -0
- package/electron/preload.cjs +135 -0
- package/electron/session-windows.cjs +99 -0
- package/electron/session-windows.test.cjs +177 -0
- package/electron/update-remote.cjs +56 -0
- package/electron/update-remote.test.cjs +78 -0
- package/electron/vscode-marketplace.cjs +331 -0
- package/electron/vscode-marketplace.test.cjs +113 -0
- package/electron/windows-child-process.test.cjs +57 -0
- package/electron/windows-user-env.cjs +76 -0
- package/electron/windows-user-env.test.cjs +90 -0
- package/electron/workspace-cwd.cjs +38 -0
- package/electron/workspace-cwd.test.cjs +45 -0
- package/eslint.config.mjs +122 -0
- package/index.html +17 -0
- package/package.json +254 -0
- package/pr-assets/session-source-folders.png +0 -0
- package/preview-demo.html +65 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/ds-assets/filler-bg0.jpg +0 -0
- package/public/nastech-frames/nastech-frame-0.png +0 -0
- package/public/nastech-frames/nastech-frame-1.png +0 -0
- package/public/nastech-frames/nastech-frame-2.png +0 -0
- package/public/nastech-frames/nastech-frame-3.png +0 -0
- package/public/nastech-frames/nastech-frame-4.png +0 -0
- package/public/nastech-frames/nastech-frame-5.png +0 -0
- package/public/nastech-frames/nastech-frame-6.png +0 -0
- package/public/nastech-frames/nastech-frame-7.png +0 -0
- package/public/nastech-girl.jpg +0 -0
- package/public/nastech-sprite.png +0 -0
- package/public/nastech.png +0 -0
- package/scripts/after-pack.cjs +41 -0
- package/scripts/assert-dist-built.cjs +70 -0
- package/scripts/assert-dist-built.test.cjs +84 -0
- package/scripts/assert-root-install.cjs +13 -0
- package/scripts/before-build.cjs +11 -0
- package/scripts/before-pack.cjs +78 -0
- package/scripts/before-pack.test.cjs +53 -0
- package/scripts/click-session.mjs +51 -0
- package/scripts/dev-no-hmr.mjs +22 -0
- package/scripts/diag-jump.mjs +115 -0
- package/scripts/diag-scroll-reset.mjs +229 -0
- package/scripts/eval.mjs +21 -0
- package/scripts/leak-typing.mjs +222 -0
- package/scripts/measure-jump.mjs +108 -0
- package/scripts/measure-latency.mjs +184 -0
- package/scripts/measure-real-stream.mjs +252 -0
- package/scripts/measure-submit.mjs +179 -0
- package/scripts/measure-synthetic-stream.mjs +322 -0
- package/scripts/notarize-artifact.cjs +77 -0
- package/scripts/notarize.cjs +100 -0
- package/scripts/patch-electron-builder-mac-binary.cjs +59 -0
- package/scripts/probe-renderer.mjs +38 -0
- package/scripts/probe-thread.mjs +40 -0
- package/scripts/profile-long-stream.mjs +191 -0
- package/scripts/profile-real-stream.mjs +137 -0
- package/scripts/profile-synth-stream.mjs +103 -0
- package/scripts/profile-typing-lag.md +381 -0
- package/scripts/profile-typing.mjs +260 -0
- package/scripts/reload-renderer.mjs +25 -0
- package/scripts/reload.mjs +36 -0
- package/scripts/set-exe-identity.cjs +94 -0
- package/scripts/stage-native-deps.cjs +159 -0
- package/scripts/test-desktop.mjs +425 -0
- package/scripts/write-build-stamp.cjs +126 -0
- package/src/app/agents/index.tsx +398 -0
- package/src/app/artifacts/index.test.ts +62 -0
- package/src/app/artifacts/index.tsx +906 -0
- package/src/app/chat/chat-drop-overlay.tsx +48 -0
- package/src/app/chat/chat-swap-overlay.tsx +47 -0
- package/src/app/chat/composer/attachments.tsx +114 -0
- package/src/app/chat/composer/completion-drawer.tsx +63 -0
- package/src/app/chat/composer/context-menu.tsx +172 -0
- package/src/app/chat/composer/controls.tsx +289 -0
- package/src/app/chat/composer/drop-affordance.ts +2 -0
- package/src/app/chat/composer/enter-submit-dom-race.test.tsx +218 -0
- package/src/app/chat/composer/focus.ts +134 -0
- package/src/app/chat/composer/help-hint.tsx +59 -0
- package/src/app/chat/composer/hooks/use-at-completions.ts +141 -0
- package/src/app/chat/composer/hooks/use-live-completion-adapter.ts +119 -0
- package/src/app/chat/composer/hooks/use-mic-recorder.ts +291 -0
- package/src/app/chat/composer/hooks/use-slash-completions.ts +114 -0
- package/src/app/chat/composer/hooks/use-voice-conversation.ts +390 -0
- package/src/app/chat/composer/hooks/use-voice-recorder.ts +116 -0
- package/src/app/chat/composer/ime-composition-dom-repro.test.tsx +108 -0
- package/src/app/chat/composer/index.tsx +1611 -0
- package/src/app/chat/composer/inline-refs.ts +138 -0
- package/src/app/chat/composer/model-pill.tsx +86 -0
- package/src/app/chat/composer/queue-panel.tsx +130 -0
- package/src/app/chat/composer/rich-editor.test.ts +18 -0
- package/src/app/chat/composer/rich-editor.ts +165 -0
- package/src/app/chat/composer/skin-slash-popover.tsx +61 -0
- package/src/app/chat/composer/slash-nav-dom-repro.test.tsx +186 -0
- package/src/app/chat/composer/status-stack/index.tsx +202 -0
- package/src/app/chat/composer/status-stack/status-row.tsx +155 -0
- package/src/app/chat/composer/text-utils.test.ts +77 -0
- package/src/app/chat/composer/text-utils.ts +107 -0
- package/src/app/chat/composer/trigger-popover.test.tsx +42 -0
- package/src/app/chat/composer/trigger-popover.tsx +116 -0
- package/src/app/chat/composer/types.ts +64 -0
- package/src/app/chat/composer/url-dialog.tsx +82 -0
- package/src/app/chat/composer/voice-activity.tsx +252 -0
- package/src/app/chat/hooks/use-composer-actions.test.ts +57 -0
- package/src/app/chat/hooks/use-composer-actions.ts +525 -0
- package/src/app/chat/hooks/use-file-drop-zone.ts +118 -0
- package/src/app/chat/index.tsx +390 -0
- package/src/app/chat/perf-probe.tsx +269 -0
- package/src/app/chat/right-rail/index.ts +1 -0
- package/src/app/chat/right-rail/preview-console-state.ts +82 -0
- package/src/app/chat/right-rail/preview-console.tsx +290 -0
- package/src/app/chat/right-rail/preview-file.tsx +559 -0
- package/src/app/chat/right-rail/preview-pane.test.tsx +43 -0
- package/src/app/chat/right-rail/preview-pane.tsx +657 -0
- package/src/app/chat/right-rail/preview.tsx +171 -0
- package/src/app/chat/scroll-to-bottom-button.test.tsx +67 -0
- package/src/app/chat/scroll-to-bottom-button.tsx +74 -0
- package/src/app/chat/sidebar/cron-jobs-section.tsx +325 -0
- package/src/app/chat/sidebar/index.tsx +1219 -0
- package/src/app/chat/sidebar/load-more-row.tsx +30 -0
- package/src/app/chat/sidebar/order.test.ts +21 -0
- package/src/app/chat/sidebar/order.ts +17 -0
- package/src/app/chat/sidebar/profile-switcher.tsx +516 -0
- package/src/app/chat/sidebar/session-actions-menu.tsx +264 -0
- package/src/app/chat/sidebar/session-row.tsx +257 -0
- package/src/app/chat/sidebar/virtual-session-list.tsx +154 -0
- package/src/app/chat/sidebar/workspace-groups.test.ts +149 -0
- package/src/app/chat/sidebar/workspace-groups.ts +326 -0
- package/src/app/chat/thread-loading.test.ts +34 -0
- package/src/app/chat/thread-loading.ts +26 -0
- package/src/app/command-center/index.tsx +654 -0
- package/src/app/command-palette/index.tsx +513 -0
- package/src/app/command-palette/marketplace-theme-page.tsx +157 -0
- package/src/app/cron/index.tsx +942 -0
- package/src/app/cron/job-state.ts +29 -0
- package/src/app/desktop-controller.tsx +938 -0
- package/src/app/floating-hud.ts +22 -0
- package/src/app/gateway/hooks/use-gateway-boot.test.tsx +265 -0
- package/src/app/gateway/hooks/use-gateway-boot.ts +387 -0
- package/src/app/gateway/hooks/use-gateway-request.ts +138 -0
- package/src/app/hooks/use-keybinds.ts +186 -0
- package/src/app/hooks/use-refresh-hotkey.ts +45 -0
- package/src/app/hooks/use-route-enum-param.ts +38 -0
- package/src/app/index.tsx +1 -0
- package/src/app/layout-constants.ts +13 -0
- package/src/app/messaging/index.tsx +648 -0
- package/src/app/messaging/platform-icon.tsx +93 -0
- package/src/app/model-picker-overlay.tsx +42 -0
- package/src/app/model-visibility-overlay.tsx +31 -0
- package/src/app/overlays/overlay-chrome.tsx +66 -0
- package/src/app/overlays/overlay-search-input.tsx +33 -0
- package/src/app/overlays/overlay-split-layout.tsx +130 -0
- package/src/app/overlays/overlay-view.tsx +91 -0
- package/src/app/page-search-shell.tsx +75 -0
- package/src/app/profiles/create-profile-dialog.tsx +154 -0
- package/src/app/profiles/delete-profile-dialog.tsx +65 -0
- package/src/app/profiles/index.tsx +671 -0
- package/src/app/profiles/rename-profile-dialog.tsx +125 -0
- package/src/app/right-sidebar/files/dnd-manager.ts +27 -0
- package/src/app/right-sidebar/files/ipc.test.ts +100 -0
- package/src/app/right-sidebar/files/ipc.ts +161 -0
- package/src/app/right-sidebar/files/remote-picker.tsx +177 -0
- package/src/app/right-sidebar/files/tree.tsx +224 -0
- package/src/app/right-sidebar/files/use-project-tree.test.ts +190 -0
- package/src/app/right-sidebar/files/use-project-tree.ts +268 -0
- package/src/app/right-sidebar/index.test.tsx +75 -0
- package/src/app/right-sidebar/index.tsx +395 -0
- package/src/app/right-sidebar/store.ts +15 -0
- package/src/app/right-sidebar/terminal/buffer.ts +65 -0
- package/src/app/right-sidebar/terminal/index.tsx +98 -0
- package/src/app/right-sidebar/terminal/persistent.tsx +122 -0
- package/src/app/right-sidebar/terminal/selection.ts +75 -0
- package/src/app/right-sidebar/terminal/use-terminal-session.ts +504 -0
- package/src/app/routes.ts +88 -0
- package/src/app/session/hooks/use-context-suggestions.ts +58 -0
- package/src/app/session/hooks/use-cwd-actions.ts +109 -0
- package/src/app/session/hooks/use-message-stream.ts +957 -0
- package/src/app/session/hooks/use-model-controls.test.tsx +198 -0
- package/src/app/session/hooks/use-model-controls.ts +106 -0
- package/src/app/session/hooks/use-nastech-config.ts +74 -0
- package/src/app/session/hooks/use-preview-routing.test.tsx +168 -0
- package/src/app/session/hooks/use-preview-routing.ts +223 -0
- package/src/app/session/hooks/use-prompt-actions.test.tsx +316 -0
- package/src/app/session/hooks/use-prompt-actions.ts +1030 -0
- package/src/app/session/hooks/use-route-resume.test.tsx +136 -0
- package/src/app/session/hooks/use-route-resume.ts +115 -0
- package/src/app/session/hooks/use-session-actions.test.tsx +119 -0
- package/src/app/session/hooks/use-session-actions.ts +885 -0
- package/src/app/session/hooks/use-session-state-cache.test.tsx +118 -0
- package/src/app/session/hooks/use-session-state-cache.ts +191 -0
- package/src/app/session-picker-overlay.tsx +32 -0
- package/src/app/session-switcher.tsx +107 -0
- package/src/app/settings/about-settings.tsx +173 -0
- package/src/app/settings/appearance-settings.tsx +162 -0
- package/src/app/settings/config-settings.tsx +384 -0
- package/src/app/settings/constants.ts +545 -0
- package/src/app/settings/credential-key-ui.tsx +373 -0
- package/src/app/settings/env-credentials.tsx +198 -0
- package/src/app/settings/env-var-actions-menu.tsx +136 -0
- package/src/app/settings/field-copy.ts +56 -0
- package/src/app/settings/gateway-settings.tsx +620 -0
- package/src/app/settings/helpers.test.ts +138 -0
- package/src/app/settings/helpers.ts +151 -0
- package/src/app/settings/index.tsx +237 -0
- package/src/app/settings/keys-settings.tsx +96 -0
- package/src/app/settings/mcp-settings.tsx +271 -0
- package/src/app/settings/model-settings.test.tsx +157 -0
- package/src/app/settings/model-settings.tsx +559 -0
- package/src/app/settings/notifications-settings.tsx +150 -0
- package/src/app/settings/primitives.tsx +115 -0
- package/src/app/settings/providers-settings.test.tsx +100 -0
- package/src/app/settings/providers-settings.tsx +258 -0
- package/src/app/settings/sessions-settings.tsx +276 -0
- package/src/app/settings/toolset-config-panel.test.tsx +289 -0
- package/src/app/settings/toolset-config-panel.tsx +449 -0
- package/src/app/settings/types.ts +42 -0
- package/src/app/settings/uninstall-section.tsx +185 -0
- package/src/app/settings/use-deep-link-highlight.ts +60 -0
- package/src/app/shell/app-shell.tsx +167 -0
- package/src/app/shell/gateway-menu-panel.tsx +150 -0
- package/src/app/shell/hooks/use-overlay-routing.ts +71 -0
- package/src/app/shell/hooks/use-status-snapshot.ts +57 -0
- package/src/app/shell/hooks/use-statusbar-items.tsx +403 -0
- package/src/app/shell/keybind-panel.tsx +220 -0
- package/src/app/shell/model-edit-submenu.test.tsx +84 -0
- package/src/app/shell/model-edit-submenu.tsx +245 -0
- package/src/app/shell/model-menu-panel.tsx +295 -0
- package/src/app/shell/sidebar-label.tsx +22 -0
- package/src/app/shell/statusbar-controls.tsx +185 -0
- package/src/app/shell/titlebar-controls.tsx +244 -0
- package/src/app/shell/titlebar.test.ts +26 -0
- package/src/app/shell/titlebar.ts +45 -0
- package/src/app/shell/use-group-registry.ts +39 -0
- package/src/app/skills/index.test.tsx +103 -0
- package/src/app/skills/index.tsx +371 -0
- package/src/app/types.ts +99 -0
- package/src/app/updates-overlay.tsx +369 -0
- package/src/components/Backdrop.tsx +114 -0
- package/src/components/assistant-ui/ansi-text.tsx +34 -0
- package/src/components/assistant-ui/clarify-tool.tsx +281 -0
- package/src/components/assistant-ui/directive-text.test.ts +39 -0
- package/src/components/assistant-ui/directive-text.tsx +389 -0
- package/src/components/assistant-ui/markdown-text.test.ts +204 -0
- package/src/components/assistant-ui/markdown-text.tsx +497 -0
- package/src/components/assistant-ui/message-render-boundary.test.tsx +80 -0
- package/src/components/assistant-ui/message-render-boundary.tsx +48 -0
- package/src/components/assistant-ui/streaming.test.tsx +739 -0
- package/src/components/assistant-ui/thread-list.tsx +307 -0
- package/src/components/assistant-ui/thread-virtualizer.tsx +512 -0
- package/src/components/assistant-ui/thread.tsx +1474 -0
- package/src/components/assistant-ui/todo-tool.tsx +109 -0
- package/src/components/assistant-ui/tool-approval-group.test.tsx +158 -0
- package/src/components/assistant-ui/tool-approval.test.tsx +81 -0
- package/src/components/assistant-ui/tool-approval.tsx +209 -0
- package/src/components/assistant-ui/tool-fallback-model.test.ts +66 -0
- package/src/components/assistant-ui/tool-fallback-model.ts +1368 -0
- package/src/components/assistant-ui/tool-fallback.tsx +466 -0
- package/src/components/assistant-ui/tooltip-icon-button.tsx +33 -0
- package/src/components/assistant-ui/user-message-edit.test.tsx +141 -0
- package/src/components/assistant-ui/user-message-text.tsx +150 -0
- package/src/components/boot-failure-overlay.tsx +246 -0
- package/src/components/boot-failure-reauth.test.ts +100 -0
- package/src/components/boot-failure-reauth.ts +81 -0
- package/src/components/brand-mark.tsx +19 -0
- package/src/components/chat/activity-timer-text.tsx +24 -0
- package/src/components/chat/activity-timer.test.tsx +43 -0
- package/src/components/chat/activity-timer.ts +64 -0
- package/src/components/chat/code-card.tsx +78 -0
- package/src/components/chat/compact-markdown.tsx +113 -0
- package/src/components/chat/composer-dock.ts +31 -0
- package/src/components/chat/diff-lines.tsx +54 -0
- package/src/components/chat/disclosure-row.tsx +63 -0
- package/src/components/chat/generated-image-context.tsx +19 -0
- package/src/components/chat/generated-image-result.tsx +174 -0
- package/src/components/chat/image-generation-placeholder.tsx +279 -0
- package/src/components/chat/intro-copy.jsonl +75 -0
- package/src/components/chat/intro.tsx +182 -0
- package/src/components/chat/preview-attachment.tsx +125 -0
- package/src/components/chat/shiki-highlighter.tsx +107 -0
- package/src/components/chat/status-row.tsx +70 -0
- package/src/components/chat/status-section.tsx +42 -0
- package/src/components/chat/terminal-output.tsx +50 -0
- package/src/components/chat/zoomable-image.tsx +177 -0
- package/src/components/desktop-install-overlay.tsx +595 -0
- package/src/components/desktop-onboarding-overlay.test.tsx +100 -0
- package/src/components/desktop-onboarding-overlay.tsx +1286 -0
- package/src/components/error-boundary.tsx +77 -0
- package/src/components/gateway-connecting-overlay.test.tsx +143 -0
- package/src/components/gateway-connecting-overlay.tsx +183 -0
- package/src/components/haptics-provider.tsx +19 -0
- package/src/components/language-switcher.test.tsx +53 -0
- package/src/components/language-switcher.tsx +175 -0
- package/src/components/model-picker.tsx +340 -0
- package/src/components/model-visibility-dialog.tsx +155 -0
- package/src/components/notifications.tsx +196 -0
- package/src/components/page-loader.tsx +34 -0
- package/src/components/pane-shell/context.ts +14 -0
- package/src/components/pane-shell/index.ts +4 -0
- package/src/components/pane-shell/pane-shell.test.tsx +333 -0
- package/src/components/pane-shell/pane-shell.tsx +330 -0
- package/src/components/prompt-overlays.tsx +234 -0
- package/src/components/session-picker.tsx +108 -0
- package/src/components/status-dot.tsx +26 -0
- package/src/components/ui/action-status.tsx +25 -0
- package/src/components/ui/alert.tsx +53 -0
- package/src/components/ui/badge.tsx +35 -0
- package/src/components/ui/braille-spinner.tsx +61 -0
- package/src/components/ui/button.tsx +81 -0
- package/src/components/ui/checkbox.tsx +27 -0
- package/src/components/ui/codicon.tsx +20 -0
- package/src/components/ui/command.tsx +111 -0
- package/src/components/ui/confirm-dialog.tsx +109 -0
- package/src/components/ui/context-menu.tsx +141 -0
- package/src/components/ui/control.ts +25 -0
- package/src/components/ui/copy-button.test.tsx +36 -0
- package/src/components/ui/copy-button.tsx +229 -0
- package/src/components/ui/dialog.tsx +152 -0
- package/src/components/ui/disclosure-caret.tsx +20 -0
- package/src/components/ui/dropdown-menu.tsx +291 -0
- package/src/components/ui/error-state.tsx +50 -0
- package/src/components/ui/fade-text.tsx +110 -0
- package/src/components/ui/glyph-spinner.tsx +63 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/kbd.tsx +37 -0
- package/src/components/ui/loader.tsx +558 -0
- package/src/components/ui/log-view.tsx +17 -0
- package/src/components/ui/pagination.tsx +114 -0
- package/src/components/ui/popover.tsx +44 -0
- package/src/components/ui/scroll-area.tsx +43 -0
- package/src/components/ui/search-field.tsx +80 -0
- package/src/components/ui/segmented-control.tsx +51 -0
- package/src/components/ui/select.tsx +92 -0
- package/src/components/ui/separator.tsx +26 -0
- package/src/components/ui/sheet.tsx +116 -0
- package/src/components/ui/sidebar.tsx +674 -0
- package/src/components/ui/skeleton.tsx +7 -0
- package/src/components/ui/switch.tsx +49 -0
- package/src/components/ui/tabs.tsx +36 -0
- package/src/components/ui/text-tab.tsx +43 -0
- package/src/components/ui/textarea.tsx +11 -0
- package/src/components/ui/tool-icon.tsx +65 -0
- package/src/components/ui/tooltip.tsx +69 -0
- package/src/fonts/JetBrainsMono-Bold.woff2 +0 -0
- package/src/fonts/JetBrainsMono-Italic.woff2 +0 -0
- package/src/fonts/JetBrainsMono-Regular.woff2 +0 -0
- package/src/global.d.ts +457 -0
- package/src/hooks/use-image-download.ts +85 -0
- package/src/hooks/use-media-query.ts +24 -0
- package/src/hooks/use-mobile.ts +3 -0
- package/src/hooks/use-resize-observer.ts +38 -0
- package/src/hooks/use-worktree-info.ts +68 -0
- package/src/i18n/catalog.ts +12 -0
- package/src/i18n/context.test.tsx +232 -0
- package/src/i18n/context.tsx +183 -0
- package/src/i18n/define-locale.ts +41 -0
- package/src/i18n/en.ts +1779 -0
- package/src/i18n/index.ts +20 -0
- package/src/i18n/ja.ts +1890 -0
- package/src/i18n/languages.test.ts +43 -0
- package/src/i18n/languages.ts +86 -0
- package/src/i18n/runtime.test.ts +75 -0
- package/src/i18n/runtime.ts +53 -0
- package/src/i18n/types.ts +1452 -0
- package/src/i18n/zh-hant.ts +1849 -0
- package/src/i18n/zh.ts +1923 -0
- package/src/lib/ansi.test.ts +123 -0
- package/src/lib/ansi.ts +175 -0
- package/src/lib/chat-messages.test.ts +708 -0
- package/src/lib/chat-messages.ts +885 -0
- package/src/lib/chat-runtime.test.ts +18 -0
- package/src/lib/chat-runtime.ts +335 -0
- package/src/lib/clipboard.ts +28 -0
- package/src/lib/commit-changelog.test.ts +114 -0
- package/src/lib/commit-changelog.ts +177 -0
- package/src/lib/completion-sound.ts +519 -0
- package/src/lib/desktop-fs.test.ts +116 -0
- package/src/lib/desktop-fs.ts +113 -0
- package/src/lib/desktop-slash-commands.test.ts +126 -0
- package/src/lib/desktop-slash-commands.ts +286 -0
- package/src/lib/embedded-images.test.ts +35 -0
- package/src/lib/embedded-images.ts +60 -0
- package/src/lib/external-link.test.tsx +168 -0
- package/src/lib/external-link.tsx +303 -0
- package/src/lib/gateway-events.test.ts +27 -0
- package/src/lib/gateway-events.ts +49 -0
- package/src/lib/gateway-ws-url.test.ts +78 -0
- package/src/lib/gateway-ws-url.ts +91 -0
- package/src/lib/generated-images.test.ts +97 -0
- package/src/lib/generated-images.ts +116 -0
- package/src/lib/haptics.ts +129 -0
- package/src/lib/icons.ts +203 -0
- package/src/lib/incremental-external-store-runtime.ts +188 -0
- package/src/lib/katex-memo.ts +260 -0
- package/src/lib/keybinds/actions.ts +125 -0
- package/src/lib/keybinds/combo.test.ts +86 -0
- package/src/lib/keybinds/combo.ts +169 -0
- package/src/lib/local-preview.ts +126 -0
- package/src/lib/markdown-code.test.ts +23 -0
- package/src/lib/markdown-code.ts +195 -0
- package/src/lib/markdown-preprocess.ts +386 -0
- package/src/lib/media.remote.test.ts +58 -0
- package/src/lib/media.ts +111 -0
- package/src/lib/model-status-label.test.ts +31 -0
- package/src/lib/model-status-label.ts +103 -0
- package/src/lib/mutable-ref.ts +6 -0
- package/src/lib/preview-targets.test.ts +27 -0
- package/src/lib/preview-targets.ts +63 -0
- package/src/lib/profile-color.ts +58 -0
- package/src/lib/provider-setup-errors.test.ts +26 -0
- package/src/lib/provider-setup-errors.ts +12 -0
- package/src/lib/query-client.ts +13 -0
- package/src/lib/remend-tail.test.ts +105 -0
- package/src/lib/remend-tail.ts +108 -0
- package/src/lib/runtime-readiness.test.ts +65 -0
- package/src/lib/runtime-readiness.ts +147 -0
- package/src/lib/session-export.ts +57 -0
- package/src/lib/session-search.test.ts +58 -0
- package/src/lib/session-search.ts +19 -0
- package/src/lib/session-source.ts +62 -0
- package/src/lib/speech-text.ts +35 -0
- package/src/lib/statusbar.ts +91 -0
- package/src/lib/storage.test.ts +25 -0
- package/src/lib/storage.ts +107 -0
- package/src/lib/todos.test.ts +35 -0
- package/src/lib/todos.ts +51 -0
- package/src/lib/tool-result-summary.test.ts +106 -0
- package/src/lib/tool-result-summary.ts +467 -0
- package/src/lib/update-copy.test.ts +38 -0
- package/src/lib/update-copy.ts +44 -0
- package/src/lib/use-enter-animation.ts +100 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/voice-playback.ts +128 -0
- package/src/lib/yolo-session.ts +26 -0
- package/src/main.tsx +43 -0
- package/src/nastech.test.ts +49 -0
- package/src/nastech.ts +718 -0
- package/src/store/activity.ts +100 -0
- package/src/store/boot.ts +91 -0
- package/src/store/clarify.test.ts +81 -0
- package/src/store/clarify.ts +69 -0
- package/src/store/command-palette.ts +20 -0
- package/src/store/compaction.test.ts +53 -0
- package/src/store/compaction.ts +38 -0
- package/src/store/completion-sound.ts +32 -0
- package/src/store/composer-input-history.test.ts +147 -0
- package/src/store/composer-input-history.ts +158 -0
- package/src/store/composer-queue.test.ts +148 -0
- package/src/store/composer-queue.ts +239 -0
- package/src/store/composer-status.test.ts +99 -0
- package/src/store/composer-status.ts +277 -0
- package/src/store/composer.test.ts +106 -0
- package/src/store/composer.ts +184 -0
- package/src/store/cron.ts +19 -0
- package/src/store/gateway.ts +290 -0
- package/src/store/haptics.ts +17 -0
- package/src/store/keybinds.ts +139 -0
- package/src/store/layout.ts +176 -0
- package/src/store/model-presets.test.ts +51 -0
- package/src/store/model-presets.ts +86 -0
- package/src/store/model-visibility.test.ts +37 -0
- package/src/store/model-visibility.ts +108 -0
- package/src/store/native-notifications.test.ts +192 -0
- package/src/store/native-notifications.ts +203 -0
- package/src/store/notifications.ts +165 -0
- package/src/store/onboarding.test.ts +372 -0
- package/src/store/onboarding.ts +866 -0
- package/src/store/panes.test.ts +146 -0
- package/src/store/panes.ts +145 -0
- package/src/store/preview.test.ts +135 -0
- package/src/store/preview.ts +466 -0
- package/src/store/profile.test.ts +89 -0
- package/src/store/profile.ts +365 -0
- package/src/store/prompts.test.ts +121 -0
- package/src/store/prompts.ts +115 -0
- package/src/store/session-switcher.test.ts +115 -0
- package/src/store/session-switcher.ts +128 -0
- package/src/store/session-sync.ts +25 -0
- package/src/store/session.test.ts +131 -0
- package/src/store/session.ts +255 -0
- package/src/store/subagents.test.ts +111 -0
- package/src/store/subagents.ts +260 -0
- package/src/store/thread-scroll.ts +46 -0
- package/src/store/todos.test.ts +47 -0
- package/src/store/todos.ts +64 -0
- package/src/store/tool-diffs.ts +23 -0
- package/src/store/tool-dismiss.ts +45 -0
- package/src/store/tool-view.ts +91 -0
- package/src/store/translucency.ts +38 -0
- package/src/store/updates.test.ts +77 -0
- package/src/store/updates.ts +315 -0
- package/src/store/voice-playback.ts +24 -0
- package/src/store/windows.test.ts +143 -0
- package/src/store/windows.ts +77 -0
- package/src/styles.css +1235 -0
- package/src/themes/color.ts +142 -0
- package/src/themes/context.tsx +339 -0
- package/src/themes/index.ts +3 -0
- package/src/themes/install.test.ts +119 -0
- package/src/themes/install.ts +95 -0
- package/src/themes/presets.test.ts +33 -0
- package/src/themes/presets.ts +293 -0
- package/src/themes/profile-theme.test.ts +41 -0
- package/src/themes/types.ts +66 -0
- package/src/themes/use-skin-command.ts +60 -0
- package/src/themes/user-themes.test.ts +63 -0
- package/src/themes/user-themes.ts +122 -0
- package/src/themes/vscode.test.ts +171 -0
- package/src/themes/vscode.ts +343 -0
- package/src/types/nastech.ts +646 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.json +25 -0
- 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
|
+
})
|