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
4 changes: 2 additions & 2 deletions Sources/OpenUsage/Providers/Claude/ClaudeProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ final class ClaudeProvider: ProviderRuntime {
.exportingLimit("session", unit: "percent"),
.percent(id: "claude.weekly", provider: provider, title: "Weekly")
.exportingLimit("weekly", unit: "percent"),
.percent(id: "claude.sonnet", provider: provider, title: "Sonnet")
.exportingLimit("sonnet", unit: "percent"),
.percent(id: "claude.fable", provider: provider, title: "Fable")
.exportingLimit("fable", unit: "percent"),
.percent(id: "claude.sonnet", provider: provider, title: "Sonnet")
.exportingLimit("sonnet", unit: "percent"),
.boundedDollars(id: "claude.extra", provider: provider, title: "Extra Usage", metricLabel: "Extra usage spent", limit: 100, valueWord: "spent")
.exportingLimit("extraUsage", unit: "usd", source: .progressOrValue(kind: .dollars)),
.usageTrend(provider: provider)
Expand Down
8 changes: 4 additions & 4 deletions Sources/OpenUsage/Stores/DefaultLayout.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ enum DefaultLayout {
static let metricIDs: [String] = [
"antigravity.geminiPro", "antigravity.geminiWeekly", "antigravity.claude", "antigravity.claudeWeekly",

"claude.session", "claude.weekly", "claude.trend",
"claude.session", "claude.weekly", "claude.fable", "claude.trend",
"claude.extra", "claude.today", "claude.yesterday", "claude.last30",

"codex.weekly", "codex.spark", "codex.sparkWeekly", "codex.trend",
Expand Down Expand Up @@ -77,9 +77,9 @@ enum DefaultLayout {
// Antigravity: the Gemini pool pair (5h + weekly) stays above the fold; the non-Gemini
// (Claude) pool pair sits below the caret.
"antigravity.claude", "antigravity.claudeWeekly",
// Claude's core meters (Session, Weekly, Extra, Usage Trend) stay above the fold; spend-history
// rows sit below the caret. Matches every other provider's "core above, history below" shape.
"claude.sonnet", "claude.fable", "claude.today", "claude.yesterday", "claude.last30",
// Claude's core meters (Session, Weekly, Fable, Extra, Usage Trend) stay above the fold;
// optional Sonnet and spend-history rows sit below the caret.
"claude.sonnet", "claude.today", "claude.yesterday", "claude.last30",
// Codex's core Session/Weekly meters and Usage Trend stay above the fold; Spark (the optional
// model-specific limits), credits, reset details, and spend rows sit below the caret.
"codex.spark", "codex.sparkWeekly",
Expand Down
12 changes: 11 additions & 1 deletion Sources/OpenUsage/Stores/LayoutBootstrap.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,15 @@ enum LayoutBootstrap {
shouldPersistExpanded = true
}

// A formerly optional On Demand metric can later become an always-visible default. Its old
// saved section was seeded before the user enabled it, so honor the new placement when the
// metric is first auto-added without disturbing metrics the user already arranged.
let newlyAlwaysShown = Set(seededResult.newlyPlaced).subtracting(defaults.expandedMetricIDs)
if !expandedMetricIDs.isDisjoint(with: newlyAlwaysShown) {
expandedMetricIDs.subtract(newlyAlwaysShown)
shouldPersistExpanded = true
}

Comment thread
cursor[bot] marked this conversation as resolved.
// Optional default-expanded metrics enter below the caret the first time they are enabled. The
// saved queue wins so an explicit user move is not recreated on the next launch.
let placedIDs = Set(seededResult.placed.map(\.descriptorID))
Expand All @@ -97,6 +106,7 @@ enum LayoutBootstrap {
let defaultExpandedOnEnableIDs = Set(
(savedOnEnable ?? defaults.expandedMetricIDs).filter(isExpandOnEnableCandidate)
)
let promotedQueuedIDs = Set(savedOnEnable ?? []).intersection(newlyAlwaysShown)

return LayoutInitialState(
placed: seededResult.placed,
Expand All @@ -109,7 +119,7 @@ enum LayoutBootstrap {
menuBarStyle: persistence.loadMenuBarStyle(),
shouldPersistPlaced: seededResult.shouldPersistPlaced,
shouldPersistExpanded: shouldPersistExpanded,
shouldPersistExpandOnEnable: savedOnEnable == nil,
shouldPersistExpandOnEnable: savedOnEnable == nil || !promotedQueuedIDs.isEmpty,
seededDefaultsToPersist: seededResult.shouldPersistSeededDefaults
? seededResult.seededDefaults
: nil
Expand Down
50 changes: 44 additions & 6 deletions Tests/OpenUsageTests/LayoutStoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,38 @@ final class LayoutStoreTests: XCTestCase {
XCTAssertTrue(reloaded.expandedMetricIDs.contains("claude.today"))
}

func testNewlySeededAlwaysVisibleMetricLeavesPreviouslySavedOnDemandSection() {
let defaults = makeDefaults("SeedNewAlwaysVisible")
saveStored([PlacedWidget(descriptorID: "claude.session")], forKey: "layout", in: defaults)
defaults.set(["claude.today"], forKey: "layout.expandedMetrics")
defaults.set(["claude.today", "cursor.requests"], forKey: "layout.expandOnEnable")

let makeStore = {
LayoutStore(
registry: .mock,
defaults: defaults,
storageKey: "layout",
defaultMetricIDs: ["claude.session", "claude.today"],
migrationBaselineMetricIDs: ["claude.session"],
defaultExpandedMetricIDs: ["cursor.requests"]
)
}

let store = makeStore()
XCTAssertTrue(store.isMetricEnabled("claude.today"))
XCTAssertFalse(store.expandedMetricIDs.contains("claude.today"))
XCTAssertEqual(store.defaultExpandedOnEnableIDs, ["cursor.requests"])
XCTAssertEqual(defaults.stringArray(forKey: "layout.expandOnEnable"), ["cursor.requests"])

store.setMetricEnabled("claude.today", false)

let reloaded = makeStore()
XCTAssertFalse(reloaded.expandedMetricIDs.contains("claude.today"))
reloaded.setMetricEnabled("claude.today", true)
XCTAssertFalse(reloaded.expandedMetricIDs.contains("claude.today"))
XCTAssertEqual(reloaded.defaultExpandedOnEnableIDs, ["cursor.requests"])
}

func testMigrationPersistKeepsLegacyOptionalMetricExpandOnEnableAfterReload() {
let defaults = makeDefaults("SeedExpandedKeepsFallback")
// Legacy layout: predates the expanded feature (no saved expanded set).
Expand Down Expand Up @@ -638,7 +670,7 @@ final class LayoutStoreTests: XCTestCase {
let store = LayoutStore(registry: registry, defaults: makeDefaults("FreshCustomizeOrder"), storageKey: "layout")

XCTAssertEqual(store.orderedSupportedMetrics(for: "claude").map(\.id), [
"claude.session", "claude.weekly", "claude.sonnet", "claude.fable", "claude.extra",
"claude.session", "claude.weekly", "claude.fable", "claude.sonnet", "claude.extra",
"claude.trend", "claude.today", "claude.yesterday", "claude.last30"
])
XCTAssertEqual(store.orderedSupportedMetrics(for: "codex").map(\.id), [
Expand Down Expand Up @@ -671,7 +703,7 @@ final class LayoutStoreTests: XCTestCase {
let store = LayoutStore(registry: registry, defaults: makeDefaults("RecommendedDefaults"), storageKey: "layout")

XCTAssertEqual(Set(store.placed.map(\.descriptorID)), Set([
"claude.session", "claude.weekly", "claude.trend",
"claude.session", "claude.weekly", "claude.fable", "claude.trend",
"claude.extra", "claude.today", "claude.yesterday", "claude.last30",
"codex.weekly", "codex.spark", "codex.sparkWeekly", "codex.trend",
"codex.credits", "codex.rateLimitResets", "codex.today", "codex.yesterday", "codex.last30",
Expand All @@ -682,6 +714,8 @@ final class LayoutStoreTests: XCTestCase {
"cursor.usage", "cursor.auto", "cursor.api", "cursor.grokBot", "cursor.trend",
"cursor.onDemand", "cursor.today", "cursor.yesterday", "cursor.last30"
]))
XCTAssertTrue(store.isMetricEnabled("claude.fable"))
XCTAssertFalse(store.isPinned("claude.fable"))
XCTAssertFalse(store.isMetricEnabled("claude.sonnet"))
XCTAssertFalse(store.isMetricEnabled("codex.session"))
XCTAssertFalse(store.isPinned("codex.session"))
Expand All @@ -698,10 +732,14 @@ final class LayoutStoreTests: XCTestCase {
($0.provider.id, $0.expandedMetrics.map(\.id))
})

// Claude's core meters (Session, Weekly, Extra, Usage Trend) stay primary; spend-history rows
// go below the caret — the same "core above, history below" shape as the other providers.
XCTAssertEqual(primaryByProvider["claude"], ["claude.session", "claude.weekly", "claude.extra", "claude.trend"])
XCTAssertEqual(expandedByProvider["claude"], ["claude.sonnet", "claude.fable", "claude.today", "claude.yesterday", "claude.last30"])
// Claude's core meters keep Fable directly below Weekly; optional Sonnet and spend-history
// rows go below the caret.
XCTAssertEqual(primaryByProvider["claude"], [
"claude.session", "claude.weekly", "claude.fable", "claude.extra", "claude.trend"
])
XCTAssertEqual(expandedByProvider["claude"], [
"claude.sonnet", "claude.today", "claude.yesterday", "claude.last30"
])
XCTAssertEqual(primaryByProvider["codex"], ["codex.session", "codex.weekly", "codex.trend"])
// Spark (the optional model-specific limits) leads the On Demand section, before credits.
XCTAssertEqual(expandedByProvider["codex"], [
Expand Down
Binary file added docs/assets/claude-fable-weekly.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 3 additions & 2 deletions docs/providers/claude.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ Tracks your Claude subscription limits using the login you already have from Cla
|---|---|
| Session | 5-hour rolling window usage |
| Weekly | 7-day window usage |
| Sonnet | Separate weekly Sonnet limit (plan-dependent) |
| Fable | Separate weekly Fable limit (model-scoped window from the `limits` array) |
| Sonnet | Separate weekly Sonnet limit (plan-dependent) |
| Extra Usage | Extra-usage credits spent against your monthly cap |
| Today / Yesterday / Last 30 Days | Local spend, as cost, tokens, or both (see below) |

When Claude reports your plan name, OpenUsage shows it beside the provider name.
Fable is enabled and always visible directly below Weekly by default. Sonnet stays off until you
enable it in Customize. When Claude reports your plan name, OpenUsage shows it beside the provider name.

## Where credentials come from

Expand Down