feat: unify onboarding + config schema
This commit is contained in:
@@ -51,6 +51,10 @@ actor GatewayConnection {
|
||||
case providersStatus = "providers.status"
|
||||
case configGet = "config.get"
|
||||
case configSet = "config.set"
|
||||
case wizardStart = "wizard.start"
|
||||
case wizardNext = "wizard.next"
|
||||
case wizardCancel = "wizard.cancel"
|
||||
case wizardStatus = "wizard.status"
|
||||
case talkMode = "talk.mode"
|
||||
case webLoginStart = "web.login.start"
|
||||
case webLoginWait = "web.login.wait"
|
||||
|
||||
@@ -86,6 +86,7 @@ struct OnboardingView: View {
|
||||
@State var gatewayDiscovery: GatewayDiscoveryModel
|
||||
@State var onboardingChatModel: ClawdisChatViewModel
|
||||
@State var onboardingSkillsModel = SkillsSettingsModel()
|
||||
@State var onboardingWizard = OnboardingWizardModel()
|
||||
@State var didLoadOnboardingSkills = false
|
||||
@State var localGatewayProbe: LocalGatewayProbe?
|
||||
@Bindable var state: AppState
|
||||
@@ -95,6 +96,7 @@ struct OnboardingView: View {
|
||||
let contentHeight: CGFloat = 460
|
||||
let connectionPageIndex = 1
|
||||
let anthropicAuthPageIndex = 2
|
||||
let wizardPageIndex = 3
|
||||
let onboardingChatPageIndex = 8
|
||||
|
||||
static let clipboardPoll: AnyPublisher<Date, Never> = {
|
||||
@@ -119,7 +121,7 @@ struct OnboardingView: View {
|
||||
case .unconfigured:
|
||||
needsBootstrap ? [0, 1, 8, 9] : [0, 1, 9]
|
||||
case .local:
|
||||
needsBootstrap ? [0, 1, 2, 5, 6, 8, 9] : [0, 1, 2, 5, 6, 9]
|
||||
needsBootstrap ? [0, 1, 3, 5, 8, 9] : [0, 1, 3, 5, 9]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,6 +135,11 @@ struct OnboardingView: View {
|
||||
}
|
||||
|
||||
var buttonTitle: String { self.currentPage == self.pageCount - 1 ? "Finish" : "Next" }
|
||||
var wizardPageOrderIndex: Int? { self.pageOrder.firstIndex(of: self.wizardPageIndex) }
|
||||
var isWizardBlocking: Bool {
|
||||
self.activePageIndex == self.wizardPageIndex && !self.onboardingWizard.isComplete
|
||||
}
|
||||
var canAdvance: Bool { !self.isWizardBlocking }
|
||||
var devLinkCommand: String {
|
||||
let bundlePath = Bundle.main.bundlePath
|
||||
return "ln -sf '\(bundlePath)/Contents/Resources/Relay/clawdis' /usr/local/bin/clawdis"
|
||||
|
||||
@@ -11,6 +11,7 @@ extension OnboardingView {
|
||||
}
|
||||
|
||||
func selectUnconfiguredGateway() {
|
||||
Task { await self.onboardingWizard.cancelIfRunning() }
|
||||
self.state.connectionMode = .unconfigured
|
||||
self.preferredGatewayID = nil
|
||||
self.showAdvancedConnection = false
|
||||
@@ -18,6 +19,7 @@ extension OnboardingView {
|
||||
}
|
||||
|
||||
func selectRemoteGateway(_ gateway: GatewayDiscoveryModel.DiscoveredGateway) {
|
||||
Task { await self.onboardingWizard.cancelIfRunning() }
|
||||
self.preferredGatewayID = gateway.stableID
|
||||
BridgeDiscoveryPreferences.setPreferredStableID(gateway.stableID)
|
||||
|
||||
@@ -47,6 +49,7 @@ extension OnboardingView {
|
||||
}
|
||||
|
||||
func handleNext() {
|
||||
if self.isWizardBlocking { return }
|
||||
if self.currentPage < self.pageCount - 1 {
|
||||
withAnimation { self.currentPage += 1 }
|
||||
} else {
|
||||
|
||||
@@ -46,6 +46,10 @@ 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()
|
||||
@@ -81,6 +85,7 @@ extension OnboardingView {
|
||||
}
|
||||
|
||||
var navigationBar: some View {
|
||||
let wizardLockIndex = self.wizardPageOrderIndex
|
||||
HStack(spacing: 20) {
|
||||
ZStack(alignment: .leading) {
|
||||
Button(action: {}, label: {
|
||||
@@ -107,6 +112,7 @@ extension OnboardingView {
|
||||
|
||||
HStack(spacing: 8) {
|
||||
ForEach(0..<self.pageCount, id: \.self) { index in
|
||||
let isLocked = wizardLockIndex != nil && !self.onboardingWizard.isComplete && index > (wizardLockIndex ?? 0)
|
||||
Button {
|
||||
withAnimation { self.currentPage = index }
|
||||
} label: {
|
||||
@@ -115,6 +121,8 @@ extension OnboardingView {
|
||||
.frame(width: 8, height: 8)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.disabled(isLocked)
|
||||
.opacity(isLocked ? 0.3 : 1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +134,7 @@ extension OnboardingView {
|
||||
}
|
||||
.keyboardShortcut(.return)
|
||||
.buttonStyle(.borderedProminent)
|
||||
.disabled(!self.canAdvance)
|
||||
}
|
||||
.padding(.horizontal, 28)
|
||||
.padding(.bottom, 13)
|
||||
|
||||
@@ -13,6 +13,8 @@ extension OnboardingView {
|
||||
self.connectionPage()
|
||||
case 2:
|
||||
self.anthropicAuthPage()
|
||||
case 3:
|
||||
self.wizardPage()
|
||||
case 5:
|
||||
self.permissionsPage()
|
||||
case 6:
|
||||
|
||||
@@ -47,6 +47,7 @@ extension OnboardingView {
|
||||
_ = view.welcomePage()
|
||||
_ = view.connectionPage()
|
||||
_ = view.anthropicAuthPage()
|
||||
_ = view.wizardPage()
|
||||
_ = view.permissionsPage()
|
||||
_ = view.cliPage()
|
||||
_ = view.workspacePage()
|
||||
|
||||
62
apps/macos/Sources/Clawdis/OnboardingView+Wizard.swift
Normal file
62
apps/macos/Sources/Clawdis/OnboardingView+Wizard.swift
Normal file
@@ -0,0 +1,62 @@
|
||||
import SwiftUI
|
||||
|
||||
extension OnboardingView {
|
||||
func wizardPage() -> some View {
|
||||
self.onboardingPage {
|
||||
VStack(spacing: 16) {
|
||||
Text("Setup Wizard")
|
||||
.font(.largeTitle.weight(.semibold))
|
||||
Text("Follow the guided setup from the Gateway. This keeps onboarding in sync with the CLI.")
|
||||
.font(.body)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.frame(maxWidth: 520)
|
||||
|
||||
self.onboardingCard(spacing: 14, padding: 16) {
|
||||
if let error = self.onboardingWizard.errorMessage {
|
||||
Text("Wizard error")
|
||||
.font(.headline)
|
||||
Text(error)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
Button("Retry") {
|
||||
self.onboardingWizard.reset()
|
||||
Task {
|
||||
await self.onboardingWizard.startIfNeeded(
|
||||
mode: self.state.connectionMode,
|
||||
workspace: self.workspacePath.isEmpty ? nil : self.workspacePath)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
} else if self.onboardingWizard.isStarting {
|
||||
HStack(spacing: 8) {
|
||||
ProgressView()
|
||||
Text("Starting wizard…")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
} else if let step = self.onboardingWizard.currentStep {
|
||||
OnboardingWizardStepView(
|
||||
step: step,
|
||||
isSubmitting: self.onboardingWizard.isSubmitting)
|
||||
{ value in
|
||||
Task { await self.onboardingWizard.submit(step: step, value: value) }
|
||||
}
|
||||
.id(step.id)
|
||||
} else if self.onboardingWizard.isComplete {
|
||||
Text("Wizard complete. Continue to the next step.")
|
||||
.font(.headline)
|
||||
} else {
|
||||
Text("Waiting for wizard…")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
.task {
|
||||
await self.onboardingWizard.startIfNeeded(
|
||||
mode: self.state.connectionMode,
|
||||
workspace: self.workspacePath.isEmpty ? nil : self.workspacePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
400
apps/macos/Sources/Clawdis/OnboardingWizard.swift
Normal file
400
apps/macos/Sources/Clawdis/OnboardingWizard.swift
Normal file
@@ -0,0 +1,400 @@
|
||||
import ClawdisProtocol
|
||||
import Foundation
|
||||
import Observation
|
||||
import OSLog
|
||||
import SwiftUI
|
||||
|
||||
private let onboardingWizardLogger = Logger(subsystem: "com.clawdis", category: "onboarding.wizard")
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
final class OnboardingWizardModel {
|
||||
private(set) var sessionId: String?
|
||||
private(set) var currentStep: WizardStep?
|
||||
private(set) var status: String?
|
||||
private(set) var errorMessage: String?
|
||||
var isStarting = false
|
||||
var isSubmitting = false
|
||||
|
||||
var isComplete: Bool { self.status == "done" }
|
||||
var isRunning: Bool { self.status == "running" }
|
||||
|
||||
func reset() {
|
||||
self.sessionId = nil
|
||||
self.currentStep = nil
|
||||
self.status = nil
|
||||
self.errorMessage = nil
|
||||
self.isStarting = false
|
||||
self.isSubmitting = false
|
||||
}
|
||||
|
||||
func startIfNeeded(mode: AppState.ConnectionMode, workspace: String? = nil) async {
|
||||
guard self.sessionId == nil, !self.isStarting else { return }
|
||||
guard mode == .local else { return }
|
||||
self.isStarting = true
|
||||
self.errorMessage = nil
|
||||
defer { self.isStarting = false }
|
||||
|
||||
do {
|
||||
var params: [String: AnyCodable] = ["mode": AnyCodable("local")]
|
||||
if let workspace, !workspace.isEmpty {
|
||||
params["workspace"] = AnyCodable(workspace)
|
||||
}
|
||||
let res: WizardStartResult = try await GatewayConnection.shared.requestDecoded(
|
||||
method: .wizardStart,
|
||||
params: params)
|
||||
applyStartResult(res)
|
||||
} catch {
|
||||
self.status = "error"
|
||||
self.errorMessage = error.localizedDescription
|
||||
onboardingWizardLogger.error("start failed: \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
}
|
||||
|
||||
func submit(step: WizardStep, value: AnyCodable?) async {
|
||||
guard let sessionId, !self.isSubmitting else { return }
|
||||
self.isSubmitting = true
|
||||
self.errorMessage = nil
|
||||
defer { self.isSubmitting = false }
|
||||
|
||||
do {
|
||||
var params: [String: AnyCodable] = ["sessionId": AnyCodable(sessionId)]
|
||||
var answer: [String: AnyCodable] = ["stepId": AnyCodable(step.id)]
|
||||
if let value {
|
||||
answer["value"] = value
|
||||
}
|
||||
params["answer"] = AnyCodable(answer)
|
||||
let res: WizardNextResult = try await GatewayConnection.shared.requestDecoded(
|
||||
method: .wizardNext,
|
||||
params: params)
|
||||
applyNextResult(res)
|
||||
} catch {
|
||||
self.status = "error"
|
||||
self.errorMessage = error.localizedDescription
|
||||
onboardingWizardLogger.error("submit failed: \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
}
|
||||
|
||||
func cancelIfRunning() async {
|
||||
guard let sessionId, self.isRunning else { return }
|
||||
do {
|
||||
let res: WizardStatusResult = try await GatewayConnection.shared.requestDecoded(
|
||||
method: .wizardCancel,
|
||||
params: ["sessionId": AnyCodable(sessionId)])
|
||||
applyStatusResult(res)
|
||||
} catch {
|
||||
self.status = "error"
|
||||
self.errorMessage = error.localizedDescription
|
||||
onboardingWizardLogger.error("cancel failed: \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
}
|
||||
|
||||
private func applyStartResult(_ res: WizardStartResult) {
|
||||
self.sessionId = res.sessionid
|
||||
self.status = anyCodableStringValue(res.status) ?? (res.done ? "done" : "running")
|
||||
self.errorMessage = res.error
|
||||
self.currentStep = decodeWizardStep(res.step)
|
||||
if res.done { self.currentStep = nil }
|
||||
}
|
||||
|
||||
private func applyNextResult(_ res: WizardNextResult) {
|
||||
self.status = anyCodableStringValue(res.status) ?? self.status
|
||||
self.errorMessage = res.error
|
||||
self.currentStep = decodeWizardStep(res.step)
|
||||
if res.done { self.currentStep = nil }
|
||||
if res.done || anyCodableStringValue(res.status) == "done" || anyCodableStringValue(res.status) == "cancelled"
|
||||
|| anyCodableStringValue(res.status) == "error" {
|
||||
self.sessionId = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func applyStatusResult(_ res: WizardStatusResult) {
|
||||
self.status = anyCodableStringValue(res.status) ?? "unknown"
|
||||
self.errorMessage = res.error
|
||||
self.currentStep = nil
|
||||
self.sessionId = nil
|
||||
}
|
||||
}
|
||||
|
||||
struct OnboardingWizardStepView: View {
|
||||
let step: WizardStep
|
||||
let isSubmitting: Bool
|
||||
let onSubmit: (AnyCodable?) -> Void
|
||||
|
||||
@State private var textValue: String
|
||||
@State private var confirmValue: Bool
|
||||
@State private var selectedIndex: Int
|
||||
@State private var selectedIndices: Set<Int>
|
||||
|
||||
private let optionItems: [WizardOptionItem]
|
||||
|
||||
init(step: WizardStep, isSubmitting: Bool, onSubmit: @escaping (AnyCodable?) -> Void) {
|
||||
self.step = step
|
||||
self.isSubmitting = isSubmitting
|
||||
self.onSubmit = onSubmit
|
||||
let options = parseWizardOptions(step.options).enumerated().map { index, option in
|
||||
WizardOptionItem(index: index, option: option)
|
||||
}
|
||||
self.optionItems = options
|
||||
let initialText = anyCodableString(step.initialvalue)
|
||||
let initialConfirm = anyCodableBool(step.initialvalue)
|
||||
let initialIndex = options.firstIndex(where: { anyCodableEqual($0.option.value, step.initialvalue) }) ?? 0
|
||||
let initialMulti = Set(
|
||||
options.filter { option in
|
||||
anyCodableArray(step.initialvalue).contains { anyCodableEqual($0, option.option.value) }
|
||||
}.map { $0.index }
|
||||
)
|
||||
|
||||
_textValue = State(initialValue: initialText)
|
||||
_confirmValue = State(initialValue: initialConfirm)
|
||||
_selectedIndex = State(initialValue: initialIndex)
|
||||
_selectedIndices = State(initialValue: initialMulti)
|
||||
}
|
||||
|
||||
var body: 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)
|
||||
}
|
||||
|
||||
switch wizardStepType(step) {
|
||||
case "note":
|
||||
EmptyView()
|
||||
case "text":
|
||||
textField
|
||||
case "confirm":
|
||||
Toggle("", isOn: $confirmValue)
|
||||
.toggleStyle(.switch)
|
||||
case "select":
|
||||
selectOptions
|
||||
case "multiselect":
|
||||
multiselectOptions
|
||||
case "progress":
|
||||
ProgressView()
|
||||
.controlSize(.small)
|
||||
case "action":
|
||||
EmptyView()
|
||||
default:
|
||||
Text("Unsupported step type")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Button(action: submit) {
|
||||
Text(wizardStepType(step) == "action" ? "Run" : "Continue")
|
||||
.frame(minWidth: 120)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.disabled(isSubmitting || isBlocked)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
||||
private var textField: some View {
|
||||
let isSensitive = step.sensitive == true
|
||||
if isSensitive {
|
||||
return AnyView(
|
||||
SecureField(step.placeholder ?? "", text: $textValue)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.frame(maxWidth: 360)
|
||||
)
|
||||
}
|
||||
return AnyView(
|
||||
TextField(step.placeholder ?? "", text: $textValue)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.frame(maxWidth: 360)
|
||||
)
|
||||
}
|
||||
|
||||
private var selectOptions: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
ForEach(optionItems) { item in
|
||||
Button {
|
||||
selectedIndex = item.index
|
||||
} label: {
|
||||
HStack(alignment: .top, spacing: 8) {
|
||||
Image(systemName: selectedIndex == item.index ? "largecircle.fill.circle" : "circle")
|
||||
.foregroundStyle(.accent)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(item.option.label)
|
||||
.foregroundStyle(.primary)
|
||||
if let hint = item.option.hint, !hint.isEmpty {
|
||||
Text(hint)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var multiselectOptions: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
ForEach(optionItems) { item in
|
||||
Toggle(isOn: Binding(get: {
|
||||
selectedIndices.contains(item.index)
|
||||
}, set: { newValue in
|
||||
if newValue {
|
||||
selectedIndices.insert(item.index)
|
||||
} else {
|
||||
selectedIndices.remove(item.index)
|
||||
}
|
||||
})) {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(item.option.label)
|
||||
if let hint = item.option.hint, !hint.isEmpty {
|
||||
Text(hint)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var isBlocked: Bool {
|
||||
let type = wizardStepType(step)
|
||||
if type == "select" { return optionItems.isEmpty }
|
||||
if type == "multiselect" { return optionItems.isEmpty }
|
||||
return false
|
||||
}
|
||||
|
||||
private func submit() {
|
||||
switch wizardStepType(step) {
|
||||
case "note", "progress":
|
||||
onSubmit(nil)
|
||||
case "text":
|
||||
onSubmit(AnyCodable(textValue))
|
||||
case "confirm":
|
||||
onSubmit(AnyCodable(confirmValue))
|
||||
case "select":
|
||||
guard optionItems.indices.contains(selectedIndex) else {
|
||||
onSubmit(nil)
|
||||
return
|
||||
}
|
||||
let option = optionItems[selectedIndex].option
|
||||
onSubmit(option.value ?? AnyCodable(option.label))
|
||||
case "multiselect":
|
||||
let values = optionItems
|
||||
.filter { selectedIndices.contains($0.index) }
|
||||
.map { $0.option.value ?? AnyCodable($0.option.label) }
|
||||
onSubmit(AnyCodable(values))
|
||||
case "action":
|
||||
onSubmit(AnyCodable(true))
|
||||
default:
|
||||
onSubmit(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct WizardOptionItem: Identifiable {
|
||||
let index: Int
|
||||
let option: WizardOption
|
||||
|
||||
var id: Int { index }
|
||||
}
|
||||
|
||||
private struct WizardOption {
|
||||
let value: AnyCodable?
|
||||
let label: String
|
||||
let hint: String?
|
||||
}
|
||||
|
||||
private func decodeWizardStep(_ raw: [String: AnyCodable]?) -> WizardStep? {
|
||||
guard let raw else { return nil }
|
||||
do {
|
||||
let data = try JSONEncoder().encode(raw)
|
||||
return try JSONDecoder().decode(WizardStep.self, from: data)
|
||||
} catch {
|
||||
onboardingWizardLogger.error("wizard step decode failed: \(error.localizedDescription, privacy: .public)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func parseWizardOptions(_ raw: [[String: AnyCodable]]?) -> [WizardOption] {
|
||||
guard let raw else { return [] }
|
||||
return raw.map { entry in
|
||||
let value = entry["value"]
|
||||
let label = (entry["label"]?.value as? String) ?? ""
|
||||
let hint = entry["hint"]?.value as? String
|
||||
return WizardOption(value: value, label: label, hint: hint)
|
||||
}
|
||||
}
|
||||
|
||||
private func wizardStepType(_ step: WizardStep) -> String {
|
||||
(step.type.value as? String) ?? ""
|
||||
}
|
||||
|
||||
private func anyCodableString(_ value: AnyCodable?) -> String {
|
||||
switch value?.value {
|
||||
case let string as String:
|
||||
return string
|
||||
case let int as Int:
|
||||
return String(int)
|
||||
case let double as Double:
|
||||
return String(double)
|
||||
case let bool as Bool:
|
||||
return bool ? "true" : "false"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
private func anyCodableStringValue(_ value: AnyCodable?) -> String? {
|
||||
value?.value as? String
|
||||
}
|
||||
|
||||
private func anyCodableBool(_ value: AnyCodable?) -> Bool {
|
||||
switch value?.value {
|
||||
case let bool as Bool:
|
||||
return bool
|
||||
case let string as String:
|
||||
return string.lowercased() == "true"
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private func anyCodableArray(_ value: AnyCodable?) -> [AnyCodable] {
|
||||
switch value?.value {
|
||||
case let arr as [AnyCodable]:
|
||||
return arr
|
||||
case let arr as [Any]:
|
||||
return arr.map { AnyCodable($0) }
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
private func anyCodableEqual(_ lhs: AnyCodable?, _ rhs: AnyCodable?) -> Bool {
|
||||
switch (lhs?.value, rhs?.value) {
|
||||
case let (l as String, r as String):
|
||||
return l == r
|
||||
case let (l as Int, r as Int):
|
||||
return l == r
|
||||
case let (l as Double, r as Double):
|
||||
return l == r
|
||||
case let (l as Bool, r as Bool):
|
||||
return l == r
|
||||
case let (l as String, r as Int):
|
||||
return l == String(r)
|
||||
case let (l as Int, r as String):
|
||||
return String(l) == r
|
||||
case let (l as String, r as Double):
|
||||
return l == String(r)
|
||||
case let (l as Double, r as String):
|
||||
return String(l) == r
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -701,6 +701,210 @@ public struct ConfigSetParams: Codable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct ConfigSchemaParams: Codable {
|
||||
}
|
||||
|
||||
public struct ConfigSchemaResponse: Codable {
|
||||
public let schema: AnyCodable
|
||||
public let uihints: [String: AnyCodable]
|
||||
public let version: String
|
||||
public let generatedat: String
|
||||
|
||||
public init(
|
||||
schema: AnyCodable,
|
||||
uihints: [String: AnyCodable],
|
||||
version: String,
|
||||
generatedat: String
|
||||
) {
|
||||
self.schema = schema
|
||||
self.uihints = uihints
|
||||
self.version = version
|
||||
self.generatedat = generatedat
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case schema
|
||||
case uihints = "uiHints"
|
||||
case version
|
||||
case generatedat = "generatedAt"
|
||||
}
|
||||
}
|
||||
|
||||
public struct WizardStartParams: Codable {
|
||||
public let mode: AnyCodable?
|
||||
public let workspace: String?
|
||||
|
||||
public init(
|
||||
mode: AnyCodable?,
|
||||
workspace: String?
|
||||
) {
|
||||
self.mode = mode
|
||||
self.workspace = workspace
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case mode
|
||||
case workspace
|
||||
}
|
||||
}
|
||||
|
||||
public struct WizardNextParams: Codable {
|
||||
public let sessionid: String
|
||||
public let answer: [String: AnyCodable]?
|
||||
|
||||
public init(
|
||||
sessionid: String,
|
||||
answer: [String: AnyCodable]?
|
||||
) {
|
||||
self.sessionid = sessionid
|
||||
self.answer = answer
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case sessionid = "sessionId"
|
||||
case answer
|
||||
}
|
||||
}
|
||||
|
||||
public struct WizardCancelParams: Codable {
|
||||
public let sessionid: String
|
||||
|
||||
public init(
|
||||
sessionid: String
|
||||
) {
|
||||
self.sessionid = sessionid
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case sessionid = "sessionId"
|
||||
}
|
||||
}
|
||||
|
||||
public struct WizardStatusParams: Codable {
|
||||
public let sessionid: String
|
||||
|
||||
public init(
|
||||
sessionid: String
|
||||
) {
|
||||
self.sessionid = sessionid
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case sessionid = "sessionId"
|
||||
}
|
||||
}
|
||||
|
||||
public struct WizardStep: Codable {
|
||||
public let id: String
|
||||
public let type: AnyCodable
|
||||
public let title: String?
|
||||
public let message: String?
|
||||
public let options: [[String: AnyCodable]]?
|
||||
public let initialvalue: AnyCodable?
|
||||
public let placeholder: String?
|
||||
public let sensitive: Bool?
|
||||
public let executor: AnyCodable?
|
||||
|
||||
public init(
|
||||
id: String,
|
||||
type: AnyCodable,
|
||||
title: String?,
|
||||
message: String?,
|
||||
options: [[String: AnyCodable]]?,
|
||||
initialvalue: AnyCodable?,
|
||||
placeholder: String?,
|
||||
sensitive: Bool?,
|
||||
executor: AnyCodable?
|
||||
) {
|
||||
self.id = id
|
||||
self.type = type
|
||||
self.title = title
|
||||
self.message = message
|
||||
self.options = options
|
||||
self.initialvalue = initialvalue
|
||||
self.placeholder = placeholder
|
||||
self.sensitive = sensitive
|
||||
self.executor = executor
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case type
|
||||
case title
|
||||
case message
|
||||
case options
|
||||
case initialvalue = "initialValue"
|
||||
case placeholder
|
||||
case sensitive
|
||||
case executor
|
||||
}
|
||||
}
|
||||
|
||||
public struct WizardNextResult: Codable {
|
||||
public let done: Bool
|
||||
public let step: [String: AnyCodable]?
|
||||
public let status: AnyCodable?
|
||||
public let error: String?
|
||||
|
||||
public init(
|
||||
done: Bool,
|
||||
step: [String: AnyCodable]?,
|
||||
status: AnyCodable?,
|
||||
error: String?
|
||||
) {
|
||||
self.done = done
|
||||
self.step = step
|
||||
self.status = status
|
||||
self.error = error
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case done
|
||||
case step
|
||||
case status
|
||||
case error
|
||||
}
|
||||
}
|
||||
|
||||
public struct WizardStartResult: Codable {
|
||||
public let sessionid: String
|
||||
public let done: Bool
|
||||
public let step: [String: AnyCodable]?
|
||||
public let status: AnyCodable?
|
||||
public let error: String?
|
||||
|
||||
public init(
|
||||
sessionid: String,
|
||||
done: Bool,
|
||||
step: [String: AnyCodable]?,
|
||||
status: AnyCodable?,
|
||||
error: String?
|
||||
) {
|
||||
self.sessionid = sessionid
|
||||
self.done = done
|
||||
self.step = step
|
||||
self.status = status
|
||||
self.error = error
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case sessionid = "sessionId"
|
||||
case done
|
||||
case step
|
||||
case status
|
||||
case error
|
||||
}
|
||||
}
|
||||
|
||||
public struct WizardStatusResult: Codable {
|
||||
public let status: AnyCodable
|
||||
public let error: String?
|
||||
|
||||
public init(
|
||||
status: AnyCodable,
|
||||
error: String?
|
||||
) {
|
||||
self.status = status
|
||||
self.error = error
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case status
|
||||
case error
|
||||
}
|
||||
}
|
||||
|
||||
public struct TalkModeParams: Codable {
|
||||
public let enabled: Bool
|
||||
public let phase: String?
|
||||
|
||||
@@ -17,7 +17,7 @@ struct OnboardingViewSmokeTests {
|
||||
@Test func pageOrderOmitsWorkspaceAndIdentitySteps() {
|
||||
let order = OnboardingView.pageOrder(for: .local, needsBootstrap: false)
|
||||
#expect(!order.contains(7))
|
||||
#expect(!order.contains(3))
|
||||
#expect(order.contains(3))
|
||||
}
|
||||
|
||||
@Test func pageOrderOmitsOnboardingChatWhenIdentityKnown() {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import SwiftUI
|
||||
import Testing
|
||||
import ClawdisProtocol
|
||||
@testable import Clawdis
|
||||
|
||||
@Suite(.serialized)
|
||||
@MainActor
|
||||
struct OnboardingWizardStepViewTests {
|
||||
@Test func noteStepBuilds() {
|
||||
let step = WizardStep(
|
||||
id: "step-1",
|
||||
type: AnyCodable("note"),
|
||||
title: "Welcome",
|
||||
message: "Hello",
|
||||
options: nil,
|
||||
initialvalue: nil,
|
||||
placeholder: nil,
|
||||
sensitive: nil,
|
||||
executor: nil)
|
||||
let view = OnboardingWizardStepView(step: step, isSubmitting: false, onSubmit: { _ in })
|
||||
_ = view.body
|
||||
}
|
||||
|
||||
@Test func selectStepBuilds() {
|
||||
let options: [[String: AnyCodable]] = [
|
||||
["value": AnyCodable("local"), "label": AnyCodable("Local"), "hint": AnyCodable("This Mac")],
|
||||
["value": AnyCodable("remote"), "label": AnyCodable("Remote")],
|
||||
]
|
||||
let step = WizardStep(
|
||||
id: "step-2",
|
||||
type: AnyCodable("select"),
|
||||
title: "Mode",
|
||||
message: "Choose a mode",
|
||||
options: options,
|
||||
initialvalue: AnyCodable("local"),
|
||||
placeholder: nil,
|
||||
sensitive: nil,
|
||||
executor: nil)
|
||||
let view = OnboardingWizardStepView(step: step, isSubmitting: false, onSubmit: { _ in })
|
||||
_ = view.body
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user