diff --git a/mac/VibeTunnel/Core/Services/SystemPermissionManager.swift b/mac/VibeTunnel/Core/Services/SystemPermissionManager.swift index b3284943..a166209d 100644 --- a/mac/VibeTunnel/Core/Services/SystemPermissionManager.swift +++ b/mac/VibeTunnel/Core/Services/SystemPermissionManager.swift @@ -124,6 +124,25 @@ final class SystemPermissionManager { requestPermission(permission) } } + + /// Force a permission recheck (useful when user manually changes settings) + func forcePermissionRecheck() { + logger.info("Force permission recheck requested") + + // Clear any cached values + permissions[.accessibility] = false + permissions[.screenRecording] = false + permissions[.appleScript] = false + + // Immediate check + Task { @MainActor in + await checkAllPermissions() + + // Double-check after a delay to catch any async updates + try? await Task.sleep(for: .milliseconds(500)) + await checkAllPermissions() + } + } /// Show alert explaining why a permission is needed func showPermissionAlert(for permission: SystemPermission) { @@ -262,27 +281,46 @@ final class SystemPermissionManager { private func checkAccessibilityPermission() -> Bool { // First check the API let apiResult = AXIsProcessTrusted() - - // Then do a direct test - try to get the focused element - // This will fail if we don't actually have permission + logger.debug("AXIsProcessTrusted returned: \(apiResult)") + + // More comprehensive test - try to get focused application and its windows + // This definitely requires accessibility permission let systemElement = AXUIElementCreateSystemWide() - var focusedElement: CFTypeRef? - let result = AXUIElementCopyAttributeValue( + var focusedApp: CFTypeRef? + let appResult = AXUIElementCopyAttributeValue( systemElement, - kAXFocusedUIElementAttribute as CFString, - &focusedElement + kAXFocusedApplicationAttribute as CFString, + &focusedApp ) - - // If we can get the focused element, we truly have permission - if result == .success { - logger.debug("Accessibility permission verified through direct test") - return true - } else if apiResult { - // API says yes but direct test failed - permission might be pending - logger.debug("Accessibility API reports true but direct test failed") - return false + + if appResult == .success, let app = focusedApp { + // Try to get windows from the app - this definitely needs accessibility + var windows: CFTypeRef? + let windowResult = AXUIElementCopyAttributeValue( + app as! AXUIElement, + kAXWindowsAttribute as CFString, + &windows + ) + + let hasAccess = windowResult == .success + logger.debug("Comprehensive accessibility test result: \(hasAccess), can get windows: \(windows != nil)") + + if hasAccess { + logger.debug("Accessibility permission verified through comprehensive test") + return true + } else if apiResult { + // API says yes but comprehensive test failed - permission not actually working + logger.debug("Accessibility API reports true but comprehensive test failed") + return false + } + } else { + // Can't even get focused app + logger.debug("Cannot get focused application - accessibility permission not granted") + if apiResult { + logger.debug("API reports true but cannot access UI elements") + } } - + return false } diff --git a/mac/VibeTunnel/Presentation/Components/Menu/ServerInfoSection.swift b/mac/VibeTunnel/Presentation/Components/Menu/ServerInfoSection.swift index 2db1462b..b922f508 100644 --- a/mac/VibeTunnel/Presentation/Components/Menu/ServerInfoSection.swift +++ b/mac/VibeTunnel/Presentation/Components/Menu/ServerInfoSection.swift @@ -129,25 +129,22 @@ struct ServerAddressRow: View { .buttonStyle(.plain) .pointingHandCursor() - // Copy button that appears on hover - if isHovered { - Button(action: { - copyToClipboard() - }) { - Image(systemName: showCopiedFeedback ? "checkmark.circle.fill" : "doc.on.doc") - .font(.system(size: 10)) - .foregroundColor(AppColors.Fallback.serverRunning(for: colorScheme)) - } - .buttonStyle(.plain) - .pointingHandCursor() - .help(showCopiedFeedback ? "Copied!" : "Copy to clipboard") - .transition(.scale.combined(with: .opacity)) + // Copy button - always present but opacity changes on hover + Button(action: { + copyToClipboard() + }) { + Image(systemName: showCopiedFeedback ? "checkmark.circle.fill" : "doc.on.doc") + .font(.system(size: 10)) + .foregroundColor(AppColors.Fallback.serverRunning(for: colorScheme)) } + .buttonStyle(.plain) + .pointingHandCursor() + .help(showCopiedFeedback ? "Copied!" : "Copy to clipboard") + .opacity(isHovered ? 1.0 : 0.0) + .animation(.easeInOut(duration: 0.15), value: isHovered) } .onHover { hovering in - withAnimation(.easeInOut(duration: 0.15)) { - isHovered = hovering - } + isHovered = hovering } }