refactor(observation): migrate SwiftUI state
This commit is contained in:
@@ -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<AnyCancellable>()
|
||||
private var didAutoConnect = false
|
||||
private var seenStableIDs = Set<String>()
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
|
||||
@@ -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<Void, Never>?
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<Void, Never>?
|
||||
@@ -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) } }
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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<Void, Never>?
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Void, Never>?
|
||||
private let refreshInterval: TimeInterval = 60
|
||||
|
||||
@@ -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?
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import SwiftUI
|
||||
|
||||
struct InstancesSettings: View {
|
||||
@ObservedObject var store: InstancesStore
|
||||
var store: InstancesStore
|
||||
|
||||
init(store: InstancesStore = .shared) {
|
||||
self.store = store
|
||||
|
||||
@@ -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<Void, Never>?
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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?
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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] = []
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<Void, Never>?
|
||||
@ObservationIgnored
|
||||
private nonisolated(unsafe) var eventTask: Task<Void, Never>?
|
||||
private var pendingRuns = Set<String>() {
|
||||
didSet { self.pendingRunCount = self.pendingRuns.count }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user