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,512 @@
|
|
|
1
|
+
import { ThreadPrimitive, useAuiEvent, useAuiState } from '@assistant-ui/react'
|
|
2
|
+
import { useVirtualizer, type Virtualizer } from '@tanstack/react-virtual'
|
|
3
|
+
import {
|
|
4
|
+
type ComponentProps,
|
|
5
|
+
type FC,
|
|
6
|
+
memo,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
useCallback,
|
|
9
|
+
useEffect,
|
|
10
|
+
useLayoutEffect,
|
|
11
|
+
useMemo,
|
|
12
|
+
useRef
|
|
13
|
+
} from 'react'
|
|
14
|
+
|
|
15
|
+
import { setMutableRef } from '@/lib/mutable-ref'
|
|
16
|
+
import { cn } from '@/lib/utils'
|
|
17
|
+
import { setThreadScrolledUp } from '@/store/thread-scroll'
|
|
18
|
+
|
|
19
|
+
const ESTIMATED_ITEM_HEIGHT = 220
|
|
20
|
+
const OVERSCAN = 4
|
|
21
|
+
const AT_BOTTOM_THRESHOLD = 4
|
|
22
|
+
const POST_RUN_BOTTOM_LOCK_MS = 1_200
|
|
23
|
+
|
|
24
|
+
type ThreadMessageComponents = ComponentProps<typeof ThreadPrimitive.MessageByIndex>['components']
|
|
25
|
+
|
|
26
|
+
type MessageGroup = { id: string; index: number; kind: 'standalone' } | { id: string; indices: number[]; kind: 'turn' }
|
|
27
|
+
|
|
28
|
+
interface VirtualizedThreadProps {
|
|
29
|
+
clampToComposer: boolean
|
|
30
|
+
components: ThreadMessageComponents
|
|
31
|
+
emptyPlaceholder?: ReactNode
|
|
32
|
+
loadingIndicator?: ReactNode
|
|
33
|
+
sessionKey?: string | null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function buildGroups(signature: string): MessageGroup[] {
|
|
37
|
+
if (!signature) {
|
|
38
|
+
return []
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const messages = signature.split('\n').map(row => {
|
|
42
|
+
const [index, id, role] = row.split(':')
|
|
43
|
+
|
|
44
|
+
return { id, index: Number(index), role }
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const groups: MessageGroup[] = []
|
|
48
|
+
|
|
49
|
+
for (let i = 0; i < messages.length; i++) {
|
|
50
|
+
const message = messages[i]
|
|
51
|
+
|
|
52
|
+
if (message.role !== 'user') {
|
|
53
|
+
groups.push({ id: message.id, index: message.index, kind: 'standalone' })
|
|
54
|
+
|
|
55
|
+
continue
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const indices = [message.index]
|
|
59
|
+
|
|
60
|
+
while (i + 1 < messages.length && messages[i + 1].role !== 'user') {
|
|
61
|
+
indices.push(messages[++i].index)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
groups.push({ id: message.id, indices, kind: 'turn' })
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return groups
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const VirtualizedThreadInner: FC<VirtualizedThreadProps> = ({
|
|
71
|
+
clampToComposer,
|
|
72
|
+
components,
|
|
73
|
+
emptyPlaceholder,
|
|
74
|
+
loadingIndicator,
|
|
75
|
+
sessionKey
|
|
76
|
+
}) => {
|
|
77
|
+
const messageSignature = useAuiState(s =>
|
|
78
|
+
s.thread.messages.map((message, index) => `${index}:${message.id}:${message.role}`).join('\n')
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
const isRunning = useAuiState(s => s.thread.isRunning)
|
|
82
|
+
|
|
83
|
+
const groups = useMemo(() => buildGroups(messageSignature), [messageSignature])
|
|
84
|
+
const renderEmpty = groups.length === 0 && Boolean(emptyPlaceholder)
|
|
85
|
+
const scrollerRef = useRef<HTMLDivElement | null>(null)
|
|
86
|
+
|
|
87
|
+
// Shared ref so scrollToFn can check whether the user is parked at the
|
|
88
|
+
// bottom without needing a ref from inside useThreadScrollAnchor.
|
|
89
|
+
const stickyBottomRef = useRef(true)
|
|
90
|
+
|
|
91
|
+
const virtualizer = useVirtualizer({
|
|
92
|
+
count: groups.length,
|
|
93
|
+
estimateSize: () => ESTIMATED_ITEM_HEIGHT,
|
|
94
|
+
getItemKey: index => groups[index]?.id ?? index,
|
|
95
|
+
getScrollElement: () => scrollerRef.current,
|
|
96
|
+
// Seed the rect so the initial range mounts something before
|
|
97
|
+
// `observeElementRect` reports the real layout (it overrides this).
|
|
98
|
+
initialRect: { height: 600, width: 800 },
|
|
99
|
+
overscan: OVERSCAN,
|
|
100
|
+
// When the virtualizer adjusts scroll due to item measurement changes,
|
|
101
|
+
// skip the adjustment if the user is at the bottom. Our ResizeObserver +
|
|
102
|
+
// pinToBottom loop handles scroll anchoring; letting the virtualizer also
|
|
103
|
+
// adjust creates a feedback loop where the two fight each other,
|
|
104
|
+
// producing visible rubber-banding (the view snaps to the composer
|
|
105
|
+
// then jumps back up).
|
|
106
|
+
scrollToFn: (offset, _options, instance) => {
|
|
107
|
+
const el = instance.scrollElement
|
|
108
|
+
|
|
109
|
+
if (!el) {
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (stickyBottomRef.current) {
|
|
114
|
+
const maxScroll = el.scrollHeight - el.clientHeight
|
|
115
|
+
const distFromBottom = maxScroll - el.scrollTop
|
|
116
|
+
|
|
117
|
+
if (distFromBottom <= AT_BOTTOM_THRESHOLD && offset < maxScroll) {
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
;(el as HTMLElement).scrollTo(0, offset)
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
useThreadScrollAnchor({
|
|
127
|
+
enabled: !renderEmpty,
|
|
128
|
+
groupCount: groups.length,
|
|
129
|
+
isRunning,
|
|
130
|
+
scrollerRef,
|
|
131
|
+
sessionKey: sessionKey ?? null,
|
|
132
|
+
stickyBottomRef,
|
|
133
|
+
virtualizer
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
const virtualItems = virtualizer.getVirtualItems()
|
|
137
|
+
const totalSize = virtualizer.getTotalSize()
|
|
138
|
+
const paddingTop = virtualItems[0]?.start ?? 0
|
|
139
|
+
const paddingBottom = Math.max(0, totalSize - (virtualItems.at(-1)?.end ?? 0))
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<div
|
|
143
|
+
className="relative min-h-0 max-w-full overflow-hidden contain-[layout_paint]"
|
|
144
|
+
style={{ height: clampToComposer ? 'var(--thread-viewport-height)' : '100%' }}
|
|
145
|
+
>
|
|
146
|
+
<div
|
|
147
|
+
className="size-full overflow-x-hidden overflow-y-auto overscroll-contain"
|
|
148
|
+
data-slot="aui_thread-viewport"
|
|
149
|
+
ref={scrollerRef}
|
|
150
|
+
>
|
|
151
|
+
{renderEmpty ? (
|
|
152
|
+
<div
|
|
153
|
+
className="mx-auto grid h-full w-full max-w-(--composer-width) grid-rows-[minmax(0,1fr)_auto] min-w-0 gap-(--conversation-turn-gap) px-6 py-8"
|
|
154
|
+
data-slot="aui_thread-content"
|
|
155
|
+
>
|
|
156
|
+
{emptyPlaceholder}
|
|
157
|
+
</div>
|
|
158
|
+
) : (
|
|
159
|
+
<div
|
|
160
|
+
className={cn(
|
|
161
|
+
'mx-auto flex w-full max-w-(--composer-width) min-w-0 flex-col px-6 pt-[calc(var(--titlebar-height)+1.5rem)]'
|
|
162
|
+
)}
|
|
163
|
+
data-slot="aui_thread-content"
|
|
164
|
+
>
|
|
165
|
+
{/* Natural-flow virtualization: mounted items render as normal
|
|
166
|
+
flex siblings so `position: sticky` on the human bubble
|
|
167
|
+
resolves against the scroller without transform interference.
|
|
168
|
+
Padding spacers reserve scroll space for unmounted items. */}
|
|
169
|
+
<div style={{ paddingBottom: `${paddingBottom}px`, paddingTop: `${paddingTop}px` }}>
|
|
170
|
+
{virtualItems.map(virtualItem => {
|
|
171
|
+
const group = groups[virtualItem.index]
|
|
172
|
+
|
|
173
|
+
if (!group) {
|
|
174
|
+
return null
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<div
|
|
179
|
+
className="flex min-w-0 flex-col gap-(--conversation-turn-gap) pb-(--conversation-turn-gap)"
|
|
180
|
+
data-index={virtualItem.index}
|
|
181
|
+
key={virtualItem.key}
|
|
182
|
+
ref={virtualizer.measureElement}
|
|
183
|
+
>
|
|
184
|
+
{group.kind === 'turn' ? (
|
|
185
|
+
<div
|
|
186
|
+
className="composer-human-ai-pair-container relative flex min-w-0 flex-col gap-(--conversation-turn-gap)"
|
|
187
|
+
data-slot="aui_turn-pair"
|
|
188
|
+
>
|
|
189
|
+
{group.indices.map(index => (
|
|
190
|
+
<ThreadPrimitive.MessageByIndex components={components} index={index} key={index} />
|
|
191
|
+
))}
|
|
192
|
+
</div>
|
|
193
|
+
) : (
|
|
194
|
+
<ThreadPrimitive.MessageByIndex components={components} index={group.index} />
|
|
195
|
+
)}
|
|
196
|
+
</div>
|
|
197
|
+
)
|
|
198
|
+
})}
|
|
199
|
+
</div>
|
|
200
|
+
{loadingIndicator}
|
|
201
|
+
{clampToComposer && (
|
|
202
|
+
<div
|
|
203
|
+
aria-hidden="true"
|
|
204
|
+
className="shrink-0"
|
|
205
|
+
data-slot="aui_composer-clearance"
|
|
206
|
+
style={{ height: 'var(--thread-last-message-clearance)' }}
|
|
207
|
+
/>
|
|
208
|
+
)}
|
|
209
|
+
</div>
|
|
210
|
+
)}
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export const VirtualizedThread = memo(VirtualizedThreadInner)
|
|
217
|
+
|
|
218
|
+
function scrollElementToBottom(el: HTMLDivElement) {
|
|
219
|
+
el.scrollTop = el.scrollHeight
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
interface ScrollAnchorOptions {
|
|
223
|
+
enabled: boolean
|
|
224
|
+
groupCount: number
|
|
225
|
+
isRunning: boolean
|
|
226
|
+
scrollerRef: React.RefObject<HTMLDivElement | null>
|
|
227
|
+
sessionKey: string | null
|
|
228
|
+
stickyBottomRef: React.MutableRefObject<boolean>
|
|
229
|
+
virtualizer: Virtualizer<HTMLDivElement, Element>
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function useThreadScrollAnchor({
|
|
233
|
+
enabled,
|
|
234
|
+
groupCount,
|
|
235
|
+
isRunning,
|
|
236
|
+
scrollerRef,
|
|
237
|
+
sessionKey,
|
|
238
|
+
stickyBottomRef,
|
|
239
|
+
virtualizer
|
|
240
|
+
}: ScrollAnchorOptions) {
|
|
241
|
+
// `stickyBottomRef` = parked at bottom, content growth should follow. Cleared on
|
|
242
|
+
// user-driven upward scroll; re-armed when they reach bottom again.
|
|
243
|
+
// This is a shared ref — scrollToFn reads it to prevent the virtualizer's
|
|
244
|
+
// measurement adjustments from fighting our pinToBottom.
|
|
245
|
+
const lastTopRef = useRef(0)
|
|
246
|
+
const lastHeightRef = useRef(0)
|
|
247
|
+
const lastClientHeightRef = useRef(0)
|
|
248
|
+
// Counter that tracks how many scroll events we expect to be ours rather
|
|
249
|
+
// than the user's. `pinToBottom` writes `el.scrollTop`, which fires an
|
|
250
|
+
// async `scroll` event; without this guard the on-scroll handler can race
|
|
251
|
+
// with the programmatic write (because content also grew, the *resulting*
|
|
252
|
+
// scrollTop can be lower than `lastTopRef` from the previous frame) and
|
|
253
|
+
// misread the programmatic pin as the user scrolling up — which disarms
|
|
254
|
+
// sticky-bottom and the user's just-submitted message slides above the
|
|
255
|
+
// fold. See `apps/desktop/scripts/measure-jump.mjs` for the repro
|
|
256
|
+
// (distFromBottom 0 → 49 within one frame, sticking forever).
|
|
257
|
+
const programmaticScrollPendingRef = useRef(0)
|
|
258
|
+
const prevSessionKeyRef = useRef(sessionKey)
|
|
259
|
+
const prevGroupCountRef = useRef(0)
|
|
260
|
+
|
|
261
|
+
const pinToBottom = useCallback(() => {
|
|
262
|
+
const el = scrollerRef.current
|
|
263
|
+
|
|
264
|
+
if (!el) {
|
|
265
|
+
return
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Hold the disarm gate across the scroll event the next line will fire.
|
|
269
|
+
programmaticScrollPendingRef.current += 1
|
|
270
|
+
scrollElementToBottom(el)
|
|
271
|
+
lastTopRef.current = el.scrollTop
|
|
272
|
+
lastHeightRef.current = el.scrollHeight
|
|
273
|
+
lastClientHeightRef.current = el.clientHeight
|
|
274
|
+
}, [scrollerRef])
|
|
275
|
+
|
|
276
|
+
const jumpToBottom = useCallback(() => {
|
|
277
|
+
setMutableRef(stickyBottomRef, true)
|
|
278
|
+
|
|
279
|
+
if (groupCount > 0) {
|
|
280
|
+
virtualizer.scrollToIndex(groupCount - 1, { align: 'end', behavior: 'auto' })
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
requestAnimationFrame(() => {
|
|
284
|
+
if (stickyBottomRef.current) {
|
|
285
|
+
pinToBottom()
|
|
286
|
+
}
|
|
287
|
+
})
|
|
288
|
+
}, [groupCount, pinToBottom, stickyBottomRef, virtualizer])
|
|
289
|
+
|
|
290
|
+
useEffect(() => () => setThreadScrolledUp(false), [])
|
|
291
|
+
|
|
292
|
+
// Track at-bottom state, dim composer when scrolled up, disarm on user
|
|
293
|
+
// scroll/wheel/touch.
|
|
294
|
+
useEffect(() => {
|
|
295
|
+
const el = scrollerRef.current
|
|
296
|
+
|
|
297
|
+
if (!el) {
|
|
298
|
+
return undefined
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const disarm = () => {
|
|
302
|
+
setMutableRef(stickyBottomRef, false)
|
|
303
|
+
programmaticScrollPendingRef.current = 0
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const onScroll = () => {
|
|
307
|
+
const top = el.scrollTop
|
|
308
|
+
|
|
309
|
+
// If this scroll event is the consequence of `pinToBottom` writing
|
|
310
|
+
// `el.scrollTop`, treat it as ours: don't disarm. The RO + rAF pin
|
|
311
|
+
// loop will re-pin on the next frame if the browser clamped us
|
|
312
|
+
// short of bottom (because content grew in the same frame).
|
|
313
|
+
// Without this guard the post-pin scrollTop gets misread as the
|
|
314
|
+
// user scrolling up, disarming sticky-bottom permanently and
|
|
315
|
+
// leaving the just-submitted message below the fold.
|
|
316
|
+
if (programmaticScrollPendingRef.current > 0) {
|
|
317
|
+
programmaticScrollPendingRef.current -= 1
|
|
318
|
+
lastTopRef.current = top
|
|
319
|
+
lastHeightRef.current = el.scrollHeight
|
|
320
|
+
lastClientHeightRef.current = el.clientHeight
|
|
321
|
+
// Always re-arm — sticky-bottom should hold through clamp races.
|
|
322
|
+
setMutableRef(stickyBottomRef, true)
|
|
323
|
+
const atBottom = el.scrollHeight - (top + el.clientHeight) <= AT_BOTTOM_THRESHOLD
|
|
324
|
+
setThreadScrolledUp(!atBottom)
|
|
325
|
+
|
|
326
|
+
return
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Disarm only when `scrollTop` decreases while both content height and
|
|
330
|
+
// viewport height are stable. A bare `top < lastTopRef.current` check is
|
|
331
|
+
// unsafe: virtualizer measurement, streaming markdown, composer resizing,
|
|
332
|
+
// window resizing, and toolbar/status updates can all move scrollTop as a
|
|
333
|
+
// layout side effect. Wheel-up and touchmove still disarm immediately via
|
|
334
|
+
// their own listeners below, so real user intent remains covered.
|
|
335
|
+
const heightGrew = el.scrollHeight > lastHeightRef.current
|
|
336
|
+
const clientHeightChanged = Math.abs(el.clientHeight - lastClientHeightRef.current) > 1
|
|
337
|
+
|
|
338
|
+
if (!heightGrew && !clientHeightChanged && top + 1 < lastTopRef.current) {
|
|
339
|
+
setMutableRef(stickyBottomRef, false)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
lastTopRef.current = top
|
|
343
|
+
lastHeightRef.current = el.scrollHeight
|
|
344
|
+
lastClientHeightRef.current = el.clientHeight
|
|
345
|
+
|
|
346
|
+
const atBottom = el.scrollHeight - (top + el.clientHeight) <= AT_BOTTOM_THRESHOLD
|
|
347
|
+
|
|
348
|
+
if (atBottom) {
|
|
349
|
+
setMutableRef(stickyBottomRef, true)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
setThreadScrolledUp(!atBottom)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const onWheel = (event: WheelEvent) => {
|
|
356
|
+
if (event.deltaY < 0) {
|
|
357
|
+
disarm()
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
el.addEventListener('scroll', onScroll, { passive: true })
|
|
362
|
+
el.addEventListener('wheel', onWheel, { passive: true })
|
|
363
|
+
el.addEventListener('touchmove', disarm, { passive: true })
|
|
364
|
+
|
|
365
|
+
return () => {
|
|
366
|
+
el.removeEventListener('scroll', onScroll)
|
|
367
|
+
el.removeEventListener('wheel', onWheel)
|
|
368
|
+
el.removeEventListener('touchmove', disarm)
|
|
369
|
+
}
|
|
370
|
+
}, [scrollerRef, stickyBottomRef])
|
|
371
|
+
|
|
372
|
+
// Follow content growth (streaming, item measurements, loading indicator)
|
|
373
|
+
// while armed. During fast streaming the ResizeObserver can fire many
|
|
374
|
+
// times per frame as Streamdown re-tokenizes; coalesce to one pin per
|
|
375
|
+
// animation frame so we don't run the scroll-event/re-pin chain
|
|
376
|
+
// (~20+ ms self in `Virtualizer.getMaxScrollOffset`) several times per
|
|
377
|
+
// token.
|
|
378
|
+
useEffect(() => {
|
|
379
|
+
if (!enabled || !isRunning) {
|
|
380
|
+
return undefined
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const el = scrollerRef.current
|
|
384
|
+
|
|
385
|
+
if (!el) {
|
|
386
|
+
return undefined
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
let pinRafScheduled = false
|
|
390
|
+
|
|
391
|
+
const schedulePin = () => {
|
|
392
|
+
if (pinRafScheduled || !stickyBottomRef.current) {
|
|
393
|
+
return
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
pinRafScheduled = true
|
|
397
|
+
requestAnimationFrame(() => {
|
|
398
|
+
pinRafScheduled = false
|
|
399
|
+
|
|
400
|
+
if (stickyBottomRef.current) {
|
|
401
|
+
pinToBottom()
|
|
402
|
+
}
|
|
403
|
+
})
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const observer = new ResizeObserver(schedulePin)
|
|
407
|
+
|
|
408
|
+
// Observe ONLY the content (firstElementChild), not the scroller `el`
|
|
409
|
+
// itself. Resizes of the viewport/scroller (window resize, devtools
|
|
410
|
+
// panel toggle) shouldn't trigger a pin — only content growth should.
|
|
411
|
+
if (el.firstElementChild) {
|
|
412
|
+
observer.observe(el.firstElementChild)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return () => observer.disconnect()
|
|
416
|
+
}, [enabled, isRunning, pinToBottom, scrollerRef, stickyBottomRef])
|
|
417
|
+
|
|
418
|
+
// Jump to bottom on session change OR when an empty thread first gets
|
|
419
|
+
// content. Both share the same intent and the same effect.
|
|
420
|
+
useEffect(() => {
|
|
421
|
+
const sessionChanged = prevSessionKeyRef.current !== sessionKey
|
|
422
|
+
const becameNonEmpty = prevGroupCountRef.current === 0 && groupCount > 0
|
|
423
|
+
|
|
424
|
+
prevSessionKeyRef.current = sessionKey
|
|
425
|
+
prevGroupCountRef.current = groupCount
|
|
426
|
+
|
|
427
|
+
if (enabled && (sessionChanged || becameNonEmpty)) {
|
|
428
|
+
jumpToBottom()
|
|
429
|
+
}
|
|
430
|
+
}, [enabled, groupCount, jumpToBottom, sessionKey])
|
|
431
|
+
|
|
432
|
+
// Pre-paint pin: when groupCount increases while armed (optimistic user
|
|
433
|
+
// message insert, streaming assistant turn arriving, etc.), pin BEFORE
|
|
434
|
+
// the browser commits the layout to screen. Using useLayoutEffect rather
|
|
435
|
+
// than useEffect so this runs synchronously after React commits the DOM
|
|
436
|
+
// mutation but before the browser paints. Without this, there's a ~50ms
|
|
437
|
+
// visual window where the new message sits below the fold while we wait
|
|
438
|
+
// for the ResizeObserver / scroll event chain to fire and re-pin.
|
|
439
|
+
//
|
|
440
|
+
// We pin TWICE in this critical path — once synchronously, then once on
|
|
441
|
+
// the next rAF. The second pin catches the case where React mounts the
|
|
442
|
+
// new message in the second commit (after our layout effect ran), which
|
|
443
|
+
// grows scrollHeight again; without the rAF pin the user briefly sees a
|
|
444
|
+
// ~15 px gap below the new message until the RO catches up. Streaming
|
|
445
|
+
// tokens use the rate-limited RO path only; only the group-count change
|
|
446
|
+
// (which fires once per user submit / new turn arrival) pays for the
|
|
447
|
+
// extra pin.
|
|
448
|
+
const prevGroupCountForLayoutRef = useRef(groupCount)
|
|
449
|
+
useLayoutEffect(() => {
|
|
450
|
+
if (!enabled) {
|
|
451
|
+
return
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (groupCount > prevGroupCountForLayoutRef.current && stickyBottomRef.current) {
|
|
455
|
+
// Defer to rAF so that browser scroll/wheel events from the current
|
|
456
|
+
// frame are processed first. Without this deferral, a trackpad
|
|
457
|
+
// scroll-up during streaming can race with this effect: the wheel
|
|
458
|
+
// event hasn't fired yet so stickyBottomRef is still true, and the
|
|
459
|
+
// immediate pinToBottom() would snap the viewport back to bottom
|
|
460
|
+
// against the user's intent.
|
|
461
|
+
requestAnimationFrame(() => {
|
|
462
|
+
if (stickyBottomRef.current) {
|
|
463
|
+
pinToBottom()
|
|
464
|
+
}
|
|
465
|
+
})
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
prevGroupCountForLayoutRef.current = groupCount
|
|
469
|
+
}, [enabled, groupCount, pinToBottom, stickyBottomRef])
|
|
470
|
+
|
|
471
|
+
// Completion swaps streaming placeholders/plain code for final rendered DOM
|
|
472
|
+
// (notably Shiki-highlighted code). Keep following the bottom briefly after
|
|
473
|
+
// `isRunning` flips false so that final measurement pass cannot strand the
|
|
474
|
+
// viewport near the top of a large code block.
|
|
475
|
+
const prevIsRunningForLayoutRef = useRef(isRunning)
|
|
476
|
+
useLayoutEffect(() => {
|
|
477
|
+
const finishedRun = prevIsRunningForLayoutRef.current && !isRunning
|
|
478
|
+
prevIsRunningForLayoutRef.current = isRunning
|
|
479
|
+
|
|
480
|
+
if (!enabled || !finishedRun || !stickyBottomRef.current) {
|
|
481
|
+
return undefined
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const lockUntil = performance.now() + POST_RUN_BOTTOM_LOCK_MS
|
|
485
|
+
let lockRaf: number | null = null
|
|
486
|
+
|
|
487
|
+
const lockFrame = () => {
|
|
488
|
+
lockRaf = null
|
|
489
|
+
|
|
490
|
+
if (!stickyBottomRef.current) {
|
|
491
|
+
return
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
pinToBottom()
|
|
495
|
+
|
|
496
|
+
if (performance.now() < lockUntil) {
|
|
497
|
+
lockRaf = requestAnimationFrame(lockFrame)
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
pinToBottom()
|
|
502
|
+
lockRaf = requestAnimationFrame(lockFrame)
|
|
503
|
+
|
|
504
|
+
return () => {
|
|
505
|
+
if (lockRaf !== null) {
|
|
506
|
+
cancelAnimationFrame(lockRaf)
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}, [enabled, isRunning, pinToBottom, stickyBottomRef])
|
|
510
|
+
|
|
511
|
+
useAuiEvent('thread.runStart', jumpToBottom)
|
|
512
|
+
}
|