Skip to content
Open
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
10 changes: 10 additions & 0 deletions Cotabby.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
1D5389E9562AF6315BFCDCE1 /* NOTICE.md in Resources */ = {isa = PBXBuildFile; fileRef = 66CF2A70D4699421AC9BD849 /* NOTICE.md */; };
1D54C941ED086C6427CFD773 /* MacroEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 974A8708D2006767BD76862A /* MacroEngine.swift */; };
1DF03DC12D45C22753F683BC /* SymSpell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E3B1232C4BE8072A5183F9C /* SymSpell.swift */; };
1DF1A9DFF50E9219875E24FF /* SuggestionPauseModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2FDC5884B07B19B9B4917A6 /* SuggestionPauseModels.swift */; };
1E0796CADFB4F561EC94602A /* HuggingFaceModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = A520809E71697E3BB9A8139C /* HuggingFaceModels.swift */; };
1EB90D3D8A5BA028B86E4D9F /* SettingsContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0CE9AB1286367BA2E82392 /* SettingsContainerView.swift */; };
1F714771EE793A5AF92F1E2B /* ModelDownloadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51020F8CD58338BD643FBF63 /* ModelDownloadManager.swift */; };
Expand Down Expand Up @@ -234,6 +235,7 @@
4CCF29A7EA1B7D37841C135D /* DateMacroEvaluatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 313EDBA60565836F32CEEC10 /* DateMacroEvaluatorTests.swift */; };
4D583CB3DA253FB795EE54F9 /* ArithmeticEvaluatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78AFA4586C82E92D7FBF381B /* ArithmeticEvaluatorTests.swift */; };
4E2DEFF3CA51E8B160B802CA /* SettingsIndex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34EF2406E7A384F3325AAF9A /* SettingsIndex.swift */; };
4E49547F22DC8663171E3160 /* SuggestionPauseModelsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 224494DA9610B169F1C5BCFF /* SuggestionPauseModelsTests.swift */; };
4E7AC9EBE2781AC49E167608 /* EmojiPickerModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5F074ED7E340E9B9E4C5E0 /* EmojiPickerModels.swift */; };
4F369F5284DDCEABF082E59B /* SuggestionAvailabilityEvaluator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3609CC88A5280B3AA40414DF /* SuggestionAvailabilityEvaluator.swift */; };
4F38CE1C2602CF4F41323032 /* PermissionOverlayTrackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12DD19BCE610808F1E38702D /* PermissionOverlayTrackerTests.swift */; };
Expand Down Expand Up @@ -374,6 +376,7 @@
7E9413CE7C999C4612348248 /* SuggestionSessionReconcilerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C8F07AC52C7A482F5FE34C5 /* SuggestionSessionReconcilerTests.swift */; };
7E99F5676A1D1DF7EA7D7702 /* SuggestionCoordinator+Prediction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED1EA9282E0AC7592E60889 /* SuggestionCoordinator+Prediction.swift */; };
7EB20783E0D36715D1230A5C /* PromptSectionBudgetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E260C4D08C786CDBD527B329 /* PromptSectionBudgetTests.swift */; };
7EB2CB1000EA2407A2F59B4E /* SuggestionPauseModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2FDC5884B07B19B9B4917A6 /* SuggestionPauseModels.swift */; };
7EEE6AEBFBD419FFE7C544BA /* SuggestionSettingsData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93563FDA25DFC0038E5F887 /* SuggestionSettingsData.swift */; };
7FC103944F4EF39DB965F469 /* InMemoryLogging in Frameworks */ = {isa = PBXBuildFile; productRef = 88921938DC814625ED57D605 /* InMemoryLogging */; };
814E348C663B697537594F0C /* EmojiRecentsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 671689F289D45A124639C9C6 /* EmojiRecentsTests.swift */; };
Expand Down Expand Up @@ -769,6 +772,7 @@
210F9AD332273FE2EB3A9A01 /* WebContentFieldDetectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebContentFieldDetectorTests.swift; sourceTree = "<group>"; };
21CB3008986BE7FD2A4D9132 /* WelcomeCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeCoordinator.swift; sourceTree = "<group>"; };
224438039A86E5619294EAF7 /* EmojiUsageStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiUsageStoreTests.swift; sourceTree = "<group>"; };
224494DA9610B169F1C5BCFF /* SuggestionPauseModelsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionPauseModelsTests.swift; sourceTree = "<group>"; };
22544F4B756E3E4144497D17 /* SuggestionCoordinator+Input.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SuggestionCoordinator+Input.swift"; sourceTree = "<group>"; };
22707E26E2106DF0E826D32D /* ControlTokenMarkersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlTokenMarkersTests.swift; sourceTree = "<group>"; };
22BE47D1DBF6C23151458836 /* MacroTriggerStateMachineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacroTriggerStateMachineTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1030,6 +1034,7 @@
C05B0439348261163B37C508 /* SuggestionAvailabilityEvaluatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionAvailabilityEvaluatorTests.swift; sourceTree = "<group>"; };
C1C5DE0F3FF63545000E2453 /* DisplayCoordinateConverterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayCoordinateConverterTests.swift; sourceTree = "<group>"; };
C2AD7D366EC4344D332EE6A3 /* Signing.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Signing.xcconfig; sourceTree = "<group>"; };
C2FDC5884B07B19B9B4917A6 /* SuggestionPauseModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionPauseModels.swift; sourceTree = "<group>"; };
C375227649689775275AA4B3 /* SuggestionCoordinatorAcceptanceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionCoordinatorAcceptanceTests.swift; sourceTree = "<group>"; };
C379D77029D6E88C8C1B9AF7 /* emoji.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = emoji.json; sourceTree = "<group>"; };
C3A35FAA742408D002B75920 /* WebContentFieldDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebContentFieldDetector.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1402,6 +1407,7 @@
F4D9DF8723AF32C058BFACDE /* SpellingDictionaryCatalog.swift */,
ADBE3E6CC585C1683787C877 /* SuggestionEngineModels.swift */,
386C98FFCF76EC1C8C7E82BB /* SuggestionModels.swift */,
C2FDC5884B07B19B9B4917A6 /* SuggestionPauseModels.swift */,
81718CA62FBC775A6CEBCED1 /* SuggestionQualityMetricsStore.swift */,
D93563FDA25DFC0038E5F887 /* SuggestionSettingsData.swift */,
86460C747AA883FDE756BDBA /* SuggestionSettingsModel.swift */,
Expand Down Expand Up @@ -1583,6 +1589,7 @@
45A896811745673061AF3612 /* SuggestionFocusFreshnessTests.swift */,
8CB1D4F2681FAF59014AE115 /* SuggestionInteractionStateTests.swift */,
CDB25ABC4FFB0E63477CDCB0 /* SuggestionOverlayStabilityGateTests.swift */,
224494DA9610B169F1C5BCFF /* SuggestionPauseModelsTests.swift */,
B4CC566AC1DE33FD0CD30E1E /* SuggestionQualityMetricsStoreTests.swift */,
EE94342B888A5A2CCF66BC93 /* SuggestionRequestFactoryTests.swift */,
9C8F07AC52C7A482F5FE34C5 /* SuggestionSessionReconcilerTests.swift */,
Expand Down Expand Up @@ -2225,6 +2232,7 @@
7DEFC57991AB0C5379AD9CBF /* SuggestionModels.swift in Sources */,
EE2C9177CE615298595215A8 /* SuggestionOverlayPresenter.swift in Sources */,
DFF3AA49E0770DE3CFBC24C1 /* SuggestionOverlayStabilityGate.swift in Sources */,
7EB2CB1000EA2407A2F59B4E /* SuggestionPauseModels.swift in Sources */,
18680D0D66469A2954A50B6C /* SuggestionQualityMetricsStore.swift in Sources */,
B691B8378FD73E186A72450C /* SuggestionRequestFactory.swift in Sources */,
532283A7651F7E66635F4281 /* SuggestionSessionReconciler.swift in Sources */,
Expand Down Expand Up @@ -2475,6 +2483,7 @@
0AF568AB234033BA2DE4CAA7 /* SuggestionModels.swift in Sources */,
02DA43985CDAE6859014F14F /* SuggestionOverlayPresenter.swift in Sources */,
0F3267956257401F39386773 /* SuggestionOverlayStabilityGate.swift in Sources */,
1DF1A9DFF50E9219875E24FF /* SuggestionPauseModels.swift in Sources */,
5687320132AD97B4086260DF /* SuggestionQualityMetricsStore.swift in Sources */,
46F341472191BC451B6BF6B5 /* SuggestionRequestFactory.swift in Sources */,
CA5B2D226FBAA5419E78F14F /* SuggestionSessionReconciler.swift in Sources */,
Expand Down Expand Up @@ -2651,6 +2660,7 @@
5CED06E89FBEF557DCD6C684 /* SuggestionFocusFreshnessTests.swift in Sources */,
6CBEF02FCDFCF406E378C27C /* SuggestionInteractionStateTests.swift in Sources */,
4C6D8ED0A7B45D2EADF06DA5 /* SuggestionOverlayStabilityGateTests.swift in Sources */,
4E49547F22DC8663171E3160 /* SuggestionPauseModelsTests.swift in Sources */,
695E431AC3FF79769E2C5EEF /* SuggestionQualityMetricsStoreTests.swift in Sources */,
B93AB7E845086F6FBB068369 /* SuggestionRequestFactoryTests.swift in Sources */,
7E9413CE7C999C4612348248 /* SuggestionSessionReconcilerTests.swift in Sources */,
Expand Down
15 changes: 15 additions & 0 deletions Cotabby/App/Coordinators/SuggestionCoordinator+Acceptance.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,21 @@ extension SuggestionCoordinator {
) -> Bool {
let snapshot = focusModel.snapshot

if let disabledReason = currentDisabledReason(focusSnapshot: snapshot) {
return passTabThrough(reason: disabledReason)
}

return acceptEnabledSuggestion(fullText: fullText, keyName: keyName, snapshot: snapshot)
}

