Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Cotabby/Models/PermissionModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
9 changes: 9 additions & 0 deletions Cotabby/Services/Permission/PermissionManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
14 changes: 9 additions & 5 deletions Cotabby/UI/MenuBarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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(
Expand Down Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions Cotabby/UI/PermissionReminderView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 9 additions & 8 deletions Cotabby/UI/Settings/Panes/PermissionsPaneView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
}
Expand Down Expand Up @@ -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
Expand Down