diff --git a/apps/ios/Sources/Bridge/BridgeConnectionController.swift b/apps/ios/Sources/Bridge/BridgeConnectionController.swift index 48013f20c..25e25a774 100644 --- a/apps/ios/Sources/Bridge/BridgeConnectionController.swift +++ b/apps/ios/Sources/Bridge/BridgeConnectionController.swift @@ -1,18 +1,18 @@ import ClawdisKit -import Combine import Foundation import Network +import Observation import SwiftUI @MainActor -final class BridgeConnectionController: ObservableObject { - @Published private(set) var bridges: [BridgeDiscoveryModel.DiscoveredBridge] = [] - @Published private(set) var discoveryStatusText: String = "Idle" - @Published private(set) var discoveryDebugLog: [BridgeDiscoveryModel.DebugLogEntry] = [] +@Observable +final class BridgeConnectionController { + private(set) var bridges: [BridgeDiscoveryModel.DiscoveredBridge] = [] + private(set) var discoveryStatusText: String = "Idle" + private(set) var discoveryDebugLog: [BridgeDiscoveryModel.DebugLogEntry] = [] private let discovery = BridgeDiscoveryModel() private weak var appModel: NodeAppModel? - private var cancellables = Set() private var didAutoConnect = false private var seenStableIDs = Set() @@ -23,20 +23,8 @@ final class BridgeConnectionController: ObservableObject { self.discovery.setDebugLoggingEnabled( UserDefaults.standard.bool(forKey: "bridge.discovery.debugLogs")) - self.discovery.$bridges - .sink { [weak self] newValue in - guard let self else { return } - self.bridges = newValue - self.updateLastDiscoveredBridge(from: newValue) - self.maybeAutoConnect() - } - .store(in: &self.cancellables) - - self.discovery.$statusText - .assign(to: &self.$discoveryStatusText) - - self.discovery.$debugLog - .assign(to: &self.$discoveryDebugLog) + self.updateFromDiscovery() + self.observeDiscovery() if startDiscovery { self.discovery.start() @@ -58,6 +46,29 @@ final class BridgeConnectionController: ObservableObject { } } + private func updateFromDiscovery() { + let newBridges = self.discovery.bridges + self.bridges = newBridges + self.discoveryStatusText = self.discovery.statusText + self.discoveryDebugLog = self.discovery.debugLog + self.updateLastDiscoveredBridge(from: newBridges) + self.maybeAutoConnect() + } + + private func observeDiscovery() { + withObservationTracking { + _ = self.discovery.bridges + _ = self.discovery.statusText + _ = self.discovery.debugLog + } onChange: { [weak self] in + Task { @MainActor in + guard let self else { return } + self.updateFromDiscovery() + self.observeDiscovery() + } + } + } + private func maybeAutoConnect() { guard !self.didAutoConnect else { return } guard let appModel = self.appModel else { return } diff --git a/apps/ios/Sources/Bridge/BridgeDiscoveryDebugLogView.swift b/apps/ios/Sources/Bridge/BridgeDiscoveryDebugLogView.swift index 8091b1cc6..26a946213 100644 --- a/apps/ios/Sources/Bridge/BridgeDiscoveryDebugLogView.swift +++ b/apps/ios/Sources/Bridge/BridgeDiscoveryDebugLogView.swift @@ -2,7 +2,7 @@ import SwiftUI import UIKit struct BridgeDiscoveryDebugLogView: View { - @EnvironmentObject private var bridgeController: BridgeConnectionController + @Environment(BridgeConnectionController.self) private var bridgeController @AppStorage("bridge.discovery.debugLogs") private var debugLogsEnabled: Bool = false var body: some View { diff --git a/apps/ios/Sources/Bridge/BridgeDiscoveryModel.swift b/apps/ios/Sources/Bridge/BridgeDiscoveryModel.swift index 1bffbf878..dbe31189c 100644 --- a/apps/ios/Sources/Bridge/BridgeDiscoveryModel.swift +++ b/apps/ios/Sources/Bridge/BridgeDiscoveryModel.swift @@ -1,9 +1,11 @@ import ClawdisKit import Foundation import Network +import Observation @MainActor -final class BridgeDiscoveryModel: ObservableObject { +@Observable +final class BridgeDiscoveryModel { struct DebugLogEntry: Identifiable, Equatable { var id = UUID() var ts: Date @@ -18,9 +20,9 @@ final class BridgeDiscoveryModel: ObservableObject { var debugID: String } - @Published var bridges: [DiscoveredBridge] = [] - @Published var statusText: String = "Idle" - @Published private(set) var debugLog: [DebugLogEntry] = [] + var bridges: [DiscoveredBridge] = [] + var statusText: String = "Idle" + private(set) var debugLog: [DebugLogEntry] = [] private var browser: NWBrowser? private var debugLoggingEnabled = false diff --git a/apps/ios/Sources/Chat/ChatSheet.swift b/apps/ios/Sources/Chat/ChatSheet.swift index 9ce28fbc5..706a6b789 100644 --- a/apps/ios/Sources/Chat/ChatSheet.swift +++ b/apps/ios/Sources/Chat/ChatSheet.swift @@ -3,12 +3,12 @@ import SwiftUI struct ChatSheet: View { @Environment(\.dismiss) private var dismiss - @StateObject private var viewModel: ClawdisChatViewModel + @State private var viewModel: ClawdisChatViewModel init(bridge: BridgeSession, sessionKey: String = "main") { let transport = IOSBridgeChatTransport(bridge: bridge) - self._viewModel = StateObject( - wrappedValue: ClawdisChatViewModel( + self._viewModel = State( + initialValue: ClawdisChatViewModel( sessionKey: sessionKey, transport: transport)) } diff --git a/apps/ios/Sources/ClawdisApp.swift b/apps/ios/Sources/ClawdisApp.swift index 89bf736df..d4c5cbd79 100644 --- a/apps/ios/Sources/ClawdisApp.swift +++ b/apps/ios/Sources/ClawdisApp.swift @@ -2,23 +2,23 @@ import SwiftUI @main struct ClawdisApp: App { - @StateObject private var appModel: NodeAppModel - @StateObject private var bridgeController: BridgeConnectionController + @State private var appModel: NodeAppModel + @State private var bridgeController: BridgeConnectionController @Environment(\.scenePhase) private var scenePhase init() { BridgeSettingsStore.bootstrapPersistence() let appModel = NodeAppModel() - _appModel = StateObject(wrappedValue: appModel) - _bridgeController = StateObject(wrappedValue: BridgeConnectionController(appModel: appModel)) + _appModel = State(initialValue: appModel) + _bridgeController = State(initialValue: BridgeConnectionController(appModel: appModel)) } var body: some Scene { WindowGroup { RootCanvas() - .environmentObject(self.appModel) - .environmentObject(self.appModel.voiceWake) - .environmentObject(self.bridgeController) + .environment(self.appModel) + .environment(self.appModel.voiceWake) + .environment(self.bridgeController) .onOpenURL { url in Task { await self.appModel.handleDeepLink(url: url) } } diff --git a/apps/ios/Sources/Model/NodeAppModel.swift b/apps/ios/Sources/Model/NodeAppModel.swift index 8af819881..c6735013d 100644 --- a/apps/ios/Sources/Model/NodeAppModel.swift +++ b/apps/ios/Sources/Model/NodeAppModel.swift @@ -1,16 +1,18 @@ import ClawdisKit import Network +import Observation import SwiftUI @MainActor -final class NodeAppModel: ObservableObject { - @Published var isBackgrounded: Bool = false +@Observable +final class NodeAppModel { + var isBackgrounded: Bool = false let screen = ScreenController() let camera = CameraController() - @Published var bridgeStatusText: String = "Not connected" - @Published var bridgeServerName: String? - @Published var bridgeRemoteAddress: String? - @Published var connectedBridgeID: String? + var bridgeStatusText: String = "Not connected" + var bridgeServerName: String? + var bridgeRemoteAddress: String? + var connectedBridgeID: String? private let bridge = BridgeSession() private var bridgeTask: Task? diff --git a/apps/ios/Sources/RootCanvas.swift b/apps/ios/Sources/RootCanvas.swift index 95d635dfb..9def87f22 100644 --- a/apps/ios/Sources/RootCanvas.swift +++ b/apps/ios/Sources/RootCanvas.swift @@ -1,8 +1,8 @@ import SwiftUI struct RootCanvas: View { - @EnvironmentObject private var appModel: NodeAppModel - @EnvironmentObject private var voiceWake: VoiceWakeManager + @Environment(NodeAppModel.self) private var appModel + @Environment(VoiceWakeManager.self) private var voiceWake @AppStorage(VoiceWakePreferences.enabledKey) private var voiceWakeEnabled: Bool = false @State private var presentedSheet: PresentedSheet? @State private var voiceWakeToastText: String? diff --git a/apps/ios/Sources/RootTabs.swift b/apps/ios/Sources/RootTabs.swift index 169234aa0..dc2508895 100644 --- a/apps/ios/Sources/RootTabs.swift +++ b/apps/ios/Sources/RootTabs.swift @@ -1,8 +1,8 @@ import SwiftUI struct RootTabs: View { - @EnvironmentObject private var appModel: NodeAppModel - @EnvironmentObject private var voiceWake: VoiceWakeManager + @Environment(NodeAppModel.self) private var appModel + @Environment(VoiceWakeManager.self) private var voiceWake @AppStorage(VoiceWakePreferences.enabledKey) private var voiceWakeEnabled: Bool = false @State private var selectedTab: Int = 0 @State private var voiceWakeToastText: String? diff --git a/apps/ios/Sources/Screen/ScreenController.swift b/apps/ios/Sources/Screen/ScreenController.swift index e48a797b9..6d53ec338 100644 --- a/apps/ios/Sources/Screen/ScreenController.swift +++ b/apps/ios/Sources/Screen/ScreenController.swift @@ -1,14 +1,16 @@ import ClawdisKit +import Observation import SwiftUI import WebKit @MainActor -final class ScreenController: ObservableObject { +@Observable +final class ScreenController { let webView: WKWebView - @Published var mode: ClawdisScreenMode = .canvas - @Published var urlString: String = "" - @Published var errorText: String? + var mode: ClawdisScreenMode = .canvas + var urlString: String = "" + var errorText: String? init() { let config = WKWebViewConfiguration() diff --git a/apps/ios/Sources/Screen/ScreenTab.swift b/apps/ios/Sources/Screen/ScreenTab.swift index 893450525..aff9320c7 100644 --- a/apps/ios/Sources/Screen/ScreenTab.swift +++ b/apps/ios/Sources/Screen/ScreenTab.swift @@ -2,7 +2,7 @@ import ClawdisKit import SwiftUI struct ScreenTab: View { - @EnvironmentObject private var appModel: NodeAppModel + @Environment(NodeAppModel.self) private var appModel var body: some View { ZStack(alignment: .top) { diff --git a/apps/ios/Sources/Screen/ScreenWebView.swift b/apps/ios/Sources/Screen/ScreenWebView.swift index 7d141d57a..c7cb229d0 100644 --- a/apps/ios/Sources/Screen/ScreenWebView.swift +++ b/apps/ios/Sources/Screen/ScreenWebView.swift @@ -3,7 +3,7 @@ import SwiftUI import WebKit struct ScreenWebView: UIViewRepresentable { - @ObservedObject var controller: ScreenController + var controller: ScreenController func makeUIView(context: Context) -> WKWebView { self.controller.webView diff --git a/apps/ios/Sources/Settings/SettingsTab.swift b/apps/ios/Sources/Settings/SettingsTab.swift index a8fb4cf72..c4eb2ffed 100644 --- a/apps/ios/Sources/Settings/SettingsTab.swift +++ b/apps/ios/Sources/Settings/SettingsTab.swift @@ -1,19 +1,21 @@ import ClawdisKit import Network +import Observation import SwiftUI import UIKit @MainActor -private final class ConnectStatusStore: ObservableObject { - @Published var text: String? +@Observable +private final class ConnectStatusStore { + var text: String? } extension ConnectStatusStore: @unchecked Sendable {} struct SettingsTab: View { - @EnvironmentObject private var appModel: NodeAppModel - @EnvironmentObject private var voiceWake: VoiceWakeManager - @EnvironmentObject private var bridgeController: BridgeConnectionController + @Environment(NodeAppModel.self) private var appModel: NodeAppModel + @Environment(VoiceWakeManager.self) private var voiceWake: VoiceWakeManager + @Environment(BridgeConnectionController.self) private var bridgeController: BridgeConnectionController @Environment(\.dismiss) private var dismiss @AppStorage("node.displayName") private var displayName: String = "iOS Node" @AppStorage("node.instanceId") private var instanceId: String = UUID().uuidString @@ -25,7 +27,7 @@ struct SettingsTab: View { @AppStorage("bridge.manual.host") private var manualBridgeHost: String = "" @AppStorage("bridge.manual.port") private var manualBridgePort: Int = 18790 @AppStorage("bridge.discovery.debugLogs") private var discoveryDebugLogsEnabled: Bool = false - @StateObject private var connectStatus = ConnectStatusStore() + @State private var connectStatus = ConnectStatusStore() @State private var connectingBridgeID: String? @State private var localIPAddress: String? @@ -264,6 +266,7 @@ struct SettingsTab: View { defer { self.connectingBridgeID = nil } do { + let statusStore = self.connectStatus let existing = KeychainStore.loadString( service: "com.steipete.clawdis.bridge", account: self.keychainAccount()) @@ -281,9 +284,8 @@ struct SettingsTab: View { endpoint: bridge.endpoint, hello: hello, onStatus: { status in - let store = self.connectStatus Task { @MainActor in - store.text = status + statusStore.text = status } }) @@ -330,6 +332,7 @@ struct SettingsTab: View { let endpoint: NWEndpoint = .hostPort(host: NWEndpoint.Host(host), port: port) do { + let statusStore = self.connectStatus let existing = KeychainStore.loadString( service: "com.steipete.clawdis.bridge", account: self.keychainAccount()) @@ -347,9 +350,8 @@ struct SettingsTab: View { endpoint: endpoint, hello: hello, onStatus: { status in - let store = self.connectStatus Task { @MainActor in - store.text = status + statusStore.text = status } }) diff --git a/apps/ios/Sources/Voice/VoiceTab.swift b/apps/ios/Sources/Voice/VoiceTab.swift index 53e762f3b..59e1cd6d4 100644 --- a/apps/ios/Sources/Voice/VoiceTab.swift +++ b/apps/ios/Sources/Voice/VoiceTab.swift @@ -1,8 +1,8 @@ import SwiftUI struct VoiceTab: View { - @EnvironmentObject private var appModel: NodeAppModel - @EnvironmentObject private var voiceWake: VoiceWakeManager + @Environment(NodeAppModel.self) private var appModel + @Environment(VoiceWakeManager.self) private var voiceWake @AppStorage("voiceWake.enabled") private var voiceWakeEnabled: Bool = false var body: some View { diff --git a/apps/ios/Sources/Voice/VoiceWakeManager.swift b/apps/ios/Sources/Voice/VoiceWakeManager.swift index aef96b06b..7807d1ea6 100644 --- a/apps/ios/Sources/Voice/VoiceWakeManager.swift +++ b/apps/ios/Sources/Voice/VoiceWakeManager.swift @@ -1,5 +1,6 @@ import AVFAudio import Foundation +import Observation import Speech private func makeAudioTapEnqueueCallback(queue: AudioBufferQueue) -> @Sendable (AVAudioPCMBuffer, AVAudioTime) -> Void { @@ -76,12 +77,13 @@ extension AVAudioPCMBuffer { } @MainActor -final class VoiceWakeManager: NSObject, ObservableObject { - @Published var isEnabled: Bool = false - @Published var isListening: Bool = false - @Published var statusText: String = "Off" - @Published var triggerWords: [String] = VoiceWakePreferences.loadTriggerWords() - @Published var lastTriggeredCommand: String? +@Observable +final class VoiceWakeManager: NSObject { + var isEnabled: Bool = false + var isListening: Bool = false + var statusText: String = "Off" + var triggerWords: [String] = VoiceWakePreferences.loadTriggerWords() + var lastTriggeredCommand: String? private let audioEngine = AVAudioEngine() private var speechRecognizer: SFSpeechRecognizer? diff --git a/apps/macos/Sources/Clawdis/AgentEventStore.swift b/apps/macos/Sources/Clawdis/AgentEventStore.swift index 369a5d687..780867a32 100644 --- a/apps/macos/Sources/Clawdis/AgentEventStore.swift +++ b/apps/macos/Sources/Clawdis/AgentEventStore.swift @@ -1,10 +1,12 @@ import Foundation +import Observation @MainActor -final class AgentEventStore: ObservableObject { +@Observable +final class AgentEventStore { static let shared = AgentEventStore() - @Published private(set) var events: [ControlAgentEvent] = [] + private(set) var events: [ControlAgentEvent] = [] private let maxEvents = 400 func append(_ event: ControlAgentEvent) { diff --git a/apps/macos/Sources/Clawdis/AgentEventsWindow.swift b/apps/macos/Sources/Clawdis/AgentEventsWindow.swift index cc4a8d1f4..f37961ae3 100644 --- a/apps/macos/Sources/Clawdis/AgentEventsWindow.swift +++ b/apps/macos/Sources/Clawdis/AgentEventsWindow.swift @@ -2,7 +2,7 @@ import SwiftUI @MainActor struct AgentEventsWindow: View { - @ObservedObject private var store = AgentEventStore.shared + private let store = AgentEventStore.shared var body: some View { VStack(alignment: .leading, spacing: 6) { diff --git a/apps/macos/Sources/Clawdis/AppState.swift b/apps/macos/Sources/Clawdis/AppState.swift index f567e30df..1b250f389 100644 --- a/apps/macos/Sources/Clawdis/AppState.swift +++ b/apps/macos/Sources/Clawdis/AppState.swift @@ -1,10 +1,12 @@ import AppKit import Foundation +import Observation import ServiceManagement import SwiftUI @MainActor -final class AppState: ObservableObject { +@Observable +final class AppState { private let isPreview: Bool private var suppressVoiceWakeGlobalSync = false private var voiceWakeGlobalSyncTask: Task? @@ -19,26 +21,26 @@ final class AppState: ObservableObject { case remote } - @Published var isPaused: Bool { + var isPaused: Bool { didSet { self.ifNotPreview { UserDefaults.standard.set(self.isPaused, forKey: pauseDefaultsKey) } } } - @Published var launchAtLogin: Bool { + var launchAtLogin: Bool { didSet { self.ifNotPreview { Task { AppStateStore.updateLaunchAtLogin(enabled: self.launchAtLogin) } } } } - @Published var onboardingSeen: Bool { + var onboardingSeen: Bool { didSet { self.ifNotPreview { UserDefaults.standard.set(self.onboardingSeen, forKey: "clawdis.onboardingSeen") } } } - @Published var debugPaneEnabled: Bool { + var debugPaneEnabled: Bool { didSet { self.ifNotPreview { UserDefaults.standard.set(self.debugPaneEnabled, forKey: "clawdis.debugPaneEnabled") } } } - @Published var swabbleEnabled: Bool { + var swabbleEnabled: Bool { didSet { self.ifNotPreview { UserDefaults.standard.set(self.swabbleEnabled, forKey: swabbleEnabledKey) @@ -47,7 +49,7 @@ final class AppState: ObservableObject { } } - @Published var swabbleTriggerWords: [String] { + var swabbleTriggerWords: [String] { didSet { // Preserve the raw editing state; sanitization happens when we actually use the triggers. self.ifNotPreview { @@ -60,21 +62,21 @@ final class AppState: ObservableObject { } } - @Published var voiceWakeTriggerChime: VoiceWakeChime { + var voiceWakeTriggerChime: VoiceWakeChime { didSet { self.ifNotPreview { self.storeChime(self.voiceWakeTriggerChime, key: voiceWakeTriggerChimeKey) } } } - @Published var voiceWakeSendChime: VoiceWakeChime { + var voiceWakeSendChime: VoiceWakeChime { didSet { self.ifNotPreview { self.storeChime(self.voiceWakeSendChime, key: voiceWakeSendChimeKey) } } } - @Published var iconAnimationsEnabled: Bool { + var iconAnimationsEnabled: Bool { didSet { self.ifNotPreview { UserDefaults.standard.set( self.iconAnimationsEnabled, forKey: iconAnimationsEnabledKey) } } } - @Published var showDockIcon: Bool { + var showDockIcon: Bool { didSet { self.ifNotPreview { UserDefaults.standard.set(self.showDockIcon, forKey: showDockIconKey) @@ -83,7 +85,7 @@ final class AppState: ObservableObject { } } - @Published var voiceWakeMicID: String { + var voiceWakeMicID: String { didSet { self.ifNotPreview { UserDefaults.standard.set(self.voiceWakeMicID, forKey: voiceWakeMicKey) @@ -94,7 +96,7 @@ final class AppState: ObservableObject { } } - @Published var voiceWakeLocaleID: String { + var voiceWakeLocaleID: String { didSet { self.ifNotPreview { UserDefaults.standard.set(self.voiceWakeLocaleID, forKey: voiceWakeLocaleKey) @@ -105,27 +107,27 @@ final class AppState: ObservableObject { } } - @Published var voiceWakeAdditionalLocaleIDs: [String] { + var voiceWakeAdditionalLocaleIDs: [String] { didSet { self.ifNotPreview { UserDefaults.standard.set( self.voiceWakeAdditionalLocaleIDs, forKey: voiceWakeAdditionalLocalesKey) } } } - @Published var voicePushToTalkEnabled: Bool { + var voicePushToTalkEnabled: Bool { didSet { self.ifNotPreview { UserDefaults.standard.set( self.voicePushToTalkEnabled, forKey: voicePushToTalkEnabledKey) } } } - @Published var iconOverride: IconOverrideSelection { + var iconOverride: IconOverrideSelection { didSet { self.ifNotPreview { UserDefaults.standard.set(self.iconOverride.rawValue, forKey: iconOverrideKey) } } } - @Published var isWorking: Bool = false - @Published var earBoostActive: Bool = false - @Published var blinkTick: Int = 0 - @Published var sendCelebrationTick: Int = 0 - @Published var heartbeatsEnabled: Bool { + var isWorking: Bool = false + var earBoostActive: Bool = false + var blinkTick: Int = 0 + var sendCelebrationTick: Int = 0 + var heartbeatsEnabled: Bool { didSet { self.ifNotPreview { UserDefaults.standard.set(self.heartbeatsEnabled, forKey: heartbeatsEnabledKey) @@ -134,31 +136,31 @@ final class AppState: ObservableObject { } } - @Published var connectionMode: ConnectionMode { + var connectionMode: ConnectionMode { didSet { self.ifNotPreview { UserDefaults.standard.set(self.connectionMode.rawValue, forKey: connectionModeKey) } } } - @Published var webChatEnabled: Bool { + var webChatEnabled: Bool { didSet { self.ifNotPreview { UserDefaults.standard.set(self.webChatEnabled, forKey: webChatEnabledKey) } } } - @Published var webChatSwiftUIEnabled: Bool { + var webChatSwiftUIEnabled: Bool { didSet { self.ifNotPreview { UserDefaults.standard.set( self.webChatSwiftUIEnabled, forKey: webChatSwiftUIEnabledKey) } } } - @Published var webChatPort: Int { + var webChatPort: Int { didSet { self.ifNotPreview { UserDefaults.standard.set(self.webChatPort, forKey: webChatPortKey) } } } - @Published var canvasEnabled: Bool { + var canvasEnabled: Bool { didSet { self.ifNotPreview { UserDefaults.standard.set(self.canvasEnabled, forKey: canvasEnabledKey) } } } - @Published var peekabooBridgeEnabled: Bool { + var peekabooBridgeEnabled: Bool { didSet { self.ifNotPreview { UserDefaults.standard.set(self.peekabooBridgeEnabled, forKey: peekabooBridgeEnabledKey) @@ -167,7 +169,7 @@ final class AppState: ObservableObject { } } - @Published var attachExistingGatewayOnly: Bool { + var attachExistingGatewayOnly: Bool { didSet { self.ifNotPreview { UserDefaults.standard.set(self.attachExistingGatewayOnly, forKey: attachExistingGatewayOnlyKey) @@ -175,15 +177,15 @@ final class AppState: ObservableObject { } } - @Published var remoteTarget: String { + var remoteTarget: String { didSet { self.ifNotPreview { UserDefaults.standard.set(self.remoteTarget, forKey: remoteTargetKey) } } } - @Published var remoteIdentity: String { + var remoteIdentity: String { didSet { self.ifNotPreview { UserDefaults.standard.set(self.remoteIdentity, forKey: remoteIdentityKey) } } } - @Published var remoteProjectRoot: String { + var remoteProjectRoot: String { didSet { self.ifNotPreview { UserDefaults.standard.set(self.remoteProjectRoot, forKey: remoteProjectRootKey) } } } diff --git a/apps/macos/Sources/Clawdis/ControlChannel.swift b/apps/macos/Sources/Clawdis/ControlChannel.swift index c62fa5d1d..92decf315 100644 --- a/apps/macos/Sources/Clawdis/ControlChannel.swift +++ b/apps/macos/Sources/Clawdis/ControlChannel.swift @@ -1,5 +1,6 @@ import ClawdisProtocol import Foundation +import Observation import OSLog import SwiftUI @@ -36,7 +37,8 @@ enum ControlChannelError: Error, LocalizedError { } @MainActor -final class ControlChannel: ObservableObject { +@Observable +final class ControlChannel { static let shared = ControlChannel() enum Mode { @@ -51,8 +53,8 @@ final class ControlChannel: ObservableObject { case degraded(String) } - @Published private(set) var state: ConnectionState = .disconnected - @Published private(set) var lastPingMs: Double? + private(set) var state: ConnectionState = .disconnected + private(set) var lastPingMs: Double? private let logger = Logger(subsystem: "com.steipete.clawdis", category: "control") diff --git a/apps/macos/Sources/Clawdis/CronJobsStore.swift b/apps/macos/Sources/Clawdis/CronJobsStore.swift index 48a896332..b2d4a624d 100644 --- a/apps/macos/Sources/Clawdis/CronJobsStore.swift +++ b/apps/macos/Sources/Clawdis/CronJobsStore.swift @@ -1,23 +1,25 @@ import ClawdisProtocol import Foundation +import Observation import OSLog @MainActor -final class CronJobsStore: ObservableObject { +@Observable +final class CronJobsStore { static let shared = CronJobsStore() - @Published var jobs: [CronJob] = [] - @Published var selectedJobId: String? - @Published var runEntries: [CronRunLogEntry] = [] + var jobs: [CronJob] = [] + var selectedJobId: String? + var runEntries: [CronRunLogEntry] = [] - @Published var schedulerEnabled: Bool? - @Published var schedulerStorePath: String? - @Published var schedulerNextWakeAtMs: Int? + var schedulerEnabled: Bool? + var schedulerStorePath: String? + var schedulerNextWakeAtMs: Int? - @Published var isLoadingJobs = false - @Published var isLoadingRuns = false - @Published var lastError: String? - @Published var statusMessage: String? + var isLoadingJobs = false + var isLoadingRuns = false + var lastError: String? + var statusMessage: String? private let logger = Logger(subsystem: "com.steipete.clawdis", category: "cron.ui") private var refreshTask: Task? diff --git a/apps/macos/Sources/Clawdis/CronSettings.swift b/apps/macos/Sources/Clawdis/CronSettings.swift index f14b23345..c5d1bb016 100644 --- a/apps/macos/Sources/Clawdis/CronSettings.swift +++ b/apps/macos/Sources/Clawdis/CronSettings.swift @@ -1,7 +1,8 @@ +import Observation import SwiftUI struct CronSettings: View { - @ObservedObject var store: CronJobsStore + @Bindable var store: CronJobsStore @State private var showEditor = false @State private var editingJob: CronJob? @State private var editorError: String? diff --git a/apps/macos/Sources/Clawdis/DebugSettings.swift b/apps/macos/Sources/Clawdis/DebugSettings.swift index 02e548d67..e2f3b5f87 100644 --- a/apps/macos/Sources/Clawdis/DebugSettings.swift +++ b/apps/macos/Sources/Clawdis/DebugSettings.swift @@ -12,8 +12,8 @@ struct DebugSettings: View { @State private var modelsCount: Int? @State private var modelsLoading = false @State private var modelsError: String? - @ObservedObject private var gatewayManager = GatewayProcessManager.shared - @ObservedObject private var healthStore = HealthStore.shared + private let gatewayManager = GatewayProcessManager.shared + private let healthStore = HealthStore.shared @State private var gatewayRootInput: String = GatewayProcessManager.shared.projectRootPath() @State private var sessionStorePath: String = SessionLoader.defaultStorePath @State private var sessionStoreSaveError: String? diff --git a/apps/macos/Sources/Clawdis/GatewayProcessManager.swift b/apps/macos/Sources/Clawdis/GatewayProcessManager.swift index 07d14f5e3..1eadcd252 100644 --- a/apps/macos/Sources/Clawdis/GatewayProcessManager.swift +++ b/apps/macos/Sources/Clawdis/GatewayProcessManager.swift @@ -1,5 +1,6 @@ import Foundation import Network +import Observation import OSLog import Subprocess #if canImport(Darwin) @@ -12,7 +13,8 @@ import SystemPackage #endif @MainActor -final class GatewayProcessManager: ObservableObject { +@Observable +final class GatewayProcessManager { static let shared = GatewayProcessManager() enum Status: Equatable { @@ -39,11 +41,11 @@ final class GatewayProcessManager: ObservableObject { } } - @Published private(set) var status: Status = .stopped - @Published private(set) var log: String = "" - @Published private(set) var restartCount: Int = 0 - @Published private(set) var environmentStatus: GatewayEnvironmentStatus = .checking - @Published private(set) var existingGatewayDetails: String? + private(set) var status: Status = .stopped + private(set) var log: String = "" + private(set) var restartCount: Int = 0 + private(set) var environmentStatus: GatewayEnvironmentStatus = .checking + private(set) var existingGatewayDetails: String? private var execution: Execution? private var lastPid: Int32? diff --git a/apps/macos/Sources/Clawdis/GeneralSettings.swift b/apps/macos/Sources/Clawdis/GeneralSettings.swift index 7ff42a8ea..e1e432b5b 100644 --- a/apps/macos/Sources/Clawdis/GeneralSettings.swift +++ b/apps/macos/Sources/Clawdis/GeneralSettings.swift @@ -1,13 +1,14 @@ import AppKit +import Observation import SwiftUI struct GeneralSettings: View { - @ObservedObject var state: AppState + @Bindable var state: AppState @AppStorage(cameraEnabledKey) private var cameraEnabled: Bool = true - @ObservedObject private var healthStore = HealthStore.shared - @ObservedObject private var gatewayManager = GatewayProcessManager.shared + private let healthStore = HealthStore.shared + private let gatewayManager = GatewayProcessManager.shared // swiftlint:disable:next inclusive_language - @StateObject private var masterDiscovery = MasterDiscoveryModel() + @State private var masterDiscovery = MasterDiscoveryModel() @State private var isInstallingCLI = false @State private var cliStatus: String? @State private var cliInstalled = false diff --git a/apps/macos/Sources/Clawdis/HealthStore.swift b/apps/macos/Sources/Clawdis/HealthStore.swift index bced307fa..262291824 100644 --- a/apps/macos/Sources/Clawdis/HealthStore.swift +++ b/apps/macos/Sources/Clawdis/HealthStore.swift @@ -1,5 +1,6 @@ import Foundation import Network +import Observation import OSLog import SwiftUI @@ -53,15 +54,16 @@ enum HealthState: Equatable { } @MainActor -final class HealthStore: ObservableObject { +@Observable +final class HealthStore { static let shared = HealthStore() private static let logger = Logger(subsystem: "com.steipete.clawdis", category: "health") - @Published private(set) var snapshot: HealthSnapshot? - @Published private(set) var lastSuccess: Date? - @Published private(set) var lastError: String? - @Published private(set) var isRefreshing = false + private(set) var snapshot: HealthSnapshot? + private(set) var lastSuccess: Date? + private(set) var lastError: String? + private(set) var isRefreshing = false private var loopTask: Task? private let refreshInterval: TimeInterval = 60 diff --git a/apps/macos/Sources/Clawdis/HeartbeatStore.swift b/apps/macos/Sources/Clawdis/HeartbeatStore.swift index bc3608ae3..6bd7bb525 100644 --- a/apps/macos/Sources/Clawdis/HeartbeatStore.swift +++ b/apps/macos/Sources/Clawdis/HeartbeatStore.swift @@ -1,11 +1,13 @@ import Foundation +import Observation import SwiftUI @MainActor -final class HeartbeatStore: ObservableObject { +@Observable +final class HeartbeatStore { static let shared = HeartbeatStore() - @Published private(set) var lastEvent: ControlHeartbeatEvent? + private(set) var lastEvent: ControlHeartbeatEvent? private var observer: NSObjectProtocol? diff --git a/apps/macos/Sources/Clawdis/InstancesSettings.swift b/apps/macos/Sources/Clawdis/InstancesSettings.swift index bf4d94cfc..fb656e225 100644 --- a/apps/macos/Sources/Clawdis/InstancesSettings.swift +++ b/apps/macos/Sources/Clawdis/InstancesSettings.swift @@ -1,7 +1,7 @@ import SwiftUI struct InstancesSettings: View { - @ObservedObject var store: InstancesStore + var store: InstancesStore init(store: InstancesStore = .shared) { self.store = store diff --git a/apps/macos/Sources/Clawdis/InstancesStore.swift b/apps/macos/Sources/Clawdis/InstancesStore.swift index 7dad9d3b5..e48f326fc 100644 --- a/apps/macos/Sources/Clawdis/InstancesStore.swift +++ b/apps/macos/Sources/Clawdis/InstancesStore.swift @@ -1,6 +1,7 @@ import ClawdisProtocol import Cocoa import Foundation +import Observation import OSLog struct InstanceInfo: Identifiable, Codable { @@ -27,14 +28,15 @@ struct InstanceInfo: Identifiable, Codable { } @MainActor -final class InstancesStore: ObservableObject { +@Observable +final class InstancesStore { static let shared = InstancesStore() let isPreview: Bool - @Published var instances: [InstanceInfo] = [] - @Published var lastError: String? - @Published var statusMessage: String? - @Published var isLoading = false + var instances: [InstanceInfo] = [] + var lastError: String? + var statusMessage: String? + var isLoading = false private let logger = Logger(subsystem: "com.steipete.clawdis", category: "instances") private var task: Task? diff --git a/apps/macos/Sources/Clawdis/MasterDiscoveryMenu.swift b/apps/macos/Sources/Clawdis/MasterDiscoveryMenu.swift index 6957e6dae..1470496b0 100644 --- a/apps/macos/Sources/Clawdis/MasterDiscoveryMenu.swift +++ b/apps/macos/Sources/Clawdis/MasterDiscoveryMenu.swift @@ -3,7 +3,7 @@ import SwiftUI // “master” is part of the discovery protocol naming; keep UI components consistent. // swiftlint:disable:next inclusive_language struct MasterDiscoveryInlineList: View { - @ObservedObject var discovery: MasterDiscoveryModel + var discovery: MasterDiscoveryModel var currentTarget: String? var onSelect: (MasterDiscoveryModel.DiscoveredMaster) -> Void @State private var hoveredMasterID: MasterDiscoveryModel.DiscoveredMaster.ID? @@ -109,7 +109,7 @@ struct MasterDiscoveryInlineList: View { // swiftlint:disable:next inclusive_language struct MasterDiscoveryMenu: View { - @ObservedObject var discovery: MasterDiscoveryModel + var discovery: MasterDiscoveryModel var onSelect: (MasterDiscoveryModel.DiscoveredMaster) -> Void var body: some View { diff --git a/apps/macos/Sources/Clawdis/MasterDiscoveryModel.swift b/apps/macos/Sources/Clawdis/MasterDiscoveryModel.swift index 561157f7e..3a547d63b 100644 --- a/apps/macos/Sources/Clawdis/MasterDiscoveryModel.swift +++ b/apps/macos/Sources/Clawdis/MasterDiscoveryModel.swift @@ -1,10 +1,12 @@ import Foundation import Network +import Observation // We use “master” as the on-the-wire service name; keep the model aligned with the protocol/docs. @MainActor // swiftlint:disable:next inclusive_language -final class MasterDiscoveryModel: ObservableObject { +@Observable +final class MasterDiscoveryModel { // swiftlint:disable:next inclusive_language struct DiscoveredMaster: Identifiable, Equatable { var id: String { self.debugID } @@ -16,8 +18,8 @@ final class MasterDiscoveryModel: ObservableObject { } // swiftlint:disable:next inclusive_language - @Published var masters: [DiscoveredMaster] = [] - @Published var statusText: String = "Idle" + var masters: [DiscoveredMaster] = [] + var statusText: String = "Idle" private var browser: NWBrowser? diff --git a/apps/macos/Sources/Clawdis/MenuBar.swift b/apps/macos/Sources/Clawdis/MenuBar.swift index fc79535dc..f98a7ad79 100644 --- a/apps/macos/Sources/Clawdis/MenuBar.swift +++ b/apps/macos/Sources/Clawdis/MenuBar.swift @@ -9,9 +9,9 @@ import SwiftUI @main struct ClawdisApp: App { @NSApplicationDelegateAdaptor(AppDelegate.self) private var delegate - @StateObject private var state: AppState - @StateObject private var gatewayManager = GatewayProcessManager.shared - @StateObject private var activityStore = WorkActivityStore.shared + @State private var state: AppState + private let gatewayManager = GatewayProcessManager.shared + private let activityStore = WorkActivityStore.shared @State private var statusItem: NSStatusItem? @State private var isMenuPresented = false @State private var isPanelVisible = false @@ -23,7 +23,7 @@ struct ClawdisApp: App { } init() { - _state = StateObject(wrappedValue: AppStateStore.shared) + _state = State(initialValue: AppStateStore.shared) } var body: some Scene { diff --git a/apps/macos/Sources/Clawdis/MenuContentView.swift b/apps/macos/Sources/Clawdis/MenuContentView.swift index e693c858f..556c2b693 100644 --- a/apps/macos/Sources/Clawdis/MenuContentView.swift +++ b/apps/macos/Sources/Clawdis/MenuContentView.swift @@ -1,17 +1,18 @@ import AppKit import AVFoundation import Foundation +import Observation import SwiftUI /// Menu contents for the Clawdis menu bar extra. struct MenuContent: View { - @ObservedObject var state: AppState + @Bindable var state: AppState let updater: UpdaterProviding? - @ObservedObject private var gatewayManager = GatewayProcessManager.shared - @ObservedObject private var healthStore = HealthStore.shared - @ObservedObject private var heartbeatStore = HeartbeatStore.shared - @ObservedObject private var controlChannel = ControlChannel.shared - @ObservedObject private var activityStore = WorkActivityStore.shared + private let gatewayManager = GatewayProcessManager.shared + private let healthStore = HealthStore.shared + private let heartbeatStore = HeartbeatStore.shared + private let controlChannel = ControlChannel.shared + private let activityStore = WorkActivityStore.shared @Environment(\.openSettings) private var openSettings @State private var availableMics: [AudioInputDevice] = [] @State private var loadingMics = false diff --git a/apps/macos/Sources/Clawdis/NotifyOverlay.swift b/apps/macos/Sources/Clawdis/NotifyOverlay.swift index d95613269..1191c7e22 100644 --- a/apps/macos/Sources/Clawdis/NotifyOverlay.swift +++ b/apps/macos/Sources/Clawdis/NotifyOverlay.swift @@ -1,13 +1,15 @@ import AppKit +import Observation import QuartzCore import SwiftUI /// Lightweight, borderless panel for in-app "toast" notifications (bypasses macOS Notification Center). @MainActor -final class NotifyOverlayController: ObservableObject { +@Observable +final class NotifyOverlayController { static let shared = NotifyOverlayController() - @Published private(set) var model = Model() + private(set) var model = Model() var isVisible: Bool { self.model.isVisible } struct Model { @@ -159,7 +161,7 @@ final class NotifyOverlayController: ObservableObject { } private struct NotifyOverlayView: View { - @ObservedObject var controller: NotifyOverlayController + var controller: NotifyOverlayController var body: some View { VStack(alignment: .leading, spacing: 6) { diff --git a/apps/macos/Sources/Clawdis/Onboarding.swift b/apps/macos/Sources/Clawdis/Onboarding.swift index 16f61ea41..c0a0b7b5f 100644 --- a/apps/macos/Sources/Clawdis/Onboarding.swift +++ b/apps/macos/Sources/Clawdis/Onboarding.swift @@ -1,5 +1,6 @@ import AppKit import ClawdisIPC +import Observation import SwiftUI enum UIStrings { @@ -68,9 +69,9 @@ struct OnboardingView: View { @State private var gatewayInstalling = false @State private var gatewayInstallMessage: String? // swiftlint:disable:next inclusive_language - @StateObject private var masterDiscovery: MasterDiscoveryModel - @ObservedObject private var state: AppState - @ObservedObject private var permissionMonitor: PermissionMonitor + @State private var masterDiscovery: MasterDiscoveryModel + @Bindable private var state: AppState + private var permissionMonitor: PermissionMonitor private let pageWidth: CGFloat = 680 private let contentHeight: CGFloat = 520 @@ -99,9 +100,9 @@ struct OnboardingView: View { permissionMonitor: PermissionMonitor = .shared, discoveryModel: MasterDiscoveryModel = MasterDiscoveryModel()) { - self._state = ObservedObject(wrappedValue: state) - self._permissionMonitor = ObservedObject(wrappedValue: permissionMonitor) - self._masterDiscovery = StateObject(wrappedValue: discoveryModel) + self.state = state + self.permissionMonitor = permissionMonitor + self._masterDiscovery = State(initialValue: discoveryModel) } var body: some View { diff --git a/apps/macos/Sources/Clawdis/PermissionManager.swift b/apps/macos/Sources/Clawdis/PermissionManager.swift index 023f10bd4..9d8269df5 100644 --- a/apps/macos/Sources/Clawdis/PermissionManager.swift +++ b/apps/macos/Sources/Clawdis/PermissionManager.swift @@ -4,6 +4,7 @@ import AVFoundation import ClawdisIPC import CoreGraphics import Foundation +import Observation import OSLog import Speech import UserNotifications @@ -236,10 +237,11 @@ enum AppleScriptPermission { } @MainActor -final class PermissionMonitor: ObservableObject { +@Observable +final class PermissionMonitor { static let shared = PermissionMonitor() - @Published private(set) var status: [Capability: Bool] = [:] + private(set) var status: [Capability: Bool] = [:] private var monitorTimer: Timer? private var isChecking = false diff --git a/apps/macos/Sources/Clawdis/SessionsSettings.swift b/apps/macos/Sources/Clawdis/SessionsSettings.swift index 671dd1690..cc9b4fcb2 100644 --- a/apps/macos/Sources/Clawdis/SessionsSettings.swift +++ b/apps/macos/Sources/Clawdis/SessionsSettings.swift @@ -4,7 +4,7 @@ import SwiftUI @MainActor struct SessionsSettings: View { private let isPreview: Bool - @ObservedObject private var state = AppStateStore.shared + private let state = AppStateStore.shared @State private var rows: [SessionRow] @State private var storePath: String = SessionLoader.defaultStorePath @State private var lastLoaded: Date? diff --git a/apps/macos/Sources/Clawdis/SettingsRootView.swift b/apps/macos/Sources/Clawdis/SettingsRootView.swift index 11bd831d0..2ce2d44b9 100644 --- a/apps/macos/Sources/Clawdis/SettingsRootView.swift +++ b/apps/macos/Sources/Clawdis/SettingsRootView.swift @@ -1,8 +1,9 @@ +import Observation import SwiftUI struct SettingsRootView: View { - @ObservedObject var state: AppState - @ObservedObject private var permissionMonitor = PermissionMonitor.shared + @Bindable var state: AppState + private let permissionMonitor = PermissionMonitor.shared @State private var monitoringPermissions = false @State private var selectedTab: SettingsTab = .general let updater: UpdaterProviding? diff --git a/apps/macos/Sources/Clawdis/VoiceSessionCoordinator.swift b/apps/macos/Sources/Clawdis/VoiceSessionCoordinator.swift index 0bf00713c..070a066d4 100644 --- a/apps/macos/Sources/Clawdis/VoiceSessionCoordinator.swift +++ b/apps/macos/Sources/Clawdis/VoiceSessionCoordinator.swift @@ -1,9 +1,11 @@ import AppKit import Foundation +import Observation import OSLog @MainActor -final class VoiceSessionCoordinator: ObservableObject { +@Observable +final class VoiceSessionCoordinator { static let shared = VoiceSessionCoordinator() enum Source: String { case wakeWord, pushToTalk } diff --git a/apps/macos/Sources/Clawdis/VoiceWakeOverlay.swift b/apps/macos/Sources/Clawdis/VoiceWakeOverlay.swift index dc4515a75..5125ba65d 100644 --- a/apps/macos/Sources/Clawdis/VoiceWakeOverlay.swift +++ b/apps/macos/Sources/Clawdis/VoiceWakeOverlay.swift @@ -1,11 +1,13 @@ import AppKit +import Observation import OSLog import QuartzCore import SwiftUI /// Lightweight, borderless panel that shows the current voice wake transcript near the menu bar. @MainActor -final class VoiceWakeOverlayController: ObservableObject { +@Observable +final class VoiceWakeOverlayController { static let shared = VoiceWakeOverlayController() private let logger = Logger(subsystem: "com.steipete.clawdis", category: "voicewake.overlay") @@ -17,7 +19,7 @@ final class VoiceWakeOverlayController: ObservableObject { enum Source: String { case wakeWord, pushToTalk } - @Published private(set) var model = Model() + private(set) var model = Model() var isVisible: Bool { self.model.isVisible } struct Model { @@ -465,7 +467,7 @@ final class VoiceWakeOverlayController: ObservableObject { } struct VoiceWakeOverlayView: View { - @ObservedObject var controller: VoiceWakeOverlayController + var controller: VoiceWakeOverlayController @FocusState private var textFocused: Bool @State private var isHovering: Bool = false @State private var closeHovering: Bool = false diff --git a/apps/macos/Sources/Clawdis/VoiceWakeSettings.swift b/apps/macos/Sources/Clawdis/VoiceWakeSettings.swift index 13bbebb10..3c78d77ce 100644 --- a/apps/macos/Sources/Clawdis/VoiceWakeSettings.swift +++ b/apps/macos/Sources/Clawdis/VoiceWakeSettings.swift @@ -1,11 +1,12 @@ import AppKit import AVFoundation +import Observation import Speech import SwiftUI import UniformTypeIdentifiers struct VoiceWakeSettings: View { - @ObservedObject var state: AppState + @Bindable var state: AppState @State private var testState: VoiceWakeTestState = .idle @State private var tester = VoiceWakeTester() @State private var isTesting = false diff --git a/apps/macos/Sources/Clawdis/WorkActivityStore.swift b/apps/macos/Sources/Clawdis/WorkActivityStore.swift index d72443e7f..84913f16f 100644 --- a/apps/macos/Sources/Clawdis/WorkActivityStore.swift +++ b/apps/macos/Sources/Clawdis/WorkActivityStore.swift @@ -1,8 +1,10 @@ import Foundation +import Observation import SwiftUI @MainActor -final class WorkActivityStore: ObservableObject { +@Observable +final class WorkActivityStore { static let shared = WorkActivityStore() struct Activity: Equatable { @@ -14,8 +16,8 @@ final class WorkActivityStore: ObservableObject { var lastUpdate: Date } - @Published private(set) var current: Activity? - @Published private(set) var iconState: IconState = .idle + private(set) var current: Activity? + private(set) var iconState: IconState = .idle private var active: [String: Activity] = [:] private var currentSessionKey: String? diff --git a/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatComposer.swift b/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatComposer.swift index a3dfc8cdf..14156ce22 100644 --- a/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatComposer.swift +++ b/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatComposer.swift @@ -1,4 +1,5 @@ import Foundation +import Observation import SwiftUI #if !os(macOS) @@ -8,7 +9,7 @@ import UniformTypeIdentifiers @MainActor struct ClawdisChatComposer: View { - @ObservedObject var viewModel: ClawdisChatViewModel + @Bindable var viewModel: ClawdisChatViewModel #if !os(macOS) @State private var pickerItems: [PhotosPickerItem] = [] diff --git a/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatView.swift b/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatView.swift index adfd93382..e0b547cdb 100644 --- a/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatView.swift +++ b/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatView.swift @@ -2,11 +2,11 @@ import SwiftUI @MainActor public struct ClawdisChatView: View { - @StateObject private var viewModel: ClawdisChatViewModel + @State private var viewModel: ClawdisChatViewModel @State private var scrollerBottomID = UUID() public init(viewModel: ClawdisChatViewModel) { - self._viewModel = StateObject(wrappedValue: viewModel) + self._viewModel = State(initialValue: viewModel) } public var body: some View { diff --git a/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatViewModel.swift b/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatViewModel.swift index 98a34d056..b536b5223 100644 --- a/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatViewModel.swift +++ b/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatViewModel.swift @@ -1,5 +1,6 @@ import ClawdisKit import Foundation +import Observation import OSLog import UniformTypeIdentifiers @@ -12,21 +13,23 @@ import UIKit private let chatUILogger = Logger(subsystem: "com.steipete.clawdis", category: "ClawdisChatUI") @MainActor -public final class ClawdisChatViewModel: ObservableObject { - @Published public private(set) var messages: [ClawdisChatMessage] = [] - @Published public var input: String = "" - @Published public var thinkingLevel: String = "off" - @Published public private(set) var isLoading = false - @Published public private(set) var isSending = false - @Published public var errorText: String? - @Published public var attachments: [ClawdisPendingAttachment] = [] - @Published public private(set) var healthOK: Bool = true - @Published public private(set) var pendingRunCount: Int = 0 +@Observable +public final class ClawdisChatViewModel { + public private(set) var messages: [ClawdisChatMessage] = [] + public var input: String = "" + public var thinkingLevel: String = "off" + public private(set) var isLoading = false + public private(set) var isSending = false + public var errorText: String? + public var attachments: [ClawdisPendingAttachment] = [] + public private(set) var healthOK: Bool = true + public private(set) var pendingRunCount: Int = 0 public let sessionKey: String private let transport: any ClawdisChatTransport - private var eventTask: Task? + @ObservationIgnored + private nonisolated(unsafe) var eventTask: Task? private var pendingRuns = Set() { didSet { self.pendingRunCount = self.pendingRuns.count } }