import AppKit import SwiftUI /// macOS-standard tabbed Settings window. New per-provider sections (Codex, /// Cursor, Copilot, etc.) plug in as additional tabs. Each tab owns its own /// concerns; this top-level view only hosts the TabView shell. struct SettingsView: View { @Environment(AppStore.self) private var store var body: some View { 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") KimiSettingsTab() .tabItem { Label("Kimi", systemImage: "moon.stars") } .tag("kimi") DevinSettingsTab() .tabItem { Label("Devin", systemImage: "flame.fill") } .tag("devin") AboutSettingsTab() .tabItem { Label("About", systemImage: "info.circle") } .tag("about") } // 6 tabs need ~600pt to render as a visible tab bar; narrower widths // make SwiftUI collapse the tab bar into a ">>" overflow menu. .frame(width: 600, height: 430) } } // MARK: - General private struct GeneralSettingsTab: View { @Environment(AppStore.self) private var store // "Custom…" budget entry state, one per metric (cost in dollars, tokens in // millions). When custom is active the picker shows "Custom…" and a field // appears for an exact amount. @State private var costCustom = false @State private var tokenCustom = false @State private var costText = "" @State private var tokenText = "" // AppStorage (not a computed Binding over UsageRefreshCadence.current): // a plain UserDefaults write does not invalidate the view, so the picker // label would never reflect the selection even though the value landed. @AppStorage(UsageRefreshCadence.defaultsKey) private var usageRefreshSeconds: Int = UsageRefreshCadence.default.rawValue private let costPresets: Set = [25, 50, 100, 200, 500] private let tokenPresets: Set = [1_000_000, 5_000_000, 10_000_000, 25_000_000, 50_000_000, 100_000_000] private func applyCostBudget() { store.dailyBudget = max(0, Double(costText.trimmingCharacters(in: .whitespaces)) ?? 0) } private func applyTokenBudget() { let millions = Double(tokenText.trimmingCharacters(in: .whitespaces)) ?? 0 store.dailyTokenBudget = max(0, millions * 1_000_000) } private func trimNumber(_ v: Double) -> String { v == v.rounded() ? String(Int(v)) : String(v) } // Help text under the budget picker. When "Custom…" is selected but no amount // has been entered, the budget is effectively 0 (off); call that out so the // alert does not look armed when it isn't. private var alertHelpText: String { let customEmpty = store.isTokenMetric ? (tokenCustom && store.dailyTokenBudget == 0) : (costCustom && store.dailyBudget == 0) if customEmpty { return "Enter an amount above, or the alert stays off." } return "Flame icon turns yellow when today's \(store.isTokenMetric ? "tokens" : "cost") pass the daily budget." } var body: some View { Form { Section("Display") { Picker("Currency", selection: Binding( get: { store.currency }, set: { applyCurrency(code: $0) } )) { ForEach(SupportedCurrency.allCases) { currency in Text("\(currency.rawValue) — \(currency.displayName)").tag(currency.rawValue) } } Picker("Metric", selection: Binding( get: { store.displayMetric }, set: { store.displayMetric = $0 } )) { Text("Cost ($)").tag(DisplayMetric.cost) Text("Tokens (↑↓)").tag(DisplayMetric.tokens) Text("Total Tokens").tag(DisplayMetric.totalTokens) Text("Credits (Codex)").tag(DisplayMetric.credits) Text("Icon Only").tag(DisplayMetric.iconOnly) } Picker("Period", selection: Binding( get: { store.menubarPeriod }, set: { store.setMenubarPeriod($0) } )) { ForEach(Period.menubarMetricCases) { period in Text(period.menubarMetricLabel).tag(period) } } .pickerStyle(.menu) Picker("Scope", selection: Binding( get: { store.menubarScope }, set: { store.setMenubarScope($0) } )) { ForEach(MenubarScope.allCases) { scope in Text(scope.rawValue).tag(scope) } } .pickerStyle(.menu) Picker("Accent", selection: Binding( get: { store.accentPreset }, set: { store.accentPreset = $0 } )) { ForEach(AccentPreset.allCases) { preset in Text(preset.rawValue).tag(preset) } } } Section("Usage Refresh") { Picker("Update every", selection: Binding( get: { UsageRefreshCadence(rawValue: usageRefreshSeconds) ?? .default }, set: { usageRefreshSeconds = $0.rawValue } )) { ForEach(UsageRefreshCadence.allCases) { cadence in Text(cadence.label).tag(cadence) } } .pickerStyle(.menu) Text("How often the menubar figure re-reads your local session data. Auto refreshes every 30 seconds while you're plugged in and backs off on battery; Manual only refreshes when you open the popover or click Refresh Now.") .font(.system(size: 11)) .foregroundStyle(.secondary) } Section("Alerts") { // The budget tracks whatever the menubar metric shows: dollars for // the Cost metric, tokens for the Tokens / Total Tokens metrics. // "Custom…" reveals a field for an exact amount. if store.isTokenMetric { Picker("Daily budget", selection: Binding( get: { tokenCustom ? -1.0 : store.dailyTokenBudget }, set: { sel in if sel < 0 { tokenCustom = true tokenText = store.dailyTokenBudget > 0 ? trimNumber(store.dailyTokenBudget / 1_000_000) : "" } else { tokenCustom = false store.dailyTokenBudget = sel } } )) { Text("Off").tag(0.0) Text("1M").tag(1_000_000.0) Text("5M").tag(5_000_000.0) Text("10M").tag(10_000_000.0) Text("25M").tag(25_000_000.0) Text("50M").tag(50_000_000.0) Text("100M").tag(100_000_000.0) Text("Custom…").tag(-1.0) } if tokenCustom { HStack { TextField("Amount", text: $tokenText) .multilineTextAlignment(.trailing) .onSubmit { applyTokenBudget() } .onChange(of: tokenText) { _, _ in applyTokenBudget() } Text("M tokens").foregroundStyle(.secondary) } } } else { Picker("Daily budget", selection: Binding( get: { costCustom ? -1.0 : store.dailyBudget }, set: { sel in if sel < 0 { costCustom = true costText = store.dailyBudget > 0 ? trimNumber(store.dailyBudget) : "" } else { costCustom = false store.dailyBudget = sel } } )) { Text("Off").tag(0.0) Text("$25").tag(25.0) Text("$50").tag(50.0) Text("$100").tag(100.0) Text("$200").tag(200.0) Text("$500").tag(500.0) Text("Custom…").tag(-1.0) } if costCustom { HStack { Text("$").foregroundStyle(.secondary) TextField("Amount", text: $costText) .multilineTextAlignment(.trailing) .onSubmit { applyCostBudget() } .onChange(of: costText) { _, _ in applyCostBudget() } } } } Text(alertHelpText) .font(.system(size: 11)) .foregroundStyle(.secondary) } .onAppear { costCustom = store.dailyBudget > 0 && !costPresets.contains(store.dailyBudget) if costCustom { costText = trimNumber(store.dailyBudget) } tokenCustom = store.dailyTokenBudget > 0 && !tokenPresets.contains(store.dailyTokenBudget) if tokenCustom { tokenText = trimNumber(store.dailyTokenBudget / 1_000_000) } } } .formStyle(.grouped) .padding() } private func applyCurrency(code: String) { let symbol = CurrencyState.symbolForCode(code) Task { let cached = await FXRateCache.shared.cachedRate(for: code) if let cached { store.currency = code CurrencyState.shared.apply(code: code, rate: cached, symbol: symbol) } let fresh = await FXRateCache.shared.rate(for: code) store.currency = code CurrencyState.shared.apply(code: code, rate: fresh ?? cached, symbol: symbol) } CLICurrencyConfig.persist(code: code) } } // MARK: - Claude private struct ClaudeSettingsTab: View { @Environment(AppStore.self) private var store var body: some View { Form { Section("Connection") { ClaudeConnectionRow() } Section { ClaudeConfigDirsSection() } header: { Text("Config Directories") } footer: { Text("Aggregate usage across multiple Claude config directories (e.g. work and personal accounts). Leave empty to track just the default `~/.claude`. The `CLAUDE_CONFIG_DIRS` environment variable, if set, overrides this list.") .font(.system(size: 11)) .foregroundStyle(.secondary) } Section("Quota Refresh") { Picker("Update every", selection: Binding( get: { SubscriptionRefreshCadence.current }, set: { SubscriptionRefreshCadence.current = $0 } )) { ForEach(SubscriptionRefreshCadence.allCases) { cadence in Text(cadence.label).tag(cadence) } } .pickerStyle(.menu) Text("Anthropic rate-limits this endpoint per account. 2 minutes is plenty for the 5-hour and weekly windows; pick Manual if you only want updates on demand.") .font(.system(size: 11)) .foregroundStyle(.secondary) Button("Refresh Now") { if let delegate = NSApp.delegate as? AppDelegate { delegate.refreshSubscriptionNow() } else { Task { await store.refreshSubscription() } } } } } .formStyle(.grouped) .padding() } } private struct ClaudeConnectionRow: View { @Environment(AppStore.self) private var store @State private var showDisconnectConfirm = false var body: some View { HStack(alignment: .top, spacing: 12) { Image(systemName: stateIcon) .font(.system(size: 18)) .foregroundStyle(stateTint) .frame(width: 22) VStack(alignment: .leading, spacing: 2) { Text(stateTitle) .font(.system(size: 12, weight: .semibold)) Text(stateDetail) .font(.system(size: 11)) .foregroundStyle(.secondary) .lineLimit(2) } Spacer() actionButton } .padding(.vertical, 4) } private var stateIcon: String { switch store.subscriptionLoadState { case .loaded: return "checkmark.circle.fill" case .terminalFailure: return "exclamationmark.triangle.fill" case .transientFailure: return "clock.arrow.circlepath" case .bootstrapping, .loading: return "ellipsis.circle" case .notBootstrapped, .dormant, .noCredentials: return "link.circle" case .failed: return "xmark.circle" } } private var stateTint: Color { switch store.subscriptionLoadState { case .loaded: return .green case .terminalFailure, .failed: return .red case .transientFailure: return .orange default: return .secondary } } private var stateTitle: String { switch store.subscriptionLoadState { case .loaded: return "Connected" case let .terminalFailure(reason): return reason ?? "Reconnect required" case .transientFailure: return "Backing off" case .bootstrapping: return "Connecting…" case .loading: return "Refreshing…" case .dormant: return "Ready" case .notBootstrapped, .noCredentials: return "Not connected" case .failed: return "Couldn't load plan data" } } private var stateDetail: String { switch store.subscriptionLoadState { case .loaded: if let tier = store.subscription?.tier.displayName { return "Plan: \(tier)" } return "Live quota tracked from Anthropic." case .terminalFailure: return "Open Claude Code in your terminal and type `/login`, then click Reconnect." case .transientFailure: return store.subscriptionError ?? "Anthropic rate-limited; auto-retrying." case .bootstrapping: return "macOS may ask permission to read your credentials." case .loading: return "Background refresh in progress." case .dormant: return "Tap Load Quota to fetch live usage from Anthropic." case .notBootstrapped, .noCredentials: return "Click Connect to read your Claude Code credentials and start tracking quota." case .failed: return store.subscriptionError ?? "" } } @ViewBuilder private var actionButton: some View { switch store.subscriptionLoadState { case .loaded, .transientFailure, .loading: Button("Disconnect") { showDisconnectConfirm = true } .confirmationDialog( "Disconnect Claude?", isPresented: $showDisconnectConfirm ) { Button("Disconnect", role: .destructive) { store.disconnectSubscription() } Button("Cancel", role: .cancel) {} } message: { Text("CodeBurn will stop tracking quota and delete its local copy of your Claude credentials. Your Claude Code keychain entry is untouched — Claude Code keeps working.") } case .terminalFailure, .noCredentials, .failed: Button("Reconnect") { Task { await store.bootstrapSubscription() } } .buttonStyle(.borderedProminent) case .dormant: Button("Load Quota") { Task { await store.activateClaudeFromDormant() } } .buttonStyle(.borderedProminent) case .notBootstrapped: Button("Connect") { Task { await store.bootstrapSubscription() } } .buttonStyle(.borderedProminent) case .bootstrapping: ProgressView().controlSize(.small) } } } // MARK: - Claude config directories private struct ClaudeConfigDirsSection: View { @Environment(AppStore.self) private var store @State private var dirs: [String] = CLIClaudeConfig.load() var body: some View { VStack(alignment: .leading, spacing: 8) { if dirs.isEmpty { Text("No extra directories — tracking the default `~/.claude`.") .font(.system(size: 11)) .foregroundStyle(.secondary) } else { ForEach(Array(dirs.enumerated()), id: \.offset) { index, dir in HStack(spacing: 8) { Image(systemName: "folder") .font(.system(size: 12)) .foregroundStyle(.secondary) Text(dir) .font(.system(size: 12)) .truncationMode(.middle) .lineLimit(1) .help(dir) Spacer() Button { remove(at: index) } label: { Image(systemName: "minus.circle.fill") .foregroundStyle(.secondary) } .buttonStyle(.plain) .help("Remove") } } } Button { addDirectory() } label: { Label("Add Directory…", systemImage: "plus") } .controlSize(.small) } .padding(.vertical, 2) } private func addDirectory() { let panel = NSOpenPanel() panel.canChooseDirectories = true panel.canChooseFiles = false panel.allowsMultipleSelection = true panel.prompt = "Add" panel.message = "Choose one or more Claude config directories (each containing a `projects` folder)." guard panel.runModal() == .OK else { return } let added = panel.urls.map { $0.path } var next = dirs for path in added where !next.contains(path) { next.append(path) } apply(next) } private func remove(at index: Int) { guard dirs.indices.contains(index) else { return } var next = dirs next.remove(at: index) apply(next) } /// Persists the new list and kicks a forced refresh so the dashboard /// reflects the changed aggregation immediately. private func apply(_ next: [String]) { dirs = next CLIClaudeConfig.persist(dirs: next) Task { await store.refresh(includeOptimize: false, force: true, showLoading: true) } } } // MARK: - Codex private struct CodexSettingsTab: View { @Environment(AppStore.self) private var store var body: some View { Form { Section("Connection") { CodexConnectionRow() } Section { Text("Codex live-quota tracking reads `~/.codex/auth.json` once on Connect, then keeps a local copy under Application Support so subsequent quota fetches don't re-read the original. Only ChatGPT-mode auth (Plus / Pro / Team / Business) is supported — API-key users are billed per request and have a different reporting surface.") .font(.system(size: 11)) .foregroundStyle(.secondary) } header: { Text("How it works") } } .formStyle(.grouped) .padding() } } private struct CodexConnectionRow: View { @Environment(AppStore.self) private var store @State private var showDisconnectConfirm = false var body: some View { HStack(alignment: .top, spacing: 12) { Image(systemName: stateIcon) .font(.system(size: 18)) .foregroundStyle(stateTint) .frame(width: 22) VStack(alignment: .leading, spacing: 2) { Text(stateTitle) .font(.system(size: 12, weight: .semibold)) Text(stateDetail) .font(.system(size: 11)) .foregroundStyle(.secondary) .lineLimit(2) } Spacer() actionButton } .padding(.vertical, 4) } private var stateIcon: String { switch store.codexLoadState { case .loaded: return "checkmark.circle.fill" case .terminalFailure: return "exclamationmark.triangle.fill" case .transientFailure: return "clock.arrow.circlepath" case .bootstrapping, .loading: return "ellipsis.circle" case .notBootstrapped, .dormant, .noCredentials: return "link.circle" case .failed: return "xmark.circle" } } private var stateTint: Color { switch store.codexLoadState { case .loaded: return .green case .terminalFailure, .failed: return .red case .transientFailure: return .orange default: return .secondary } } private var stateTitle: String { switch store.codexLoadState { case .loaded: return "Connected" case let .terminalFailure(reason): return reason ?? "Reconnect required" case .transientFailure: return "Backing off" case .bootstrapping: return "Connecting…" case .loading: return "Refreshing…" case .dormant: return "Ready" case .notBootstrapped, .noCredentials: return "Not connected" case .failed: return "Couldn't load Codex quota" } } private var stateDetail: String { switch store.codexLoadState { case .loaded: if let plan = store.codexUsage?.plan.displayName { return "Plan: \(plan)" } return "Live quota tracked from chatgpt.com." case .terminalFailure: // Be specific about the cause: the message we already surface in // codexError will say "API-key mode" if that's the situation, so // the generic "run codex login" hint covers both cases. if let err = store.codexError, err.lowercased().contains("api-key") { return "Codex is in API-key mode. Run `codex login` and choose a ChatGPT plan to enable quota tracking." } return "Run `codex login` in your terminal to sign in again, then click Reconnect." case .transientFailure: return store.codexError ?? "ChatGPT rate-limited; auto-retrying." case .bootstrapping: return "Reading ~/.codex/auth.json." case .loading: return "Background refresh in progress." case .dormant: return "Tap Load Quota to fetch live usage from chatgpt.com." case .notBootstrapped, .noCredentials: return "Click Connect to read your Codex CLI credentials. If Connect fails, run `codex login` in your terminal first to create ~/.codex/auth.json." case .failed: return store.codexError ?? "" } } @ViewBuilder private var actionButton: some View { switch store.codexLoadState { case .loaded, .transientFailure, .loading: Button("Disconnect") { showDisconnectConfirm = true } .confirmationDialog( "Disconnect Codex?", isPresented: $showDisconnectConfirm ) { Button("Disconnect", role: .destructive) { store.disconnectCodex() } Button("Cancel", role: .cancel) {} } message: { Text("CodeBurn will stop tracking quota and delete its local copy of your Codex credentials. Your ~/.codex/auth.json is untouched — Codex CLI keeps working.") } case .terminalFailure, .noCredentials, .failed: Button("Reconnect") { Task { await store.bootstrapCodex() } } .buttonStyle(.borderedProminent) case .dormant: Button("Load Quota") { Task { await store.activateCodexFromDormant() } } .buttonStyle(.borderedProminent) case .notBootstrapped: Button("Connect") { Task { await store.bootstrapCodex() } } .buttonStyle(.borderedProminent) case .bootstrapping: ProgressView().controlSize(.small) } } } // MARK: - Kimi Code private struct KimiSettingsTab: View { var body: some View { Form { Section("Connection") { KimiConnectionRow() } Section { Text("Kimi Code live-quota tracking reads `~/.kimi-code/credentials/kimi-code.json` directly — nothing is copied or stored. Access tokens are short-lived (~15 minutes) and only the Kimi CLI refreshes them, so if the connection shows as expired, run the Kimi CLI once and click Reconnect.") .font(.system(size: 11)) .foregroundStyle(.secondary) } header: { Text("How it works") } } .formStyle(.grouped) .padding() } } private struct KimiConnectionRow: View { @Environment(AppStore.self) private var store @State private var showDisconnectConfirm = false var body: some View { HStack(alignment: .top, spacing: 12) { Image(systemName: stateIcon) .font(.system(size: 18)) .foregroundStyle(stateTint) .frame(width: 22) VStack(alignment: .leading, spacing: 2) { Text(stateTitle) .font(.system(size: 12, weight: .semibold)) Text(stateDetail) .font(.system(size: 11)) .foregroundStyle(.secondary) .lineLimit(2) } Spacer() actionButton } .padding(.vertical, 4) } private var stateIcon: String { switch store.kimiLoadState { case .loaded: return "checkmark.circle.fill" case .terminalFailure: return "exclamationmark.triangle.fill" case .transientFailure: return "clock.arrow.circlepath" case .bootstrapping, .loading: return "ellipsis.circle" case .notBootstrapped, .dormant, .noCredentials: return "link.circle" case .failed: return "xmark.circle" } } private var stateTint: Color { switch store.kimiLoadState { case .loaded: return .green case .terminalFailure, .failed: return .red case .transientFailure: return .orange default: return .secondary } } private var stateTitle: String { switch store.kimiLoadState { case .loaded: return "Connected" case let .terminalFailure(reason): return reason ?? "Login refresh required" case .transientFailure: return "Backing off" case .bootstrapping: return "Connecting…" case .loading: return "Refreshing…" case .dormant: return "Ready" case .notBootstrapped, .noCredentials: return "Not connected" case .failed: return "Couldn't load Kimi quota" } } private var stateDetail: String { switch store.kimiLoadState { case .loaded: return "Live quota tracked from api.kimi.com." case .terminalFailure: return "Run the Kimi CLI once to refresh your login, then click Reconnect." case .transientFailure: return store.kimiError ?? "Kimi rate-limited; auto-retrying." case .bootstrapping: return "Reading ~/.kimi-code credentials." case .loading: return "Background refresh in progress." case .dormant: return "Tap Load Quota to fetch live usage from api.kimi.com." case .notBootstrapped, .noCredentials: return "Sign in with the Kimi CLI first, then click Connect." case .failed: return store.kimiError ?? "" } } @ViewBuilder private var actionButton: some View { switch store.kimiLoadState { case .loaded, .transientFailure, .loading: Button("Disconnect") { showDisconnectConfirm = true } .confirmationDialog( "Disconnect Kimi Code?", isPresented: $showDisconnectConfirm ) { Button("Disconnect", role: .destructive) { store.disconnectKimi() } Button("Cancel", role: .cancel) {} } message: { Text("CodeBurn will stop tracking Kimi Code quota. Your ~/.kimi-code credentials are untouched — the Kimi CLI keeps working.") } case .terminalFailure, .noCredentials, .failed: Button("Reconnect") { Task { await store.bootstrapKimi() } } .buttonStyle(.borderedProminent) case .dormant: Button("Load Quota") { Task { await store.bootstrapKimi() } } .buttonStyle(.borderedProminent) case .notBootstrapped: Button("Connect") { Task { await store.bootstrapKimi() } } .buttonStyle(.borderedProminent) case .bootstrapping: ProgressView().controlSize(.small) } } } // MARK: - Devin private struct DevinSettingsTab: View { @State private var rateText: String = "" @State private var statusText: String = "" private var parsedRate: Double? { let trimmed = rateText.trimmingCharacters(in: .whitespacesAndNewlines) guard let value = Double(trimmed), value.isFinite, value > 0 else { return nil } return value } var body: some View { Form { Section("ACU Conversion") { HStack(alignment: .center, spacing: 10) { Text("USD per ACU") Spacer() TextField("", text: $rateText) .textFieldStyle(.roundedBorder) .multilineTextAlignment(.trailing) .frame(width: 96) .accessibilityLabel("USD per ACU") Text("USD") .foregroundStyle(.secondary) .frame(width: 36, alignment: .leading) } Button("Save") { saveRate() } .buttonStyle(.borderedProminent) .disabled(parsedRate == nil) if !statusText.isEmpty { Text(statusText) .font(.system(size: 11)) .foregroundStyle(.secondary) } } Section { Text("CodeBurn reads Devin ACU usage from local transcripts only after this rate is configured, then multiplies each step by the rate before reporting cost.") .font(.system(size: 11)) .foregroundStyle(.secondary) } header: { Text("How it works") } } .formStyle(.grouped) .padding() .onAppear { if let rate = CLIDevinConfig.loadAcuUsdRate() { rateText = Self.format(rate) } } } private func saveRate() { guard let rate = parsedRate else { return } CLIDevinConfig.persistAcuUsdRate(rate) rateText = Self.format(rate) statusText = "Saved. Refresh CodeBurn to recalculate Devin cost." } private static func format(_ value: Double) -> String { let formatter = NumberFormatter() formatter.locale = Locale(identifier: "en_US_POSIX") formatter.numberStyle = .decimal formatter.minimumFractionDigits = 0 formatter.maximumFractionDigits = 6 return formatter.string(from: NSNumber(value: value)) ?? String(value) } } // MARK: - About private struct AboutSettingsTab: View { private let appVersion: String = AppVersion.normalizedBundleShortVersion private let buildVersion: String = AppVersion.normalizedBundleBuildVersion var body: some View { VStack(spacing: 14) { Image(systemName: "flame.fill") .font(.system(size: 40)) .foregroundStyle(Theme.brandAccent) Text("CodeBurn") .font(.system(size: 18, weight: .semibold)) Text("AI Coding Cost Tracker") .font(.system(size: 12)) .foregroundStyle(.secondary) 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)) } .frame(maxWidth: .infinity, maxHeight: .infinity) .padding() } }