Skip to content

Add Ollama Cloud / Pro provider#197

Open
pdurlej wants to merge 2 commits into
tddworks:mainfrom
pdurlej:feature/ollama-provider
Open

Add Ollama Cloud / Pro provider#197
pdurlej wants to merge 2 commits into
tddworks:mainfrom
pdurlej:feature/ollama-provider

Conversation

@pdurlej

@pdurlej pdurlej commented May 25, 2026

Copy link
Copy Markdown

Summary

Adds a new OllamaProvider to 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 KimiProvider already uses (API mode + Web mode, user-switchable in Settings), and reuses the existing SweetCookieKit dependency — no new external packages.

What's new

  • New provider: Ollama (ollama), graphite/black gradient branding, dashboard link to https://ollama.com/settings.
  • API mode (default) — Bearer-token auth against https://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. Recognises __Secure-next-auth.session-token plus chunked variants and the older session / ollama_session cookie names. Parses session + weekly usage percent and reset times out of the rendered HTML.
  • Settings UI — new OllamaConfigCard with 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

Layer File Purpose
Domain Sources/Domain/Provider/Ollama/OllamaProvider.swift @Observable AIProvider, dual-probe shape
Domain Sources/Domain/Provider/Ollama/OllamaProbeMode.swift .api / .web enum, mirrors KimiProbeMode
Infra Sources/Infrastructure/Ollama/OllamaUsageProbe.swift Bearer-token API probe
Infra Sources/Infrastructure/Ollama/OllamaWebUsageProbe.swift Cookie-scraping web probe
App Sources/App/Views/Settings/OllamaConfigCard.swift Settings card

Wiring touches (matching the conventions other providers already use): ProviderSettingsRepository, the JSON and UserDefaults repositories, AppSettings, ClaudeBarApp DI 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)

  1. No public Ollama usage-JSON endpoint at the time of writing. The API probe currently uses /api/tags as a connectivity check and returns a connectivity-only snapshot (same shape used by Mistral). When Ollama publishes a real usage endpoint, the URL and parseUsageResponse(_:) in OllamaUsageProbe.swift need updating — there is a clearly marked TODO. The Web probe already returns full session + weekly quotas today.
  2. No asset catalog logo. ProviderIconView falls back to cloud.fill SF Symbol over the graphite gradient until someone drops in a proper Ollama logo asset.
  3. Defaults to disabled — matches CodexBar's defaultEnabled: false for the same provider.

Build / verification

  • Domain target: clean build
  • Infrastructure target: clean build
  • ⚠️ Full ClaudeBar workspace build currently fails on InternalAWSCognitoIdentity/CognitoIdentityClient.swift with a Cognito generic-inference error inside the upstream aws-sdk-swift package. Pre-existing — unrelated to this PR. Reproduces on main with 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

  • Enable Ollama in Settings with OLLAMA_API_KEY env var set → Ollama row appears in the menu with current snapshot
  • Switch probe mode to Web with no env var, browser logged in to ollama.com → cookie fallback fetches session + weekly quotas
  • With no auth available, provider reports notLoggedIn cleanly (no crash, no infinite spinner)
  • [Unreleased] CHANGELOG entry added

First PR to this repo — happy to revise anything that doesn't match upstream conventions. Calling out my assumptions explicitly:

  • Defaulted to disabled to match CodexBar; flip if you'd prefer it on for early testers.
  • Used KimiProvider shape (dual probe + settings repo + UI card) rather than the simpler CursorProvider shape, because Ollama legitimately benefits from both API + Web auth and the Kimi precedent is already there.
  • All new Settings repository keys namespaced under ollama.* — happy to rename if there's a preferred prefix convention.

Summary by CodeRabbit

  • New Features
    • Added Ollama Cloud/Pro quota tracking with two probe modes: API key and session-cookie (web) monitoring.
    • New Ollama settings panel for choosing probe mode, entering/saving API credentials, environment-var support, and connection testing.
    • Integrated Ollama into provider list with visual identity (icon, color, theme) and quota monitoring support.

Review Change Stack

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).
@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6de82a53-0162-4d87-85f1-de923cdc4fb3

📥 Commits

Reviewing files that changed from the base of the PR and between 2e928d7 and 30a6f31.

📒 Files selected for processing (2)
  • Sources/App/Views/Settings/OllamaConfigCard.swift
  • Sources/Infrastructure/Ollama/OllamaUsageProbe.swift

📝 Walkthrough

Walkthrough

This 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.

Changes

Ollama Cloud/Pro quota monitoring

