mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
feat(mac): native pairing approval dialog with instant decisions (#106091)
* feat(mac): native pairing approval dialog with instant decisions * chore(i18n): sync native inventory and locale artifacts * chore(i18n): regenerate native inventory, locale artifacts, and projections * chore(i18n): re-sync native inventory ordering
This commit is contained in:
committed by
GitHub
parent
471f60bc84
commit
50e5ffcd25
@@ -29563,7 +29563,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 426,
|
||||
"line": 435,
|
||||
"path": "apps/macos/Sources/OpenClaw/NodePairingApprovalPrompter.swift",
|
||||
"source": "Node pairing approved",
|
||||
"surface": "apple",
|
||||
@@ -29571,7 +29571,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 426,
|
||||
"line": 435,
|
||||
"path": "apps/macos/Sources/OpenClaw/NodePairingApprovalPrompter.swift",
|
||||
"source": "Node pairing rejected",
|
||||
"surface": "apple",
|
||||
@@ -30811,7 +30811,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-call",
|
||||
"line": 59,
|
||||
"line": 40,
|
||||
"path": "apps/macos/Sources/OpenClaw/PairingApprovalPanelView.swift",
|
||||
"source": "Not Now",
|
||||
"surface": "apple",
|
||||
@@ -30819,7 +30819,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-call",
|
||||
"line": 76,
|
||||
"line": 56,
|
||||
"path": "apps/macos/Sources/OpenClaw/PairingApprovalPanelView.swift",
|
||||
"source": "Reject All",
|
||||
"surface": "apple",
|
||||
@@ -30827,7 +30827,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-call",
|
||||
"line": 83,
|
||||
"line": 63,
|
||||
"path": "apps/macos/Sources/OpenClaw/PairingApprovalPanelView.swift",
|
||||
"source": "Approve All",
|
||||
"surface": "apple",
|
||||
@@ -30835,7 +30835,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 103,
|
||||
"line": 82,
|
||||
"path": "apps/macos/Sources/OpenClaw/PairingApprovalPanelView.swift",
|
||||
"source": "Pairing Request",
|
||||
"surface": "apple",
|
||||
@@ -30843,7 +30843,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 103,
|
||||
"line": 82,
|
||||
"path": "apps/macos/Sources/OpenClaw/PairingApprovalPanelView.swift",
|
||||
"source": "Pairing Requests",
|
||||
"surface": "apple",
|
||||
@@ -30851,7 +30851,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-modifier",
|
||||
"line": 212,
|
||||
"line": 188,
|
||||
"path": "apps/macos/Sources/OpenClaw/PairingApprovalPanelView.swift",
|
||||
"source": "Copy full ID",
|
||||
"surface": "apple",
|
||||
@@ -30859,7 +30859,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 227,
|
||||
"line": 203,
|
||||
"path": "apps/macos/Sources/OpenClaw/PairingApprovalPanelView.swift",
|
||||
"source": "Approve Device",
|
||||
"surface": "apple",
|
||||
@@ -30867,7 +30867,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 227,
|
||||
"line": 203,
|
||||
"path": "apps/macos/Sources/OpenClaw/PairingApprovalPanelView.swift",
|
||||
"source": "Approve Node",
|
||||
"surface": "apple",
|
||||
@@ -30875,7 +30875,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-call",
|
||||
"line": 238,
|
||||
"line": 214,
|
||||
"path": "apps/macos/Sources/OpenClaw/PairingApprovalPanelView.swift",
|
||||
"source": "Reject",
|
||||
"surface": "apple",
|
||||
@@ -30883,7 +30883,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 295,
|
||||
"line": 271,
|
||||
"path": "apps/macos/Sources/OpenClaw/PairingApprovalPanelView.swift",
|
||||
"source": "A node wants to connect to OpenClaw.",
|
||||
"surface": "apple",
|
||||
@@ -30891,7 +30891,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 296,
|
||||
"line": 272,
|
||||
"path": "apps/macos/Sources/OpenClaw/PairingApprovalPanelView.swift",
|
||||
"source": "A device wants to connect to OpenClaw.",
|
||||
"surface": "apple",
|
||||
@@ -30899,7 +30899,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 306,
|
||||
"line": 282,
|
||||
"path": "apps/macos/Sources/OpenClaw/PairingApprovalPanelView.swift",
|
||||
"source": "New device",
|
||||
"surface": "apple",
|
||||
@@ -30907,7 +30907,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 306,
|
||||
"line": 282,
|
||||
"path": "apps/macos/Sources/OpenClaw/PairingApprovalPanelView.swift",
|
||||
"source": "OpenClaw Mac app",
|
||||
"surface": "apple",
|
||||
@@ -30915,7 +30915,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 323,
|
||||
"line": 299,
|
||||
"path": "apps/macos/Sources/OpenClaw/PairingApprovalPanelView.swift",
|
||||
"source": "Operator",
|
||||
"surface": "apple",
|
||||
@@ -30923,12 +30923,28 @@
|
||||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 523,
|
||||
"line": 499,
|
||||
"path": "apps/macos/Sources/OpenClaw/PairingApprovalPanelView.swift",
|
||||
"source": "Mac",
|
||||
"surface": "apple",
|
||||
"id": "native.apple.7a62dc7bc8d3fab9"
|
||||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 102,
|
||||
"path": "apps/macos/Sources/OpenClaw/PairingPromptSupport.swift",
|
||||
"source": "Device",
|
||||
"surface": "apple",
|
||||
"id": "native.apple.fb09a404bfd12fa9"
|
||||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 102,
|
||||
"path": "apps/macos/Sources/OpenClaw/PairingPromptSupport.swift",
|
||||
"source": "Node",
|
||||
"surface": "apple",
|
||||
"id": "native.apple.7ed2af81aa7647fb"
|
||||
},
|
||||
{
|
||||
"kind": "ui-named-argument",
|
||||
"line": 15,
|
||||
|
||||
@@ -19333,6 +19333,16 @@
|
||||
"source": "Mac",
|
||||
"translated": "Mac"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.fb09a404bfd12fa9",
|
||||
"source": "Device",
|
||||
"translated": "الجهاز"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7ed2af81aa7647fb",
|
||||
"source": "Node",
|
||||
"translated": "العقدة"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6223e5f12aa9464b",
|
||||
"source": "Permissions",
|
||||
|
||||
@@ -19333,6 +19333,16 @@
|
||||
"source": "Mac",
|
||||
"translated": "Mac"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.fb09a404bfd12fa9",
|
||||
"source": "Device",
|
||||
"translated": "Gerät"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7ed2af81aa7647fb",
|
||||
"source": "Node",
|
||||
"translated": "Knoten"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6223e5f12aa9464b",
|
||||
"source": "Permissions",
|
||||
|
||||
@@ -19333,6 +19333,16 @@
|
||||
"source": "Mac",
|
||||
"translated": "Mac"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.fb09a404bfd12fa9",
|
||||
"source": "Device",
|
||||
"translated": "Dispositivo"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7ed2af81aa7647fb",
|
||||
"source": "Node",
|
||||
"translated": "Nodo"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6223e5f12aa9464b",
|
||||
"source": "Permissions",
|
||||
|
||||
@@ -19333,6 +19333,16 @@
|
||||
"source": "Mac",
|
||||
"translated": "Mac"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.fb09a404bfd12fa9",
|
||||
"source": "Device",
|
||||
"translated": "دستگاه"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7ed2af81aa7647fb",
|
||||
"source": "Node",
|
||||
"translated": "گره"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6223e5f12aa9464b",
|
||||
"source": "Permissions",
|
||||
|
||||
@@ -19333,6 +19333,16 @@
|
||||
"source": "Mac",
|
||||
"translated": "Mac"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.fb09a404bfd12fa9",
|
||||
"source": "Device",
|
||||
"translated": "Appareil"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7ed2af81aa7647fb",
|
||||
"source": "Node",
|
||||
"translated": "Nœud"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6223e5f12aa9464b",
|
||||
"source": "Permissions",
|
||||
|
||||
@@ -19333,6 +19333,16 @@
|
||||
"source": "Mac",
|
||||
"translated": "Mac"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.fb09a404bfd12fa9",
|
||||
"source": "Device",
|
||||
"translated": "डिवाइस"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7ed2af81aa7647fb",
|
||||
"source": "Node",
|
||||
"translated": "नोड"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6223e5f12aa9464b",
|
||||
"source": "Permissions",
|
||||
|
||||
@@ -19333,6 +19333,16 @@
|
||||
"source": "Mac",
|
||||
"translated": "Mac"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.fb09a404bfd12fa9",
|
||||
"source": "Device",
|
||||
"translated": "Perangkat"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7ed2af81aa7647fb",
|
||||
"source": "Node",
|
||||
"translated": "Node"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6223e5f12aa9464b",
|
||||
"source": "Permissions",
|
||||
|
||||
@@ -19333,6 +19333,16 @@
|
||||
"source": "Mac",
|
||||
"translated": "Mac"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.fb09a404bfd12fa9",
|
||||
"source": "Device",
|
||||
"translated": "Dispositivo"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7ed2af81aa7647fb",
|
||||
"source": "Node",
|
||||
"translated": "Nodo"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6223e5f12aa9464b",
|
||||
"source": "Permissions",
|
||||
|
||||
@@ -19333,6 +19333,16 @@
|
||||
"source": "Mac",
|
||||
"translated": "Mac"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.fb09a404bfd12fa9",
|
||||
"source": "Device",
|
||||
"translated": "デバイス"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7ed2af81aa7647fb",
|
||||
"source": "Node",
|
||||
"translated": "ノード"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6223e5f12aa9464b",
|
||||
"source": "Permissions",
|
||||
|
||||
@@ -19333,6 +19333,16 @@
|
||||
"source": "Mac",
|
||||
"translated": "Mac"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.fb09a404bfd12fa9",
|
||||
"source": "Device",
|
||||
"translated": "기기"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7ed2af81aa7647fb",
|
||||
"source": "Node",
|
||||
"translated": "노드"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6223e5f12aa9464b",
|
||||
"source": "Permissions",
|
||||
|
||||
@@ -19333,6 +19333,16 @@
|
||||
"source": "Mac",
|
||||
"translated": "Mac"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.fb09a404bfd12fa9",
|
||||
"source": "Device",
|
||||
"translated": "Apparaat"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7ed2af81aa7647fb",
|
||||
"source": "Node",
|
||||
"translated": "Node"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6223e5f12aa9464b",
|
||||
"source": "Permissions",
|
||||
|
||||
@@ -19333,6 +19333,16 @@
|
||||
"source": "Mac",
|
||||
"translated": "Mac"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.fb09a404bfd12fa9",
|
||||
"source": "Device",
|
||||
"translated": "Urządzenie"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7ed2af81aa7647fb",
|
||||
"source": "Node",
|
||||
"translated": "Węzeł"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6223e5f12aa9464b",
|
||||
"source": "Permissions",
|
||||
|
||||
@@ -19333,6 +19333,16 @@
|
||||
"source": "Mac",
|
||||
"translated": "Mac"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.fb09a404bfd12fa9",
|
||||
"source": "Device",
|
||||
"translated": "Dispositivo"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7ed2af81aa7647fb",
|
||||
"source": "Node",
|
||||
"translated": "Nó"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6223e5f12aa9464b",
|
||||
"source": "Permissions",
|
||||
|
||||
@@ -19333,6 +19333,16 @@
|
||||
"source": "Mac",
|
||||
"translated": "Mac"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.fb09a404bfd12fa9",
|
||||
"source": "Device",
|
||||
"translated": "Устройство"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7ed2af81aa7647fb",
|
||||
"source": "Node",
|
||||
"translated": "Узел"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6223e5f12aa9464b",
|
||||
"source": "Permissions",
|
||||
|
||||
@@ -19333,6 +19333,16 @@
|
||||
"source": "Mac",
|
||||
"translated": "Mac"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.fb09a404bfd12fa9",
|
||||
"source": "Device",
|
||||
"translated": "Enhet"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7ed2af81aa7647fb",
|
||||
"source": "Node",
|
||||
"translated": "Nod"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6223e5f12aa9464b",
|
||||
"source": "Permissions",
|
||||
|
||||
@@ -19333,6 +19333,16 @@
|
||||
"source": "Mac",
|
||||
"translated": "Mac"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.fb09a404bfd12fa9",
|
||||
"source": "Device",
|
||||
"translated": "อุปกรณ์"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7ed2af81aa7647fb",
|
||||
"source": "Node",
|
||||
"translated": "โหนด"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6223e5f12aa9464b",
|
||||
"source": "Permissions",
|
||||
|
||||
@@ -19333,6 +19333,16 @@
|
||||
"source": "Mac",
|
||||
"translated": "Mac"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.fb09a404bfd12fa9",
|
||||
"source": "Device",
|
||||
"translated": "Aygıt"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7ed2af81aa7647fb",
|
||||
"source": "Node",
|
||||
"translated": "Düğüm"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6223e5f12aa9464b",
|
||||
"source": "Permissions",
|
||||
|
||||
@@ -19333,6 +19333,16 @@
|
||||
"source": "Mac",
|
||||
"translated": "Mac"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.fb09a404bfd12fa9",
|
||||
"source": "Device",
|
||||
"translated": "Пристрій"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7ed2af81aa7647fb",
|
||||
"source": "Node",
|
||||
"translated": "Вузол"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6223e5f12aa9464b",
|
||||
"source": "Permissions",
|
||||
|
||||
@@ -19333,6 +19333,16 @@
|
||||
"source": "Mac",
|
||||
"translated": "Mac"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.fb09a404bfd12fa9",
|
||||
"source": "Device",
|
||||
"translated": "Thiết bị"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7ed2af81aa7647fb",
|
||||
"source": "Node",
|
||||
"translated": "Nút"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6223e5f12aa9464b",
|
||||
"source": "Permissions",
|
||||
|
||||
@@ -19333,6 +19333,16 @@
|
||||
"source": "Mac",
|
||||
"translated": "Mac"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.fb09a404bfd12fa9",
|
||||
"source": "Device",
|
||||
"translated": "设备"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7ed2af81aa7647fb",
|
||||
"source": "Node",
|
||||
"translated": "节点"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6223e5f12aa9464b",
|
||||
"source": "Permissions",
|
||||
|
||||
@@ -19333,6 +19333,16 @@
|
||||
"source": "Mac",
|
||||
"translated": "Mac"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.fb09a404bfd12fa9",
|
||||
"source": "Device",
|
||||
"translated": "裝置"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.7ed2af81aa7647fb",
|
||||
"source": "Node",
|
||||
"translated": "節點"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.6223e5f12aa9464b",
|
||||
"source": "Permissions",
|
||||
|
||||
@@ -24,6 +24,9 @@ final class DevicePairingApprovalPrompter {
|
||||
/// state is unknown until fresh gateway truth applies (stale snapshots
|
||||
/// must not produce a positive "previously paired" claim).
|
||||
private var trustUnknownRequestIds: Set<String> = []
|
||||
/// Requests whose approve/reject RPC is still in flight; their cards are
|
||||
/// hidden optimistically and restored by the failure path.
|
||||
private var pendingLocalDecisionRequestIds: Set<String> = []
|
||||
|
||||
private struct PairingList: Codable {
|
||||
let pending: [PendingRequest]
|
||||
@@ -81,6 +84,7 @@ final class DevicePairingApprovalPrompter {
|
||||
task: &self.task,
|
||||
queue: &self.queue)
|
||||
PairingApprovalCenter.shared.unregister(kind: .device)
|
||||
self.pendingLocalDecisionRequestIds.removeAll(keepingCapacity: false)
|
||||
self.updatePendingCounts()
|
||||
}
|
||||
|
||||
@@ -117,7 +121,11 @@ final class DevicePairingApprovalPrompter {
|
||||
|
||||
private func syncCards() {
|
||||
guard !self.isStopping else { return }
|
||||
let cards = self.queue.map { self.card(for: $0) }
|
||||
// A pending local decision hides the card immediately (the decision is
|
||||
// optimistic); the failure path re-syncs so the card can come back.
|
||||
let cards = self.queue
|
||||
.filter { !self.pendingLocalDecisionRequestIds.contains($0.requestId) }
|
||||
.map { self.card(for: $0) }
|
||||
PairingApprovalCenter.shared.sync(kind: .device, cards: cards)
|
||||
}
|
||||
|
||||
@@ -148,21 +156,34 @@ final class DevicePairingApprovalPrompter {
|
||||
guard !self.isStopping else { return }
|
||||
guard let request = self.queue.first(where: { $0.requestId == card.requestId }) else { return }
|
||||
|
||||
switch decision {
|
||||
self.pendingLocalDecisionRequestIds.insert(request.requestId)
|
||||
// Optimistic dismiss: the card leaves the panel before the RPC
|
||||
// round-trip.
|
||||
self.syncCards()
|
||||
let rpcOk: Bool = switch decision {
|
||||
case .approve:
|
||||
if await !(self.approve(requestId: request.requestId)) {
|
||||
// Stale request (expired or superseded on the gateway): re-sync the
|
||||
// queue with gateway truth so accumulated stale cards collapse at once.
|
||||
await self.loadPendingRequestsFromGateway()
|
||||
return
|
||||
}
|
||||
await self.approve(requestId: request.requestId)
|
||||
case .reject:
|
||||
if await !(self.reject(requestId: request.requestId)) {
|
||||
// Failed reject leaves the request pending on the gateway;
|
||||
// re-sync instead of hiding a still-live card.
|
||||
await self.loadPendingRequestsFromGateway()
|
||||
return
|
||||
await self.reject(requestId: request.requestId)
|
||||
}
|
||||
self.pendingLocalDecisionRequestIds.remove(request.requestId)
|
||||
|
||||
if !rpcOk {
|
||||
// Stale request (expired/superseded/resolved elsewhere) or gateway
|
||||
// failure: re-sync with gateway truth so stale cards collapse. A
|
||||
// request that is genuinely still pending comes back, and the
|
||||
// notification explains why the optimistic dismiss did not stick.
|
||||
await self.loadPendingRequestsFromGateway()
|
||||
self.syncCards()
|
||||
if self.queue.contains(where: { $0.requestId == request.requestId }) {
|
||||
await PairingPromptSupport.notifyDecisionFailed(
|
||||
kind: .device,
|
||||
decision: decision,
|
||||
subject: PairingPromptSupport.subjectLabel(
|
||||
displayName: request.displayName,
|
||||
fallback: request.deviceId))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Discard any in-flight list snapshot: it predates this resolution
|
||||
|
||||
@@ -6,7 +6,6 @@ import OpenClawIPC
|
||||
import OpenClawKit
|
||||
import OpenClawProtocol
|
||||
import OSLog
|
||||
import UserNotifications
|
||||
|
||||
enum NodePairingReconcilePolicy {
|
||||
static let activeIntervalMs: UInt64 = 15000
|
||||
@@ -330,8 +329,13 @@ final class NodePairingApprovalPrompter {
|
||||
|
||||
private func syncCards() {
|
||||
guard !self.isStopping else { return }
|
||||
// A pending local decision hides the card immediately (the decision is
|
||||
// optimistic); the failure path re-syncs so the card can come back.
|
||||
let cards = self.queue
|
||||
.filter { !self.autoApproveInFlight.contains($0.requestId) }
|
||||
.filter {
|
||||
!self.autoApproveInFlight.contains($0.requestId) &&
|
||||
!self.pendingLocalDecisionRequestIds.contains($0.requestId)
|
||||
}
|
||||
.map { self.card(for: $0) }
|
||||
PairingApprovalCenter.shared.sync(kind: .node, cards: cards)
|
||||
}
|
||||
@@ -364,6 +368,9 @@ final class NodePairingApprovalPrompter {
|
||||
guard let request = self.queue.first(where: { $0.requestId == card.requestId }) else { return }
|
||||
|
||||
self.pendingLocalDecisionRequestIds.insert(request.requestId)
|
||||
// Optimistic dismiss: the card leaves the panel before the RPC
|
||||
// round-trip; the outcome arrives as a notification instead.
|
||||
self.syncCards()
|
||||
let expected: PairingResolution = decision == .approve ? .approved : .rejected
|
||||
let rpcOk: Bool = switch decision {
|
||||
case .approve:
|
||||
@@ -382,8 +389,16 @@ final class NodePairingApprovalPrompter {
|
||||
} else if rpcOk {
|
||||
await self.notify(resolution: expected, request: request, via: "local")
|
||||
} else {
|
||||
// RPC failed and nothing resolved it elsewhere: keep the card and
|
||||
// RPC failed and nothing resolved it elsewhere: bring the card
|
||||
// back, tell the user the optimistic dismiss did not stick, and
|
||||
// re-sync with gateway truth instead of claiming an outcome.
|
||||
self.syncCards()
|
||||
await PairingPromptSupport.notifyDecisionFailed(
|
||||
kind: .node,
|
||||
decision: decision,
|
||||
subject: PairingPromptSupport.subjectLabel(
|
||||
displayName: request.displayName,
|
||||
fallback: request.nodeId))
|
||||
self.scheduleReconcileOnce(delayMs: 0)
|
||||
return
|
||||
}
|
||||
@@ -415,17 +430,12 @@ final class NodePairingApprovalPrompter {
|
||||
}
|
||||
|
||||
private func notify(resolution: PairingResolution, request: PendingRequest, via: String) async {
|
||||
let center = UNUserNotificationCenter.current()
|
||||
let settings = await center.notificationSettings()
|
||||
guard settings.authorizationStatus == .authorized ||
|
||||
settings.authorizationStatus == .provisional
|
||||
else {
|
||||
return
|
||||
}
|
||||
guard await PairingPromptSupport.notificationsAuthorized() else { return }
|
||||
|
||||
let title = resolution == .approved ? "Node pairing approved" : "Node pairing rejected"
|
||||
let name = request.displayName?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let device = name?.isEmpty == false ? name! : request.nodeId
|
||||
let device = PairingPromptSupport.subjectLabel(
|
||||
displayName: request.displayName,
|
||||
fallback: request.nodeId)
|
||||
let body = "\(device)\n(via \(via))"
|
||||
|
||||
_ = await NotificationManager().send(
|
||||
|
||||
@@ -191,20 +191,10 @@ final class PairingApprovalCenter {
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Borderless floating panel hosting the SwiftUI approval UI. Replaces the
|
||||
/// old invisible-host-window + NSAlert sheet machinery.
|
||||
/// Floating panel with native window chrome hosting the SwiftUI approval UI,
|
||||
/// so the prompt reads like a standard system dialog.
|
||||
@MainActor
|
||||
final class PairingApprovalPanelController {
|
||||
private final class KeyablePanel: NSPanel {
|
||||
override var canBecomeKey: Bool {
|
||||
true
|
||||
}
|
||||
|
||||
override var canBecomeMain: Bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private let center: PairingApprovalCenter
|
||||
private var panel: NSPanel?
|
||||
private var hostingView: NSHostingView<PairingApprovalPanelView>?
|
||||
@@ -268,22 +258,24 @@ final class PairingApprovalPanelController {
|
||||
if let panel = self.panel {
|
||||
return panel
|
||||
}
|
||||
let panel = KeyablePanel(
|
||||
// Titled so the system draws normal dialog chrome (opaque background,
|
||||
// rounded corners, shadow, key-window focus); the title bar itself is
|
||||
// invisible and buttonless so it reads as an alert, not a document.
|
||||
let panel = NSPanel(
|
||||
contentRect: NSRect(x: 0, y: 0, width: Self.panelWidth, height: 200),
|
||||
styleMask: [.borderless, .nonactivatingPanel],
|
||||
styleMask: [.titled, .fullSizeContentView],
|
||||
backing: .buffered,
|
||||
defer: false)
|
||||
panel.isOpaque = false
|
||||
panel.backgroundColor = .clear
|
||||
// The SwiftUI view draws its own rounded shadow; the window shadow
|
||||
// would trace the square window frame and look like a border.
|
||||
panel.hasShadow = false
|
||||
panel.titleVisibility = .hidden
|
||||
panel.titlebarAppearsTransparent = true
|
||||
for buttonType in [NSWindow.ButtonType.closeButton, .miniaturizeButton, .zoomButton] {
|
||||
panel.standardWindowButton(buttonType)?.isHidden = true
|
||||
}
|
||||
panel.level = .floating
|
||||
panel.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
|
||||
panel.hidesOnDeactivate = false
|
||||
panel.isMovableByWindowBackground = true
|
||||
panel.isReleasedWhenClosed = false
|
||||
panel.becomesKeyOnlyIfNeeded = false
|
||||
|
||||
let host = NSHostingView(rootView: PairingApprovalPanelView(center: self.center))
|
||||
panel.contentView = host
|
||||
|
||||
@@ -1,32 +1,14 @@
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
|
||||
/// Floating approval UI listing every pending pairing request as a card.
|
||||
/// Liquid Glass surface on macOS 26+, material fallback on macOS 15.
|
||||
/// Approval dialog listing every pending pairing request as a card. The host
|
||||
/// panel draws native window chrome; this view only lays out the content.
|
||||
struct PairingApprovalPanelView: View {
|
||||
let center: PairingApprovalCenter
|
||||
|
||||
/// Transparent margin around the glass so the drawn shadow has room;
|
||||
/// the NSPanel shadow is disabled (it would trace a square window edge).
|
||||
static let shadowMargin: CGFloat = 32
|
||||
|
||||
var body: some View {
|
||||
self.surface
|
||||
self.content
|
||||
.frame(width: PairingApprovalPanelController.panelWidth)
|
||||
.compositingGroup()
|
||||
.shadow(color: .black.opacity(0.28), radius: 22, x: 0, y: 10)
|
||||
.padding(Self.shadowMargin)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var surface: some View {
|
||||
if #available(macOS 26.0, *) {
|
||||
self.content
|
||||
.glassEffect(.regular, in: .rect(cornerRadius: 24))
|
||||
} else {
|
||||
self.content
|
||||
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 24))
|
||||
}
|
||||
}
|
||||
|
||||
private var content: some View {
|
||||
@@ -40,7 +22,6 @@ struct PairingApprovalPanelView: View {
|
||||
ForEach(cards) { card in
|
||||
PairingRequestCardView(
|
||||
card: card,
|
||||
isBusy: self.center.decisionsInFlight.contains(card.requestId),
|
||||
isOnlyRequest: cards.count == 1,
|
||||
onDecision: { self.center.decide(card, $0) })
|
||||
}
|
||||
@@ -61,7 +42,6 @@ struct PairingApprovalPanelView: View {
|
||||
.buttonStyle(.plain)
|
||||
.foregroundStyle(.secondary)
|
||||
if cards.count > 1 {
|
||||
let allBusy = cards.allSatisfy { self.center.decisionsInFlight.contains($0.id) }
|
||||
HStack(spacing: 8) {
|
||||
notNow
|
||||
Spacer()
|
||||
@@ -85,7 +65,6 @@ struct PairingApprovalPanelView: View {
|
||||
}
|
||||
.pairingActionStyle(prominent: true)
|
||||
}
|
||||
.disabled(allBusy)
|
||||
} else {
|
||||
HStack {
|
||||
Spacer()
|
||||
@@ -113,7 +92,6 @@ struct PairingApprovalPanelView: View {
|
||||
|
||||
struct PairingRequestCardView: View {
|
||||
let card: PairingApprovalCenter.Card
|
||||
let isBusy: Bool
|
||||
let isOnlyRequest: Bool
|
||||
let onDecision: (PairingApprovalCenter.Decision) -> Void
|
||||
|
||||
@@ -149,8 +127,6 @@ struct PairingRequestCardView: View {
|
||||
}
|
||||
.padding(14)
|
||||
.background(RoundedRectangle(cornerRadius: 16).fill(.quinary))
|
||||
.disabled(self.isBusy)
|
||||
.opacity(self.isBusy ? 0.6 : 1)
|
||||
}
|
||||
|
||||
private var icon: some View {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Foundation
|
||||
import OpenClawKit
|
||||
import OSLog
|
||||
import UserNotifications
|
||||
|
||||
/// Shared plumbing for the node/device pairing prompters: gateway push
|
||||
/// subscription lifecycle and approve/reject RPC logging.
|
||||
@@ -72,6 +73,38 @@ enum PairingPromptSupport {
|
||||
}
|
||||
}
|
||||
|
||||
/// Human-readable subject for pairing notifications: display name when
|
||||
/// present, otherwise the raw node/device id.
|
||||
static func subjectLabel(displayName: String?, fallback: String) -> String {
|
||||
let name = displayName?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return name?.isEmpty == false ? name! : fallback
|
||||
}
|
||||
|
||||
static func notificationsAuthorized() async -> Bool {
|
||||
let settings = await UNUserNotificationCenter.current().notificationSettings()
|
||||
return settings.authorizationStatus == .authorized ||
|
||||
settings.authorizationStatus == .provisional
|
||||
}
|
||||
|
||||
/// Decisions resolve the card optimistically before the RPC returns; when
|
||||
/// the RPC then fails the card comes back and this explains why. A failed
|
||||
/// RPC does not prove the gateway rejected the decision (it may have
|
||||
/// committed before a timeout), so the copy claims only lost confirmation;
|
||||
/// resolved events / reconcile report the authoritative outcome.
|
||||
static func notifyDecisionFailed(
|
||||
kind: PairingApprovalCenter.Kind,
|
||||
decision: PairingApprovalCenter.Decision,
|
||||
subject: String) async
|
||||
{
|
||||
guard await self.notificationsAuthorized() else { return }
|
||||
let action = decision == .approve ? "approval" : "rejection"
|
||||
_ = await NotificationManager().send(
|
||||
title: "\(kind == .node ? "Node" : "Device") pairing \(action) not confirmed",
|
||||
body: "\(subject)\nThe gateway did not confirm the \(action); the request may still be pending.",
|
||||
sound: nil,
|
||||
priority: .active)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
static func rejectRequest(
|
||||
requestId: String,
|
||||
|
||||
Reference in New Issue
Block a user