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
12 changes: 12 additions & 0 deletions Cotabby.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
0DDC0CFF5558A8F4355836B2 /* OverlayController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F308F6E274CC645E27CB651F /* OverlayController.swift */; };
0F3267956257401F39386773 /* SuggestionOverlayStabilityGate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2F95847D76893C8A5B504B4 /* SuggestionOverlayStabilityGate.swift */; };
1003373E13779882503C0E9D /* DisplayCoordinateConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74BD1D4DB27D5D96D1E06096 /* DisplayCoordinateConverter.swift */; };
12995E5DDB11E3395E6AF82F /* ShortcutsPaneView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB630F9814388203DD1CA2EC /* ShortcutsPaneView.swift */; };
14D77F0B8A195AC2FA8D24A9 /* MirrorOverlayLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC83D14A7557BC0196E59007 /* MirrorOverlayLayoutTests.swift */; };
156E6AB3D24134EEC29FDB93 /* FocusSnapshotResolverSelectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA705EDFE1C41294F0E381F1 /* FocusSnapshotResolverSelectionTests.swift */; };
157A55EB796BEB7819B90D5D /* ClipboardRelevanceFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3A2AC525DC664DB540D4F19 /* ClipboardRelevanceFilter.swift */; };
Expand Down Expand Up @@ -161,13 +162,15 @@
E17CAA453B1F534D284F0D89 /* PermissionHostApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6ACCB12E4DB32D2F2BEA567 /* PermissionHostApp.swift */; };
E313639E71AE1374D2B9A956 /* SuggestionWorkController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B2D97BAA3618A7D0357AC44 /* SuggestionWorkController.swift */; };
E442A19096A4B5BDA944BDEA /* OpenSourcePaneView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E7064AC8B6EFEA971F4CA2 /* OpenSourcePaneView.swift */; };
E51FA12B690428CA431328FC /* WritingPaneView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48B95B6665109B6C6A63B42 /* WritingPaneView.swift */; };
E6EE3C13FA31F261CD734C69 /* DownloadOutcomeClassifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DE1975F3B5F4A70478DBF41 /* DownloadOutcomeClassifier.swift */; };
E912D4617AE1376061DF1F00 /* LanguageSupportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4793D4EA5D36D7E5CC216C27 /* LanguageSupportTests.swift */; };
E994FE418A961FB234D9057A /* DownloadFileRescuerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2F46767D9D1F0D44E239CA8 /* DownloadFileRescuerTests.swift */; };
E9E4CC657771DF9F4C56183C /* VisualContextCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A854CAFB1F557BC4CAED8819 /* VisualContextCoordinator.swift */; };
ED9C51B0D7056F0753AADF2D /* GhostSuggestionLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 043E8AA850F930222DD112C0 /* GhostSuggestionLayout.swift */; };
EDA8E8250FC2F70B206B4894 /* LlamaVisualContextSummarizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D2782B6C7BE3F56BCB22DE /* LlamaVisualContextSummarizer.swift */; };
EE87886AC1BFC8BB3DE09762 /* HuggingFaceModelBrowserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78E49BDA7F3A42455C4C5350 /* HuggingFaceModelBrowserView.swift */; };
EF0DE5E045F328F1E912A02A /* AppsPaneView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C1C921A1CDA2ADFC39EA01 /* AppsPaneView.swift */; };
F08C139B246C1EC7BB435455 /* MenuBarPresentationObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00824BDD8D0E9B3063827C78 /* MenuBarPresentationObserver.swift */; };
F0DEEE8A866ABB560E7A7E6A /* LaunchAtLoginService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 220CD4AFA1E96A37BC4514AD /* LaunchAtLoginService.swift */; };
F4EEE6291095B0BF2D3FBA21 /* GhostTextColorPreset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E44E393DD58B978B1EAB6CF /* GhostTextColorPreset.swift */; };
Expand Down Expand Up @@ -328,12 +331,14 @@
CE8C2569A8217EE9BD3B197F /* FileLogHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileLogHandler.swift; sourceTree = "<group>"; };
D2F46767D9D1F0D44E239CA8 /* DownloadFileRescuerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadFileRescuerTests.swift; sourceTree = "<group>"; };
D3A2AC525DC664DB540D4F19 /* ClipboardRelevanceFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardRelevanceFilter.swift; sourceTree = "<group>"; };
D48B95B6665109B6C6A63B42 /* WritingPaneView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WritingPaneView.swift; sourceTree = "<group>"; };
D49F3B597374208594861B9B /* FoundationModelDriftEvalTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FoundationModelDriftEvalTests.swift; sourceTree = "<group>"; };
D4F6D5F94B238F7B4BE7C247 /* FocusCapabilityResolverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusCapabilityResolverTests.swift; sourceTree = "<group>"; };
D504BEB224E0C176F5FCFF6E /* CompletionRenderModePolicyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletionRenderModePolicyTests.swift; sourceTree = "<group>"; };
D5916CF13B85EFCE6A049296 /* AppleIntelligencePaneView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleIntelligencePaneView.swift; sourceTree = "<group>"; };
D5D6C2318E405AA717D1C256 /* WelcomePermissionStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomePermissionStepView.swift; sourceTree = "<group>"; };
D84D4528EEC9EFEB8AE8E318 /* ActivationIndicatorController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivationIndicatorController.swift; sourceTree = "<group>"; };
D9C1C921A1CDA2ADFC39EA01 /* AppsPaneView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppsPaneView.swift; sourceTree = "<group>"; };
DB0CE9AB1286367BA2E82392 /* SettingsContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsContainerView.swift; sourceTree = "<group>"; };
DDE858CB1E687E3CEB8FDD5B /* SuggestionRequestFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionRequestFactory.swift; sourceTree = "<group>"; };
DEB16474A67CE1D210B944C9 /* SuggestionSubsystemContracts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionSubsystemContracts.swift; sourceTree = "<group>"; };
Expand All @@ -344,6 +349,7 @@
E6423D6CC8CC371D2DA899DE /* PermissionOverlayTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionOverlayTracker.swift; sourceTree = "<group>"; };
E7F42112F14026E6253BB865 /* PermissionAndContextModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionAndContextModelTests.swift; sourceTree = "<group>"; };
EAAE6B395FAB604DF059280A /* KeyCodeLabels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyCodeLabels.swift; sourceTree = "<group>"; };
EB630F9814388203DD1CA2EC /* ShortcutsPaneView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutsPaneView.swift; sourceTree = "<group>"; };
ED8672B87CEC72BE3978C6BB /* CotabbyTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CotabbyTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
EE94342B888A5A2CCF66BC93 /* SuggestionRequestFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionRequestFactoryTests.swift; sourceTree = "<group>"; };
EFD89799BB82AF7A92559AEB /* ClipboardContentDistillerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardContentDistillerTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -433,11 +439,14 @@
A3FA53BBC3D81503C1D17477 /* AboutPaneView.swift */,
2B7A28471B8526C2693FFF65 /* AcknowledgementsView.swift */,
D5916CF13B85EFCE6A049296 /* AppleIntelligencePaneView.swift */,
D9C1C921A1CDA2ADFC39EA01 /* AppsPaneView.swift */,
FC9ECD5408B0F5708149B5C0 /* EngineAndModelPaneView.swift */,
07480CE96ED0EBD94817C6B1 /* GeneralPaneView.swift */,
65E7064AC8B6EFEA971F4CA2 /* OpenSourcePaneView.swift */,
7113D3373525113CA69E7597 /* PermissionsPaneView.swift */,
93028F328388432E72C58D09 /* PlaceholderPaneView.swift */,
EB630F9814388203DD1CA2EC /* ShortcutsPaneView.swift */,
D48B95B6665109B6C6A63B42 /* WritingPaneView.swift */,
);
path = Panes;
sourceTree = "<group>";
Expand Down Expand Up @@ -824,6 +833,7 @@
C4C6734678797669055988E0 /* AppUpdateManager.swift in Sources */,
8557DF86F088D19D2DAC70BC /* AppleIntelligencePaneView.swift in Sources */,
66C23A7C2FCDE0266FF425F8 /* ApplicationBundleMetadata.swift in Sources */,
EF0DE5E045F328F1E912A02A /* AppsPaneView.swift in Sources */,
3CBBC3BFAC0DC8952EE24EF7 /* BundledRuntimeLocator.swift in Sources */,
76FD91607794883F8E121450 /* CaretGeometrySelector.swift in Sources */,
6E01052209B73D7361C12CEF /* ClipboardContentDistiller.swift in Sources */,
Expand Down Expand Up @@ -906,6 +916,7 @@
4B93D26BACEEA932E92B1A19 /* SettingsPaneScaffold.swift in Sources */,
27D4F5CACADE171F142178B4 /* SettingsSidebarView.swift in Sources */,
A440C596EFD9CD1E44F2579B /* SettingsView.swift in Sources */,
12995E5DDB11E3395E6AF82F /* ShortcutsPaneView.swift in Sources */,
4F369F5284DDCEABF082E59B /* SuggestionAvailabilityEvaluator.swift in Sources */,
A0657CE0488F69F0BD559CBC /* SuggestionCoordinator+Acceptance.swift in Sources */,
D2F1DD215989BF32675308C2 /* SuggestionCoordinator+Input.swift in Sources */,
Expand Down Expand Up @@ -942,6 +953,7 @@
9ABF75CDA78B27453C3F5B34 /* WelcomeView.swift in Sources */,
1F8CC88AFFE67C08944CF506 /* WindowScreenshotService.swift in Sources */,
22DF59FD6F3B83B092A6F5ED /* WordCountFormatter.swift in Sources */,
E51FA12B690428CA431328FC /* WritingPaneView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
109 changes: 109 additions & 0 deletions Cotabby/UI/Settings/Panes/AppsPaneView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import AppKit
import SwiftUI
import UniformTypeIdentifiers

/// File overview:
/// "Apps" detail pane of the redesigned Settings window. Lists every app where Cotabby is
/// disabled, lets the user remove individual rules, and offers a file-picker entry point for apps
/// that can't be reached from the menu-bar toggle (launchers like Raycast or Spotlight that
/// dismiss themselves the moment the menu bar is clicked). Lifted from the legacy
/// `SettingsView.appsSection` so behavior is preserved.
struct AppsPaneView: View {
@ObservedObject var suggestionSettings: SuggestionSettingsModel

var body: some View {
SettingsPaneScaffold {
Section("Apps") {
Text("Cotabby won't autocomplete in these apps. Add an app you can't disable from the "
+ "menu bar, like a launcher that closes the moment it loses focus.")
.font(.caption)
.foregroundStyle(.secondary)

if suggestionSettings.disabledAppRules.isEmpty {
Text("No apps are disabled. Cotabby is active in every supported field.")
.font(.callout)
.foregroundStyle(.secondary)
} else {
ForEach(suggestionSettings.disabledAppRules) { rule in
disabledAppRuleRow(rule)
}
}

Button("Add App…") {
presentDisabledAppPicker()
}
}
}
}

@ViewBuilder
private func disabledAppRuleRow(_ rule: DisabledApplicationRule) -> some View {
HStack(spacing: 12) {
Image(nsImage: icon(for: rule))
.resizable()
.frame(width: 28, height: 28)
.accessibilityHidden(true)

VStack(alignment: .leading, spacing: 2) {
Text(rule.displayName)

Text(rule.bundleIdentifier)
.font(.caption)
.foregroundStyle(.secondary)
.textSelection(.enabled)
}

Spacer(minLength: 0)

Button {
suggestionSettings.removeDisabledApplication(
bundleIdentifier: rule.bundleIdentifier
)
} label: {
Image(systemName: "xmark.circle.fill")
.foregroundStyle(.secondary)
}
.buttonStyle(.borderless)
}
}

/// Bundle IDs are durable; app paths are not. Resolve the current app URL at render time so
/// Settings naturally picks up app updates, moves, or reinstalls without persisting UI cache.
private func icon(for rule: DisabledApplicationRule) -> NSImage {
guard let appURL = NSWorkspace.shared.urlForApplication(
withBundleIdentifier: rule.bundleIdentifier
) else {
return NSWorkspace.shared.icon(for: .applicationBundle)
}
return NSWorkspace.shared.icon(forFile: appURL.path)
}

/// Lets the user disable Cotabby in an app they can't reach from the menu bar. The menu-bar
/// "Enable in <app>" switch only targets the frontmost app, so a launcher like Raycast or
/// Spotlight (which dismisses itself the instant the menu bar is clicked) can never be turned
/// off that way. An open panel names any installed app whether or not it is running.
private func presentDisabledAppPicker() {
let panel = NSOpenPanel()
panel.allowedContentTypes = [.application]
panel.allowsMultipleSelection = true
panel.canChooseDirectories = false
panel.canChooseFiles = true
panel.directoryURL = URL(fileURLWithPath: "/Applications", isDirectory: true)
panel.prompt = "Disable"
panel.message = "Choose apps where Cotabby should not autocomplete."

guard panel.runModal() == .OK else {
return
}

for url in panel.urls {
guard let metadata = ApplicationBundleMetadata(appURL: url) else {
continue
}
suggestionSettings.disableApplication(
bundleIdentifier: metadata.bundleIdentifier,
displayName: metadata.displayName
)
}
}
}
126 changes: 126 additions & 0 deletions Cotabby/UI/Settings/Panes/ShortcutsPaneView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import SwiftUI

/// File overview:
/// "Shortcuts" detail pane of the redesigned Settings window. Surfaces the two keybindings that
/// drive suggestion acceptance: word-by-word and full-suggestion. Lifted from the legacy
/// `SettingsView.shortcutsSection` so binding capture, conflict resolution, and reset / clear
/// semantics are preserved exactly.
struct ShortcutsPaneView: View {
@ObservedObject var suggestionSettings: SuggestionSettingsModel

@State private var isRecordingKeybind = false
@State private var isRecordingFullAcceptKeybind = false
Comment on lines +11 to +12
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Dual active recorders can fire on the same keypress

isRecordingKeybind and isRecordingFullAcceptKeybind are independent, so both can be true simultaneously. When that happens, both KeyRecorderView instances call NSEvent.addLocalMonitorForEvents. Local monitors fire in LIFO order, so whichever "Change" button was clicked second will capture the next key — even if the user intended it for the first row. The first KeyRecorderView then stays in "Press a key…" state until a further key is pressed, silently recording a second shortcut the user didn't intend. Setting the other flag to false (or using a single activeRecorder enum) when one starts recording would prevent this.

Fix in Codex Fix in Claude Code


var body: some View {
SettingsPaneScaffold {
Section("Shortcuts") {
LabeledContent("Accept Word") {
KeybindRow(
label: suggestionSettings.acceptanceKeyLabel,
keyCode: suggestionSettings.acceptanceKeyCode,
modifiers: suggestionSettings.acceptanceKeyModifiers,
defaultKeyCode: SuggestionSettingsModel.defaultAcceptanceKeyCode,
isRecording: $isRecordingKeybind,
onRecord: { keyCode, modifiers, label in
suggestionSettings.setAcceptanceKey(
keyCode: keyCode,
modifiers: modifiers,
label: label
)
},
onReset: {
suggestionSettings.setAcceptanceKey(
keyCode: SuggestionSettingsModel.defaultAcceptanceKeyCode,
modifiers: [],
label: SuggestionSettingsModel.defaultAcceptanceKeyLabel
)
},
onClear: { suggestionSettings.clearAcceptanceKey() },
clearHelp: "Unbind this shortcut. No key will accept word-by-word."
)
}

LabeledContent("Accept Entire Suggestion") {
KeybindRow(
label: suggestionSettings.fullAcceptanceKeyLabel,
keyCode: suggestionSettings.fullAcceptanceKeyCode,
modifiers: suggestionSettings.fullAcceptanceKeyModifiers,
defaultKeyCode: SuggestionSettingsModel.defaultFullAcceptanceKeyCode,
isRecording: $isRecordingFullAcceptKeybind,
onRecord: { keyCode, modifiers, label in
suggestionSettings.setFullAcceptanceKey(
keyCode: keyCode,
modifiers: modifiers,
label: label
)
},
onReset: {
suggestionSettings.setFullAcceptanceKey(
keyCode: SuggestionSettingsModel.defaultFullAcceptanceKeyCode,
modifiers: [],
label: SuggestionSettingsModel.defaultFullAcceptanceKeyLabel
)
},
onClear: { suggestionSettings.clearFullAcceptanceKey() },
clearHelp: "Unbind this shortcut. No key will accept the whole suggestion at once."
)
}
}
}
}
}

/// Shared row chrome for one keybinding. Owns the badge / Change / Reset / Clear layout and the
/// `KeyRecorderView` recording state hand-off so the surrounding pane stays focused on what each
/// binding does rather than how it is rendered.
private struct KeybindRow: View {
let label: String
let keyCode: CGKeyCode
let modifiers: ShortcutModifierMask
let defaultKeyCode: CGKeyCode
@Binding var isRecording: Bool
let onRecord: (CGKeyCode, ShortcutModifierMask, String) -> Void
let onReset: () -> Void
let onClear: () -> Void
let clearHelp: String
Comment on lines +76 to +85
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Unused clearHelp parameter

clearHelp: String is declared in KeybindRow's stored properties and passed by both call sites, but it is never referenced anywhere in body. The PR description notes tooltip removal happened in #362, so this was almost certainly meant to appear as .help(clearHelp) on the Clear button. As-is, the parameter is dead weight and the Clear button is left with no help text for VoiceOver or on-hover display.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Codex Fix in Claude Code


var body: some View {
HStack(spacing: 8) {
Text(label)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(
RoundedRectangle(cornerRadius: 6)
.fill(.quaternary)
)

if isRecording {
KeyRecorderView(
onKeyRecorded: { keyCode, modifiers, label in
onRecord(keyCode, modifiers, label)
isRecording = false
},
onCancelled: { isRecording = false }
)
} else {
Button("Change") {
isRecording = true
}
}

if keyCode != defaultKeyCode || !modifiers.isEmpty {
Button("Reset") {
onReset()
isRecording = false
}
}

if keyCode != SuggestionSettingsModel.disabledKeyCode {
Button("Clear") {
onClear()
isRecording = false
}
}
}
}
}
Loading