mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-16 13:41:37 +00:00
* feat(mac): unified Liquid Glass pairing approval panel Replace the serial node/device pairing NSAlerts with one floating approval panel. Both prompters feed request cards into a shared PairingApprovalCenter; each card shows a hardware icon, platform and model, app/core version, source IP, a copyable short id, request age, a trust line (first connection / already-paired-id caution / repair token rotation), and a friendly access summary with an elevated warning for system.run-class commands. Requests resolved elsewhere disappear live; Not Now snoozes without resolving (gateway TTL applies) and the menu-bar pending line reopens the panel. Liquid Glass surface on macOS 26+, material fallback on macOS 15. Deletes the NSAlert + invisible-host-window machinery, dead node isRepair handling, and text-only describe() body. Closes #102535 * fix(mac): satisfy swiftformat conditionalAssignment and resync native i18n inventory
93 lines
2.8 KiB
Swift
93 lines
2.8 KiB
Swift
import Foundation
|
|
import OpenClawKit
|
|
import OSLog
|
|
|
|
/// Shared plumbing for the node/device pairing prompters: gateway push
|
|
/// subscription lifecycle and approve/reject RPC logging.
|
|
@MainActor
|
|
enum PairingPromptSupport {
|
|
enum PairingResolution: String {
|
|
case approved
|
|
case rejected
|
|
}
|
|
|
|
struct PairingResolvedEvent: Codable {
|
|
let requestId: String
|
|
let decision: String
|
|
let ts: Double
|
|
}
|
|
|
|
static func runPairingPushTask(
|
|
bufferingNewest: Int = 200,
|
|
loadPending: @escaping @MainActor () async -> Void,
|
|
handlePush: @escaping @MainActor (GatewayPush) -> Void) async
|
|
{
|
|
_ = try? await GatewayConnection.shared.refresh()
|
|
await loadPending()
|
|
await GatewayPushSubscription.consume(bufferingNewest: bufferingNewest, onPush: handlePush)
|
|
}
|
|
|
|
static func startPairingPushTask(
|
|
task: inout Task<Void, Never>?,
|
|
isStopping: inout Bool,
|
|
bufferingNewest: Int = 200,
|
|
loadPending: @escaping @MainActor () async -> Void,
|
|
handlePush: @escaping @MainActor (GatewayPush) -> Void)
|
|
{
|
|
guard task == nil else { return }
|
|
isStopping = false
|
|
task = Task {
|
|
await self.runPairingPushTask(
|
|
bufferingNewest: bufferingNewest,
|
|
loadPending: loadPending,
|
|
handlePush: handlePush)
|
|
}
|
|
}
|
|
|
|
static func stopPairingPrompter(
|
|
isStopping: inout Bool,
|
|
task: inout Task<Void, Never>?,
|
|
queue: inout [some Any])
|
|
{
|
|
isStopping = true
|
|
task?.cancel()
|
|
task = nil
|
|
queue.removeAll(keepingCapacity: false)
|
|
}
|
|
|
|
static func approveRequest(
|
|
requestId: String,
|
|
kind: String,
|
|
logger: Logger,
|
|
action: @escaping () async throws -> Void) async -> Bool
|
|
{
|
|
do {
|
|
try await action()
|
|
logger.info("approved \(kind, privacy: .public) pairing requestId=\(requestId, privacy: .public)")
|
|
return true
|
|
} catch {
|
|
logger.error("approve failed requestId=\(requestId, privacy: .public)")
|
|
logger.error("approve failed: \(error.localizedDescription, privacy: .public)")
|
|
return false
|
|
}
|
|
}
|
|
|
|
@discardableResult
|
|
static func rejectRequest(
|
|
requestId: String,
|
|
kind: String,
|
|
logger: Logger,
|
|
action: @escaping () async throws -> Void) async -> Bool
|
|
{
|
|
do {
|
|
try await action()
|
|
logger.info("rejected \(kind, privacy: .public) pairing requestId=\(requestId, privacy: .public)")
|
|
return true
|
|
} catch {
|
|
logger.error("reject failed requestId=\(requestId, privacy: .public)")
|
|
logger.error("reject failed: \(error.localizedDescription, privacy: .public)")
|
|
return false
|
|
}
|
|
}
|
|
}
|