Add Ollama Cloud / Pro provider#197
Conversation
New OllamaProvider tracks quotas for the Ollama paid subscription with
two probe modes, following the dual-probe pattern used by Kimi:
- API mode (default): Bearer-token auth against ollama.com/api/tags.
Resolves the key from a user-configured env-var name, then
OLLAMA_API_KEY / OLLAMA_KEY, then a value stored in Settings.
- Web mode: cookie-based scraping of ollama.com/settings via
SweetCookieKit (already a project dependency). Recognises
__Secure-next-auth.session-token plus chunked variants and the
older session / ollama_session names; parses session and weekly
usage percent and reset times from the rendered HTML.
Files added:
- Sources/Domain/Provider/Ollama/OllamaProvider.swift
- Sources/Domain/Provider/Ollama/OllamaProbeMode.swift
- Sources/Infrastructure/Ollama/OllamaUsageProbe.swift
- Sources/Infrastructure/Ollama/OllamaWebUsageProbe.swift
- Sources/App/Views/Settings/OllamaConfigCard.swift
Wiring touches:
- ProviderSettingsRepository protocol gets OllamaSettingsRepository
- JSON and UserDefaults repositories add Ollama settings + persistence
- AppSettings exposes the Ollama accessor
- ClaudeBarApp registers OllamaProvider in the AIProviders list
- ProviderVisualIdentity + ProviderIcons add Ollama branding
(graphite/black gradient, cloud.fill SF Symbol fallback)
- SettingsView renders the Ollama config card when the provider
is enabled
Endpoints, cookie names, and parser markers were derived from the
working Ollama support in steipete/CodexBar (MIT-licensed). No code
copied verbatim; structure adapted to ClaudeBar's Domain / Infra /
App layering.
Defaults to disabled — matches CodexBar's default. Ollama has no
public usage-JSON endpoint at the time of writing, so the API probe
returns a connectivity-only snapshot until one exists; the Web probe
returns full session and weekly quotas today. A TODO in
OllamaUsageProbe.swift flags the URL + parser change needed when an
API endpoint ships.
Domain target build: clean. Infrastructure target build: clean.
Full ClaudeBar app build hits pre-existing Cognito generic-inference
errors in aws-sdk-swift (upstream, unrelated to this change).
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThis PR adds Ollama Cloud/Pro quota monitoring to ClaudeBar via dual authentication modes: API-key bearer tokens and cookie-based web scraping. The change includes probes, a provider, settings persistence, theming/icons, a SwiftUI settings card, app wiring, and a changelog entry. ChangesOllama Cloud/Pro quota monitoring
🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Sources/App/Views/Settings/OllamaConfigCard.swift`:
- Around line 366-368: Replace direct logging and UI exposure of the raw error
from monitor.provider(for: "ollama")?.lastError: do not interpolate
error.localizedDescription into AppLog.credentials.error or into
ollamaTestResult; instead log a static/redacted message via
AppLog.credentials.error (e.g. "Ollama connection test failed: [REDACTED]") and
set ollamaTestResult to a non-sensitive string (e.g. "Failed") or a redacted
message, while still preserving the original error locally if needed for secure
telemetry (but never interpolating it into logs or UI).
In `@Sources/Domain/Provider/Ollama/OllamaProvider.swift`:
- Around line 14-15: OllamaProvider.refresh() mutates observable state
(isSyncing, snapshot, lastError) then awaits activeProbe.probe(), which can race
when called concurrently (e.g., via QuotaMonitor.refresh(providerId:) or UI);
fix by serializing/coalescing refreshes or by actor-isolating mutable state:
either (A) convert the mutable state into an actor (or make OllamaProvider an
actor) and remove `@unchecked` Sendable so isSyncing/snapshot/lastError are
actor-isolated and refresh() awaits inside that actor, or (B) keep the class but
add an internal single-flight/serialization mechanism (e.g., an internal
refreshTask/lock) inside OllamaProvider.refresh() so if a refresh is in-flight
it returns/awaits the existing Task instead of starting a new one, and ensure
all mutations to isSyncing/snapshot/lastError happen only under that
serialization; update QuotaMonitor.refresh(providerId:) usage to rely on the
serialized refresh behavior rather than checking isSyncing externally.
In `@Sources/Infrastructure/Ollama/OllamaUsageProbe.swift`:
- Around line 109-111: The error log currently prints the raw response body
(variable body) via AppLog.probes.error and must be redacted; change the
AppLog.probes.error call in OllamaUsageProbe (where httpResponse.statusCode and
body are used) to not interpolate or log the raw body—either log a fixed
redacted placeholder like "<redacted response body>" or a safely
truncated/redacted summary produced by a dedicated sanitizer function (e.g.,
redactSensitive(_:) or redactResponse(_:)) and include only non-sensitive
metadata (HTTP status and safe summary/length), keeping the thrown
ProbeError.executionFailed message unchanged.
In `@Sources/Infrastructure/Storage/JSONSettingsRepository.swift`:
- Around line 519-535: The Ollama credential methods (saveOllamaApiKey,
getOllamaApiKey, deleteOllamaApiKey, hasOllamaApiKey) currently use the
UserDefaults instance named credentials, creating a second backend; change them
to persist into the JSONSettingsRepository/JSONSettingsStore instead using a
dot-notation key (e.g., "credentials.ollama.apiKey"). Replace
reads/writes/removes to credentials.* with JSONSettingsStore calls that set,
get, remove the key path, and update hasOllamaApiKey to check the JSON value
presence; keep the public API names the same but delegate all storage to
JSONSettingsRepository/JSONSettingsStore to maintain a single settings file.
In `@Sources/Infrastructure/Storage/UserDefaultsProviderSettingsRepository.swift`:
- Line 6: The UserDefaultsProviderSettingsRepository incorrectly adds Ollama
persistence (it currently conforms to OllamaSettingsRepository and stores Ollama
keys in UserDefaults) which duplicates the JSON-backed store; remove
Ollama-specific UserDefaults keys and the OllamaSettingsRepository conformance
from UserDefaultsProviderSettingsRepository and instead route all Ollama
get/set/remove operations to the single JSONSettingsRepository (either by
delegating to an injected JSONSettingsRepository instance or by removing the
Ollama methods here so the JSONSettingsRepository implementation is used).
Update any helper methods that read/write Ollama settings in
UserDefaultsProviderSettingsRepository (references to Ollama keys and methods
around lines noted in the review) to forward to JSONSettingsRepository to
preserve single-source-of-truth for settings.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 080a60dd-96e4-41c3-bf3a-6c8fee423b4c
📒 Files selected for processing (14)
CHANGELOG.mdSources/App/ClaudeBarApp.swiftSources/App/Settings/AppSettings.swiftSources/App/Views/ProviderIcons.swiftSources/App/Views/ProviderVisualIdentity.swiftSources/App/Views/Settings/OllamaConfigCard.swiftSources/App/Views/SettingsView.swiftSources/Domain/Provider/Ollama/OllamaProbeMode.swiftSources/Domain/Provider/Ollama/OllamaProvider.swiftSources/Domain/Provider/ProviderSettingsRepository.swiftSources/Infrastructure/Ollama/OllamaUsageProbe.swiftSources/Infrastructure/Ollama/OllamaWebUsageProbe.swiftSources/Infrastructure/Storage/JSONSettingsRepository.swiftSources/Infrastructure/Storage/UserDefaultsProviderSettingsRepository.swift
| public final class OllamaProvider: AIProvider, @unchecked Sendable { | ||
| // MARK: - Identity (Protocol Requirement) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify provider concurrency contracts and call sites.
# Expectation:
# 1) Confirm whether AIProvider requires Sendable/global actor isolation.
# 2) Inspect how/where OllamaProvider.refresh() is invoked concurrently.
rg -n --type=swift -C3 'protocol\s+AIProvider|`@MainActor`|Sendable|`@unchecked`\s+Sendable' Sources
rg -n --type=swift -C2 '\bOllamaProvider\b|\.refresh\s*\(|\.isAvailable\s*\(' SourcesRepository: tddworks/ClaudeBar
Length of output: 50377
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Inspect OllamaProvider around the reported lines
echo "=== OllamaProvider.swift: identity + state ==="
sed -n '1,220p' Sources/Domain/Provider/Ollama/OllamaProvider.swift | nl -ba | sed -n '1,120p'
echo "=== OllamaProvider.swift: refresh() implementation ==="
sed -n '1,220p' Sources/Domain/Provider/Ollama/OllamaProvider.swift | nl -ba | sed -n '110,210p'
# 2) Find the refresh() signature and any related flags/state
echo "=== OllamaProvider.swift: refresh-related declarations ==="
rg -n --type=swift '\brefresh\b|isSyncing|lastError|snapshot' Sources/Domain/Provider/Ollama/OllamaProvider.swift
# 3) Locate AIProvider protocol definition and any actor/isolation requirements
echo "=== AIProvider protocol definition ==="
rg -n --type=swift 'protocol\s+AIProvider\b' Sources/Domain Sources
# 4) Trace OllamaProvider.refresh() call sites
echo "=== Call sites referencing OllamaProvider or .refresh() ==="
rg -n --type=swift '\bOllamaProvider\b' Sources
rg -n --type=swift '\.refresh\s*\(' SourcesRepository: tddworks/ClaudeBar
Length of output: 170
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Focused call-site search: refresh() invoked on an AIProvider reference
rg -n --type=swift 'refresh\s*\(' Sources | head -n 200Repository: tddworks/ClaudeBar
Length of output: 5103
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Check if OllamaProvider is used in an actor/MainActor context
rg -n --type=swift 'MainActor|`@MainActor`|`@Observable`' Sources/Domain/Provider/Ollama Sources/Domain SourcesRepository: tddworks/ClaudeBar
Length of output: 5551
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== AIProvider.swift ==="
sed -n '1,140p' Sources/Domain/Provider/AIProvider.swift | cat -n
echo "=== QuotaMonitor.swift (refresh paths) ==="
sed -n '1,220p' Sources/Domain/Monitor/QuotaMonitor.swift | cat -n
sed -n '220,360p' Sources/Domain/Monitor/QuotaMonitor.swift | cat -n
echo "=== OllamaProvider.swift (full file if small, else targeted) ==="
wc -l Sources/Domain/Provider/Ollama/OllamaProvider.swift
sed -n '1,220p' Sources/Domain/Provider/Ollama/OllamaProvider.swift | cat -nRepository: tddworks/ClaudeBar
Length of output: 21916
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Inspect QuotaMonitor.startMonitoring usage (where it’s started/stopped)
echo "=== startMonitoring call sites ==="
rg -n --type=swift 'startMonitoring\(' Sources
# 2) Inspect MenuContentView refresh logic / Task usage (manual refresh)
echo "=== MenuContentView.swift refresh() helper ==="
sed -n '740,860p' Sources/App/Views/MenuContentView.swift | cat -n
# 3) Inspect OllamaConfigCard manual refresh action
echo "=== OllamaConfigCard.swift manual refresh ==="
sed -n '90,170p' Sources/App/Views/Settings/OllamaConfigCard.swift | cat -n
sed -n '330,390p' Sources/App/Views/Settings/OllamaConfigCard.swift | cat -n
# 4) Compare other providers’ refresh concurrency patterns
echo "=== Other providers using isSyncing/snapshot/lastError ==="
rg -n --type=swift 'isSyncing\s*=\s*true|defer\s*\{\s*isSyncing\s*=\s*false\s*\}|lastError\s*=' Sources/Domain/Provider
# Show one or two representative providers for pattern match
echo "=== ClaudeProvider.refresh() snippet ==="
sed -n '120,220p' Sources/Domain/Provider/Claude/ClaudeProvider.swift | cat -n
echo "=== CopilotProvider.refresh() snippet ==="
sed -n '120,230p' Sources/Domain/Provider/Copilot/CopilotProvider.swift | cat -n
echo "=== ZaiProvider.refresh() snippet ==="
sed -n '90,190p' Sources/Domain/Provider/Zai/ZaiProvider.swift | cat -n
echo "=== MistralProvider.refresh() snippet ==="
sed -n '40,130p' Sources/Domain/Provider/Mistral/MistralProvider.swift | cat -nRepository: tddworks/ClaudeBar
Length of output: 25400
Unisolated mutable state in OllamaProvider.refresh() can race across concurrent refresh calls
OllamaProvider is @Observable with mutable state (isSyncing, snapshot, lastError) but opts out of Swift’s concurrency checking via @unchecked Sendable. refresh() mutates that shared state and then awaits activeProbe.probe() (lines ~134-146), so concurrent refreshes can interleave.
QuotaMonitor.refresh(providerId:) calls provider.refresh() without checking provider.isSyncing, and the UI can trigger refresh concurrently (e.g., OllamaConfigCard calls await monitor.refresh(providerId: "ollama") while monitoring may also be running). Even though other call sites guard provider.isSyncing, that guard isn’t applied when going through QuotaMonitor.
Suggested direction
+@MainActor
`@Observable`
public final class OllamaProvider: AIProvider, `@unchecked` Sendable {Or, if AIProvider conformance allows it, actor-isolate the mutable state and remove @unchecked Sendable; otherwise add internal refresh serialization/coalescing so two refresh() calls for the same provider can’t run concurrently.
</details>
<details>
<summary>🤖 Prompt for AI Agents</summary>
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @Sources/Domain/Provider/Ollama/OllamaProvider.swift around lines 14 - 15,
OllamaProvider.refresh() mutates observable state (isSyncing, snapshot,
lastError) then awaits activeProbe.probe(), which can race when called
concurrently (e.g., via QuotaMonitor.refresh(providerId:) or UI); fix by
serializing/coalescing refreshes or by actor-isolating mutable state: either (A)
convert the mutable state into an actor (or make OllamaProvider an actor) and
remove @unchecked Sendable so isSyncing/snapshot/lastError are actor-isolated
and refresh() awaits inside that actor, or (B) keep the class but add an
internal single-flight/serialization mechanism (e.g., an internal
refreshTask/lock) inside OllamaProvider.refresh() so if a refresh is in-flight
it returns/awaits the existing Task instead of starting a new one, and ensure
all mutations to isSyncing/snapshot/lastError happen only under that
serialization; update QuotaMonitor.refresh(providerId:) usage to rely on the
serialized refresh behavior rather than checking isSyncing externally.
</details>
<!-- fingerprinting:phantom:poseidon:hawk -->
<!-- This is an auto-generated comment by CodeRabbit -->
| // Ollama Credentials (UserDefaults for now, Keychain migration later) | ||
|
|
||
| public func saveOllamaApiKey(_ key: String) { | ||
| credentials.set(key, forKey: "com.claudebar.credentials.ollama-api-key") | ||
| } | ||
|
|
||
| public func getOllamaApiKey() -> String? { | ||
| credentials.string(forKey: "com.claudebar.credentials.ollama-api-key") | ||
| } | ||
|
|
||
| public func deleteOllamaApiKey() { | ||
| credentials.removeObject(forKey: "com.claudebar.credentials.ollama-api-key") | ||
| } | ||
|
|
||
| public func hasOllamaApiKey() -> Bool { | ||
| getOllamaApiKey() != nil | ||
| } |
There was a problem hiding this comment.
Store Ollama API key in JSONSettingsStore instead of UserDefaults.
These methods introduce a second backend for Ollama settings (credentials in UserDefaults, config in JSON), which creates split state and violates the storage contract for this layer.
Proposed fix
- // Ollama Credentials (UserDefaults for now, Keychain migration later)
+ // MARK: - OllamaSettingsRepository (Credentials)
public func saveOllamaApiKey(_ key: String) {
- credentials.set(key, forKey: "com.claudebar.credentials.ollama-api-key")
+ store.write(value: key, key: "ollama.apiKey")
}
public func getOllamaApiKey() -> String? {
- credentials.string(forKey: "com.claudebar.credentials.ollama-api-key")
+ store.read(key: "ollama.apiKey")
}
public func deleteOllamaApiKey() {
- credentials.removeObject(forKey: "com.claudebar.credentials.ollama-api-key")
+ store.write(value: Optional<String>.none, key: "ollama.apiKey")
}
public func hasOllamaApiKey() -> Bool {
- getOllamaApiKey() != nil
+ (getOllamaApiKey()?.isEmpty == false)
}As per coding guidelines Sources/Infrastructure/Storage/*.swift: “Persist all settings in a single JSON file at ~/.claudebar/settings.json via JSONSettingsRepository” and “Use dot-notation key paths in JSON settings.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Sources/Infrastructure/Storage/JSONSettingsRepository.swift` around lines 519
- 535, The Ollama credential methods (saveOllamaApiKey, getOllamaApiKey,
deleteOllamaApiKey, hasOllamaApiKey) currently use the UserDefaults instance
named credentials, creating a second backend; change them to persist into the
JSONSettingsRepository/JSONSettingsStore instead using a dot-notation key (e.g.,
"credentials.ollama.apiKey"). Replace reads/writes/removes to credentials.* with
JSONSettingsStore calls that set, get, remove the key path, and update
hasOllamaApiKey to check the JSON value presence; keep the public API names the
same but delegate all storage to JSONSettingsRepository/JSONSettingsStore to
maintain a single settings file.
| /// UserDefaults-based implementation of ProviderSettingsRepository and its sub-protocols. | ||
| /// Persists provider settings like isEnabled state and provider-specific configuration. | ||
| public final class UserDefaultsProviderSettingsRepository: ZaiSettingsRepository, CopilotSettingsRepository, BedrockSettingsRepository, ClaudeSettingsRepository, CodexSettingsRepository, KimiSettingsRepository, MiniMaxSettingsRepository, AlibabaSettingsRepository, HookSettingsRepository, @unchecked Sendable { | ||
| public final class UserDefaultsProviderSettingsRepository: ZaiSettingsRepository, CopilotSettingsRepository, BedrockSettingsRepository, ClaudeSettingsRepository, CodexSettingsRepository, KimiSettingsRepository, MiniMaxSettingsRepository, AlibabaSettingsRepository, OllamaSettingsRepository, HookSettingsRepository, @unchecked Sendable { |
There was a problem hiding this comment.
Avoid adding Ollama persistence to the UserDefaults storage path.
This introduces a second persistence source for Ollama (UserDefaults) instead of the JSON-backed settings store, which can drift from JSONSettingsRepository and breaks the storage-layer consistency requirement. Prefer routing Ollama persistence through the JSON repository only (or delegating these methods to it) rather than defining separate UserDefaults keys.
As per coding guidelines Sources/Infrastructure/Storage/*.swift: “Persist all settings in a single JSON file at ~/.claudebar/settings.json via JSONSettingsRepository.”
Also applies to: 359-394, 455-458
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Sources/Infrastructure/Storage/UserDefaultsProviderSettingsRepository.swift`
at line 6, The UserDefaultsProviderSettingsRepository incorrectly adds Ollama
persistence (it currently conforms to OllamaSettingsRepository and stores Ollama
keys in UserDefaults) which duplicates the JSON-backed store; remove
Ollama-specific UserDefaults keys and the OllamaSettingsRepository conformance
from UserDefaultsProviderSettingsRepository and instead route all Ollama
get/set/remove operations to the single JSONSettingsRepository (either by
delegating to an injected JSONSettingsRepository instance or by removing the
Ollama methods here so the JSONSettingsRepository implementation is used).
Update any helper methods that read/write Ollama settings in
UserDefaultsProviderSettingsRepository (references to Ollama keys and methods
around lines noted in the review) to forward to JSONSettingsRepository to
preserve single-source-of-truth for settings.
Two narrow fixes from the CodeRabbit review on this PR (both Major,
both straightforward quick wins):
1. OllamaConfigCard.swift:367 — `AppLog.credentials.error(...)` no
longer interpolates `error.localizedDescription`. The user-facing
`ollamaTestResult` string still surfaces the description so the
person running the test sees what failed, but the system log
stops at a static "Ollama connection test failed" line. Avoids
leaking bearer-token fragments or session cookie context into
os_log persistence.
2. OllamaUsageProbe.swift:109 — non-2xx HTTP responses from
ollama.com no longer log the first 200 bytes of the response
body. Ollama responses can embed account email, plan name, and
session token IDs; status code plus response length is enough
signal for debugging.
The remaining three findings (concurrency check on the @observable
provider, and the two storage-split critiques) are answered inline on
the PR — they match existing patterns used elsewhere in the repo
(Codex/Kimi for `@unchecked Sendable`, Copilot for split JSON-config
+ credentials-UserDefaults).
|
Thanks @coderabbitai — useful pass. Quick disposition on the five findings: ✅ Addressed in 30a6f31:
🛡️ Defended (existing repo convention):
Net: 2 fixes pushed, 3 defended with pointers to existing precedent. Let me know if any of the three should still be addressed in this PR and I'll iterate. |
|
Tip For best results, initiate chat on the files or code changes.
✅ Fixes confirmed — items 1 and 3 look good in 30a6f31. Static log strings + byte-count-only logging is exactly the right trade-off. 🛡️ Defenses accepted — the precedent pointers for items 2, 4, and 5 are solid:
Would you like me to open a follow-up GitHub issue to track the Keychain migration for all providers (Copilot, Ollama, and future ones)? I can reference this PR and the existing (´。• ᵕ •。`) ♡ ✏️ Learnings added
|
There was a problem hiding this comment.
Pull request overview
Adds first-class support for tracking Ollama Cloud / Pro usage in ClaudeBar via a new provider that can probe either via API key (connectivity check) or browser session cookies (quota scraping), plus corresponding settings persistence and UI configuration.
Changes:
- Introduces
OllamaProvider+OllamaProbeModewith dual-probe behavior (API + Web/cookie). - Adds Infrastructure probes for Ollama API connectivity and
ollama.com/settingsHTML scraping. - Wires Ollama into settings storage (JSON + UserDefaults), UI settings card, and provider visual identity/icon fallbacks.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| Sources/Infrastructure/Storage/UserDefaultsProviderSettingsRepository.swift | Adds Ollama settings + credential persistence in UserDefaults. |
| Sources/Infrastructure/Storage/JSONSettingsRepository.swift | Adds Ollama settings + credential persistence in JSON/credentials stores. |
| Sources/Infrastructure/Ollama/OllamaWebUsageProbe.swift | New SweetCookieKit-backed web probe scraping quotas from ollama.com/settings. |
| Sources/Infrastructure/Ollama/OllamaUsageProbe.swift | New API-key probe (connectivity check via /api/tags). |
| Sources/Domain/Provider/ProviderSettingsRepository.swift | Defines OllamaSettingsRepository protocol. |
| Sources/Domain/Provider/Ollama/OllamaProvider.swift | New provider domain model with probe-mode switching. |
| Sources/Domain/Provider/Ollama/OllamaProbeMode.swift | New enum for .api/.web probe modes. |
| Sources/App/Views/SettingsView.swift | Shows Ollama settings card when Ollama provider is enabled. |
| Sources/App/Views/Settings/OllamaConfigCard.swift | New UI for configuring probe mode, API key/env var, and connection testing. |
| Sources/App/Views/ProviderVisualIdentity.swift | Adds Ollama theme color/gradient + lookup entries. |
| Sources/App/Views/ProviderIcons.swift | Adds Ollama symbol fallback icon mapping. |
| Sources/App/Settings/AppSettings.swift | Exposes ollama settings repository accessor. |
| Sources/App/ClaudeBarApp.swift | Registers OllamaProvider and its probes in DI/provider list. |
| CHANGELOG.md | Documents Ollama Cloud/Pro support and limitations. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| AppLog.credentials.info("Testing Ollama connection via provider refresh") | ||
| await monitor.refresh(providerId: "ollama") | ||
|
|
||
| if let error = monitor.provider(for: "ollama")?.lastError { | ||
| // Keep the credential log static — `error.localizedDescription` can | ||
| // contain bearer-token fragments or session cookie context that | ||
| // shouldn't land in os_log (CodeRabbit review on PR #197). | ||
| // The user-facing string in the UI still shows the message so the | ||
| // human running the test sees what happened; the system log does | ||
| // not. | ||
| AppLog.credentials.error("Ollama connection test failed") | ||
| ollamaTestResult = "Failed: \(error.localizedDescription)" | ||
| } else { | ||
| AppLog.credentials.info("Ollama connection test succeeded") | ||
| ollamaTestResult = "Success: Connection verified" | ||
| } | ||
|
|
| public func ollamaProbeMode() -> OllamaProbeMode { | ||
| guard let raw: String = store.read(key: "ollama.probeMode"), | ||
| let mode = OllamaProbeMode(rawValue: raw) else { | ||
| return .api | ||
| } | ||
| return mode | ||
| } | ||
|
|
||
| public func setOllamaProbeMode(_ mode: OllamaProbeMode) { | ||
| store.write(value: mode.rawValue, key: "ollama.probeMode") | ||
| } | ||
|
|
||
| public func ollamaAuthEnvVar() -> String { | ||
| store.read(key: "ollama.authEnvVar") ?? "" | ||
| } | ||
|
|
||
| public func setOllamaAuthEnvVar(_ envVar: String) { | ||
| store.write(value: envVar, key: "ollama.authEnvVar") | ||
| } |
| public func ollamaProbeMode() -> OllamaProbeMode { | ||
| guard let rawValue = userDefaults.string(forKey: Keys.ollamaProbeMode) else { | ||
| return .api // Default to API mode (more reliable than cookie scraping) | ||
| } | ||
| return OllamaProbeMode(rawValue: rawValue) ?? .api | ||
| } | ||
|
|
||
| public func setOllamaProbeMode(_ mode: OllamaProbeMode) { | ||
| userDefaults.set(mode.rawValue, forKey: Keys.ollamaProbeMode) | ||
| } | ||
|
|
||
| public func ollamaAuthEnvVar() -> String { | ||
| userDefaults.string(forKey: Keys.ollamaAuthEnvVar) ?? "" | ||
| } | ||
|
|
||
| public func setOllamaAuthEnvVar(_ envVar: String) { | ||
| userDefaults.set(envVar, forKey: Keys.ollamaAuthEnvVar) | ||
| } | ||
|
|
||
| public func saveOllamaApiKey(_ key: String) { | ||
| userDefaults.set(key, forKey: Keys.ollamaApiKey) | ||
| } | ||
|
|
||
| public func getOllamaApiKey() -> String? { | ||
| userDefaults.string(forKey: Keys.ollamaApiKey) | ||
| } | ||
|
|
||
| public func deleteOllamaApiKey() { | ||
| userDefaults.removeObject(forKey: Keys.ollamaApiKey) | ||
| } | ||
|
|
||
| public func hasOllamaApiKey() -> Bool { | ||
| userDefaults.object(forKey: Keys.ollamaApiKey) != nil | ||
| } |
| /// Returns the probe matching the current mode, falling back to the | ||
| /// other probe when the requested one is unavailable. |
Summary
Adds a new
OllamaProviderto ClaudeBar that tracks usage quotas for the Ollama Cloud / Pro subscription. Closes a gap for users who pay for Ollama Pro alongside Claude / Codex / Gemini and want it surfaced in the same menu bar instead of a separate app.Follows the dual-probe pattern that
KimiProvideralready uses (API mode + Web mode, user-switchable in Settings), and reuses the existingSweetCookieKitdependency — no new external packages.What's new
ollama), graphite/black gradient branding, dashboard link to https://ollama.com/settings.https://ollama.com/api/tags. Resolves the key from a user-configured env-var name, thenOLLAMA_API_KEY/OLLAMA_KEY, then a value stored in Settings.ollama.com/settingsvia SweetCookieKit. Recognises__Secure-next-auth.session-tokenplus chunked variants and the oldersession/ollama_sessioncookie names. Parses session + weekly usage percent and reset times out of the rendered HTML.OllamaConfigCardwith a probe-mode segmented picker, secure API-key field with show/hide, env-var name override, lookup-order legend, and a Save & Test button.Files
Sources/Domain/Provider/Ollama/OllamaProvider.swift@Observable AIProvider, dual-probe shapeSources/Domain/Provider/Ollama/OllamaProbeMode.swift.api/.webenum, mirrorsKimiProbeModeSources/Infrastructure/Ollama/OllamaUsageProbe.swiftSources/Infrastructure/Ollama/OllamaWebUsageProbe.swiftSources/App/Views/Settings/OllamaConfigCard.swiftWiring touches (matching the conventions other providers already use):
ProviderSettingsRepository, the JSON and UserDefaults repositories,AppSettings,ClaudeBarAppDI list,ProviderVisualIdentity,ProviderIcons,SettingsView.Reference
Endpoints, cookie names, and parser markers were derived from the working Ollama support in steipete/CodexBar (MIT). No code copied verbatim — structure adapted to ClaudeBar's Domain / Infrastructure / App architecture.
Known limitations (documented inline)
/api/tagsas a connectivity check and returns a connectivity-only snapshot (same shape used by Mistral). When Ollama publishes a real usage endpoint, the URL andparseUsageResponse(_:)inOllamaUsageProbe.swiftneed updating — there is a clearly marked TODO. The Web probe already returns full session + weekly quotas today.ProviderIconViewfalls back tocloud.fillSF Symbol over the graphite gradient until someone drops in a proper Ollama logo asset.defaultEnabled: falsefor the same provider.Build / verification
Domaintarget: clean buildInfrastructuretarget: clean buildClaudeBarworkspace build currently fails onInternalAWSCognitoIdentity/CognitoIdentityClient.swiftwith a Cognito generic-inference error inside the upstreamaws-sdk-swiftpackage. Pre-existing — unrelated to this PR. Reproduces onmainwith no Ollama changes; appears to be a Swift 6 / aws-sdk-swift version interaction. Happy to investigate separately if useful.Tested on macOS 26.5 + Xcode 26.5 with Tuist 4.x.
Test plan
OLLAMA_API_KEYenv var set → Ollama row appears in the menu with current snapshotnotLoggedIncleanly (no crash, no infinite spinner)[Unreleased]CHANGELOG entry addedFirst PR to this repo — happy to revise anything that doesn't match upstream conventions. Calling out my assumptions explicitly:
KimiProvidershape (dual probe + settings repo + UI card) rather than the simplerCursorProvidershape, because Ollama legitimately benefits from both API + Web auth and the Kimi precedent is already there.ollama.*— happy to rename if there's a preferred prefix convention.Summary by CodeRabbit