From 5eb6b779f552025f95daddaf01027cd16e5b3555 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 4 Jan 2026 17:57:16 +0100 Subject: [PATCH] fix: macOS Swift cleanup --- apps/macos/Sources/Clawdbot/AppState.swift | 3 +- .../NodeMode/MacNodeLocationService.swift | 44 +++++++++---- .../Clawdbot/OnboardingView+Layout.swift | 2 +- .../Clawdbot/OnboardingView+Wizard.swift | 1 + .../Sources/Clawdbot/OnboardingWizard.swift | 64 +++++++++++-------- .../Sources/Clawdbot/TalkModeRuntime.swift | 9 ++- .../ClawdbotProtocol/GatewayModels.swift | 6 +- 7 files changed, 81 insertions(+), 48 deletions(-) diff --git a/apps/macos/Sources/Clawdbot/AppState.swift b/apps/macos/Sources/Clawdbot/AppState.swift index e46bc82de..62b0ab75d 100644 --- a/apps/macos/Sources/Clawdbot/AppState.swift +++ b/apps/macos/Sources/Clawdbot/AppState.swift @@ -9,7 +9,7 @@ import SwiftUI final class AppState { private let isPreview: Bool private var isInitializing = true - private nonisolated var configWatcher: ConfigFileWatcher? + private var configWatcher: ConfigFileWatcher? private var suppressVoiceWakeGlobalSync = false private var voiceWakeGlobalSyncTask: Task? @@ -321,6 +321,7 @@ final class AppState { } } + @MainActor deinit { self.configWatcher?.stop() } diff --git a/apps/macos/Sources/Clawdbot/NodeMode/MacNodeLocationService.swift b/apps/macos/Sources/Clawdbot/NodeMode/MacNodeLocationService.swift index c88faf8b8..a1374ad7d 100644 --- a/apps/macos/Sources/Clawdbot/NodeMode/MacNodeLocationService.swift +++ b/apps/macos/Sources/Clawdbot/NodeMode/MacNodeLocationService.swift @@ -11,9 +11,6 @@ final class MacNodeLocationService: NSObject, CLLocationManagerDelegate { private let manager = CLLocationManager() private var locationContinuation: CheckedContinuation? - private struct UncheckedSendable: @unchecked Sendable { - let value: T - } override init() { super.init() @@ -63,7 +60,7 @@ final class MacNodeLocationService: NSObject, CLLocationManagerDelegate { } } - private func withTimeout( + private func withTimeout( timeoutMs: Int, operation: @escaping () async throws -> T) async throws -> T { @@ -71,15 +68,38 @@ final class MacNodeLocationService: NSObject, CLLocationManagerDelegate { return try await operation() } - return try await withThrowingTaskGroup(of: UncheckedSendable.self) { group in - group.addTask { try await UncheckedSendable(value: operation()) } - group.addTask { - try await Task.sleep(nanoseconds: UInt64(timeoutMs) * 1_000_000) - throw Error.timeout + return try await withCheckedThrowingContinuation { continuation in + var didFinish = false + + func finish(returning value: T) { + guard !didFinish else { return } + didFinish = true + continuation.resume(returning: value) + } + + func finish(throwing error: Swift.Error) { + guard !didFinish else { return } + didFinish = true + continuation.resume(throwing: error) + } + + let timeoutItem = DispatchWorkItem { + finish(throwing: Error.timeout) + } + DispatchQueue.main.asyncAfter( + deadline: .now() + .milliseconds(timeoutMs), + execute: timeoutItem) + + Task { @MainActor in + do { + let value = try await operation() + timeoutItem.cancel() + finish(returning: value) + } catch { + timeoutItem.cancel() + finish(throwing: error) + } } - let result = try await group.next()! - group.cancelAll() - return result.value } } diff --git a/apps/macos/Sources/Clawdbot/OnboardingView+Layout.swift b/apps/macos/Sources/Clawdbot/OnboardingView+Layout.swift index e2a553286..e31253738 100644 --- a/apps/macos/Sources/Clawdbot/OnboardingView+Layout.swift +++ b/apps/macos/Sources/Clawdbot/OnboardingView+Layout.swift @@ -86,7 +86,7 @@ extension OnboardingView { var navigationBar: some View { let wizardLockIndex = self.wizardPageOrderIndex - HStack(spacing: 20) { + return HStack(spacing: 20) { ZStack(alignment: .leading) { Button(action: {}, label: { Label("Back", systemImage: "chevron.left").labelStyle(.iconOnly) diff --git a/apps/macos/Sources/Clawdbot/OnboardingView+Wizard.swift b/apps/macos/Sources/Clawdbot/OnboardingView+Wizard.swift index e2776945d..2fe3a0090 100644 --- a/apps/macos/Sources/Clawdbot/OnboardingView+Wizard.swift +++ b/apps/macos/Sources/Clawdbot/OnboardingView+Wizard.swift @@ -1,3 +1,4 @@ +import ClawdbotProtocol import Observation import SwiftUI diff --git a/apps/macos/Sources/Clawdbot/OnboardingWizard.swift b/apps/macos/Sources/Clawdbot/OnboardingWizard.swift index 845ea0531..48406aa7c 100644 --- a/apps/macos/Sources/Clawdbot/OnboardingWizard.swift +++ b/apps/macos/Sources/Clawdbot/OnboardingWizard.swift @@ -232,44 +232,52 @@ struct OnboardingWizardStepView: View { private var selectOptions: some View { VStack(alignment: .leading, spacing: 8) { - ForEach(self.optionItems) { item in - Button { - self.selectedIndex = item.index - } label: { - HStack(alignment: .top, spacing: 8) { - Image(systemName: self.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) + ForEach(self.optionItems, id: \.index) { item in + self.selectOptionRow(item) } } } private var multiselectOptions: some View { VStack(alignment: .leading, spacing: 8) { - ForEach(self.optionItems) { item in - Toggle(isOn: self.bindingForOption(item)) { - VStack(alignment: .leading, spacing: 2) { - Text(item.option.label) - if let hint = item.option.hint, !hint.isEmpty { - Text(hint) - .font(.caption) - .foregroundStyle(.secondary) - } + ForEach(self.optionItems, id: \.index) { item in + self.multiselectOptionRow(item) + } + } + } + + private func selectOptionRow(_ item: WizardOptionItem) -> some View { + Button { + self.selectedIndex = item.index + } label: { + HStack(alignment: .top, spacing: 8) { + Image(systemName: self.selectedIndex == item.index ? "largecircle.fill.circle" : "circle") + .foregroundStyle(Color.accentColor) + 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 func multiselectOptionRow(_ item: WizardOptionItem) -> some View { + Toggle(isOn: self.bindingForOption(item)) { + VStack(alignment: .leading, spacing: 2) { + Text(item.option.label) + if let hint = item.option.hint, !hint.isEmpty { + Text(hint) + .font(.caption) + .foregroundStyle(.secondary) + } + } + } } private func bindingForOption(_ item: WizardOptionItem) -> Binding { diff --git a/apps/macos/Sources/Clawdbot/TalkModeRuntime.swift b/apps/macos/Sources/Clawdbot/TalkModeRuntime.swift index ee964628b..faca37bdf 100644 --- a/apps/macos/Sources/Clawdbot/TalkModeRuntime.swift +++ b/apps/macos/Sources/Clawdbot/TalkModeRuntime.swift @@ -588,7 +588,9 @@ actor TalkModeRuntime { let stream = client.streamSynthesize(voiceId: voiceId, request: request) guard self.isCurrent(input.generation) else { return } - if self.interruptOnSpeech, ! await self.prepareForPlayback(generation: input.generation) { return } + if self.interruptOnSpeech { + guard await self.prepareForPlayback(generation: input.generation) else { return } + } await MainActor.run { TalkModeController.shared.updatePhase(.speaking) } self.phase = .speaking @@ -643,7 +645,9 @@ actor TalkModeRuntime { private func playSystemVoice(input: TalkPlaybackInput) async throws { self.ttsLogger.info("talk system voice start chars=\(input.cleanedText.count, privacy: .public)") - if self.interruptOnSpeech, ! await self.prepareForPlayback(generation: input.generation) { return } + if self.interruptOnSpeech { + guard await self.prepareForPlayback(generation: input.generation) else { return } + } await MainActor.run { TalkModeController.shared.updatePhase(.speaking) } self.phase = .speaking await TalkSystemSpeechSynthesizer.shared.stop() @@ -727,7 +731,6 @@ actor TalkModeRuntime { } extension TalkModeRuntime { - // MARK: - Audio playback (MainActor helpers) @MainActor diff --git a/apps/macos/Sources/ClawdbotProtocol/GatewayModels.swift b/apps/macos/Sources/ClawdbotProtocol/GatewayModels.swift index 9fc796c63..ce2a8abaa 100644 --- a/apps/macos/Sources/ClawdbotProtocol/GatewayModels.swift +++ b/apps/macos/Sources/ClawdbotProtocol/GatewayModels.swift @@ -875,7 +875,7 @@ public struct WizardStep: Codable { } } -public struct WizardNextResult: Codable { +public struct WizardNextResult: Codable, Sendable { public let done: Bool public let step: [String: AnyCodable]? public let status: AnyCodable? @@ -900,7 +900,7 @@ public struct WizardNextResult: Codable { } } -public struct WizardStartResult: Codable { +public struct WizardStartResult: Codable, Sendable { public let sessionid: String public let done: Bool public let step: [String: AnyCodable]? @@ -929,7 +929,7 @@ public struct WizardStartResult: Codable { } } -public struct WizardStatusResult: Codable { +public struct WizardStatusResult: Codable, Sendable { public let status: AnyCodable public let error: String?