feat(account): add managed credits and cloud gateway
Introduce optional Apple account-backed credits with scoped gateway access while preserving local and BYOK paths. Refresh assistant behavior, tests, privacy disclosures, docs, and the website for the 2.0 experience.
This commit is contained in:
@@ -13,7 +13,7 @@ public struct AIAgentSkillLayout: Codable, Equatable, Sendable {
|
||||
public static let defaultEnabledIDs = [
|
||||
AIClipboardSkillCatalog.replyID,
|
||||
AIClipboardSkillCatalog.summarizeID,
|
||||
AIClipboardSkillCatalog.translateID,
|
||||
AIClipboardSkillCatalog.translateID
|
||||
]
|
||||
|
||||
/// Keyboard chip order. Unknown / unconfirmed IDs are dropped on sanitize.
|
||||
|
||||
@@ -110,7 +110,7 @@ public enum AIUserSkillLimits {
|
||||
"number",
|
||||
"at",
|
||||
"tray.fill",
|
||||
"quote.bubble.fill",
|
||||
"quote.bubble.fill"
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@ public struct AppGroupConfiguration: Sendable, Equatable {
|
||||
public static let modeId = "config.modeId"
|
||||
public static let localeId = "config.localeId"
|
||||
public static let engineMode = "config.engineMode"
|
||||
/// Credential ownership is independent from local/cloud ASR selection.
|
||||
public static let credentialSource = "config.credentialSource"
|
||||
public static let hasCompletedOnboarding = "config.hasCompletedOnboarding"
|
||||
public static let onboardingPage = "config.onboardingPage"
|
||||
public static let hasAcknowledgedCloudSharing = "config.hasAcknowledgedCloudSharing"
|
||||
@@ -87,6 +89,7 @@ public struct AppGroupConfiguration: Sendable, Equatable {
|
||||
public var modeId: String
|
||||
public var localeId: String
|
||||
public var engineMode: String
|
||||
public var credentialSource: CredentialSource
|
||||
public var hasCompletedOnboarding: Bool
|
||||
public var onboardingPage: Int
|
||||
public var hasAcknowledgedCloudSharing: Bool
|
||||
@@ -146,11 +149,13 @@ public struct AppGroupConfiguration: Sendable, Equatable {
|
||||
|
||||
public var isCloudLLMKeyMissing: Bool {
|
||||
guard engineMode == "cloud" else { return false }
|
||||
guard credentialSource == .byok else { return false }
|
||||
return apiKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
}
|
||||
|
||||
public var isCloudASRKeyMissing: Bool {
|
||||
guard engineMode == "cloud" else { return false }
|
||||
guard credentialSource == .byok else { return false }
|
||||
let key = asrApiKey.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !key.isEmpty else { return true }
|
||||
// Volcengine may store auth_mode JSON before credentials are filled.
|
||||
@@ -161,7 +166,8 @@ public struct AppGroupConfiguration: Sendable, Equatable {
|
||||
}
|
||||
|
||||
public var isPolishKeyMissing: Bool {
|
||||
apiKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
guard credentialSource == .byok else { return false }
|
||||
return apiKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
}
|
||||
|
||||
public var isCloudAPIKeyMissingForVoiceInput: Bool {
|
||||
@@ -187,8 +193,15 @@ public struct AppGroupConfiguration: Sendable, Equatable {
|
||||
)
|
||||
}
|
||||
|
||||
public func makeClient() -> LLMClient {
|
||||
OpenAICompatibleClient(
|
||||
public func makeClient(taskKind: ManagedGatewayTaskKind? = nil) -> LLMClient {
|
||||
if credentialSource == .managed {
|
||||
return ManagedLLMClient(
|
||||
capability: .polish,
|
||||
taskKind: taskKind,
|
||||
grants: GatewayGrantCoordinator()
|
||||
)
|
||||
}
|
||||
return OpenAICompatibleClient(
|
||||
baseURL: baseURL,
|
||||
apiKey: apiKey,
|
||||
model: model,
|
||||
@@ -255,6 +268,9 @@ public struct AppGroupConfiguration: Sendable, Equatable {
|
||||
// cloud default would contradict every privacy claim the app
|
||||
// makes in its docs, App Store listing, and permission prompts.
|
||||
engineMode: defaults.string(forKey: Keys.engineMode) ?? "local",
|
||||
credentialSource: CredentialSource.fromStored(
|
||||
defaults.string(forKey: Keys.credentialSource)
|
||||
),
|
||||
hasCompletedOnboarding: defaults.bool(forKey: Keys.hasCompletedOnboarding),
|
||||
onboardingPage: {
|
||||
let saved = defaults.integer(forKey: Keys.onboardingPage)
|
||||
@@ -387,7 +403,7 @@ public struct AppGroupConfiguration: Sendable, Equatable {
|
||||
// who later choose 30m / 10m again keep that choice.
|
||||
let previousDefaults: Set<String> = [
|
||||
FlowInactivityDuration.thirtyMinutes.rawValue,
|
||||
FlowInactivityDuration.tenMinutes.rawValue,
|
||||
FlowInactivityDuration.tenMinutes.rawValue
|
||||
]
|
||||
if previousDefaults.contains(config.flowInactivityDuration.rawValue) {
|
||||
config.flowInactivityDuration = .default
|
||||
@@ -415,6 +431,7 @@ public struct AppGroupConfiguration: Sendable, Equatable {
|
||||
defaults.set(modeId, forKey: Keys.modeId)
|
||||
defaults.set(localeId, forKey: Keys.localeId)
|
||||
defaults.set(engineMode, forKey: Keys.engineMode)
|
||||
defaults.set(credentialSource.rawValue, forKey: Keys.credentialSource)
|
||||
defaults.set(hasCompletedOnboarding, forKey: Keys.hasCompletedOnboarding)
|
||||
defaults.set(onboardingPage, forKey: Keys.onboardingPage)
|
||||
defaults.set(hasAcknowledgedCloudSharing, forKey: Keys.hasAcknowledgedCloudSharing)
|
||||
@@ -438,6 +455,123 @@ public struct AppGroupConfiguration: Sendable, Equatable {
|
||||
Self.encodePolishStyleCatalog(polishStyleCatalog, to: defaults)
|
||||
}
|
||||
|
||||
/// Persists only fields changed from the caller's last observed snapshot.
|
||||
///
|
||||
/// Main app and keyboard extension are separate processes. Rewriting every
|
||||
/// key from a stale snapshot can undo a newer, unrelated setting written by
|
||||
/// the other process. Field-level writes keep unrelated updates intact.
|
||||
public func saveChanges(since baseline: Self, to defaults: UserDefaults) {
|
||||
func set<Value: Equatable>(_ value: Value, previous: Value, key: String) {
|
||||
guard value != previous else { return }
|
||||
defaults.set(value, forKey: key)
|
||||
}
|
||||
|
||||
set(providerId, previous: baseline.providerId, key: Keys.providerId)
|
||||
set(baseURL, previous: baseline.baseURL, key: Keys.baseURL)
|
||||
set(model, previous: baseline.model, key: Keys.model)
|
||||
set(asrProviderId, previous: baseline.asrProviderId, key: Keys.asrProviderId)
|
||||
set(asrBaseURL, previous: baseline.asrBaseURL, key: Keys.asrBaseURL)
|
||||
set(asrModel, previous: baseline.asrModel, key: Keys.asrModel)
|
||||
set(modeId, previous: baseline.modeId, key: Keys.modeId)
|
||||
set(localeId, previous: baseline.localeId, key: Keys.localeId)
|
||||
set(engineMode, previous: baseline.engineMode, key: Keys.engineMode)
|
||||
set(
|
||||
credentialSource.rawValue,
|
||||
previous: baseline.credentialSource.rawValue,
|
||||
key: Keys.credentialSource
|
||||
)
|
||||
set(
|
||||
hasCompletedOnboarding,
|
||||
previous: baseline.hasCompletedOnboarding,
|
||||
key: Keys.hasCompletedOnboarding
|
||||
)
|
||||
set(onboardingPage, previous: baseline.onboardingPage, key: Keys.onboardingPage)
|
||||
set(
|
||||
hasAcknowledgedCloudSharing,
|
||||
previous: baseline.hasAcknowledgedCloudSharing,
|
||||
key: Keys.hasAcknowledgedCloudSharing
|
||||
)
|
||||
set(uiLanguage.rawValue, previous: baseline.uiLanguage.rawValue, key: Keys.uiLanguage)
|
||||
set(
|
||||
translationTargetLocaleId,
|
||||
previous: baseline.translationTargetLocaleId,
|
||||
key: Keys.translationTargetLocaleId
|
||||
)
|
||||
set(
|
||||
handednessPreference.rawValue,
|
||||
previous: baseline.handednessPreference.rawValue,
|
||||
key: Keys.handednessPreference
|
||||
)
|
||||
set(
|
||||
cursorDragNavigationEnabled,
|
||||
previous: baseline.cursorDragNavigationEnabled,
|
||||
key: Keys.cursorDragNavigationEnabled
|
||||
)
|
||||
set(
|
||||
keyboardHapticIntensity.rawValue,
|
||||
previous: baseline.keyboardHapticIntensity.rawValue,
|
||||
key: Keys.keyboardHapticIntensity
|
||||
)
|
||||
set(
|
||||
polishIntensity.rawValue,
|
||||
previous: baseline.polishIntensity.rawValue,
|
||||
key: Keys.polishIntensity
|
||||
)
|
||||
set(
|
||||
aiResponseLength.rawValue,
|
||||
previous: baseline.aiResponseLength.rawValue,
|
||||
key: Keys.aiResponseLength
|
||||
)
|
||||
set(
|
||||
llmThinkingEnabled,
|
||||
previous: baseline.llmThinkingEnabled,
|
||||
key: Keys.llmThinkingEnabled
|
||||
)
|
||||
set(
|
||||
clipboardHistoryEnabled,
|
||||
previous: baseline.clipboardHistoryEnabled,
|
||||
key: Keys.clipboardHistoryEnabled
|
||||
)
|
||||
set(
|
||||
clipboardCandidateBarEnabled,
|
||||
previous: baseline.clipboardCandidateBarEnabled,
|
||||
key: Keys.clipboardCandidateBarEnabled
|
||||
)
|
||||
set(
|
||||
activePolishStyleId,
|
||||
previous: baseline.activePolishStyleId,
|
||||
key: Keys.activePolishStyleId
|
||||
)
|
||||
set(flowSkipAppSwitch, previous: baseline.flowSkipAppSwitch, key: Keys.flowSkipAppSwitch)
|
||||
set(
|
||||
flowInactivityDuration.rawValue,
|
||||
previous: baseline.flowInactivityDuration.rawValue,
|
||||
key: Keys.flowInactivityDuration
|
||||
)
|
||||
set(
|
||||
localASRCustomLanguageModelEnabled,
|
||||
previous: baseline.localASRCustomLanguageModelEnabled,
|
||||
key: Keys.localASRCustomLanguageModelEnabled
|
||||
)
|
||||
set(
|
||||
personalDictionaryICloudSyncEnabled,
|
||||
previous: baseline.personalDictionaryICloudSyncEnabled,
|
||||
key: Keys.personalDictionaryICloudSyncEnabled
|
||||
)
|
||||
set(
|
||||
settingsICloudSyncEnabled,
|
||||
previous: baseline.settingsICloudSyncEnabled,
|
||||
key: Keys.settingsICloudSyncEnabled
|
||||
)
|
||||
|
||||
if personalDictionary != baseline.personalDictionary {
|
||||
Self.encodePersonalDictionary(personalDictionary, to: defaults)
|
||||
}
|
||||
if polishStyleCatalog != baseline.polishStyleCatalog {
|
||||
Self.encodePolishStyleCatalog(polishStyleCatalog, to: defaults)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private helpers
|
||||
|
||||
private static func decodePersonalDictionary(from defaults: UserDefaults) -> PersonalDictionary {
|
||||
@@ -514,7 +648,7 @@ public struct AppGroupConfiguration: Sendable, Equatable {
|
||||
"work": "builtin.formal",
|
||||
"document": "builtin.structured",
|
||||
"todo": "builtin.structured",
|
||||
"social_lifestyle": "builtin.xhs",
|
||||
"social_lifestyle": "builtin.xhs"
|
||||
]
|
||||
if let legacyID = defaults.string(forKey: Keys.legacyPolishScenarioId),
|
||||
let mappedID = legacyMappings[legacyID] {
|
||||
|
||||
@@ -96,7 +96,7 @@ enum BuiltinPolishStyleLoader {
|
||||
// fall back to main / class bundle the same way other Shared resources do.
|
||||
var bundles: [Bundle] = [
|
||||
Bundle(for: BundleToken.self),
|
||||
Bundle.main,
|
||||
Bundle.main
|
||||
]
|
||||
#if !os(macOS)
|
||||
if let shared = Bundle(identifier: "com.osgkeyboard.ios.shared") {
|
||||
|
||||
@@ -71,7 +71,7 @@ public enum CloudASRModelCatalog {
|
||||
"openrouter",
|
||||
"mimo",
|
||||
"volcengine",
|
||||
"custom",
|
||||
"custom"
|
||||
]
|
||||
|
||||
/// Sync Fun-ASR Flash — base64 upload, ≤ 5 min, supports context + vocabulary.
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
// CredentialSource.swift
|
||||
// OSGKeyboard · Shared
|
||||
//
|
||||
// Credential ownership is independent from the ASR engine. This keeps local
|
||||
// ASR + managed polish, direct BYOK cloud, and fully managed flows composable.
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum CredentialSource: String, CaseIterable, Codable, Sendable {
|
||||
case byok
|
||||
case managed
|
||||
|
||||
public static func fromStored(_ value: String?) -> CredentialSource {
|
||||
CredentialSource(rawValue: value ?? "") ?? .byok
|
||||
}
|
||||
}
|
||||
@@ -20,8 +20,8 @@ public struct FlowCommand: Codable, Equatable, Sendable {
|
||||
case submitAIQuestion
|
||||
}
|
||||
|
||||
/// Wire version that includes submitAIQuestion + aiQuestionText.
|
||||
public static let currentProtocolVersion = 5
|
||||
/// Wire version that includes managed-gateway AI task intent.
|
||||
public static let currentProtocolVersion = 6
|
||||
|
||||
public let protocolVersion: Int
|
||||
public let sessionId: UUID
|
||||
@@ -41,6 +41,8 @@ public struct FlowCommand: Codable, Equatable, Sendable {
|
||||
public let aiConversationID: UUID?
|
||||
/// Prefilled question used only by `.submitAIQuestion`.
|
||||
public let aiQuestionText: String?
|
||||
/// Fine-grained managed-gateway intent for AI question submissions.
|
||||
public let aiTaskKind: ManagedGatewayTaskKind?
|
||||
/// Clipboard-skill thinking override. Nil keeps AI-mode default (on).
|
||||
public let aiThinkingEnabled: Bool?
|
||||
/// Absolute wall-clock deadlines survive extension reconstruction.
|
||||
@@ -62,6 +64,7 @@ public struct FlowCommand: Codable, Equatable, Sendable {
|
||||
sourceHistoryEntryRevision: Int64? = nil,
|
||||
aiConversationID: UUID? = nil,
|
||||
aiQuestionText: String? = nil,
|
||||
aiTaskKind: ManagedGatewayTaskKind? = nil,
|
||||
aiThinkingEnabled: Bool? = nil,
|
||||
startDeadlineAt: TimeInterval? = nil,
|
||||
processingDeadlineAt: TimeInterval? = nil
|
||||
@@ -80,6 +83,7 @@ public struct FlowCommand: Codable, Equatable, Sendable {
|
||||
self.sourceHistoryEntryRevision = sourceHistoryEntryRevision
|
||||
self.aiConversationID = aiConversationID
|
||||
self.aiQuestionText = aiQuestionText
|
||||
self.aiTaskKind = aiTaskKind
|
||||
self.aiThinkingEnabled = aiThinkingEnabled
|
||||
self.startDeadlineAt = startDeadlineAt
|
||||
self.processingDeadlineAt = processingDeadlineAt
|
||||
|
||||
@@ -37,7 +37,7 @@ public struct FlowFieldContext: Codable, Equatable, Sendable {
|
||||
keyboardType ?? "",
|
||||
returnKeyType ?? "",
|
||||
precedingText.map { String($0.suffix(80)) } ?? "",
|
||||
followingText.map { String($0.prefix(40)) } ?? "",
|
||||
followingText.map { String($0.prefix(40)) } ?? ""
|
||||
].joined(separator: "|")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ public struct FlowUtteranceRequest: Equatable, Sendable {
|
||||
public let aiConversationID: UUID?
|
||||
/// When set with `.aiQuestion`, host skips ASR and answers this text.
|
||||
public let aiQuestionText: String?
|
||||
/// Fine-grained managed-gateway intent. Regular questions keep the default.
|
||||
public let aiTaskKind: ManagedGatewayTaskKind?
|
||||
/// Clipboard-skill thinking override. Nil keeps AI-mode default (on).
|
||||
public let aiThinkingEnabled: Bool?
|
||||
|
||||
@@ -25,6 +27,7 @@ public struct FlowUtteranceRequest: Equatable, Sendable {
|
||||
sourceHistoryEntryRevision: Int64? = nil,
|
||||
aiConversationID: UUID? = nil,
|
||||
aiQuestionText: String? = nil,
|
||||
aiTaskKind: ManagedGatewayTaskKind? = nil,
|
||||
aiThinkingEnabled: Bool? = nil
|
||||
) {
|
||||
self.mode = mode
|
||||
@@ -33,6 +36,7 @@ public struct FlowUtteranceRequest: Equatable, Sendable {
|
||||
self.sourceHistoryEntryRevision = sourceHistoryEntryRevision
|
||||
self.aiConversationID = aiConversationID
|
||||
self.aiQuestionText = aiQuestionText
|
||||
self.aiTaskKind = aiTaskKind
|
||||
self.aiThinkingEnabled = aiThinkingEnabled
|
||||
}
|
||||
|
||||
@@ -53,12 +57,14 @@ public struct FlowUtteranceRequest: Equatable, Sendable {
|
||||
public static func aiQuestion(
|
||||
conversationID: UUID,
|
||||
prefilledQuestion: String? = nil,
|
||||
taskKind: ManagedGatewayTaskKind = .aiQuestion,
|
||||
thinkingEnabled: Bool? = nil
|
||||
) -> FlowUtteranceRequest {
|
||||
FlowUtteranceRequest(
|
||||
mode: .aiQuestion,
|
||||
aiConversationID: conversationID,
|
||||
aiQuestionText: prefilledQuestion,
|
||||
aiTaskKind: taskKind,
|
||||
aiThinkingEnabled: thinkingEnabled
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
// Formats the user dictionary for cloud ASR bias (hotwords, Alibaba
|
||||
// vocabulary entries, or Whisper-style prompt fragments).
|
||||
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
import Foundation
|
||||
|
||||
public struct AlibabaHotwordEntry: Codable, Sendable, Equatable {
|
||||
public let text: String
|
||||
|
||||
@@ -225,7 +225,7 @@ extension PersonalDictionary {
|
||||
createdAt: Date(timeIntervalSince1970: 0),
|
||||
updatedAt: Date(timeIntervalSince1970: 0),
|
||||
usageCount: 0
|
||||
),
|
||||
)
|
||||
]
|
||||
|
||||
/// User entries plus built-in system terms (deduped by term).
|
||||
|
||||
@@ -64,7 +64,7 @@ public struct PolishStylePack: Codable, Equatable, Identifiable, Sendable {
|
||||
"outranks global R5",
|
||||
"may add emojis",
|
||||
"allow mood emoji",
|
||||
"allowsAddedEmoji",
|
||||
"allowsAddedEmoji"
|
||||
]
|
||||
return markers.contains { prompt.localizedCaseInsensitiveContains($0) }
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
// inits after upgrade, a legacy plaintext value from UserDefaults is
|
||||
// migrated to the Keychain and removed from UserDefaults.
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
/// UI-owned ObservableObject; construct and mutate it on the main thread.
|
||||
/// `@unchecked Sendable` does not make `@Published` thread-safe. Credential
|
||||
@@ -125,6 +125,16 @@ public final class ProviderConfig: ObservableObject, @unchecked Sendable {
|
||||
persistConfiguration(postConfigChanged: true)
|
||||
}
|
||||
}
|
||||
/// Orthogonal to `engineMode`: direct provider credentials or an OSG
|
||||
/// scope-limited grant. Local and BYOK behavior remain the default.
|
||||
@Published public var credentialSource: CredentialSource {
|
||||
didSet {
|
||||
guard !isApplyingConfiguration,
|
||||
credentialSource != configuration.credentialSource else { return }
|
||||
configuration.credentialSource = credentialSource
|
||||
persistConfiguration(postConfigChanged: true)
|
||||
}
|
||||
}
|
||||
@Published public var hasCompletedOnboarding: Bool {
|
||||
didSet {
|
||||
guard !isApplyingConfiguration,
|
||||
@@ -331,6 +341,7 @@ public final class ProviderConfig: ObservableObject, @unchecked Sendable {
|
||||
}
|
||||
|
||||
public var isPolishConfigured: Bool {
|
||||
if credentialSource == .managed { return true }
|
||||
guard !apiKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
||||
return false
|
||||
}
|
||||
@@ -339,6 +350,7 @@ public final class ProviderConfig: ObservableObject, @unchecked Sendable {
|
||||
|
||||
public var isASRConfigured: Bool {
|
||||
guard !isLocalEngine else { return true }
|
||||
if credentialSource == .managed { return true }
|
||||
let key = asrApiKey.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let hasKey: Bool = {
|
||||
if asrProviderId == "volcengine" {
|
||||
@@ -360,6 +372,7 @@ public final class ProviderConfig: ObservableObject, @unchecked Sendable {
|
||||
|
||||
private let defaults: UserDefaults
|
||||
private var configuration: AppGroupConfiguration
|
||||
private var persistedConfigurationSnapshot: AppGroupConfiguration
|
||||
/// Suppresses `@Published` observer persistence while a complete snapshot
|
||||
/// or preset is applied, preventing reentrant writes of partial state.
|
||||
private var isApplyingConfiguration = false
|
||||
@@ -374,7 +387,9 @@ public final class ProviderConfig: ObservableObject, @unchecked Sendable {
|
||||
)
|
||||
}
|
||||
self.defaults = resolvedDefaults
|
||||
self.configuration = AppGroupConfiguration.load(fromAvailable: resolvedDefaults)
|
||||
let loadedConfiguration = AppGroupConfiguration.load(fromAvailable: resolvedDefaults)
|
||||
self.configuration = loadedConfiguration
|
||||
self.persistedConfigurationSnapshot = loadedConfiguration
|
||||
|
||||
// Fresh app container (reinstall after delete): wipe stale Keychain
|
||||
// onboarding so the welcome flow shows again. Reboot races still use
|
||||
@@ -423,6 +438,7 @@ public final class ProviderConfig: ObservableObject, @unchecked Sendable {
|
||||
modeId = configuration.modeId
|
||||
localeId = configuration.localeId
|
||||
engineMode = configuration.engineMode
|
||||
credentialSource = configuration.credentialSource
|
||||
hasCompletedOnboarding = configuration.hasCompletedOnboarding
|
||||
onboardingPage = configuration.onboardingPage
|
||||
hasAcknowledgedCloudSharing = configuration.hasAcknowledgedCloudSharing
|
||||
@@ -469,6 +485,7 @@ public final class ProviderConfig: ObservableObject, @unchecked Sendable {
|
||||
clipboardHistoryEnabled = false
|
||||
clipboardCandidateBarEnabled = false
|
||||
hasAcknowledgedCloudSharing = false
|
||||
credentialSource = .byok
|
||||
configuration.providerId = polishPreset.id
|
||||
configuration.baseURL = polishPreset.defaultBaseURL
|
||||
configuration.model = polishPreset.defaultModel
|
||||
@@ -484,12 +501,14 @@ public final class ProviderConfig: ObservableObject, @unchecked Sendable {
|
||||
configuration.clipboardHistoryEnabled = false
|
||||
configuration.clipboardCandidateBarEnabled = false
|
||||
configuration.hasAcknowledgedCloudSharing = false
|
||||
configuration.credentialSource = .byok
|
||||
isApplyingConfiguration = false
|
||||
persistConfiguration()
|
||||
}
|
||||
|
||||
private func persistConfiguration(postConfigChanged: Bool = false) {
|
||||
configuration.save(to: defaults)
|
||||
configuration.saveChanges(since: persistedConfigurationSnapshot, to: defaults)
|
||||
persistedConfigurationSnapshot = configuration
|
||||
if postConfigChanged {
|
||||
AppGroupConfigDarwin.postConfigChanged()
|
||||
}
|
||||
@@ -498,7 +517,8 @@ public final class ProviderConfig: ObservableObject, @unchecked Sendable {
|
||||
|
||||
/// Re-read App Group defaults after a cloud pull updates the cache.
|
||||
public func reloadFromPersistedStorage() {
|
||||
var fresh = AppGroupConfiguration.load(fromAvailable: defaults)
|
||||
let persisted = AppGroupConfiguration.load(fromAvailable: defaults)
|
||||
var fresh = persisted
|
||||
// Keep the reboot-durable onboarding marker authoritative across cloud
|
||||
// pulls, matching the resilience applied at init.
|
||||
let freshOnboarding = fresh.hasCompletedOnboarding
|
||||
@@ -514,6 +534,7 @@ public final class ProviderConfig: ObservableObject, @unchecked Sendable {
|
||||
}
|
||||
isApplyingConfiguration = true
|
||||
configuration = fresh
|
||||
persistedConfigurationSnapshot = persisted
|
||||
providerId = fresh.providerId
|
||||
baseURL = fresh.baseURL
|
||||
model = fresh.model
|
||||
@@ -523,6 +544,7 @@ public final class ProviderConfig: ObservableObject, @unchecked Sendable {
|
||||
modeId = fresh.modeId
|
||||
localeId = fresh.localeId
|
||||
engineMode = fresh.engineMode
|
||||
credentialSource = fresh.credentialSource
|
||||
hasCompletedOnboarding = fresh.hasCompletedOnboarding
|
||||
onboardingPage = fresh.onboardingPage
|
||||
hasAcknowledgedCloudSharing = fresh.hasAcknowledgedCloudSharing
|
||||
@@ -576,6 +598,7 @@ public final class ProviderConfig: ObservableObject, @unchecked Sendable {
|
||||
|
||||
public func applyAsr(preset: LLMProvider) {
|
||||
isApplyingConfiguration = true
|
||||
engineMode = "cloud"
|
||||
asrProviderId = preset.id
|
||||
if !preset.defaultBaseURL.isEmpty {
|
||||
asrBaseURL = preset.defaultBaseURL
|
||||
@@ -584,10 +607,11 @@ public final class ProviderConfig: ObservableObject, @unchecked Sendable {
|
||||
configuration.asrProviderId = asrProviderId
|
||||
configuration.asrBaseURL = asrBaseURL
|
||||
configuration.asrModel = asrModel
|
||||
configuration.engineMode = engineMode
|
||||
isSyncingASRProviderAPIKey = true
|
||||
asrApiKey = configuration.asrApiKey
|
||||
isSyncingASRProviderAPIKey = false
|
||||
isApplyingConfiguration = false
|
||||
persistConfiguration()
|
||||
persistConfiguration(postConfigChanged: true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,7 +254,7 @@ public struct SyncedAppSettingsV2: Codable, Equatable, Sendable {
|
||||
activePolishStyleId.updatedAt,
|
||||
llmThinkingEnabled.updatedAt,
|
||||
flowSkipAppSwitch.updatedAt,
|
||||
flowInactivityDuration.updatedAt,
|
||||
flowInactivityDuration.updatedAt
|
||||
].max() ?? .distantPast
|
||||
}
|
||||
|
||||
|
||||
@@ -184,7 +184,7 @@ public struct SyncedUsageStatisticsV2: Codable, Equatable, Sendable {
|
||||
dictationCharacterCount: legacy.dictationCharacterCount,
|
||||
translationCharacterCount: legacy.translationCharacterCount,
|
||||
aiCharacterCount: legacy.aiCharacterCount
|
||||
),
|
||||
)
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,16 +82,16 @@ public enum TranslationLanguageCatalog {
|
||||
/// "turn off" action is one tap away from any enabled state.
|
||||
public static let all: [TranslationLanguage] = [
|
||||
TranslationLanguage(id: offLocaleId, promptLanguageName: "", nativeName: ""),
|
||||
TranslationLanguage(id: "en", promptLanguageName: "English", nativeName: "English"),
|
||||
TranslationLanguage(id: "en", promptLanguageName: "English", nativeName: "English"),
|
||||
TranslationLanguage(id: "zh-Hans", promptLanguageName: "Simplified Chinese", nativeName: "简体中文"),
|
||||
TranslationLanguage(id: "zh-Hant", promptLanguageName: "Traditional Chinese", nativeName: "繁體中文"),
|
||||
TranslationLanguage(id: "ja", promptLanguageName: "Japanese", nativeName: "日本語"),
|
||||
TranslationLanguage(id: "ko", promptLanguageName: "Korean", nativeName: "한국어"),
|
||||
TranslationLanguage(id: "fr", promptLanguageName: "French", nativeName: "Français"),
|
||||
TranslationLanguage(id: "de", promptLanguageName: "German", nativeName: "Deutsch"),
|
||||
TranslationLanguage(id: "es", promptLanguageName: "Spanish", nativeName: "Español"),
|
||||
TranslationLanguage(id: "ru", promptLanguageName: "Russian", nativeName: "Русский"),
|
||||
TranslationLanguage(id: "pt", promptLanguageName: "Portuguese", nativeName: "Português"),
|
||||
TranslationLanguage(id: "ja", promptLanguageName: "Japanese", nativeName: "日本語"),
|
||||
TranslationLanguage(id: "ko", promptLanguageName: "Korean", nativeName: "한국어"),
|
||||
TranslationLanguage(id: "fr", promptLanguageName: "French", nativeName: "Français"),
|
||||
TranslationLanguage(id: "de", promptLanguageName: "German", nativeName: "Deutsch"),
|
||||
TranslationLanguage(id: "es", promptLanguageName: "Spanish", nativeName: "Español"),
|
||||
TranslationLanguage(id: "ru", promptLanguageName: "Russian", nativeName: "Русский"),
|
||||
TranslationLanguage(id: "pt", promptLanguageName: "Portuguese", nativeName: "Português")
|
||||
]
|
||||
|
||||
/// True when the given id is the "off" sentinel. Used by the picker
|
||||
@@ -113,4 +113,4 @@ public enum TranslationLanguageCatalog {
|
||||
}
|
||||
return all.first { $0.id == offLocaleId } ?? all[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
// App Group-backed Chinese input settings shared by the host app and
|
||||
// keyboard extension. Fuzzy pairs are opt-in to avoid noisy candidates.
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
public enum TypingInputSchema: String, CaseIterable, Identifiable, Codable, Sendable {
|
||||
case fullPinyin = "osg_pinyin"
|
||||
|
||||
@@ -55,7 +55,7 @@ public struct VolcengineASRFields: Sendable, Equatable {
|
||||
public var encodedAPIKey: String {
|
||||
var object: [String: String] = [
|
||||
"auth_mode": authMode.rawValue,
|
||||
"resource_id": Self.fixedResourceID,
|
||||
"resource_id": Self.fixedResourceID
|
||||
]
|
||||
// Persist both credential sets so toggling auth mode is non-destructive.
|
||||
if !appID.isEmpty { object["app_id"] = appID }
|
||||
|
||||
Reference in New Issue
Block a user