Improve macOS onboarding UX and gateway setup

This commit is contained in:
ImLukeF 2026-03-15 22:55:59 +11:00
parent f4aff83c51
commit 01a6e0da81
No known key found for this signature in database
17 changed files with 386 additions and 115 deletions

View File

@ -2,6 +2,13 @@ import Foundation
@MainActor
enum CLIInstaller {
struct PreflightStatus: Equatable {
let needsCommandLineTools: Bool
let message: String?
static let ready = PreflightStatus(needsCommandLineTools: false, message: nil)
}
static func installedLocation() -> String? {
self.installedLocation(
searchPaths: CommandResolver.preferredPaths(),
@ -34,6 +41,50 @@ enum CLIInstaller {
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 {
let expected = GatewayEnvironment.expectedGatewayVersionString() ?? "latest"
let prefix = Self.installPrefix()

View File

@ -14,6 +14,10 @@ enum CronCustomSessionTarget: Codable, Equatable {
case predefined(CronSessionTarget)
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 {
switch self {
case .predefined(let target):
@ -254,6 +258,38 @@ struct CronJob: Identifiable, Codable, Equatable {
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)
var parsedSessionTarget: CronCustomSessionTarget {
CronCustomSessionTarget.from(self.sessionTargetRaw)

View File

@ -15,7 +15,7 @@ struct CronSettings_Previews: PreviewProvider {
createdAtMs: 0,
updatedAtMs: 0,
schedule: .every(everyMs: 86_400_000, anchorMs: nil),
sessionTarget: .isolated,
sessionTarget: .predefined(.isolated),
wakeMode: .now,
payload: .agentTurn(
message: "Summarize inbox",
@ -69,7 +69,7 @@ extension CronSettings {
createdAtMs: 1_700_000_000_000,
updatedAtMs: 1_700_000_100_000,
schedule: .cron(expr: "0 8 * * *", tz: "UTC"),
sessionTarget: .isolated,
sessionTarget: .predefined(.isolated),
wakeMode: .nextHeartbeat,
payload: .agentTurn(
message: "Summarize",

View File

@ -90,7 +90,18 @@ enum GatewayEnvironment {
}
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)
return (trimmed?.isEmpty == false) ? trimmed : nil
}

View File

@ -1,5 +1,6 @@
import Foundation
import Observation
import OpenClawKit
@MainActor
@Observable
@ -196,13 +197,9 @@ final class GatewayProcessManager {
let instanceText = instance.map { self.describe(instance: $0) }
let hasListener = instance != nil
let attemptAttach = {
try await self.connection.requestRaw(method: .health, timeoutMs: 2000)
}
for attempt in 0..<(hasListener ? 3 : 1) {
do {
let data = try await attemptAttach()
let data = try await self.probeLocalGatewayHealth(timeoutMs: 2000)
let snap = decodeHealthSnapshot(from: data)
let details = self.describe(details: instanceText, port: port, snap: snap)
self.existingGatewayDetails = details
@ -337,7 +334,7 @@ final class GatewayProcessManager {
while Date() < deadline {
if !self.desiredActive { return }
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 details = instance.map { "pid \($0.pid)" }
self.clearLastFailure()
@ -380,7 +377,7 @@ final class GatewayProcessManager {
while Date() < deadline {
if !self.desiredActive { return false }
do {
_ = try await self.connection.requestRaw(method: .health, timeoutMs: 1500)
_ = try await self.probeLocalGatewayHealth(timeoutMs: 1500)
self.clearLastFailure()
return true
} catch {
@ -413,6 +410,20 @@ final class GatewayProcessManager {
if text.count <= limit { return text }
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

View File

@ -33,6 +33,7 @@ final class MacNodeModeCoordinator {
var retryDelay: UInt64 = 1_000_000_000
var lastCameraEnabled: Bool?
var lastBrowserControlEnabled: Bool?
var lastBlockedOnOnboarding = false
let defaults = UserDefaults.standard
while !Task.isCancelled {
@ -41,6 +42,20 @@ final class MacNodeModeCoordinator {
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
if lastCameraEnabled == nil {
lastCameraEnabled = cameraEnabled
@ -116,6 +131,10 @@ final class MacNodeModeCoordinator {
}
}
static func shouldConnectNodeMode(onboardingSeen: Bool, onboardingVersion: Int) -> Bool {
onboardingSeen && onboardingVersion >= currentOnboardingVersion
}
private func currentCaps() -> [String] {
var caps: [String] = [OpenClawCapability.canvas.rawValue, OpenClawCapability.screen.rawValue]
if OpenClawConfigFile.browserControlEnabled() {

View File

@ -67,6 +67,8 @@ struct OnboardingView: View {
@State var isRequesting = false
@State var installingCLI = false
@State var cliStatus: String?
@State var cliPreflightStatus: String?
@State var cliNeedsCommandLineTools = false
@State var copied = false
@State var monitoringPermissions = false
@State var monitoringDiscovery = false
@ -97,6 +99,7 @@ struct OnboardingView: View {
let pageWidth: CGFloat = Self.windowWidth
let contentHeight: CGFloat = 460
let connectionPageIndex = 1
let cliPageIndex = 6
let wizardPageIndex = 3
let onboardingChatPageIndex = 8
@ -113,7 +116,7 @@ struct OnboardingView: View {
case .unconfigured:
showOnboardingChat ? [0, 1, 8, 9] : [0, 1, 9]
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 {
!self.isWizardBlocking
if self.activePageIndex == self.cliPageIndex {
return self.cliInstalled && !self.installingCLI
}
return !self.isWizardBlocking
}
var devLinkCommand: String {

View File

@ -4,7 +4,10 @@ import SwiftUI
extension OnboardingView {
var body: some View {
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)
.frame(height: 145)
@ -46,10 +49,6 @@ extension OnboardingView {
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 {
self.stopPermissionMonitoring()
self.stopDiscovery()
@ -57,7 +56,7 @@ extension OnboardingView {
}
.task {
await self.refreshPerms()
self.refreshCLIStatus()
await self.refreshCLIInstallerReadiness()
await self.loadWorkspaceDefaults()
await self.ensureDefaultWorkspace()
self.refreshBootstrapStatus()
@ -156,6 +155,16 @@ extension OnboardingView {
.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(
spacing: CGFloat = 12,
padding: CGFloat = 16,
@ -166,10 +175,6 @@ extension OnboardingView {
}
.padding(padding)
.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(

View File

@ -43,6 +43,11 @@ extension OnboardingView {
self.updatePermissionMonitoring(for: pageIndex)
self.updateDiscoveryMonitoring(for: pageIndex)
self.maybeKickoffOnboardingChat(for: pageIndex)
if pageIndex == self.cliPageIndex {
Task { @MainActor in
await self.refreshCLIInstallerReadiness()
}
}
}
func stopPermissionMonitoring() {
@ -57,12 +62,20 @@ extension OnboardingView {
func installCLI() async {
guard !self.installingCLI else { return }
await self.refreshCLIInstallerReadiness()
if self.cliNeedsCommandLineTools {
await self.requestCommandLineToolsInstall()
return
}
self.installingCLI = true
defer { installingCLI = false }
await CLIInstaller.install { message in
self.cliStatus = message
}
self.refreshCLIStatus()
await self.refreshCLIInstallerReadiness()
}
func refreshCLIStatus() {
@ -71,6 +84,29 @@ extension OnboardingView {
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 {
let port = GatewayEnvironment.gatewayPort()
let desc = await PortGuardian.shared.describe(port: port)

View File

@ -33,7 +33,7 @@ extension OnboardingView {
VStack(spacing: 22) {
Text("Welcome to OpenClaw")
.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)
.foregroundStyle(.secondary)
.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)
}
.padding(.top, 16)
@ -633,57 +640,92 @@ extension OnboardingView {
self.onboardingPage {
Text("Install the CLI")
.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)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.frame(maxWidth: 520)
.fixedSize(horizontal: false, vertical: true)
self.onboardingCard(spacing: 10) {
HStack(spacing: 12) {
Button {
Task { await self.installCLI() }
} label: {
let title = self.cliInstalled ? "Reinstall CLI" : "Install CLI"
ZStack {
Text(title)
.opacity(self.installingCLI ? 0 : 1)
if self.installingCLI {
ProgressView()
.controlSize(.mini)
self.onboardingCard(spacing: 12) {
Button {
Task {
if self.cliNeedsCommandLineTools {
await self.requestCommandLineToolsInstall()
} else {
await self.installCLI()
}
}
} label: {
let title: String = if self.cliNeedsCommandLineTools {
"Install Apple Developer Tools"
} else if self.cliInstalled {
"Reinstall CLI"
} else {
"Install CLI"
}
ZStack {
Text(title)
.opacity(self.installingCLI ? 0 : 1)
if self.installingCLI {
ProgressView()
.controlSize(.mini)
}
}
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.disabled(self.installingCLI)
if self.cliNeedsCommandLineTools {
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)
}
}
.frame(minWidth: 120)
}
.buttonStyle(.borderedProminent)
.disabled(self.installingCLI)
Button(self.copied ? "Copied" : "Copy install command") {
self.copyToPasteboard(self.devLinkCommand)
}
.disabled(self.installingCLI)
if self.cliInstalled, let loc = self.cliInstallLocation {
Label("Installed at \(loc)", systemImage: "checkmark.circle.fill")
.font(.footnote)
.foregroundStyle(.green)
.buttonStyle(.bordered)
}
}
if let cliStatus {
if self.cliInstalled, let loc = self.cliInstallLocation {
Label("Installed at \(loc)", systemImage: "checkmark.circle.fill")
.font(.footnote)
.foregroundStyle(.green)
}
if let cliPreflightStatus, self.cliNeedsCommandLineTools {
Text(cliPreflightStatus)
.font(.caption)
.foregroundStyle(.secondary)
} else if let cliStatus {
Text(cliStatus)
.font(.caption)
.foregroundStyle(.secondary)
} else if !self.cliInstalled, self.cliInstallLocation == nil {
Text(
"""
Installs a user-space Node 22+ runtime and the CLI (no Homebrew).
Rerun anytime to reinstall or update.
""")
Text("Installs a user-space Node 22+ runtime (no Homebrew required).")
.font(.footnote)
.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)
}
}
}

View File

@ -4,11 +4,11 @@ import SwiftUI
extension OnboardingView {
func wizardPage() -> some View {
self.onboardingPage {
self.onboardingFixedPage {
VStack(spacing: 16) {
Text("Setup Wizard")
Text("Configure OpenClaw")
.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)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)

View File

@ -193,28 +193,13 @@ final class OnboardingWizardModel {
private func shouldSkipWizard() -> Bool {
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 {
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
}
}
@ -254,17 +239,19 @@ struct OnboardingWizardStepView: 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) {
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)
}
self.stepHeader
switch wizardStepType(self.step) {
case "note":
@ -274,8 +261,6 @@ struct OnboardingWizardStepView: View {
case "confirm":
Toggle("", isOn: self.$confirmValue)
.toggleStyle(.switch)
case "select":
self.selectOptions
case "multiselect":
self.multiselectOptions
case "progress":
@ -288,14 +273,63 @@ struct OnboardingWizardStepView: View {
.foregroundStyle(.secondary)
}
Button(action: self.submit) {
Text(wizardStepType(self.step) == "action" ? "Run" : "Continue")
.frame(minWidth: 120)
}
.buttonStyle(.borderedProminent)
.disabled(self.isSubmitting || self.isBlocked)
self.primaryActionButton
}
.frame(maxWidth: .infinity, alignment: .leading)
}
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) {
Text(wizardStepType(self.step) == "action" ? "Run" : "Continue")
.frame(minWidth: 120)
}
.buttonStyle(.borderedProminent)
.disabled(self.isSubmitting || self.isBlocked)
}
@ViewBuilder
@ -332,11 +366,12 @@ struct OnboardingWizardStepView: View {
Button {
self.selectedIndex = item.index
} label: {
HStack(alignment: .top, spacing: 8) {
HStack(alignment: .top, spacing: 10) {
Image(systemName: self.selectedIndex == item.index ? "largecircle.fill.circle" : "circle")
.foregroundStyle(Color.accentColor)
VStack(alignment: .leading, spacing: 2) {
Text(item.option.label)
.font(.body.weight(self.selectedIndex == item.index ? .semibold : .regular))
.foregroundStyle(.primary)
if let hint = item.option.hint, !hint.isEmpty {
Text(hint)
@ -344,7 +379,10 @@ struct OnboardingWizardStepView: View {
.foregroundStyle(.secondary)
}
}
Spacer(minLength: 0)
}
.padding(.vertical, 6)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
}
@ -381,6 +419,22 @@ struct OnboardingWizardStepView: View {
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() {
switch wizardStepType(self.step) {
case "note", "progress":

View File

@ -37,22 +37,16 @@ const AUTH_CHOICE_GROUP_DEFS: {
hint: "setup-token + API key",
choices: ["token", "apiKey"],
},
{
value: "chutes",
label: "Chutes",
hint: "OAuth",
choices: ["chutes"],
},
{
value: "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"],
},
{
value: "moonshot",
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"],
},
{
@ -67,6 +61,12 @@ const AUTH_CHOICE_GROUP_DEFS: {
hint: "API key",
choices: ["xai-api-key"],
},
{
value: "chutes",
label: "Chutes",
hint: "OAuth",
choices: ["chutes"],
},
{
value: "mistral",
label: "Mistral AI",
@ -106,7 +106,7 @@ const AUTH_CHOICE_GROUP_DEFS: {
{
value: "zai",
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"],
},
{
@ -124,7 +124,7 @@ const AUTH_CHOICE_GROUP_DEFS: {
{
value: "copilot",
label: "Copilot",
hint: "GitHub + local proxy",
hint: "OAuth (GitHub device flow) or local proxy",
choices: ["github-copilot", "copilot-proxy"],
},
{
@ -148,7 +148,7 @@ const AUTH_CHOICE_GROUP_DEFS: {
{
value: "synthetic",
label: "Synthetic",
hint: "Anthropic-compatible (multi-model)",
hint: "API key · Anthropic-compatible (multi-model)",
choices: ["synthetic-api-key"],
},
{
@ -166,13 +166,13 @@ const AUTH_CHOICE_GROUP_DEFS: {
{
value: "venice",
label: "Venice AI",
hint: "Privacy-focused (uncensored models)",
hint: "API key · privacy-focused",
choices: ["venice-api-key"],
},
{
value: "litellm",
label: "LiteLLM",
hint: "Unified LLM gateway (100+ providers)",
hint: "API key · 100+ providers",
choices: ["litellm-api-key"],
},
{

View File

@ -28,7 +28,7 @@ export async function promptAuthChoiceGrouped(params: {
];
const providerSelection = (await params.prompter.select({
message: "Model/auth provider",
message: "Choose how you want to connect.",
options: providerOptions,
})) as string;

View File

@ -42,7 +42,7 @@ export async function applyAuthChoiceGitHubCopilot(
});
if (params.setDefaultModel) {
const model = "github-copilot/gpt-4o";
const model = "github-copilot/gpt-5.4";
nextConfig = {
...nextConfig,
agents: {

View File

@ -1,7 +1,7 @@
import type { OpenClawConfig } from "../config/config.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 {
const next = ensureModelAllowlistEntry({

View File

@ -122,7 +122,7 @@ export async function runOnboardingWizard(
let flow: WizardFlow =
explicitFlow ??
(await prompter.select({
message: "Onboarding mode",
message: "Setup mode",
options: [
{ value: "quickstart", label: "QuickStart", hint: quickstartHint },
{ value: "advanced", label: "Manual", hint: manualHint },