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