fix: macOS Swift cleanup

This commit is contained in:
Peter Steinberger
2026-01-04 17:57:16 +01:00
parent 0928e3c866
commit 5eb6b779f5
7 changed files with 81 additions and 48 deletions

View File

@@ -9,7 +9,7 @@ import SwiftUI
final class AppState { final class AppState {
private let isPreview: Bool private let isPreview: Bool
private var isInitializing = true private var isInitializing = true
private nonisolated var configWatcher: ConfigFileWatcher? private var configWatcher: ConfigFileWatcher?
private var suppressVoiceWakeGlobalSync = false private var suppressVoiceWakeGlobalSync = false
private var voiceWakeGlobalSyncTask: Task<Void, Never>? private var voiceWakeGlobalSyncTask: Task<Void, Never>?
@@ -321,6 +321,7 @@ final class AppState {
} }
} }
@MainActor
deinit { deinit {
self.configWatcher?.stop() self.configWatcher?.stop()
} }

View File

@@ -11,9 +11,6 @@ final class MacNodeLocationService: NSObject, CLLocationManagerDelegate {
private let manager = CLLocationManager() private let manager = CLLocationManager()
private var locationContinuation: CheckedContinuation<CLLocation, Swift.Error>? private var locationContinuation: CheckedContinuation<CLLocation, Swift.Error>?
private struct UncheckedSendable<T>: @unchecked Sendable {
let value: T
}
override init() { override init() {
super.init() super.init()
@@ -63,7 +60,7 @@ final class MacNodeLocationService: NSObject, CLLocationManagerDelegate {
} }
} }
private func withTimeout<T>( private func withTimeout<T: Sendable>(
timeoutMs: Int, timeoutMs: Int,
operation: @escaping () async throws -> T) async throws -> T operation: @escaping () async throws -> T) async throws -> T
{ {
@@ -71,15 +68,38 @@ final class MacNodeLocationService: NSObject, CLLocationManagerDelegate {
return try await operation() return try await operation()
} }
return try await withThrowingTaskGroup(of: UncheckedSendable<T>.self) { group in return try await withCheckedThrowingContinuation { continuation in
group.addTask { try await UncheckedSendable(value: operation()) } var didFinish = false
group.addTask {
try await Task.sleep(nanoseconds: UInt64(timeoutMs) * 1_000_000) func finish(returning value: T) {
throw Error.timeout 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
} }
} }

View File

@@ -86,7 +86,7 @@ extension OnboardingView {
var navigationBar: some View { var navigationBar: some View {
let wizardLockIndex = self.wizardPageOrderIndex let wizardLockIndex = self.wizardPageOrderIndex
HStack(spacing: 20) { return HStack(spacing: 20) {
ZStack(alignment: .leading) { ZStack(alignment: .leading) {
Button(action: {}, label: { Button(action: {}, label: {
Label("Back", systemImage: "chevron.left").labelStyle(.iconOnly) Label("Back", systemImage: "chevron.left").labelStyle(.iconOnly)

View File

@@ -1,3 +1,4 @@
import ClawdbotProtocol
import Observation import Observation
import SwiftUI import SwiftUI

View File

@@ -232,44 +232,52 @@ struct OnboardingWizardStepView: View {
private var selectOptions: some View { private var selectOptions: some View {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
ForEach(self.optionItems) { item in ForEach(self.optionItems, id: \.index) { item in
Button { self.selectOptionRow(item)
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)
} }
} }
} }
private var multiselectOptions: some View { private var multiselectOptions: some View {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
ForEach(self.optionItems) { item in ForEach(self.optionItems, id: \.index) { item in
Toggle(isOn: self.bindingForOption(item)) { self.multiselectOptionRow(item)
VStack(alignment: .leading, spacing: 2) { }
Text(item.option.label) }
if let hint = item.option.hint, !hint.isEmpty { }
Text(hint)
.font(.caption) private func selectOptionRow(_ item: WizardOptionItem) -> some View {
.foregroundStyle(.secondary) 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<Bool> { private func bindingForOption(_ item: WizardOptionItem) -> Binding<Bool> {

View File

@@ -588,7 +588,9 @@ actor TalkModeRuntime {
let stream = client.streamSynthesize(voiceId: voiceId, request: request) let stream = client.streamSynthesize(voiceId: voiceId, request: request)
guard self.isCurrent(input.generation) else { return } 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) } await MainActor.run { TalkModeController.shared.updatePhase(.speaking) }
self.phase = .speaking self.phase = .speaking
@@ -643,7 +645,9 @@ actor TalkModeRuntime {
private func playSystemVoice(input: TalkPlaybackInput) async throws { private func playSystemVoice(input: TalkPlaybackInput) async throws {
self.ttsLogger.info("talk system voice start chars=\(input.cleanedText.count, privacy: .public)") 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) } await MainActor.run { TalkModeController.shared.updatePhase(.speaking) }
self.phase = .speaking self.phase = .speaking
await TalkSystemSpeechSynthesizer.shared.stop() await TalkSystemSpeechSynthesizer.shared.stop()
@@ -727,7 +731,6 @@ actor TalkModeRuntime {
} }
extension TalkModeRuntime { extension TalkModeRuntime {
// MARK: - Audio playback (MainActor helpers) // MARK: - Audio playback (MainActor helpers)
@MainActor @MainActor

View File

@@ -875,7 +875,7 @@ public struct WizardStep: Codable {
} }
} }
public struct WizardNextResult: Codable { public struct WizardNextResult: Codable, Sendable {
public let done: Bool public let done: Bool
public let step: [String: AnyCodable]? public let step: [String: AnyCodable]?
public let status: 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 sessionid: String
public let done: Bool public let done: Bool
public let step: [String: AnyCodable]? 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 status: AnyCodable
public let error: String? public let error: String?