From ddf8aef4f7b4a55a95dba39bf58e143ed0b561ee Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 7 Dec 2025 04:38:08 +0000 Subject: [PATCH] Settings: move session store path to Debug --- .../Sources/Clawdis/ConfigSettings.swift | 56 +++++-------- .../macos/Sources/Clawdis/DebugSettings.swift | 80 ++++++++++++++++++- 2 files changed, 97 insertions(+), 39 deletions(-) diff --git a/apps/macos/Sources/Clawdis/ConfigSettings.swift b/apps/macos/Sources/Clawdis/ConfigSettings.swift index f4658a12f..55f3b5ba8 100644 --- a/apps/macos/Sources/Clawdis/ConfigSettings.swift +++ b/apps/macos/Sources/Clawdis/ConfigSettings.swift @@ -4,7 +4,6 @@ import SwiftUI struct ConfigSettings: View { @State private var configModel: String = "" @State private var customModel: String = "" - @State private var configStorePath: String = SessionLoader.defaultStorePath @State private var configSaving = false @State private var hasLoaded = false @State private var models: [ModelChoice] = [] @@ -93,15 +92,6 @@ struct ConfigSettings: View { } } - LabeledContent("Session store") { - TextField("Path", text: self.$configStorePath) - .textFieldStyle(.roundedBorder) - .frame(width: 360) - .onChange(of: self.configStorePath) { _, _ in - self.autosaveConfig() - } - } - Spacer() } .frame(maxWidth: .infinity, alignment: .leading) @@ -128,26 +118,13 @@ struct ConfigSettings: View { } private func loadConfig() { - let url = self.configURL() - guard let data = try? Data(contentsOf: url) else { - self.configModel = SessionLoader.fallbackModel - self.configStorePath = SessionLoader.defaultStorePath - return - } - guard - let parsed = try? JSONSerialization.jsonObject(with: data) as? [String: Any], - let inbound = parsed["inbound"] as? [String: Any], - let reply = inbound["reply"] as? [String: Any] - else { - return - } + let parsed = self.loadConfigDict() + let inbound = parsed["inbound"] as? [String: Any] + let reply = inbound?["reply"] as? [String: Any] + let agent = reply?["agent"] as? [String: Any] + let heartbeatMinutes = reply?["heartbeatMinutes"] as? Int + let heartbeatBody = reply?["heartbeatBody"] as? String - let session = reply["session"] as? [String: Any] - let agent = reply["agent"] as? [String: Any] - let heartbeatMinutes = reply["heartbeatMinutes"] as? Int - let heartbeatBody = reply["heartbeatBody"] as? String - - self.configStorePath = (session?["store"] as? String) ?? SessionLoader.defaultStorePath let loadedModel = (agent?["model"] as? String) ?? "" if !loadedModel.isEmpty { self.configModel = loadedModel @@ -171,19 +148,16 @@ struct ConfigSettings: View { self.configSaving = true defer { self.configSaving = false } - var session: [String: Any] = [:] - var agent: [String: Any] = [:] - var reply: [String: Any] = [:] - - let trimmedStore = self.configStorePath.trimmingCharacters(in: .whitespacesAndNewlines) - if !trimmedStore.isEmpty { session["store"] = trimmedStore } + var root = self.loadConfigDict() + var inbound = root["inbound"] as? [String: Any] ?? [:] + var reply = inbound["reply"] as? [String: Any] ?? [:] + var agent = reply["agent"] as? [String: Any] ?? [:] let chosenModel = (self.configModel == "__custom__" ? self.customModel : self.configModel) .trimmingCharacters(in: .whitespacesAndNewlines) let trimmedModel = chosenModel if !trimmedModel.isEmpty { agent["model"] = trimmedModel } - reply["session"] = session reply["agent"] = agent if let heartbeatMinutes { @@ -195,8 +169,8 @@ struct ConfigSettings: View { reply["heartbeatBody"] = trimmedBody } - let inbound: [String: Any] = ["reply": reply] - let root: [String: Any] = ["inbound": inbound] + inbound["reply"] = reply + root["inbound"] = inbound do { let data = try JSONSerialization.data(withJSONObject: root, options: [.prettyPrinted, .sortedKeys]) @@ -208,6 +182,12 @@ struct ConfigSettings: View { } catch {} } + private func loadConfigDict() -> [String: Any] { + let url = self.configURL() + guard let data = try? Data(contentsOf: url) else { return [:] } + return (try? JSONSerialization.jsonObject(with: data) as? [String: Any]) ?? [:] + } + private func loadModels() async { guard !self.modelsLoading else { return } self.modelsLoading = true diff --git a/apps/macos/Sources/Clawdis/DebugSettings.swift b/apps/macos/Sources/Clawdis/DebugSettings.swift index 09df172fe..914194746 100644 --- a/apps/macos/Sources/Clawdis/DebugSettings.swift +++ b/apps/macos/Sources/Clawdis/DebugSettings.swift @@ -10,6 +10,8 @@ struct DebugSettings: View { @State private var modelsError: String? @ObservedObject private var relayManager = RelayProcessManager.shared @State private var relayRootInput: String = RelayProcessManager.shared.projectRootPath() + @State private var sessionStorePath: String = SessionLoader.defaultStorePath + @State private var sessionStoreSaveError: String? var body: some View { VStack(alignment: .leading, spacing: 10) { @@ -65,6 +67,27 @@ struct DebugSettings: View { .font(.caption2) .foregroundStyle(.secondary) } + LabeledContent("Session store") { + VStack(alignment: .leading, spacing: 6) { + HStack(spacing: 8) { + TextField("Path", text: self.$sessionStorePath) + .textFieldStyle(.roundedBorder) + .font(.caption.monospaced()) + .frame(width: 340) + Button("Save") { self.saveSessionStorePath() } + .buttonStyle(.borderedProminent) + } + if let sessionStoreSaveError { + Text(sessionStoreSaveError) + .font(.footnote) + .foregroundStyle(.secondary) + } else { + Text("Used by the CLI session loader; stored in ~/.clawdis/clawdis.json.") + .font(.footnote) + .foregroundStyle(.secondary) + } + } + } LabeledContent("Model catalog") { VStack(alignment: .leading, spacing: 6) { Text(self.modelCatalogPath) @@ -115,7 +138,10 @@ struct DebugSettings: View { } .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, 12) - .task { await self.reloadModels() } + .task { + await self.reloadModels() + self.loadSessionStorePath() + } } private var pinoLogPath: String { @@ -200,4 +226,56 @@ struct DebugSettings: View { private func saveRelayRoot() { RelayProcessManager.shared.setProjectRoot(path: self.relayRootInput) } + + private func loadSessionStorePath() { + let url = self.configURL() + guard + let data = try? Data(contentsOf: url), + let parsed = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + let inbound = parsed["inbound"] as? [String: Any], + let reply = inbound["reply"] as? [String: Any], + let session = reply["session"] as? [String: Any], + let path = session["store"] as? String + else { + self.sessionStorePath = SessionLoader.defaultStorePath + return + } + self.sessionStorePath = path + } + + private func saveSessionStorePath() { + let trimmed = self.sessionStorePath.trimmingCharacters(in: .whitespacesAndNewlines) + var root: [String: Any] = [:] + let url = self.configURL() + if let data = try? Data(contentsOf: url), + let parsed = try? JSONSerialization.jsonObject(with: data) as? [String: Any] + { + root = parsed + } + + var inbound = root["inbound"] as? [String: Any] ?? [:] + var reply = inbound["reply"] as? [String: Any] ?? [:] + var session = reply["session"] as? [String: Any] ?? [:] + session["store"] = trimmed.isEmpty ? SessionLoader.defaultStorePath : trimmed + reply["session"] = session + inbound["reply"] = reply + root["inbound"] = inbound + + do { + let data = try JSONSerialization.data(withJSONObject: root, options: [.prettyPrinted, .sortedKeys]) + try FileManager.default.createDirectory( + at: url.deletingLastPathComponent(), + withIntermediateDirectories: true) + try data.write(to: url, options: [.atomic]) + self.sessionStoreSaveError = nil + } catch { + self.sessionStoreSaveError = error.localizedDescription + } + } + + private func configURL() -> URL { + FileManager.default.homeDirectoryForCurrentUser + .appendingPathComponent(".clawdis") + .appendingPathComponent("clawdis.json") + } }