feat(polish): add configurable style packs
Add shared prompt composition, custom style management, iCloud sync, and native iOS/macOS selection interfaces so users can keep a consistent writing voice across devices.
This commit is contained in:
@@ -40,6 +40,12 @@ public struct AppGroupConfiguration: Sendable, Equatable {
|
||||
public static let detectedAppContext = "config.detectedAppContext"
|
||||
public static let detectedAppContextAt = "config.detectedAppContextAt"
|
||||
public static let personalDictionary = "config.personalDictionary.v1"
|
||||
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.
|
||||
public static let legacyPolishScenarioId = "config.polishScenarioId"
|
||||
public static let legacySystemPrompt = "config.systemPrompt"
|
||||
/// When true, the main app mirrors the personal dictionary via iCloud KVS.
|
||||
public static let personalDictionaryICloudSyncEnabled = "config.personalDictionary.iCloudSyncEnabled"
|
||||
/// When true, the main app mirrors user settings via iCloud KVS.
|
||||
@@ -82,6 +88,8 @@ public struct AppGroupConfiguration: Sendable, Equatable {
|
||||
/// Enables provider-specific reasoning / thinking controls for polish LLM requests.
|
||||
public var llmThinkingEnabled: Bool
|
||||
public var personalDictionary: PersonalDictionary
|
||||
public var polishStyleCatalog: PolishStyleCatalog
|
||||
public var activePolishStyleId: String
|
||||
/// Opt-in iCloud KVS sync for the personal dictionary (main app only).
|
||||
public var personalDictionaryICloudSyncEnabled: Bool
|
||||
/// Opt-in iCloud KVS sync for user settings (main app only).
|
||||
@@ -244,6 +252,9 @@ public struct AppGroupConfiguration: Sendable, Equatable {
|
||||
polishIntensity: resolvePolishIntensity(from: defaults),
|
||||
llmThinkingEnabled: defaults.bool(forKey: Keys.llmThinkingEnabled),
|
||||
personalDictionary: decodePersonalDictionary(from: defaults),
|
||||
polishStyleCatalog: decodePolishStyleCatalog(from: defaults),
|
||||
activePolishStyleId: defaults.string(forKey: Keys.activePolishStyleId)
|
||||
?? PolishStylePackCatalog.defaultID,
|
||||
personalDictionaryICloudSyncEnabled: {
|
||||
if defaults.object(forKey: Keys.personalDictionaryICloudSyncEnabled) == nil {
|
||||
return true
|
||||
@@ -347,6 +358,7 @@ public struct AppGroupConfiguration: Sendable, Equatable {
|
||||
config.modeId = "polish"
|
||||
defaults.set("polish", forKey: Keys.modeId)
|
||||
}
|
||||
migrateLegacyPolishStyleIfNeeded(configuration: &config, defaults: defaults)
|
||||
return config
|
||||
}
|
||||
|
||||
@@ -369,12 +381,14 @@ public struct AppGroupConfiguration: Sendable, Equatable {
|
||||
defaults.set(cursorDragNavigationEnabled, forKey: Keys.cursorDragNavigationEnabled)
|
||||
defaults.set(polishIntensity.rawValue, forKey: Keys.polishIntensity)
|
||||
defaults.set(llmThinkingEnabled, forKey: Keys.llmThinkingEnabled)
|
||||
defaults.set(activePolishStyleId, forKey: Keys.activePolishStyleId)
|
||||
defaults.set(flowSkipAppSwitch, forKey: Keys.flowSkipAppSwitch)
|
||||
defaults.set(flowInactivityDuration.rawValue, forKey: Keys.flowInactivityDuration)
|
||||
defaults.set(localASRCustomLanguageModelEnabled, forKey: Keys.localASRCustomLanguageModelEnabled)
|
||||
defaults.set(personalDictionaryICloudSyncEnabled, forKey: Keys.personalDictionaryICloudSyncEnabled)
|
||||
defaults.set(settingsICloudSyncEnabled, forKey: Keys.settingsICloudSyncEnabled)
|
||||
Self.encodePersonalDictionary(personalDictionary, to: defaults)
|
||||
Self.encodePolishStyleCatalog(polishStyleCatalog, to: defaults)
|
||||
}
|
||||
|
||||
// MARK: - Private helpers
|
||||
@@ -421,6 +435,57 @@ public struct AppGroupConfiguration: Sendable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
private static func decodePolishStyleCatalog(from defaults: UserDefaults) -> PolishStyleCatalog {
|
||||
guard let data = defaults.data(forKey: Keys.polishStyleCatalog) else { return .empty }
|
||||
do {
|
||||
return try JSONDecoder().decode(PolishStyleCatalog.self, from: data)
|
||||
} catch {
|
||||
OSGLog.config.warning("polishStyleCatalog decode failed: \(error.localizedDescription, privacy: .public)")
|
||||
return .empty
|
||||
}
|
||||
}
|
||||
|
||||
private static func encodePolishStyleCatalog(_ catalog: PolishStyleCatalog, to defaults: UserDefaults) {
|
||||
do {
|
||||
defaults.set(try JSONEncoder().encode(catalog), forKey: Keys.polishStyleCatalog)
|
||||
} catch {
|
||||
OSGLog.config.warning("polishStyleCatalog encode failed: \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
}
|
||||
|
||||
private static func migrateLegacyPolishStyleIfNeeded(
|
||||
configuration: inout AppGroupConfiguration,
|
||||
defaults: UserDefaults
|
||||
) {
|
||||
guard !defaults.bool(forKey: Keys.polishStylesMigrated) else { return }
|
||||
defer { defaults.set(true, forKey: Keys.polishStylesMigrated) }
|
||||
|
||||
if let legacyPrompt = defaults.string(forKey: Keys.legacySystemPrompt)?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!legacyPrompt.isEmpty {
|
||||
let boundedPrompt = String(legacyPrompt.prefix(PolishStyleLimits.maximumPromptCharacters))
|
||||
let custom = PolishStylePack(name: "自定义", prompt: boundedPrompt)
|
||||
if (try? configuration.polishStyleCatalog.upsert(custom)) != nil {
|
||||
configuration.activePolishStyleId = custom.id
|
||||
defaults.set(custom.id, forKey: Keys.activePolishStyleId)
|
||||
encodePolishStyleCatalog(configuration.polishStyleCatalog, to: defaults)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let legacyMappings = [
|
||||
"daily_chat": "builtin.chat",
|
||||
"work": "builtin.formal",
|
||||
"document": "builtin.structured",
|
||||
"todo": "builtin.structured",
|
||||
]
|
||||
if let legacyID = defaults.string(forKey: Keys.legacyPolishScenarioId),
|
||||
let mappedID = legacyMappings[legacyID] {
|
||||
configuration.activePolishStyleId = mappedID
|
||||
defaults.set(mappedID, forKey: Keys.activePolishStyleId)
|
||||
}
|
||||
}
|
||||
|
||||
/// Read the API key from the Keychain, falling back to a one-time migration from UserDefaults.
|
||||
static func resolveAPIKey(
|
||||
defaults: UserDefaults?,
|
||||
|
||||
Reference in New Issue
Block a user