fix: route macOS remote config via gateway
This commit is contained in:
@@ -26,6 +26,7 @@
|
|||||||
- macOS codesign: skip hardened runtime for ad-hoc signing and avoid empty options args (#70) — thanks @petter-b
|
- macOS codesign: skip hardened runtime for ad-hoc signing and avoid empty options args (#70) — thanks @petter-b
|
||||||
- macOS packaging: move rpath config into swift build for reliability (#69) — thanks @petter-b
|
- macOS packaging: move rpath config into swift build for reliability (#69) — thanks @petter-b
|
||||||
- macOS: prioritize main bundle for device resources to prevent crash (#73) — thanks @petter-b
|
- macOS: prioritize main bundle for device resources to prevent crash (#73) — thanks @petter-b
|
||||||
|
- macOS remote: route settings through gateway config and avoid local config reads in remote mode.
|
||||||
- Chat UI: clear composer input immediately and allow clear while editing to prevent duplicate sends (#72) — thanks @hrdwdmrbl
|
- Chat UI: clear composer input immediately and allow clear while editing to prevent duplicate sends (#72) — thanks @hrdwdmrbl
|
||||||
- Restart: use systemd on Linux (and report actual restart method) instead of always launchctl.
|
- Restart: use systemd on Linux (and report actual restart method) instead of always launchctl.
|
||||||
- Gateway relay: detect Bun binaries via execPath to resolve packaged assets on macOS.
|
- Gateway relay: detect Bun binaries via execPath to resolve packaged assets on macOS.
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ struct ConfigSettings: View {
|
|||||||
guard !self.hasLoaded else { return }
|
guard !self.hasLoaded else { return }
|
||||||
guard !self.isPreview else { return }
|
guard !self.isPreview else { return }
|
||||||
self.hasLoaded = true
|
self.hasLoaded = true
|
||||||
self.loadConfig()
|
await self.loadConfig()
|
||||||
await self.loadModels()
|
await self.loadModels()
|
||||||
await self.refreshGatewayTalkApiKey()
|
await self.refreshGatewayTalkApiKey()
|
||||||
self.allowAutosave = true
|
self.allowAutosave = true
|
||||||
@@ -369,8 +369,8 @@ struct ConfigSettings: View {
|
|||||||
.padding(.top, 2)
|
.padding(.top, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadConfig() {
|
private func loadConfig() async {
|
||||||
let parsed = self.loadConfigDict()
|
let parsed = await ConfigStore.load()
|
||||||
let agent = parsed["agent"] as? [String: Any]
|
let agent = parsed["agent"] as? [String: Any]
|
||||||
let heartbeatMinutes = agent?["heartbeatMinutes"] as? Int
|
let heartbeatMinutes = agent?["heartbeatMinutes"] as? Int
|
||||||
let heartbeatBody = agent?["heartbeatBody"] as? String
|
let heartbeatBody = agent?["heartbeatBody"] as? String
|
||||||
@@ -429,7 +429,7 @@ struct ConfigSettings: View {
|
|||||||
self.configSaving = true
|
self.configSaving = true
|
||||||
defer { self.configSaving = false }
|
defer { self.configSaving = false }
|
||||||
|
|
||||||
var root = self.loadConfigDict()
|
var root = await ConfigStore.load()
|
||||||
var agent = root["agent"] as? [String: Any] ?? [:]
|
var agent = root["agent"] as? [String: Any] ?? [:]
|
||||||
var browser = root["browser"] as? [String: Any] ?? [:]
|
var browser = root["browser"] as? [String: Any] ?? [:]
|
||||||
var talk = root["talk"] as? [String: Any] ?? [:]
|
var talk = root["talk"] as? [String: Any] ?? [:]
|
||||||
@@ -473,11 +473,11 @@ struct ConfigSettings: View {
|
|||||||
talk["interruptOnSpeech"] = self.talkInterruptOnSpeech
|
talk["interruptOnSpeech"] = self.talkInterruptOnSpeech
|
||||||
root["talk"] = talk
|
root["talk"] = talk
|
||||||
|
|
||||||
ClawdisConfigFile.saveDict(root)
|
do {
|
||||||
}
|
try await ConfigStore.save(root)
|
||||||
|
} catch {
|
||||||
private func loadConfigDict() -> [String: Any] {
|
self.modelError = error.localizedDescription
|
||||||
ClawdisConfigFile.loadDict()
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var browserColor: Color {
|
private var browserColor: Color {
|
||||||
|
|||||||
48
apps/macos/Sources/Clawdis/ConfigStore.swift
Normal file
48
apps/macos/Sources/Clawdis/ConfigStore.swift
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum ConfigStore {
|
||||||
|
private static func isRemoteMode() async -> Bool {
|
||||||
|
await MainActor.run { AppStateStore.shared.connectionMode == .remote }
|
||||||
|
}
|
||||||
|
|
||||||
|
static func load() async -> [String: Any] {
|
||||||
|
if await self.isRemoteMode() {
|
||||||
|
return await self.loadFromGateway()
|
||||||
|
}
|
||||||
|
return ClawdisConfigFile.loadDict()
|
||||||
|
}
|
||||||
|
|
||||||
|
static func save(_ root: [String: Any]) async throws {
|
||||||
|
if await self.isRemoteMode() {
|
||||||
|
try await self.saveToGateway(root)
|
||||||
|
} else {
|
||||||
|
ClawdisConfigFile.saveDict(root)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func loadFromGateway() async -> [String: Any] {
|
||||||
|
do {
|
||||||
|
let snap: ConfigSnapshot = try await GatewayConnection.shared.requestDecoded(
|
||||||
|
method: .configGet,
|
||||||
|
params: nil,
|
||||||
|
timeoutMs: 8000)
|
||||||
|
return snap.config?.mapValues { $0.foundationValue } ?? [:]
|
||||||
|
} catch {
|
||||||
|
return [:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func saveToGateway(_ root: [String: Any]) async throws {
|
||||||
|
let data = try JSONSerialization.data(withJSONObject: root, options: [.prettyPrinted, .sortedKeys])
|
||||||
|
guard let raw = String(data: data, encoding: .utf8) else {
|
||||||
|
throw NSError(domain: "ConfigStore", code: 1, userInfo: [
|
||||||
|
NSLocalizedDescriptionKey: "Failed to encode config."
|
||||||
|
])
|
||||||
|
}
|
||||||
|
let params: [String: AnyCodable] = ["raw": AnyCodable(raw)]
|
||||||
|
_ = try await GatewayConnection.shared.requestRaw(
|
||||||
|
method: .configSet,
|
||||||
|
params: params,
|
||||||
|
timeoutMs: 10000)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,6 +45,13 @@ enum DebugActions {
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
static func openSessionStore() {
|
static func openSessionStore() {
|
||||||
|
if AppStateStore.shared.connectionMode == .remote {
|
||||||
|
let alert = NSAlert()
|
||||||
|
alert.messageText = "Remote mode"
|
||||||
|
alert.informativeText = "Session store lives on the gateway host in remote mode."
|
||||||
|
alert.runModal()
|
||||||
|
return
|
||||||
|
}
|
||||||
let path = self.resolveSessionStorePath()
|
let path = self.resolveSessionStorePath()
|
||||||
let url = URL(fileURLWithPath: path)
|
let url = URL(fileURLWithPath: path)
|
||||||
if FileManager.default.fileExists(atPath: path) {
|
if FileManager.default.fileExists(atPath: path) {
|
||||||
|
|||||||
@@ -219,6 +219,9 @@ enum GatewayEnvironment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static func preferredGatewayBind() -> String? {
|
private static func preferredGatewayBind() -> String? {
|
||||||
|
if CommandResolver.connectionModeIsRemote() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if let env = ProcessInfo.processInfo.environment["CLAWDIS_GATEWAY_BIND"] {
|
if let env = ProcessInfo.processInfo.environment["CLAWDIS_GATEWAY_BIND"] {
|
||||||
let trimmed = env.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
let trimmed = env.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||||
if self.supportedBindModes.contains(trimmed) {
|
if self.supportedBindModes.contains(trimmed) {
|
||||||
|
|||||||
@@ -117,6 +117,9 @@ enum GatewayLaunchAgentManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static func preferredGatewayBind() -> String? {
|
private static func preferredGatewayBind() -> String? {
|
||||||
|
if CommandResolver.connectionModeIsRemote() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if let env = ProcessInfo.processInfo.environment["CLAWDIS_GATEWAY_BIND"] {
|
if let env = ProcessInfo.processInfo.environment["CLAWDIS_GATEWAY_BIND"] {
|
||||||
let trimmed = env.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
let trimmed = env.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||||
if self.supportedBindModes.contains(trimmed) {
|
if self.supportedBindModes.contains(trimmed) {
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ struct MenuContent: View {
|
|||||||
get: { self.browserControlEnabled },
|
get: { self.browserControlEnabled },
|
||||||
set: { enabled in
|
set: { enabled in
|
||||||
self.browserControlEnabled = enabled
|
self.browserControlEnabled = enabled
|
||||||
ClawdisConfigFile.setBrowserControlEnabled(enabled)
|
Task { await self.saveBrowserControlEnabled(enabled) }
|
||||||
})) {
|
})) {
|
||||||
Label("Browser Control", systemImage: "globe")
|
Label("Browser Control", systemImage: "globe")
|
||||||
}
|
}
|
||||||
@@ -140,8 +140,8 @@ struct MenuContent: View {
|
|||||||
.onChange(of: self.state.voicePushToTalkEnabled) { _, enabled in
|
.onChange(of: self.state.voicePushToTalkEnabled) { _, enabled in
|
||||||
VoicePushToTalkHotkey.shared.setEnabled(voiceWakeSupported && enabled)
|
VoicePushToTalkHotkey.shared.setEnabled(voiceWakeSupported && enabled)
|
||||||
}
|
}
|
||||||
.onAppear {
|
.task(id: self.state.connectionMode) {
|
||||||
self.browserControlEnabled = ClawdisConfigFile.browserControlEnabled()
|
await self.loadBrowserControlEnabled()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,6 +156,25 @@ struct MenuContent: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func loadBrowserControlEnabled() async {
|
||||||
|
let root = await ConfigStore.load()
|
||||||
|
let browser = root["browser"] as? [String: Any]
|
||||||
|
let enabled = browser?["enabled"] as? Bool ?? true
|
||||||
|
await MainActor.run { self.browserControlEnabled = enabled }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func saveBrowserControlEnabled(_ enabled: Bool) async {
|
||||||
|
var root = await ConfigStore.load()
|
||||||
|
var browser = root["browser"] as? [String: Any] ?? [:]
|
||||||
|
browser["enabled"] = enabled
|
||||||
|
root["browser"] = browser
|
||||||
|
do {
|
||||||
|
try await ConfigStore.save(root)
|
||||||
|
} catch {
|
||||||
|
await self.loadBrowserControlEnabled()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var debugMenu: some View {
|
private var debugMenu: some View {
|
||||||
if self.state.debugPaneEnabled {
|
if self.state.debugPaneEnabled {
|
||||||
|
|||||||
@@ -54,8 +54,8 @@ extension OnboardingView {
|
|||||||
.task {
|
.task {
|
||||||
await self.refreshPerms()
|
await self.refreshPerms()
|
||||||
self.refreshCLIStatus()
|
self.refreshCLIStatus()
|
||||||
self.loadWorkspaceDefaults()
|
await self.loadWorkspaceDefaults()
|
||||||
self.ensureDefaultWorkspace()
|
await self.ensureDefaultWorkspace()
|
||||||
self.refreshAnthropicOAuthStatus()
|
self.refreshAnthropicOAuthStatus()
|
||||||
self.refreshBootstrapStatus()
|
self.refreshBootstrapStatus()
|
||||||
self.preferredGatewayID = BridgeDiscoveryPreferences.preferredStableID()
|
self.preferredGatewayID = BridgeDiscoveryPreferences.preferredStableID()
|
||||||
|
|||||||
@@ -564,9 +564,14 @@ extension OnboardingView {
|
|||||||
.disabled(self.workspaceApplying)
|
.disabled(self.workspaceApplying)
|
||||||
|
|
||||||
Button("Save in config") {
|
Button("Save in config") {
|
||||||
let url = AgentWorkspace.resolveWorkspaceURL(from: self.workspacePath)
|
Task {
|
||||||
ClawdisConfigFile.setAgentWorkspace(AgentWorkspace.displayPath(for: url))
|
let url = AgentWorkspace.resolveWorkspaceURL(from: self.workspacePath)
|
||||||
self.workspaceStatus = "Saved to ~/.clawdis/clawdis.json (agent.workspace)"
|
let saved = await self.saveAgentWorkspace(AgentWorkspace.displayPath(for: url))
|
||||||
|
if saved {
|
||||||
|
self.workspaceStatus =
|
||||||
|
"Saved to ~/.clawdis/clawdis.json (agent.workspace)"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.buttonStyle(.bordered)
|
.buttonStyle(.bordered)
|
||||||
.disabled(self.workspaceApplying)
|
.disabled(self.workspaceApplying)
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
extension OnboardingView {
|
extension OnboardingView {
|
||||||
func loadWorkspaceDefaults() {
|
func loadWorkspaceDefaults() async {
|
||||||
guard self.workspacePath.isEmpty else { return }
|
guard self.workspacePath.isEmpty else { return }
|
||||||
let configured = ClawdisConfigFile.agentWorkspace()
|
let configured = await self.loadAgentWorkspace()
|
||||||
let url = AgentWorkspace.resolveWorkspaceURL(from: configured)
|
let url = AgentWorkspace.resolveWorkspaceURL(from: configured)
|
||||||
self.workspacePath = AgentWorkspace.displayPath(for: url)
|
self.workspacePath = AgentWorkspace.displayPath(for: url)
|
||||||
self.refreshBootstrapStatus()
|
self.refreshBootstrapStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
func ensureDefaultWorkspace() {
|
func ensureDefaultWorkspace() async {
|
||||||
guard self.state.connectionMode == .local else { return }
|
guard self.state.connectionMode == .local else { return }
|
||||||
let configured = ClawdisConfigFile.agentWorkspace()
|
let configured = await self.loadAgentWorkspace()
|
||||||
let url = AgentWorkspace.resolveWorkspaceURL(from: configured)
|
let url = AgentWorkspace.resolveWorkspaceURL(from: configured)
|
||||||
switch AgentWorkspace.bootstrapSafety(for: url) {
|
switch AgentWorkspace.bootstrapSafety(for: url) {
|
||||||
case .safe:
|
case .safe:
|
||||||
do {
|
do {
|
||||||
_ = try AgentWorkspace.bootstrap(workspaceURL: url)
|
_ = try AgentWorkspace.bootstrap(workspaceURL: url)
|
||||||
if (configured ?? "").trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
if (configured ?? "").trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||||
ClawdisConfigFile.setAgentWorkspace(AgentWorkspace.displayPath(for: url))
|
await self.saveAgentWorkspace(AgentWorkspace.displayPath(for: url))
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
self.workspaceStatus = "Failed to create workspace: \(error.localizedDescription)"
|
self.workspaceStatus = "Failed to create workspace: \(error.localizedDescription)"
|
||||||
@@ -66,4 +66,33 @@ extension OnboardingView {
|
|||||||
self.workspaceStatus = "Failed to create workspace: \(error.localizedDescription)"
|
self.workspaceStatus = "Failed to create workspace: \(error.localizedDescription)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func loadAgentWorkspace() async -> String? {
|
||||||
|
let root = await ConfigStore.load()
|
||||||
|
let agent = root["agent"] as? [String: Any]
|
||||||
|
return agent?["workspace"] as? String
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveAgentWorkspace(_ workspace: String?) async -> Bool {
|
||||||
|
var root = await ConfigStore.load()
|
||||||
|
var agent = root["agent"] as? [String: Any] ?? [:]
|
||||||
|
let trimmed = workspace?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||||
|
if trimmed.isEmpty {
|
||||||
|
agent.removeValue(forKey: "workspace")
|
||||||
|
} else {
|
||||||
|
agent["workspace"] = trimmed
|
||||||
|
}
|
||||||
|
if agent.isEmpty {
|
||||||
|
root.removeValue(forKey: "agent")
|
||||||
|
} else {
|
||||||
|
root["agent"] = agent
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
try await ConfigStore.save(root)
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
self.workspaceStatus = "Failed to save config: \(error.localizedDescription)"
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ struct TailscaleIntegrationSection: View {
|
|||||||
.disabled(self.connectionMode != .local)
|
.disabled(self.connectionMode != .local)
|
||||||
.task {
|
.task {
|
||||||
guard !self.hasLoaded else { return }
|
guard !self.hasLoaded else { return }
|
||||||
self.loadConfig()
|
await self.loadConfig()
|
||||||
self.hasLoaded = true
|
self.hasLoaded = true
|
||||||
await self.effectiveService.checkTailscaleStatus()
|
await self.effectiveService.checkTailscaleStatus()
|
||||||
self.startStatusTimer()
|
self.startStatusTimer()
|
||||||
@@ -113,10 +113,10 @@ struct TailscaleIntegrationSection: View {
|
|||||||
self.stopStatusTimer()
|
self.stopStatusTimer()
|
||||||
}
|
}
|
||||||
.onChange(of: self.tailscaleMode) { _, _ in
|
.onChange(of: self.tailscaleMode) { _, _ in
|
||||||
self.applySettings()
|
Task { await self.applySettings() }
|
||||||
}
|
}
|
||||||
.onChange(of: self.requireCredentialsForServe) { _, _ in
|
.onChange(of: self.requireCredentialsForServe) { _, _ in
|
||||||
self.applySettings()
|
Task { await self.applySettings() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,17 +233,18 @@ struct TailscaleIntegrationSection: View {
|
|||||||
SecureField("Password", text: self.$password)
|
SecureField("Password", text: self.$password)
|
||||||
.textFieldStyle(.roundedBorder)
|
.textFieldStyle(.roundedBorder)
|
||||||
.frame(maxWidth: 240)
|
.frame(maxWidth: 240)
|
||||||
.onSubmit { self.applySettings() }
|
.onSubmit { Task { await self.applySettings() } }
|
||||||
Text("Stored in ~/.clawdis/clawdis.json. Prefer CLAWDIS_GATEWAY_PASSWORD for production.")
|
Text("Stored in ~/.clawdis/clawdis.json. Prefer CLAWDIS_GATEWAY_PASSWORD for production.")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
Button("Update password") { self.applySettings() }
|
Button("Update password") { Task { await self.applySettings() } }
|
||||||
.buttonStyle(.bordered)
|
.buttonStyle(.bordered)
|
||||||
.controlSize(.small)
|
.controlSize(.small)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadConfig() {
|
private func loadConfig() async {
|
||||||
let gateway = ClawdisConfigFile.loadGatewayDict()
|
let root = await ConfigStore.load()
|
||||||
|
let gateway = root["gateway"] as? [String: Any] ?? [:]
|
||||||
let tailscale = gateway["tailscale"] as? [String: Any] ?? [:]
|
let tailscale = gateway["tailscale"] as? [String: Any] ?? [:]
|
||||||
let modeRaw = (tailscale["mode"] as? String) ?? "serve"
|
let modeRaw = (tailscale["mode"] as? String) ?? "serve"
|
||||||
self.tailscaleMode = GatewayTailscaleMode(rawValue: modeRaw) ?? .off
|
self.tailscaleMode = GatewayTailscaleMode(rawValue: modeRaw) ?? .off
|
||||||
@@ -266,7 +267,7 @@ struct TailscaleIntegrationSection: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func applySettings() {
|
private func applySettings() async {
|
||||||
guard self.hasLoaded else { return }
|
guard self.hasLoaded else { return }
|
||||||
self.validationMessage = nil
|
self.validationMessage = nil
|
||||||
self.statusMessage = nil
|
self.statusMessage = nil
|
||||||
@@ -279,18 +280,20 @@ struct TailscaleIntegrationSection: View {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ClawdisConfigFile.updateGatewayDict { gateway in
|
var root = await ConfigStore.load()
|
||||||
var tailscale = gateway["tailscale"] as? [String: Any] ?? [:]
|
var gateway = root["gateway"] as? [String: Any] ?? [:]
|
||||||
tailscale["mode"] = self.tailscaleMode.rawValue
|
var tailscale = gateway["tailscale"] as? [String: Any] ?? [:]
|
||||||
gateway["tailscale"] = tailscale
|
tailscale["mode"] = self.tailscaleMode.rawValue
|
||||||
|
gateway["tailscale"] = tailscale
|
||||||
|
|
||||||
if self.tailscaleMode != .off {
|
if self.tailscaleMode != .off {
|
||||||
gateway["bind"] = "loopback"
|
gateway["bind"] = "loopback"
|
||||||
}
|
}
|
||||||
|
|
||||||
guard self.tailscaleMode != .off else { return }
|
if self.tailscaleMode == .off {
|
||||||
|
gateway.removeValue(forKey: "auth")
|
||||||
|
} else {
|
||||||
var auth = gateway["auth"] as? [String: Any] ?? [:]
|
var auth = gateway["auth"] as? [String: Any] ?? [:]
|
||||||
|
|
||||||
if self.tailscaleMode == .serve, !self.requireCredentialsForServe {
|
if self.tailscaleMode == .serve, !self.requireCredentialsForServe {
|
||||||
auth["allowTailscale"] = true
|
auth["allowTailscale"] = true
|
||||||
auth.removeValue(forKey: "mode")
|
auth.removeValue(forKey: "mode")
|
||||||
@@ -308,6 +311,19 @@ struct TailscaleIntegrationSection: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if gateway.isEmpty {
|
||||||
|
root.removeValue(forKey: "gateway")
|
||||||
|
} else {
|
||||||
|
root["gateway"] = gateway
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
try await ConfigStore.save(root)
|
||||||
|
} catch {
|
||||||
|
self.statusMessage = error.localizedDescription
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if self.connectionMode == .local, !self.isPaused {
|
if self.connectionMode == .local, !self.isPaused {
|
||||||
self.statusMessage = "Saved to ~/.clawdis/clawdis.json. Restarting gateway…"
|
self.statusMessage = "Saved to ~/.clawdis/clawdis.json. Restarting gateway…"
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user