fix: resolve macOS config store concurrency

This commit is contained in:
Peter Steinberger
2026-01-01 21:31:37 +01:00
parent 9ad6863567
commit 7b1687d7e5
4 changed files with 24 additions and 9 deletions

View File

@@ -3,10 +3,10 @@ import Foundation
enum ConfigStore { enum ConfigStore {
struct Overrides: Sendable { struct Overrides: Sendable {
var isRemoteMode: (@Sendable () async -> Bool)? var isRemoteMode: (@Sendable () async -> Bool)?
var loadLocal: (@Sendable () -> [String: Any])? var loadLocal: (@MainActor @Sendable () -> [String: Any])?
var saveLocal: (@Sendable ([String: Any]) -> Void)? var saveLocal: (@MainActor @Sendable ([String: Any]) -> Void)?
var loadRemote: (@Sendable () async -> [String: Any])? var loadRemote: (@MainActor @Sendable () async -> [String: Any])?
var saveRemote: (@Sendable ([String: Any]) async throws -> Void)? var saveRemote: (@MainActor @Sendable ([String: Any]) async throws -> Void)?
} }
private actor OverrideStore { private actor OverrideStore {
@@ -24,9 +24,10 @@ enum ConfigStore {
if let override = overrides.isRemoteMode { if let override = overrides.isRemoteMode {
return await override() return await override()
} }
await MainActor.run { AppStateStore.shared.connectionMode == .remote } return await MainActor.run { AppStateStore.shared.connectionMode == .remote }
} }
@MainActor
static func load() async -> [String: Any] { static func load() async -> [String: Any] {
let overrides = await self.overrideStore.overrides let overrides = await self.overrideStore.overrides
if await self.isRemoteMode() { if await self.isRemoteMode() {
@@ -41,6 +42,7 @@ enum ConfigStore {
return ClawdisConfigFile.loadDict() return ClawdisConfigFile.loadDict()
} }
@MainActor
static func save(_ root: [String: Any]) async throws { static func save(_ root: [String: Any]) async throws {
let overrides = await self.overrideStore.overrides let overrides = await self.overrideStore.overrides
if await self.isRemoteMode() { if await self.isRemoteMode() {
@@ -58,6 +60,7 @@ enum ConfigStore {
} }
} }
@MainActor
private static func loadFromGateway() async -> [String: Any] { private static func loadFromGateway() async -> [String: Any] {
do { do {
let snap: ConfigSnapshot = try await GatewayConnection.shared.requestDecoded( let snap: ConfigSnapshot = try await GatewayConnection.shared.requestDecoded(
@@ -70,6 +73,7 @@ enum ConfigStore {
} }
} }
@MainActor
private static func saveToGateway(_ root: [String: Any]) async throws { private static func saveToGateway(_ root: [String: Any]) async throws {
let data = try JSONSerialization.data(withJSONObject: root, options: [.prettyPrinted, .sortedKeys]) let data = try JSONSerialization.data(withJSONObject: root, options: [.prettyPrinted, .sortedKeys])
guard let raw = String(data: data, encoding: .utf8) else { guard let raw = String(data: data, encoding: .utf8) else {

View File

@@ -73,6 +73,7 @@ extension OnboardingView {
return agent?["workspace"] as? String return agent?["workspace"] as? String
} }
@discardableResult
func saveAgentWorkspace(_ workspace: String?) async -> Bool { func saveAgentWorkspace(_ workspace: String?) async -> Bool {
var root = await ConfigStore.load() var root = await ConfigStore.load()
var agent = root["agent"] as? [String: Any] ?? [:] var agent = root["agent"] as? [String: Any] ?? [:]

View File

@@ -6,6 +6,7 @@ struct SettingsRootView: View {
private let permissionMonitor = PermissionMonitor.shared private let permissionMonitor = PermissionMonitor.shared
@State private var monitoringPermissions = false @State private var monitoringPermissions = false
@State private var selectedTab: SettingsTab = .general @State private var selectedTab: SettingsTab = .general
@State private var snapshotPaths: (configPath: String?, stateDir: String?) = (nil, nil)
let updater: UpdaterProviding? let updater: UpdaterProviding?
private let isPreview = ProcessInfo.processInfo.isPreview private let isPreview = ProcessInfo.processInfo.isPreview
private let isNixMode = ProcessInfo.processInfo.isNixMode private let isNixMode = ProcessInfo.processInfo.isNixMode
@@ -102,13 +103,16 @@ struct SettingsRootView: View {
guard !self.isPreview else { return } guard !self.isPreview else { return }
await self.refreshPerms() await self.refreshPerms()
} }
.task(id: self.state.connectionMode) {
guard !self.isPreview else { return }
await self.refreshSnapshotPaths()
}
} }
private var nixManagedBanner: some View { private var nixManagedBanner: some View {
// Prefer gateway-resolved paths; fall back to local env defaults if disconnected. // Prefer gateway-resolved paths; fall back to local env defaults if disconnected.
let snapshotPaths = GatewayConnection.shared.snapshotPaths() let configPath = self.snapshotPaths.configPath ?? ClawdisPaths.configURL.path
let configPath = snapshotPaths.configPath ?? ClawdisPaths.configURL.path let stateDir = self.snapshotPaths.stateDir ?? ClawdisPaths.stateDirURL.path
let stateDir = snapshotPaths.stateDir ?? ClawdisPaths.stateDirURL.path
return VStack(alignment: .leading, spacing: 6) { return VStack(alignment: .leading, spacing: 6) {
HStack(spacing: 8) { HStack(spacing: 8) {
@@ -140,6 +144,12 @@ struct SettingsRootView: View {
return requested return requested
} }
@MainActor
private func refreshSnapshotPaths() async {
let paths = await GatewayConnection.shared.snapshotPaths()
self.snapshotPaths = paths
}
@MainActor @MainActor
private func refreshPerms() async { private func refreshPerms() async {
guard !self.isPreview else { return } guard !self.isPreview else { return }

View File

@@ -1 +1 @@
700c6959d0a4aa855933f5e9c0d4894a07e2cfd4fa4b4aca181a078d61104704 13cc362f2bc44e2a05a6da5e5ba66ea602755f18ed82b18cf244c8044aa84c36