mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-04-28 06:59:37 +00:00
The tab strip was visible for everyone regardless of which tools they actually run, which produced a row of All + one provider for Claude-only users and a row of All + zeros for users on exotic stacks. Hide the whole row until a second provider has real spend, matching the behavior the GNOME extension ships with. Also expand ProviderFilter to include every provider the CLI supports (OpenCode and Pi were missing) so their tabs appear when those tools produce sessions. The CLI already emits pi and opencode in the payload's providers map; the Mac app just wasn't offering a tab for them. visibleFilters now filters on value > 0 instead of key presence, because the CLI includes zero-cost entries for discovered-but-unused providers and we don't want those rendering as blank tabs.
103 lines
3.7 KiB
Swift
103 lines
3.7 KiB
Swift
import SwiftUI
|
|
|
|
struct AgentTabStrip: View {
|
|
@Environment(AppStore.self) private var store
|
|
|
|
var body: some View {
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
HStack(spacing: 5) {
|
|
ForEach(visibleFilters) { filter in
|
|
Button {
|
|
Task { await store.switchTo(provider: filter) }
|
|
} label: {
|
|
AgentTab(
|
|
filter: filter,
|
|
cost: cost(for: filter),
|
|
isActive: store.selectedProvider == filter
|
|
)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
.padding(.horizontal, 12)
|
|
.padding(.top, 8)
|
|
.padding(.bottom, 4)
|
|
}
|
|
}
|
|
|
|
/// Drive tab visibility and per-tab cost labels from the *all-provider* payload (today),
|
|
/// not the currently selected provider's payload. Without this, switching to Codex (which
|
|
/// has no data) would hide every other tab including Claude.
|
|
private var allProvidersToday: MenubarPayload {
|
|
store.todayPayload ?? store.payload
|
|
}
|
|
|
|
private var visibleFilters: [ProviderFilter] {
|
|
// Only surface providers that actually spent money in the all-provider view.
|
|
// The CLI includes zero-cost providers in the payload for completeness, so
|
|
// filtering on value > 0 keeps the tab row honest and avoids showing tabs
|
|
// for tools the user hasn't run yet.
|
|
let activeKeys = Set(
|
|
allProvidersToday.current.providers
|
|
.filter { $0.value > 0 }
|
|
.keys
|
|
.map { $0.lowercased() }
|
|
)
|
|
return ProviderFilter.allCases.filter { filter in
|
|
if filter == .all { return true }
|
|
return activeKeys.contains(filter.rawValue.lowercased())
|
|
}
|
|
}
|
|
|
|
private func cost(for filter: ProviderFilter) -> Double? {
|
|
switch filter {
|
|
case .all:
|
|
return allProvidersToday.current.cost
|
|
default:
|
|
let key = filter.rawValue.lowercased()
|
|
return allProvidersToday.current.providers[key]
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct AgentTab: View {
|
|
let filter: ProviderFilter
|
|
let cost: Double?
|
|
let isActive: Bool
|
|
|
|
var body: some View {
|
|
HStack(spacing: 5) {
|
|
Text(filter.rawValue)
|
|
.font(.system(size: 11.5, weight: .medium))
|
|
.tracking(-0.05)
|
|
if let cost, cost > 0 {
|
|
Text(cost.asCompactCurrency())
|
|
.font(.codeMono(size: 10.5, weight: .medium))
|
|
.foregroundStyle(isActive ? AnyShapeStyle(.white.opacity(0.8)) : AnyShapeStyle(.secondary))
|
|
.tracking(-0.2)
|
|
}
|
|
}
|
|
.padding(.horizontal, 10)
|
|
.padding(.vertical, 4)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 6)
|
|
.fill(isActive ? AnyShapeStyle(Theme.brandAccent) : AnyShapeStyle(Color.secondary.opacity(0.08)))
|
|
)
|
|
.foregroundStyle(isActive ? AnyShapeStyle(.white) : AnyShapeStyle(.secondary))
|
|
.contentShape(Rectangle())
|
|
}
|
|
}
|
|
|
|
extension ProviderFilter {
|
|
var color: Color {
|
|
switch self {
|
|
case .all: return Theme.brandAccent
|
|
case .claude: return Theme.categoricalClaude
|
|
case .codex: return Theme.categoricalCodex
|
|
case .cursor: return Theme.categoricalCursor
|
|
case .copilot: return Color(red: 0x6D/255.0, green: 0x8F/255.0, blue: 0xA6/255.0)
|
|
case .opencode: return Color(red: 0x5B/255.0, green: 0x83/255.0, blue: 0x5B/255.0)
|
|
case .pi: return Color(red: 0xB2/255.0, green: 0x6B/255.0, blue: 0x3D/255.0)
|
|
}
|
|
}
|
|
}
|