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,866 @@
|
|
|
1
|
+
import { atom } from 'nanostores'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
cancelOAuthSession,
|
|
5
|
+
getGlobalModelOptions,
|
|
6
|
+
getRecommendedDefaultModel,
|
|
7
|
+
listOAuthProviders,
|
|
8
|
+
pollOAuthSession,
|
|
9
|
+
setEnvVar,
|
|
10
|
+
setModelAssignment,
|
|
11
|
+
startOAuthLogin,
|
|
12
|
+
submitOAuthCode,
|
|
13
|
+
validateProviderCredential
|
|
14
|
+
} from '@/nastech'
|
|
15
|
+
import { evaluateRuntimeReadiness, type RuntimeReadinessResult } from '@/lib/runtime-readiness'
|
|
16
|
+
import { notify, notifyError } from '@/store/notifications'
|
|
17
|
+
import type { ModelOptionProvider, OAuthProvider, OAuthStartResponse } from '@/types/nastech'
|
|
18
|
+
|
|
19
|
+
type PkceStart = Extract<OAuthStartResponse, { flow: 'pkce' }>
|
|
20
|
+
type DeviceStart = Extract<OAuthStartResponse, { flow: 'device_code' }>
|
|
21
|
+
type LoopbackStart = Extract<OAuthStartResponse, { flow: 'loopback' }>
|
|
22
|
+
|
|
23
|
+
export type OnboardingMode = 'apikey' | 'oauth'
|
|
24
|
+
|
|
25
|
+
export type OnboardingFlow =
|
|
26
|
+
| { status: 'idle' }
|
|
27
|
+
| { provider: OAuthProvider; status: 'starting' }
|
|
28
|
+
| { code: string; provider: OAuthProvider; start: PkceStart; status: 'awaiting_user' }
|
|
29
|
+
| { copied: boolean; provider: OAuthProvider; start: DeviceStart; status: 'polling' }
|
|
30
|
+
// Loopback PKCE (xAI Grok): browser opens, the local backend's 127.0.0.1
|
|
31
|
+
// listener catches the redirect, and we poll until the worker finishes.
|
|
32
|
+
// No code to paste and no user_code to show — just a waiting state.
|
|
33
|
+
| { provider: OAuthProvider; start: LoopbackStart; status: 'awaiting_browser' }
|
|
34
|
+
| { provider: OAuthProvider; start: OAuthStartResponse; status: 'submitting' }
|
|
35
|
+
| { copied: boolean; provider: OAuthProvider; status: 'external_pending' }
|
|
36
|
+
| { provider: OAuthProvider; status: 'success' }
|
|
37
|
+
| {
|
|
38
|
+
// After successful credential acquisition, before completing
|
|
39
|
+
// onboarding: show the user which model they're getting and let
|
|
40
|
+
// them change it. providerSlug is the model.options slug for the
|
|
41
|
+
// just-authenticated provider (used to persist the chosen model
|
|
42
|
+
// via /api/model/set). The change-model UI uses the existing
|
|
43
|
+
// ModelPickerDialog, which fetches its own model list from
|
|
44
|
+
// /api/model/options — no need to cache the list here.
|
|
45
|
+
currentModel: string
|
|
46
|
+
label: string
|
|
47
|
+
providerSlug: string
|
|
48
|
+
saving: boolean
|
|
49
|
+
status: 'confirming_model'
|
|
50
|
+
}
|
|
51
|
+
| { message: string; provider?: OAuthProvider; start?: OAuthStartResponse; status: 'error' }
|
|
52
|
+
|
|
53
|
+
export interface DesktopOnboardingState {
|
|
54
|
+
/** null until the first runtime check resolves. Seeded from localStorage so
|
|
55
|
+
* returning users skip the boot overlay entirely instead of flashing it
|
|
56
|
+
* every reload. */
|
|
57
|
+
configured: boolean | null
|
|
58
|
+
flow: OnboardingFlow
|
|
59
|
+
mode: OnboardingMode
|
|
60
|
+
providers: null | OAuthProvider[]
|
|
61
|
+
reason: null | string
|
|
62
|
+
requested: boolean
|
|
63
|
+
/** True when the user explicitly chose "I'll choose a provider later" on the
|
|
64
|
+
* first-run picker. Persisted to localStorage so the blocking overlay never
|
|
65
|
+
* re-nags on subsequent launches — the user can connect a provider any time
|
|
66
|
+
* from Settings → Providers (or the model picker's "Add provider"). Distinct
|
|
67
|
+
* from `configured`: the app still has no usable provider, so chat won't work
|
|
68
|
+
* until one is connected; we just stop forcing the choice up front. */
|
|
69
|
+
firstRunSkipped: boolean
|
|
70
|
+
/** True when the user explicitly opened the provider selector to add /
|
|
71
|
+
* switch providers from an already-configured app (e.g. via the model
|
|
72
|
+
* picker's "Add provider" button). Forces the overlay to show the picker
|
|
73
|
+
* even when configured === true, and adds a close affordance. */
|
|
74
|
+
manual: boolean
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface OnboardingContext {
|
|
78
|
+
onCompleted?: () => void
|
|
79
|
+
requestGateway: <T = unknown>(method: string, params?: Record<string, unknown>) => Promise<T>
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const CONFIGURED_CACHE_KEY = 'NASTECH-desktop-onboarded-v1'
|
|
83
|
+
const SKIP_CACHE_KEY = 'NASTECH-onboarding-skipped-v1'
|
|
84
|
+
const POLL_MS = 2000
|
|
85
|
+
const COPY_FLASH_MS = 1500
|
|
86
|
+
export const DEFAULT_ONBOARDING_REASON = 'No inference provider is configured.'
|
|
87
|
+
export const DEFAULT_MANUAL_ONBOARDING_REASON = 'Add or switch inference provider.'
|
|
88
|
+
|
|
89
|
+
function readCachedConfigured(): boolean | null {
|
|
90
|
+
if (typeof window === 'undefined') {
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
return window.localStorage.getItem(CONFIGURED_CACHE_KEY) === '1' ? true : null
|
|
96
|
+
} catch {
|
|
97
|
+
return null
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function writeCachedConfigured(value: boolean) {
|
|
102
|
+
if (typeof window === 'undefined') {
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
if (value) {
|
|
108
|
+
window.localStorage.setItem(CONFIGURED_CACHE_KEY, '1')
|
|
109
|
+
} else {
|
|
110
|
+
window.localStorage.removeItem(CONFIGURED_CACHE_KEY)
|
|
111
|
+
}
|
|
112
|
+
} catch {
|
|
113
|
+
// localStorage unavailable — degrade silently.
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function readCachedSkipped(): boolean {
|
|
118
|
+
if (typeof window === 'undefined') {
|
|
119
|
+
return false
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
return window.localStorage.getItem(SKIP_CACHE_KEY) === '1'
|
|
124
|
+
} catch {
|
|
125
|
+
return false
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function writeCachedSkipped(value: boolean) {
|
|
130
|
+
if (typeof window === 'undefined') {
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
if (value) {
|
|
136
|
+
window.localStorage.setItem(SKIP_CACHE_KEY, '1')
|
|
137
|
+
} else {
|
|
138
|
+
window.localStorage.removeItem(SKIP_CACHE_KEY)
|
|
139
|
+
}
|
|
140
|
+
} catch {
|
|
141
|
+
// localStorage unavailable — degrade silently.
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const INITIAL: DesktopOnboardingState = {
|
|
146
|
+
configured: readCachedConfigured(),
|
|
147
|
+
flow: { status: 'idle' },
|
|
148
|
+
mode: 'oauth',
|
|
149
|
+
providers: null,
|
|
150
|
+
reason: null,
|
|
151
|
+
requested: false,
|
|
152
|
+
firstRunSkipped: readCachedSkipped(),
|
|
153
|
+
manual: false
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export const $desktopOnboarding = atom<DesktopOnboardingState>(INITIAL)
|
|
157
|
+
|
|
158
|
+
let pollTimer: number | null = null
|
|
159
|
+
let providersRefreshPromise: null | Promise<void> = null
|
|
160
|
+
|
|
161
|
+
const errMessage = (e: unknown) => (e instanceof Error ? e.message : String(e))
|
|
162
|
+
|
|
163
|
+
const patch = (update: Partial<DesktopOnboardingState>) =>
|
|
164
|
+
$desktopOnboarding.set({ ...$desktopOnboarding.get(), ...update })
|
|
165
|
+
|
|
166
|
+
const setFlow = (flow: OnboardingFlow) =>
|
|
167
|
+
patch(flow.status === 'idle' ? { flow } : { flow, reason: null })
|
|
168
|
+
|
|
169
|
+
const sessionIdFor = (flow: OnboardingFlow) => ('start' in flow && flow.start ? flow.start.session_id : undefined)
|
|
170
|
+
|
|
171
|
+
function clearPoll() {
|
|
172
|
+
if (pollTimer !== null) {
|
|
173
|
+
window.clearInterval(pollTimer)
|
|
174
|
+
pollTimer = null
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function checkRuntime(ctx: OnboardingContext): Promise<RuntimeReadinessResult> {
|
|
179
|
+
return evaluateRuntimeReadiness(ctx.requestGateway, {
|
|
180
|
+
defaultReason: DEFAULT_ONBOARDING_REASON,
|
|
181
|
+
unknownReady: false
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function notifyReady(provider: string) {
|
|
186
|
+
notify({ kind: 'success', title: 'NasTech is ready', message: `${provider} connected.` })
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Human-friendly labels for tools auto-routed through the NasTech Tool Gateway,
|
|
190
|
+
// mirroring nastech_cli/nastech_subscription._GATEWAY_TOOL_LABELS so the GUI and
|
|
191
|
+
// CLI describe the same thing.
|
|
192
|
+
const GATEWAY_TOOL_LABELS: Record<string, string> = {
|
|
193
|
+
browser: 'browser automation',
|
|
194
|
+
image_gen: 'image generation',
|
|
195
|
+
tts: 'text-to-speech',
|
|
196
|
+
video_gen: 'video generation',
|
|
197
|
+
web: 'web search & extract'
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// When switching to NasTech Portal auto-routes unconfigured tools through the Tool
|
|
201
|
+
// Gateway, tell the user which ones — same information the CLI prints. Silent
|
|
202
|
+
// when nothing changed (subscriber already configured, has own keys, etc.).
|
|
203
|
+
function notifyGatewayTools(tools: string[] | undefined) {
|
|
204
|
+
if (!tools || tools.length === 0) {
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const labels = tools.map(t => GATEWAY_TOOL_LABELS[t] ?? t)
|
|
209
|
+
const list = labels.length === 1 ? labels[0] : `${labels.slice(0, -1).join(', ')} and ${labels[labels.length - 1]}`
|
|
210
|
+
|
|
211
|
+
notify({
|
|
212
|
+
durationMs: 8000,
|
|
213
|
+
kind: 'info',
|
|
214
|
+
message: `${list} now run through your NasTech Portal subscription — no separate API keys needed.`,
|
|
215
|
+
title: 'Tool Gateway enabled'
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// After credentials are persisted, ask the backend which provider+models
|
|
220
|
+
// are now authenticated. Pick the first curated model for the matching
|
|
221
|
+
// provider as a sensible default, persist it via /api/model/set, and
|
|
222
|
+
// transition to the model-confirmation step. If anything goes wrong
|
|
223
|
+
// fetching options (no providers returned, network error), the caller
|
|
224
|
+
// falls through to completing onboarding without showing the confirm
|
|
225
|
+
// card — the user gets the undefined-model auto-selection behaviour
|
|
226
|
+
// we had before, which works but is surprising. The confirm step is
|
|
227
|
+
// opportunistic polish, not a hard requirement for onboarding.
|
|
228
|
+
async function fetchProviderDefaultModel(
|
|
229
|
+
preferredSlugs: string[]
|
|
230
|
+
): Promise<null | { providerSlug: string; defaultModel: string }> {
|
|
231
|
+
let options
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
options = await getGlobalModelOptions()
|
|
235
|
+
} catch {
|
|
236
|
+
return null
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const providers = options?.providers ?? []
|
|
240
|
+
|
|
241
|
+
if (providers.length === 0) {
|
|
242
|
+
return null
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Try each preferred slug (lowercased), fall back to the first provider
|
|
246
|
+
// returned (model.options orders by recency / authenticated state, so
|
|
247
|
+
// the just-authenticated provider is usually first anyway).
|
|
248
|
+
const lower = preferredSlugs.map(s => s.toLowerCase())
|
|
249
|
+
|
|
250
|
+
const matched =
|
|
251
|
+
providers.find((p: ModelOptionProvider) => lower.includes(String(p.slug).toLowerCase())) ?? providers[0]
|
|
252
|
+
|
|
253
|
+
const models = matched.models ?? []
|
|
254
|
+
|
|
255
|
+
if (models.length === 0) {
|
|
256
|
+
return null
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Prefer the backend's recommended default — it mirrors the curation
|
|
260
|
+
// `NASTECH model` does (for NasTech Portal it honors the user's free/paid tier, so a
|
|
261
|
+
// free user gets a free model rather than a paid default like opus). Fall
|
|
262
|
+
// back to the first curated model if the endpoint can't resolve one.
|
|
263
|
+
let defaultModel = String(models[0])
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
const recommended = await getRecommendedDefaultModel(String(matched.slug))
|
|
267
|
+
|
|
268
|
+
if (recommended.model && models.map(String).includes(recommended.model)) {
|
|
269
|
+
defaultModel = recommended.model
|
|
270
|
+
} else if (recommended.model) {
|
|
271
|
+
// Recommended model isn't in the curated options list (e.g. a Portal
|
|
272
|
+
// free-recommendation the picker list didn't include); trust it anyway.
|
|
273
|
+
defaultModel = recommended.model
|
|
274
|
+
}
|
|
275
|
+
} catch {
|
|
276
|
+
// Endpoint unavailable — keep models[0]. Non-fatal: the confirm card still
|
|
277
|
+
// shows and the user can change it.
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
providerSlug: String(matched.slug),
|
|
282
|
+
defaultModel
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// After OAuth/API-key success: reload the backend env, verify runtime,
|
|
287
|
+
// then either show the model-confirm step or fall straight through to
|
|
288
|
+
// completion if we can't determine a default.
|
|
289
|
+
//
|
|
290
|
+
// onFail receives the runtime-readiness `reason` from checkRuntime so
|
|
291
|
+
// the caller can fold it into a user-facing error — same contract as
|
|
292
|
+
// reloadAndConnect used to have (which this replaces).
|
|
293
|
+
async function completeWithModelConfirm(
|
|
294
|
+
ctx: OnboardingContext,
|
|
295
|
+
providerLabel: string,
|
|
296
|
+
preferredSlugs: string[],
|
|
297
|
+
onFail: (reason: null | string) => void,
|
|
298
|
+
// When true, a failing runtime check no longer blocks progression — the
|
|
299
|
+
// user is allowed through onboarding regardless. Used by the API-key path,
|
|
300
|
+
// where we intentionally don't validate the key (it blocked too many users).
|
|
301
|
+
ignoreRuntimeGate = false
|
|
302
|
+
) {
|
|
303
|
+
await ctx.requestGateway('reload.env').catch(() => undefined)
|
|
304
|
+
const runtime = await checkRuntime(ctx)
|
|
305
|
+
|
|
306
|
+
if (!runtime.ready && !ignoreRuntimeGate) {
|
|
307
|
+
onFail(runtime.reason)
|
|
308
|
+
|
|
309
|
+
return
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const defaults = await fetchProviderDefaultModel(preferredSlugs)
|
|
313
|
+
|
|
314
|
+
if (!defaults) {
|
|
315
|
+
// Couldn't get a sensible default — proceed without confirm step.
|
|
316
|
+
notifyReady(providerLabel)
|
|
317
|
+
completeDesktopOnboarding()
|
|
318
|
+
ctx.onCompleted?.()
|
|
319
|
+
|
|
320
|
+
return
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Persist the default model BEFORE showing the confirm card so that:
|
|
324
|
+
// (1) "current default: X" shown in the UI is what's actually written
|
|
325
|
+
// to config — no lying.
|
|
326
|
+
// (2) If the user clicks "Start chatting" without changing anything,
|
|
327
|
+
// no extra write is needed.
|
|
328
|
+
// (3) If they bail out (e.g., refresh the page), they still end up
|
|
329
|
+
// with a working config, not an empty-model fallback.
|
|
330
|
+
try {
|
|
331
|
+
const res = await setModelAssignment({
|
|
332
|
+
scope: 'main',
|
|
333
|
+
provider: defaults.providerSlug,
|
|
334
|
+
model: defaults.defaultModel
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
notifyGatewayTools(res.gateway_tools)
|
|
338
|
+
} catch {
|
|
339
|
+
// Persistence failed — still show the confirm card so the user can
|
|
340
|
+
// pick something explicitly. The backend will pick its own default
|
|
341
|
+
// at chat time if we end up never persisting.
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
setFlow({
|
|
345
|
+
status: 'confirming_model',
|
|
346
|
+
providerSlug: defaults.providerSlug,
|
|
347
|
+
currentModel: defaults.defaultModel,
|
|
348
|
+
label: providerLabel,
|
|
349
|
+
saving: false
|
|
350
|
+
})
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function providerResolutionFailure(reason: null | string) {
|
|
354
|
+
const detail = reason?.trim()
|
|
355
|
+
|
|
356
|
+
return detail
|
|
357
|
+
? `Connected, but NasTech still cannot resolve a usable provider. ${detail}`
|
|
358
|
+
: 'Connected, but NasTech still cannot resolve a usable provider.'
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function refreshProviders() {
|
|
362
|
+
if (providersRefreshPromise) {
|
|
363
|
+
await providersRefreshPromise
|
|
364
|
+
|
|
365
|
+
return
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
providersRefreshPromise = (async () => {
|
|
369
|
+
try {
|
|
370
|
+
const { providers } = await listOAuthProviders()
|
|
371
|
+
patch({ mode: providers.length > 0 ? 'oauth' : 'apikey', providers })
|
|
372
|
+
} catch {
|
|
373
|
+
patch({ mode: 'apikey', providers: [] })
|
|
374
|
+
} finally {
|
|
375
|
+
providersRefreshPromise = null
|
|
376
|
+
}
|
|
377
|
+
})()
|
|
378
|
+
|
|
379
|
+
await providersRefreshPromise
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function requestDesktopOnboarding(reason = DEFAULT_ONBOARDING_REASON) {
|
|
383
|
+
patch({ reason: reason.trim() || DEFAULT_ONBOARDING_REASON, requested: true })
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Open the onboarding provider selector on demand from an already-configured
|
|
387
|
+
// app — e.g. the model picker's "Add provider" button. Reuses the entire
|
|
388
|
+
// onboarding flow (OAuth rows, API-key form, model-confirm) instead of
|
|
389
|
+
// duplicating provider UI. Sets manual=true so the overlay shows the picker
|
|
390
|
+
// even though configured===true, and refreshes the provider list.
|
|
391
|
+
export function startManualOnboarding(reason: null | string = DEFAULT_MANUAL_ONBOARDING_REASON) {
|
|
392
|
+
patch({
|
|
393
|
+
manual: true,
|
|
394
|
+
requested: true,
|
|
395
|
+
// `null` opts out of the prompt banner entirely (e.g. when the user already
|
|
396
|
+
// picked a specific provider and we auto-start its sign-in).
|
|
397
|
+
reason: reason ? reason.trim() || DEFAULT_ONBOARDING_REASON : null,
|
|
398
|
+
flow: { status: 'idle' }
|
|
399
|
+
})
|
|
400
|
+
void refreshProviders()
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// One-shot hand-off used when the dedicated Providers settings page launches a
|
|
404
|
+
// specific provider's sign-in: we open the manual onboarding overlay AND
|
|
405
|
+
// remember which provider to start, so the overlay drives that exact OAuth
|
|
406
|
+
// flow instead of re-showing the picker the user just clicked through.
|
|
407
|
+
// Module-level (not store state) because it's consumed immediately on the next
|
|
408
|
+
// overlay render and never needs to persist or re-render anything itself.
|
|
409
|
+
let pendingProviderOAuthId: null | string = null
|
|
410
|
+
|
|
411
|
+
export function startManualProviderOAuth(providerId: string, reason: null | string = null) {
|
|
412
|
+
pendingProviderOAuthId = providerId
|
|
413
|
+
startManualOnboarding(reason)
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Read the pending provider id without clearing it. The overlay only clears it
|
|
417
|
+
// (via clearPendingProviderOAuth) once it has actually launched that provider,
|
|
418
|
+
// so a transient empty/failed provider fetch doesn't drop the hand-off and the
|
|
419
|
+
// deep-link can still auto-start after the list loads.
|
|
420
|
+
export function peekPendingProviderOAuth(): null | string {
|
|
421
|
+
return pendingProviderOAuthId
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export function clearPendingProviderOAuth() {
|
|
425
|
+
pendingProviderOAuthId = null
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Dismiss a manually-opened provider selector without touching the existing
|
|
429
|
+
// (working) configuration. Only valid in the manual path — the unconfigured
|
|
430
|
+
// first-run flow has no close affordance because the app can't run yet.
|
|
431
|
+
export function closeManualOnboarding() {
|
|
432
|
+
pendingProviderOAuthId = null
|
|
433
|
+
|
|
434
|
+
patch({ manual: false, requested: false, flow: { status: 'idle' } })
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export function completeDesktopOnboarding() {
|
|
438
|
+
clearPoll()
|
|
439
|
+
writeCachedConfigured(true)
|
|
440
|
+
// A real provider is now connected, so any earlier "choose later" skip is
|
|
441
|
+
// moot — clear it so the flag never lingers in a configured install.
|
|
442
|
+
writeCachedSkipped(false)
|
|
443
|
+
$desktopOnboarding.set({
|
|
444
|
+
configured: true,
|
|
445
|
+
flow: { status: 'idle' },
|
|
446
|
+
mode: 'oauth',
|
|
447
|
+
providers: null,
|
|
448
|
+
reason: null,
|
|
449
|
+
requested: false,
|
|
450
|
+
firstRunSkipped: false,
|
|
451
|
+
manual: false
|
|
452
|
+
})
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// "I'll choose a provider later" on the first-run picker. Persists the skip so
|
|
456
|
+
// the blocking overlay never re-nags on future launches, and dismisses it now
|
|
457
|
+
// so the user lands in the app. Chat won't work until a provider is connected
|
|
458
|
+
// (from Settings → Providers or the model picker's "Add provider") — this only
|
|
459
|
+
// stops forcing the choice up front. Distinct from completeDesktopOnboarding,
|
|
460
|
+
// which marks the app actually configured.
|
|
461
|
+
export function dismissFirstRunOnboarding() {
|
|
462
|
+
clearPoll()
|
|
463
|
+
writeCachedSkipped(true)
|
|
464
|
+
patch({ firstRunSkipped: true, requested: false, manual: false, flow: { status: 'idle' } })
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export function setOnboardingMode(mode: OnboardingMode) {
|
|
468
|
+
patch({ mode })
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export async function refreshOnboarding(ctx: OnboardingContext) {
|
|
472
|
+
// Manual mode (user opened the selector from a working app): never
|
|
473
|
+
// auto-dismiss on runtime-ready — the whole point is to let them add /
|
|
474
|
+
// switch a provider while already configured. Just ensure the provider
|
|
475
|
+
// list is loaded and show the picker.
|
|
476
|
+
if ($desktopOnboarding.get().manual) {
|
|
477
|
+
await refreshProviders()
|
|
478
|
+
|
|
479
|
+
return false
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const runtime = await checkRuntime(ctx)
|
|
483
|
+
|
|
484
|
+
if (runtime.ready) {
|
|
485
|
+
completeDesktopOnboarding()
|
|
486
|
+
ctx.onCompleted?.()
|
|
487
|
+
|
|
488
|
+
return true
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const state = $desktopOnboarding.get()
|
|
492
|
+
const reason = runtime.reason || state.reason || DEFAULT_ONBOARDING_REASON
|
|
493
|
+
|
|
494
|
+
writeCachedConfigured(false)
|
|
495
|
+
patch({ configured: false, reason })
|
|
496
|
+
|
|
497
|
+
if (state.providers !== null && !state.requested) {
|
|
498
|
+
return false
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
await refreshProviders()
|
|
502
|
+
|
|
503
|
+
return false
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Open a sign-in URL via the desktop bridge, falling back to window.open
|
|
507
|
+
// when the bridge isn't present (e.g. the web dashboard / dev preview) so
|
|
508
|
+
// the flow never silently stalls in a waiting state. Mirrors the pattern in
|
|
509
|
+
// apps/desktop/src/app/artifacts/index.tsx.
|
|
510
|
+
async function openSignInUrl(url: string) {
|
|
511
|
+
if (window.NASTECHDesktop?.openExternal) {
|
|
512
|
+
try {
|
|
513
|
+
await window.NASTECHDesktop.openExternal(url)
|
|
514
|
+
|
|
515
|
+
return
|
|
516
|
+
} catch {
|
|
517
|
+
// Bridge present but failed (no OS handler, user denied, etc.). Fall
|
|
518
|
+
// through to window.open so the sign-in URL still opens and the flow
|
|
519
|
+
// doesn't strand a pending OAuth session in a waiting state.
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
window.open(url, '_blank', 'noopener,noreferrer')
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
export async function startProviderOAuth(provider: OAuthProvider, ctx: OnboardingContext) {
|
|
527
|
+
clearPoll()
|
|
528
|
+
|
|
529
|
+
if (provider.flow === 'external') {
|
|
530
|
+
setFlow({ status: 'external_pending', provider, copied: false })
|
|
531
|
+
|
|
532
|
+
return
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
setFlow({ status: 'starting', provider })
|
|
536
|
+
|
|
537
|
+
try {
|
|
538
|
+
const start = await startOAuthLogin(provider.id)
|
|
539
|
+
const browserUrl = start.flow === 'device_code' ? start.verification_url : start.auth_url
|
|
540
|
+
await openSignInUrl(browserUrl)
|
|
541
|
+
|
|
542
|
+
if (start.flow === 'pkce') {
|
|
543
|
+
setFlow({ status: 'awaiting_user', provider, start, code: '' })
|
|
544
|
+
|
|
545
|
+
return
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (start.flow === 'loopback') {
|
|
549
|
+
// No code to paste: the redirect lands on the backend's loopback
|
|
550
|
+
// listener. Just wait and poll the session until the worker finishes.
|
|
551
|
+
setFlow({ status: 'awaiting_browser', provider, start })
|
|
552
|
+
pollTimer = window.setInterval(() => void pollSession(provider, start, ctx), POLL_MS)
|
|
553
|
+
|
|
554
|
+
return
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
setFlow({ status: 'polling', provider, start, copied: false })
|
|
558
|
+
pollTimer = window.setInterval(() => void pollSession(provider, start, ctx), POLL_MS)
|
|
559
|
+
} catch (error) {
|
|
560
|
+
setFlow({ status: 'error', provider, message: `Could not start sign-in: ${errMessage(error)}` })
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Poll a session-backed flow (device_code or loopback) until it resolves.
|
|
565
|
+
// Both shapes only need the session_id to poll; the start is threaded
|
|
566
|
+
// through to the error flow so the user can retry from the same context.
|
|
567
|
+
async function pollSession(provider: OAuthProvider, start: DeviceStart | LoopbackStart, ctx: OnboardingContext) {
|
|
568
|
+
try {
|
|
569
|
+
const { error_message, status } = await pollOAuthSession(provider.id, start.session_id)
|
|
570
|
+
|
|
571
|
+
if (status === 'approved') {
|
|
572
|
+
clearPoll()
|
|
573
|
+
setFlow({ status: 'success', provider })
|
|
574
|
+
await completeWithModelConfirm(ctx, provider.name, [provider.id], reason =>
|
|
575
|
+
setFlow({
|
|
576
|
+
status: 'error',
|
|
577
|
+
provider,
|
|
578
|
+
message: providerResolutionFailure(reason)
|
|
579
|
+
})
|
|
580
|
+
)
|
|
581
|
+
} else if (status !== 'pending') {
|
|
582
|
+
clearPoll()
|
|
583
|
+
setFlow({ status: 'error', provider, start, message: error_message || `Sign-in ${status}.` })
|
|
584
|
+
}
|
|
585
|
+
} catch (error) {
|
|
586
|
+
clearPoll()
|
|
587
|
+
setFlow({ status: 'error', provider, start, message: `Polling failed: ${errMessage(error)}` })
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
export function setOnboardingCode(code: string) {
|
|
592
|
+
const { flow } = $desktopOnboarding.get()
|
|
593
|
+
|
|
594
|
+
if (flow.status === 'awaiting_user') {
|
|
595
|
+
setFlow({ ...flow, code })
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
export async function submitOnboardingCode(ctx: OnboardingContext) {
|
|
600
|
+
const { flow } = $desktopOnboarding.get()
|
|
601
|
+
|
|
602
|
+
if (flow.status !== 'awaiting_user' || !flow.code.trim()) {
|
|
603
|
+
return
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const { provider, start, code } = flow
|
|
607
|
+
setFlow({ status: 'submitting', provider, start })
|
|
608
|
+
|
|
609
|
+
try {
|
|
610
|
+
const resp = await submitOAuthCode(provider.id, start.session_id, code.trim())
|
|
611
|
+
|
|
612
|
+
if (resp.ok && resp.status === 'approved') {
|
|
613
|
+
setFlow({ status: 'success', provider })
|
|
614
|
+
await completeWithModelConfirm(ctx, provider.name, [provider.id], reason =>
|
|
615
|
+
setFlow({
|
|
616
|
+
status: 'error',
|
|
617
|
+
provider,
|
|
618
|
+
message: providerResolutionFailure(reason)
|
|
619
|
+
})
|
|
620
|
+
)
|
|
621
|
+
} else {
|
|
622
|
+
setFlow({ status: 'error', provider, start, message: resp.message || 'Token exchange failed.' })
|
|
623
|
+
}
|
|
624
|
+
} catch (error) {
|
|
625
|
+
setFlow({ status: 'error', provider, start, message: errMessage(error) })
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
export function cancelOnboardingFlow() {
|
|
630
|
+
clearPoll()
|
|
631
|
+
const sessionId = sessionIdFor($desktopOnboarding.get().flow)
|
|
632
|
+
|
|
633
|
+
if (sessionId) {
|
|
634
|
+
cancelOAuthSession(sessionId).catch(() => undefined)
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
setFlow({ status: 'idle' })
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
async function copyAndFlash(text: string, predicate: (flow: OnboardingFlow) => boolean) {
|
|
641
|
+
try {
|
|
642
|
+
await navigator.clipboard.writeText(text)
|
|
643
|
+
} catch {
|
|
644
|
+
return
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const { flow } = $desktopOnboarding.get()
|
|
648
|
+
|
|
649
|
+
if (!predicate(flow) || !('copied' in flow)) {
|
|
650
|
+
return
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
setFlow({ ...flow, copied: true })
|
|
654
|
+
window.setTimeout(() => {
|
|
655
|
+
const current = $desktopOnboarding.get().flow
|
|
656
|
+
|
|
657
|
+
if (predicate(current) && 'copied' in current) {
|
|
658
|
+
setFlow({ ...current, copied: false })
|
|
659
|
+
}
|
|
660
|
+
}, COPY_FLASH_MS)
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
export async function copyDeviceCode() {
|
|
664
|
+
const { flow } = $desktopOnboarding.get()
|
|
665
|
+
|
|
666
|
+
if (flow.status !== 'polling') {
|
|
667
|
+
return
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const sid = flow.start.session_id
|
|
671
|
+
await copyAndFlash(flow.start.user_code, f => f.status === 'polling' && f.start.session_id === sid)
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
export async function copyExternalCommand() {
|
|
675
|
+
const { flow } = $desktopOnboarding.get()
|
|
676
|
+
|
|
677
|
+
if (flow.status !== 'external_pending') {
|
|
678
|
+
return
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const id = flow.provider.id
|
|
682
|
+
await copyAndFlash(flow.provider.cli_command, f => f.status === 'external_pending' && f.provider.id === id)
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
export async function recheckExternalSignin(ctx: OnboardingContext) {
|
|
686
|
+
const { flow } = $desktopOnboarding.get()
|
|
687
|
+
|
|
688
|
+
if (flow.status !== 'external_pending') {
|
|
689
|
+
return
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const { provider } = flow
|
|
693
|
+
await completeWithModelConfirm(ctx, provider.name, [provider.id], reason =>
|
|
694
|
+
setFlow({
|
|
695
|
+
status: 'error',
|
|
696
|
+
provider,
|
|
697
|
+
message:
|
|
698
|
+
reason?.trim() ||
|
|
699
|
+
`NasTech still cannot reach ${provider.name}. Run \`${provider.cli_command}\` in a terminal first.`
|
|
700
|
+
})
|
|
701
|
+
)
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
export async function saveOnboardingApiKey(envKey: string, value: string, label: string, ctx: OnboardingContext) {
|
|
705
|
+
const trimmed = value.trim()
|
|
706
|
+
|
|
707
|
+
if (!trimmed) {
|
|
708
|
+
return { ok: false, message: 'Enter a value first.' }
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// The "Local / custom endpoint" option carries a base URL, not an API key.
|
|
712
|
+
// It must be wired into config (provider=custom + base_url + model), not
|
|
713
|
+
// dropped into .env — runtime resolution ignores OPENAI_BASE_URL.
|
|
714
|
+
if (envKey === 'OPENAI_BASE_URL') {
|
|
715
|
+
return saveOnboardingLocalEndpoint(trimmed, ctx)
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// No key validation here on purpose: we previously live-probed the key and
|
|
719
|
+
// hard-blocked on a runtime check after saving, which rejected too many
|
|
720
|
+
// legitimate users (corporate proxies, regional blocks, flaky/rate-limited
|
|
721
|
+
// provider probes, self-hosted endpoints). We now save the value as-is and
|
|
722
|
+
// let the user proceed; an actually-bad key surfaces later at chat time.
|
|
723
|
+
try {
|
|
724
|
+
await setEnvVar(envKey, trimmed)
|
|
725
|
+
// For API-key flows we don't have a definitive provider id (the
|
|
726
|
+
// user picked which API key they're entering, but the corresponding
|
|
727
|
+
// backend slug — e.g. OPENROUTER_API_KEY → "openrouter" — is the
|
|
728
|
+
// env-key prefix stripped). Pass a couple of likely candidates;
|
|
729
|
+
// fetchProviderDefaultModel falls back to the first authenticated
|
|
730
|
+
// provider returned by /api/model/options if none match.
|
|
731
|
+
const slugCandidates = [envKey.replace(/_API_KEY$/, '').toLowerCase(), label.toLowerCase()]
|
|
732
|
+
// ignoreRuntimeGate=true: never block onboarding on the runtime check.
|
|
733
|
+
await completeWithModelConfirm(ctx, label, slugCandidates, () => undefined, true)
|
|
734
|
+
|
|
735
|
+
return { ok: true }
|
|
736
|
+
} catch (error) {
|
|
737
|
+
notifyError(error, `Could not save ${label}`)
|
|
738
|
+
|
|
739
|
+
return { ok: false, message: errMessage(error) }
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// Configure a local / self-hosted OpenAI-compatible endpoint (vLLM, llama.cpp,
|
|
744
|
+
// Ollama, …). Unlike API-key providers, a local endpoint is defined by its URL
|
|
745
|
+
// and usually needs NO key. The runtime resolver reads model.base_url from
|
|
746
|
+
// config (it ignores the OPENAI_BASE_URL env var), so we persist
|
|
747
|
+
// provider=custom + base_url + model via /api/model/set rather than dropping an
|
|
748
|
+
// env var that resolution never consults.
|
|
749
|
+
//
|
|
750
|
+
// The model is auto-discovered from the endpoint's /v1/models (surfaced by the
|
|
751
|
+
// validate probe) so the user only has to paste a URL — no extra UI field.
|
|
752
|
+
//
|
|
753
|
+
// We deliberately don't route through completeWithModelConfirm: that path
|
|
754
|
+
// re-assigns the model from /api/model/options WITHOUT a base_url, which would
|
|
755
|
+
// wipe the base_url we just wrote. We have a concrete model already, so we
|
|
756
|
+
// verify the runtime directly and finish.
|
|
757
|
+
export async function saveOnboardingLocalEndpoint(baseUrl: string, ctx: OnboardingContext) {
|
|
758
|
+
const url = baseUrl.trim()
|
|
759
|
+
|
|
760
|
+
if (!url) {
|
|
761
|
+
return { ok: false, message: 'Enter the endpoint URL first.' }
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Probe connectivity + discover the served models. Any HTTP response proves
|
|
765
|
+
// the endpoint is up; an unreachable probe hard-blocks because we can't
|
|
766
|
+
// resolve a model to route to.
|
|
767
|
+
let model = ''
|
|
768
|
+
|
|
769
|
+
try {
|
|
770
|
+
const probe = await validateProviderCredential('OPENAI_BASE_URL', url)
|
|
771
|
+
|
|
772
|
+
if (!probe.ok && probe.reachable) {
|
|
773
|
+
return { ok: false, message: probe.message || 'Could not reach that endpoint.' }
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
if (!probe.reachable) {
|
|
777
|
+
return { ok: false, message: probe.message || `Could not reach ${url}.` }
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
model = (probe.models?.[0] ?? '').trim()
|
|
781
|
+
} catch {
|
|
782
|
+
return { ok: false, message: `Could not reach ${url}.` }
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
if (!model) {
|
|
786
|
+
return {
|
|
787
|
+
ok: false,
|
|
788
|
+
message: `Connected to ${url}, but it advertised no models at /v1/models. Start a model on that endpoint and try again.`
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
try {
|
|
793
|
+
await setModelAssignment({ scope: 'main', provider: 'custom', model, base_url: url })
|
|
794
|
+
await ctx.requestGateway('reload.env').catch(() => undefined)
|
|
795
|
+
|
|
796
|
+
const runtime = await checkRuntime(ctx)
|
|
797
|
+
|
|
798
|
+
if (!runtime.ready) {
|
|
799
|
+
const detail = (runtime.reason ?? '').trim()
|
|
800
|
+
|
|
801
|
+
return { ok: false, message: detail || `Saved, but NasTech still cannot reach ${url}.` }
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
notifyReady('Local / custom endpoint')
|
|
805
|
+
completeDesktopOnboarding()
|
|
806
|
+
ctx.onCompleted?.()
|
|
807
|
+
|
|
808
|
+
return { ok: true }
|
|
809
|
+
} catch (error) {
|
|
810
|
+
notifyError(error, 'Could not save local endpoint')
|
|
811
|
+
|
|
812
|
+
return { ok: false, message: errMessage(error) }
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// User picked a different model from the dropdown on the confirm card.
|
|
817
|
+
// Persists immediately so the displayed value is always what's on disk.
|
|
818
|
+
export async function setOnboardingModel(model: string) {
|
|
819
|
+
const { flow } = $desktopOnboarding.get()
|
|
820
|
+
|
|
821
|
+
if (flow.status !== 'confirming_model') {
|
|
822
|
+
return
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Optimistic update so the dropdown feels instant; revert on failure.
|
|
826
|
+
const previous = flow.currentModel
|
|
827
|
+
setFlow({ ...flow, currentModel: model, saving: true })
|
|
828
|
+
|
|
829
|
+
try {
|
|
830
|
+
await setModelAssignment({
|
|
831
|
+
scope: 'main',
|
|
832
|
+
provider: flow.providerSlug,
|
|
833
|
+
model
|
|
834
|
+
})
|
|
835
|
+
const current = $desktopOnboarding.get().flow
|
|
836
|
+
|
|
837
|
+
if (current.status === 'confirming_model') {
|
|
838
|
+
setFlow({ ...current, currentModel: model, saving: false })
|
|
839
|
+
}
|
|
840
|
+
} catch (error) {
|
|
841
|
+
notifyError(error, 'Could not change model')
|
|
842
|
+
const current = $desktopOnboarding.get().flow
|
|
843
|
+
|
|
844
|
+
if (current.status === 'confirming_model') {
|
|
845
|
+
setFlow({ ...current, currentModel: previous, saving: false })
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// User clicked "Start chatting" on the confirm card. Finalizes onboarding
|
|
851
|
+
// — the model was already persisted by completeWithModelConfirm (or by
|
|
852
|
+
// setOnboardingModel if they changed it), so all that's left is to mark
|
|
853
|
+
// onboarding done and unblock the rest of the app.
|
|
854
|
+
export function confirmOnboardingModel(ctx: OnboardingContext) {
|
|
855
|
+
const { flow } = $desktopOnboarding.get()
|
|
856
|
+
|
|
857
|
+
if (flow.status !== 'confirming_model') {
|
|
858
|
+
return
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// No success toast here: the confirm-model screen already showed "<provider>
|
|
862
|
+
// connected." notifyReady is reserved for completion paths that SKIP this
|
|
863
|
+
// screen (no-default fallthrough, local endpoint) so feedback isn't lost.
|
|
864
|
+
completeDesktopOnboarding()
|
|
865
|
+
ctx.onCompleted?.()
|
|
866
|
+
}
|