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,1286 @@
|
|
|
1
|
+
import { useStore } from '@nanostores/react'
|
|
2
|
+
import { useQuery } from '@tanstack/react-query'
|
|
3
|
+
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
import { ModelPickerDialog } from '@/components/model-picker'
|
|
6
|
+
import { Button } from '@/components/ui/button'
|
|
7
|
+
import { Codicon } from '@/components/ui/codicon'
|
|
8
|
+
import { ErrorIcon } from '@/components/ui/error-state'
|
|
9
|
+
import { Input } from '@/components/ui/input'
|
|
10
|
+
import { Loader } from '@/components/ui/loader'
|
|
11
|
+
import { getGlobalModelOptions } from '@/nastech'
|
|
12
|
+
import { useI18n } from '@/i18n'
|
|
13
|
+
import {
|
|
14
|
+
Check,
|
|
15
|
+
ChevronDown,
|
|
16
|
+
ChevronLeft,
|
|
17
|
+
ChevronRight,
|
|
18
|
+
ExternalLink,
|
|
19
|
+
KeyRound,
|
|
20
|
+
Loader2,
|
|
21
|
+
Terminal
|
|
22
|
+
} from '@/lib/icons'
|
|
23
|
+
import { isProviderSetupErrorMessage } from '@/lib/provider-setup-errors'
|
|
24
|
+
import { cn } from '@/lib/utils'
|
|
25
|
+
import { $desktopBoot, type DesktopBootState } from '@/store/boot'
|
|
26
|
+
import {
|
|
27
|
+
$desktopOnboarding,
|
|
28
|
+
cancelOnboardingFlow,
|
|
29
|
+
clearPendingProviderOAuth,
|
|
30
|
+
closeManualOnboarding,
|
|
31
|
+
confirmOnboardingModel,
|
|
32
|
+
copyDeviceCode,
|
|
33
|
+
copyExternalCommand,
|
|
34
|
+
DEFAULT_MANUAL_ONBOARDING_REASON,
|
|
35
|
+
DEFAULT_ONBOARDING_REASON,
|
|
36
|
+
dismissFirstRunOnboarding,
|
|
37
|
+
type OnboardingContext,
|
|
38
|
+
type OnboardingFlow,
|
|
39
|
+
peekPendingProviderOAuth,
|
|
40
|
+
recheckExternalSignin,
|
|
41
|
+
refreshOnboarding,
|
|
42
|
+
saveOnboardingApiKey,
|
|
43
|
+
setOnboardingCode,
|
|
44
|
+
setOnboardingMode,
|
|
45
|
+
setOnboardingModel,
|
|
46
|
+
startProviderOAuth,
|
|
47
|
+
submitOnboardingCode
|
|
48
|
+
} from '@/store/onboarding'
|
|
49
|
+
import type { ModelOptionProvider, OAuthProvider } from '@/types/nastech'
|
|
50
|
+
|
|
51
|
+
interface DesktopOnboardingOverlayProps {
|
|
52
|
+
enabled: boolean
|
|
53
|
+
onCompleted?: () => void
|
|
54
|
+
requestGateway: OnboardingContext['requestGateway']
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface ApiKeyOption {
|
|
58
|
+
description?: string
|
|
59
|
+
docsUrl: string
|
|
60
|
+
envKey: string
|
|
61
|
+
id: string
|
|
62
|
+
name: string
|
|
63
|
+
placeholder?: string
|
|
64
|
+
short?: string
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const API_KEY_OPTIONS: ApiKeyOption[] = [
|
|
68
|
+
{
|
|
69
|
+
id: 'openrouter',
|
|
70
|
+
name: 'OpenRouter',
|
|
71
|
+
envKey: 'OPENROUTER_API_KEY',
|
|
72
|
+
docsUrl: 'https://openrouter.ai/keys'
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: 'openai',
|
|
76
|
+
name: 'OpenAI',
|
|
77
|
+
envKey: 'OPENAI_API_KEY',
|
|
78
|
+
docsUrl: 'https://platform.openai.com/api-keys'
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 'gemini',
|
|
82
|
+
name: 'Google Gemini',
|
|
83
|
+
envKey: 'GEMINI_API_KEY',
|
|
84
|
+
docsUrl: 'https://aistudio.google.com/app/apikey'
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: 'xai',
|
|
88
|
+
name: 'xAI Grok',
|
|
89
|
+
envKey: 'XAI_API_KEY',
|
|
90
|
+
docsUrl: 'https://console.x.ai/'
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: 'local',
|
|
94
|
+
name: 'Local / custom endpoint',
|
|
95
|
+
envKey: 'OPENAI_BASE_URL',
|
|
96
|
+
docsUrl: 'https://github.com/nastech-ai/NasTech-Agent#bring-your-own-endpoint',
|
|
97
|
+
placeholder: 'http://127.0.0.1:8000/v1'
|
|
98
|
+
}
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
// Build the FULL API-key provider catalog from the backend model options so the
|
|
102
|
+
// onboarding / Providers key form lists every `api_key` provider `nastech model`
|
|
103
|
+
// knows about — not just the hand-curated five. Curated entries keep their
|
|
104
|
+
// richer copy + placeholders and float to the top (recommended defaults); every
|
|
105
|
+
// other api_key provider is appended with a generic "paste {KEY}" affordance.
|
|
106
|
+
// OAuth / external providers are intentionally excluded here — they go through
|
|
107
|
+
// the OAuth picker / sign-in flow, not a pasted key.
|
|
108
|
+
function useApiKeyCatalog(): ApiKeyOption[] {
|
|
109
|
+
const [rows, setRows] = useState<ModelOptionProvider[]>([])
|
|
110
|
+
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
let cancelled = false
|
|
113
|
+
|
|
114
|
+
// Best-effort — on failure the curated defaults still render. Wrapped in
|
|
115
|
+
// Promise.resolve().then so a synchronous throw (e.g. no desktop bridge in
|
|
116
|
+
// tests) is funneled into the same .catch instead of escaping.
|
|
117
|
+
void Promise.resolve()
|
|
118
|
+
.then(() => getGlobalModelOptions())
|
|
119
|
+
.then(res => {
|
|
120
|
+
if (!cancelled) {
|
|
121
|
+
setRows(res.providers ?? [])
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
.catch(() => {
|
|
125
|
+
// Ignore — fall back to the curated API_KEY_OPTIONS only.
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
return () => {
|
|
129
|
+
cancelled = true
|
|
130
|
+
}
|
|
131
|
+
}, [])
|
|
132
|
+
|
|
133
|
+
return useMemo(() => {
|
|
134
|
+
const curatedByEnv = new Map(API_KEY_OPTIONS.map(o => [o.envKey, o]))
|
|
135
|
+
const derived: ApiKeyOption[] = []
|
|
136
|
+
const seenEnv = new Set<string>(API_KEY_OPTIONS.map(o => o.envKey))
|
|
137
|
+
|
|
138
|
+
for (const row of rows) {
|
|
139
|
+
// Only api_key providers can be activated with a pasted key. Skip OAuth /
|
|
140
|
+
// external / managed flows and anything missing an env var to write to.
|
|
141
|
+
if (row.auth_type && row.auth_type !== 'api_key') {
|
|
142
|
+
continue
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const envKey = row.key_env
|
|
146
|
+
|
|
147
|
+
if (!envKey || seenEnv.has(envKey)) {
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
seenEnv.add(envKey)
|
|
152
|
+
derived.push({
|
|
153
|
+
id: row.slug,
|
|
154
|
+
name: row.name,
|
|
155
|
+
envKey,
|
|
156
|
+
description: `Direct API access to ${row.name}.`,
|
|
157
|
+
docsUrl: ''
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Curated first (recommended order), then the rest alphabetically so the
|
|
162
|
+
// long tail is scannable.
|
|
163
|
+
derived.sort((a, b) => a.name.localeCompare(b.name))
|
|
164
|
+
|
|
165
|
+
return [...API_KEY_OPTIONS.filter(o => curatedByEnv.has(o.envKey)), ...derived]
|
|
166
|
+
}, [rows])
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const PROVIDER_DISPLAY: Record<string, { order: number; title: string }> = {
|
|
170
|
+
nastech: { order: 0, title: 'NasTech Portal' },
|
|
171
|
+
'openai-codex': { order: 1, title: 'OpenAI OAuth (ChatGPT)' },
|
|
172
|
+
'minimax-oauth': { order: 2, title: 'MiniMax' },
|
|
173
|
+
'qwen-oauth': { order: 3, title: 'Qwen Code' },
|
|
174
|
+
'xai-oauth': { order: 4, title: 'xAI Grok' },
|
|
175
|
+
// Both Anthropic entries sit at the bottom: the API-key path first, then
|
|
176
|
+
// the subscription OAuth path (only works with extra usage credits).
|
|
177
|
+
anthropic: { order: 5, title: 'Anthropic API Key' },
|
|
178
|
+
'claude-code': { order: 6, title: 'Anthropic OAuth: Required Extra Usage Credits to Use Subscription' }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const assetPath = (path: string) => `${import.meta.env.BASE_URL}${path.replace(/^\/+/, '')}`
|
|
182
|
+
|
|
183
|
+
const providerTitle = (p: OAuthProvider) => PROVIDER_DISPLAY[p.id]?.title ?? p.name
|
|
184
|
+
const orderOf = (p: OAuthProvider) => PROVIDER_DISPLAY[p.id]?.order ?? 99
|
|
185
|
+
|
|
186
|
+
export const sortProviders = (providers: OAuthProvider[]) =>
|
|
187
|
+
[...providers].sort((a, b) => orderOf(a) - orderOf(b) || a.name.localeCompare(b.name))
|
|
188
|
+
|
|
189
|
+
// Exit choreography, mirroring the gateway "connecting" overlay's timing:
|
|
190
|
+
// text-out (360ms: CONNECTED fades down, rest scrambles+fades) → hold (300ms)
|
|
191
|
+
// → surface-out (520ms, held back by [transition-delay:660ms]). Finalize after.
|
|
192
|
+
const ONBOARDING_EXIT_MS = 1180
|
|
193
|
+
|
|
194
|
+
export function DesktopOnboardingOverlay({ enabled, onCompleted, requestGateway }: DesktopOnboardingOverlayProps) {
|
|
195
|
+
const { t } = useI18n()
|
|
196
|
+
const onboarding = useStore($desktopOnboarding)
|
|
197
|
+
const boot = useStore($desktopBoot)
|
|
198
|
+
const ctxRef = useRef<OnboardingContext>({ requestGateway, onCompleted })
|
|
199
|
+
ctxRef.current = { requestGateway, onCompleted }
|
|
200
|
+
|
|
201
|
+
const ctx = useMemo<OnboardingContext>(
|
|
202
|
+
() => ({
|
|
203
|
+
requestGateway: (...args) => ctxRef.current.requestGateway(...args),
|
|
204
|
+
onCompleted: () => ctxRef.current.onCompleted?.()
|
|
205
|
+
}),
|
|
206
|
+
[]
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
// Cinematic exit on "Begin": dissolve the panel + overlay (revealing the chat
|
|
210
|
+
// behind), THEN finalize so the unmount lands after the fade — mirrors the
|
|
211
|
+
// connecting overlay's exit choreography instead of cutting instantly.
|
|
212
|
+
const [leaving, setLeaving] = useState(false)
|
|
213
|
+
|
|
214
|
+
const finalizeOnboarding = () => {
|
|
215
|
+
if (leaving) {
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const reduce =
|
|
220
|
+
typeof window !== 'undefined' && window.matchMedia?.('(prefers-reduced-motion: reduce)').matches
|
|
221
|
+
|
|
222
|
+
if (reduce) {
|
|
223
|
+
confirmOnboardingModel(ctx)
|
|
224
|
+
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
setLeaving(true)
|
|
229
|
+
window.setTimeout(() => confirmOnboardingModel(ctx), ONBOARDING_EXIT_MS)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
useEffect(() => {
|
|
233
|
+
if (enabled || onboarding.requested) {
|
|
234
|
+
void refreshOnboarding(ctx)
|
|
235
|
+
}
|
|
236
|
+
}, [ctx, enabled, onboarding.requested])
|
|
237
|
+
|
|
238
|
+
// When the Providers settings page asked to connect a specific provider, the
|
|
239
|
+
// store stashed its id. Once the provider list has loaded and we're back at
|
|
240
|
+
// an idle picker, launch that exact OAuth flow so the user lands directly in
|
|
241
|
+
// sign-in instead of the picker they just came from.
|
|
242
|
+
useEffect(() => {
|
|
243
|
+
if (!onboarding.manual || onboarding.providers === null || onboarding.flow.status !== 'idle') {
|
|
244
|
+
return
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const pendingId = peekPendingProviderOAuth()
|
|
248
|
+
|
|
249
|
+
if (!pendingId) {
|
|
250
|
+
return
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const provider = onboarding.providers.find(p => p.id === pendingId)
|
|
254
|
+
|
|
255
|
+
if (provider) {
|
|
256
|
+
// Only clear once we've committed to launching it, so a failed/empty
|
|
257
|
+
// provider fetch doesn't silently drop the hand-off.
|
|
258
|
+
clearPendingProviderOAuth()
|
|
259
|
+
void startProviderOAuth(provider, ctx)
|
|
260
|
+
} else if (onboarding.providers.length > 0) {
|
|
261
|
+
// The list loaded but the id isn't a real provider — drop the stale
|
|
262
|
+
// hand-off. An empty list means the fetch isn't ready yet, so keep it
|
|
263
|
+
// and let a later refresh retry.
|
|
264
|
+
clearPendingProviderOAuth()
|
|
265
|
+
}
|
|
266
|
+
}, [ctx, onboarding.flow.status, onboarding.manual, onboarding.providers])
|
|
267
|
+
|
|
268
|
+
// Mount from frame 1 so we replace the boot overlay seamlessly. The
|
|
269
|
+
// configured field stays null until the runtime check resolves; only then
|
|
270
|
+
// do we know whether to dismiss (true) or surface the picker (false).
|
|
271
|
+
// EXCEPTION: manual mode (user opened the selector from a working app to
|
|
272
|
+
// add/switch a provider) shows the overlay regardless of configured state.
|
|
273
|
+
if (onboarding.configured === true && !onboarding.manual) {
|
|
274
|
+
return null
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// The user chose "I'll choose a provider later" on first run. Stay out of the
|
|
278
|
+
// way on every subsequent launch — they re-enter via Settings → Providers
|
|
279
|
+
// (manual mode), which sets manual=true and bypasses this gate.
|
|
280
|
+
if (onboarding.firstRunSkipped && !onboarding.manual) {
|
|
281
|
+
return null
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const { flow } = onboarding
|
|
285
|
+
// Show the launch reason only when it's a meaningful, caller-supplied prompt —
|
|
286
|
+
// suppress the generic defaults (useless noise) and provider-setup errors
|
|
287
|
+
// (those are surfaced by FlowPanel, not as a banner).
|
|
288
|
+
const rawReason = onboarding.reason?.trim() || null
|
|
289
|
+
|
|
290
|
+
const reason =
|
|
291
|
+
rawReason &&
|
|
292
|
+
!isProviderSetupErrorMessage(rawReason) &&
|
|
293
|
+
rawReason !== DEFAULT_ONBOARDING_REASON &&
|
|
294
|
+
rawReason !== DEFAULT_MANUAL_ONBOARDING_REASON
|
|
295
|
+
? rawReason
|
|
296
|
+
: null
|
|
297
|
+
|
|
298
|
+
// In manual mode the app is already configured, so the flow is "ready"
|
|
299
|
+
// immediately — no runtime gate needed. Otherwise wait for the readiness
|
|
300
|
+
// check (configured === false) before showing the picker.
|
|
301
|
+
const ready = onboarding.manual || (enabled && onboarding.configured === false)
|
|
302
|
+
const showPicker = flow.status === 'idle' || flow.status === 'success'
|
|
303
|
+
// The final "you're in" screen drops the card chrome and floats centered on
|
|
304
|
+
// the surface — same bare, cinematic treatment as the connecting overlay.
|
|
305
|
+
const bare = ready && !showPicker && flow.status === 'confirming_model'
|
|
306
|
+
|
|
307
|
+
return (
|
|
308
|
+
<div
|
|
309
|
+
className={cn(
|
|
310
|
+
'fixed inset-0 z-1300 flex items-center justify-center bg-(--ui-chat-surface-background) p-6 transition-opacity duration-[520ms] ease-out',
|
|
311
|
+
// On the bare confirm screen, hold the surface (text-out + hold) so the
|
|
312
|
+
// per-element exit plays before it dissolves.
|
|
313
|
+
bare && leaving ? '[transition-delay:660ms]' : '',
|
|
314
|
+
leaving ? 'pointer-events-none opacity-0' : 'opacity-100'
|
|
315
|
+
)}
|
|
316
|
+
>
|
|
317
|
+
<div
|
|
318
|
+
className={cn(
|
|
319
|
+
'relative w-full max-w-[45rem] transition-all duration-500 ease-out',
|
|
320
|
+
bare
|
|
321
|
+
? ''
|
|
322
|
+
: 'overflow-hidden rounded-xl border border-(--stroke-nastech) bg-(--ui-chat-bubble-background) shadow-nastech',
|
|
323
|
+
// Bare confirm screen orchestrates its own per-element exit; the
|
|
324
|
+
// carded states use the simple lift/blur dissolve.
|
|
325
|
+
leaving && !bare
|
|
326
|
+
? '-translate-y-1 scale-[0.985] opacity-0 blur-[2px]'
|
|
327
|
+
: 'translate-y-0 scale-100 opacity-100 blur-0'
|
|
328
|
+
)}
|
|
329
|
+
>
|
|
330
|
+
{showPicker || !ready ? <Header /> : null}
|
|
331
|
+
{onboarding.manual ? (
|
|
332
|
+
<Button
|
|
333
|
+
aria-label={t.common.close}
|
|
334
|
+
className="absolute right-3 top-3 z-10 text-(--ui-text-tertiary) hover:bg-(--chrome-action-hover) hover:text-foreground"
|
|
335
|
+
onClick={() => closeManualOnboarding()}
|
|
336
|
+
size="icon-sm"
|
|
337
|
+
variant="ghost"
|
|
338
|
+
>
|
|
339
|
+
<Codicon name="close" size="1rem" />
|
|
340
|
+
</Button>
|
|
341
|
+
) : null}
|
|
342
|
+
<div className="grid gap-3 p-5">
|
|
343
|
+
{reason ? <ReasonNotice reason={reason} /> : null}
|
|
344
|
+
{ready ? (
|
|
345
|
+
showPicker ? (
|
|
346
|
+
<Picker ctx={ctx} />
|
|
347
|
+
) : (
|
|
348
|
+
<FlowPanel ctx={ctx} flow={flow} leaving={leaving} onBegin={finalizeOnboarding} />
|
|
349
|
+
)
|
|
350
|
+
) : (
|
|
351
|
+
<Preparing boot={boot} />
|
|
352
|
+
)}
|
|
353
|
+
</div>
|
|
354
|
+
</div>
|
|
355
|
+
</div>
|
|
356
|
+
)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// The launch reason is a prompt ("why am I seeing this"), not an error. Only
|
|
360
|
+
// rendered for meaningful caller-supplied reasons (defaults are filtered out
|
|
361
|
+
// upstream), so it never shows the generic "no provider configured" noise.
|
|
362
|
+
function ReasonNotice({ reason }: { reason: string }) {
|
|
363
|
+
return (
|
|
364
|
+
<div className="rounded-2xl border border-(--ui-stroke-tertiary) bg-(--ui-bg-tertiary)/40 px-4 py-3 text-sm text-muted-foreground">
|
|
365
|
+
{reason}
|
|
366
|
+
</div>
|
|
367
|
+
)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function Preparing({ boot }: { boot: DesktopBootState }) {
|
|
371
|
+
const { t } = useI18n()
|
|
372
|
+
const progress = Math.max(2, Math.min(100, Math.round(boot.progress)))
|
|
373
|
+
const hasError = Boolean(boot.error)
|
|
374
|
+
const installing = boot.phase.startsWith('runtime.')
|
|
375
|
+
|
|
376
|
+
return (
|
|
377
|
+
<div className="grid gap-3" role="status">
|
|
378
|
+
<p className="text-sm text-muted-foreground">
|
|
379
|
+
{installing ? t.onboarding.preparingInstall : t.onboarding.starting}
|
|
380
|
+
</p>
|
|
381
|
+
<div className="h-2 overflow-hidden rounded-full bg-muted">
|
|
382
|
+
<div
|
|
383
|
+
className={cn(
|
|
384
|
+
'h-full rounded-full bg-primary transition-[width] duration-300 ease-out',
|
|
385
|
+
hasError && 'bg-destructive'
|
|
386
|
+
)}
|
|
387
|
+
style={{ width: `${progress}%` }}
|
|
388
|
+
/>
|
|
389
|
+
</div>
|
|
390
|
+
<div className="flex items-center justify-between gap-3 text-xs text-muted-foreground">
|
|
391
|
+
<span className="truncate">{boot.message}</span>
|
|
392
|
+
<span>{progress}%</span>
|
|
393
|
+
</div>
|
|
394
|
+
{hasError ? <p className="text-xs text-destructive">{boot.error}</p> : null}
|
|
395
|
+
</div>
|
|
396
|
+
)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function Header() {
|
|
400
|
+
const { t } = useI18n()
|
|
401
|
+
|
|
402
|
+
return (
|
|
403
|
+
<div className="bg-(--ui-chat-bubble-background) px-5 pt-5 pb-1">
|
|
404
|
+
<h2 className="text-[0.9375rem] font-semibold tracking-tight">{t.onboarding.headerTitle}</h2>
|
|
405
|
+
<p className="mt-1 max-w-xl text-[0.8125rem] leading-5 text-(--ui-text-tertiary)">{t.onboarding.headerDesc}</p>
|
|
406
|
+
</div>
|
|
407
|
+
)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export const FEATURED_ID = 'nastech'
|
|
411
|
+
const SHOW_ALL_KEY = 'nastech-onboarding-show-all-v1'
|
|
412
|
+
|
|
413
|
+
const readShowAll = () => {
|
|
414
|
+
try {
|
|
415
|
+
return window.localStorage.getItem(SHOW_ALL_KEY) === '1'
|
|
416
|
+
} catch {
|
|
417
|
+
return false
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const persistShowAll = (value: boolean) => {
|
|
422
|
+
try {
|
|
423
|
+
window.localStorage.setItem(SHOW_ALL_KEY, value ? '1' : '0')
|
|
424
|
+
} catch {
|
|
425
|
+
// localStorage unavailable — degrade silently.
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return value
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export function Picker({ ctx }: { ctx: OnboardingContext }) {
|
|
432
|
+
const { t } = useI18n()
|
|
433
|
+
const { manual, mode, providers } = useStore($desktopOnboarding)
|
|
434
|
+
const [showAll, setShowAll] = useState(readShowAll)
|
|
435
|
+
const ordered = useMemo(() => (providers ? sortProviders(providers) : []), [providers])
|
|
436
|
+
const hasOauth = ordered.length > 0
|
|
437
|
+
const apiKeyOptions = useApiKeyCatalog()
|
|
438
|
+
|
|
439
|
+
if (mode === 'apikey' || !hasOauth) {
|
|
440
|
+
return (
|
|
441
|
+
<div className="grid gap-3">
|
|
442
|
+
<ApiKeyForm
|
|
443
|
+
canGoBack={hasOauth}
|
|
444
|
+
onBack={() => setOnboardingMode('oauth')}
|
|
445
|
+
onSave={(envKey, value, name) => saveOnboardingApiKey(envKey, value, name, ctx)}
|
|
446
|
+
options={apiKeyOptions}
|
|
447
|
+
/>
|
|
448
|
+
{manual ? null : (
|
|
449
|
+
<div className="flex justify-center border-t border-(--ui-stroke-tertiary) pt-3">
|
|
450
|
+
<ChooseLaterLink />
|
|
451
|
+
</div>
|
|
452
|
+
)}
|
|
453
|
+
</div>
|
|
454
|
+
)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (providers === null) {
|
|
458
|
+
return <Status>{t.onboarding.lookingUpProviders}</Status>
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const select = (p: OAuthProvider) => void startProviderOAuth(p, ctx)
|
|
462
|
+
const featured = ordered.find(p => p.id === FEATURED_ID) ?? null
|
|
463
|
+
const rest = featured ? ordered.filter(p => p.id !== FEATURED_ID) : ordered
|
|
464
|
+
// Collapse the secondary providers behind a disclosure only when NasTech Portal
|
|
465
|
+
// Portal is present to anchor the choice — otherwise show the full list.
|
|
466
|
+
const collapsible = Boolean(featured) && rest.length > 0
|
|
467
|
+
const showRest = !collapsible || showAll
|
|
468
|
+
|
|
469
|
+
return (
|
|
470
|
+
<div className="grid gap-2">
|
|
471
|
+
<div className="grid max-h-[60dvh] gap-2 overflow-y-auto p-1">
|
|
472
|
+
{featured ? <FeaturedProviderRow onSelect={select} provider={featured} /> : null}
|
|
473
|
+
{showRest ? (
|
|
474
|
+
<>
|
|
475
|
+
{rest.map(p => (
|
|
476
|
+
<ProviderRow key={p.id} onSelect={select} provider={p} />
|
|
477
|
+
))}
|
|
478
|
+
<KeyProviderRow onClick={() => setOnboardingMode('apikey')} />
|
|
479
|
+
</>
|
|
480
|
+
) : null}
|
|
481
|
+
</div>
|
|
482
|
+
{collapsible ? (
|
|
483
|
+
<Button
|
|
484
|
+
className="mt-1 self-center font-medium"
|
|
485
|
+
onClick={() => setShowAll(persistShowAll(!showAll))}
|
|
486
|
+
size="xs"
|
|
487
|
+
type="button"
|
|
488
|
+
variant="text"
|
|
489
|
+
>
|
|
490
|
+
{showAll ? t.onboarding.collapse : t.onboarding.otherProviders}
|
|
491
|
+
<ChevronDown className={cn('size-3.5 transition', showAll && 'rotate-180')} />
|
|
492
|
+
</Button>
|
|
493
|
+
) : null}
|
|
494
|
+
<div className="flex items-center justify-between gap-3 pt-1">
|
|
495
|
+
{/* First run only: let the user defer the choice and land in the app.
|
|
496
|
+
In manual mode the overlay already has a close affordance, so the
|
|
497
|
+
"choose later" escape would be redundant — hide it. */}
|
|
498
|
+
{manual ? <span /> : <ChooseLaterLink />}
|
|
499
|
+
<Button
|
|
500
|
+
className="-mr-2 font-medium"
|
|
501
|
+
onClick={() => setOnboardingMode('apikey')}
|
|
502
|
+
size="xs"
|
|
503
|
+
type="button"
|
|
504
|
+
variant="text"
|
|
505
|
+
>
|
|
506
|
+
{t.onboarding.haveApiKey}
|
|
507
|
+
</Button>
|
|
508
|
+
</div>
|
|
509
|
+
</div>
|
|
510
|
+
)
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// "I'll choose a provider later" — dismisses the first-run picker and persists
|
|
514
|
+
// the skip so it never re-nags. The user connects a provider any time from
|
|
515
|
+
// Settings → Providers. Rendered only on the unconfigured first-run flow.
|
|
516
|
+
function ChooseLaterLink() {
|
|
517
|
+
const { t } = useI18n()
|
|
518
|
+
|
|
519
|
+
return (
|
|
520
|
+
<Button
|
|
521
|
+
className="font-medium"
|
|
522
|
+
onClick={() => dismissFirstRunOnboarding()}
|
|
523
|
+
size="xs"
|
|
524
|
+
type="button"
|
|
525
|
+
variant="text"
|
|
526
|
+
>
|
|
527
|
+
{t.onboarding.chooseLater}
|
|
528
|
+
</Button>
|
|
529
|
+
)
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
export function FeaturedProviderRow({
|
|
533
|
+
onSelect,
|
|
534
|
+
provider
|
|
535
|
+
}: {
|
|
536
|
+
onSelect: (provider: OAuthProvider) => void
|
|
537
|
+
provider: OAuthProvider
|
|
538
|
+
}) {
|
|
539
|
+
const { t } = useI18n()
|
|
540
|
+
const loggedIn = provider.status?.logged_in
|
|
541
|
+
|
|
542
|
+
return (
|
|
543
|
+
<button
|
|
544
|
+
className="group relative flex w-full items-center justify-between gap-4 rounded-[8px] bg-primary/[0.06] px-3 py-2.5 text-left transition-colors hover:bg-primary/10"
|
|
545
|
+
onClick={() => onSelect(provider)}
|
|
546
|
+
type="button"
|
|
547
|
+
>
|
|
548
|
+
<span aria-hidden className="arc-border arc-reverse arc-nastech" />
|
|
549
|
+
<div className="min-w-0">
|
|
550
|
+
<div className="flex items-center gap-2">
|
|
551
|
+
<img alt="" className="size-5 shrink-0 rounded" src={assetPath('apple-touch-icon.png')} />
|
|
552
|
+
<span className="text-[length:var(--conversation-text-font-size)] font-semibold">
|
|
553
|
+
{providerTitle(provider)}
|
|
554
|
+
</span>
|
|
555
|
+
{loggedIn ? (
|
|
556
|
+
<ConnectedTag />
|
|
557
|
+
) : (
|
|
558
|
+
<span className="inline-flex items-center gap-1.5 bg-primary px-2 py-0.5 text-[0.64rem] font-semibold uppercase tracking-[0.16em] text-primary-foreground">
|
|
559
|
+
<span aria-hidden="true" className="dither inline-block size-2 shrink-0" />
|
|
560
|
+
{t.onboarding.recommended}
|
|
561
|
+
</span>
|
|
562
|
+
)}
|
|
563
|
+
</div>
|
|
564
|
+
<p className="mt-1 text-xs leading-5 text-muted-foreground">{t.onboarding.featuredPitch}</p>
|
|
565
|
+
</div>
|
|
566
|
+
<ChevronRight className="size-4 shrink-0 text-primary transition group-hover:translate-x-0.5" />
|
|
567
|
+
</button>
|
|
568
|
+
)
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function ConnectedTag() {
|
|
572
|
+
const { t } = useI18n()
|
|
573
|
+
|
|
574
|
+
return (
|
|
575
|
+
<span className="inline-flex items-center gap-1 bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary">
|
|
576
|
+
<Check className="size-3" />
|
|
577
|
+
{t.onboarding.connected}
|
|
578
|
+
</span>
|
|
579
|
+
)
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const PROVIDER_ROW_CLASS =
|
|
583
|
+
'group flex w-full items-center justify-between gap-3 rounded-[6px] px-3 py-2.5 text-left transition-colors hover:bg-(--ui-control-hover-background)'
|
|
584
|
+
|
|
585
|
+
export function KeyProviderRow({ onClick }: { onClick: () => void }) {
|
|
586
|
+
const { t } = useI18n()
|
|
587
|
+
|
|
588
|
+
return (
|
|
589
|
+
<button className={PROVIDER_ROW_CLASS} onClick={onClick} type="button">
|
|
590
|
+
<div className="min-w-0">
|
|
591
|
+
<span className="text-[length:var(--conversation-text-font-size)] font-semibold">OpenRouter</span>
|
|
592
|
+
<p className="mt-1 text-xs leading-5 text-muted-foreground">{t.onboarding.openRouterPitch}</p>
|
|
593
|
+
</div>
|
|
594
|
+
<ChevronRight className="size-4 text-muted-foreground transition group-hover:text-foreground" />
|
|
595
|
+
</button>
|
|
596
|
+
)
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
export function ProviderRow({
|
|
600
|
+
onSelect,
|
|
601
|
+
provider
|
|
602
|
+
}: {
|
|
603
|
+
onSelect: (provider: OAuthProvider) => void
|
|
604
|
+
provider: OAuthProvider
|
|
605
|
+
}) {
|
|
606
|
+
const { t } = useI18n()
|
|
607
|
+
const loggedIn = provider.status?.logged_in
|
|
608
|
+
const Trail = provider.flow === 'external' ? Terminal : ChevronRight
|
|
609
|
+
|
|
610
|
+
return (
|
|
611
|
+
<button className={PROVIDER_ROW_CLASS} onClick={() => onSelect(provider)} type="button">
|
|
612
|
+
<div className="min-w-0">
|
|
613
|
+
<div className="flex items-center gap-2">
|
|
614
|
+
<span className="text-[length:var(--conversation-text-font-size)] font-semibold">
|
|
615
|
+
{providerTitle(provider)}
|
|
616
|
+
</span>
|
|
617
|
+
{loggedIn ? <ConnectedTag /> : null}
|
|
618
|
+
</div>
|
|
619
|
+
<p className="mt-1 text-xs leading-5 text-muted-foreground">{t.onboarding.flowSubtitles[provider.flow]}</p>
|
|
620
|
+
</div>
|
|
621
|
+
<Trail className="size-4 text-muted-foreground transition group-hover:text-foreground" />
|
|
622
|
+
</button>
|
|
623
|
+
)
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Presentational two-column key picker. Onboarding feeds it its curated
|
|
627
|
+
// options + a ctx-bound save; the Providers settings page feeds it the full
|
|
628
|
+
// provider catalog + a setEnvVar-backed save (plus `isSet`/`onClear` so it can
|
|
629
|
+
// double as a manage surface). Keep it free of store/ctx coupling so both
|
|
630
|
+
// surfaces render the identical form.
|
|
631
|
+
export function ApiKeyForm({
|
|
632
|
+
canGoBack,
|
|
633
|
+
isSet,
|
|
634
|
+
onBack,
|
|
635
|
+
onClear,
|
|
636
|
+
onSave,
|
|
637
|
+
options = API_KEY_OPTIONS,
|
|
638
|
+
redactedValue
|
|
639
|
+
}: {
|
|
640
|
+
canGoBack: boolean
|
|
641
|
+
isSet?: (envKey: string) => boolean
|
|
642
|
+
onBack: () => void
|
|
643
|
+
onClear?: (envKey: string) => void
|
|
644
|
+
onSave: (envKey: string, value: string, name: string) => Promise<{ message?: string; ok: boolean }>
|
|
645
|
+
options?: ApiKeyOption[]
|
|
646
|
+
redactedValue?: (envKey: string) => null | string | undefined
|
|
647
|
+
}) {
|
|
648
|
+
const { t } = useI18n()
|
|
649
|
+
const [option, setOption] = useState<ApiKeyOption>(options[0])
|
|
650
|
+
const [value, setValue] = useState('')
|
|
651
|
+
const [saving, setSaving] = useState(false)
|
|
652
|
+
const [error, setError] = useState<null | string>(null)
|
|
653
|
+
// `options` can change at runtime when callers filter the catalog (e.g. the
|
|
654
|
+
// Providers page wiring its search into this grid). Keep the selection valid
|
|
655
|
+
// by snapping back to the first remaining option when the current one drops.
|
|
656
|
+
useEffect(() => {
|
|
657
|
+
if (options.length > 0 && !options.some(o => o.envKey === option.envKey)) {
|
|
658
|
+
setOption(options[0])
|
|
659
|
+
setValue('')
|
|
660
|
+
setError(null)
|
|
661
|
+
}
|
|
662
|
+
}, [option.envKey, options])
|
|
663
|
+
// The catalog grid can be tall, leaving the entry field far below the fold.
|
|
664
|
+
// On selection we scroll the field into view and focus it so it's always
|
|
665
|
+
// obvious where to paste next.
|
|
666
|
+
const entryRef = useRef<HTMLDivElement>(null)
|
|
667
|
+
|
|
668
|
+
const pick = (o: ApiKeyOption) => {
|
|
669
|
+
setOption(o)
|
|
670
|
+
setValue('')
|
|
671
|
+
setError(null)
|
|
672
|
+
requestAnimationFrame(() => {
|
|
673
|
+
entryRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
674
|
+
entryRef.current?.querySelector('input')?.focus()
|
|
675
|
+
})
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const isLocal = option.envKey === 'OPENAI_BASE_URL'
|
|
679
|
+
const alreadySet = isSet?.(option.envKey) ?? false
|
|
680
|
+
// When set, surface the backend's redacted value (e.g. "sk-12…wxyz") as the
|
|
681
|
+
// placeholder so users can eyeball that the right key is in place.
|
|
682
|
+
const currentRedacted = alreadySet ? (redactedValue?.(option.envKey) ?? null) : null
|
|
683
|
+
// Only require a non-empty value — no length/format validation, so a short
|
|
684
|
+
// or unusual key can't block the user from continuing.
|
|
685
|
+
const canSave = value.trim().length >= 1
|
|
686
|
+
const optionCopy = t.onboarding.apiKeyOptions[option.id]
|
|
687
|
+
const optionDescription = optionCopy?.description ?? option.description
|
|
688
|
+
|
|
689
|
+
const submit = async () => {
|
|
690
|
+
if (!canSave || saving) {
|
|
691
|
+
return
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
setSaving(true)
|
|
695
|
+
setError(null)
|
|
696
|
+
const result = await onSave(option.envKey, value, option.name)
|
|
697
|
+
|
|
698
|
+
if (result.ok) {
|
|
699
|
+
setValue('')
|
|
700
|
+
} else {
|
|
701
|
+
setError(result.message ?? t.onboarding.couldNotSave)
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
setSaving(false)
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
return (
|
|
708
|
+
<div className="grid gap-4">
|
|
709
|
+
{canGoBack ? (
|
|
710
|
+
<Button
|
|
711
|
+
className="-mt-1 self-start font-medium"
|
|
712
|
+
onClick={onBack}
|
|
713
|
+
size="xs"
|
|
714
|
+
type="button"
|
|
715
|
+
variant="text"
|
|
716
|
+
>
|
|
717
|
+
<ChevronLeft className="size-3" />
|
|
718
|
+
{t.onboarding.backToSignIn}
|
|
719
|
+
</Button>
|
|
720
|
+
) : null}
|
|
721
|
+
|
|
722
|
+
<div className="grid max-h-[42dvh] gap-2 overflow-y-auto p-1 sm:grid-cols-2">
|
|
723
|
+
{options.map(o => (
|
|
724
|
+
<button
|
|
725
|
+
className={cn(
|
|
726
|
+
'rounded-2xl border bg-background/60 p-3 text-left transition hover:bg-accent/50',
|
|
727
|
+
option.envKey === o.envKey ? 'border-primary ring-2 ring-primary/20' : 'border-transparent'
|
|
728
|
+
)}
|
|
729
|
+
key={o.envKey}
|
|
730
|
+
onClick={() => pick(o)}
|
|
731
|
+
type="button"
|
|
732
|
+
>
|
|
733
|
+
<div className="flex items-center justify-between gap-2">
|
|
734
|
+
<span className="text-sm font-medium">{o.name}</span>
|
|
735
|
+
{isSet?.(o.envKey) ? <Check className="size-3.5 text-muted-foreground" /> : null}
|
|
736
|
+
</div>
|
|
737
|
+
{(t.onboarding.apiKeyOptions[o.id]?.short ?? o.short) ? (
|
|
738
|
+
<p className="mt-1 text-xs text-muted-foreground">{t.onboarding.apiKeyOptions[o.id]?.short ?? o.short}</p>
|
|
739
|
+
) : null}
|
|
740
|
+
</button>
|
|
741
|
+
))}
|
|
742
|
+
</div>
|
|
743
|
+
|
|
744
|
+
<div className="grid scroll-mt-4 gap-2" ref={entryRef}>
|
|
745
|
+
<div className="flex items-center justify-between gap-3">
|
|
746
|
+
<p className="text-sm leading-6 text-muted-foreground">{optionDescription}</p>
|
|
747
|
+
{option.docsUrl ? <DocsLink href={option.docsUrl}>{t.onboarding.getKey}</DocsLink> : null}
|
|
748
|
+
</div>
|
|
749
|
+
<Input
|
|
750
|
+
autoComplete="off"
|
|
751
|
+
autoFocus
|
|
752
|
+
className="font-mono"
|
|
753
|
+
onChange={e => setValue(e.target.value)}
|
|
754
|
+
onKeyDown={e => e.key === 'Enter' && void submit()}
|
|
755
|
+
placeholder={
|
|
756
|
+
currentRedacted ??
|
|
757
|
+
(alreadySet ? t.onboarding.replaceCurrent : option.placeholder || t.onboarding.pasteApiKey)
|
|
758
|
+
}
|
|
759
|
+
type={isLocal ? 'text' : 'password'}
|
|
760
|
+
value={value}
|
|
761
|
+
/>
|
|
762
|
+
{error ? <p className="text-xs text-destructive">{error}</p> : null}
|
|
763
|
+
</div>
|
|
764
|
+
|
|
765
|
+
<div className="flex items-center justify-between gap-3">
|
|
766
|
+
<div>
|
|
767
|
+
{alreadySet && onClear ? (
|
|
768
|
+
<Button onClick={() => onClear(option.envKey)} size="sm" variant="ghost">
|
|
769
|
+
{t.common.remove}
|
|
770
|
+
</Button>
|
|
771
|
+
) : null}
|
|
772
|
+
</div>
|
|
773
|
+
<Button disabled={!canSave || saving} onClick={() => void submit()}>
|
|
774
|
+
{saving ? <Loader2 className="animate-spin" /> : <KeyRound />}
|
|
775
|
+
{saving ? t.onboarding.connecting : alreadySet ? t.onboarding.update : t.common.connect}
|
|
776
|
+
</Button>
|
|
777
|
+
</div>
|
|
778
|
+
</div>
|
|
779
|
+
)
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function FlowPanel({
|
|
783
|
+
ctx,
|
|
784
|
+
flow,
|
|
785
|
+
leaving,
|
|
786
|
+
onBegin
|
|
787
|
+
}: {
|
|
788
|
+
ctx: OnboardingContext
|
|
789
|
+
flow: OnboardingFlow
|
|
790
|
+
leaving: boolean
|
|
791
|
+
onBegin: () => void
|
|
792
|
+
}) {
|
|
793
|
+
const { t } = useI18n()
|
|
794
|
+
const title = 'provider' in flow && flow.provider ? providerTitle(flow.provider) : ''
|
|
795
|
+
|
|
796
|
+
if (flow.status === 'starting') {
|
|
797
|
+
return <Status>{t.onboarding.startingSignIn(title)}</Status>
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
if (flow.status === 'submitting') {
|
|
801
|
+
return <Status>{t.onboarding.verifyingCode(title)}</Status>
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
if (flow.status === 'success') {
|
|
805
|
+
return (
|
|
806
|
+
<DecodedLabel text={t.onboarding.connectedPicking(title)} />
|
|
807
|
+
)
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
if (flow.status === 'confirming_model') {
|
|
811
|
+
return <ConfirmingModelPanel flow={flow} leaving={leaving} onBegin={onBegin} />
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
if (flow.status === 'error') {
|
|
815
|
+
return (
|
|
816
|
+
<div className="grid gap-3">
|
|
817
|
+
<div className="flex items-center gap-1.5 text-sm text-destructive">
|
|
818
|
+
<ErrorIcon className="shrink-0" size="0.875rem" />
|
|
819
|
+
<span>{flow.message || t.onboarding.signInFailed}</span>
|
|
820
|
+
</div>
|
|
821
|
+
<div className="flex justify-end">
|
|
822
|
+
<Button onClick={cancelOnboardingFlow} variant="outline">
|
|
823
|
+
{t.onboarding.pickDifferentProvider}
|
|
824
|
+
</Button>
|
|
825
|
+
</div>
|
|
826
|
+
</div>
|
|
827
|
+
)
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
if (flow.status === 'awaiting_user') {
|
|
831
|
+
return (
|
|
832
|
+
<Step title={t.onboarding.signInWith(title)}>
|
|
833
|
+
<ol className="list-decimal space-y-1 pl-5 text-sm text-muted-foreground">
|
|
834
|
+
<li>{t.onboarding.openedBrowser(title)}</li>
|
|
835
|
+
<li>{t.onboarding.authorizeThere}</li>
|
|
836
|
+
<li>{t.onboarding.copyAuthCode}</li>
|
|
837
|
+
</ol>
|
|
838
|
+
<Input
|
|
839
|
+
autoFocus
|
|
840
|
+
onChange={e => setOnboardingCode(e.target.value)}
|
|
841
|
+
onKeyDown={e => e.key === 'Enter' && void submitOnboardingCode(ctx)}
|
|
842
|
+
placeholder={t.onboarding.pasteAuthCode}
|
|
843
|
+
value={flow.code}
|
|
844
|
+
/>
|
|
845
|
+
<FlowFooter left={<DocsLink href={flow.start.auth_url}>{t.onboarding.reopenAuthPage}</DocsLink>}>
|
|
846
|
+
<CancelBtn />
|
|
847
|
+
<Button disabled={!flow.code.trim()} onClick={() => void submitOnboardingCode(ctx)}>
|
|
848
|
+
{t.common.continue}
|
|
849
|
+
</Button>
|
|
850
|
+
</FlowFooter>
|
|
851
|
+
</Step>
|
|
852
|
+
)
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
if (flow.status === 'awaiting_browser') {
|
|
856
|
+
return (
|
|
857
|
+
<Step title={t.onboarding.signInWith(title)}>
|
|
858
|
+
<p className="text-sm text-muted-foreground">{t.onboarding.autoBrowser(title)}</p>
|
|
859
|
+
<FlowFooter left={<DocsLink href={flow.start.auth_url}>{t.onboarding.reopenSignInPage}</DocsLink>}>
|
|
860
|
+
<span className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
861
|
+
<Loader2 className="size-3 animate-spin" />
|
|
862
|
+
{t.onboarding.waitingAuthorize}
|
|
863
|
+
</span>
|
|
864
|
+
<CancelBtn size="sm" />
|
|
865
|
+
</FlowFooter>
|
|
866
|
+
</Step>
|
|
867
|
+
)
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
if (flow.status === 'external_pending') {
|
|
871
|
+
return (
|
|
872
|
+
<Step title={t.onboarding.signInWith(title)}>
|
|
873
|
+
<p className="text-sm text-muted-foreground">{t.onboarding.externalPending(title)}</p>
|
|
874
|
+
<CodeBlock copied={flow.copied} onCopy={() => void copyExternalCommand()} text={flow.provider.cli_command} />
|
|
875
|
+
<FlowFooter
|
|
876
|
+
left={
|
|
877
|
+
flow.provider.docs_url ? (
|
|
878
|
+
<DocsLink href={flow.provider.docs_url}>{t.onboarding.docs(title)}</DocsLink>
|
|
879
|
+
) : null
|
|
880
|
+
}
|
|
881
|
+
>
|
|
882
|
+
<CancelBtn />
|
|
883
|
+
<Button onClick={() => void recheckExternalSignin(ctx)}>{t.onboarding.signedIn}</Button>
|
|
884
|
+
</FlowFooter>
|
|
885
|
+
</Step>
|
|
886
|
+
)
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
if (flow.status !== 'polling') {
|
|
890
|
+
return null
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
return (
|
|
894
|
+
<Step title={t.onboarding.signInWith(title)}>
|
|
895
|
+
<p className="text-sm text-muted-foreground">{t.onboarding.deviceCodeOpened(title)}</p>
|
|
896
|
+
<DeviceCode code={flow.start.user_code} copied={flow.copied} onCopy={() => void copyDeviceCode()} />
|
|
897
|
+
<FlowFooter left={<DocsLink href={flow.start.verification_url}>{t.onboarding.reopenVerification}</DocsLink>}>
|
|
898
|
+
<span className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
899
|
+
<Loader2 className="size-3 animate-spin" />
|
|
900
|
+
{t.onboarding.waitingAuthorize}
|
|
901
|
+
</span>
|
|
902
|
+
<CancelBtn size="sm" />
|
|
903
|
+
</FlowFooter>
|
|
904
|
+
</Step>
|
|
905
|
+
)
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
function Step({ children, title }: { children: React.ReactNode; title: string }) {
|
|
909
|
+
return (
|
|
910
|
+
<div className="grid gap-4">
|
|
911
|
+
<h3 className="text-sm font-semibold">{title}</h3>
|
|
912
|
+
{children}
|
|
913
|
+
</div>
|
|
914
|
+
)
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// Device-code display: OTP-style — each character in its own readonly cell.
|
|
918
|
+
// The whole row is the copy button (no side button, no checkmark); on copy the
|
|
919
|
+
// cells flash emerald for feedback. Dashes render as quiet separators.
|
|
920
|
+
function DeviceCode({ code, copied, onCopy }: { code: string; copied: boolean; onCopy: () => void }) {
|
|
921
|
+
const { t } = useI18n()
|
|
922
|
+
|
|
923
|
+
return (
|
|
924
|
+
<button
|
|
925
|
+
aria-label={t.onboarding.copy}
|
|
926
|
+
className="group flex w-full items-center justify-center gap-1.5"
|
|
927
|
+
onClick={onCopy}
|
|
928
|
+
type="button"
|
|
929
|
+
>
|
|
930
|
+
{[...code].map((ch, i) =>
|
|
931
|
+
ch === '-' || ch === ' ' ? (
|
|
932
|
+
<span className="w-1.5 text-center text-lg text-muted-foreground" key={i}>
|
|
933
|
+
–
|
|
934
|
+
</span>
|
|
935
|
+
) : (
|
|
936
|
+
<span
|
|
937
|
+
className={cn(
|
|
938
|
+
'flex size-10 items-center justify-center rounded-md border font-mono text-xl font-semibold uppercase transition-colors',
|
|
939
|
+
copied
|
|
940
|
+
? 'border-primary/50 text-primary'
|
|
941
|
+
: 'border-(--stroke-nastech) text-foreground group-hover:border-(--ui-stroke-secondary)'
|
|
942
|
+
)}
|
|
943
|
+
key={i}
|
|
944
|
+
>
|
|
945
|
+
{ch}
|
|
946
|
+
</span>
|
|
947
|
+
)
|
|
948
|
+
)}
|
|
949
|
+
</button>
|
|
950
|
+
)
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function CodeBlock({ copied, onCopy, text }: { copied: boolean; onCopy: () => void; text: string }) {
|
|
954
|
+
const { t } = useI18n()
|
|
955
|
+
|
|
956
|
+
return (
|
|
957
|
+
<div className="flex items-center justify-between gap-3 rounded-md border border-(--stroke-nastech) px-3 py-2">
|
|
958
|
+
<code className="min-w-0 flex-1 truncate font-mono text-sm">
|
|
959
|
+
<span className="mr-2 select-none text-muted-foreground">$</span>
|
|
960
|
+
{text}
|
|
961
|
+
</code>
|
|
962
|
+
<Button onClick={onCopy} size="sm" variant="outline">
|
|
963
|
+
{copied ? t.common.copied : t.onboarding.copy}
|
|
964
|
+
</Button>
|
|
965
|
+
</div>
|
|
966
|
+
)
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function FlowFooter({ children, left }: { children: React.ReactNode; left?: React.ReactNode }) {
|
|
970
|
+
return (
|
|
971
|
+
<div className="flex items-center justify-between gap-3">
|
|
972
|
+
<div className="min-w-0">{left}</div>
|
|
973
|
+
<div className="flex items-center gap-3">{children}</div>
|
|
974
|
+
</div>
|
|
975
|
+
)
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function CancelBtn({ size = 'default' }: { size?: 'default' | 'sm' }) {
|
|
979
|
+
const { t } = useI18n()
|
|
980
|
+
|
|
981
|
+
return (
|
|
982
|
+
<Button onClick={cancelOnboardingFlow} size={size} variant="ghost">
|
|
983
|
+
{t.common.cancel}
|
|
984
|
+
</Button>
|
|
985
|
+
)
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// Borrowed from the gateway "connecting" overlay: a mono, letter-spaced label
|
|
989
|
+
// that decodes left-to-right from scrambled glyphs into the real text, with a
|
|
990
|
+
// blinking block cursor. Ties onboarding's success moment to that same motif.
|
|
991
|
+
// Cuneiform glyphs (array, since each is a surrogate pair) for the scramble.
|
|
992
|
+
// Hero "X CONNECTED" decode uses the SAME ascii map as the connecting overlay.
|
|
993
|
+
const ASCII_GLYPHS = [...'/\\|-_=+<>~:*']
|
|
994
|
+
const pickAscii = () => ASCII_GLYPHS[(Math.random() * ASCII_GLYPHS.length) | 0]
|
|
995
|
+
// Cuneiform is reserved for the subtle "other text" (model name + BEGIN) easter egg.
|
|
996
|
+
const SCRAMBLE_GLYPHS = [...'𒀀𒀁𒀂𒀅𒀊𒀖𒀜𒀭𒀲𒀸𒁀𒁉𒁒𒁕𒁹𒂊𒃻𒄆𒄴𒅀𒆍𒇽𒈨𒉡']
|
|
997
|
+
const GLYPH_SET = new Set(SCRAMBLE_GLYPHS)
|
|
998
|
+
const pickGlyph = () => SCRAMBLE_GLYPHS[(Math.random() * SCRAMBLE_GLYPHS.length) | 0]
|
|
999
|
+
// How many trailing characters of each word scramble during decode-in.
|
|
1000
|
+
const DECODE_TAIL = 4
|
|
1001
|
+
|
|
1002
|
+
// Renders text where cuneiform scramble-glyphs are dropped to a smaller em-size
|
|
1003
|
+
// (resolved Latin chars stay full size) — keeps the easter-egg glyphs subtle.
|
|
1004
|
+
function GlyphText({ text }: { text: string }) {
|
|
1005
|
+
return (
|
|
1006
|
+
<>
|
|
1007
|
+
{Array.from(text, (ch, i) =>
|
|
1008
|
+
GLYPH_SET.has(ch) ? (
|
|
1009
|
+
<span className="text-[0.62em]" key={i}>
|
|
1010
|
+
{ch}
|
|
1011
|
+
</span>
|
|
1012
|
+
) : (
|
|
1013
|
+
ch
|
|
1014
|
+
)
|
|
1015
|
+
)}
|
|
1016
|
+
</>
|
|
1017
|
+
)
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
function useDecoded(text: string): string {
|
|
1021
|
+
const [out, setOut] = useState(text)
|
|
1022
|
+
|
|
1023
|
+
useEffect(() => {
|
|
1024
|
+
if (typeof window !== 'undefined' && window.matchMedia?.('(prefers-reduced-motion: reduce)').matches) {
|
|
1025
|
+
setOut(text)
|
|
1026
|
+
|
|
1027
|
+
return
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// Each WORD keeps its head static and only churns its tail (last few chars),
|
|
1031
|
+
// resolving left-to-right across all tails — same anchor-the-prefix trick the
|
|
1032
|
+
// connecting overlay uses ("CONN" static, "ECTING" churns), applied per word
|
|
1033
|
+
// so both the provider and "CONNECTED" decode and time stays constant.
|
|
1034
|
+
const chars = [...text]
|
|
1035
|
+
const scrambleable = chars.map(() => false)
|
|
1036
|
+
|
|
1037
|
+
for (let i = 0; i < chars.length; ) {
|
|
1038
|
+
if (!/[a-z0-9]/i.test(chars[i])) {
|
|
1039
|
+
i += 1
|
|
1040
|
+
|
|
1041
|
+
continue
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
let j = i
|
|
1045
|
+
|
|
1046
|
+
while (j < chars.length && /[a-z0-9]/i.test(chars[j])) {
|
|
1047
|
+
j += 1
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
for (let k = Math.max(i, j - DECODE_TAIL); k < j; k += 1) {
|
|
1051
|
+
scrambleable[k] = true
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
i = j
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
const tailIndices = chars.map((_, idx) => idx).filter(idx => scrambleable[idx])
|
|
1058
|
+
let resolved = 0
|
|
1059
|
+
|
|
1060
|
+
const id = window.setInterval(() => {
|
|
1061
|
+
resolved += 0.5
|
|
1062
|
+
const settled = new Set(tailIndices.slice(0, Math.floor(resolved)))
|
|
1063
|
+
|
|
1064
|
+
setOut(chars.map((ch, idx) => (scrambleable[idx] && !settled.has(idx) ? pickAscii() : ch)).join(''))
|
|
1065
|
+
|
|
1066
|
+
if (Math.floor(resolved) >= tailIndices.length) {
|
|
1067
|
+
window.clearInterval(id)
|
|
1068
|
+
}
|
|
1069
|
+
}, 45)
|
|
1070
|
+
|
|
1071
|
+
return () => window.clearInterval(id)
|
|
1072
|
+
}, [text])
|
|
1073
|
+
|
|
1074
|
+
return out
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// Continuously scrambles alphanumeric chars while `active` (used on exit so the
|
|
1078
|
+
// model name / button decay into ascii noise as they fade).
|
|
1079
|
+
function useScramble(text: string, active: boolean): string {
|
|
1080
|
+
const [out, setOut] = useState(text)
|
|
1081
|
+
|
|
1082
|
+
useEffect(() => {
|
|
1083
|
+
if (!active) {
|
|
1084
|
+
setOut(text)
|
|
1085
|
+
|
|
1086
|
+
return
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
const id = window.setInterval(() => {
|
|
1090
|
+
setOut(Array.from(text, ch => (/[a-z0-9]/i.test(ch) ? pickGlyph() : ch)).join(''))
|
|
1091
|
+
}, 45)
|
|
1092
|
+
|
|
1093
|
+
return () => window.clearInterval(id)
|
|
1094
|
+
}, [text, active])
|
|
1095
|
+
|
|
1096
|
+
return out
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function DecodedLabel({ leaving, text }: { leaving?: boolean; text: string }) {
|
|
1100
|
+
const decoded = useDecoded(text.toUpperCase())
|
|
1101
|
+
|
|
1102
|
+
return (
|
|
1103
|
+
<span
|
|
1104
|
+
className={cn(
|
|
1105
|
+
'inline-flex items-center font-mono text-xs font-semibold uppercase tracking-[0.28em] tabular-nums text-primary transition duration-[360ms] ease-out',
|
|
1106
|
+
leaving ? 'translate-y-2 opacity-0 saturate-0' : 'translate-y-0 opacity-100 saturate-100'
|
|
1107
|
+
)}
|
|
1108
|
+
>
|
|
1109
|
+
<GlyphText text={decoded} />
|
|
1110
|
+
<span
|
|
1111
|
+
aria-hidden="true"
|
|
1112
|
+
className="dither ml-1.5 -mr-[0.875rem] inline-block size-2 shrink-0 -translate-y-px rounded-[1px] text-primary"
|
|
1113
|
+
style={{ animation: 'ob-decode-cursor 1s step-end infinite' }}
|
|
1114
|
+
/>
|
|
1115
|
+
<style>{'@keyframes ob-decode-cursor { 0%, 49% { opacity: 1 } 50%, 100% { opacity: 0 } }'}</style>
|
|
1116
|
+
</span>
|
|
1117
|
+
)
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// Terminal-flavored CTA to match the connecting overlay's hacker aesthetic:
|
|
1121
|
+
// mono, uppercase, letter-spaced, wrapped in primary brackets that light up on
|
|
1122
|
+
// hover. The whole onboarding "you're in" moment leans into this motif.
|
|
1123
|
+
function HackeryButton({
|
|
1124
|
+
disabled,
|
|
1125
|
+
label,
|
|
1126
|
+
loading,
|
|
1127
|
+
onClick
|
|
1128
|
+
}: {
|
|
1129
|
+
disabled?: boolean
|
|
1130
|
+
label: React.ReactNode
|
|
1131
|
+
loading?: boolean
|
|
1132
|
+
onClick: () => void
|
|
1133
|
+
}) {
|
|
1134
|
+
return (
|
|
1135
|
+
<button
|
|
1136
|
+
className={cn(
|
|
1137
|
+
'group inline-flex items-center gap-2 rounded-md border border-(--stroke-nastech) px-6 py-2.5',
|
|
1138
|
+
'font-mono text-xs font-semibold uppercase text-primary',
|
|
1139
|
+
'transition-all duration-150 hover:border-primary/60 hover:bg-primary/[0.06]',
|
|
1140
|
+
'disabled:pointer-events-none disabled:opacity-50'
|
|
1141
|
+
)}
|
|
1142
|
+
disabled={disabled}
|
|
1143
|
+
onClick={onClick}
|
|
1144
|
+
type="button"
|
|
1145
|
+
>
|
|
1146
|
+
<span className="text-primary/40 transition-colors group-hover:text-primary">[</span>
|
|
1147
|
+
{loading ? <Loader2 className="size-3 animate-spin" /> : null}
|
|
1148
|
+
<span className="-mr-[0.25em] pl-[0.25em] tracking-[0.25em]">{label}</span>
|
|
1149
|
+
<span className="text-primary/40 transition-colors group-hover:text-primary">]</span>
|
|
1150
|
+
</button>
|
|
1151
|
+
)
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
function ConfirmingModelPanel({
|
|
1155
|
+
flow,
|
|
1156
|
+
leaving,
|
|
1157
|
+
onBegin
|
|
1158
|
+
}: {
|
|
1159
|
+
flow: Extract<OnboardingFlow, { status: 'confirming_model' }>
|
|
1160
|
+
leaving: boolean
|
|
1161
|
+
onBegin: () => void
|
|
1162
|
+
}) {
|
|
1163
|
+
const { t } = useI18n()
|
|
1164
|
+
const scrambledModel = useScramble(flow.currentModel, leaving)
|
|
1165
|
+
const scrambledBegin = useScramble(t.onboarding.startChatting, leaving)
|
|
1166
|
+
// Local state controls whether the model picker dialog is open.
|
|
1167
|
+
// We reuse the existing ModelPickerDialog component (the same picker
|
|
1168
|
+
// available from the chat shell) rather than building an inline
|
|
1169
|
+
// dropdown — gives us search, multi-provider listing if relevant, and
|
|
1170
|
+
// a familiar UI for users who'll see this picker again later.
|
|
1171
|
+
const [pickerOpen, setPickerOpen] = useState(false)
|
|
1172
|
+
|
|
1173
|
+
// Pull pricing + tier for the just-picked default so the confirm card
|
|
1174
|
+
// shows the same $/Mtok + Free/Pro info the picker and CLI do.
|
|
1175
|
+
const options = useQuery({
|
|
1176
|
+
queryKey: ['onboarding-model-options', flow.providerSlug],
|
|
1177
|
+
queryFn: () => getGlobalModelOptions()
|
|
1178
|
+
})
|
|
1179
|
+
|
|
1180
|
+
const providerRow = options.data?.providers?.find(
|
|
1181
|
+
p => String(p.slug).toLowerCase() === flow.providerSlug.toLowerCase()
|
|
1182
|
+
)
|
|
1183
|
+
|
|
1184
|
+
const price = providerRow?.pricing?.[flow.currentModel]
|
|
1185
|
+
const freeTier = providerRow?.free_tier
|
|
1186
|
+
|
|
1187
|
+
return (
|
|
1188
|
+
<div className="grid place-items-center gap-7 py-6 text-center">
|
|
1189
|
+
<DecodedLabel leaving={leaving} text={t.onboarding.connectedProvider(flow.label)} />
|
|
1190
|
+
|
|
1191
|
+
<div
|
|
1192
|
+
className={cn(
|
|
1193
|
+
'grid justify-items-center gap-1.5 transition duration-[360ms] ease-out',
|
|
1194
|
+
leaving ? 'opacity-0 saturate-0' : 'opacity-100 saturate-100'
|
|
1195
|
+
)}
|
|
1196
|
+
>
|
|
1197
|
+
<div className="flex items-center gap-2">
|
|
1198
|
+
<span className="font-mono text-[0.625rem] uppercase tracking-[0.2em] text-muted-foreground">
|
|
1199
|
+
{t.onboarding.defaultModel}
|
|
1200
|
+
</span>
|
|
1201
|
+
{freeTier === true && (
|
|
1202
|
+
<span className="rounded-sm bg-emerald-500/15 px-1 py-0.5 text-[0.6rem] font-semibold uppercase tracking-wide text-emerald-600 dark:text-emerald-400">
|
|
1203
|
+
{t.onboarding.freeTier}
|
|
1204
|
+
</span>
|
|
1205
|
+
)}
|
|
1206
|
+
{freeTier === false && (
|
|
1207
|
+
<span className="rounded-sm bg-primary/15 px-1 py-0.5 text-[0.6rem] font-semibold uppercase tracking-wide text-primary">
|
|
1208
|
+
{t.onboarding.pro}
|
|
1209
|
+
</span>
|
|
1210
|
+
)}
|
|
1211
|
+
</div>
|
|
1212
|
+
<p className="font-mono text-base">
|
|
1213
|
+
<GlyphText text={scrambledModel} />
|
|
1214
|
+
</p>
|
|
1215
|
+
{price && (price.input || price.output) && (
|
|
1216
|
+
<p className="font-mono text-xs text-muted-foreground">
|
|
1217
|
+
{price.free ? t.onboarding.free : t.onboarding.price(price.input || '?', price.output || '?')}
|
|
1218
|
+
</p>
|
|
1219
|
+
)}
|
|
1220
|
+
<Button
|
|
1221
|
+
className="mt-0.5 text-xs"
|
|
1222
|
+
disabled={flow.saving}
|
|
1223
|
+
onClick={() => setPickerOpen(true)}
|
|
1224
|
+
size="inline"
|
|
1225
|
+
variant="text"
|
|
1226
|
+
>
|
|
1227
|
+
{t.onboarding.change}
|
|
1228
|
+
</Button>
|
|
1229
|
+
</div>
|
|
1230
|
+
|
|
1231
|
+
<div
|
|
1232
|
+
className={cn(
|
|
1233
|
+
'transition duration-[360ms] ease-out',
|
|
1234
|
+
leaving ? 'opacity-0 saturate-0' : 'opacity-100 saturate-100'
|
|
1235
|
+
)}
|
|
1236
|
+
>
|
|
1237
|
+
<HackeryButton
|
|
1238
|
+
disabled={flow.saving}
|
|
1239
|
+
label={<GlyphText text={scrambledBegin} />}
|
|
1240
|
+
loading={flow.saving}
|
|
1241
|
+
onClick={onBegin}
|
|
1242
|
+
/>
|
|
1243
|
+
</div>
|
|
1244
|
+
|
|
1245
|
+
{/*
|
|
1246
|
+
ModelPickerDialog defaults to z-130 on its content, which renders
|
|
1247
|
+
UNDER the onboarding overlay (z-1300) and breaks pointer events.
|
|
1248
|
+
Bump it above with z-[1310] so the picker sits on top of the
|
|
1249
|
+
onboarding panel. The dialog's own dim-backdrop layer stays at
|
|
1250
|
+
its default z-120 — the onboarding overlay is already dimming
|
|
1251
|
+
the rest of the screen, so we don't want a second backdrop.
|
|
1252
|
+
*/}
|
|
1253
|
+
<ModelPickerDialog
|
|
1254
|
+
contentClassName="z-[1310]"
|
|
1255
|
+
currentModel={flow.currentModel}
|
|
1256
|
+
currentProvider={flow.providerSlug}
|
|
1257
|
+
onOpenChange={setPickerOpen}
|
|
1258
|
+
onSelect={({ model }) => {
|
|
1259
|
+
void setOnboardingModel(model)
|
|
1260
|
+
setPickerOpen(false)
|
|
1261
|
+
}}
|
|
1262
|
+
open={pickerOpen}
|
|
1263
|
+
/>
|
|
1264
|
+
</div>
|
|
1265
|
+
)
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
function DocsLink({ children, href }: { children: React.ReactNode; href: string }) {
|
|
1269
|
+
return (
|
|
1270
|
+
<Button asChild size="xs" variant="text">
|
|
1271
|
+
<a href={href} rel="noreferrer" target="_blank">
|
|
1272
|
+
<ExternalLink className="size-3" />
|
|
1273
|
+
{children}
|
|
1274
|
+
</a>
|
|
1275
|
+
</Button>
|
|
1276
|
+
)
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
function Status({ children }: { children: React.ReactNode }) {
|
|
1280
|
+
return (
|
|
1281
|
+
<div className="flex items-center gap-2.5 py-1 text-sm text-muted-foreground" role="status">
|
|
1282
|
+
<Loader className="size-7" type="lemniscate-bloom" />
|
|
1283
|
+
{children}
|
|
1284
|
+
</div>
|
|
1285
|
+
)
|
|
1286
|
+
}
|