Files
2026-07-13 12:22:33 +08:00

133 lines
5.3 KiB
Swift

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/<slug>, 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))
}
}