Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
48 changes: 48 additions & 0 deletions AIMeter.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,35 @@ All notable changes to AIMeter will be documented in this file.

This project follows semantic versioning while it is practical for a small macOS utility.

## [0.4.0] - 2026-05-07

### Added

- Claude support with an isolated local web session, URL validation, and usage parsing from Claude's usage settings page.
- Provider-aware dashboard state for tracking Cursor and Claude side by side.
- Claude usage cards for current session, All models, Claude Design, and reset timing when Claude exposes it.
- Start-at-login setting using macOS login items.
- Local development and notarized DMG verification helper scripts.

### Changed

- Menu bar progress now uses the highest connected provider usage percentage.
- Popover only shows connected providers and keeps a loading state visible while saved provider sessions are still being checked.
- Claude usage UI now renders a stable set of cards instead of changing shape while the page loads.

### Fixed

- Deduplicated repeated Claude usage rows so `All models` and `Claude Design` appear only once.
- Filtered Claude template and explanatory copy so it does not become bogus usage text.
- Prevented the first-run provider connection screen from flashing during startup loading.

## [0.3.0] - 2026-05-06

### Changed

- Replaced menu bar percentage text with a compact progress bar-only status item.
- Kept warning/disconnected states visible in the menu bar with a small status indicator.

## [0.1.0] - 2026-05-05

### Added
Expand Down
4 changes: 3 additions & 1 deletion Sources/AIMeter/App/AIMeterApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ struct AIMeterApp: App {
SettingsView(
settingsStore: environment.settingsStore,
dashboardStore: environment.dashboardStore,
cursorUsageCoordinator: environment.cursorUsageCoordinator
cursorUsageCoordinator: environment.cursorUsageCoordinator,
claudeUsageCoordinator: environment.claudeUsageCoordinator,
launchAtLoginController: environment.launchAtLoginController
)
}
}
Expand Down
2 changes: 2 additions & 0 deletions Sources/AIMeter/App/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
let environment = AppEnvironment.shared
environment.menuBarController.install()
environment.cursorUsageCoordinator.start()
environment.claudeUsageCoordinator.start()
}

func applicationWillTerminate(_ notification: Notification) {
Task { @MainActor in
AppEnvironment.shared.cursorUsageCoordinator.stop()
AppEnvironment.shared.claudeUsageCoordinator.stop()
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
26 changes: 24 additions & 2 deletions Sources/AIMeter/App/AppEnvironment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,45 +6,67 @@ final class AppEnvironment {

let settingsStore: SettingsStore
let cursorSessionManager: CursorSessionManager
let claudeSessionManager: ClaudeSessionManager
let cursorUsageClient: CursorUsageClient
let claudeUsageClient: ProviderUsageClient
let cursorUsageCoordinator: CursorUsageCoordinator
let claudeUsageCoordinator: ClaudeUsageCoordinator
let dashboardStore: DashboardStore
let launchAtLoginController: LaunchAtLoginController

lazy var settingsWindowController: SettingsWindowController = {
SettingsWindowController(
settingsStore: settingsStore,
dashboardStore: dashboardStore,
cursorUsageCoordinator: cursorUsageCoordinator
cursorUsageCoordinator: cursorUsageCoordinator,
claudeUsageCoordinator: claudeUsageCoordinator,
launchAtLoginController: launchAtLoginController
)
}()
lazy var menuBarController: MenuBarController = {
MenuBarController(
dashboardStore: dashboardStore,
cursorUsageCoordinator: cursorUsageCoordinator,
claudeUsageCoordinator: claudeUsageCoordinator,
settingsWindowController: settingsWindowController
)
}()

private init() {
let settingsStore = SettingsStore()
let cursorSessionManager = CursorSessionManager()
let claudeSessionManager = ClaudeSessionManager()
let cursorUsageClient = CursorDashboardClient(
settingsStore: settingsStore,
sessionManager: cursorSessionManager
)
let claudeUsageClient = ClaudeDashboardClient(
settingsStore: settingsStore,
sessionManager: claudeSessionManager
)
let cursorUsageCoordinator = CursorUsageCoordinator(
settingsStore: settingsStore,
client: cursorUsageClient
)
let claudeUsageCoordinator = ClaudeUsageCoordinator(
settingsStore: settingsStore,
client: claudeUsageClient
)
let dashboardStore = DashboardStore(
settingsStore: settingsStore,
cursorUsageCoordinator: cursorUsageCoordinator
cursorUsageCoordinator: cursorUsageCoordinator,
claudeUsageCoordinator: claudeUsageCoordinator
)
let launchAtLoginController = LaunchAtLoginController()

self.settingsStore = settingsStore
self.cursorSessionManager = cursorSessionManager
self.claudeSessionManager = claudeSessionManager
self.cursorUsageClient = cursorUsageClient
self.claudeUsageClient = claudeUsageClient
self.cursorUsageCoordinator = cursorUsageCoordinator
self.claudeUsageCoordinator = claudeUsageCoordinator
self.dashboardStore = dashboardStore
self.launchAtLoginController = launchAtLoginController
}
}
41 changes: 41 additions & 0 deletions Sources/AIMeter/Claude/ClaudeDashboardClient.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import Foundation

@MainActor
final class ClaudeDashboardClient: ProviderUsageClient {
let provider = UsageProvider.claude

private let settingsStore: SettingsStore
private let sessionManager: ClaudeSessionManaging
private var pendingConnectedSnapshot: ProviderUsageSnapshot?

init(settingsStore: SettingsStore, sessionManager: ClaudeSessionManaging) {
self.settingsStore = settingsStore
self.sessionManager = sessionManager
}

func connect() async throws {
let usagePageURL = try resolvedUsagePageURL()
pendingConnectedSnapshot = try await sessionManager.connect(to: usagePageURL)
}

func fetchUsage() async throws -> ProviderUsageSnapshot {
if let pendingConnectedSnapshot {
self.pendingConnectedSnapshot = nil
if pendingConnectedSnapshot.progressPercent != nil {
return pendingConnectedSnapshot
}
}

let usagePageURL = try resolvedUsagePageURL()
return try await sessionManager.fetchUsage(from: usagePageURL)
}

func disconnect() {
pendingConnectedSnapshot = nil
sessionManager.disconnect()
}

private func resolvedUsagePageURL() throws -> URL {
try ClaudeURLValidator.validatedUsageURL(from: settingsStore.settings.claude.usagePageURL)
}
}
Loading
Loading