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
2 changes: 2 additions & 0 deletions Cotabby/App/Core/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
let permissionGuidanceController: PermissionGuidanceController
let suggestionSettings: SuggestionSettingsModel
let foundationModelAvailabilityService: FoundationModelAvailabilityService
let powerSourceMonitor: PowerSourceMonitor
let suggestionCoordinator: SuggestionCoordinator
let inlineCommandCoordinator: InlineCommandCoordinator
let welcomeCoordinator: WelcomeCoordinator
Expand Down Expand Up @@ -55,6 +56,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
permissionGuidanceController = environment.permissionGuidanceController
suggestionSettings = environment.suggestionSettings
foundationModelAvailabilityService = environment.foundationModelAvailabilityService
powerSourceMonitor = environment.powerSourceMonitor
suggestionCoordinator = environment.suggestionCoordinator
inlineCommandCoordinator = environment.inlineCommandCoordinator
welcomeCoordinator = environment.welcomeCoordinator
Expand Down
1 change: 1 addition & 0 deletions Cotabby/App/Core/CotabbyApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ struct CotabbyApp: App {
permissionGuidanceController: appDelegate.permissionGuidanceController,
suggestionSettings: appDelegate.suggestionSettings,
foundationModelAvailabilityService: appDelegate.foundationModelAvailabilityService,
powerSourceMonitor: appDelegate.powerSourceMonitor,
appUpdateManager: appDelegate.appUpdateManager,
onOpenSettings: {
appDelegate.settingsCoordinator.showSettings()
Expand Down
39 changes: 36 additions & 3 deletions Cotabby/UI/MenuBarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ struct MenuBarView: View {
let permissionGuidanceController: PermissionGuidanceController
@ObservedObject var suggestionSettings: SuggestionSettingsModel
@ObservedObject var foundationModelAvailabilityService: FoundationModelAvailabilityService
@ObservedObject var powerSourceMonitor: PowerSourceMonitor
let appUpdateManager: AppUpdateManager
let onOpenSettings: () -> Void
let onReportFeedback: () -> Void
Expand Down Expand Up @@ -338,7 +339,19 @@ struct MenuBarView: View {
Binding(
get: { suggestionSettings.selectedEngine },
set: { engine in
suggestionSettings.selectEngine(engine)
// With power-based switching on, the active engine is owned by the current power
// source's profile. Editing it here writes that profile (battery vs. plugged-in)
// instead of `selectedEngine`, which the switcher would otherwise revert. The profile
// carries engine + model, so an Apple Intelligence pick drops the model and an Open
// Source pick keeps the currently selected one.
guard suggestionSettings.isPowerBasedModelSwitchingEnabled else {
suggestionSettings.selectEngine(engine)
return
}
let profile: PowerProfile = engine == .appleIntelligence
? .appleIntelligence
: .llama(filename: runtimeModel.selectedModelFilename ?? "")
applyProfileForCurrentPowerSource(profile)
}
)
}
Expand Down Expand Up @@ -394,13 +407,33 @@ struct MenuBarView: View {
?? ""
},
set: { filename in
Task {
await runtimeModel.selectModel(filename)
// With power-based switching on, write the model into the current power source's
// profile so it sticks for that source (and the other source keeps its own model).
// Calling `selectModel` directly would be reverted by the power switcher on its next
// evaluation, which read as "the popup just resets my choice".
guard suggestionSettings.isPowerBasedModelSwitchingEnabled else {
Task {
await runtimeModel.selectModel(filename)
}
return
}
applyProfileForCurrentPowerSource(.llama(filename: filename))
}
)
}

/// Writes a profile into whichever per-power-source slot is currently active, so a menu-bar edit
/// updates the battery profile while on battery and the plugged-in profile while charging. The
/// power-source observer then applies it to the runtime, so no direct `selectModel`/`selectEngine`
/// call is needed here.
private func applyProfileForCurrentPowerSource(_ profile: PowerProfile) {
if powerSourceMonitor.isPluggedIn {
suggestionSettings.setPluggedInProfile(profile)
} else {
suggestionSettings.setBatteryProfile(profile)
}
}

private var runtimePickerDisabled: Bool {
switch runtimeModel.state {
case .starting, .loading:
Expand Down
11 changes: 9 additions & 2 deletions Cotabby/UI/Settings/Panes/EngineAndModelPaneView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -218,11 +218,18 @@ struct EngineAndModelPaneView: View {
} label: {
SettingsRowLabel(
title: "Selected Model",
description: "Which downloaded model file generates suggestions. " +
"Larger models are slower but write better.",
description: suggestionSettings.isPowerBasedModelSwitchingEnabled
? "Set automatically by power source. Use the On Battery / Plugged In " +
"pickers in the Power section, or turn off power-based switching."
: "Which downloaded model file generates suggestions. " +
"Larger models are slower but write better.",
systemImage: "shippingbox"
)
}
// Redundant while power-based switching owns the active model: the Power section's
// per-source profile pickers are the source of truth, and any pick here would be
// reverted on the next power evaluation.
.disabled(suggestionSettings.isPowerBasedModelSwitchingEnabled)
}

DownloadableModelCatalogView(
Expand Down
16 changes: 2 additions & 14 deletions Cotabby/UI/Settings/Panes/WritingPaneView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,25 +33,13 @@ struct WritingPaneView: View {
value: customLowBinding,
in: SuggestionWordRange.minimumWord...SuggestionWordRange.maximumWord
) {
HStack(spacing: 4) {
Text("Min:")
TextField("", value: customLowBinding, format: .number)
.textFieldStyle(.plain)
.frame(width: 32)
.multilineTextAlignment(.trailing)
}
Text("Min: \(suggestionSettings.customWordCountLowWords)")
}
Stepper(
value: customHighBinding,
in: SuggestionWordRange.minimumWord...SuggestionWordRange.maximumWord
) {
HStack(spacing: 4) {
Text("Max:")
TextField("", value: customHighBinding, format: .number)
.textFieldStyle(.plain)
.frame(width: 32)
.multilineTextAlignment(.trailing)
}
Text("Max: \(suggestionSettings.customWordCountHighWords)")
}
Comment on lines +36 to 43
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 Direct numeric input removed from custom word count steppers

Replacing the labeled TextField with a display-only Text means users can no longer type a value directly; they must click the stepper arrow once per word, which is impractical when jumping from, say, 5 to 50. The old TextField(…, format: .number) approach provided instant entry at no extra complexity cost. Worth confirming this simplification is intentional before shipping.

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

}
Text("Token budget scales by your selected language. Multiple languages or a " +
Expand Down
173 changes: 173 additions & 0 deletions FAQ.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# Cotabby FAQ

Frequently asked questions about Cotabby, the free, open-source, on-device AI
autocomplete for macOS.

## 1. What is Cotabby and how does it work?

Cotabby is a free, open-source macOS menu bar app that adds AI autocomplete to
almost any text field on your Mac. As you type, it shows a gray "ghost text"
suggestion inline next to your cursor. Press `Tab` to accept it, or just keep
typing to ignore it.

Under the hood, Cotabby notices which text field you are focused on, reads what
you have typed (and optionally the surrounding on-screen context), generates a
short continuation with a local AI model, and inserts the text you accept right
where your cursor is.

Beyond sentence completion, Cotabby also includes:

- Inline emoji autocomplete (type `:smile` and accept).
- Slash macros for quick math, unit and currency conversion, dates, and more
(type `/`).
- Autocorrect that fixes typos with a single keystroke.

Cotabby is currently in beta.

## 2. Is Cotabby free?

Yes. Cotabby is completely free and open source, released under the GNU Affero
General Public License v3.0 (AGPL-3.0). There is no subscription, no account, and
no paid tier. You are free to read, modify, and redistribute the source under the
terms of that license. The code lives at
[github.com/FuJacob/cotabby](https://github.com/FuJacob/cotabby).

The downloadable AI models are free too, and Apple Intelligence is built into
macOS, so there are no usage costs of any kind.

## 3. Is my data private? Does Cotabby send what I type to the cloud?

Privacy is the core design principle. Everything that produces a suggestion runs
on your Mac:

- All AI text generation happens on-device, using either Apple Intelligence
(built into macOS) or a local model running directly on your machine. There is
no hosted API and no cloud round-trip.
- When screen context is used, the screenshot is captured and read entirely
on-device with Apple's built-in text recognition. No images or recognized text
are ever uploaded.
- Cotabby contains no analytics, no telemetry, and no crash reporting.

The only times Cotabby uses the network are to download an AI model when you
choose to, to let you search for models in the optional in-app model browser, and
to check for app updates. None of these carry the text you type, your
suggestions, or anything from your screen.

For the technically curious: a normal install never writes your typed text to
disk. Only special developer debug builds write local diagnostic logs, and even
those stay on your Mac and are never transmitted.

## 4. What are the system requirements?

- macOS 14 (Sonoma) or later.
- Apple Silicon (M-series) is strongly recommended for good local-model
performance.
- Free disk space for any local models you download (from under 1 GB to around
5 GB each, depending on the model you pick).
- To use the Apple Intelligence engine specifically, you need macOS 26 or later
on a Mac that supports Apple Intelligence, with Apple Intelligence turned on in
System Settings. On older Macs, use the built-in Open Source engine instead.

## 5. How do I install Cotabby?

There are three ways:

- **Homebrew (recommended):**
```sh
brew tap FuJacob/cotabby
brew install --cask cotabby
```
Update later with `brew upgrade --cask cotabby`.
- **Direct download:** get the latest release from
[cotabby.app](https://cotabby.app) (or the GitHub Releases page) and drag
Cotabby into your Applications folder.
- **Build from source:** clone
[github.com/FuJacob/cotabby](https://github.com/FuJacob/cotabby) and open the
project in Xcode.

After launching, Cotabby lives in your menu bar and walks you through a short
setup. It checks for and installs updates automatically when new versions ship.

## 6. Why does Cotabby need Accessibility, Input Monitoring, and Screen Recording permissions?

Cotabby works inside other apps, so macOS requires you to grant three
permissions. Each one maps to a specific feature:

- **Accessibility:** to read the text and cursor position in the field you are
typing in, and to insert the text you accept.
- **Input Monitoring:** to notice your typing so it knows when to suggest, and to
recognize the accept key (`Tab`).
- **Screen Recording:** to capture the area around your cursor for visual
context, which makes suggestions more relevant.

Cotabby guides you through granting each one during setup and shows a reminder if
a permission is later turned off. You can change them anytime in System Settings
under Privacy & Security. Cotabby never operates in password or other secure
fields.

## 7. How do I use Cotabby, and how do I accept or dismiss a suggestion?

Start typing in any supported text field. When a suggestion appears as ghost
text:

- Press `Tab` to accept it. By default `Tab` accepts one word (or a full phrase,
depending on your Acceptance Mode setting).
- Press the accept-entire-suggestion key (backtick `` ` `` by default) to take
the whole suggestion at once.
- Keep typing to ignore the suggestion, or press `Esc` to dismiss it.

All of these keys are rebindable in Settings, under Shortcuts, so you can pick
whatever feels natural.

## 8. Which apps does Cotabby work in?

Cotabby works system-wide in almost any standard, editable text field, including
native Mac apps and most web and Electron apps (such as Chrome).

For your safety and privacy, it deliberately stays out of:

- Password and other secure or sensitive fields (passcodes, card numbers,
one-time codes, and the like).
- Terminal apps (Terminal, iTerm2, and others).

Some browser and Electron editors expose their contents to macOS a little
differently, so Cotabby includes special handling for them. If a particular
field does not expose what Cotabby needs, it simply stays quiet there.

## 9. Which AI model does Cotabby use, and does it work offline?

Cotabby gives you two engines, and you choose which one to use:

- **Apple Intelligence:** Apple's on-device model built into macOS (requires
macOS 26 or later on a supported Mac). Nothing to download.
- **Open Source:** a local model that runs directly on your Mac. You can pick
from several tiers, from a lightweight model (under 1 GB) up to a larger,
higher-quality one (around 5 GB). Larger models write better but run slower.
You can also import your own GGUF model or browse Hugging Face from inside the
app.

Both engines run entirely on your Mac. Once a model is downloaded (or Apple
Intelligence is set up), suggestions work fully offline, with no internet
required.

## 10. How do I customize Cotabby or turn it off?

Open Settings from the menu bar icon. A few of the things you can adjust:

- Suggestion length, multi-line suggestions, and Fast Mode (which skips screen
context for faster results).
- Ghost text color and opacity, and whether suggestions appear inline or as a
popup.
- Engine and model selection, emoji style, slash macros, and your keyboard
shortcuts.

To pause Cotabby:

- Turn off "Enable Globally" in the menu or General settings to disable it
everywhere without quitting.
- Add specific apps to the Disabled Apps list (or toggle it off for the app you
are currently in) to silence it just there.
- Set a global toggle shortcut in Settings, under Shortcuts, to flip it on and
off from the keyboard.

To quit entirely, choose Quit from the menu bar.