Layer / File(s) Summary
Domain contracts and probe modes
Sources/Domain/Provider/Ollama/OllamaProbeMode.swift, Sources/Domain/Provider/ProviderSettingsRepository.swift
OllamaProbeMode enum defines API and Web authentication modes; OllamaSettingsRepository protocol specifies probe mode selection, auth environment variable configuration, and API key credential CRUD operations.
OllamaProvider implementation
Sources/Domain/Provider/Ollama/OllamaProvider.swift
Observable OllamaProvider class implements AIProvider with persisted isEnabled, probe-mode selection with fallback logic, and dual-probe support. Delegates refresh() and isAvailable() to the active probe and exposes observable sync state.
API probe (bearer token)
Sources/Infrastructure/Ollama/OllamaUsageProbe.swift
OllamaUsageProbe resolves an API key (custom env var, standard env vars, or saved credential), issues GET /api/tags with Bearer auth, maps HTTP status codes (200/401/403/429) to outcomes, and parses the tags response into a UsageSnapshot.
Web probe (cookie-based scraping)
Sources/Infrastructure/Ollama/OllamaWebUsageProbe.swift
OllamaWebUsageProbe locates session cookies across browser stores, fetches https://ollama.com/settings using a cookie header, and extracts plan, account email, and usage percentages/resets via permissive regex parsing to produce a UsageSnapshot.
Settings persistence
Sources/Infrastructure/Storage/JSONSettingsRepository.swift, Sources/Infrastructure/Storage/UserDefaultsProviderSettingsRepository.swift
Both repositories conform to OllamaSettingsRepository. Probe mode and auth env-var persist via JSONSettingsStore; API key credentials are stored/retrieved/deleted/checked via UserDefaults.
Visual identity and icons
Sources/App/Views/ProviderVisualIdentity.swift, Sources/App/Views/ProviderIcons.swift
Add OllamaProvider visual identity: icon asset name, SF Symbol fallback cloud.fill, dark/light theme colors and gradients, and lookup mappings for providerId "ollama".
Settings UI and configuration
Sources/App/Views/Settings/OllamaConfigCard.swift
OllamaConfigCard provides probe-mode picker that persists and refreshes the provider, API key secure/plain entry with show/hide, alternative env-var input, Save & Test Connection async flow, and Remove API Key action.
App wiring and view integration
Sources/App/ClaudeBarApp.swift, Sources/App/Settings/AppSettings.swift, Sources/App/Views/SettingsView.swift
Wire OllamaProvider (API + Web probes) into the providers registry; expose ollama settings accessor; add ProviderID.ollama, isOllamaEnabled, and conditional OllamaConfigCard rendering in Settings.
Changelog
CHANGELOG.md
Add an [Unreleased] entry documenting Ollama Cloud/Pro quota tracking, probe modes, Settings UI, parsing notes, default-disabled behavior, and a TODO for future API endpoint updates.

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

"🐰 A cloud drifts down from Ollama's sky,
Two probes hop out—one low, one high;
Keys and cookies in a nimble chase,
Quotas counted with a SwiftUI grace,
Settings card tucked in a cozy place."

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.82% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Add Ollama Cloud / Pro provider' is concise, specific, and directly describes the main change—introducing support for Ollama Cloud/Pro subscription monitoring as a new AI provider in ClaudeBar.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

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.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 208d433 and 2e928d7.

📒 Files selected for processing (14)
  • CHANGELOG.md
  • Sources/App/ClaudeBarApp.swift
  • Sources/App/Settings/AppSettings.swift
  • Sources/App/Views/ProviderIcons.swift
  • Sources/App/Views/ProviderVisualIdentity.swift
  • Sources/App/Views/Settings/OllamaConfigCard.swift
  • Sources/App/Views/SettingsView.swift
  • Sources/Domain/Provider/Ollama/OllamaProbeMode.swift
  • Sources/Domain/Provider/Ollama/OllamaProvider.swift
  • Sources/Domain/Provider/ProviderSettingsRepository.swift
  • Sources/Infrastructure/Ollama/OllamaUsageProbe.swift
  • Sources/Infrastructure/Ollama/OllamaWebUsageProbe.swift
  • Sources/Infrastructure/Storage/JSONSettingsRepository.swift
  • Sources/Infrastructure/Storage/UserDefaultsProviderSettingsRepository.swift

