Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 110 additions & 36 deletions app/macos/Sources/OpenClawLib/LauncherViews.swift
Original file line number Diff line number Diff line change
Expand Up @@ -339,13 +339,13 @@ struct SetupView: View {
// Bottom actions
VStack(spacing: 12) {
if launcher.state == .needsAuth {
AuthChoiceView(launcher: launcher)
ProviderSelectionView(launcher: launcher)
} else if launcher.state == .selectingProvider {
AuthMethodView(launcher: launcher)
} else if launcher.state == .waitingForApiKey {
ApiKeyInputView(launcher: launcher)
} else if launcher.state == .waitingForOAuthCode {
if launcher.showApiKeyField {
ApiKeyInputView(launcher: launcher)
} else {
OAuthCodeInputView(launcher: launcher)
}
OAuthCodeInputView(launcher: launcher)
} else if launcher.state == .stopped {
Button("Start OpenClaw") {
launcher.start()
Expand Down Expand Up @@ -380,65 +380,139 @@ struct SetupView: View {
}
}

struct AuthChoiceView: View {
// MARK: - Provider Selection View

struct ProviderSelectionView: View {
@ObservedObject var launcher: OpenClawLauncher

var body: some View {
VStack(spacing: 12) {
Text("Authentication")
VStack(spacing: 16) {
Text("Connect AI Model")
.font(.system(size: 14, weight: .semibold))
Text("Choose how to connect to Anthropic.")
Text("Choose your AI provider to get started.")
.font(.system(size: 11))
.foregroundStyle(.secondary)

VStack(spacing: 8) {
Button("Sign in with Claude") {
launcher.startOAuth()
}
.buttonStyle(.borderedProminent)
.controlSize(.large)

Button("Use API Key") {
launcher.showApiKeyInput()
ForEach(AuthProvider.allCases) { provider in
Button(action: { launcher.selectProvider(provider) }) {
HStack {
Text(provider.displayName)
.fontWeight(.medium)
Spacer()
Image(systemName: "chevron.right")
.font(.system(size: 12))
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity)
.padding(.horizontal, 16)
.frame(height: 44)
}
.buttonStyle(.bordered)
.controlSize(.large)
}
.buttonStyle(.bordered)
.controlSize(.large)

Button("Skip") {
Button("Skip for now") {
launcher.skipAuth()
}
.buttonStyle(.borderless)
.foregroundStyle(.secondary)
.font(.system(size: 12))
.padding(.top, 4)
}
.frame(maxWidth: 300)

Text("You can always change this in the Control UI later.")
.font(.system(size: 10))
.foregroundStyle(.tertiary)
}
}
}

// MARK: - Auth Method View (OAuth vs API Key for providers that support both)

struct AuthMethodView: View {
@ObservedObject var launcher: OpenClawLauncher

var body: some View {
VStack(spacing: 12) {
if let provider = launcher.selectedProvider {
Text(provider.displayName)
.font(.system(size: 14, weight: .semibold))
Text(provider.description)
.font(.system(size: 11))
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)

VStack(spacing: 8) {
if provider.supportsOAuth {
Button("Sign in with Claude") {
launcher.startOAuthForProvider()
}
.buttonStyle(.borderedProminent)
.controlSize(.large)

Button("Use API Key") {
launcher.showApiKeyInputForProvider()
}
.buttonStyle(.bordered)
.controlSize(.large)
} else {
Button("Use API Key") {
launcher.showApiKeyInputForProvider()
}
.buttonStyle(.borderedProminent)
.controlSize(.large)
}

Button("Back") {
launcher.backToProviderSelection()
}
.buttonStyle(.borderless)
.foregroundStyle(.secondary)
.font(.system(size: 12))
}
}
}
}
}

// MARK: - API Key Input View

struct ApiKeyInputView: View {
@ObservedObject var launcher: OpenClawLauncher

var body: some View {
VStack(spacing: 12) {
Text("API Key Setup")
.font(.system(size: 14, weight: .semibold))
Text("Enter your Anthropic API key.")
.font(.system(size: 11))
.foregroundStyle(.secondary)
SecureField("sk-ant-...", text: $launcher.apiKeyInput)
.textFieldStyle(.roundedBorder)
.font(.system(size: 12, design: .monospaced))
.frame(maxWidth: 360)
HStack(spacing: 12) {
Button("Continue") { launcher.submitApiKey() }
.buttonStyle(.borderedProminent).controlSize(.large)
Button("Back") { launcher.state = .needsAuth }
.buttonStyle(.bordered).controlSize(.large)
if let provider = launcher.selectedProvider {
Text("\(provider.displayName) API Key")
.font(.system(size: 14, weight: .semibold))
Text("Enter your \(provider.rawValue) API key.")
.font(.system(size: 11))
.foregroundStyle(.secondary)
SecureField(provider.apiKeyPlaceholder, text: $launcher.apiKeyInput)
.textFieldStyle(.roundedBorder)
.font(.system(size: 12, design: .monospaced))
.frame(maxWidth: 360)
HStack(spacing: 12) {
Button("Continue") { launcher.submitApiKey() }
.buttonStyle(.borderedProminent).controlSize(.large)
Button("Back") {
if launcher.selectedProvider?.supportsOAuth == true {
launcher.state = .selectingProvider
} else {
launcher.backToProviderSelection()
}
}
.buttonStyle(.bordered).controlSize(.large)
}
}
}
}
}

// MARK: - OAuth Code Input View

struct OAuthCodeInputView: View {
@ObservedObject var launcher: OpenClawLauncher

Expand All @@ -458,7 +532,7 @@ struct OAuthCodeInputView: View {
Button("Exchange") { launcher.exchangeOAuthCode() }
.buttonStyle(.borderedProminent).controlSize(.large)
.disabled(launcher.oauthCodeInput.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
Button("Back") { launcher.state = .needsAuth }
Button("Back") { launcher.state = .selectingProvider }
.buttonStyle(.bordered).controlSize(.large)
}
}
Expand Down
51 changes: 50 additions & 1 deletion app/macos/Sources/OpenClawLib/Models.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,59 @@ import Foundation

public enum StepStatus { case pending, running, done, error, warning }

public enum LauncherState { case idle, working, needsAuth, waitingForOAuthCode, running, stopped, error }
public enum LauncherState { case idle, working, needsAuth, selectingProvider, waitingForApiKey, waitingForOAuthCode, running, stopped, error }

public enum MenuBarStatus { case starting, running, stopped }

// MARK: - Auth Providers

public enum AuthProvider: String, CaseIterable, Identifiable {
case anthropic = "Anthropic"
case openai = "OpenAI"
case google = "Google AI"

public var id: String { rawValue }

public var displayName: String {
switch self {
case .anthropic: return "Claude (Anthropic)"
case .openai: return "GPT (OpenAI)"
case .google: return "Gemini (Google AI)"
}
}

public var description: String {
switch self {
case .anthropic: return "Sign in with your Claude account or use an API key"
case .openai: return "Enter your OpenAI API key"
case .google: return "Enter your Google AI API key"
}
}

public var supportsOAuth: Bool {
switch self {
case .anthropic: return true
case .openai, .google: return false
}
}

public var apiKeyPrefix: String {
switch self {
case .anthropic: return "sk-ant-"
case .openai: return "sk-"
case .google: return "" // Google AI keys don't have a standard prefix
}
}

public var apiKeyPlaceholder: String {
switch self {
case .anthropic: return "sk-ant-api..."
case .openai: return "sk-proj-..."
case .google: return "AIza..."
}
}
}

public struct GatewayStatus: Codable {
public let uptime: Int?
}
Expand Down
Loading