import CodexBarCore import SwiftUI @MainActor struct ProviderSettingsToggleRowView: View { let toggle: ProviderSettingsToggleDescriptor var body: some View { let isEnabled = self.toggle.isEnabled?() ?? true VStack(alignment: .leading, spacing: 8) { HStack(alignment: .firstTextBaseline, spacing: 12) { VStack(alignment: .leading, spacing: 4) { Text(L(self.toggle.title)) .font(.subheadline.weight(.semibold)) .foregroundStyle(isEnabled ? .primary : .tertiary) Text(L(self.toggle.subtitle)) .font(.footnote) .foregroundStyle(isEnabled ? .secondary : .tertiary) .fixedSize(horizontal: false, vertical: true) } Spacer(minLength: 8) Toggle("", isOn: self.toggle.binding) .labelsHidden() .toggleStyle(.switch) } if self.toggle.binding.wrappedValue { if let status = self.toggle.statusText?(), !status.isEmpty { Text(status) .font(.footnote) .foregroundStyle(.secondary) .lineLimit(4) .fixedSize(horizontal: false, vertical: true) } let actions = self.toggle.actions.filter { $0.isVisible?() ?? true } if !actions.isEmpty { HStack(spacing: 10) { ForEach(actions) { action in Button(L(action.title)) { Task { @MainActor in await action.perform() } } .applyProviderSettingsButtonStyle(action.style) .controlSize(.small) } } } } } .disabled(!isEnabled) .onChange(of: self.toggle.binding.wrappedValue) { _, enabled in guard let onChange = self.toggle.onChange else { return } Task { @MainActor in await onChange(enabled) } } .task(id: self.toggle.binding.wrappedValue) { guard self.toggle.binding.wrappedValue else { return } guard let onAppear = self.toggle.onAppearWhenEnabled else { return } await onAppear() } } } @MainActor struct ProviderSettingsPickerRowView: View { let picker: ProviderSettingsPickerDescriptor var body: some View { let isEnabled = self.picker.isEnabled?() ?? true let subtitle = self.picker.dynamicSubtitle?() ?? self.picker.subtitle let trimmedSubtitle = subtitle.trimmingCharacters(in: .whitespacesAndNewlines) LabeledContent { HStack(spacing: 8) { if let trailingText = self.picker.trailingText?(), !trailingText.isEmpty { Text(trailingText) .font(.footnote) .foregroundStyle(.secondary) .lineLimit(1) .truncationMode(.tail) } Picker("", selection: self.picker.binding) { ForEach(self.picker.options) { option in Text(L(option.title)).tag(option.id) } } .labelsHidden() .pickerStyle(.menu) .fixedSize() } } label: { SettingsRowLabel( L(self.picker.title), subtitle: trimmedSubtitle.isEmpty ? nil : L(trimmedSubtitle)) } .disabled(!isEnabled) .onChange(of: self.picker.binding.wrappedValue) { _, selection in guard let onChange = self.picker.onChange else { return } Task { @MainActor in await onChange(selection) } } } } /// Renders a provider settings field descriptor as its own grouped-form section: /// title becomes the header, subtitle/footer text become the footer, and the /// placeholder stays inside the field. @MainActor struct ProviderSettingsFieldRowView: View { let field: ProviderSettingsFieldDescriptor var body: some View { let trimmedTitle = self.field.title.trimmingCharacters(in: .whitespacesAndNewlines) Section { self.fieldView let actions = self.field.actions.filter { $0.isVisible?() ?? true } if !actions.isEmpty { HStack(spacing: 10) { ForEach(actions) { action in Button(L(action.title)) { Task { @MainActor in await action.perform() } } .applyProviderSettingsButtonStyle(action.style) .controlSize(.small) } } } } header: { if !trimmedTitle.isEmpty { Text(L(trimmedTitle)) } } footer: { self.footerView } } private var fieldView: some View { let prompt = (self.field.placeholder?.isEmpty == false) ? Text(L(self.field.placeholder ?? "")) : nil return Group { switch self.field.kind { case .plain: TextField(text: self.field.binding, prompt: prompt) { EmptyView() } case .secure: SecureField(text: self.field.binding, prompt: prompt) { EmptyView() } } } .labelsHidden() .textFieldStyle(.plain) .onTapGesture { self.field.onActivate?() } } @ViewBuilder private var footerView: some View { let trimmedSubtitle = self.field.subtitle.trimmingCharacters(in: .whitespacesAndNewlines) let footer = (self.field.footerText?.isEmpty == false) ? self.field.footerText : nil if !trimmedSubtitle.isEmpty || footer != nil { SettingsSectionFooter { VStack(alignment: .leading, spacing: 3) { if !trimmedSubtitle.isEmpty { Text(L(trimmedSubtitle)) } if let footer { Text(L(footer)) } } } } } } @MainActor struct ProviderSettingsActionsRowView: View { let descriptor: ProviderSettingsActionsDescriptor var body: some View { VStack(alignment: .leading, spacing: 8) { Text(L(self.descriptor.title)) .font(.subheadline.weight(.semibold)) if !self.descriptor.subtitle.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { Text(L(self.descriptor.subtitle)) .font(.footnote) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) } let actions = self.descriptor.actions.filter { $0.isVisible?() ?? true } if !actions.isEmpty { HStack(spacing: 10) { ForEach(actions) { action in Button(L(action.title)) { Task { @MainActor in await action.perform() } } .applyProviderSettingsButtonStyle(action.style) .controlSize(.small) } } } } } } @MainActor struct ProviderSettingsTokenAccountsRowView: View { struct TeamAccountDraft: Equatable { var teamMode: Bool var organizationID: String var projectID: String func normalizedForPersistence() -> Self { guard self.teamMode else { return Self(teamMode: false, organizationID: "", projectID: "") } return Self( teamMode: true, organizationID: self.organizationID.trimmingCharacters(in: .whitespacesAndNewlines), projectID: self.projectID.trimmingCharacters(in: .whitespacesAndNewlines)) } } let descriptor: ProviderSettingsTokenAccountsDescriptor @State private var newLabel: String = "" @State private var newToken: String = "" @State private var newOrgID: String = "" @State private var newProjectID: String = "" @State private var newTeamMode = false @State private var teamDrafts: [UUID: TeamAccountDraft] = [:] var body: some View { Section { let accounts = self.descriptor.accounts() if accounts.isEmpty { Text(L("No token accounts yet.")) .font(.footnote) .foregroundStyle(.secondary) } else { ForEach(Array(accounts.enumerated()), id: \.element.id) { index, account in VStack(alignment: .leading, spacing: 8) { HStack(alignment: .center, spacing: 10) { Button { self.descriptor.setActiveIndex(index) } label: { HStack(alignment: .center, spacing: 8) { Image(systemName: self.isActive(index: index, accountCount: accounts.count) ? "checkmark.circle.fill" : "circle") .font(.system(size: 13, weight: .medium)) .foregroundStyle(self.isActive(index: index, accountCount: accounts.count) ? Color.accentColor : Color.secondary) Text(account.displayName) .font( .footnote.weight( self.isActive(index: index, accountCount: accounts.count) ? .semibold : .regular)) .foregroundStyle(.primary) Spacer(minLength: 0) } .contentShape(Rectangle()) } .buttonStyle(.plain) Button(L("Remove")) { self.descriptor.removeAccount(account.id) } .buttonStyle(.bordered) .controlSize(.small) } if self.descriptor.showsTeamModeControls { self.teamModeEditor(account: account) } } } } if self.descriptor.primaryAddAction == nil { VStack(alignment: .leading, spacing: 8) { HStack(spacing: 8) { TextField(text: self.$newLabel, prompt: Text(L("Label"))) { EmptyView() } .labelsHidden() .textFieldStyle(.roundedBorder) .font(.footnote) .frame(maxWidth: 160) SecureField(text: self.$newToken, prompt: Text(L(self.descriptor.placeholder))) { EmptyView() } .labelsHidden() .textFieldStyle(.roundedBorder) .font(.footnote) Button(L("Add")) { let label = self.newLabel.trimmingCharacters(in: .whitespacesAndNewlines) let token = self.newToken.trimmingCharacters(in: .whitespacesAndNewlines) guard !label.isEmpty, !token.isEmpty else { return } let orgID = self.descriptor.showsOrganizationField ? self.newOrgID.trimmingCharacters(in: .whitespacesAndNewlines) : "" let teamOrgID = self.newOrgID.trimmingCharacters(in: .whitespacesAndNewlines) let projectID = self.newProjectID.trimmingCharacters(in: .whitespacesAndNewlines) let usageScope = self.descriptor.showsTeamModeControls ? (self.newTeamMode ? "team" : "personal") : nil let accountOrganizationID = if self.newTeamMode { teamOrgID.isEmpty ? nil : teamOrgID } else { orgID.isEmpty ? nil : orgID } let accountWorkspaceID = self.newTeamMode && !projectID.isEmpty ? projectID : nil self.descriptor.addAccount( label, token, usageScope, accountOrganizationID, accountWorkspaceID) self.newLabel = "" self.newToken = "" self.newOrgID = "" self.newProjectID = "" self.newTeamMode = false } .buttonStyle(.bordered) .controlSize(.small) .disabled(Self.isAddDisabled( label: self.newLabel, token: self.newToken, showsTeamModeControls: self.descriptor.showsTeamModeControls, teamMode: self.newTeamMode, teamContext: (organizationID: self.newOrgID, projectID: self.newProjectID))) } if self.descriptor.showsOrganizationField { TextField(text: self.$newOrgID, prompt: Text(L("Org ID (optional)"))) { EmptyView() } .labelsHidden() .textFieldStyle(.roundedBorder) .font(.footnote) .help( L("Optional organization ID for accounts linked to multiple Anthropic organizations.")) } if self.descriptor.showsTeamModeControls { Toggle(L("Team mode"), isOn: self.$newTeamMode) .toggleStyle(.checkbox) .font(.footnote) if self.newTeamMode { HStack(spacing: 8) { TextField(text: self.$newOrgID, prompt: Text(L("Organization ID"))) { EmptyView() } .labelsHidden() .textFieldStyle(.roundedBorder) .font(.footnote) TextField(text: self.$newProjectID, prompt: Text(L("Project ID"))) { EmptyView() } .labelsHidden() .textFieldStyle(.roundedBorder) .font(.footnote) } } } } } HStack(spacing: 10) { Button(L("Open token file")) { self.descriptor.openConfigFile() } .buttonStyle(.link) .controlSize(.small) Button(L("Reload")) { self.descriptor.reloadFromDisk() } .buttonStyle(.link) .controlSize(.small) Spacer(minLength: 0) if let title = self.descriptor.primaryAddActionTitle, let action = self.descriptor.primaryAddAction { Button(L(title)) { Task { @MainActor in await action() } } .buttonStyle(.bordered) .controlSize(.small) } } } header: { Text(L(self.descriptor.title)) } footer: { if !self.descriptor.subtitle.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { SettingsSectionFooter(L(self.descriptor.subtitle)) } } } private func isActive(index: Int, accountCount: Int) -> Bool { guard accountCount > 0 else { return false } let selectedIndex = min(self.descriptor.activeIndex(), max(0, accountCount - 1)) return selectedIndex == index } private func teamModeEditor(account: ProviderTokenAccount) -> some View { let draft = self.teamDraft(for: account) let original = Self.teamAccountDraft(for: account) return VStack(alignment: .leading, spacing: 6) { Toggle(L("Team mode"), isOn: self.teamModeDraftBinding(account: account)) .toggleStyle(.checkbox) .font(.footnote) if draft.teamMode { HStack(spacing: 8) { TextField( text: self.organizationIDDraftBinding(account: account), prompt: Text(L("Organization ID"))) { EmptyView() } .labelsHidden() .textFieldStyle(.roundedBorder) .font(.footnote) TextField( text: self.projectIDDraftBinding(account: account), prompt: Text(L("Project ID"))) { EmptyView() } .labelsHidden() .textFieldStyle(.roundedBorder) .font(.footnote) } } Button(L("apply")) { self.applyTeamDraft(account: account) } .buttonStyle(.bordered) .controlSize(.small) .disabled(Self.isTeamDraftApplyDisabled(draft: draft, original: original)) } .padding(.leading, 24) } private func teamModeDraftBinding(account: ProviderTokenAccount) -> Binding { Binding( get: { self.teamDraft(for: account).teamMode }, set: { enabled in self.updateTeamDraft(account: account) { draft in draft.teamMode = enabled } }) } private func organizationIDDraftBinding(account: ProviderTokenAccount) -> Binding { Binding( get: { self.teamDraft(for: account).organizationID }, set: { value in self.updateTeamDraft(account: account) { draft in draft.organizationID = value } }) } private func projectIDDraftBinding(account: ProviderTokenAccount) -> Binding { Binding( get: { self.teamDraft(for: account).projectID }, set: { value in self.updateTeamDraft(account: account) { draft in draft.projectID = value } }) } private func teamDraft(for account: ProviderTokenAccount) -> TeamAccountDraft { self.teamDrafts[account.id] ?? Self.teamAccountDraft(for: account) } private func updateTeamDraft( account: ProviderTokenAccount, mutate: (inout TeamAccountDraft) -> Void) { var draft = self.teamDraft(for: account) mutate(&draft) self.teamDrafts[account.id] = draft } private func applyTeamDraft(account: ProviderTokenAccount) { let draft = self.teamDraft(for: account) let original = Self.teamAccountDraft(for: account) guard !Self.isTeamDraftApplyDisabled(draft: draft, original: original) else { return } let normalized = draft.normalizedForPersistence() self.descriptor.updateAccount( account.id, normalized.teamMode ? "team" : "personal", normalized.teamMode ? normalized.organizationID : nil, normalized.teamMode ? normalized.projectID : nil) self.teamDrafts[account.id] = nil } static func teamAccountDraft(for account: ProviderTokenAccount) -> TeamAccountDraft { let teamMode = account.sanitizedUsageScope?.lowercased() == "team" return TeamAccountDraft( teamMode: teamMode, organizationID: teamMode ? (account.sanitizedOrganizationID ?? "") : "", projectID: teamMode ? (account.sanitizedWorkspaceID ?? "") : "") } static func isTeamDraftApplyDisabled(draft: TeamAccountDraft, original: TeamAccountDraft) -> Bool { let draft = draft.normalizedForPersistence() let original = original.normalizedForPersistence() guard draft != original else { return true } guard draft.teamMode else { return false } return draft.organizationID.isEmpty || draft.projectID.isEmpty } static func isAddDisabled( label: String, token: String, showsTeamModeControls: Bool, teamMode: Bool, teamContext: (organizationID: String, projectID: String)) -> Bool { let label = label.trimmingCharacters(in: .whitespacesAndNewlines) let token = token.trimmingCharacters(in: .whitespacesAndNewlines) guard !label.isEmpty, !token.isEmpty else { return true } guard showsTeamModeControls, teamMode else { return false } return teamContext.organizationID.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || teamContext.projectID.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } } extension View { @ViewBuilder fileprivate func applyProviderSettingsButtonStyle(_ style: ProviderSettingsActionDescriptor.Style) -> some View { switch style { case .bordered: self.buttonStyle(.bordered) case .link: self.buttonStyle(.link) } } } @MainActor struct ProviderSettingsOrganizationsRowView: View { let descriptor: ProviderSettingsOrganizationsDescriptor @State private var errorMessage: String? @State private var isRefreshing = false var body: some View { Section { let entries = self.descriptor.entries() if entries.allSatisfy(\.isLocked) { Text(L("No organizations loaded. Click Refresh after setting your API key.")) .font(.footnote) .foregroundStyle(.secondary) } else { ForEach(entries) { entry in Toggle(isOn: Binding( get: { entry.isEnabled }, set: { newValue in self.descriptor.onToggle(entry.id, newValue) })) { VStack(alignment: .leading, spacing: 1) { Text(entry.localizesTitle ? L(entry.title) : entry.title) if let subtitle = entry.subtitle, !subtitle.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { Text(entry.localizesSubtitle ? L(subtitle) : subtitle) .font(.caption) .foregroundStyle(.secondary) } } } .toggleStyle(.switch) .controlSize(.small) .disabled(entry.isLocked) } } HStack(spacing: 10) { Button(L("Refresh organizations")) { Task { @MainActor in self.isRefreshing = true let result = await self.descriptor.onRefresh() self.isRefreshing = false self.errorMessage = result.success ? nil : result.errorMessage } } .buttonStyle(.bordered) .controlSize(.small) .disabled(!self.descriptor.canRefresh() || self.isRefreshing) if let errorMessage = self.errorMessage, !errorMessage.isEmpty { Text(errorMessage) .font(.caption) .foregroundStyle(.red) } } } header: { Text(L(self.descriptor.title)) } footer: { if let subtitle = self.descriptor.subtitle, !subtitle.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { SettingsSectionFooter(L(subtitle)) } } } }