From f2d25ab94bb88ea21e390d052c45ad30b58b034e Mon Sep 17 00:00:00 2001 From: "vargas@vellum.ai" Date: Sat, 14 Mar 2026 22:14:55 +0000 Subject: [PATCH 1/3] chore: update install.sh URL to vellum.ai and clean up old user-hosted onboarding logic - Update all install.sh references from assistant.vellum.ai to vellum.ai - Update production VELLUM_PLATFORM_URL fallback to vellum.ai - Remove CloudCredentialsStepView.swift (replaced by inline cloud fields in APIKeyStepView) - Remove dead code ModelSelectionStepView.swift (unused in onboarding flow) - Show inline cloud credential fields for all paths, not just managed sign-in - Simplify onboarding flow to always be 3 steps (WakeUp -> APIKey -> ImproveExperience) - Remove needsCloudCredentials property and related branching - Update primaryButtonDisabled to validate cloud fields inline - Simplify saveAndContinue to use showHostingSelector for cloud provider selection Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- cli/src/adapters/install.sh | 2 +- cli/src/commands/hatch.ts | 2 +- cli/src/lib/gcp.ts | 2 +- .../vellum-assistant/App/AssistantCli.swift | 2 +- .../APIKeyStepView+CloudFields.swift | 2 +- .../Features/Onboarding/APIKeyStepView.swift | 34 +- .../Onboarding/CloudCredentialsStepView.swift | 486 ------------------ .../ImproveExperienceStepView.swift | 9 +- .../Onboarding/ModelSelectionStepView.swift | 162 ------ .../Onboarding/OnboardingFlowView.swift | 12 +- .../Features/Onboarding/OnboardingState.swift | 14 +- scripts/fetch-qr-code.sh | 2 +- 12 files changed, 31 insertions(+), 698 deletions(-) delete mode 100644 clients/macos/vellum-assistant/Features/Onboarding/CloudCredentialsStepView.swift delete mode 100644 clients/macos/vellum-assistant/Features/Onboarding/ModelSelectionStepView.swift diff --git a/cli/src/adapters/install.sh b/cli/src/adapters/install.sh index d82988f220d..68adb4f3b91 100755 --- a/cli/src/adapters/install.sh +++ b/cli/src/adapters/install.sh @@ -231,7 +231,7 @@ symlink_vellum() { # Append PATH setup to ~/.config/vellum/env so callers can pick up PATH # changes without restarting their shell: -# curl -fsSL https://assistant.vellum.ai/install.sh | bash && . ~/.config/vellum/env +# curl -fsSL https://vellum.ai/install.sh | bash && . ~/.config/vellum/env write_env_file() { local env_dir="${XDG_CONFIG_HOME:-$HOME/.config}/vellum" local env_file="$env_dir/env" diff --git a/cli/src/commands/hatch.ts b/cli/src/commands/hatch.ts index a8ef6dc75e3..5e357de0d64 100644 --- a/cli/src/commands/hatch.ts +++ b/cli/src/commands/hatch.ts @@ -104,7 +104,7 @@ export async function buildStartupScript( cloud: RemoteHost, ): Promise { const platformUrl = - process.env.VELLUM_PLATFORM_URL ?? "https://assistant.vellum.ai"; + process.env.VELLUM_PLATFORM_URL ?? "https://vellum.ai"; const logPath = cloud === "custom" ? "/tmp/vellum-startup.log" diff --git a/cli/src/lib/gcp.ts b/cli/src/lib/gcp.ts index fea80c18159..820617c76fc 100644 --- a/cli/src/lib/gcp.ts +++ b/cli/src/lib/gcp.ts @@ -637,7 +637,7 @@ export async function hatchGcp( species === "vellum" && (await checkCurlFailure(instanceName, project, zone, account)) ) { - const installScriptUrl = `${process.env.VELLUM_PLATFORM_URL ?? "https://assistant.vellum.ai"}/install.sh`; + const installScriptUrl = `${process.env.VELLUM_PLATFORM_URL ?? "https://vellum.ai"}/install.sh`; console.log( `\ud83d\udd04 Detected install script curl failure for ${installScriptUrl}, attempting recovery...`, ); diff --git a/clients/macos/vellum-assistant/App/AssistantCli.swift b/clients/macos/vellum-assistant/App/AssistantCli.swift index eb0bf4adf0f..476a7b51337 100644 --- a/clients/macos/vellum-assistant/App/AssistantCli.swift +++ b/clients/macos/vellum-assistant/App/AssistantCli.swift @@ -374,7 +374,7 @@ final class AssistantCli { #if DEBUG env["VELLUM_PLATFORM_URL"] = "https://dev-assistant.vellum.ai" #else - env["VELLUM_PLATFORM_URL"] = "https://assistant.vellum.ai" + env["VELLUM_PLATFORM_URL"] = "https://vellum.ai" #endif } diff --git a/clients/macos/vellum-assistant/Features/Onboarding/APIKeyStepView+CloudFields.swift b/clients/macos/vellum-assistant/Features/Onboarding/APIKeyStepView+CloudFields.swift index 29e065f5683..658da8d5bc1 100644 --- a/clients/macos/vellum-assistant/Features/Onboarding/APIKeyStepView+CloudFields.swift +++ b/clients/macos/vellum-assistant/Features/Onboarding/APIKeyStepView+CloudFields.swift @@ -184,7 +184,7 @@ extension APIKeyStepView { .font(.system(size: 13, weight: .medium)) .foregroundColor(VColor.contentSecondary) VStack(alignment: .leading, spacing: VSpacing.xs) { - setupStep("1. On your Mac mini, run: curl -fsSL https://assistant.vellum.ai/install.sh | bash") + setupStep("1. On your Mac mini, run: curl -fsSL https://vellum.ai/install.sh | bash") setupStep("2. Upload the QR code PNG generated by the install script below.") } } diff --git a/clients/macos/vellum-assistant/Features/Onboarding/APIKeyStepView.swift b/clients/macos/vellum-assistant/Features/Onboarding/APIKeyStepView.swift index 5f0039cd69e..52287a0a27c 100644 --- a/clients/macos/vellum-assistant/Features/Onboarding/APIKeyStepView.swift +++ b/clients/macos/vellum-assistant/Features/Onboarding/APIKeyStepView.swift @@ -83,7 +83,7 @@ struct APIKeyStepView: View { } private var showInlineCloudFields: Bool { - managedSignInEnabled && (hostingMode == .gcp || hostingMode == .aws || hostingMode == .customHardware) + hostingMode == .gcp || hostingMode == .aws || hostingMode == .customHardware } var body: some View { @@ -159,7 +159,7 @@ struct APIKeyStepView: View { } if !managedSignInEnabled { - OnboardingFooter(currentStep: state.currentStep, totalSteps: userHostedEnabled ? 4 : 3) + OnboardingFooter(currentStep: state.currentStep, totalSteps: 3) .padding(.bottom, VSpacing.lg) } } @@ -321,7 +321,20 @@ struct APIKeyStepView: View { // MARK: - Helpers private var primaryButtonDisabled: Bool { - apiKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + if apiKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + return true + } + switch hostingMode { + case .gcp: + return state.gcpProjectId.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + || state.gcpServiceAccountKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + case .aws: + return state.awsRoleArn.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + case .customHardware: + return state.customQRCodeImageData.isEmpty + default: + return false + } } private var hatchButtonDisabled: Bool { @@ -377,8 +390,10 @@ struct APIKeyStepView: View { } private func saveAndContinue() { - if userHostedEnabled { + if showHostingSelector { state.cloudProvider = hostingMode.rawValue + } else { + state.cloudProvider = "local" } let trimmed = apiKey.trimmingCharacters(in: .whitespacesAndNewlines) @@ -386,15 +401,8 @@ struct APIKeyStepView: View { APIKeyManager.setKey(trimmed, for: "anthropic") APIKeyManager.syncKeyToDaemon(provider: "anthropic", value: trimmed) - saveModelToConfig("claude-opus-4-6") - if userHostedEnabled && hostingMode != .local && hostingMode != .docker { - state.advance() - } else if userHostedEnabled { - state.advance() - } else { - state.cloudProvider = "local" - state.advance() - } + saveModelToConfig("claude-opus-4-6") + state.advance() } private func saveModelToConfig(_ model: String) { diff --git a/clients/macos/vellum-assistant/Features/Onboarding/CloudCredentialsStepView.swift b/clients/macos/vellum-assistant/Features/Onboarding/CloudCredentialsStepView.swift deleted file mode 100644 index f42f1a5fd67..00000000000 --- a/clients/macos/vellum-assistant/Features/Onboarding/CloudCredentialsStepView.swift +++ /dev/null @@ -1,486 +0,0 @@ -import VellumAssistantShared -import SwiftUI -import UniformTypeIdentifiers - -@MainActor -struct CloudCredentialsStepView: View { - @Bindable var state: OnboardingState - - @State private var assistantCli = AssistantCli() - @State private var showTitle = false - @State private var showContent = false - - @State private var gcpServiceAccountFileName: String = "" - @State private var sshPrivateKeyFileName: String = "" - @State private var qrCodeImageFileName: String = "" - - @FocusState private var arnFieldFocused: Bool - @FocusState private var projectIdFieldFocused: Bool - @FocusState private var sshHostFieldFocused: Bool - - private var isAws: Bool { - state.cloudProvider == "aws" - } - - private var isCustomHardware: Bool { - state.cloudProvider == "customHardware" - } - - var body: some View { - Text(titleText) - .font(.system(size: 32, weight: .regular, design: .serif)) - .foregroundColor(VColor.contentDefault) - .textSelection(.enabled) - .opacity(showTitle ? 1 : 0) - .offset(y: showTitle ? 0 : 8) - .padding(.bottom, VSpacing.md) - - Text(subtitleText) - .font(.system(size: 16)) - .foregroundColor(VColor.contentSecondary) - .multilineTextAlignment(.center) - .textSelection(.enabled) - .opacity(showTitle ? 1 : 0) - .offset(y: showTitle ? 0 : 8) - - Spacer() - - VStack(spacing: VSpacing.md) { - if isCustomHardware { - customHardwareFields - } else if isAws { - awsFields - } else { - gcpFields - } - - continueButton - - backButton - - OnboardingFooter(currentStep: state.currentStep, totalSteps: 4) - } - .padding(.horizontal, VSpacing.xxl) - .padding(.bottom, VSpacing.lg) - .opacity(showContent ? 1 : 0) - .offset(y: showContent ? 0 : 12) - .onAppear { - if !state.gcpServiceAccountKey.isEmpty { - gcpServiceAccountFileName = "service-account-key.json" - } - if !state.sshPrivateKey.isEmpty { - sshPrivateKeyFileName = "id_rsa" - } - if !state.customQRCodeImageData.isEmpty { - qrCodeImageFileName = "qr-code.png" - } else if isCustomHardware { - // Auto-detect QR code PNG from the well-known XDG data path - // where fetch-qr-code.sh places it after SCP from the Mac mini. - let xdgDataHome = ProcessInfo.processInfo.environment["XDG_DATA_HOME"] - ?? (FileManager.default.homeDirectoryForCurrentUser.path + "/.local/share") - let qrPath = URL(fileURLWithPath: xdgDataHome) - .appendingPathComponent("vellum/pairing-qr/initial.png") - if let data = try? Data(contentsOf: qrPath), !data.isEmpty { - state.customQRCodeImageData = data - qrCodeImageFileName = "initial.png" - } - } - withAnimation(.easeOut(duration: 0.5).delay(0.1)) { - showTitle = true - } - withAnimation(.easeOut(duration: 0.5).delay(0.3)) { - showContent = true - } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) { - if isCustomHardware { - // No text field to focus for custom hardware — just the file picker - } else if isAws { - arnFieldFocused = true - } else { - projectIdFieldFocused = true - } - } - } - } - - // MARK: - AWS Fields - - private var awsFields: some View { - VStack(spacing: VSpacing.sm) { - awsSetupBlurb - - VStack(alignment: .leading, spacing: VSpacing.xs) { - Text("IAM Role ARN") - .font(.system(size: 13, weight: .medium)) - .foregroundColor(VColor.contentSecondary) - TextField("arn:aws:iam::123456789012:role/VellumAssistantRole", text: $state.awsRoleArn) - .textFieldStyle(.plain) - .font(.system(size: 14, weight: .medium, design: .monospaced)) - .foregroundColor(VColor.contentDefault) - .padding(.horizontal, VSpacing.lg) - .padding(.vertical, VSpacing.md) - .background( - RoundedRectangle(cornerRadius: VRadius.lg) - .stroke(VColor.borderBase, lineWidth: 1) - ) - .focused($arnFieldFocused) - .onSubmit { - saveAndContinue() - } - } - } - } - - // MARK: - Custom Hardware Fields - - private var customHardwareFields: some View { - VStack(spacing: VSpacing.sm) { - customHardwareSetupBlurb - - VStack(alignment: .leading, spacing: VSpacing.xs) { - Text("QR Code Image") - .font(.system(size: 13, weight: .medium)) - .foregroundColor(VColor.contentSecondary) - filePickerButton( - fileName: qrCodeImageFileName, - prompt: "Select QR Code PNG", - onPick: { pickQRCodeImageFile() }, - onClear: { - state.customQRCodeImageData = Data() - qrCodeImageFileName = "" - } - ) - } - - } - } - - private static let gcpZones = [ - "us-central1-a", - "us-east1-b", - "us-east4-a", - "us-west1-a", - "us-west2-a", - ] - - // MARK: - GCP Fields - - private var gcpFields: some View { - VStack(spacing: VSpacing.sm) { - gcpSetupBlurb - - VStack(alignment: .leading, spacing: VSpacing.xs) { - Text("Project ID") - .font(.system(size: 13, weight: .medium)) - .foregroundColor(VColor.contentSecondary) - TextField("my-gcp-project-id", text: $state.gcpProjectId) - .textFieldStyle(.plain) - .font(.system(size: 14, weight: .medium, design: .monospaced)) - .foregroundColor(VColor.contentDefault) - .padding(.horizontal, VSpacing.lg) - .padding(.vertical, VSpacing.md) - .background( - RoundedRectangle(cornerRadius: VRadius.lg) - .stroke(VColor.borderBase, lineWidth: 1) - ) - .focused($projectIdFieldFocused) - } - - VStack(alignment: .leading, spacing: VSpacing.xs) { - Text("Zone") - .font(.system(size: 13, weight: .medium)) - .foregroundColor(VColor.contentSecondary) - Picker("", selection: $state.gcpZone) { - ForEach(Self.gcpZones, id: \.self) { zone in - Text(zone).tag(zone) - } - } - .pickerStyle(.menu) - .labelsHidden() - .font(.system(size: 14, weight: .medium, design: .monospaced)) - .foregroundColor(VColor.contentDefault) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.horizontal, VSpacing.sm) - .padding(.vertical, VSpacing.xs) - .background( - RoundedRectangle(cornerRadius: VRadius.lg) - .stroke(VColor.borderBase, lineWidth: 1) - ) - } - - VStack(alignment: .leading, spacing: VSpacing.xs) { - Text("Service Account Key (JSON)") - .font(.system(size: 13, weight: .medium)) - .foregroundColor(VColor.contentSecondary) - filePickerButton( - fileName: gcpServiceAccountFileName, - prompt: "Select Service Account JSON File", - onPick: { pickGCPServiceAccountFile() }, - onClear: { - state.gcpServiceAccountKey = "" - gcpServiceAccountFileName = "" - } - ) - } - } - } - - private var awsSetupBlurb: some View { - VStack(alignment: .leading, spacing: VSpacing.sm) { - Text("Before continuing, set up the following in the AWS Console:") - .font(.system(size: 13, weight: .medium)) - .foregroundColor(VColor.contentSecondary) - VStack(alignment: .leading, spacing: VSpacing.xs) { - setupStep("1. Create an IAM role with EC2 full access permissions (e.g., AmazonEC2FullAccess).") - setupStep("2. Configure the role's trust policy to allow Vellum to assume it.") - setupStep("3. Ensure your account has a default VPC in the target region.") - } - Link(destination: URL(string: "https://console.aws.amazon.com/iam/home#/roles")!) { - Text("Open AWS IAM Console") - .font(.system(size: 12, weight: .medium)) - .foregroundColor(VColor.primaryBase) - } - .pointerCursor() - } - .padding(VSpacing.md) - .frame(maxWidth: .infinity, alignment: .leading) - .textSelection(.enabled) - .background( - RoundedRectangle(cornerRadius: VRadius.lg) - .fill(VColor.surfaceActive) - ) - } - - private var customHardwareSetupBlurb: some View { - VStack(alignment: .leading, spacing: VSpacing.sm) { - Text("Set up your Mac mini, then upload the QR code:") - .font(.system(size: 13, weight: .medium)) - .foregroundColor(VColor.contentSecondary) - VStack(alignment: .leading, spacing: VSpacing.xs) { - setupStep("1. On your Mac mini, run: curl -fsSL https://assistant.vellum.ai/install.sh | bash") - setupStep("2. Upload the QR code PNG generated by the install script below.") - } - } - .padding(VSpacing.md) - .frame(maxWidth: .infinity, alignment: .leading) - .textSelection(.enabled) - .background( - RoundedRectangle(cornerRadius: VRadius.lg) - .fill(VColor.surfaceActive) - ) - } - - private var gcpSetupBlurb: some View { - VStack(alignment: .leading, spacing: VSpacing.sm) { - Text("Before continuing, set up the following in the Google Cloud Console:") - .font(.system(size: 13, weight: .medium)) - .foregroundColor(VColor.contentSecondary) - VStack(alignment: .leading, spacing: VSpacing.xs) { - setupStep("1. Create or select a GCP project with the Compute Engine API enabled.") - setupStep("2. Create a Service Account with the Compute Admin role.") - setupStep("3. Generate a JSON key for the service account and download it.") - } - Link(destination: URL(string: "https://console.cloud.google.com/iam-admin/serviceaccounts")!) { - Text("Open Google Cloud Console") - .font(.system(size: 12, weight: .medium)) - .foregroundColor(VColor.primaryBase) - } - .pointerCursor() - } - .padding(VSpacing.md) - .frame(maxWidth: .infinity, alignment: .leading) - .textSelection(.enabled) - .background( - RoundedRectangle(cornerRadius: VRadius.lg) - .fill(VColor.surfaceActive) - ) - } - - private func setupStep(_ text: String) -> some View { - Text(text) - .font(.system(size: 12)) - .foregroundColor(VColor.contentTertiary) - } - - // MARK: - File Picker UI - - @ViewBuilder - private func filePickerButton( - fileName: String, - prompt: String, - onPick: @escaping () -> Void, - onClear: @escaping () -> Void - ) -> some View { - if fileName.isEmpty { - Button(action: onPick) { - HStack(spacing: VSpacing.sm) { - VIconView(.filePlus, size: 14) - .foregroundColor(VColor.contentSecondary) - Text(prompt) - .font(.system(size: 14, weight: .medium)) - .foregroundColor(VColor.contentSecondary) - } - .frame(maxWidth: .infinity) - .padding(.vertical, VSpacing.lg) - .background( - RoundedRectangle(cornerRadius: VRadius.lg) - .stroke(VColor.borderBase, style: StrokeStyle(lineWidth: 1, dash: [6, 3])) - ) - } - .buttonStyle(.plain) - .pointerCursor() - } else { - HStack(spacing: VSpacing.sm) { - VIconView(.file, size: 14) - .foregroundColor(VColor.primaryBase) - Text(fileName) - .font(.system(size: 14, weight: .medium, design: .monospaced)) - .foregroundColor(VColor.contentDefault) - .lineLimit(1) - .truncationMode(.middle) - .textSelection(.enabled) - Spacer() - Button(action: onClear) { - VIconView(.circleX, size: 14) - .foregroundColor(VColor.contentTertiary) - } - .buttonStyle(.plain) - .pointerCursor() - } - .padding(.horizontal, VSpacing.lg) - .padding(.vertical, VSpacing.md) - .background( - RoundedRectangle(cornerRadius: VRadius.lg) - .stroke(VColor.borderBase, lineWidth: 1) - ) - } - } - - // MARK: - File Picking - - private func pickGCPServiceAccountFile() { - let panel = NSOpenPanel() - panel.title = "Select Service Account JSON File" - panel.allowedContentTypes = [UTType.json] - panel.allowsMultipleSelection = false - panel.canChooseDirectories = false - - if panel.runModal() == .OK, let url = panel.url { - do { - let contents = try String(contentsOf: url, encoding: .utf8) - state.gcpServiceAccountKey = contents - gcpServiceAccountFileName = url.lastPathComponent - } catch { - state.gcpServiceAccountKey = "" - gcpServiceAccountFileName = "" - } - } - } - - private func pickSSHKeyFile() { - let panel = NSOpenPanel() - panel.title = "Select SSH Private Key File" - panel.allowedContentTypes = [UTType.data, UTType.plainText] - panel.allowsMultipleSelection = false - panel.canChooseDirectories = false - panel.showsHiddenFiles = true - panel.treatsFilePackagesAsDirectories = true - - if panel.runModal() == .OK, let url = panel.url { - do { - let contents = try String(contentsOf: url, encoding: .utf8) - state.sshPrivateKey = contents - sshPrivateKeyFileName = url.lastPathComponent - } catch { - state.sshPrivateKey = "" - sshPrivateKeyFileName = "" - } - } - } - - private func pickQRCodeImageFile() { - let panel = NSOpenPanel() - panel.title = "Select QR Code PNG" - panel.allowedContentTypes = [UTType.png, UTType.image] - panel.allowsMultipleSelection = false - panel.canChooseDirectories = false - - if panel.runModal() == .OK, let url = panel.url { - do { - let data = try Data(contentsOf: url) - state.customQRCodeImageData = data - qrCodeImageFileName = url.lastPathComponent - } catch { - state.customQRCodeImageData = Data() - qrCodeImageFileName = "" - } - } - } - - // MARK: - Buttons - - private var continueButton: some View { - OnboardingButton( - title: "Continue", - style: .primary, - disabled: continueDisabled - ) { - saveAndContinue() - } - } - - private var backButton: some View { - Button(action: { goBack() }) { - Text("Back") - .font(.system(size: 13)) - .foregroundColor(VColor.contentTertiary) - } - .buttonStyle(.plain) - .pointerCursor() - .padding(.top, VSpacing.xs) - } - - // MARK: - Helpers - - private var titleText: String { - if isCustomHardware { - return "Connect your hardware" - } else if isAws { - return "Connect your AWS account" - } else { - return "Connect your GCP project" - } - } - - private var subtitleText: String { - if isCustomHardware { - return "Upload the QR code from your Mac mini to pair with the assistant." - } else if isAws { - return "Provide your IAM Role ARN so we can provision resources in your AWS account." - } else { - return "Provide your project details so we can provision resources in your GCP project." - } - } - - private var continueDisabled: Bool { - if isCustomHardware { - return state.customQRCodeImageData.isEmpty - } else if isAws { - return state.awsRoleArn.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty - } else { - return state.gcpProjectId.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty - || state.gcpServiceAccountKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty - } - } - - private func goBack() { - withAnimation(.spring(duration: 0.6, bounce: 0.15)) { - state.currentStep = 1 - } - } - - private func saveAndContinue() { - guard !continueDisabled else { return } - state.advance() - } -} diff --git a/clients/macos/vellum-assistant/Features/Onboarding/ImproveExperienceStepView.swift b/clients/macos/vellum-assistant/Features/Onboarding/ImproveExperienceStepView.swift index d58e22dd9b2..fb8bfaa006e 100644 --- a/clients/macos/vellum-assistant/Features/Onboarding/ImproveExperienceStepView.swift +++ b/clients/macos/vellum-assistant/Features/Onboarding/ImproveExperienceStepView.swift @@ -97,13 +97,6 @@ struct ImproveExperienceStepView: View { UserDefaults.standard.set(true, forKey: "sendPerformanceReports") } - // Reset stale cloud provider when the user didn't go through CloudCredentials - // (e.g., user_hosted_enabled was turned off after a previous session set cloudProvider to "aws"). - // Preserve "docker" since Docker users intentionally chose that path. - if !state.needsCloudCredentials && state.cloudProvider != "local" && state.cloudProvider != "docker" { - state.cloudProvider = "local" - } - withAnimation(.easeOut(duration: 0.5).delay(0.1)) { showTitle = true } @@ -112,7 +105,7 @@ struct ImproveExperienceStepView: View { } } - OnboardingFooter(currentStep: state.currentStep, totalSteps: state.needsCloudCredentials ? 4 : 3) + OnboardingFooter(currentStep: state.currentStep, totalSteps: 3) .padding(.bottom, VSpacing.lg) } diff --git a/clients/macos/vellum-assistant/Features/Onboarding/ModelSelectionStepView.swift b/clients/macos/vellum-assistant/Features/Onboarding/ModelSelectionStepView.swift deleted file mode 100644 index e7351544f7e..00000000000 --- a/clients/macos/vellum-assistant/Features/Onboarding/ModelSelectionStepView.swift +++ /dev/null @@ -1,162 +0,0 @@ -import VellumAssistantShared -import SwiftUI - -@MainActor -struct ModelSelectionStepView: View { - @Bindable var state: OnboardingState - - private var userHostedEnabled: Bool { - MacOSClientFeatureFlagManager.shared.isEnabled("user_hosted_enabled") - } - - @State private var showTitle = false - @State private var showContent = false - - private static let models: [(id: String, name: String, detail: String)] = [ - ("claude-opus-4-6", "Opus 4.6", "Most capable"), - ("claude-sonnet-4-6", "Sonnet 4.6", "Balanced"), - ("claude-haiku-4-5-20251001", "Haiku 4.5", "Fastest"), - ] - - var body: some View { - // Title - Text("Choose your model") - .font(.system(size: 32, weight: .regular, design: .serif)) - .foregroundColor(VColor.contentDefault) - .textSelection(.enabled) - .opacity(showTitle ? 1 : 0) - .offset(y: showTitle ? 0 : 8) - .padding(.bottom, VSpacing.md) - - // Subtitle - Text("Pick the model that powers your assistant.") - .font(.system(size: 16)) - .foregroundColor(VColor.contentSecondary) - .textSelection(.enabled) - .opacity(showTitle ? 1 : 0) - .offset(y: showTitle ? 0 : 8) - - Spacer() - - // Content - VStack(spacing: VSpacing.md) { - // Model selection cards - VStack(spacing: VSpacing.sm) { - ForEach(Self.models, id: \.id) { model in - modelCard(id: model.id, name: model.name, detail: model.detail) - } - } - - // Primary button - Button(action: { saveModelAndContinue() }) { - Text("Select model") - .font(.system(size: 15, weight: .medium)) - .foregroundColor(VColor.auxWhite) - .frame(maxWidth: .infinity) - .padding(.vertical, VSpacing.lg) - .background( - RoundedRectangle(cornerRadius: VRadius.lg) - .fill(VColor.primaryBase) - ) - } - .buttonStyle(.plain) - .pointerCursor() - - Button(action: { goBack() }) { - Text("Back") - .font(.system(size: 13)) - .foregroundColor(VColor.contentTertiary) - } - .buttonStyle(.plain) - .pointerCursor() - .padding(.top, VSpacing.xs) - - OnboardingFooter(currentStep: state.currentStep, totalSteps: userHostedEnabled ? 4 : 3) - } - .padding(.horizontal, VSpacing.xxl) - .padding(.bottom, VSpacing.lg) - .opacity(showContent ? 1 : 0) - .offset(y: showContent ? 0 : 12) - .onAppear { - withAnimation(.easeOut(duration: 0.5).delay(0.1)) { - showTitle = true - } - withAnimation(.easeOut(duration: 0.5).delay(0.3)) { - showContent = true - } - } - } - - // MARK: - Model Card - - private func modelCard(id: String, name: String, detail: String) -> some View { - let isSelected = state.selectedModel == id - return Button(action: { state.selectedModel = id }) { - HStack { - VStack(alignment: .leading, spacing: 2) { - Text(name) - .font(.system(size: 15, weight: .medium)) - .foregroundColor(VColor.contentDefault) - Text(detail) - .font(.system(size: 12)) - .foregroundColor(VColor.contentSecondary) - } - Spacer() - Circle() - .fill(isSelected ? VColor.primaryBase : Color.clear) - .overlay( - Circle().stroke(isSelected ? VColor.primaryBase : VColor.borderBase, lineWidth: 1.5) - ) - .overlay( - isSelected - ? Circle().fill(VColor.auxWhite).frame(width: 6, height: 6) - : nil - ) - .frame(width: 18, height: 18) - } - .padding(.horizontal, VSpacing.lg) - .padding(.vertical, VSpacing.md) - .contentShape(Rectangle()) - .background( - RoundedRectangle(cornerRadius: VRadius.lg) - .fill(isSelected ? VColor.primaryBase.opacity(0.1) : Color.clear) - .overlay( - RoundedRectangle(cornerRadius: VRadius.lg) - .stroke(isSelected ? VColor.primaryBase.opacity(0.5) : VColor.borderBase, lineWidth: 1) - ) - ) - } - .buttonStyle(.plain) - .pointerCursor() - } - - // MARK: - Helpers - - private func goBack() { - withAnimation(.spring(duration: 0.6, bounce: 0.15)) { - if userHostedEnabled && state.cloudProvider == "local" { - state.currentStep = 1 - } else { - state.currentStep -= 1 - } - } - } - - private func saveModelAndContinue() { - state.advance() - } -} - -#Preview { - ZStack { - VColor.surfaceOverlay.ignoresSafeArea() - VStack(spacing: 0) { - ModelSelectionStepView(state: { - let s = OnboardingState() - s.currentStep = 3 - return s - }()) - } - } - .frame(width: 460, height: 620) -} diff --git a/clients/macos/vellum-assistant/Features/Onboarding/OnboardingFlowView.swift b/clients/macos/vellum-assistant/Features/Onboarding/OnboardingFlowView.swift index 58f3958db63..791de51793a 100644 --- a/clients/macos/vellum-assistant/Features/Onboarding/OnboardingFlowView.swift +++ b/clients/macos/vellum-assistant/Features/Onboarding/OnboardingFlowView.swift @@ -30,7 +30,7 @@ struct OnboardingFlowView: View { if managedSignInEnabled { return 1 } - return state.userHostedEnabled ? 3 : 2 + return 2 } var body: some View { @@ -54,9 +54,7 @@ struct OnboardingFlowView: View { .ignoresSafeArea() ) } else if (0...maxOnboardingStep).contains(state.currentStep) { - // Trimmed onboarding flow. - // When userHostedEnabled: WakeUp → APIKey → CloudCredentials → ImproveExperience (steps 0–3) - // Otherwise: WakeUp → APIKey → ImproveExperience (steps 0–2) + // Onboarding flow: WakeUp → APIKey → ImproveExperience (steps 0–2) VStack(spacing: 0) { Spacer() @@ -106,12 +104,6 @@ struct OnboardingFlowView: View { } ) case 2: - if state.needsCloudCredentials { - CloudCredentialsStepView(state: state) - } else { - ImproveExperienceStepView(state: state) - } - case 3: ImproveExperienceStepView(state: state) default: EmptyView() diff --git a/clients/macos/vellum-assistant/Features/Onboarding/OnboardingState.swift b/clients/macos/vellum-assistant/Features/Onboarding/OnboardingState.swift index 94b69dff097..905051d6582 100644 --- a/clients/macos/vellum-assistant/Features/Onboarding/OnboardingState.swift +++ b/clients/macos/vellum-assistant/Features/Onboarding/OnboardingState.swift @@ -41,12 +41,6 @@ final class OnboardingState { var cloudProvider: String = "local" var onboardingVariant: OnboardingVariant = .default - /// Whether the user's hosting choice requires cloud credentials (not local/docker). - var needsCloudCredentials: Bool { - let userHosted = MacOSClientFeatureFlagManager.shared.isEnabled("user_hosted_enabled") - return userHosted && cloudProvider != "local" && cloudProvider != "docker" - } - /// When false, step changes are not written to UserDefaults (used by auth gate). var shouldPersist: Bool = true @@ -143,12 +137,6 @@ final class OnboardingState { // Clamp restored step to the variant's maximum to prevent out-of-range // rendering (e.g. a step saved from the 8-step default flow would be // invalid for the 5-step first-meeting flow). - // Default onboarding now exits immediately after the first post-hatch - // conversation entry point (step 2). Prevent stale persisted indices - // from reopening legacy permission-request steps. - // When userHostedEnabled is on and a cloud provider is selected, the flow - // has 4 steps (0–3); otherwise it stays at 3 steps (0–2). - let hasCloudStep = MacOSClientFeatureFlagManager.shared.isEnabled("user_hosted_enabled") && cloudProvider != "local" && cloudProvider != "docker" let isManagedSignIn = MacOSClientFeatureFlagManager.shared.isEnabled("managed_sign_in_enabled") let maxStep: Int if isManagedSignIn { @@ -156,7 +144,7 @@ final class OnboardingState { } else if onboardingVariant == .firstMeeting { maxStep = 4 } else { - maxStep = hasCloudStep ? 3 : 2 + maxStep = 2 } if currentStep > maxStep { currentStep = maxStep diff --git a/scripts/fetch-qr-code.sh b/scripts/fetch-qr-code.sh index 94f6fdae818..c2c4b085853 100755 --- a/scripts/fetch-qr-code.sh +++ b/scripts/fetch-qr-code.sh @@ -2,7 +2,7 @@ # # fetch-qr-code.sh — SCP the pairing QR code PNG from a Mac mini to this machine. # -# After running `curl -fsSL https://assistant.vellum.ai/install.sh | bash` on a +# After running `curl -fsSL https://vellum.ai/install.sh | bash` on a # Mac mini, this script copies the generated QR code PNG to a well-known local # XDG data path so the Desktop app can auto-detect it for pairing. # From 1373863fd8d66cd7753652d2bf4a5733e0b7bfaf Mon Sep 17 00:00:00 2001 From: "vargas@vellum.ai" Date: Sat, 14 Mar 2026 22:20:21 +0000 Subject: [PATCH 2/3] fix: bump flow version and guard onSubmit against primaryButtonDisabled - Bump currentFlowVersion to 10 so users upgrading from old step layout get their persisted step reset instead of landing on a stale index - Add primaryButtonDisabled guard to onSubmit handler to prevent Enter from bypassing cloud credential field validation Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../vellum-assistant/Features/Onboarding/APIKeyStepView.swift | 1 + .../vellum-assistant/Features/Onboarding/OnboardingState.swift | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/clients/macos/vellum-assistant/Features/Onboarding/APIKeyStepView.swift b/clients/macos/vellum-assistant/Features/Onboarding/APIKeyStepView.swift index 52287a0a27c..b9e4653d229 100644 --- a/clients/macos/vellum-assistant/Features/Onboarding/APIKeyStepView.swift +++ b/clients/macos/vellum-assistant/Features/Onboarding/APIKeyStepView.swift @@ -254,6 +254,7 @@ struct APIKeyStepView: View { guard !hatchButtonDisabled else { return } saveAndHatch() } else { + guard !primaryButtonDisabled else { return } saveAndContinue() } } diff --git a/clients/macos/vellum-assistant/Features/Onboarding/OnboardingState.swift b/clients/macos/vellum-assistant/Features/Onboarding/OnboardingState.swift index 905051d6582..42568ba1c08 100644 --- a/clients/macos/vellum-assistant/Features/Onboarding/OnboardingState.swift +++ b/clients/macos/vellum-assistant/Features/Onboarding/OnboardingState.swift @@ -27,7 +27,7 @@ enum ActivationKey: String, CaseIterable { final class OnboardingState { /// Bump this version whenever the default-flow step order changes so that /// persisted step indices from a previous layout are not consumed as-is. - private static let currentFlowVersion = 9 + private static let currentFlowVersion = 10 var currentStep: Int = 0 var assistantName: String = "Velly" From 10bf106938b29bf6f74c4453f8c6e7f1963819cd Mon Sep 17 00:00:00 2001 From: "vargas@vellum.ai" Date: Sat, 14 Mar 2026 22:26:28 +0000 Subject: [PATCH 3/3] chore: remove unused userHostedEnabled and managedSignInEnabled from OnboardingState MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These properties are no longer referenced — each view reads the flags directly from MacOSClientFeatureFlagManager.shared via local computed properties. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../Features/Onboarding/OnboardingState.swift | 8 -------- 1 file changed, 8 deletions(-) diff --git a/clients/macos/vellum-assistant/Features/Onboarding/OnboardingState.swift b/clients/macos/vellum-assistant/Features/Onboarding/OnboardingState.swift index 42568ba1c08..73bd3811286 100644 --- a/clients/macos/vellum-assistant/Features/Onboarding/OnboardingState.swift +++ b/clients/macos/vellum-assistant/Features/Onboarding/OnboardingState.swift @@ -74,14 +74,6 @@ final class OnboardingState { !speechGranted || !accessibilityGranted || !screenGranted } - var userHostedEnabled: Bool { - MacOSClientFeatureFlagManager.shared.isEnabled("user_hosted_enabled") - } - - var managedSignInEnabled: Bool { - MacOSClientFeatureFlagManager.shared.isEnabled("managed_sign_in_enabled") - } - /// Continuous crack progress (0.0–1.0) derived from step and permission state. /// For the first meeting variant, uses a timer-driven stored property instead. var crackProgress: CGFloat {