import AppKit import CodexBarCore import Foundation import SwiftUI struct DevinProviderImplementation: ProviderImplementation { let id: UsageProvider = .devin let supportsLoginFlow: Bool = true @MainActor func presentation(context _: ProviderPresentationContext) -> ProviderPresentation { ProviderPresentation { context in context.store.sourceLabel(for: context.provider) } } @MainActor func observeSettings(_ settings: SettingsStore) { _ = settings.devinCookieSource _ = settings.devinBearerToken _ = settings.devinOrganization } @MainActor func settingsSnapshot(context: ProviderSettingsSnapshotContext) -> ProviderSettingsSnapshotContribution? { .devin(context.settings.devinSettingsSnapshot(tokenOverride: context.tokenOverride)) } @MainActor func settingsPickers(context: ProviderSettingsContext) -> [ProviderSettingsPickerDescriptor] { let cookieBinding = Binding( get: { context.settings.devinCookieSource.rawValue }, set: { raw in context.settings.devinCookieSource = ProviderCookieSource(rawValue: raw) ?? .auto }) let cookieOptions = ProviderCookieSourceUI.options( allowsOff: false, keychainDisabled: context.settings.debugDisableKeychainAccess) let subtitle: () -> String? = { ProviderCookieSourceUI.subtitle( source: context.settings.devinCookieSource, keychainDisabled: context.settings.debugDisableKeychainAccess, auto: "Automatically imports the app.devin.ai session from Chrome.", manual: "Paste an Authorization Bearer token from app.devin.ai.", off: "Paste an Authorization Bearer token from app.devin.ai.") } return [ ProviderSettingsPickerDescriptor( id: "devin-cookie-source", title: "Auth source", subtitle: "Automatically imports the app.devin.ai session from Chrome.", dynamicSubtitle: subtitle, binding: cookieBinding, options: cookieOptions, isVisible: nil, onChange: nil), ] } @MainActor func settingsFields(context: ProviderSettingsContext) -> [ProviderSettingsFieldDescriptor] { [ ProviderSettingsFieldDescriptor( id: "devin-organization", title: "Organization", subtitle: "Optional. Use the slug from app.devin.ai/org/, or paste the full Devin org URL.", kind: .plain, placeholder: "org/example-org", binding: context.stringBinding(\.devinOrganization), actions: [ ProviderSettingsActionDescriptor( id: "devin-open-usage", title: "Open Devin Usage", style: .link, isVisible: nil, perform: { NSWorkspace.shared.open(Self.usageURL(organization: context.settings.devinOrganization)) }), ], isVisible: nil, onActivate: nil), ProviderSettingsFieldDescriptor( id: "devin-bearer-token", title: "Bearer token", subtitle: "Paste the Authorization header value from app.devin.ai.", kind: .secure, placeholder: "Bearer eyJ...", binding: context.stringBinding(\.devinBearerToken), actions: [], isVisible: { context.settings.devinCookieSource == .manual }, onActivate: nil), ] } @MainActor func loginMenuAction(context _: ProviderMenuLoginContext) -> (label: String, action: MenuDescriptor.MenuAction)? { ("Open Devin...", .loginToProvider(url: Self.usageURL(organization: nil).absoluteString)) } @MainActor func runLoginFlow(context: ProviderLoginContext) async -> Bool { let organization = context.controller.settings.devinOrganization NSWorkspace.shared.open(Self.usageURL(organization: organization)) return false } private static func usageURL(organization: String?) -> URL { let normalized = DevinUsageFetcher.normalizedOrganization(organization) let urlString: String if let normalized, normalized.hasPrefix("org/") { let slug = String(normalized.dropFirst(4)) urlString = "https://app.devin.ai/org/\(slug)/settings/usage" } else { urlString = "https://app.devin.ai/settings/usage" } return URL(string: urlString) ?? URL(string: "https://app.devin.ai")! } @MainActor func appendUsageMenuEntries(context: ProviderMenuUsageContext, entries: inout [ProviderMenuEntry]) { guard context.settings.showOptionalCreditsAndExtraUsage, let cost = context.snapshot?.providerCost, cost.period == "Extra usage balance" else { return } let balance = UsageFormatter.currencyString(cost.used, currencyCode: cost.currencyCode) entries.append(.text(L("Extra usage balance: %@", balance), .primary)) } }