mirror of https://github.com/openclaw/openclaw.git
Improve macOS onboarding UX and gateway setup
This commit is contained in:
parent
f4aff83c51
commit
01a6e0da81
|
|
@ -2,6 +2,13 @@ import Foundation
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
enum CLIInstaller {
|
enum CLIInstaller {
|
||||||
|
struct PreflightStatus: Equatable {
|
||||||
|
let needsCommandLineTools: Bool
|
||||||
|
let message: String?
|
||||||
|
|
||||||
|
static let ready = PreflightStatus(needsCommandLineTools: false, message: nil)
|
||||||
|
}
|
||||||
|
|
||||||
static func installedLocation() -> String? {
|
static func installedLocation() -> String? {
|
||||||
self.installedLocation(
|
self.installedLocation(
|
||||||
searchPaths: CommandResolver.preferredPaths(),
|
searchPaths: CommandResolver.preferredPaths(),
|
||||||
|
|
@ -34,6 +41,50 @@ enum CLIInstaller {
|
||||||
self.installedLocation() != nil
|
self.installedLocation() != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func preflight() async -> PreflightStatus {
|
||||||
|
let response = await ShellExecutor.runDetailed(
|
||||||
|
command: ["/usr/bin/xcode-select", "-p"],
|
||||||
|
cwd: nil,
|
||||||
|
env: nil,
|
||||||
|
timeout: 10)
|
||||||
|
|
||||||
|
guard response.success else {
|
||||||
|
return PreflightStatus(
|
||||||
|
needsCommandLineTools: true,
|
||||||
|
message: """
|
||||||
|
Apple Developer Tools are required before OpenClaw can install the CLI.
|
||||||
|
Install them first, then come back and click “I've Installed It, Recheck”.
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
|
||||||
|
return .ready
|
||||||
|
}
|
||||||
|
|
||||||
|
static func requestCommandLineToolsInstall(
|
||||||
|
statusHandler: @escaping @MainActor @Sendable (String) async -> Void
|
||||||
|
) async {
|
||||||
|
await statusHandler("Opening Apple developer tools installer…")
|
||||||
|
let response = await ShellExecutor.runDetailed(
|
||||||
|
command: ["/usr/bin/xcode-select", "--install"],
|
||||||
|
cwd: nil,
|
||||||
|
env: nil,
|
||||||
|
timeout: 10)
|
||||||
|
|
||||||
|
let combined = [response.stdout, response.stderr]
|
||||||
|
.joined(separator: "\n")
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
.lowercased()
|
||||||
|
|
||||||
|
if combined.contains("already installed") || combined.contains("softwareupdate") {
|
||||||
|
await statusHandler(
|
||||||
|
"Apple Developer Tools installer is already open or installed. Finish that step, then click “I've Installed It, Recheck”.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await statusHandler(
|
||||||
|
"Complete Apple's developer tools installer dialog, then click “I've Installed It, Recheck”.")
|
||||||
|
}
|
||||||
|
|
||||||
static func install(statusHandler: @escaping @MainActor @Sendable (String) async -> Void) async {
|
static func install(statusHandler: @escaping @MainActor @Sendable (String) async -> Void) async {
|
||||||
let expected = GatewayEnvironment.expectedGatewayVersionString() ?? "latest"
|
let expected = GatewayEnvironment.expectedGatewayVersionString() ?? "latest"
|
||||||
let prefix = Self.installPrefix()
|
let prefix = Self.installPrefix()
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,10 @@ enum CronCustomSessionTarget: Codable, Equatable {
|
||||||
case predefined(CronSessionTarget)
|
case predefined(CronSessionTarget)
|
||||||
case session(id: String)
|
case session(id: String)
|
||||||
|
|
||||||
|
static let main: CronCustomSessionTarget = .predefined(.main)
|
||||||
|
static let isolated: CronCustomSessionTarget = .predefined(.isolated)
|
||||||
|
static let current: CronCustomSessionTarget = .predefined(.current)
|
||||||
|
|
||||||
var rawValue: String {
|
var rawValue: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .predefined(let target):
|
case .predefined(let target):
|
||||||
|
|
@ -254,6 +258,38 @@ struct CronJob: Identifiable, Codable, Equatable {
|
||||||
case state
|
case state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init(
|
||||||
|
id: String,
|
||||||
|
agentId: String?,
|
||||||
|
name: String,
|
||||||
|
description: String?,
|
||||||
|
enabled: Bool,
|
||||||
|
deleteAfterRun: Bool?,
|
||||||
|
createdAtMs: Int,
|
||||||
|
updatedAtMs: Int,
|
||||||
|
schedule: CronSchedule,
|
||||||
|
sessionTarget: CronCustomSessionTarget,
|
||||||
|
wakeMode: CronWakeMode,
|
||||||
|
payload: CronPayload,
|
||||||
|
delivery: CronDelivery?,
|
||||||
|
state: CronJobState
|
||||||
|
) {
|
||||||
|
self.id = id
|
||||||
|
self.agentId = agentId
|
||||||
|
self.name = name
|
||||||
|
self.description = description
|
||||||
|
self.enabled = enabled
|
||||||
|
self.deleteAfterRun = deleteAfterRun
|
||||||
|
self.createdAtMs = createdAtMs
|
||||||
|
self.updatedAtMs = updatedAtMs
|
||||||
|
self.schedule = schedule
|
||||||
|
self.sessionTargetRaw = sessionTarget.rawValue
|
||||||
|
self.wakeMode = wakeMode
|
||||||
|
self.payload = payload
|
||||||
|
self.delivery = delivery
|
||||||
|
self.state = state
|
||||||
|
}
|
||||||
|
|
||||||
/// Parsed session target (predefined or custom session ID)
|
/// Parsed session target (predefined or custom session ID)
|
||||||
var parsedSessionTarget: CronCustomSessionTarget {
|
var parsedSessionTarget: CronCustomSessionTarget {
|
||||||
CronCustomSessionTarget.from(self.sessionTargetRaw)
|
CronCustomSessionTarget.from(self.sessionTargetRaw)
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ struct CronSettings_Previews: PreviewProvider {
|
||||||
createdAtMs: 0,
|
createdAtMs: 0,
|
||||||
updatedAtMs: 0,
|
updatedAtMs: 0,
|
||||||
schedule: .every(everyMs: 86_400_000, anchorMs: nil),
|
schedule: .every(everyMs: 86_400_000, anchorMs: nil),
|
||||||
sessionTarget: .isolated,
|
sessionTarget: .predefined(.isolated),
|
||||||
wakeMode: .now,
|
wakeMode: .now,
|
||||||
payload: .agentTurn(
|
payload: .agentTurn(
|
||||||
message: "Summarize inbox",
|
message: "Summarize inbox",
|
||||||
|
|
@ -69,7 +69,7 @@ extension CronSettings {
|
||||||
createdAtMs: 1_700_000_000_000,
|
createdAtMs: 1_700_000_000_000,
|
||||||
updatedAtMs: 1_700_000_100_000,
|
updatedAtMs: 1_700_000_100_000,
|
||||||
schedule: .cron(expr: "0 8 * * *", tz: "UTC"),
|
schedule: .cron(expr: "0 8 * * *", tz: "UTC"),
|
||||||
sessionTarget: .isolated,
|
sessionTarget: .predefined(.isolated),
|
||||||
wakeMode: .nextHeartbeat,
|
wakeMode: .nextHeartbeat,
|
||||||
payload: .agentTurn(
|
payload: .agentTurn(
|
||||||
message: "Summarize",
|
message: "Summarize",
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,18 @@ enum GatewayEnvironment {
|
||||||
}
|
}
|
||||||
|
|
||||||
static func expectedGatewayVersionString() -> String? {
|
static func expectedGatewayVersionString() -> String? {
|
||||||
let bundleVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
|
self.expectedGatewayVersionString(
|
||||||
|
bundleVersion: Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String,
|
||||||
|
bundleIdentifier: Bundle.main.bundleIdentifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func expectedGatewayVersionString(bundleVersion: String?, bundleIdentifier: String?) -> String? {
|
||||||
|
if let bundleIdentifier,
|
||||||
|
bundleIdentifier.trimmingCharacters(in: .whitespacesAndNewlines).hasSuffix(".debug")
|
||||||
|
{
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
let trimmed = bundleVersion?.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmed = bundleVersion?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
return (trimmed?.isEmpty == false) ? trimmed : nil
|
return (trimmed?.isEmpty == false) ? trimmed : nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import Observation
|
import Observation
|
||||||
|
import OpenClawKit
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@Observable
|
@Observable
|
||||||
|
|
@ -196,13 +197,9 @@ final class GatewayProcessManager {
|
||||||
let instanceText = instance.map { self.describe(instance: $0) }
|
let instanceText = instance.map { self.describe(instance: $0) }
|
||||||
let hasListener = instance != nil
|
let hasListener = instance != nil
|
||||||
|
|
||||||
let attemptAttach = {
|
|
||||||
try await self.connection.requestRaw(method: .health, timeoutMs: 2000)
|
|
||||||
}
|
|
||||||
|
|
||||||
for attempt in 0..<(hasListener ? 3 : 1) {
|
for attempt in 0..<(hasListener ? 3 : 1) {
|
||||||
do {
|
do {
|
||||||
let data = try await attemptAttach()
|
let data = try await self.probeLocalGatewayHealth(timeoutMs: 2000)
|
||||||
let snap = decodeHealthSnapshot(from: data)
|
let snap = decodeHealthSnapshot(from: data)
|
||||||
let details = self.describe(details: instanceText, port: port, snap: snap)
|
let details = self.describe(details: instanceText, port: port, snap: snap)
|
||||||
self.existingGatewayDetails = details
|
self.existingGatewayDetails = details
|
||||||
|
|
@ -337,7 +334,7 @@ final class GatewayProcessManager {
|
||||||
while Date() < deadline {
|
while Date() < deadline {
|
||||||
if !self.desiredActive { return }
|
if !self.desiredActive { return }
|
||||||
do {
|
do {
|
||||||
_ = try await self.connection.requestRaw(method: .health, timeoutMs: 1500)
|
_ = try await self.probeLocalGatewayHealth(timeoutMs: 1500)
|
||||||
let instance = await PortGuardian.shared.describe(port: port)
|
let instance = await PortGuardian.shared.describe(port: port)
|
||||||
let details = instance.map { "pid \($0.pid)" }
|
let details = instance.map { "pid \($0.pid)" }
|
||||||
self.clearLastFailure()
|
self.clearLastFailure()
|
||||||
|
|
@ -380,7 +377,7 @@ final class GatewayProcessManager {
|
||||||
while Date() < deadline {
|
while Date() < deadline {
|
||||||
if !self.desiredActive { return false }
|
if !self.desiredActive { return false }
|
||||||
do {
|
do {
|
||||||
_ = try await self.connection.requestRaw(method: .health, timeoutMs: 1500)
|
_ = try await self.probeLocalGatewayHealth(timeoutMs: 1500)
|
||||||
self.clearLastFailure()
|
self.clearLastFailure()
|
||||||
return true
|
return true
|
||||||
} catch {
|
} catch {
|
||||||
|
|
@ -413,6 +410,20 @@ final class GatewayProcessManager {
|
||||||
if text.count <= limit { return text }
|
if text.count <= limit { return text }
|
||||||
return String(text.suffix(limit))
|
return String(text.suffix(limit))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func probeLocalGatewayHealth(timeoutMs: Double) async throws -> Data {
|
||||||
|
let config = GatewayEndpointStore.localConfig()
|
||||||
|
let channel = GatewayChannelActor(
|
||||||
|
url: config.url,
|
||||||
|
token: config.token,
|
||||||
|
password: config.password)
|
||||||
|
defer {
|
||||||
|
Task {
|
||||||
|
await channel.shutdown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return try await channel.request(method: GatewayConnection.Method.health.rawValue, params: nil, timeoutMs: timeoutMs)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ final class MacNodeModeCoordinator {
|
||||||
var retryDelay: UInt64 = 1_000_000_000
|
var retryDelay: UInt64 = 1_000_000_000
|
||||||
var lastCameraEnabled: Bool?
|
var lastCameraEnabled: Bool?
|
||||||
var lastBrowserControlEnabled: Bool?
|
var lastBrowserControlEnabled: Bool?
|
||||||
|
var lastBlockedOnOnboarding = false
|
||||||
let defaults = UserDefaults.standard
|
let defaults = UserDefaults.standard
|
||||||
|
|
||||||
while !Task.isCancelled {
|
while !Task.isCancelled {
|
||||||
|
|
@ -41,6 +42,20 @@ final class MacNodeModeCoordinator {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let onboardingComplete = Self.shouldConnectNodeMode(
|
||||||
|
onboardingSeen: defaults.bool(forKey: onboardingSeenKey),
|
||||||
|
onboardingVersion: defaults.integer(forKey: onboardingVersionKey))
|
||||||
|
if !onboardingComplete {
|
||||||
|
if !lastBlockedOnOnboarding {
|
||||||
|
self.logger.info("mac node waiting for onboarding completion")
|
||||||
|
lastBlockedOnOnboarding = true
|
||||||
|
}
|
||||||
|
await self.session.disconnect()
|
||||||
|
try? await Task.sleep(nanoseconds: 1_000_000_000)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
lastBlockedOnOnboarding = false
|
||||||
|
|
||||||
let cameraEnabled = defaults.object(forKey: cameraEnabledKey) as? Bool ?? false
|
let cameraEnabled = defaults.object(forKey: cameraEnabledKey) as? Bool ?? false
|
||||||
if lastCameraEnabled == nil {
|
if lastCameraEnabled == nil {
|
||||||
lastCameraEnabled = cameraEnabled
|
lastCameraEnabled = cameraEnabled
|
||||||
|
|
@ -116,6 +131,10 @@ final class MacNodeModeCoordinator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func shouldConnectNodeMode(onboardingSeen: Bool, onboardingVersion: Int) -> Bool {
|
||||||
|
onboardingSeen && onboardingVersion >= currentOnboardingVersion
|
||||||
|
}
|
||||||
|
|
||||||
private func currentCaps() -> [String] {
|
private func currentCaps() -> [String] {
|
||||||
var caps: [String] = [OpenClawCapability.canvas.rawValue, OpenClawCapability.screen.rawValue]
|
var caps: [String] = [OpenClawCapability.canvas.rawValue, OpenClawCapability.screen.rawValue]
|
||||||
if OpenClawConfigFile.browserControlEnabled() {
|
if OpenClawConfigFile.browserControlEnabled() {
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,8 @@ struct OnboardingView: View {
|
||||||
@State var isRequesting = false
|
@State var isRequesting = false
|
||||||
@State var installingCLI = false
|
@State var installingCLI = false
|
||||||
@State var cliStatus: String?
|
@State var cliStatus: String?
|
||||||
|
@State var cliPreflightStatus: String?
|
||||||
|
@State var cliNeedsCommandLineTools = false
|
||||||
@State var copied = false
|
@State var copied = false
|
||||||
@State var monitoringPermissions = false
|
@State var monitoringPermissions = false
|
||||||
@State var monitoringDiscovery = false
|
@State var monitoringDiscovery = false
|
||||||
|
|
@ -97,6 +99,7 @@ struct OnboardingView: View {
|
||||||
let pageWidth: CGFloat = Self.windowWidth
|
let pageWidth: CGFloat = Self.windowWidth
|
||||||
let contentHeight: CGFloat = 460
|
let contentHeight: CGFloat = 460
|
||||||
let connectionPageIndex = 1
|
let connectionPageIndex = 1
|
||||||
|
let cliPageIndex = 6
|
||||||
let wizardPageIndex = 3
|
let wizardPageIndex = 3
|
||||||
let onboardingChatPageIndex = 8
|
let onboardingChatPageIndex = 8
|
||||||
|
|
||||||
|
|
@ -113,7 +116,7 @@ struct OnboardingView: View {
|
||||||
case .unconfigured:
|
case .unconfigured:
|
||||||
showOnboardingChat ? [0, 1, 8, 9] : [0, 1, 9]
|
showOnboardingChat ? [0, 1, 8, 9] : [0, 1, 9]
|
||||||
case .local:
|
case .local:
|
||||||
showOnboardingChat ? [0, 1, 3, 5, 8, 9] : [0, 1, 3, 5, 9]
|
showOnboardingChat ? [0, 1, 6, 3, 5, 8, 9] : [0, 1, 6, 3, 5, 9]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -146,7 +149,10 @@ struct OnboardingView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
var canAdvance: Bool {
|
var canAdvance: Bool {
|
||||||
!self.isWizardBlocking
|
if self.activePageIndex == self.cliPageIndex {
|
||||||
|
return self.cliInstalled && !self.installingCLI
|
||||||
|
}
|
||||||
|
return !self.isWizardBlocking
|
||||||
}
|
}
|
||||||
|
|
||||||
var devLinkCommand: String {
|
var devLinkCommand: String {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,10 @@ import SwiftUI
|
||||||
extension OnboardingView {
|
extension OnboardingView {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
GlowingOpenClawIcon(size: 130, glowIntensity: 0.28)
|
Image(nsImage: NSApp.applicationIconImage)
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 130, height: 130)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 130 * 0.22, style: .continuous))
|
||||||
.offset(y: 10)
|
.offset(y: 10)
|
||||||
.frame(height: 145)
|
.frame(height: 145)
|
||||||
|
|
||||||
|
|
@ -46,10 +49,6 @@ extension OnboardingView {
|
||||||
self.currentPage = max(0, self.pageOrder.count - 1)
|
self.currentPage = max(0, self.pageOrder.count - 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: self.onboardingWizard.isComplete) { _, newValue in
|
|
||||||
guard newValue, self.activePageIndex == self.wizardPageIndex else { return }
|
|
||||||
self.handleNext()
|
|
||||||
}
|
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
self.stopPermissionMonitoring()
|
self.stopPermissionMonitoring()
|
||||||
self.stopDiscovery()
|
self.stopDiscovery()
|
||||||
|
|
@ -57,7 +56,7 @@ extension OnboardingView {
|
||||||
}
|
}
|
||||||
.task {
|
.task {
|
||||||
await self.refreshPerms()
|
await self.refreshPerms()
|
||||||
self.refreshCLIStatus()
|
await self.refreshCLIInstallerReadiness()
|
||||||
await self.loadWorkspaceDefaults()
|
await self.loadWorkspaceDefaults()
|
||||||
await self.ensureDefaultWorkspace()
|
await self.ensureDefaultWorkspace()
|
||||||
self.refreshBootstrapStatus()
|
self.refreshBootstrapStatus()
|
||||||
|
|
@ -156,6 +155,16 @@ extension OnboardingView {
|
||||||
.frame(width: self.pageWidth, alignment: .top)
|
.frame(width: self.pageWidth, alignment: .top)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func onboardingFixedPage(@ViewBuilder _ content: () -> some View) -> some View {
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
content()
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||||
|
.padding(.horizontal, 28)
|
||||||
|
.frame(width: self.pageWidth, alignment: .top)
|
||||||
|
}
|
||||||
|
|
||||||
func onboardingCard(
|
func onboardingCard(
|
||||||
spacing: CGFloat = 12,
|
spacing: CGFloat = 12,
|
||||||
padding: CGFloat = 16,
|
padding: CGFloat = 16,
|
||||||
|
|
@ -166,10 +175,6 @@ extension OnboardingView {
|
||||||
}
|
}
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
|
||||||
.fill(Color(NSColor.controlBackgroundColor))
|
|
||||||
.shadow(color: .black.opacity(0.06), radius: 8, y: 3))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func onboardingGlassCard(
|
func onboardingGlassCard(
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,11 @@ extension OnboardingView {
|
||||||
self.updatePermissionMonitoring(for: pageIndex)
|
self.updatePermissionMonitoring(for: pageIndex)
|
||||||
self.updateDiscoveryMonitoring(for: pageIndex)
|
self.updateDiscoveryMonitoring(for: pageIndex)
|
||||||
self.maybeKickoffOnboardingChat(for: pageIndex)
|
self.maybeKickoffOnboardingChat(for: pageIndex)
|
||||||
|
if pageIndex == self.cliPageIndex {
|
||||||
|
Task { @MainActor in
|
||||||
|
await self.refreshCLIInstallerReadiness()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func stopPermissionMonitoring() {
|
func stopPermissionMonitoring() {
|
||||||
|
|
@ -57,12 +62,20 @@ extension OnboardingView {
|
||||||
|
|
||||||
func installCLI() async {
|
func installCLI() async {
|
||||||
guard !self.installingCLI else { return }
|
guard !self.installingCLI else { return }
|
||||||
|
await self.refreshCLIInstallerReadiness()
|
||||||
|
|
||||||
|
if self.cliNeedsCommandLineTools {
|
||||||
|
await self.requestCommandLineToolsInstall()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
self.installingCLI = true
|
self.installingCLI = true
|
||||||
defer { installingCLI = false }
|
defer { installingCLI = false }
|
||||||
await CLIInstaller.install { message in
|
await CLIInstaller.install { message in
|
||||||
self.cliStatus = message
|
self.cliStatus = message
|
||||||
}
|
}
|
||||||
self.refreshCLIStatus()
|
self.refreshCLIStatus()
|
||||||
|
await self.refreshCLIInstallerReadiness()
|
||||||
}
|
}
|
||||||
|
|
||||||
func refreshCLIStatus() {
|
func refreshCLIStatus() {
|
||||||
|
|
@ -71,6 +84,29 @@ extension OnboardingView {
|
||||||
self.cliInstalled = installLocation != nil
|
self.cliInstalled = installLocation != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func refreshCLIInstallerReadiness() async {
|
||||||
|
self.refreshCLIStatus()
|
||||||
|
|
||||||
|
if self.cliInstalled {
|
||||||
|
self.cliNeedsCommandLineTools = false
|
||||||
|
self.cliPreflightStatus = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let preflight = await CLIInstaller.preflight()
|
||||||
|
self.cliNeedsCommandLineTools = preflight.needsCommandLineTools
|
||||||
|
self.cliPreflightStatus = preflight.message
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func requestCommandLineToolsInstall() async {
|
||||||
|
await CLIInstaller.requestCommandLineToolsInstall { message in
|
||||||
|
self.cliPreflightStatus = message
|
||||||
|
}
|
||||||
|
await self.refreshCLIInstallerReadiness()
|
||||||
|
}
|
||||||
|
|
||||||
func refreshLocalGatewayProbe() async {
|
func refreshLocalGatewayProbe() async {
|
||||||
let port = GatewayEnvironment.gatewayPort()
|
let port = GatewayEnvironment.gatewayPort()
|
||||||
let desc = await PortGuardian.shared.describe(port: port)
|
let desc = await PortGuardian.shared.describe(port: port)
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ extension OnboardingView {
|
||||||
VStack(spacing: 22) {
|
VStack(spacing: 22) {
|
||||||
Text("Welcome to OpenClaw")
|
Text("Welcome to OpenClaw")
|
||||||
.font(.largeTitle.weight(.semibold))
|
.font(.largeTitle.weight(.semibold))
|
||||||
Text("OpenClaw is a powerful personal AI assistant that can connect to WhatsApp or Telegram.")
|
Text("OpenClaw is a powerful personal AI assistant that connects to the apps you already use — WhatsApp, Telegram, Slack, and more.")
|
||||||
.font(.body)
|
.font(.body)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
|
|
@ -64,6 +64,13 @@ extension OnboardingView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
||||||
|
.fill(Color(NSColor.controlBackgroundColor))
|
||||||
|
.shadow(color: .black.opacity(0.06), radius: 8, y: 3))
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
||||||
|
.fill(Color.orange.opacity(0.06)))
|
||||||
.frame(maxWidth: 520)
|
.frame(maxWidth: 520)
|
||||||
}
|
}
|
||||||
.padding(.top, 16)
|
.padding(.top, 16)
|
||||||
|
|
@ -633,19 +640,33 @@ extension OnboardingView {
|
||||||
self.onboardingPage {
|
self.onboardingPage {
|
||||||
Text("Install the CLI")
|
Text("Install the CLI")
|
||||||
.font(.largeTitle.weight(.semibold))
|
.font(.largeTitle.weight(.semibold))
|
||||||
Text("Required for local mode: installs `openclaw` so launchd can run the gateway.")
|
Text(
|
||||||
|
self.cliNeedsCommandLineTools
|
||||||
|
? "OpenClaw needs Apple Developer Tools first. Install those, then come back to install the CLI."
|
||||||
|
: "Installs the OpenClaw command-line tool so the gateway can run in the background.")
|
||||||
.font(.body)
|
.font(.body)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.frame(maxWidth: 520)
|
.frame(maxWidth: 520)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
|
||||||
self.onboardingCard(spacing: 10) {
|
self.onboardingCard(spacing: 12) {
|
||||||
HStack(spacing: 12) {
|
|
||||||
Button {
|
Button {
|
||||||
Task { await self.installCLI() }
|
Task {
|
||||||
|
if self.cliNeedsCommandLineTools {
|
||||||
|
await self.requestCommandLineToolsInstall()
|
||||||
|
} else {
|
||||||
|
await self.installCLI()
|
||||||
|
}
|
||||||
|
}
|
||||||
} label: {
|
} label: {
|
||||||
let title = self.cliInstalled ? "Reinstall CLI" : "Install CLI"
|
let title: String = if self.cliNeedsCommandLineTools {
|
||||||
|
"Install Apple Developer Tools"
|
||||||
|
} else if self.cliInstalled {
|
||||||
|
"Reinstall CLI"
|
||||||
|
} else {
|
||||||
|
"Install CLI"
|
||||||
|
}
|
||||||
ZStack {
|
ZStack {
|
||||||
Text(title)
|
Text(title)
|
||||||
.opacity(self.installingCLI ? 0 : 1)
|
.opacity(self.installingCLI ? 0 : 1)
|
||||||
|
|
@ -654,36 +675,57 @@ extension OnboardingView {
|
||||||
.controlSize(.mini)
|
.controlSize(.mini)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(minWidth: 120)
|
.frame(maxWidth: .infinity)
|
||||||
}
|
}
|
||||||
.buttonStyle(.borderedProminent)
|
.buttonStyle(.borderedProminent)
|
||||||
.disabled(self.installingCLI)
|
.disabled(self.installingCLI)
|
||||||
|
|
||||||
Button(self.copied ? "Copied" : "Copy install command") {
|
if self.cliNeedsCommandLineTools {
|
||||||
self.copyToPasteboard(self.devLinkCommand)
|
HStack(spacing: 10) {
|
||||||
|
Button("I've Installed It, Recheck") {
|
||||||
|
Task { await self.refreshCLIInstallerReadiness() }
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
|
||||||
|
Button("Open Software Update") {
|
||||||
|
if let url = URL(string: "x-apple.systempreferences:com.apple.preferences.softwareupdate") {
|
||||||
|
NSWorkspace.shared.open(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.disabled(self.installingCLI)
|
|
||||||
|
|
||||||
if self.cliInstalled, let loc = self.cliInstallLocation {
|
if self.cliInstalled, let loc = self.cliInstallLocation {
|
||||||
Label("Installed at \(loc)", systemImage: "checkmark.circle.fill")
|
Label("Installed at \(loc)", systemImage: "checkmark.circle.fill")
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
.foregroundStyle(.green)
|
.foregroundStyle(.green)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if let cliStatus {
|
if let cliPreflightStatus, self.cliNeedsCommandLineTools {
|
||||||
|
Text(cliPreflightStatus)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
} else if let cliStatus {
|
||||||
Text(cliStatus)
|
Text(cliStatus)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
} else if !self.cliInstalled, self.cliInstallLocation == nil {
|
} else if !self.cliInstalled, self.cliInstallLocation == nil {
|
||||||
Text(
|
Text("Installs a user-space Node 22+ runtime (no Homebrew required).")
|
||||||
"""
|
|
||||||
Installs a user-space Node 22+ runtime and the CLI (no Homebrew).
|
|
||||||
Rerun anytime to reinstall or update.
|
|
||||||
""")
|
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
Text("Prefer to install manually?")
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
Button(self.copied ? "Copied" : "Copy install command") {
|
||||||
|
self.copyToPasteboard(self.devLinkCommand)
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
.disabled(self.installingCLI)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,11 @@ import SwiftUI
|
||||||
|
|
||||||
extension OnboardingView {
|
extension OnboardingView {
|
||||||
func wizardPage() -> some View {
|
func wizardPage() -> some View {
|
||||||
self.onboardingPage {
|
self.onboardingFixedPage {
|
||||||
VStack(spacing: 16) {
|
VStack(spacing: 16) {
|
||||||
Text("Setup Wizard")
|
Text("Configure OpenClaw")
|
||||||
.font(.largeTitle.weight(.semibold))
|
.font(.largeTitle.weight(.semibold))
|
||||||
Text("Follow the guided setup from the Gateway. This keeps onboarding in sync with the CLI.")
|
Text("Follow the steps below to configure your AI provider and gateway.")
|
||||||
.font(.body)
|
.font(.body)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
|
|
|
||||||
|
|
@ -193,28 +193,13 @@ final class OnboardingWizardModel {
|
||||||
|
|
||||||
private func shouldSkipWizard() -> Bool {
|
private func shouldSkipWizard() -> Bool {
|
||||||
let root = OpenClawConfigFile.loadDict()
|
let root = OpenClawConfigFile.loadDict()
|
||||||
|
return Self.shouldSkipWizard(root: root)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func shouldSkipWizard(root: [String: Any]) -> Bool {
|
||||||
if let wizard = root["wizard"] as? [String: Any], !wizard.isEmpty {
|
if let wizard = root["wizard"] as? [String: Any], !wizard.isEmpty {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if let gateway = root["gateway"] as? [String: Any],
|
|
||||||
let auth = gateway["auth"] as? [String: Any]
|
|
||||||
{
|
|
||||||
if let mode = auth["mode"] as? String,
|
|
||||||
!mode.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
|
||||||
{
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if let token = auth["token"] as? String,
|
|
||||||
!token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
|
||||||
{
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if let password = auth["password"] as? String,
|
|
||||||
!password.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
|
||||||
{
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -254,17 +239,19 @@ struct OnboardingWizardStepView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
Group {
|
||||||
|
if wizardStepType(self.step) == "select" {
|
||||||
|
self.selectStepLayout
|
||||||
|
} else {
|
||||||
|
self.standardStepLayout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var standardStepLayout: some View {
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
if let title = step.title, !title.isEmpty {
|
self.stepHeader
|
||||||
Text(title)
|
|
||||||
.font(.title2.weight(.semibold))
|
|
||||||
}
|
|
||||||
if let message = step.message, !message.isEmpty {
|
|
||||||
Text(message)
|
|
||||||
.font(.body)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch wizardStepType(self.step) {
|
switch wizardStepType(self.step) {
|
||||||
case "note":
|
case "note":
|
||||||
|
|
@ -274,8 +261,6 @@ struct OnboardingWizardStepView: View {
|
||||||
case "confirm":
|
case "confirm":
|
||||||
Toggle("", isOn: self.$confirmValue)
|
Toggle("", isOn: self.$confirmValue)
|
||||||
.toggleStyle(.switch)
|
.toggleStyle(.switch)
|
||||||
case "select":
|
|
||||||
self.selectOptions
|
|
||||||
case "multiselect":
|
case "multiselect":
|
||||||
self.multiselectOptions
|
self.multiselectOptions
|
||||||
case "progress":
|
case "progress":
|
||||||
|
|
@ -288,6 +273,57 @@ struct OnboardingWizardStepView: View {
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.primaryActionButton
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var selectStepLayout: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
self.stepHeader
|
||||||
|
|
||||||
|
ScrollView {
|
||||||
|
self.selectOptions
|
||||||
|
.padding(.vertical, 2)
|
||||||
|
}
|
||||||
|
.frame(minHeight: 220, maxHeight: 320)
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
HStack(alignment: .center, spacing: 12) {
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text("Selected: \(self.selectedOptionLabel)")
|
||||||
|
.font(.subheadline.weight(.medium))
|
||||||
|
if let hint = self.selectedOptionHint {
|
||||||
|
Text(hint)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
.truncationMode(.tail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(minLength: 12)
|
||||||
|
|
||||||
|
self.primaryActionButton
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var stepHeader: some View {
|
||||||
|
if let title = step.title, !title.isEmpty {
|
||||||
|
Text(title)
|
||||||
|
.font(.title2.weight(.semibold))
|
||||||
|
}
|
||||||
|
if let message = step.message, !message.isEmpty {
|
||||||
|
Text(message)
|
||||||
|
.font(.body)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var primaryActionButton: some View {
|
||||||
Button(action: self.submit) {
|
Button(action: self.submit) {
|
||||||
Text(wizardStepType(self.step) == "action" ? "Run" : "Continue")
|
Text(wizardStepType(self.step) == "action" ? "Run" : "Continue")
|
||||||
.frame(minWidth: 120)
|
.frame(minWidth: 120)
|
||||||
|
|
@ -295,8 +331,6 @@ struct OnboardingWizardStepView: View {
|
||||||
.buttonStyle(.borderedProminent)
|
.buttonStyle(.borderedProminent)
|
||||||
.disabled(self.isSubmitting || self.isBlocked)
|
.disabled(self.isSubmitting || self.isBlocked)
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var textField: some View {
|
private var textField: some View {
|
||||||
|
|
@ -332,11 +366,12 @@ struct OnboardingWizardStepView: View {
|
||||||
Button {
|
Button {
|
||||||
self.selectedIndex = item.index
|
self.selectedIndex = item.index
|
||||||
} label: {
|
} label: {
|
||||||
HStack(alignment: .top, spacing: 8) {
|
HStack(alignment: .top, spacing: 10) {
|
||||||
Image(systemName: self.selectedIndex == item.index ? "largecircle.fill.circle" : "circle")
|
Image(systemName: self.selectedIndex == item.index ? "largecircle.fill.circle" : "circle")
|
||||||
.foregroundStyle(Color.accentColor)
|
.foregroundStyle(Color.accentColor)
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text(item.option.label)
|
Text(item.option.label)
|
||||||
|
.font(.body.weight(self.selectedIndex == item.index ? .semibold : .regular))
|
||||||
.foregroundStyle(.primary)
|
.foregroundStyle(.primary)
|
||||||
if let hint = item.option.hint, !hint.isEmpty {
|
if let hint = item.option.hint, !hint.isEmpty {
|
||||||
Text(hint)
|
Text(hint)
|
||||||
|
|
@ -344,7 +379,10 @@ struct OnboardingWizardStepView: View {
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Spacer(minLength: 0)
|
||||||
}
|
}
|
||||||
|
.padding(.vertical, 6)
|
||||||
|
.contentShape(Rectangle())
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
|
|
@ -381,6 +419,22 @@ struct OnboardingWizardStepView: View {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var selectedOptionLabel: String {
|
||||||
|
guard self.optionItems.indices.contains(self.selectedIndex) else {
|
||||||
|
return "None"
|
||||||
|
}
|
||||||
|
return self.optionItems[self.selectedIndex].option.label
|
||||||
|
}
|
||||||
|
|
||||||
|
private var selectedOptionHint: String? {
|
||||||
|
guard self.optionItems.indices.contains(self.selectedIndex) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let hint = self.optionItems[self.selectedIndex].option.hint?.trimmingCharacters(
|
||||||
|
in: .whitespacesAndNewlines)
|
||||||
|
return hint?.isEmpty == false ? hint : nil
|
||||||
|
}
|
||||||
|
|
||||||
private func submit() {
|
private func submit() {
|
||||||
switch wizardStepType(self.step) {
|
switch wizardStepType(self.step) {
|
||||||
case "note", "progress":
|
case "note", "progress":
|
||||||
|
|
|
||||||
|
|
@ -37,22 +37,16 @@ const AUTH_CHOICE_GROUP_DEFS: {
|
||||||
hint: "setup-token + API key",
|
hint: "setup-token + API key",
|
||||||
choices: ["token", "apiKey"],
|
choices: ["token", "apiKey"],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
value: "chutes",
|
|
||||||
label: "Chutes",
|
|
||||||
hint: "OAuth",
|
|
||||||
choices: ["chutes"],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
value: "minimax",
|
value: "minimax",
|
||||||
label: "MiniMax",
|
label: "MiniMax",
|
||||||
hint: "M2.5 (recommended)",
|
hint: "OAuth or API key · M2.5",
|
||||||
choices: ["minimax-global-oauth", "minimax-global-api", "minimax-cn-oauth", "minimax-cn-api"],
|
choices: ["minimax-global-oauth", "minimax-global-api", "minimax-cn-oauth", "minimax-cn-api"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "moonshot",
|
value: "moonshot",
|
||||||
label: "Moonshot AI (Kimi K2.5)",
|
label: "Moonshot AI (Kimi K2.5)",
|
||||||
hint: "Kimi K2.5 + Kimi Coding",
|
hint: "API key · Kimi K2.5 + Kimi Coding",
|
||||||
choices: ["moonshot-api-key", "moonshot-api-key-cn", "kimi-code-api-key"],
|
choices: ["moonshot-api-key", "moonshot-api-key-cn", "kimi-code-api-key"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -67,6 +61,12 @@ const AUTH_CHOICE_GROUP_DEFS: {
|
||||||
hint: "API key",
|
hint: "API key",
|
||||||
choices: ["xai-api-key"],
|
choices: ["xai-api-key"],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: "chutes",
|
||||||
|
label: "Chutes",
|
||||||
|
hint: "OAuth",
|
||||||
|
choices: ["chutes"],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
value: "mistral",
|
value: "mistral",
|
||||||
label: "Mistral AI",
|
label: "Mistral AI",
|
||||||
|
|
@ -106,7 +106,7 @@ const AUTH_CHOICE_GROUP_DEFS: {
|
||||||
{
|
{
|
||||||
value: "zai",
|
value: "zai",
|
||||||
label: "Z.AI",
|
label: "Z.AI",
|
||||||
hint: "GLM Coding Plan / Global / CN",
|
hint: "API key · GLM Coding Plan / Global / CN",
|
||||||
choices: ["zai-coding-global", "zai-coding-cn", "zai-global", "zai-cn"],
|
choices: ["zai-coding-global", "zai-coding-cn", "zai-global", "zai-cn"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -124,7 +124,7 @@ const AUTH_CHOICE_GROUP_DEFS: {
|
||||||
{
|
{
|
||||||
value: "copilot",
|
value: "copilot",
|
||||||
label: "Copilot",
|
label: "Copilot",
|
||||||
hint: "GitHub + local proxy",
|
hint: "OAuth (GitHub device flow) or local proxy",
|
||||||
choices: ["github-copilot", "copilot-proxy"],
|
choices: ["github-copilot", "copilot-proxy"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -148,7 +148,7 @@ const AUTH_CHOICE_GROUP_DEFS: {
|
||||||
{
|
{
|
||||||
value: "synthetic",
|
value: "synthetic",
|
||||||
label: "Synthetic",
|
label: "Synthetic",
|
||||||
hint: "Anthropic-compatible (multi-model)",
|
hint: "API key · Anthropic-compatible (multi-model)",
|
||||||
choices: ["synthetic-api-key"],
|
choices: ["synthetic-api-key"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -166,13 +166,13 @@ const AUTH_CHOICE_GROUP_DEFS: {
|
||||||
{
|
{
|
||||||
value: "venice",
|
value: "venice",
|
||||||
label: "Venice AI",
|
label: "Venice AI",
|
||||||
hint: "Privacy-focused (uncensored models)",
|
hint: "API key · privacy-focused",
|
||||||
choices: ["venice-api-key"],
|
choices: ["venice-api-key"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "litellm",
|
value: "litellm",
|
||||||
label: "LiteLLM",
|
label: "LiteLLM",
|
||||||
hint: "Unified LLM gateway (100+ providers)",
|
hint: "API key · 100+ providers",
|
||||||
choices: ["litellm-api-key"],
|
choices: ["litellm-api-key"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ export async function promptAuthChoiceGrouped(params: {
|
||||||
];
|
];
|
||||||
|
|
||||||
const providerSelection = (await params.prompter.select({
|
const providerSelection = (await params.prompter.select({
|
||||||
message: "Model/auth provider",
|
message: "Choose how you want to connect.",
|
||||||
options: providerOptions,
|
options: providerOptions,
|
||||||
})) as string;
|
})) as string;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ export async function applyAuthChoiceGitHubCopilot(
|
||||||
});
|
});
|
||||||
|
|
||||||
if (params.setDefaultModel) {
|
if (params.setDefaultModel) {
|
||||||
const model = "github-copilot/gpt-4o";
|
const model = "github-copilot/gpt-5.4";
|
||||||
nextConfig = {
|
nextConfig = {
|
||||||
...nextConfig,
|
...nextConfig,
|
||||||
agents: {
|
agents: {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import type { OpenClawConfig } from "../config/config.js";
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
import { ensureModelAllowlistEntry } from "./model-allowlist.js";
|
import { ensureModelAllowlistEntry } from "./model-allowlist.js";
|
||||||
|
|
||||||
export const OPENAI_DEFAULT_MODEL = "openai/gpt-5.1-codex";
|
export const OPENAI_DEFAULT_MODEL = "openai/gpt-5.4";
|
||||||
|
|
||||||
export function applyOpenAIProviderConfig(cfg: OpenClawConfig): OpenClawConfig {
|
export function applyOpenAIProviderConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||||
const next = ensureModelAllowlistEntry({
|
const next = ensureModelAllowlistEntry({
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,7 @@ export async function runOnboardingWizard(
|
||||||
let flow: WizardFlow =
|
let flow: WizardFlow =
|
||||||
explicitFlow ??
|
explicitFlow ??
|
||||||
(await prompter.select({
|
(await prompter.select({
|
||||||
message: "Onboarding mode",
|
message: "Setup mode",
|
||||||
options: [
|
options: [
|
||||||
{ value: "quickstart", label: "QuickStart", hint: quickstartHint },
|
{ value: "quickstart", label: "QuickStart", hint: quickstartHint },
|
||||||
{ value: "advanced", label: "Manual", hint: manualHint },
|
{ value: "advanced", label: "Manual", hint: manualHint },
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue