import CodexBarCore import SwiftUI @MainActor struct MenuPane: View { @Bindable var settings: SettingsStore @Bindable var store: UsageStore var body: some View { Form { Section { SettingsMenuPicker( selection: self.$settings.usageBarsFillOption, options: MenuSettingsMenuOptions.usageBarsFill, label: { Text(L("usage_bars_fill_title")) }, optionLabel: { option in Text(option.label) }) Toggle(isOn: self.$settings.quotaWarningMarkersVisible) { SettingsRowLabel( L("show_quota_warning_markers_title"), subtitle: L("show_quota_warning_markers_subtitle")) } SettingsMenuPicker( selection: self.$settings.weeklyProgressWorkDays, options: MenuSettingsMenuOptions.weeklyProgressWorkDays, label: { Text(L("weekly_progress_work_days_title")) }, optionLabel: { workDays in Text(MenuSettingsMenuOptions.weeklyProgressWorkDaysLabel(workDays)) }) SettingsMenuPicker( selection: self.$settings.resetTimesOption, options: MenuSettingsMenuOptions.resetTimes, label: { Text(L("reset_times_title")) }, optionLabel: { option in Text(option.label) }) } header: { Text(L("section_usage")) } Section { Toggle(L("show_provider_changelog_links_title"), isOn: self.$settings.providerChangelogLinksEnabled) Toggle(isOn: self.$settings.showOptionalCreditsAndExtraUsage) { SettingsRowLabel( L("show_credits_extra_usage_title"), subtitle: L("show_credits_extra_usage_subtitle")) } SettingsMenuPicker( selection: self.$settings.multiAccountMenuLayout, options: MenuSettingsMenuOptions.multiAccountLayouts, label: { Text(L("multi_account_layout_title")) }, optionLabel: { layout in Text(layout.label) }) } header: { Text(L("section_content")) } CostSummarySettingsSection(settings: self.settings, store: self.store) Section { Toggle(isOn: self.$settings.agentSessionsEnabled) { SettingsRowLabel( L("agent_sessions_title"), subtitle: L("agent_sessions_subtitle")) } TextField(L("agent_sessions_hosts_title"), text: self.$settings.agentSessionsManualHosts) .disabled(!self.settings.agentSessionsEnabled) } header: { Text(L("section_agent_sessions")) } footer: { SettingsSectionFooter(L("agent_sessions_footer")) } } .formStyle(.grouped) .toggleStyle(.switch) .scrollContentBackground(.hidden) .background(FocusResigningBackground()) } } /// Cost summary settings grouped-form section, including per-provider fetch status in the footer. @MainActor struct CostSummarySettingsSection: View { @Bindable var settings: SettingsStore @Bindable var store: UsageStore var body: some View { Section { SettingsMenuPicker( selection: self.$settings.costSummaryOption, options: MenuSettingsMenuOptions.costSummaries, label: { SettingsRowLabel(L("cost_summary_title"), subtitle: L("show_cost_summary_subtitle")) }, optionLabel: { option in Text(option.label) }) if self.settings.costUsageEnabled { CostHistoryDaysEditor(settings: self.settings) Toggle(isOn: self.$settings.costComparisonPeriodsEnabled) { SettingsRowLabel( L("cost_comparison_periods_title"), subtitle: L("cost_comparison_periods_subtitle")) } } } header: { Text(L("section_cost_summary")) } footer: { if self.settings.costUsageEnabled { SettingsSectionFooter { VStack(alignment: .leading, spacing: 3) { Text(L("cost_auto_refresh_info")) self.costStatusLine(provider: .claude) self.costStatusLine(provider: .codex) } } } } } private func costStatusLine(provider: UsageProvider) -> Text { let name = ProviderDescriptorRegistry.descriptor(for: provider).metadata.displayName guard provider == .claude || provider == .codex else { return Text(String(format: L("cost_status_unsupported"), name)) } if self.store.isTokenRefreshInFlight(for: provider) { let elapsed: String = { guard let startedAt = self.store.tokenLastAttemptAt(for: provider) else { return "" } let seconds = max(0, Date().timeIntervalSince(startedAt)) let formatter = DateComponentsFormatter() formatter.allowedUnits = seconds < 60 ? [.second] : [.minute, .second] formatter.unitsStyle = .abbreviated return formatter.string(from: seconds).map { " (\($0))" } ?? "" }() return Text(String(format: L("cost_status_fetching"), name, elapsed)) } if let snapshot = self.store.tokenSnapshot(for: provider) { let updated = UsageFormatter.updatedString(from: snapshot.updatedAt) let cost = snapshot.last30DaysCostUSD .map { UsageFormatter.currencyString($0, currencyCode: snapshot.currencyCode) } ?? "—" let window = snapshot.historyLabel ?? (snapshot.historyDays == 1 ? "today" : "\(snapshot.historyDays)d") return Text(String(format: L("cost_status_snapshot"), name, updated, window, cost)) } if let error = self.store.tokenError(for: provider), !error.isEmpty { let truncated = UsageFormatter.truncatedSingleLine(error, max: 120) return Text(String(format: L("cost_status_error"), name, truncated)) } if let lastAttempt = self.store.tokenLastAttemptAt(for: provider) { let rel = RelativeDateTimeFormatter() rel.locale = Locale(identifier: "en_US") rel.unitsStyle = .abbreviated let when = rel.localizedString(for: lastAttempt, relativeTo: Date()) return Text(String(format: L("cost_status_last_attempt"), name, when)) } return Text(String(format: L("cost_status_no_data"), name)) } } @MainActor struct CostHistoryDaysEditor: View { @Bindable var settings: SettingsStore static func title(days: Int) -> String { String(format: L("cost_history_days_title"), days) } var body: some View { LabeledContent(Self.title(days: self.settings.costUsageHistoryDays)) { HStack(spacing: 8) { TextField( Self.title(days: self.settings.costUsageHistoryDays), value: self.$settings.costUsageHistoryDays, format: .number) .labelsHidden() .textFieldStyle(.roundedBorder) .multilineTextAlignment(.trailing) .monospacedDigit() .frame(width: 64) Stepper(value: self.$settings.costUsageHistoryDays, in: 1...365, step: 1) { EmptyView() } .labelsHidden() } } } }