diff --git a/Sources/Runway/App/SettingsMigrator.swift b/Sources/Runway/App/SettingsMigrator.swift index f4845e615..c0648170f 100644 --- a/Sources/Runway/App/SettingsMigrator.swift +++ b/Sources/Runway/App/SettingsMigrator.swift @@ -30,7 +30,7 @@ struct SettingsMigration: Sendable { enum SettingsSchema { /// Current schema version. Keep equal to the highest migration `version` below (or the baseline when /// there are none). This is NOT the app version — bump it only alongside a migration you add. - static let current = 5 + static let current = 6 /// The provider IDs that existed when the v2 migration shipped, frozen forever. A migration is a /// point-in-time transform: any future build with more providers also contains this migration, so a @@ -97,6 +97,25 @@ enum SettingsSchema { ) } defaults.set(try JSONEncoder().encode(order), forKey: key) + }, + // v6 promotes Muse Weekly Usage out of On Demand (it shipped below the caret) and stars + // both Muse meters. Fresh installs already declare that layout; this only rewrites saved + // expanded / pin lists so existing cards match without a reset. + SettingsMigration(version: 6) { defaults in + if var expanded = defaults.stringArray(forKey: "runway.layout.v1.expandedMetrics") { + expanded.removeAll { $0 == "muse.weekly" } + defaults.set(expanded, forKey: "runway.layout.v1.expandedMetrics") + } + if var onEnable = defaults.stringArray(forKey: "runway.layout.v1.expandOnEnable") { + onEnable.removeAll { $0 == "muse.weekly" } + defaults.set(onEnable, forKey: "runway.layout.v1.expandOnEnable") + } + if var pins = defaults.stringArray(forKey: "runway.layout.v1.menuBarPins") { + for id in ["muse.session", "muse.weekly"] where !pins.contains(id) { + pins.append(id) + } + defaults.set(pins, forKey: "runway.layout.v1.menuBarPins") + } } ] diff --git a/Sources/Runway/Resources/ProviderIcons/muse.svg b/Sources/Runway/Resources/ProviderIcons/muse.svg index c318a8770..2bc8f11e3 100644 --- a/Sources/Runway/Resources/ProviderIcons/muse.svg +++ b/Sources/Runway/Resources/ProviderIcons/muse.svg @@ -1,3 +1,3 @@ - - + + diff --git a/Sources/Runway/Stores/DefaultLayout.swift b/Sources/Runway/Stores/DefaultLayout.swift index 236a171ff..90b7908ca 100644 --- a/Sources/Runway/Stores/DefaultLayout.swift +++ b/Sources/Runway/Stores/DefaultLayout.swift @@ -71,6 +71,7 @@ enum DefaultLayout { "codex.weekly", "cursor.auto", "cursor.api", "copilot.premium", + "muse.session", "muse.weekly", "openrouter.credits", "zai.session", "zai.weekly" ] @@ -121,8 +122,7 @@ enum DefaultLayout { // Kimi: Five-Hour Usage stays above the fold; Weekly Usage and the two Extra Usage // account/billing rows sit below the caret. "kimi.weekly", "kimi.extraBalance", "kimi.extraMonthly", - // Muse: Five-Hour Usage stays above the fold; Weekly Usage sits below the caret. - "muse.weekly", + // Muse: Five-Hour Usage and Weekly Usage both stay above the fold; there is no On Demand row. // OpenCode: the three Go caps (Session/Weekly/Monthly) and Usage Trend stay above the fold — // matching every other provider — with the spend tiles (Today/Yesterday/Last 30 Days) below. "opencode.today", "opencode.yesterday", "opencode.last30", diff --git a/Tests/RunwayTests/MuseProviderTests.swift b/Tests/RunwayTests/MuseProviderTests.swift index 8833aaca9..8488b4c07 100644 --- a/Tests/RunwayTests/MuseProviderTests.swift +++ b/Tests/RunwayTests/MuseProviderTests.swift @@ -366,11 +366,11 @@ final class MuseLayoutTests: XCTestCase { ) XCTAssertEqual(store.placed.map(\.descriptorID), ["muse.session", "muse.weekly"]) - XCTAssertTrue(store.pinnedMetricIDs.isEmpty) + XCTAssertEqual(Set(store.pinnedMetricIDs), ["muse.session", "muse.weekly"]) let group = store.customizeGroups.first { $0.provider.id == "muse" } - XCTAssertEqual(group?.alwaysShownMetrics.map(\.id), ["muse.session"]) - XCTAssertEqual(group?.expandedMetrics.map(\.id), ["muse.weekly"]) + XCTAssertEqual(group?.alwaysShownMetrics.map(\.id), ["muse.session", "muse.weekly"]) + XCTAssertEqual(group?.expandedMetrics.map(\.id) ?? [], []) } } diff --git a/Tests/RunwayTests/ProviderMarksTests.swift b/Tests/RunwayTests/ProviderMarksTests.swift index 5735630da..1d6259e7e 100644 --- a/Tests/RunwayTests/ProviderMarksTests.swift +++ b/Tests/RunwayTests/ProviderMarksTests.swift @@ -3,6 +3,17 @@ import XCTest @MainActor final class ProviderMarksTests: XCTestCase { + func testMuseUsesMetaInfinityMark() throws { + let mark = try XCTUnwrap(ProviderMarks.mark(for: "muse")) + XCTAssertTrue( + mark.pathData.hasPrefix("M6.915"), + "Muse must use the Meta infinity-M path, not the old sparkle star" + ) + XCTAssertGreaterThan(mark.bounds.width, 20) + XCTAssertGreaterThan(mark.bounds.height, 14) + XCTAssertFalse(mark.path.isEmpty) + } + func testGrokResolvesToVectorMarkNotBoltFallback() { let mark = ProviderMarks.mark(for: "grok") XCTAssertNotNil(mark, "Grok must load a real vector mark instead of the bolt.fill fallback") diff --git a/Tests/RunwayTests/SettingsMigratorTests.swift b/Tests/RunwayTests/SettingsMigratorTests.swift index bbe16f65f..03b1d5b11 100644 --- a/Tests/RunwayTests/SettingsMigratorTests.swift +++ b/Tests/RunwayTests/SettingsMigratorTests.swift @@ -313,6 +313,39 @@ final class SettingsMigratorTests: XCTestCase { XCTAssertEqual(migrated["codex"], saved["codex"], "unrelated providers stay untouched") } + func testV6PromotesMuseWeeklyAndStarsBothMeters() { + let (defaults, domain) = makeDefaults("V6MuseLayout") + defer { defaults.removePersistentDomain(forName: domain) } + defaults.set(5, forKey: SettingsMigrator.schemaVersionKey) + defaults.set( + ["muse.weekly", "claude.trend", "cursor.onDemand"], + forKey: "runway.layout.v1.expandedMetrics" + ) + defaults.set( + ["muse.weekly", "cursor.requests"], + forKey: "runway.layout.v1.expandOnEnable" + ) + defaults.set( + ["claude.session", "cursor.auto"], + forKey: "runway.layout.v1.menuBarPins" + ) + + XCTAssertEqual(SettingsMigrator.migrate(defaults: defaults, domainName: domain), SettingsSchema.current) + + XCTAssertEqual( + defaults.stringArray(forKey: "runway.layout.v1.expandedMetrics"), + ["claude.trend", "cursor.onDemand"] + ) + XCTAssertEqual( + defaults.stringArray(forKey: "runway.layout.v1.expandOnEnable"), + ["cursor.requests"] + ) + XCTAssertEqual( + defaults.stringArray(forKey: "runway.layout.v1.menuBarPins"), + ["claude.session", "cursor.auto", "muse.session", "muse.weekly"] + ) + } + /// A legacy install with no disabled providers (the all-on default) converts to all-on. func testV2ConvertsAllOnLegacyInstall() { let (defaults, domain) = makeDefaults("V2AllOn") diff --git a/docs/dashboard.md b/docs/dashboard.md index dc6a9e077..99eef788f 100644 --- a/docs/dashboard.md +++ b/docs/dashboard.md @@ -83,7 +83,7 @@ A provider's **detail** has a back button and provider-specific Reset control in Drag-reorder also works directly on the dashboard — drag a row within its provider, drag it across the caret boundary while the card is open, or drag a provider header to reorder sections. On a Force Touch trackpad you'll feel a light tap each time the dragged item snaps into a new slot. -For Claude, the default reset layout keeps Session, Weekly, and Fable always visible. Codex and Grok keep only Weekly always visible, while Sakana Fugu keeps Five-Hour Usage and Weekly Usage always visible. For those four providers, Usage Trend and the Today, Yesterday, and Last 30 Days history rows start on demand; Codex and Grok also put Rate Limit Resets there, above the usage history. Their other metrics start off. Other providers keep their own core meters above the caret and secondary details on demand. +For Claude, the default reset layout keeps Session, Weekly, and Fable always visible. Codex and Grok keep only Weekly always visible, while Sakana Fugu and Muse keep Five-Hour Usage and Weekly Usage always visible. For Claude, Codex, Grok, and Sakana Fugu, Usage Trend and the Today, Yesterday, and Last 30 Days history rows start on demand; Codex and Grok also put Rate Limit Resets there, above the usage history. Their other metrics start off. Other providers keep their own core meters above the caret and secondary details on demand. Made a change you didn't mean to? Press **⌘Z** to undo — it works anywhere in the popover (the dashboard and Customize alike) and steps back through your recent customization changes one at a time: hiding or showing a metric, reordering metrics or whole providers, starring or unstarring, and moving a metric across the divider all undo. Each step restores the exact previous arrangement. Undo is per-session (it starts fresh after a relaunch), and resetting clears it. diff --git a/docs/providers/muse.md b/docs/providers/muse.md index d87d2f303..5a86ae2ad 100644 --- a/docs/providers/muse.md +++ b/docs/providers/muse.md @@ -9,7 +9,7 @@ Tracks Muse Code subscription quota using the Meta account login from the offici | Five-Hour Usage | Usage percentage in the rolling five-hour prompt window | | Weekly Usage | Usage percentage in the weekly quota window | -When Muse reports a subscription tier (Everyday Usage, High Usage, or Power Usage), Runway shows it beside the provider name. A pay-as-you-go `META_API_KEY` is not a subscription login and is not used. +When Muse reports a subscription tier (Everyday Usage, High Usage, or Power Usage), Runway shows it beside the provider name. A pay-as-you-go `META_API_KEY` is not a subscription login and is not used. Both meters start always visible, with both starred in the menu bar. ## Where credentials come from