Files
openclaw/apps/macos/Sources/OpenClaw/GatewayConfigConflict.swift
Peter Steinberger 0335317679 fix(macos): preserve external remote gateway config edits (#118046)
* fix(macos): reconcile external gateway config edits

* refactor(macos): split gateway reconciliation helpers

* chore(macos): refresh native i18n inventory

* refactor(macos): remove obsolete gateway config helper

* test(macos): seed watcher fixtures as canonical

* fix(macos): add gateway config conflict recovery
2026-08-02 11:21:07 -07:00

137 lines
4.2 KiB
Swift

import Foundation
import SwiftUI
extension AppState {
enum GatewayConfigField: String, CaseIterable {
case mode = "gateway.mode"
case remoteTransport = "gateway.remote.transport"
case remoteUrl = "gateway.remote.url"
case remoteTarget = "gateway.remote.sshTarget"
case remoteIdentity = "gateway.remote.sshIdentity"
case remoteHostKeyPolicy = "gateway.remote.sshHostKeyPolicy"
case remoteToken = "gateway.remote.token"
var remoteKey: String? {
switch self {
case .mode:
nil
case .remoteTransport:
"transport"
case .remoteUrl:
"url"
case .remoteTarget:
"sshTarget"
case .remoteIdentity:
"sshIdentity"
case .remoteHostKeyPolicy:
"sshHostKeyPolicy"
case .remoteToken:
"token"
}
}
var displayName: String {
switch self {
case .mode:
String(localized: "Gateway location")
case .remoteTransport:
String(localized: "Transport")
case .remoteUrl:
String(localized: "Gateway URL")
case .remoteTarget:
String(localized: "SSH target")
case .remoteIdentity:
String(localized: "Identity file")
case .remoteHostKeyPolicy:
String(localized: "SSH host key policy")
case .remoteToken:
String(localized: "Gateway token")
}
}
}
struct GatewayConfigConflict: Equatable {
let fields: [GatewayConfigField]
let fieldNames: [String]
let message: String
}
enum GatewayConfigValue: Equatable {
case missing
case json(Data)
}
struct GatewayConfigSnapshot {
static let empty = GatewayConfigSnapshot(values: [:])
let values: [GatewayConfigField: GatewayConfigValue]
subscript(field: GatewayConfigField) -> GatewayConfigValue {
self.values[field] ?? .missing
}
}
struct RemoteGatewayConfigDraft {
var transport: RemoteTransport
var remoteUrl: String
var remoteHost: String?
var remoteTarget: String
var remoteIdentity: String
var remoteToken: String
var dirtyFields: Set<GatewayConfigField>
}
struct GatewayConfigSyncDraft {
var connectionMode: ConnectionMode
var remoteTransport: RemoteTransport
var remoteTarget: String
var remoteIdentity: String
var remoteUrl: String
var remoteToken: String
var dirtyFields: Set<GatewayConfigField>
}
struct GatewaySelectionSnapshot: Equatable {
let connectionMode: ConnectionMode
let remoteTransport: RemoteTransport
let remoteUrl: String
let remoteTarget: String
}
}
struct GatewayConfigConflictRecoveryView: View {
@Bindable var state: AppState
var body: some View {
if let conflict = state.gatewayConfigConflict {
HStack(alignment: .top, spacing: 10) {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundStyle(.orange)
.accessibilityHidden(true)
VStack(alignment: .leading, spacing: 8) {
Text(verbatim: conflict.message)
.font(.footnote)
.fixedSize(horizontal: false, vertical: true)
HStack(spacing: 8) {
Button("Use file version") {
self.state.useFileGatewayConfigConflict()
}
.buttonStyle(.bordered)
Button("Keep my edits") {
self.state.keepGatewayConfigEdits()
}
.buttonStyle(.borderedProminent)
}
.controlSize(.small)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 14)
.padding(.vertical, 11)
.background(.orange.opacity(0.08))
}
}
}