Comment thread Sources/App/Views/Settings/OllamaConfigCard.swift
Comment on lines +14 to +15
public final class OllamaProvider: AIProvider, @unchecked Sendable {
// MARK: - Identity (Protocol Requirement)

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.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 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*\(' Sources

Repository: 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*\(' Sources

Repository: 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 200

Repository: 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 Sources

Repository: 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 -n

Repository: 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 -n

Repository: 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 -->

Comment thread Sources/Infrastructure/Ollama/OllamaUsageProbe.swift Outdated
Comment on lines +519 to +535
// 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
}

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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 {

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.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

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).
@pdurlej

pdurlej commented May 25, 2026

Copy link
Copy Markdown
Author

Thanks @coderabbitai — useful pass. Quick disposition on the five findings:

✅ Addressed in 30a6f31:

  1. OllamaConfigCard.swift:367 — credential log leak. Right call. The error.localizedDescription interpolation is gone from AppLog.credentials.error(...); the user-facing ollamaTestResult still shows it so the human running the test sees what failed, but the system log stays static.
  2. OllamaUsageProbe.swift:109 — raw response body leak. Also right. Non-2xx responses now log HTTP <code> (<bytes> bytes) instead of body.prefix(200). Ollama responses can carry account email and session token fragments.

🛡️ Defended (existing repo convention):

  1. OllamaProvider.swift:15@unchecked Sendable on @Observable. This mirrors the pattern already used by KimiProvider and CodexProvider in this repo — both declare public final class XxxProvider: AIProvider, @unchecked Sendable with the same @Observable macro. Not introducing a new concurrency model, matching the precedent. Happy to revisit if the maintainer wants to tighten this across all providers in a separate cleanup.

  2. JSONSettingsRepository.swift:519 — Ollama credentials in credentials UserDefaults. This matches the existing GitHub Copilot pattern in the same file (saveGithubToken / getGithubToken / deleteGithubToken / hasGithubToken all use credentials.set(...forKey: "com.claudebar.credentials.github-copilot-token")). Split storage (config in JSON store, credentials in UserDefaults under a com.claudebar.credentials.* prefix) appears to be the established convention here for secrets — moving Ollama into JSON store alone would diverge from Copilot and introduce a different precedent. Open to a follow-up PR that migrates both providers (and any future ones) to a unified backend (Keychain ideally — the comment in JSON repo already flags it: // UserDefaults for now, Keychain migration later), but that feels out of scope for this PR.

  3. UserDefaultsProviderSettingsRepository.swift:6 — Ollama persistence path. Same answer as Support Claude Pro/API account detection with cost budgeting #4 — Copilot is also persisted via both storage repositories (UserDefaultsProviderSettingsRepository has saveGithubToken / getGithubToken / deleteGithubToken). The dual-repo registration matches how every other settings-bearing provider is wired. Happy to fold into the same hypothetical Keychain migration.

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.

@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@pdurlej — clean disposition, all five accounted for. Happy with the outcome:

✅ 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:

  • @unchecked Sendable + @Observable is clearly the established pattern for providers in this repo.
  • com.claudebar.credentials.* in UserDefaults is the repo-wide secret-storage convention, not a new pattern introduced here.
  • Dual-repo registration follows every other settings-bearing provider.

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 // UserDefaults for now, Keychain migration later comment as context.

(´。• ᵕ •。`) ♡


✏️ Learnings added
Learnt from: pdurlej
URL: https://github.com/tddworks/ClaudeBar/pull/197

Timestamp: 2026-05-25T06:58:44.424Z
Learning: In the ClaudeBar repository (Swift/SwiftUI macOS app), the established convention for `Observable` provider classes (e.g., KimiProvider, CodexProvider, OllamaProvider) is to declare them as `public final class XxxProvider: AIProvider, unchecked Sendable` with the `Observable` macro. Using `unchecked Sendable` here is intentional and matches existing precedent — do not flag it as a new concurrency concern.

Learnt from: pdurlej
URL: https://github.com/tddworks/ClaudeBar/pull/197

Timestamp: 2026-05-25T06:58:44.424Z
Learning: In the ClaudeBar repository, credentials/secrets for AI providers are stored in UserDefaults under the `com.claudebar.credentials.*` key prefix (e.g., `com.claudebar.credentials.github-copilot-token`, `com.claudebar.credentials.ollama-api-key`), while non-secret config is stored in the JSON settings store. This split-storage pattern (config in JSON store, credentials in UserDefaults) is the established repo convention. A comment in the codebase explicitly notes `// UserDefaults for now, Keychain migration later` — a future Keychain migration is planned but out-of-scope for individual provider PRs.

Learnt from: pdurlej
URL: https://github.com/tddworks/ClaudeBar/pull/197

Timestamp: 2026-05-25T06:58:44.424Z
Learning: In the ClaudeBar repository, settings-bearing AI providers are registered in both `JSONSettingsRepository` and `UserDefaultsProviderSettingsRepository`. Dual-repo registration for Ollama (and similar providers) follows the existing pattern for GitHub Copilot and others — do not flag it as a duplication issue.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 + OllamaProbeMode with dual-probe behavior (API + Web/cookie).
  • Adds Infrastructure probes for Ollama API connectivity and ollama.com/settings HTML 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.

Comment on lines +363 to +379
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"
}

Comment on lines +499 to +517
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")
}
Comment on lines +361 to +394
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
}
Comment on lines +77 to +78
/// Returns the probe matching the current mode, falling back to the
/// other probe when the requested one is unavailable.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants