@@ -33,7 +33,7 @@ final class DevicePairingApprovalPrompter {
3333 let remoteIp : String ?
3434 }
3535
36- private struct PendingRequest : Codable , Equatable , Identifiable {
36+ struct PendingRequest : Codable , Equatable , Identifiable {
3737 let requestId : String
3838 let deviceId : String
3939 let publicKey : String
@@ -115,14 +115,16 @@ final class DevicePairingApprovalPrompter {
115115 PairingAlertSupport . presentPairingAlert (
116116 request: req,
117117 requestId: req. requestId,
118- messageText: " Allow device to connect? " ,
119- informativeText: Self . describe ( req) ,
118+ messageText: Self . alertTitle ( for: req) ,
119+ informativeText: Self . alertSummary ( for: req) ,
120+ buttonTitles: PairingAlertSupport . ButtonTitles ( approve: Self . approveButtonTitle ( for: req) ) ,
121+ accessoryView: Self . buildAccessoryView ( for: req) ,
120122 state: self . alertState,
121123 onResponse: self . handleAlertResponse)
122124 }
123125
124126 private func handleAlertResponse( _ response: NSApplication . ModalResponse , request: PendingRequest ) async {
125- var shouldRemove = response != . alertFirstButtonReturn
127+ var shouldRemove = response != . alertSecondButtonReturn
126128 defer {
127129 if shouldRemove {
128130 if self . queue. first == request {
@@ -144,14 +146,14 @@ final class DevicePairingApprovalPrompter {
144146
145147 switch response {
146148 case . alertFirstButtonReturn:
149+ _ = await self . approve ( requestId: request. requestId)
150+ case . alertSecondButtonReturn:
147151 shouldRemove = false
148152 if let idx = self . queue. firstIndex ( of: request) {
149153 self . queue. remove ( at: idx)
150154 }
151155 self . queue. append ( request)
152156 return
153- case . alertSecondButtonReturn:
154- _ = await self . approve ( requestId: request. requestId)
155157 case . alertThirdButtonReturn:
156158 await self . reject ( requestId: request. requestId)
157159 default :
@@ -233,24 +235,166 @@ final class DevicePairingApprovalPrompter {
233235 self . updatePendingCounts ( )
234236 }
235237
236- private static func describe( _ req: PendingRequest ) -> String {
237- var lines : [ String ] = [ ]
238- lines. append ( " Device: \( req. displayName ?? req. deviceId) " )
239- if let platform = req. platform {
240- lines. append ( " Platform: \( platform) " )
238+ static func alertTitle( for req: PendingRequest ) -> String {
239+ self . isMac ( req. platform) ? " New Mac wants to connect " : " New device wants to connect "
240+ }
241+
242+ static func alertSummary( for req: PendingRequest ) -> String {
243+ let subject = self . isMac ( req. platform) ? " this Mac app " : " this device "
244+ return " Approve \( subject) to control OpenClaw. Only approve if this is yours; you can remove it later in Settings. "
245+ }
246+
247+ static func approveButtonTitle( for req: PendingRequest ) -> String {
248+ self . isMac ( req. platform) ? " Approve Mac " : " Approve Device "
249+ }
250+
251+ static func buildAccessoryView( for req: PendingRequest ) -> NSView {
252+ let stack = NSStackView ( )
253+ stack. orientation = . vertical
254+ stack. alignment = . leading
255+ stack. spacing = 8
256+ stack. edgeInsets = NSEdgeInsets ( top: 2 , left: 0 , bottom: 0 , right: 0 )
257+
258+ stack. addArrangedSubview ( self . makeValueRow ( label: " Device " , value: self . deviceName ( for: req) ) )
259+ if let platform = self . prettyPlatform ( req. platform) {
260+ stack. addArrangedSubview ( self . makeValueRow ( label: " Platform " , value: platform) )
241261 }
242- if let role = req. role {
243- lines . append ( " Role: \( role) " )
262+ if let role = self . prettyRole ( req. role) {
263+ stack . addArrangedSubview ( self . makeValueRow ( label : " Role " , value : role) )
244264 }
245- if let scopes = req. scopes, !scopes. isEmpty {
246- lines. append ( " Scopes: \( scopes. joined ( separator: " , " ) ) " )
265+ let accessItems = self . friendlyScopeNames ( req. scopes)
266+ if !accessItems. isEmpty {
267+ stack. addArrangedSubview ( self . makeSectionLabel ( " Access requested " ) )
268+ for item in accessItems {
269+ stack. addArrangedSubview ( self . makeBullet ( item) )
270+ }
247271 }
248- if let remoteIp = req. remoteIp {
249- lines. append ( " IP: \( remoteIp) " )
272+ stack. addArrangedSubview ( self . makeDetailLine ( req) )
273+
274+ let fitting = stack. fittingSize
275+ stack. frame = NSRect ( x: 0 , y: 0 , width: 420 , height: fitting. height)
276+ return stack
277+ }
278+
279+ static func deviceName( for req: PendingRequest ) -> String {
280+ let trimmedName = req. displayName? . trimmingCharacters ( in: . whitespacesAndNewlines)
281+ if let trimmedName, !trimmedName. isEmpty, trimmedName != req. deviceId {
282+ return trimmedName
283+ }
284+ return self . isMac ( req. platform) ? " OpenClaw Mac app " : " New device "
285+ }
286+
287+ static func prettyPlatform( _ raw: String ? ) -> String ? {
288+ let platform = raw? . trimmingCharacters ( in: . whitespacesAndNewlines)
289+ guard let platform, !platform. isEmpty else { return nil }
290+ switch platform. lowercased ( ) {
291+ case " macintel " , " x86_64-apple-darwin " :
292+ return " Mac (Intel) "
293+ case " macarm " , " macarm64 " , " arm64-apple-darwin " , " aarch64-apple-darwin " :
294+ return " Mac (Apple silicon) "
295+ case " darwin " :
296+ return " Mac "
297+ default :
298+ if platform. lowercased ( ) . contains ( " mac " ) {
299+ return " Mac "
300+ }
301+ return platform
302+ }
303+ }
304+
305+ static func prettyRole( _ raw: String ? ) -> String ? {
306+ let role = raw? . trimmingCharacters ( in: . whitespacesAndNewlines)
307+ guard let role, !role. isEmpty else { return nil }
308+ return role == " operator " ? " Operator " : role
309+ }
310+
311+ static func friendlyScopeNames( _ scopes: [ String ] ? ) -> [ String ] {
312+ guard let scopes else { return [ ] }
313+ var seen = Set < String > ( )
314+ return scopes. compactMap { scope in
315+ let normalized = scope. trimmingCharacters ( in: . whitespacesAndNewlines)
316+ guard !normalized. isEmpty, seen. insert ( normalized) . inserted else { return nil }
317+ switch normalized {
318+ case " operator.admin " :
319+ return " Admin access "
320+ case " operator.read " :
321+ return " Read OpenClaw data "
322+ case " operator.write " :
323+ return " Send messages and make changes "
324+ case " operator.approvals " :
325+ return " Manage approvals "
326+ case " operator.pairing " :
327+ return " Pair and repair devices "
328+ case " operator.talk.secrets " :
329+ return " Use Talk credentials "
330+ default :
331+ return normalized
332+ }
333+ }
334+ }
335+
336+ static func shortIdentifier( _ id: String ) -> String {
337+ let trimmed = id. trimmingCharacters ( in: . whitespacesAndNewlines)
338+ guard trimmed. count > 20 else { return trimmed }
339+ return " \( trimmed. prefix ( 8 ) ) ... \( trimmed. suffix ( 7 ) ) "
340+ }
341+
342+ private static func isMac( _ platform: String ? ) -> Bool {
343+ guard let platform else { return false }
344+ let lower = platform. lowercased ( )
345+ return lower. contains ( " mac " ) || lower. contains ( " darwin " )
346+ }
347+
348+ private static func makeValueRow( label: String , value: String ) -> NSView {
349+ let row = NSStackView ( )
350+ row. orientation = . horizontal
351+ row. alignment = . firstBaseline
352+ row. spacing = 8
353+
354+ let labelField = self . makeLabel ( " \( label) : " , font: . systemFont( ofSize: 12 , weight: . semibold) )
355+ labelField. textColor = . secondaryLabelColor
356+ labelField. setContentHuggingPriority ( . required, for: . horizontal)
357+ let valueField = self . makeLabel ( value, font: . systemFont( ofSize: 12 , weight: . regular) )
358+ valueField. maximumNumberOfLines = 2
359+
360+ row. addArrangedSubview ( labelField)
361+ row. addArrangedSubview ( valueField)
362+ return row
363+ }
364+
365+ private static func makeSectionLabel( _ text: String ) -> NSTextField {
366+ let label = self . makeLabel ( text, font: . systemFont( ofSize: 12 , weight: . semibold) )
367+ label. textColor = . secondaryLabelColor
368+ return label
369+ }
370+
371+ private static func makeBullet( _ text: String ) -> NSTextField {
372+ let label = self . makeLabel ( " • \( text) " , font: . systemFont( ofSize: 12 , weight: . regular) )
373+ label. maximumNumberOfLines = 2
374+ return label
375+ }
376+
377+ private static func makeDetailLine( _ req: PendingRequest ) -> NSTextField {
378+ var parts = [ " ID \( self . shortIdentifier ( req. deviceId) ) " ]
379+ if let remoteIp = req. remoteIp? . trimmingCharacters ( in: . whitespacesAndNewlines) , !remoteIp. isEmpty {
380+ parts. append ( " IP \( remoteIp. replacingOccurrences ( of: " ::ffff: " , with: " " ) ) " )
250381 }
251382 if req. isRepair == true {
252- lines . append ( " Repair: yes " )
383+ parts . append ( " repair request " )
253384 }
254- return lines. joined ( separator: " \n " )
385+ let label = self . makeLabel (
386+ parts. joined ( separator: " · " ) ,
387+ font: . monospacedSystemFont( ofSize: 11 , weight: . regular) )
388+ label. textColor = . tertiaryLabelColor
389+ label. maximumNumberOfLines = 2
390+ return label
391+ }
392+
393+ private static func makeLabel( _ text: String , font: NSFont ) -> NSTextField {
394+ let label = NSTextField ( labelWithString: text)
395+ label. font = font
396+ label. lineBreakMode = . byWordWrapping
397+ label. textColor = . labelColor
398+ return label
255399 }
256400}
0 commit comments