feat(keyboard): ship AI hint carousel, home library cards, and clipboard polish
Rotate AI idle suggestions with optional remote packs, move history/dictionary onto self-sizing Home preview cards, harden clipboard capture/prompting, and simplify keyboard chrome by dropping most liquid-glass shadows.
This commit is contained in:
@@ -49,7 +49,7 @@ public struct AppGroupConfiguration: Sendable, Equatable {
|
||||
public static let polishStyleCatalog = "config.polishStyles.v1"
|
||||
public static let activePolishStyleId = "config.activePolishStyleId"
|
||||
public static let polishStylesMigrated = "config.polishStyles.migrated"
|
||||
/// Keys used by the removed pre-v0.3 manual scenario implementation.
|
||||
/// Legacy keys from the removed manual scenario implementation.
|
||||
public static let legacyPolishScenarioId = "config.polishScenarioId"
|
||||
public static let legacySystemPrompt = "config.systemPrompt"
|
||||
/// When true, the main app mirrors the personal dictionary via iCloud KVS.
|
||||
@@ -230,7 +230,9 @@ public struct AppGroupConfiguration: Sendable, Equatable {
|
||||
return load(fromAvailable: store)
|
||||
}
|
||||
|
||||
/// Loads configuration from a known-available UserDefaults suite.
|
||||
/// Loads and idempotently migrates a known-available suite; this is not a
|
||||
/// pure read. Missing defaults distinguish upgrades (legacy cloud engine)
|
||||
/// from fresh installs (local engine) before being persisted.
|
||||
public static func load(fromAvailable defaults: UserDefaults) -> AppGroupConfiguration {
|
||||
let storedProviderId = defaults.string(forKey: Keys.providerId)
|
||||
var config = AppGroupConfiguration(
|
||||
@@ -336,6 +338,15 @@ public struct AppGroupConfiguration: Sendable, Equatable {
|
||||
|
||||
// Legacy qwen cloud ASR → bailian realtime (HTTP Flash path removed).
|
||||
if config.asrProviderId == "qwen" {
|
||||
do {
|
||||
try Keychain.copyQwenASRKeyToBailian(
|
||||
useICloudSync: config.settingsICloudSyncEnabled
|
||||
)
|
||||
} catch {
|
||||
OSGLog.config.warning(
|
||||
"qwen ASR credential migration deferred: \(String(describing: error), privacy: .public)"
|
||||
)
|
||||
}
|
||||
let bailian = LLMProvider.provider(id: "bailian")
|
||||
config.asrProviderId = "bailian"
|
||||
config.asrBaseURL = bailian.defaultBaseURL
|
||||
@@ -508,7 +519,9 @@ public struct AppGroupConfiguration: Sendable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
/// Read the API key from the Keychain, falling back to a one-time migration from UserDefaults.
|
||||
/// Resolves the provider-scoped Keychain item, then the legacy `current`
|
||||
/// account, then plaintext defaults. Legacy sources are removed after the
|
||||
/// selected local or synchronizable target is read back exactly.
|
||||
static func resolveAPIKey(
|
||||
defaults: UserDefaults?,
|
||||
providerId: String,
|
||||
@@ -518,20 +531,40 @@ public struct AppGroupConfiguration: Sendable, Equatable {
|
||||
return stored
|
||||
}
|
||||
if let legacyKeychain = Keychain.legacyAPIKey(), !legacyKeychain.isEmpty {
|
||||
try? Keychain.setAPIKey(legacyKeychain, for: providerId, useICloudSync: preferICloudSync)
|
||||
try? Keychain.deleteLegacyAPIKey()
|
||||
do {
|
||||
try Keychain.migrateLegacyAPIKey(
|
||||
to: providerId,
|
||||
useICloudSync: preferICloudSync
|
||||
)
|
||||
} catch {
|
||||
OSGLog.config.warning(
|
||||
"legacy Keychain credential migration deferred: \(String(describing: error), privacy: .public)"
|
||||
)
|
||||
}
|
||||
return legacyKeychain
|
||||
}
|
||||
if let defaults,
|
||||
let legacy = defaults.string(forKey: Keys.apiKeyLegacy),
|
||||
!legacy.isEmpty {
|
||||
try? Keychain.setAPIKey(legacy, for: providerId, useICloudSync: preferICloudSync)
|
||||
defaults.removeObject(forKey: Keys.apiKeyLegacy)
|
||||
do {
|
||||
try Keychain.copyAPIKeyToSelectedStorage(
|
||||
legacy,
|
||||
providerId: providerId,
|
||||
useICloudSync: preferICloudSync
|
||||
)
|
||||
defaults.removeObject(forKey: Keys.apiKeyLegacy)
|
||||
} catch {
|
||||
OSGLog.config.warning(
|
||||
"legacy defaults credential migration deferred: \(String(describing: error), privacy: .public)"
|
||||
)
|
||||
}
|
||||
return legacy
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
/// Resolves the ASR-scoped account first, then falls back to the matching
|
||||
/// polish-provider account used before ASR credentials were split.
|
||||
static func resolveASRAPIKey(
|
||||
defaults: UserDefaults?,
|
||||
providerId: String,
|
||||
@@ -540,6 +573,23 @@ public struct AppGroupConfiguration: Sendable, Equatable {
|
||||
if let stored = Keychain.asrApiKey(for: providerId, preferICloudSync: preferICloudSync), !stored.isEmpty {
|
||||
return stored
|
||||
}
|
||||
if providerId == "bailian" {
|
||||
try? Keychain.copyQwenASRKeyToBailian(useICloudSync: preferICloudSync)
|
||||
if let migrated = Keychain.asrApiKey(
|
||||
for: providerId,
|
||||
preferICloudSync: preferICloudSync
|
||||
), !migrated.isEmpty {
|
||||
return migrated
|
||||
}
|
||||
// Keep a compatibility read while older signed installs may still
|
||||
// hold the DashScope credential under qwen accounts.
|
||||
if let legacyQwen = Keychain.asrApiKey(
|
||||
for: "qwen",
|
||||
preferICloudSync: preferICloudSync
|
||||
), !legacyQwen.isEmpty {
|
||||
return legacyQwen
|
||||
}
|
||||
}
|
||||
// Pre-split installs: one shared key under `provider.<id>`.
|
||||
return resolveAPIKey(defaults: defaults, providerId: providerId, preferICloudSync: preferICloudSync)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user