diff --git a/mac/Sources/CodeBurnMenubar/AppStore.swift b/mac/Sources/CodeBurnMenubar/AppStore.swift index f38f1de..52dbcfa 100644 --- a/mac/Sources/CodeBurnMenubar/AppStore.swift +++ b/mac/Sources/CodeBurnMenubar/AppStore.swift @@ -52,6 +52,9 @@ final class AppStore { } var showingAccentPicker: Bool = false var currency: String = "USD" + /// Which Settings tab to show; lets the menu's "About CodeBurn" item jump + /// straight to the About tab even when the Settings window is reused. + var settingsTab: String = "general" var displayMetric: DisplayMetric = DisplayMetric(rawValue: UserDefaults.standard.string(forKey: "CodeBurnDisplayMetric") ?? "") ?? .cost { didSet { UserDefaults.standard.set(displayMetric.rawValue, forKey: "CodeBurnDisplayMetric") } } diff --git a/mac/Sources/CodeBurnMenubar/CodeBurnApp.swift b/mac/Sources/CodeBurnMenubar/CodeBurnApp.swift index 85ad75a..5655f25 100644 --- a/mac/Sources/CodeBurnMenubar/CodeBurnApp.swift +++ b/mac/Sources/CodeBurnMenubar/CodeBurnApp.swift @@ -857,31 +857,56 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate { let menu = NSMenu() - let settingsItem = NSMenuItem(title: "Settings…", action: #selector(openSettings), keyEquivalent: ",") + // Glanceable "today" usage at the top as a single non-interactive (dimmed) + // row. A real .sectionHeader adds section padding (and pulls the actions + // into its group without a separator), so use a plain disabled item. + let usageItem = NSMenuItem(title: contextMenuUsageSummary(), action: nil, keyEquivalent: "") + usageItem.isEnabled = false + menu.addItem(usageItem) + + let settingsItem = NSMenuItem(title: "Settings…", action: #selector(openSettings), keyEquivalent: "") settingsItem.target = self + settingsItem.image = NSImage(systemSymbolName: "gearshape", accessibilityDescription: "Settings") menu.addItem(settingsItem) - let refreshNow = NSMenuItem(title: "Refresh Now", action: #selector(refreshNowAction), keyEquivalent: "r") + let refreshNow = NSMenuItem(title: "Refresh Now", action: #selector(refreshNowAction), keyEquivalent: "") refreshNow.target = self menu.addItem(refreshNow) - menu.addItem(.separator()) let updateItem = NSMenuItem(title: "Check for Updates", action: #selector(checkForUpdates), keyEquivalent: "") updateItem.target = self menu.addItem(updateItem) - menu.addItem(.separator()) - let quitItem = NSMenuItem(title: "Quit CodeBurn", action: #selector(quitApp), keyEquivalent: "q") + + let aboutItem = NSMenuItem(title: "About CodeBurn", action: #selector(openAbout), keyEquivalent: "") + aboutItem.target = self + menu.addItem(aboutItem) + + let quitItem = NSMenuItem(title: "Quit CodeBurn", action: #selector(quitApp), keyEquivalent: "") quitItem.target = self menu.addItem(quitItem) // Present directly. The previous `statusItem.menu = menu; button.performClick` // trick relies on the click -> action path that macOS 27 changed; popUp is - // version-stable. - menu.popUp(positioning: nil, at: NSPoint(x: 0, y: button.bounds.height + 4), in: button) + // version-stable. Open a few px below the status item so the menu clears the + // menu bar: anchoring flush clips the top edge and makes macOS engage menu + // scrolling (a scroll chevron appears and the first row slides up on hover). + menu.popUp(positioning: nil, at: NSPoint(x: 0, y: button.bounds.height + 6), in: button) + } + + /// One-line "today" summary for the context menu's usage row. + private func contextMenuUsageSummary() -> String { + guard let current = store.todayPayload?.current else { return "Today · no usage yet" } + let calls = current.calls == 1 ? "1 call" : "\(current.calls) calls" + return "Today · \(current.cost.asCurrency()) · \(calls)" } private var settingsWindowController: NSWindowController? + @objc private func openAbout() { + store.settingsTab = "about" + openSettings() + } + @objc private func openSettings() { // Accessory-policy apps (no Dock icon, no main menu) don't get the // SwiftUI Settings scene wired into the responder chain reliably, so diff --git a/mac/Sources/CodeBurnMenubar/Views/SettingsView.swift b/mac/Sources/CodeBurnMenubar/Views/SettingsView.swift index 3197d66..0f5d030 100644 --- a/mac/Sources/CodeBurnMenubar/Views/SettingsView.swift +++ b/mac/Sources/CodeBurnMenubar/Views/SettingsView.swift @@ -8,21 +8,26 @@ struct SettingsView: View { @Environment(AppStore.self) private var store var body: some View { - TabView { + TabView(selection: Binding(get: { store.settingsTab }, set: { store.settingsTab = $0 })) { GeneralSettingsTab() .tabItem { Label("General", systemImage: "gearshape") } + .tag("general") ClaudeSettingsTab() .tabItem { Label("Claude", systemImage: "brain") } + .tag("claude") CodexSettingsTab() .tabItem { Label("Codex", systemImage: "chevron.left.forwardslash.chevron.right") } + .tag("codex") DevinSettingsTab() .tabItem { Label("Devin", systemImage: "flame.fill") } + .tag("devin") AboutSettingsTab() .tabItem { Label("About", systemImage: "info.circle") } + .tag("about") } .frame(width: 520, height: 430) } @@ -660,9 +665,16 @@ private struct AboutSettingsTab: View { Text("Version \(appVersion) (\(buildVersion))") .font(.codeMono(size: 11)) .foregroundStyle(.secondary) + Link(destination: URL(string: "https://github.com/getagentseal/codeburn")!) { + Label("Star on GitHub", systemImage: "star.fill") + .font(.system(size: 12, weight: .medium)) + } + .buttonStyle(.borderedProminent) + .tint(Theme.brandAccent) HStack(spacing: 10) { Link("GitHub", destination: URL(string: "https://github.com/getagentseal/codeburn")!) Link("Issues", destination: URL(string: "https://github.com/getagentseal/codeburn/issues")!) + Link("Sponsor", destination: URL(string: "https://github.com/sponsors/iamtoruk")!) } .font(.system(size: 12)) }