/// Continues acceptance after global, per-app, and temporary-pause gating succeeds. Keeping the
/// eligibility boundary separate prevents the state-machine-heavy acceptance path from gaining
/// another decision branch each time Cotabby adds a new top-level disable mechanism.
private func acceptEnabledSuggestion(
fullText: Bool,
keyName: String,
snapshot: FocusSnapshot
) -> Bool {
guard permissionManager.inputMonitoringGranted else {
return passTabThrough(
reason: "Input Monitoring permission is required before Cotabby can accept suggestions."
Expand Down
3 changes: 3 additions & 0 deletions Cotabby/App/Coordinators/SuggestionCoordinator+Input.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ extension SuggestionCoordinator {

if SuggestionAvailabilityEvaluator.shouldSchedulePrediction(
globallyEnabled: settingsSnapshot.isGloballyEnabled,
temporarilyPaused: settingsSnapshot.isTemporarilyPaused,
disabledAppBundleIdentifiers: settingsSnapshot.disabledAppBundleIdentifiers,
disabledDomains: PerDomainDisableSettings.disabledDomains(),
suggestInIntegratedTerminals: settingsSnapshot.suggestInIntegratedTerminals,
Expand Down Expand Up @@ -58,6 +59,7 @@ extension SuggestionCoordinator {
if let context = snapshot.context,
SuggestionAvailabilityEvaluator.shouldCaptureVisualContext(
globallyEnabled: settingsSnapshot.isGloballyEnabled,
temporarilyPaused: settingsSnapshot.isTemporarilyPaused,
disabledAppBundleIdentifiers: settingsSnapshot.disabledAppBundleIdentifiers,
disabledDomains: PerDomainDisableSettings.disabledDomains(),
suggestInIntegratedTerminals: settingsSnapshot.suggestInIntegratedTerminals,
Expand Down Expand Up @@ -87,6 +89,7 @@ extension SuggestionCoordinator {
// earlier `shouldCaptureVisualContext` check already declined.
if SuggestionAvailabilityEvaluator.shouldCaptureVisualContext(
globallyEnabled: settingsSnapshot.isGloballyEnabled,
temporarilyPaused: settingsSnapshot.isTemporarilyPaused,
disabledAppBundleIdentifiers: settingsSnapshot.disabledAppBundleIdentifiers,
disabledDomains: PerDomainDisableSettings.disabledDomains(),
suggestInIntegratedTerminals: settingsSnapshot.suggestInIntegratedTerminals,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ extension SuggestionCoordinator {
if let focusedSnapshot = focusModel.snapshot.context,
SuggestionAvailabilityEvaluator.shouldCaptureVisualContext(
globallyEnabled: settingsSnapshot.isGloballyEnabled,
temporarilyPaused: settingsSnapshot.isTemporarilyPaused,
disabledAppBundleIdentifiers: settingsSnapshot.disabledAppBundleIdentifiers,
disabledDomains: PerDomainDisableSettings.disabledDomains(),
suggestInIntegratedTerminals: settingsSnapshot.suggestInIntegratedTerminals,
Expand All @@ -85,6 +86,7 @@ extension SuggestionCoordinator {

if SuggestionAvailabilityEvaluator.shouldSchedulePrediction(
globallyEnabled: settingsSnapshot.isGloballyEnabled,
temporarilyPaused: settingsSnapshot.isTemporarilyPaused,
disabledAppBundleIdentifiers: settingsSnapshot.disabledAppBundleIdentifiers,
disabledDomains: PerDomainDisableSettings.disabledDomains(),
suggestInIntegratedTerminals: settingsSnapshot.suggestInIntegratedTerminals,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -990,6 +990,7 @@ extension SuggestionCoordinator {
func currentDisabledReason(focusSnapshot: FocusSnapshot) -> String? {
SuggestionAvailabilityEvaluator.disabledReason(
globallyEnabled: settingsSnapshot.isGloballyEnabled,
temporarilyPaused: settingsSnapshot.isTemporarilyPaused,
disabledAppBundleIdentifiers: settingsSnapshot.disabledAppBundleIdentifiers,
disabledDomains: PerDomainDisableSettings.disabledDomains(),
suggestInIntegratedTerminals: settingsSnapshot.suggestInIntegratedTerminals,
Expand Down
21 changes: 18 additions & 3 deletions Cotabby/App/Core/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,16 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
}
.store(in: &cancellables)

// Focus may stay on the same field while a menu action pauses or resumes Cotabby. React to
// settings snapshots too so the field-edge indicator cannot remain visible during a pause.
suggestionSettings.snapshotPublisher
.dropFirst()
.sink { [weak self] settings in
guard let self else { return }
self.updateActivationIndicator(for: self.focusModel.snapshot, settings: settings)
}
.store(in: &cancellables)

if let focusDebugOverlayController {
focusModel.$latestPollEvent
.compactMap { $0 }
Expand Down Expand Up @@ -226,9 +236,14 @@ final class AppDelegate: NSObject, NSApplicationDelegate {

/// Shows or hides the field-edge Cotabby icon based on focus state, global enable, per-app
/// disable rules, and the user's indicator toggle.
private func updateActivationIndicator(for snapshot: FocusSnapshot) {
guard suggestionSettings.isGloballyEnabled,
!suggestionSettings.isApplicationDisabled(bundleIdentifier: snapshot.bundleIdentifier),
private func updateActivationIndicator(
for snapshot: FocusSnapshot,
settings: SuggestionSettingsSnapshot? = nil
) {
let settings = settings ?? suggestionSettings.snapshot
guard settings.isGloballyEnabled,
!settings.isTemporarilyPaused,
!settings.disabledAppBundleIdentifiers.contains(snapshot.bundleIdentifier ?? ""),
case .supported = snapshot.capability,
let context = snapshot.context
else {
Expand Down
8 changes: 6 additions & 2 deletions Cotabby/App/Core/CotabbyAppEnvironment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,9 @@ final class CotabbyAppEnvironment {
// complete inside its own UI; the focus tracker recognises it by this AX identifier.
selfCaptureAllowedElementIdentifier: ContextLivePreview.accessibilityIdentifier,
isCaptureSuppressedForBundle: { bundleIdentifier in
guard suggestionSettings.isGloballyEnabled else { return true }
guard suggestionSettings.isGloballyEnabled,
!suggestionSettings.isTemporarilyPaused
else { return true }
if let bundleIdentifier,
suggestionSettings.isApplicationDisabled(bundleIdentifier: bundleIdentifier) {
return true
Expand All @@ -100,7 +102,9 @@ final class CotabbyAppEnvironment {
// evaluate against the previous app's identity until the next AX poll fires. This
// is the same race the downstream evaluator already has — not a new regression.
inputMonitor.shouldProcessEventsProvider = { [weak focusModel] in
guard suggestionSettings.isGloballyEnabled else { return false }
guard suggestionSettings.isGloballyEnabled,
!suggestionSettings.isTemporarilyPaused
else { return false }
guard let snapshot = focusModel?.snapshot else { return true }
if calendarAccessibilityCaptureGuard.shouldSuppressCapture(
for: snapshot.bundleIdentifier
Expand Down
2 changes: 2 additions & 0 deletions Cotabby/Models/SuggestionEngineModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ enum AcceptanceGranularity: String, CaseIterable, Codable, Sendable {
/// time. Keeping this as a value type makes change detection simple and deterministic.
struct SuggestionSettingsSnapshot: Equatable, Sendable {
let isGloballyEnabled: Bool
/// Resolved live pause state. Timed pauses publish `false` when their expiration fires.
let isTemporarilyPaused: Bool
let disabledAppBundleIdentifiers: Set<String>
/// When false (the default), ghost text is suppressed in integrated terminals (VS Code / Cursor
/// xterm.js surfaces). Power users can opt back in. Travels in the snapshot so the availability
Expand Down
Loading
Loading