diff --git a/Cotabby/Models/PermissionModels.swift b/Cotabby/Models/PermissionModels.swift index 8d0c5334..4fb47009 100644 --- a/Cotabby/Models/PermissionModels.swift +++ b/Cotabby/Models/PermissionModels.swift @@ -39,6 +39,15 @@ enum CotabbyPermissionKind: String, CaseIterable, Identifiable, Sendable { } } + /// Title for the compact permission rows (menu-bar panel, Settings list), with an "(Optional)" + /// qualifier appended for enhancement permissions. Those rows reuse the required rows' styling so + /// the permission reads as a real, grantable permission rather than a separate feature toggle, and + /// this suffix is the only thing that marks it optional there. Card surfaces (onboarding, the + /// reminder window) carry their own "Optional" capsule instead, so they keep using `title`. + var compactRowTitle: String { + isOptionalEnhancement ? "\(title) (Optional)" : title + } + var systemImageName: String { switch self { case .accessibility: diff --git a/Cotabby/Services/Permission/PermissionManager.swift b/Cotabby/Services/Permission/PermissionManager.swift index 43a69818..d964866f 100644 --- a/Cotabby/Services/Permission/PermissionManager.swift +++ b/Cotabby/Services/Permission/PermissionManager.swift @@ -111,6 +111,15 @@ final class PermissionManager: ObservableObject { .allSatisfy(isGranted(_:)) } + /// Whether every permission Cotabby can use (required ones plus the optional Screen Recording + /// enhancement) is granted. Surfaces that list all permissions (the menu-bar Permissions card) + /// use this so they keep showing the still-missing optional permission instead of vanishing as + /// soon as the required ones are satisfied. Does not gate autocomplete; that stays on + /// `requiredPermissionsGranted`. + var allPermissionsGranted: Bool { + CotabbyPermissionKind.allCases.allSatisfy(isGranted(_:)) + } + /// Shared opener used by onboarding and the menu-bar shortcuts. func openSettings(for permission: CotabbyPermissionKind) { NSWorkspace.shared.open(permission.settingsURL) diff --git a/Cotabby/UI/MenuBarView.swift b/Cotabby/UI/MenuBarView.swift index e3739d56..dfcb4ed3 100644 --- a/Cotabby/UI/MenuBarView.swift +++ b/Cotabby/UI/MenuBarView.swift @@ -233,8 +233,12 @@ struct MenuBarView: View { // MARK: - Permissions (conditional) - /// Only appears when at least one permission is missing. Once all are granted, this - /// section vanishes — no wasted space on resolved state. + /// Lists every permission Cotabby can use and appears whenever at least one is missing, including + /// the optional Screen Recording enhancement. Each row carries its own grant state, so the card + /// keeps showing the still-missing permission until nothing is left to grant, then vanishes. + /// Screen Recording is surfaced as a normal "(Optional)" permission row rather than hidden or + /// shown as a feature toggle, but it never blocks autocomplete (see + /// `CotabbyPermissionKind.isRequiredForAutocomplete`). @ViewBuilder private var permissionsCard: some View { if !allPermissionsGranted { @@ -243,9 +247,9 @@ struct MenuBarView: View { .font(.subheadline.weight(.medium)) .padding(.bottom, 2) - ForEach(CotabbyPermissionKind.allCases.filter(\.isRequiredForAutocomplete)) { permission in + ForEach(CotabbyPermissionKind.allCases) { permission in PermissionRow( - title: permission.title, + title: permission.compactRowTitle, granted: permissionManager.isGranted(permission), action: { sourceFrameInScreen in permissionGuidanceController.requestAccess( @@ -446,7 +450,7 @@ struct MenuBarView: View { // MARK: - Derived state private var allPermissionsGranted: Bool { - permissionManager.requiredPermissionsGranted + permissionManager.allPermissionsGranted } /// Fast Mode is forced on and locked while Screen Recording is unavailable, since visual context diff --git a/Cotabby/UI/PermissionReminderView.swift b/Cotabby/UI/PermissionReminderView.swift index b1922e98..f09218ce 100644 --- a/Cotabby/UI/PermissionReminderView.swift +++ b/Cotabby/UI/PermissionReminderView.swift @@ -120,9 +120,10 @@ private struct ReminderPermissionCard: View { } .foregroundStyle(.green) } else if isOptional { - // Lower-emphasis bordered button so the optional row never competes visually with the - // required Allow buttons above it. - Button("Enable") { + // Same "Allow" verb as the required rows (never a "feature toggle" like Enable), but a + // lower-emphasis bordered button so the optional row never competes visually with the + // required Allow buttons above it in this "Permissions needed" modal. + Button("Allow") { permissionGuidanceController.requestAccess( for: permission, sourceFrameInScreen: actionButtonFrame diff --git a/Cotabby/UI/Settings/Panes/PermissionsPaneView.swift b/Cotabby/UI/Settings/Panes/PermissionsPaneView.swift index e4d41eea..72ce0b55 100644 --- a/Cotabby/UI/Settings/Panes/PermissionsPaneView.swift +++ b/Cotabby/UI/Settings/Panes/PermissionsPaneView.swift @@ -41,7 +41,6 @@ struct PermissionsPaneView: View { "context. Without it, Cotabby runs in Fast Mode using only the text you've typed.", systemImage: "camera.viewfinder", granted: permissionManager.screenRecordingGranted, - isOptional: true, permissionGuidanceController: permissionGuidanceController ) } @@ -79,23 +78,25 @@ private struct SettingsPermissionRow: View { let description: String let systemImage: String let granted: Bool - var isOptional: Bool = false let permissionGuidanceController: PermissionGuidanceController @State private var actionButtonFrame: CGRect = .zero var body: some View { HStack(spacing: 10) { - SettingsRowLabel(title: permission.title, description: description, systemImage: systemImage) + SettingsRowLabel(title: permission.compactRowTitle, description: description, systemImage: systemImage) Spacer(minLength: 0) - // An ungranted optional permission reads as a neutral "Off" rather than the orange - // "Needs Access" used for required ones, so it never looks like a broken setup. - Text(granted ? "Granted" : (isOptional ? "Off" : "Needs Access")) + // Optional permissions reuse the required rows' styling and read as real permissions; the + // "(Optional)" suffix in `compactRowTitle` is what marks them optional, not a separate + // neutral "Off" state or "Enable" verb. The pane-level warning callout still fires for + // required permissions only, so nothing here claims autocomplete is broken when just + // Screen Recording is missing. + Text(granted ? "Granted" : "Needs Access") .font(.caption.weight(.medium)) - .foregroundStyle(granted ? .green : (isOptional ? .secondary : .orange)) + .foregroundStyle(granted ? .green : .orange) if !granted { - Button(isOptional ? "Enable" : "Grant Access") { + Button("Grant Access") { permissionGuidanceController.requestAccess( for: permission, sourceFrameInScreen: actionButtonFrame