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,84 @@
|
|
|
1
|
+
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
|
2
|
+
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuSub, DropdownMenuSubTrigger } from '@/components/ui/dropdown-menu'
|
|
5
|
+
import { $modelPresets, getModelPreset } from '@/store/model-presets'
|
|
6
|
+
import { $activeSessionId } from '@/store/session'
|
|
7
|
+
|
|
8
|
+
import { type FastControl, ModelEditSubmenu } from './model-edit-submenu'
|
|
9
|
+
|
|
10
|
+
// Radix calls these on open; jsdom doesn't implement them.
|
|
11
|
+
beforeAll(() => {
|
|
12
|
+
Element.prototype.scrollIntoView = vi.fn()
|
|
13
|
+
Element.prototype.hasPointerCapture = vi.fn(() => false)
|
|
14
|
+
Element.prototype.releasePointerCapture = vi.fn()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
$modelPresets.set({})
|
|
19
|
+
$activeSessionId.set(null)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
cleanup()
|
|
24
|
+
vi.clearAllMocks()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
// Render the submenu inside an open menu/sub so its content (switches) mounts.
|
|
28
|
+
function renderSubmenu(opts: { fastControl: FastControl; reasoning: boolean; requestGateway: () => Promise<unknown> }) {
|
|
29
|
+
return render(
|
|
30
|
+
<DropdownMenu open>
|
|
31
|
+
<DropdownMenuContent>
|
|
32
|
+
<DropdownMenuSub open>
|
|
33
|
+
<DropdownMenuSubTrigger>edit</DropdownMenuSubTrigger>
|
|
34
|
+
<ModelEditSubmenu
|
|
35
|
+
effort="medium"
|
|
36
|
+
fastControl={opts.fastControl}
|
|
37
|
+
isActive
|
|
38
|
+
model="m1"
|
|
39
|
+
onSelectModel={vi.fn()}
|
|
40
|
+
provider="p1"
|
|
41
|
+
reasoning={opts.reasoning}
|
|
42
|
+
requestGateway={opts.requestGateway as never}
|
|
43
|
+
/>
|
|
44
|
+
</DropdownMenuSub>
|
|
45
|
+
</DropdownMenuContent>
|
|
46
|
+
</DropdownMenu>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Regression: editing the active row before a live session exists must stay
|
|
51
|
+
// preset-only — the gateway's config.set falls back to global config when no
|
|
52
|
+
// session matches, so it must not be called. (Caught in the second review.)
|
|
53
|
+
describe('ModelEditSubmenu no-session guard', () => {
|
|
54
|
+
it('param fast: records the preset but skips the gateway without a session', () => {
|
|
55
|
+
const requestGateway = vi.fn().mockResolvedValue({})
|
|
56
|
+
renderSubmenu({ fastControl: { kind: 'param', on: false }, reasoning: false, requestGateway })
|
|
57
|
+
|
|
58
|
+
fireEvent.click(screen.getByRole('switch'))
|
|
59
|
+
|
|
60
|
+
expect(getModelPreset('p1', 'm1').fast).toBe(true)
|
|
61
|
+
expect(requestGateway).not.toHaveBeenCalled()
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('reasoning: records the preset but skips the gateway without a session', () => {
|
|
65
|
+
const requestGateway = vi.fn().mockResolvedValue({})
|
|
66
|
+
renderSubmenu({ fastControl: { kind: 'none' }, reasoning: true, requestGateway })
|
|
67
|
+
|
|
68
|
+
// Thinking starts on (medium); toggling it off routes through patchReasoning.
|
|
69
|
+
fireEvent.click(screen.getByRole('switch'))
|
|
70
|
+
|
|
71
|
+
expect(getModelPreset('p1', 'm1').effort).toBe('none')
|
|
72
|
+
expect(requestGateway).not.toHaveBeenCalled()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('param fast: pushes to the gateway once a session is active', async () => {
|
|
76
|
+
const requestGateway = vi.fn().mockResolvedValue({})
|
|
77
|
+
$activeSessionId.set('sess1')
|
|
78
|
+
renderSubmenu({ fastControl: { kind: 'param', on: false }, reasoning: false, requestGateway })
|
|
79
|
+
|
|
80
|
+
fireEvent.click(screen.getByRole('switch'))
|
|
81
|
+
|
|
82
|
+
expect(requestGateway).toHaveBeenCalledWith('config.set', { key: 'fast', session_id: 'sess1', value: 'fast' })
|
|
83
|
+
})
|
|
84
|
+
})
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { useStore } from '@nanostores/react'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
DropdownMenuItem,
|
|
5
|
+
DropdownMenuLabel,
|
|
6
|
+
DropdownMenuRadioGroup,
|
|
7
|
+
DropdownMenuRadioItem,
|
|
8
|
+
dropdownMenuRow,
|
|
9
|
+
dropdownMenuSectionLabel,
|
|
10
|
+
DropdownMenuSeparator,
|
|
11
|
+
DropdownMenuSubContent
|
|
12
|
+
} from '@/components/ui/dropdown-menu'
|
|
13
|
+
import { Switch } from '@/components/ui/switch'
|
|
14
|
+
import { useI18n } from '@/i18n'
|
|
15
|
+
import { notifyError } from '@/store/notifications'
|
|
16
|
+
import {
|
|
17
|
+
$activeSessionId,
|
|
18
|
+
$currentReasoningEffort,
|
|
19
|
+
setCurrentFastMode,
|
|
20
|
+
setCurrentReasoningEffort
|
|
21
|
+
} from '@/store/session'
|
|
22
|
+
|
|
23
|
+
// NasTech' real reasoning levels (see VALID_REASONING_EFFORTS); `none` is owned
|
|
24
|
+
// by the Thinking toggle, not the radio.
|
|
25
|
+
const EFFORT_OPTIONS = [
|
|
26
|
+
{ value: 'minimal', labelKey: 'minimal' },
|
|
27
|
+
{ value: 'low', labelKey: 'low' },
|
|
28
|
+
{ value: 'medium', labelKey: 'medium' },
|
|
29
|
+
{ value: 'high', labelKey: 'high' },
|
|
30
|
+
{ value: 'xhigh', labelKey: 'max' }
|
|
31
|
+
] as const
|
|
32
|
+
|
|
33
|
+
/** How "fast" is achieved for a given model — two different mechanisms:
|
|
34
|
+
* - `param`: the Anthropic/OpenAI `speed=fast` request parameter.
|
|
35
|
+
* - `variant`: a separate `…-fast` sibling model selected via the model field.
|
|
36
|
+
*/
|
|
37
|
+
export type FastControl =
|
|
38
|
+
| { kind: 'none' }
|
|
39
|
+
| { kind: 'param'; on: boolean }
|
|
40
|
+
| { kind: 'variant'; baseId: string; fastId: string; on: boolean }
|
|
41
|
+
|
|
42
|
+
/** Resolve the fast mechanism for a model: prefer the speed=fast parameter
|
|
43
|
+
* when the backend supports it, else fall back to a `…-fast` sibling model. */
|
|
44
|
+
export function resolveFastControl(
|
|
45
|
+
model: string,
|
|
46
|
+
providerModels: readonly string[],
|
|
47
|
+
paramSupported: boolean,
|
|
48
|
+
currentFastMode: boolean
|
|
49
|
+
): FastControl {
|
|
50
|
+
if (paramSupported) {
|
|
51
|
+
return { kind: 'param', on: currentFastMode }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (/-fast$/i.test(model)) {
|
|
55
|
+
const baseId = model.replace(/-fast$/i, '')
|
|
56
|
+
|
|
57
|
+
// Only a toggle if there's a base to switch back to; otherwise it's a
|
|
58
|
+
// standalone fast model with no "off" state.
|
|
59
|
+
return providerModels.includes(baseId) ? { kind: 'variant', baseId, fastId: model, on: true } : { kind: 'none' }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const fastId = `${model}-fast`
|
|
63
|
+
|
|
64
|
+
if (providerModels.includes(fastId)) {
|
|
65
|
+
return { kind: 'variant', baseId: model, fastId, on: false }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Fast isn't natively offered here, but if the session still has the speed
|
|
69
|
+
// param on (carried over from a previous model), expose the toggle so it can
|
|
70
|
+
// be turned off rather than stranded.
|
|
71
|
+
if (currentFastMode) {
|
|
72
|
+
return { kind: 'param', on: true }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { kind: 'none' }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface ModelEditSubmenuProps {
|
|
79
|
+
/** How fast mode is offered for this model (param toggle vs. variant swap). */
|
|
80
|
+
fastControl: FastControl
|
|
81
|
+
/** Whether this row's model is the active one. */
|
|
82
|
+
isActive: boolean
|
|
83
|
+
/** Switch to this model (resolves false on failure). Awaited before applying
|
|
84
|
+
* edits when not active so a failed switch doesn't write to the old model. */
|
|
85
|
+
onActivate: () => Promise<boolean> | void
|
|
86
|
+
/** Switch to a specific model id (used to swap base ⇄ -fast variant). */
|
|
87
|
+
onSelectModel: (model: string) => Promise<boolean> | void
|
|
88
|
+
/** Whether this model supports reasoning effort. */
|
|
89
|
+
reasoning: boolean
|
|
90
|
+
requestGateway: <T>(method: string, params?: Record<string, unknown>) => Promise<T>
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function ModelEditSubmenu({
|
|
94
|
+
fastControl,
|
|
95
|
+
isActive,
|
|
96
|
+
onActivate,
|
|
97
|
+
onSelectModel,
|
|
98
|
+
reasoning,
|
|
99
|
+
requestGateway
|
|
100
|
+
}: ModelEditSubmenuProps) {
|
|
101
|
+
const { t } = useI18n()
|
|
102
|
+
const copy = t.shell.modelOptions
|
|
103
|
+
// Reactive session state comes straight from the stores rather than being
|
|
104
|
+
// drilled through the panel, so editing it re-renders only this submenu.
|
|
105
|
+
const activeSessionId = useStore($activeSessionId)
|
|
106
|
+
const currentReasoningEffort = useStore($currentReasoningEffort)
|
|
107
|
+
|
|
108
|
+
const effort = normalizeEffort(currentReasoningEffort)
|
|
109
|
+
const thinkingOn = isThinkingEnabled(currentReasoningEffort)
|
|
110
|
+
|
|
111
|
+
// Reasoning/fast are session-scoped (they apply to the active model), so
|
|
112
|
+
// editing a non-active model first switches to it. Returns false if the
|
|
113
|
+
// switch failed, so callers skip applying to the wrong (previous) model.
|
|
114
|
+
const ensureActive = async (): Promise<boolean> => {
|
|
115
|
+
if (isActive) {
|
|
116
|
+
return true
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return (await onActivate()) !== false
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const patchReasoning = async (next: string, rollback: string) => {
|
|
123
|
+
setCurrentReasoningEffort(next)
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
if (!(await ensureActive())) {
|
|
127
|
+
setCurrentReasoningEffort(rollback)
|
|
128
|
+
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
await requestGateway('config.set', {
|
|
133
|
+
key: 'reasoning',
|
|
134
|
+
session_id: activeSessionId ?? '',
|
|
135
|
+
value: next
|
|
136
|
+
})
|
|
137
|
+
} catch (err) {
|
|
138
|
+
setCurrentReasoningEffort(rollback)
|
|
139
|
+
notifyError(err, copy.updateFailed)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const toggleFast = (enabled: boolean) => {
|
|
144
|
+
if (fastControl.kind === 'variant') {
|
|
145
|
+
// Fast is a separate model id — swap to it (or back to the base).
|
|
146
|
+
void onSelectModel(enabled ? fastControl.fastId : fastControl.baseId)
|
|
147
|
+
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (fastControl.kind === 'param') {
|
|
152
|
+
setCurrentFastMode(enabled)
|
|
153
|
+
|
|
154
|
+
void (async () => {
|
|
155
|
+
try {
|
|
156
|
+
if (!(await ensureActive())) {
|
|
157
|
+
setCurrentFastMode(!enabled)
|
|
158
|
+
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
await requestGateway('config.set', {
|
|
163
|
+
key: 'fast',
|
|
164
|
+
session_id: activeSessionId ?? '',
|
|
165
|
+
value: enabled ? 'fast' : 'normal'
|
|
166
|
+
})
|
|
167
|
+
} catch (err) {
|
|
168
|
+
setCurrentFastMode(!enabled)
|
|
169
|
+
notifyError(err, copy.fastFailed)
|
|
170
|
+
}
|
|
171
|
+
})()
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const hasFast = fastControl.kind !== 'none'
|
|
176
|
+
const fastOn = fastControl.kind === 'none' ? false : fastControl.on
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<DropdownMenuSubContent className="w-52 p-0" sideOffset={4}>
|
|
180
|
+
{!hasFast && !reasoning ? (
|
|
181
|
+
<div className="px-2.5 py-3 text-xs text-(--ui-text-tertiary)">{copy.noOptions}</div>
|
|
182
|
+
) : (
|
|
183
|
+
<>
|
|
184
|
+
<DropdownMenuLabel className={dropdownMenuSectionLabel}>{copy.options}</DropdownMenuLabel>
|
|
185
|
+
{reasoning ? (
|
|
186
|
+
<DropdownMenuItem className={dropdownMenuRow} onSelect={event => event.preventDefault()}>
|
|
187
|
+
{copy.thinking}
|
|
188
|
+
<Switch
|
|
189
|
+
checked={thinkingOn}
|
|
190
|
+
className="ml-auto"
|
|
191
|
+
onCheckedChange={checked =>
|
|
192
|
+
void patchReasoning(checked ? effort || 'medium' : 'none', currentReasoningEffort)
|
|
193
|
+
}
|
|
194
|
+
size="xs"
|
|
195
|
+
/>
|
|
196
|
+
</DropdownMenuItem>
|
|
197
|
+
) : null}
|
|
198
|
+
{hasFast ? (
|
|
199
|
+
<DropdownMenuItem className={dropdownMenuRow} onSelect={event => event.preventDefault()}>
|
|
200
|
+
{copy.fast}
|
|
201
|
+
<Switch checked={fastOn} className="ml-auto" onCheckedChange={toggleFast} size="xs" />
|
|
202
|
+
</DropdownMenuItem>
|
|
203
|
+
) : null}
|
|
204
|
+
{reasoning ? (
|
|
205
|
+
<>
|
|
206
|
+
<DropdownMenuSeparator className="mx-0" />
|
|
207
|
+
<DropdownMenuLabel className={dropdownMenuSectionLabel}>{copy.effort}</DropdownMenuLabel>
|
|
208
|
+
<DropdownMenuRadioGroup
|
|
209
|
+
onValueChange={value => void patchReasoning(value, currentReasoningEffort)}
|
|
210
|
+
value={effort}
|
|
211
|
+
>
|
|
212
|
+
{EFFORT_OPTIONS.map(option => (
|
|
213
|
+
<DropdownMenuRadioItem
|
|
214
|
+
className={dropdownMenuRow}
|
|
215
|
+
key={option.value}
|
|
216
|
+
onSelect={event => event.preventDefault()}
|
|
217
|
+
value={option.value}
|
|
218
|
+
>
|
|
219
|
+
{copy[option.labelKey]}
|
|
220
|
+
</DropdownMenuRadioItem>
|
|
221
|
+
))}
|
|
222
|
+
</DropdownMenuRadioGroup>
|
|
223
|
+
</>
|
|
224
|
+
) : null}
|
|
225
|
+
</>
|
|
226
|
+
)}
|
|
227
|
+
</DropdownMenuSubContent>
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function isThinkingEnabled(effort: string): boolean {
|
|
232
|
+
// Empty = NasTech default (medium) = on; only an explicit "none" is off.
|
|
233
|
+
return (effort || 'medium').trim().toLowerCase() !== 'none'
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function normalizeEffort(effort: string): string {
|
|
237
|
+
const value = (effort || 'medium').trim().toLowerCase()
|
|
238
|
+
|
|
239
|
+
// Thinking off → no effort selected in the radio group.
|
|
240
|
+
if (value === 'none') {
|
|
241
|
+
return ''
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return EFFORT_OPTIONS.some(option => option.value === value) ? value : 'medium'
|
|
245
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { useStore } from '@nanostores/react'
|
|
2
|
+
import { useQuery } from '@tanstack/react-query'
|
|
3
|
+
import { useMemo, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
import { Codicon } from '@/components/ui/codicon'
|
|
6
|
+
import {
|
|
7
|
+
DropdownMenuGroup,
|
|
8
|
+
DropdownMenuItem,
|
|
9
|
+
DropdownMenuLabel,
|
|
10
|
+
dropdownMenuRow,
|
|
11
|
+
DropdownMenuSearch,
|
|
12
|
+
dropdownMenuSectionLabel,
|
|
13
|
+
DropdownMenuSeparator,
|
|
14
|
+
DropdownMenuSub,
|
|
15
|
+
DropdownMenuSubTrigger
|
|
16
|
+
} from '@/components/ui/dropdown-menu'
|
|
17
|
+
import { Skeleton } from '@/components/ui/skeleton'
|
|
18
|
+
import type { NasTechGateway } from '@/nastech'
|
|
19
|
+
import { getGlobalModelOptions } from '@/nastech'
|
|
20
|
+
import { useI18n } from '@/i18n'
|
|
21
|
+
import { displayModelName, modelDisplayParts, reasoningEffortLabel } from '@/lib/model-status-label'
|
|
22
|
+
import { cn } from '@/lib/utils'
|
|
23
|
+
import {
|
|
24
|
+
$visibleModels,
|
|
25
|
+
collapseModelFamilies,
|
|
26
|
+
DEFAULT_VISIBLE_PER_PROVIDER,
|
|
27
|
+
type ModelFamily,
|
|
28
|
+
modelVisibilityKey,
|
|
29
|
+
setModelVisibilityOpen
|
|
30
|
+
} from '@/store/model-visibility'
|
|
31
|
+
import {
|
|
32
|
+
$activeSessionId,
|
|
33
|
+
$currentFastMode,
|
|
34
|
+
$currentModel,
|
|
35
|
+
$currentProvider,
|
|
36
|
+
$currentReasoningEffort
|
|
37
|
+
} from '@/store/session'
|
|
38
|
+
import type { ModelOptionProvider, ModelOptionsResponse } from '@/types/nastech'
|
|
39
|
+
|
|
40
|
+
import { ModelEditSubmenu, resolveFastControl } from './model-edit-submenu'
|
|
41
|
+
|
|
42
|
+
interface ModelMenuPanelProps {
|
|
43
|
+
gateway?: NasTechGateway
|
|
44
|
+
onSelectModel: (selection: { model: string; persistGlobal: boolean; provider: string }) => Promise<boolean> | void
|
|
45
|
+
requestGateway: <T>(method: string, params?: Record<string, unknown>) => Promise<T>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface ProviderGroup {
|
|
49
|
+
families: ModelFamily[]
|
|
50
|
+
provider: ModelOptionProvider
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function ModelMenuPanel({ gateway, onSelectModel, requestGateway }: ModelMenuPanelProps) {
|
|
54
|
+
const { t } = useI18n()
|
|
55
|
+
const copy = t.shell.modelMenu
|
|
56
|
+
const [search, setSearch] = useState('')
|
|
57
|
+
// Reactive session state is read from the stores here (not drilled in), so
|
|
58
|
+
// toggling effort/fast/model re-renders this panel in place without forcing
|
|
59
|
+
// the parent to rebuild the menu content (which would close the dropdown).
|
|
60
|
+
const activeSessionId = useStore($activeSessionId)
|
|
61
|
+
const currentFastMode = useStore($currentFastMode)
|
|
62
|
+
const currentModel = useStore($currentModel)
|
|
63
|
+
const currentProvider = useStore($currentProvider)
|
|
64
|
+
const currentReasoningEffort = useStore($currentReasoningEffort)
|
|
65
|
+
const visibleModels = useStore($visibleModels)
|
|
66
|
+
|
|
67
|
+
const modelOptions = useQuery({
|
|
68
|
+
queryKey: ['model-options', activeSessionId || 'global'],
|
|
69
|
+
queryFn: (): Promise<ModelOptionsResponse> => {
|
|
70
|
+
if (gateway && activeSessionId) {
|
|
71
|
+
return gateway.request<ModelOptionsResponse>('model.options', { session_id: activeSessionId })
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return getGlobalModelOptions()
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
const optionsModel = String(modelOptions.data?.model ?? currentModel ?? '')
|
|
79
|
+
const optionsProvider = String(modelOptions.data?.provider ?? currentProvider ?? '')
|
|
80
|
+
const loading = modelOptions.isPending && !modelOptions.data
|
|
81
|
+
|
|
82
|
+
const error = modelOptions.error
|
|
83
|
+
? modelOptions.error instanceof Error
|
|
84
|
+
? modelOptions.error.message
|
|
85
|
+
: String(modelOptions.error)
|
|
86
|
+
: null
|
|
87
|
+
|
|
88
|
+
const providers = modelOptions.data?.providers
|
|
89
|
+
|
|
90
|
+
const switchTo = (model: string, provider: string) =>
|
|
91
|
+
onSelectModel({ model, persistGlobal: !activeSessionId, provider })
|
|
92
|
+
|
|
93
|
+
const groups = useMemo(
|
|
94
|
+
() => groupModels(providers ?? [], search, { model: optionsModel, provider: optionsProvider }, visibleModels),
|
|
95
|
+
[providers, search, optionsModel, optionsProvider, visibleModels]
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<>
|
|
100
|
+
<DropdownMenuSearch
|
|
101
|
+
aria-label={copy.search}
|
|
102
|
+
onValueChange={setSearch}
|
|
103
|
+
placeholder={copy.search}
|
|
104
|
+
value={search}
|
|
105
|
+
/>
|
|
106
|
+
|
|
107
|
+
<DropdownMenuSeparator className="mx-0" />
|
|
108
|
+
|
|
109
|
+
{loading ? (
|
|
110
|
+
<DropdownMenuGroup className="py-1">
|
|
111
|
+
{Array.from({ length: 4 }, (_, index) => (
|
|
112
|
+
<DropdownMenuItem
|
|
113
|
+
className={dropdownMenuRow}
|
|
114
|
+
disabled
|
|
115
|
+
key={index}
|
|
116
|
+
onSelect={event => event.preventDefault()}
|
|
117
|
+
>
|
|
118
|
+
<Skeleton className="h-4 w-full" />
|
|
119
|
+
</DropdownMenuItem>
|
|
120
|
+
))}
|
|
121
|
+
</DropdownMenuGroup>
|
|
122
|
+
) : error ? (
|
|
123
|
+
<DropdownMenuItem className={dropdownMenuRow} disabled>
|
|
124
|
+
{error}
|
|
125
|
+
</DropdownMenuItem>
|
|
126
|
+
) : groups.length === 0 ? (
|
|
127
|
+
<DropdownMenuItem className={dropdownMenuRow} disabled>
|
|
128
|
+
{copy.noModels}
|
|
129
|
+
</DropdownMenuItem>
|
|
130
|
+
) : (
|
|
131
|
+
<div className="max-h-80 overflow-y-auto py-0.5">
|
|
132
|
+
{groups.map(group => (
|
|
133
|
+
<DropdownMenuGroup className="py-0.5" key={group.provider.slug}>
|
|
134
|
+
<DropdownMenuLabel className={dropdownMenuSectionLabel}>{group.provider.name}</DropdownMenuLabel>
|
|
135
|
+
{group.families.map(family => {
|
|
136
|
+
// The active id may be the base or its -fast sibling; either
|
|
137
|
+
// way this one family row represents both.
|
|
138
|
+
const activeId =
|
|
139
|
+
group.provider.slug === optionsProvider &&
|
|
140
|
+
(optionsModel === family.id || optionsModel === family.fastId)
|
|
141
|
+
? optionsModel
|
|
142
|
+
: null
|
|
143
|
+
|
|
144
|
+
const isCurrent = activeId !== null
|
|
145
|
+
const name = modelDisplayParts(family.id).name
|
|
146
|
+
// Capabilities are looked up against the active/base id; the
|
|
147
|
+
// -fast variant carries the same param support as its base.
|
|
148
|
+
const caps = group.provider.capabilities?.[family.id]
|
|
149
|
+
|
|
150
|
+
// Single source of truth for the active row's fast state — keeps
|
|
151
|
+
// the row label in lock-step with the submenu's Fast toggle and
|
|
152
|
+
// handles the standalone `-fast` id case.
|
|
153
|
+
const fastControl = resolveFastControl(
|
|
154
|
+
activeId ?? family.id,
|
|
155
|
+
group.provider.models ?? [],
|
|
156
|
+
caps?.fast ?? false,
|
|
157
|
+
currentFastMode
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
// Grayed text: active row shows live state (Fast + effort);
|
|
161
|
+
// others show a fast-capability hint.
|
|
162
|
+
const meta = isCurrent
|
|
163
|
+
? [
|
|
164
|
+
fastControl.kind !== 'none' && fastControl.on ? copy.fast : null,
|
|
165
|
+
reasoningEffortLabel(currentReasoningEffort) || copy.medium
|
|
166
|
+
]
|
|
167
|
+
.filter(Boolean)
|
|
168
|
+
.join(' ')
|
|
169
|
+
: caps?.fast || family.fastId
|
|
170
|
+
? copy.fast
|
|
171
|
+
: ''
|
|
172
|
+
|
|
173
|
+
// Every row is a hover-Edit submenu trigger. Activating it
|
|
174
|
+
// (pointer or keyboard) switches to the family's base model;
|
|
175
|
+
// the Fast toggle inside swaps to the -fast sibling (or flips
|
|
176
|
+
// the speed param). The sub-trigger has no `onSelect`, so wire
|
|
177
|
+
// both click and Enter/Space for keyboard parity.
|
|
178
|
+
const activate = () => {
|
|
179
|
+
if (!isCurrent) {
|
|
180
|
+
void switchTo(family.id, group.provider.slug)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<DropdownMenuSub key={`${group.provider.slug}:${family.id}`}>
|
|
186
|
+
<DropdownMenuSubTrigger
|
|
187
|
+
className={dropdownMenuRow}
|
|
188
|
+
hideChevron
|
|
189
|
+
onClick={activate}
|
|
190
|
+
onKeyDown={event => {
|
|
191
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
192
|
+
activate()
|
|
193
|
+
}
|
|
194
|
+
}}
|
|
195
|
+
>
|
|
196
|
+
<span className="min-w-0 flex-1 truncate">
|
|
197
|
+
{name}
|
|
198
|
+
{meta ? <span className="text-(--ui-text-tertiary)"> {meta}</span> : null}
|
|
199
|
+
</span>
|
|
200
|
+
{isCurrent ? <Codicon className="ml-auto text-foreground" name="check" size="0.75rem" /> : null}
|
|
201
|
+
</DropdownMenuSubTrigger>
|
|
202
|
+
<ModelEditSubmenu
|
|
203
|
+
fastControl={fastControl}
|
|
204
|
+
isActive={isCurrent}
|
|
205
|
+
onActivate={() => switchTo(family.id, group.provider.slug)}
|
|
206
|
+
onSelectModel={nextModel => switchTo(nextModel, group.provider.slug)}
|
|
207
|
+
reasoning={caps?.reasoning ?? true}
|
|
208
|
+
requestGateway={requestGateway}
|
|
209
|
+
/>
|
|
210
|
+
</DropdownMenuSub>
|
|
211
|
+
)
|
|
212
|
+
})}
|
|
213
|
+
</DropdownMenuGroup>
|
|
214
|
+
))}
|
|
215
|
+
</div>
|
|
216
|
+
)}
|
|
217
|
+
|
|
218
|
+
<DropdownMenuSeparator className="mx-0" />
|
|
219
|
+
|
|
220
|
+
<DropdownMenuItem
|
|
221
|
+
className={cn(dropdownMenuRow, 'text-(--ui-text-tertiary)')}
|
|
222
|
+
onSelect={() => setModelVisibilityOpen(true)}
|
|
223
|
+
>
|
|
224
|
+
{copy.editModels}
|
|
225
|
+
</DropdownMenuItem>
|
|
226
|
+
</>
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Collapsed we show the user's chosen models (or the curated default); typing
|
|
231
|
+
// spans every available model so anything is reachable past the cut.
|
|
232
|
+
const PER_PROVIDER_SEARCH = 12
|
|
233
|
+
|
|
234
|
+
function groupModels(
|
|
235
|
+
providers: ModelOptionProvider[],
|
|
236
|
+
search: string,
|
|
237
|
+
current: { model: string; provider: string },
|
|
238
|
+
visible: Set<string> | null
|
|
239
|
+
): ProviderGroup[] {
|
|
240
|
+
const q = search.trim().toLowerCase()
|
|
241
|
+
const groups: ProviderGroup[] = []
|
|
242
|
+
|
|
243
|
+
for (const provider of providers) {
|
|
244
|
+
const allFamilies = collapseModelFamilies(provider.models ?? [])
|
|
245
|
+
|
|
246
|
+
if (allFamilies.length === 0) {
|
|
247
|
+
continue
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const matches = (family: ModelFamily) =>
|
|
251
|
+
`${family.id} ${family.fastId ?? ''} ${provider.name} ${provider.slug} ${displayModelName(family.id)}`
|
|
252
|
+
.toLowerCase()
|
|
253
|
+
.includes(q)
|
|
254
|
+
|
|
255
|
+
// Which model ids to show (the active one is always added on top of this).
|
|
256
|
+
let shown: Set<string>
|
|
257
|
+
|
|
258
|
+
if (q) {
|
|
259
|
+
// Search spans every family, regardless of visibility.
|
|
260
|
+
shown = new Set(allFamilies.filter(matches).map(family => family.id))
|
|
261
|
+
} else if (visible) {
|
|
262
|
+
// User has customized which models show — honor their selection exactly.
|
|
263
|
+
shown = new Set(
|
|
264
|
+
allFamilies.filter(family => visible.has(modelVisibilityKey(provider.slug, family.id))).map(family => family.id)
|
|
265
|
+
)
|
|
266
|
+
} else {
|
|
267
|
+
// Default: curated top-N families per provider.
|
|
268
|
+
shown = new Set(allFamilies.slice(0, DEFAULT_VISIBLE_PER_PROVIDER).map(family => family.id))
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Always include the active model — but keep every row in the provider's
|
|
272
|
+
// stable curated order (filter `allFamilies`, never reorder), so selecting
|
|
273
|
+
// a model can't shuffle the list.
|
|
274
|
+
const activeId =
|
|
275
|
+
provider.slug === current.provider && current.model
|
|
276
|
+
? allFamilies.find(family => family.id === current.model || family.fastId === current.model)?.id
|
|
277
|
+
: undefined
|
|
278
|
+
|
|
279
|
+
let families = allFamilies.filter(family => shown.has(family.id) || family.id === activeId)
|
|
280
|
+
|
|
281
|
+
if (q) {
|
|
282
|
+
families = families.slice(0, PER_PROVIDER_SEARCH)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (families.length > 0) {
|
|
286
|
+
groups.push({ families, provider })
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Stable, logical group order: alphabetical by provider name. (The backend
|
|
291
|
+
// floats the current provider first, which would reshuffle on every switch.)
|
|
292
|
+
groups.sort((a, b) => a.provider.name.localeCompare(b.provider.name))
|
|
293
|
+
|
|
294
|
+
return groups
|
|
295
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type * as React from 'react'
|
|
2
|
+
|
|
3
|
+
import { cn } from '@/lib/utils'
|
|
4
|
+
|
|
5
|
+
interface SidebarPanelLabelProps extends React.ComponentProps<'span'> {
|
|
6
|
+
dotClassName?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function SidebarPanelLabel({ children, className, dotClassName, ...props }: SidebarPanelLabelProps) {
|
|
10
|
+
return (
|
|
11
|
+
<span
|
|
12
|
+
className={cn(
|
|
13
|
+
'flex min-w-0 items-center gap-2 pl-2 text-[0.64rem] font-semibold uppercase tracking-[0.16em] text-(--theme-primary)',
|
|
14
|
+
className
|
|
15
|
+
)}
|
|
16
|
+
{...props}
|
|
17
|
+
>
|
|
18
|
+
<span aria-hidden="true" className={cn('dither inline-block size-2 shrink-0 rounded-[1px]', dotClassName)} />
|
|
19
|
+
<span className="min-w-0 truncate leading-none">{children}</span>
|
|
20
|
+
</span>
|
|
21
|
+
)
|
|
22
|
+
}
|