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,289 @@
|
|
|
1
|
+
import { Button } from '@/components/ui/button'
|
|
2
|
+
import { Codicon } from '@/components/ui/codicon'
|
|
3
|
+
import { Tip } from '@/components/ui/tooltip'
|
|
4
|
+
import { useI18n } from '@/i18n'
|
|
5
|
+
import { triggerHaptic } from '@/lib/haptics'
|
|
6
|
+
import { AudioLines, Layers3, Loader2, Square, SteeringWheel } from '@/lib/icons'
|
|
7
|
+
import { cn } from '@/lib/utils'
|
|
8
|
+
|
|
9
|
+
import type { ConversationStatus } from './hooks/use-voice-conversation'
|
|
10
|
+
import type { ChatBarState, VoiceStatus } from './types'
|
|
11
|
+
|
|
12
|
+
export const ICON_BTN = 'size-(--composer-control-size) shrink-0 rounded-md'
|
|
13
|
+
export const GHOST_ICON_BTN = cn(
|
|
14
|
+
ICON_BTN,
|
|
15
|
+
'text-(--ui-text-tertiary) hover:bg-(--chrome-action-hover) hover:text-foreground'
|
|
16
|
+
)
|
|
17
|
+
// Send/voice-conversation primary: solid foreground-on-background circle
|
|
18
|
+
// (reads as black-on-white in light mode, white-on-black in dark mode) to
|
|
19
|
+
// match the reference composer's high-contrast CTA. Keeps the pill itself
|
|
20
|
+
// neutral and lets the action visually dominate the row.
|
|
21
|
+
export const PRIMARY_ICON_BTN = cn(
|
|
22
|
+
'size-(--composer-control-primary-size,var(--composer-control-size)) shrink-0 rounded-full p-0',
|
|
23
|
+
'bg-foreground text-background hover:bg-foreground/90',
|
|
24
|
+
'disabled:bg-foreground/30 disabled:text-background disabled:opacity-100'
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
interface ConversationProps {
|
|
28
|
+
active: boolean
|
|
29
|
+
level: number
|
|
30
|
+
muted: boolean
|
|
31
|
+
status: ConversationStatus
|
|
32
|
+
onEnd: () => void
|
|
33
|
+
onStart: () => void
|
|
34
|
+
onStopTurn: () => void
|
|
35
|
+
onToggleMute: () => void
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function ComposerControls({
|
|
39
|
+
busy,
|
|
40
|
+
busyAction,
|
|
41
|
+
canSteer,
|
|
42
|
+
canSubmit,
|
|
43
|
+
conversation,
|
|
44
|
+
disabled,
|
|
45
|
+
hasComposerPayload,
|
|
46
|
+
state,
|
|
47
|
+
voiceStatus,
|
|
48
|
+
onDictate,
|
|
49
|
+
onSteer
|
|
50
|
+
}: {
|
|
51
|
+
busy: boolean
|
|
52
|
+
busyAction: 'queue' | 'stop'
|
|
53
|
+
canSteer: boolean
|
|
54
|
+
canSubmit: boolean
|
|
55
|
+
conversation: ConversationProps
|
|
56
|
+
disabled: boolean
|
|
57
|
+
hasComposerPayload: boolean
|
|
58
|
+
state: ChatBarState
|
|
59
|
+
voiceStatus: VoiceStatus
|
|
60
|
+
onDictate: () => void
|
|
61
|
+
onSteer: () => void
|
|
62
|
+
}) {
|
|
63
|
+
const { t } = useI18n()
|
|
64
|
+
const c = t.composer
|
|
65
|
+
|
|
66
|
+
if (conversation.active) {
|
|
67
|
+
return <ConversationPill {...conversation} disabled={disabled} />
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const showVoicePrimary = !busy && !hasComposerPayload
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div className="ml-auto flex shrink-0 items-center gap-(--composer-control-gap)">
|
|
74
|
+
<DictationButton disabled={disabled} onToggle={onDictate} state={state.voice} status={voiceStatus} />
|
|
75
|
+
{canSteer && (
|
|
76
|
+
<Tip label={c.steer}>
|
|
77
|
+
<Button
|
|
78
|
+
aria-label={c.steer}
|
|
79
|
+
className={GHOST_ICON_BTN}
|
|
80
|
+
disabled={disabled}
|
|
81
|
+
onClick={onSteer}
|
|
82
|
+
size="icon"
|
|
83
|
+
type="button"
|
|
84
|
+
variant="ghost"
|
|
85
|
+
>
|
|
86
|
+
<SteeringWheel size={16} />
|
|
87
|
+
</Button>
|
|
88
|
+
</Tip>
|
|
89
|
+
)}
|
|
90
|
+
{showVoicePrimary ? (
|
|
91
|
+
<Tip label={c.startVoice}>
|
|
92
|
+
<Button
|
|
93
|
+
aria-label={c.startVoice}
|
|
94
|
+
className={PRIMARY_ICON_BTN}
|
|
95
|
+
disabled={disabled}
|
|
96
|
+
onClick={() => {
|
|
97
|
+
triggerHaptic('open')
|
|
98
|
+
conversation.onStart()
|
|
99
|
+
}}
|
|
100
|
+
size="icon"
|
|
101
|
+
type="button"
|
|
102
|
+
>
|
|
103
|
+
<AudioLines size={17} />
|
|
104
|
+
</Button>
|
|
105
|
+
</Tip>
|
|
106
|
+
) : (
|
|
107
|
+
<Tip label={busy ? (busyAction === 'queue' ? c.queueMessage : c.stop) : c.send}>
|
|
108
|
+
<Button
|
|
109
|
+
aria-label={busy ? (busyAction === 'queue' ? c.queueMessage : c.stop) : c.send}
|
|
110
|
+
className={PRIMARY_ICON_BTN}
|
|
111
|
+
disabled={disabled || !canSubmit}
|
|
112
|
+
type="submit"
|
|
113
|
+
>
|
|
114
|
+
{busy ? (
|
|
115
|
+
busyAction === 'queue' ? (
|
|
116
|
+
<Layers3 size={16} />
|
|
117
|
+
) : (
|
|
118
|
+
<span className="block size-3 rounded-[0.1875rem] bg-current" />
|
|
119
|
+
)
|
|
120
|
+
) : (
|
|
121
|
+
<Codicon name="arrow-up" size="1rem" />
|
|
122
|
+
)}
|
|
123
|
+
</Button>
|
|
124
|
+
</Tip>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function ConversationPill({
|
|
131
|
+
disabled,
|
|
132
|
+
level,
|
|
133
|
+
muted,
|
|
134
|
+
onEnd,
|
|
135
|
+
onStopTurn,
|
|
136
|
+
onToggleMute,
|
|
137
|
+
status
|
|
138
|
+
}: ConversationProps & { disabled: boolean }) {
|
|
139
|
+
const { t } = useI18n()
|
|
140
|
+
const c = t.composer
|
|
141
|
+
const speaking = status === 'speaking'
|
|
142
|
+
const listening = status === 'listening' && !muted
|
|
143
|
+
|
|
144
|
+
const label =
|
|
145
|
+
status === 'speaking'
|
|
146
|
+
? c.speaking
|
|
147
|
+
: status === 'transcribing'
|
|
148
|
+
? c.transcribing
|
|
149
|
+
: status === 'thinking'
|
|
150
|
+
? c.thinking
|
|
151
|
+
: muted
|
|
152
|
+
? c.muted
|
|
153
|
+
: c.listening
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<div className="ml-auto flex shrink-0 items-center gap-(--composer-control-gap)">
|
|
157
|
+
<Tip label={muted ? c.unmuteMic : c.muteMic}>
|
|
158
|
+
<Button
|
|
159
|
+
aria-label={muted ? c.unmuteMic : c.muteMic}
|
|
160
|
+
aria-pressed={muted}
|
|
161
|
+
className={cn(GHOST_ICON_BTN, 'p-0', muted && 'bg-muted text-muted-foreground')}
|
|
162
|
+
disabled={disabled}
|
|
163
|
+
onClick={() => {
|
|
164
|
+
triggerHaptic('selection')
|
|
165
|
+
onToggleMute()
|
|
166
|
+
}}
|
|
167
|
+
size="icon"
|
|
168
|
+
type="button"
|
|
169
|
+
variant="ghost"
|
|
170
|
+
>
|
|
171
|
+
<Codicon name={muted ? 'mic-off' : 'mic'} size="1rem" />
|
|
172
|
+
</Button>
|
|
173
|
+
</Tip>
|
|
174
|
+
{listening && (
|
|
175
|
+
<Button
|
|
176
|
+
aria-label={c.stopListening}
|
|
177
|
+
className="h-(--composer-control-size) shrink-0 gap-1.5 rounded-full px-2.5 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
178
|
+
disabled={disabled}
|
|
179
|
+
onClick={() => {
|
|
180
|
+
triggerHaptic('submit')
|
|
181
|
+
onStopTurn()
|
|
182
|
+
}}
|
|
183
|
+
title={c.stopListening}
|
|
184
|
+
type="button"
|
|
185
|
+
variant="ghost"
|
|
186
|
+
>
|
|
187
|
+
<Square className="fill-current" size={11} />
|
|
188
|
+
<span>{c.stopShort}</span>
|
|
189
|
+
</Button>
|
|
190
|
+
)}
|
|
191
|
+
<Button
|
|
192
|
+
aria-label={c.endConversation}
|
|
193
|
+
className="h-(--composer-control-size) gap-1.5 rounded-full bg-primary px-3 text-xs font-medium text-primary-foreground hover:bg-primary/90"
|
|
194
|
+
disabled={disabled}
|
|
195
|
+
onClick={() => {
|
|
196
|
+
triggerHaptic('close')
|
|
197
|
+
onEnd()
|
|
198
|
+
}}
|
|
199
|
+
title={c.endConversation}
|
|
200
|
+
type="button"
|
|
201
|
+
>
|
|
202
|
+
<ConversationIndicator level={level} listening={listening} speaking={speaking} />
|
|
203
|
+
<span>{c.endShort}</span>
|
|
204
|
+
</Button>
|
|
205
|
+
<span className="sr-only" role="status">
|
|
206
|
+
{label}
|
|
207
|
+
</span>
|
|
208
|
+
</div>
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function ConversationIndicator({
|
|
213
|
+
level,
|
|
214
|
+
listening,
|
|
215
|
+
speaking
|
|
216
|
+
}: {
|
|
217
|
+
level: number
|
|
218
|
+
listening: boolean
|
|
219
|
+
speaking: boolean
|
|
220
|
+
}) {
|
|
221
|
+
if (speaking) {
|
|
222
|
+
return <Loader2 className="animate-spin" size={12} />
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const bars = [0.55, 0.85, 1, 0.85, 0.55]
|
|
226
|
+
const normalized = Math.max(0, Math.min(level, 1))
|
|
227
|
+
|
|
228
|
+
return (
|
|
229
|
+
<span aria-hidden="true" className="flex h-3 items-center gap-0.5">
|
|
230
|
+
{bars.map((weight, index) => {
|
|
231
|
+
const height = listening ? 0.3 + Math.min(0.7, normalized * weight) : 0.3
|
|
232
|
+
|
|
233
|
+
return <span className="w-0.5 rounded-full bg-current" key={index} style={{ height: `${height * 100}%` }} />
|
|
234
|
+
})}
|
|
235
|
+
</span>
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function DictationButton({
|
|
240
|
+
disabled,
|
|
241
|
+
state,
|
|
242
|
+
status,
|
|
243
|
+
onToggle
|
|
244
|
+
}: {
|
|
245
|
+
disabled: boolean
|
|
246
|
+
state: ChatBarState['voice']
|
|
247
|
+
status: VoiceStatus
|
|
248
|
+
onToggle: () => void
|
|
249
|
+
}) {
|
|
250
|
+
const { t } = useI18n()
|
|
251
|
+
const c = t.composer
|
|
252
|
+
const active = state.active || status !== 'idle'
|
|
253
|
+
|
|
254
|
+
const aria =
|
|
255
|
+
status === 'recording' ? c.stopDictation : status === 'transcribing' ? c.transcribingDictation : c.voiceDictation
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
<Tip label={aria}>
|
|
259
|
+
<Button
|
|
260
|
+
aria-label={aria}
|
|
261
|
+
aria-pressed={active}
|
|
262
|
+
className={cn(
|
|
263
|
+
GHOST_ICON_BTN,
|
|
264
|
+
'p-0',
|
|
265
|
+
'data-[active=true]:bg-accent data-[active=true]:text-foreground',
|
|
266
|
+
status === 'recording' && 'bg-primary/10 text-primary hover:bg-primary/15 hover:text-primary',
|
|
267
|
+
status === 'transcribing' && 'bg-primary/10 text-primary'
|
|
268
|
+
)}
|
|
269
|
+
data-active={active}
|
|
270
|
+
disabled={disabled || !state.enabled || status === 'transcribing'}
|
|
271
|
+
onClick={() => {
|
|
272
|
+
triggerHaptic(active ? 'close' : 'open')
|
|
273
|
+
onToggle()
|
|
274
|
+
}}
|
|
275
|
+
size="icon"
|
|
276
|
+
type="button"
|
|
277
|
+
variant="ghost"
|
|
278
|
+
>
|
|
279
|
+
{status === 'recording' ? (
|
|
280
|
+
<Square className="fill-current" size={12} />
|
|
281
|
+
) : status === 'transcribing' ? (
|
|
282
|
+
<Loader2 className="animate-spin" size={16} />
|
|
283
|
+
) : (
|
|
284
|
+
<Codicon name="mic" size="1rem" />
|
|
285
|
+
)}
|
|
286
|
+
</Button>
|
|
287
|
+
</Tip>
|
|
288
|
+
)
|
|
289
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { act, cleanup, fireEvent, render } from '@testing-library/react'
|
|
2
|
+
import { useRef, useState } from 'react'
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
4
|
+
|
|
5
|
+
// No global setupFiles registers auto-cleanup, so unmount between tests —
|
|
6
|
+
// otherwise a second render() leaks the first editor and getByTestId('editor')
|
|
7
|
+
// matches multiple nodes.
|
|
8
|
+
afterEach(cleanup)
|
|
9
|
+
|
|
10
|
+
// Faithful mirror of index.tsx's Enter wiring (handleEditorKeyDown's Enter
|
|
11
|
+
// branch + submitDraft), driven through REAL DOM keydown events on a
|
|
12
|
+
// contentEditable.
|
|
13
|
+
//
|
|
14
|
+
// Regression repro for #39630: pressing Enter right after typing (fast typing /
|
|
15
|
+
// IME) did nothing. The composer state (`draft` from useAuiState) and its
|
|
16
|
+
// derived `hasComposerPayload` lag the DOM by a render, so the keydown handler
|
|
17
|
+
// read empty state and either dropped the message, drained a queued prompt
|
|
18
|
+
// instead of sending, or (while busy) refused to queue. The fix reads the live
|
|
19
|
+
// editor text — `hasLivePayload` in the handler and a DOM re-sync at the top of
|
|
20
|
+
// submitDraft — so the just-typed text always wins.
|
|
21
|
+
//
|
|
22
|
+
// We model the race deterministically the way the IME repro does: mutate the
|
|
23
|
+
// editor's textContent WITHOUT firing an input event, so the React `draft`
|
|
24
|
+
// state stays stale while the DOM already holds the text.
|
|
25
|
+
function Harness({
|
|
26
|
+
busy = false,
|
|
27
|
+
disabled = false,
|
|
28
|
+
queued = [],
|
|
29
|
+
onSubmit,
|
|
30
|
+
onQueue,
|
|
31
|
+
onCancel,
|
|
32
|
+
onDrain
|
|
33
|
+
}: {
|
|
34
|
+
busy?: boolean
|
|
35
|
+
disabled?: boolean
|
|
36
|
+
queued?: readonly string[]
|
|
37
|
+
onSubmit: (text: string) => void
|
|
38
|
+
onQueue: (text: string) => void
|
|
39
|
+
onCancel: () => void
|
|
40
|
+
onDrain: () => void
|
|
41
|
+
}) {
|
|
42
|
+
const editorRef = useRef<HTMLDivElement>(null)
|
|
43
|
+
const draftRef = useRef('')
|
|
44
|
+
// Mirrors `useAuiState(s => s.composer.text)` — updated only via setText, so
|
|
45
|
+
// it lags the DOM until React re-renders (the source of the bug).
|
|
46
|
+
const [draft, setDraft] = useState('')
|
|
47
|
+
const attachments: unknown[] = []
|
|
48
|
+
|
|
49
|
+
const composerPlainText = (el: HTMLElement) => el.textContent ?? ''
|
|
50
|
+
|
|
51
|
+
const setText = (next: string) => {
|
|
52
|
+
draftRef.current = next
|
|
53
|
+
setDraft(next)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const submitDraft = () => {
|
|
57
|
+
if (disabled) {
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const editor = editorRef.current
|
|
62
|
+
if (editor) {
|
|
63
|
+
const domText = composerPlainText(editor)
|
|
64
|
+
if (domText !== draftRef.current) {
|
|
65
|
+
draftRef.current = domText
|
|
66
|
+
setDraft(domText)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const text = draftRef.current
|
|
71
|
+
const payloadPresent = text.trim().length > 0 || attachments.length > 0
|
|
72
|
+
|
|
73
|
+
if (busy) {
|
|
74
|
+
if (payloadPresent) {
|
|
75
|
+
onQueue(text)
|
|
76
|
+
} else {
|
|
77
|
+
onCancel()
|
|
78
|
+
}
|
|
79
|
+
} else if (!payloadPresent && queued.length > 0) {
|
|
80
|
+
onDrain()
|
|
81
|
+
} else if (payloadPresent) {
|
|
82
|
+
onSubmit(text)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
87
|
+
if (event.key === 'Enter' && !event.shiftKey) {
|
|
88
|
+
event.preventDefault()
|
|
89
|
+
|
|
90
|
+
const editorText = editorRef.current ? composerPlainText(editorRef.current) : draftRef.current
|
|
91
|
+
const hasLivePayload = editorText.trim().length > 0 || attachments.length > 0
|
|
92
|
+
|
|
93
|
+
if (disabled) {
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!busy && !hasLivePayload && queued.length > 0) {
|
|
98
|
+
onDrain()
|
|
99
|
+
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (busy && !hasLivePayload) {
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
submitDraft()
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// `draft` is read so the lint/compiler treats the stale-state mirror as live;
|
|
112
|
+
// the assertions prove the handler never relies on it.
|
|
113
|
+
void draft
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<div
|
|
117
|
+
contentEditable
|
|
118
|
+
data-testid="editor"
|
|
119
|
+
onInput={event => setText(composerPlainText(event.currentTarget))}
|
|
120
|
+
onKeyDown={handleKeyDown}
|
|
121
|
+
ref={editorRef}
|
|
122
|
+
suppressContentEditableWarning
|
|
123
|
+
/>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
describe('composer Enter submit — live DOM vs stale composer state (#39630)', () => {
|
|
128
|
+
it('sends the just-typed text on Enter even when composer state has not synced', async () => {
|
|
129
|
+
const onSubmit = vi.fn()
|
|
130
|
+
const { getByTestId } = render(
|
|
131
|
+
<Harness onCancel={vi.fn()} onDrain={vi.fn()} onQueue={vi.fn()} onSubmit={onSubmit} />
|
|
132
|
+
)
|
|
133
|
+
const editor = getByTestId('editor')
|
|
134
|
+
|
|
135
|
+
// Fast typing: the DOM has the text but NO input event fired, so `draft`
|
|
136
|
+
// state is still empty (the exact stale-state race).
|
|
137
|
+
await act(async () => {
|
|
138
|
+
editor.textContent = 'hello world'
|
|
139
|
+
fireEvent.keyDown(editor, { key: 'Enter' })
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
expect(onSubmit).toHaveBeenCalledWith('hello world')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('queues a fast-typed message while busy instead of draining the queue or cancelling', async () => {
|
|
146
|
+
const onQueue = vi.fn()
|
|
147
|
+
const onDrain = vi.fn()
|
|
148
|
+
const onCancel = vi.fn()
|
|
149
|
+
const { getByTestId } = render(
|
|
150
|
+
<Harness busy onCancel={onCancel} onDrain={onDrain} onQueue={onQueue} onSubmit={vi.fn()} queued={['queued-1']} />
|
|
151
|
+
)
|
|
152
|
+
const editor = getByTestId('editor')
|
|
153
|
+
|
|
154
|
+
await act(async () => {
|
|
155
|
+
editor.textContent = 'urgent follow-up'
|
|
156
|
+
fireEvent.keyDown(editor, { key: 'Enter' })
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
expect(onQueue).toHaveBeenCalledWith('urgent follow-up')
|
|
160
|
+
expect(onDrain).not.toHaveBeenCalled()
|
|
161
|
+
expect(onCancel).not.toHaveBeenCalled()
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('treats an empty Enter while busy as a no-op (never an accidental Stop)', async () => {
|
|
165
|
+
const onCancel = vi.fn()
|
|
166
|
+
const onSubmit = vi.fn()
|
|
167
|
+
const onQueue = vi.fn()
|
|
168
|
+
const { getByTestId } = render(
|
|
169
|
+
<Harness busy onCancel={onCancel} onDrain={vi.fn()} onQueue={onQueue} onSubmit={onSubmit} />
|
|
170
|
+
)
|
|
171
|
+
const editor = getByTestId('editor')
|
|
172
|
+
|
|
173
|
+
await act(async () => {
|
|
174
|
+
editor.textContent = ''
|
|
175
|
+
fireEvent.keyDown(editor, { key: 'Enter' })
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
expect(onCancel).not.toHaveBeenCalled()
|
|
179
|
+
expect(onSubmit).not.toHaveBeenCalled()
|
|
180
|
+
expect(onQueue).not.toHaveBeenCalled()
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('drains the next queued prompt on Enter when idle with a truly empty editor', async () => {
|
|
184
|
+
const onDrain = vi.fn()
|
|
185
|
+
const onSubmit = vi.fn()
|
|
186
|
+
const { getByTestId } = render(
|
|
187
|
+
<Harness onCancel={vi.fn()} onDrain={onDrain} onQueue={vi.fn()} onSubmit={onSubmit} queued={['queued-1']} />
|
|
188
|
+
)
|
|
189
|
+
const editor = getByTestId('editor')
|
|
190
|
+
|
|
191
|
+
await act(async () => {
|
|
192
|
+
editor.textContent = ''
|
|
193
|
+
fireEvent.keyDown(editor, { key: 'Enter' })
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
expect(onDrain).toHaveBeenCalledTimes(1)
|
|
197
|
+
expect(onSubmit).not.toHaveBeenCalled()
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('keeps reconnect drafts editable but blocks Enter submit until the gateway returns', async () => {
|
|
201
|
+
const onSubmit = vi.fn()
|
|
202
|
+
const onDrain = vi.fn()
|
|
203
|
+
const { getByTestId } = render(
|
|
204
|
+
<Harness disabled onCancel={vi.fn()} onDrain={onDrain} onQueue={vi.fn()} onSubmit={onSubmit} queued={['queued-1']} />
|
|
205
|
+
)
|
|
206
|
+
const editor = getByTestId('editor')
|
|
207
|
+
|
|
208
|
+
await act(async () => {
|
|
209
|
+
editor.textContent = 'draft while reconnecting'
|
|
210
|
+
fireEvent.input(editor)
|
|
211
|
+
fireEvent.keyDown(editor, { key: 'Enter' })
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
expect(editor.textContent).toBe('draft while reconnecting')
|
|
215
|
+
expect(onDrain).not.toHaveBeenCalled()
|
|
216
|
+
expect(onSubmit).not.toHaveBeenCalled()
|
|
217
|
+
})
|
|
218
|
+
})
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composer focus + external-insert bus.
|
|
3
|
+
*
|
|
4
|
+
* Mutations from outside the composer (sidebar attach, drag drop, terminal
|
|
5
|
+
* Cmd+L, preview console, etc.) dispatch through here. Each composer subscribes
|
|
6
|
+
* and routes the work back into its own ref/state.
|
|
7
|
+
*
|
|
8
|
+
* `dispatch` defers to a macrotask so synchronous click/keydown handlers
|
|
9
|
+
* (react-arborist row focus, picker `node.select()`) finish first and don't
|
|
10
|
+
* steal focus from the composer effect.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { InlineRefInput } from './inline-refs'
|
|
14
|
+
import { RICH_INPUT_SLOT } from './rich-editor'
|
|
15
|
+
|
|
16
|
+
export type ComposerTarget = 'edit' | 'main'
|
|
17
|
+
export type ComposerInsertMode = 'block' | 'inline'
|
|
18
|
+
|
|
19
|
+
interface FocusDetail {
|
|
20
|
+
target: ComposerTarget
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface InsertDetail {
|
|
24
|
+
mode: ComposerInsertMode
|
|
25
|
+
target: ComposerTarget
|
|
26
|
+
text: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface InsertRefsDetail {
|
|
30
|
+
refs: InlineRefInput[]
|
|
31
|
+
target: ComposerTarget
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const FOCUS_EVENT = 'NASTECH:composer-focus'
|
|
35
|
+
const INSERT_EVENT = 'NASTECH:composer-insert'
|
|
36
|
+
const INSERT_REFS_EVENT = 'NASTECH:composer-insert-refs'
|
|
37
|
+
|
|
38
|
+
let activeTarget: ComposerTarget = 'main'
|
|
39
|
+
|
|
40
|
+
const resolve = (target: ComposerTarget | 'active') => (target === 'active' ? activeTarget : target)
|
|
41
|
+
|
|
42
|
+
const dispatch = <T>(name: string, detail: T) => {
|
|
43
|
+
if (typeof window === 'undefined') {
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
window.setTimeout(() => window.dispatchEvent(new CustomEvent<T>(name, { detail })), 0)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const subscribe = <T>(name: string, handler: (detail: T) => void) => {
|
|
51
|
+
if (typeof window === 'undefined') {
|
|
52
|
+
return () => undefined
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const listener = (event: Event) => {
|
|
56
|
+
const detail = (event as CustomEvent<T>).detail
|
|
57
|
+
|
|
58
|
+
if (detail) {
|
|
59
|
+
handler(detail)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
window.addEventListener(name, listener)
|
|
64
|
+
|
|
65
|
+
return () => window.removeEventListener(name, listener)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const markActiveComposer = (target: ComposerTarget) => {
|
|
69
|
+
activeTarget = target
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const requestComposerFocus = (target: ComposerTarget | 'active' = 'active') =>
|
|
73
|
+
dispatch<FocusDetail>(FOCUS_EVENT, { target: resolve(target) })
|
|
74
|
+
|
|
75
|
+
export const requestComposerInsert = (
|
|
76
|
+
text: string,
|
|
77
|
+
{ mode = 'block', target = 'active' }: { mode?: ComposerInsertMode; target?: ComposerTarget | 'active' } = {}
|
|
78
|
+
) => {
|
|
79
|
+
const trimmed = text.trim()
|
|
80
|
+
|
|
81
|
+
if (!trimmed) {
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
dispatch<InsertDetail>(INSERT_EVENT, { mode, target: resolve(target), text: trimmed })
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const onComposerFocusRequest = (handler: (target: ComposerTarget) => void) =>
|
|
89
|
+
subscribe<FocusDetail>(FOCUS_EVENT, ({ target }) => handler(target))
|
|
90
|
+
|
|
91
|
+
export const onComposerInsertRequest = (handler: (detail: InsertDetail) => void) =>
|
|
92
|
+
subscribe<InsertDetail>(INSERT_EVENT, handler)
|
|
93
|
+
|
|
94
|
+
/** Insert typed ref chips (carrying a display label) into a composer — the
|
|
95
|
+
* structured cousin of {@link requestComposerInsert}, used for session links. */
|
|
96
|
+
export const requestComposerInsertRefs = (
|
|
97
|
+
refs: InlineRefInput[],
|
|
98
|
+
{ target = 'active' }: { target?: ComposerTarget | 'active' } = {}
|
|
99
|
+
) => {
|
|
100
|
+
if (refs.length) {
|
|
101
|
+
dispatch<InsertRefsDetail>(INSERT_REFS_EVENT, { refs, target: resolve(target) })
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export const onComposerInsertRefsRequest = (handler: (detail: InsertRefsDetail) => void) =>
|
|
106
|
+
subscribe<InsertRefsDetail>(INSERT_REFS_EVENT, handler)
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Focus a composer input across React commit + browser focus restore.
|
|
110
|
+
*
|
|
111
|
+
* The triple-call survives:
|
|
112
|
+
* - sync: contenteditable already mounted
|
|
113
|
+
* - rAF: React just committed a `renderComposerContents` swap
|
|
114
|
+
* - 0ms: browser focus reclaim from a click target inside an external panel
|
|
115
|
+
*/
|
|
116
|
+
export const focusComposerInput = (el: HTMLElement | null) => {
|
|
117
|
+
if (!el) {
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const focus = () => el.focus({ preventScroll: true })
|
|
122
|
+
|
|
123
|
+
focus()
|
|
124
|
+
window.requestAnimationFrame(focus)
|
|
125
|
+
window.setTimeout(focus, 0)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export const blurComposerInput = () => {
|
|
129
|
+
const el = document.querySelector(`[data-slot="${RICH_INPUT_SLOT}"]`) as HTMLElement | null
|
|
130
|
+
|
|
131
|
+
if (el && document.activeElement === el) {
|
|
132
|
+
el.blur()
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
import { useI18n } from '@/i18n'
|
|
4
|
+
|
|
5
|
+
import { COMPLETION_DRAWER_CLASS } from './completion-drawer'
|
|
6
|
+
|
|
7
|
+
const COMMON_COMMAND_KEYS = ['/help', '/clear', '/resume', '/details', '/copy', '/quit']
|
|
8
|
+
const HOTKEY_KEYS = ['@', '/', '?', 'Enter', 'Cmd/Ctrl+Shift+K', 'Cmd/Ctrl+/', 'Esc', '↑ / ↓']
|
|
9
|
+
|
|
10
|
+
export function HelpHint() {
|
|
11
|
+
const { t } = useI18n()
|
|
12
|
+
const c = t.composer
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div className={COMPLETION_DRAWER_CLASS} data-slot="composer-completion-drawer" data-state="open" role="dialog">
|
|
16
|
+
<Section title={c.commonCommands}>
|
|
17
|
+
{COMMON_COMMAND_KEYS.map(key => (
|
|
18
|
+
<Row description={c.commandDescs[key] ?? ''} key={key} keyLabel={key} mono />
|
|
19
|
+
))}
|
|
20
|
+
</Section>
|
|
21
|
+
|
|
22
|
+
<Section title={c.hotkeys}>
|
|
23
|
+
{HOTKEY_KEYS.map(key => (
|
|
24
|
+
<Row description={c.hotkeyDescs[key] ?? ''} key={key} keyLabel={key} />
|
|
25
|
+
))}
|
|
26
|
+
</Section>
|
|
27
|
+
|
|
28
|
+
<p className="px-2.5 py-1 text-xs text-muted-foreground/80">
|
|
29
|
+
<span className="font-mono text-foreground/80">/help</span> {c.helpFooter}
|
|
30
|
+
</p>
|
|
31
|
+
</div>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function Section({ children, title }: { children: ReactNode; title: string }) {
|
|
36
|
+
return (
|
|
37
|
+
<div className="grid gap-0.5 pt-0.5">
|
|
38
|
+
<p className="px-2.5 pb-0.5 pt-1 text-[0.65rem] font-medium uppercase tracking-wide text-muted-foreground/75">
|
|
39
|
+
{title}
|
|
40
|
+
</p>
|
|
41
|
+
{children}
|
|
42
|
+
</div>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function Row({ description, keyLabel, mono = false }: { description: string; keyLabel: string; mono?: boolean }) {
|
|
47
|
+
return (
|
|
48
|
+
<div className="flex min-w-0 items-baseline gap-2 rounded-md px-2.5 py-1 text-xs">
|
|
49
|
+
<span
|
|
50
|
+
className={
|
|
51
|
+
mono ? 'shrink-0 truncate font-mono font-medium text-foreground/85' : 'shrink-0 truncate text-foreground/85'
|
|
52
|
+
}
|
|
53
|
+
>
|
|
54
|
+
{keyLabel}
|
|
55
|
+
</span>
|
|
56
|
+
<span className="min-w-0 truncate text-muted-foreground/80">{description}</span>
|
|
57
|
+
</div>
|
|
58
|
+
)
|
|
59
|
+
}
|