Cursor: Apply local changes for cloud agent

This commit is contained in:
Rocky
2026-08-27 18:01:46 +08:00
parent 39002336c0
commit 42e6252f01
148 changed files with 120105 additions and 8122 deletions
@@ -29,7 +29,10 @@ public struct AIAgentSkillLayout: Codable, Equatable, Sendable {
}
public func isEnabled(_ id: String) -> Bool {
enabledIDs.contains(id)
let canonical = AIClipboardSkillCatalog.canonicalID(for: id)
return enabledIDs.contains {
AIClipboardSkillCatalog.canonicalID(for: $0) == canonical
}
}
public func hasConfirmedShortcut(_ id: String) -> Bool {
@@ -0,0 +1,343 @@
// AIReplyVariant.swift
// OSGKeyboard · Shared
//
// Typed reply choices produced by one clipboard-reply request. Model output
// never controls SF Symbols or display labels; only these local allowlists do.
import Foundation
public struct AIReplyVariant: Equatable, Identifiable, Sendable {
public enum Kind: String, CaseIterable, Sendable {
case ordinary
case formal
case playful
public var systemImage: String {
switch self {
case .ordinary:
return "bubble.left.fill"
case .formal:
return "briefcase.fill"
case .playful:
return "theatermasks.fill"
}
}
public var titleKey: String {
switch self {
case .ordinary:
return "keyboard.ai.replyVariant.ordinary"
case .formal:
return "keyboard.ai.replyVariant.formal"
case .playful:
return "keyboard.ai.replyVariant.playful"
}
}
}
/// A bounded semantic hint for future feedback and presentation work.
/// Unknown model values are intentionally normalized to `.neutral`.
public enum Emotion: String, CaseIterable, Sendable {
case neutral
case warm
case celebratory
case empathetic
case encouraging
case grateful
case apologetic
case reassuring
case playful
case enthusiastic
case calm
/// The model selects only this semantic enum. SF Symbol names stay
/// local and validated so malformed model output cannot control UI.
public func systemImage(fallback kind: Kind) -> String {
switch self {
case .neutral:
return kind.systemImage
case .warm, .grateful:
return "heart.fill"
case .celebratory:
return "party.popper.fill"
case .empathetic:
return "heart.text.square.fill"
case .encouraging:
return "hand.thumbsup.fill"
case .apologetic:
return "exclamationmark.bubble.fill"
case .reassuring:
return "checkmark.shield.fill"
case .playful:
return "theatermasks.fill"
case .enthusiastic:
return "sparkles"
case .calm:
return "leaf.fill"
}
}
}
public let id: UUID
public let kind: Kind
public let emotion: Emotion
public let text: String
public init(
id: UUID = UUID(),
kind: Kind,
emotion: Emotion,
text: String
) {
self.id = id
self.kind = kind
self.emotion = emotion
self.text = text
}
}
public enum AIReplyVariantParsingResult: Equatable, Sendable {
case variants([AIReplyVariant])
case single(AIReplyVariant)
}
public enum AIReplyVariantParser {
/// The only accepted multi-reply wire shape is:
/// `{"variants":[{"kind":"ordinary","emotion":"warm","text":""}, ...]}`
/// with exactly one item of each kind and no additional JSON fields.
public static func parse(
_ raw: String,
sourceText: String? = nil
) -> [AIReplyVariant]? {
guard let data = raw.data(using: .utf8),
let root = try? JSONSerialization.jsonObject(with: data),
let object = root as? [String: Any],
Set(object.keys) == ["variants"],
let items = object["variants"] as? [[String: Any]],
items.count == AIReplyVariant.Kind.allCases.count else {
return nil
}
var variantsByKind: [AIReplyVariant.Kind: AIReplyVariant] = [:]
for item in items {
guard Set(item.keys) == ["kind", "emotion", "text"],
let rawKind = item["kind"] as? String,
let kind = AIReplyVariant.Kind(rawValue: rawKind),
variantsByKind[kind] == nil,
let rawEmotion = item["emotion"] as? String,
let rawText = item["text"] as? String else {
return nil
}
let text = rawText.trimmingCharacters(in: .whitespacesAndNewlines)
guard !text.isEmpty,
!isSourceEcho(text, sourceText: sourceText) else {
return nil
}
let emotion = AIReplyVariant.Emotion(rawValue: rawEmotion) ?? .neutral
variantsByKind[kind] = AIReplyVariant(
kind: kind,
emotion: emotion,
text: text
)
}
let ordered = AIReplyVariant.Kind.allCases.compactMap { variantsByKind[$0] }
return ordered.count == AIReplyVariant.Kind.allCases.count ? ordered : nil
}
/// Strict multi-reply parsing with a conservative single ordinary fallback.
/// Fenced or malformed JSON is never surfaced verbatim to the insertion UI.
public static func parseOrFallback(
_ raw: String,
sourceText: String? = nil
) -> AIReplyVariantParsingResult? {
if let variants = parse(raw, sourceText: sourceText) {
return .variants(variants)
}
guard let text = fallbackText(from: raw),
!isSourceEcho(text, sourceText: sourceText) else {
return nil
}
return .single(
AIReplyVariant(kind: .ordinary, emotion: .neutral, text: text)
)
}
public static func fallbackText(from raw: String) -> String? {
let unfenced = removingCodeFence(from: raw)
guard !unfenced.isEmpty else { return nil }
if let data = unfenced.data(using: .utf8),
let json = try? JSONSerialization.jsonObject(with: data),
let extracted = replyText(in: json) {
return normalized(extracted)
}
if let extracted = textValueFromMalformedJSON(unfenced) {
return normalized(extracted)
}
guard !looksLikeJSON(unfenced) else { return nil }
return normalized(unfenced)
}
private static let preferredTextKeys = [
"text", "reply", "response", "content", "message", "answer"
]
private static let metadataValues = Set(
AIReplyVariant.Kind.allCases.map(\.rawValue)
+ AIReplyVariant.Emotion.allCases.map(\.rawValue)
+ ["variants", "kind", "emotion", "text"]
)
/// Rejects responses that merely report the clipboard message back to its
/// sender. A bounded LCS catches close paraphrases while still allowing a
/// reply to mention a necessary name or short keyword.
private static func isSourceEcho(
_ candidate: String,
sourceText: String?
) -> Bool {
guard let sourceText else { return false }
let source = normalizedComparisonCharacters(sourceText)
let reply = normalizedComparisonCharacters(candidate)
guard source.count >= 8, reply.count >= 8 else { return false }
let overlap = longestCommonSubsequenceLength(source, reply)
guard overlap >= 8 else { return false }
return Double(overlap) / Double(source.count) >= 0.68
&& Double(overlap) / Double(reply.count) >= 0.45
}
private static func normalizedComparisonCharacters(
_ text: String
) -> [Character] {
Array(
text.lowercased().filter { character in
character.unicodeScalars.contains {
CharacterSet.alphanumerics.contains($0)
}
}.prefix(1_000)
)
}
private static func longestCommonSubsequenceLength(
_ lhs: [Character],
_ rhs: [Character]
) -> Int {
let shorter: [Character]
let longer: [Character]
if lhs.count <= rhs.count {
shorter = lhs
longer = rhs
} else {
shorter = rhs
longer = lhs
}
var previous = Array(repeating: 0, count: shorter.count + 1)
var current = previous
for longCharacter in longer {
for index in shorter.indices {
if longCharacter == shorter[index] {
current[index + 1] = previous[index] + 1
} else {
current[index + 1] = max(
previous[index + 1],
current[index]
)
}
}
swap(&previous, &current)
current = Array(repeating: 0, count: shorter.count + 1)
}
return previous[shorter.count]
}
private static func replyText(in value: Any) -> String? {
if let object = value as? [String: Any] {
for key in preferredTextKeys {
if let text = object[key] as? String,
let normalized = normalized(text) {
return normalized
}
}
for nested in object.values {
if let text = replyText(in: nested) {
return text
}
}
}
if let array = value as? [Any] {
for nested in array {
if let text = replyText(in: nested) {
return text
}
}
}
if let text = value as? String,
!metadataValues.contains(text),
let normalized = normalized(text) {
return normalized
}
return nil
}
private static func removingCodeFence(from raw: String) -> String {
var lines = raw
.trimmingCharacters(in: .whitespacesAndNewlines)
.components(separatedBy: .newlines)
if lines.first?.trimmingCharacters(in: .whitespacesAndNewlines)
.hasPrefix("```") == true {
lines.removeFirst()
}
if lines.last?.trimmingCharacters(in: .whitespacesAndNewlines) == "```" {
lines.removeLast()
}
return lines
.joined(separator: "\n")
.trimmingCharacters(in: .whitespacesAndNewlines)
}
private static func textValueFromMalformedJSON(_ raw: String) -> String? {
let keyPattern = #""(?:text|reply|response|content|message|answer)"\s*:\s*("(?:\\.|[^"\\])*")"#
if let regex = try? NSRegularExpression(pattern: keyPattern),
let match = regex.firstMatch(
in: raw,
range: NSRange(raw.startIndex..., in: raw)
),
let range = Range(match.range(at: 1), in: raw) {
return decodeJSONString(String(raw[range]))
}
let stringPattern = #""(?:\\.|[^"\\])*""#
guard let regex = try? NSRegularExpression(pattern: stringPattern) else {
return nil
}
let candidates = regex.matches(
in: raw,
range: NSRange(raw.startIndex..., in: raw)
).compactMap { match -> String? in
guard let range = Range(match.range, in: raw),
let decoded = decodeJSONString(String(raw[range])),
!metadataValues.contains(decoded) else {
return nil
}
return normalized(decoded)
}
return candidates.max { $0.count < $1.count }
}
private static func decodeJSONString(_ quoted: String) -> String? {
guard let data = quoted.data(using: .utf8) else { return nil }
return try? JSONDecoder().decode(String.self, from: data)
}
private static func looksLikeJSON(_ value: String) -> Bool {
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.hasPrefix("{") || trimmed.hasPrefix("[")
}
private static func normalized(_ value: String) -> String? {
let text = value.trimmingCharacters(in: .whitespacesAndNewlines)
return text.isEmpty ? nil : text
}
}
@@ -70,6 +70,10 @@ public struct AISessionState: Equatable, Sendable {
public private(set) var conversationID: UUID?
public private(set) var activeUtteranceID: UUID?
public private(set) var answer: AIAnswer?
/// Three reply choices retained until the user taps one whole card.
public private(set) var replyVariants: [AIReplyVariant]
/// Stable selection hook for future local feedback recording.
public private(set) var selectedReplyVariant: AIReplyVariant?
/// Live LLM draft while `phase == .generating`. Cleared on final/cancel.
public private(set) var draftAnswerText: String?
public private(set) var transcript: String
@@ -82,6 +86,8 @@ public struct AISessionState: Equatable, Sendable {
conversationID: UUID? = nil,
activeUtteranceID: UUID? = nil,
answer: AIAnswer? = nil,
replyVariants: [AIReplyVariant] = [],
selectedReplyVariant: AIReplyVariant? = nil,
draftAnswerText: String? = nil,
transcript: String = "",
errorMessage: String? = nil
@@ -90,6 +96,8 @@ public struct AISessionState: Equatable, Sendable {
self.conversationID = conversationID
self.activeUtteranceID = activeUtteranceID
self.answer = answer
self.replyVariants = replyVariants
self.selectedReplyVariant = selectedReplyVariant
self.draftAnswerText = draftAnswerText
self.transcript = transcript
self.errorMessage = errorMessage
@@ -110,6 +118,13 @@ public struct AISessionState: Equatable, Sendable {
phase == .ready && answer?.deliveryState == .ready
}
public var canSelectReplyVariant: Bool {
phase == .ready
&& answer == nil
&& selectedReplyVariant == nil
&& replyVariants.count == AIReplyVariant.Kind.allCases.count
}
public var canSend: Bool {
phase == .awaitingSend && answer?.deliveryState == .awaitingSend
}
@@ -172,12 +187,49 @@ public struct AISessionState: Equatable, Sendable {
public mutating func receiveAnswer(_ text: String, utteranceID: UUID) {
guard isActive, activeUtteranceID == utteranceID else { return }
answer = AIAnswer(text: text)
replyVariants = []
selectedReplyVariant = nil
draftAnswerText = nil
phase = .ready
activeUtteranceID = nil
errorMessage = nil
}
public mutating func receiveReplyVariants(
_ variants: [AIReplyVariant],
utteranceID: UUID
) {
guard isActive, activeUtteranceID == utteranceID else { return }
let kinds = Set(variants.map(\.kind))
guard variants.count == AIReplyVariant.Kind.allCases.count,
kinds == Set(AIReplyVariant.Kind.allCases) else {
return
}
answer = nil
replyVariants = AIReplyVariant.Kind.allCases.compactMap { kind in
variants.first { $0.kind == kind }
}
selectedReplyVariant = nil
draftAnswerText = nil
phase = .ready
activeUtteranceID = nil
errorMessage = nil
}
/// Converts the tapped choice into the existing insertion model. Keeping
/// the selected variant exposes kind/emotion for later feedback recording.
@discardableResult
public mutating func selectReplyVariant(id: UUID) -> AIAnswer? {
guard canSelectReplyVariant,
let variant = replyVariants.first(where: { $0.id == id }) else {
return nil
}
let selectedAnswer = AIAnswer(id: variant.id, text: variant.text)
selectedReplyVariant = variant
answer = selectedAnswer
return selectedAnswer
}
public mutating func markAnswerInserted(offersSend: Bool) {
guard canInsert else { return }
answer?.markInserted(offersSend: offersSend)
@@ -195,6 +247,8 @@ public struct AISessionState: Equatable, Sendable {
public mutating func discardReadyAnswer() {
guard phase == .ready else { return }
answer = nil
replyVariants = []
selectedReplyVariant = nil
activeUtteranceID = nil
draftAnswerText = nil
transcript = ""
@@ -233,6 +287,9 @@ public struct AISessionState: Equatable, Sendable {
}
private var restingPhase: Phase {
if !replyVariants.isEmpty, selectedReplyVariant == nil {
return .ready
}
guard let answer else { return .idle }
switch answer.deliveryState {
case .ready:
@@ -44,6 +44,7 @@ public struct AppGroupConfiguration: Sendable, Equatable {
public static let keyboardHapticIntensity = "config.keyboardHapticIntensity"
public static let polishIntensity = "config.polishIntensity"
public static let aiResponseLength = "config.aiResponseLength"
public static let multipleReplyVariantsEnabled = "config.multipleReplyVariantsEnabled"
public static let llmThinkingEnabled = "config.llmThinkingEnabled"
/// When true, the keyboard records system clipboard text into local history.
public static let clipboardHistoryEnabled = "config.clipboardHistoryEnabled"
@@ -113,6 +114,8 @@ public struct AppGroupConfiguration: Sendable, Equatable {
public var polishIntensity: PolishIntensity
/// Soft AI-mode answer length preference (medium by default).
public var aiResponseLength: AIResponseLength
/// When true, Reply may provide several sendable variants.
public var multipleReplyVariantsEnabled: Bool
/// Enables provider-specific reasoning / thinking controls for polish LLM requests.
public var llmThinkingEnabled: Bool
/// Opt-in clipboard history capture in the keyboard extension.
@@ -314,6 +317,12 @@ public struct AppGroupConfiguration: Sendable, Equatable {
aiResponseLength: AIResponseLength.resolve(
storedRawValue: defaults.string(forKey: Keys.aiResponseLength)
),
multipleReplyVariantsEnabled: {
if defaults.object(forKey: Keys.multipleReplyVariantsEnabled) == nil {
return true
}
return defaults.bool(forKey: Keys.multipleReplyVariantsEnabled)
}(),
llmThinkingEnabled: defaults.bool(forKey: Keys.llmThinkingEnabled),
clipboardHistoryEnabled: defaults.bool(forKey: Keys.clipboardHistoryEnabled),
clipboardCandidateBarEnabled: defaults.bool(forKey: Keys.clipboardCandidateBarEnabled),
@@ -458,6 +467,7 @@ public struct AppGroupConfiguration: Sendable, Equatable {
defaults.set(keyboardHapticIntensity.rawValue, forKey: Keys.keyboardHapticIntensity)
defaults.set(polishIntensity.rawValue, forKey: Keys.polishIntensity)
defaults.set(aiResponseLength.rawValue, forKey: Keys.aiResponseLength)
defaults.set(multipleReplyVariantsEnabled, forKey: Keys.multipleReplyVariantsEnabled)
defaults.set(llmThinkingEnabled, forKey: Keys.llmThinkingEnabled)
defaults.set(clipboardHistoryEnabled, forKey: Keys.clipboardHistoryEnabled)
defaults.set(clipboardCandidateBarEnabled, forKey: Keys.clipboardCandidateBarEnabled)
@@ -538,6 +548,11 @@ public struct AppGroupConfiguration: Sendable, Equatable {
previous: baseline.aiResponseLength.rawValue,
key: Keys.aiResponseLength
)
set(
multipleReplyVariantsEnabled,
previous: baseline.multipleReplyVariantsEnabled,
key: Keys.multipleReplyVariantsEnabled
)
set(
llmThinkingEnabled,
previous: baseline.llmThinkingEnabled,
+41 -1
View File
@@ -12,12 +12,45 @@ public struct PolishStylePack: Codable, Equatable, Identifiable, Sendable {
case user
}
public struct LearningMetadata: Codable, Equatable, Sendable {
public let schemaVersion: Int
public let evidenceStatus: String
public let confidence: Double
public let asrExampleCount: Int
public let asrEffectiveCharacterCount: Int
public let replyExampleCount: Int
public let replyFinalEditCount: Int
public let generatedAt: Date
public init(
schemaVersion: Int,
evidenceStatus: String,
confidence: Double,
asrExampleCount: Int,
asrEffectiveCharacterCount: Int,
replyExampleCount: Int,
replyFinalEditCount: Int,
generatedAt: Date
) {
self.schemaVersion = schemaVersion
self.evidenceStatus = evidenceStatus
self.confidence = confidence
self.asrExampleCount = asrExampleCount
self.asrEffectiveCharacterCount = asrEffectiveCharacterCount
self.replyExampleCount = replyExampleCount
self.replyFinalEditCount = replyFinalEditCount
self.generatedAt = generatedAt
}
}
public let id: String
public var name: String
public var prompt: String
/// When true, polish may keep model-added emoji and the prompt overrides R5.
/// Defaults off so existing / builtin styles stay emoji-strict.
public var allowsAddedEmoji: Bool
/// Optional learning provenance. It is never included in the runtime prompt.
public var learningMetadata: LearningMetadata?
public let kind: Kind
public let createdAt: Date
public var updatedAt: Date
@@ -27,6 +60,7 @@ public struct PolishStylePack: Codable, Equatable, Identifiable, Sendable {
name: String,
prompt: String,
allowsAddedEmoji: Bool = false,
learningMetadata: LearningMetadata? = nil,
kind: Kind = .user,
createdAt: Date = Date(),
updatedAt: Date? = nil
@@ -35,6 +69,7 @@ public struct PolishStylePack: Codable, Equatable, Identifiable, Sendable {
self.name = name
self.prompt = prompt
self.allowsAddedEmoji = allowsAddedEmoji
self.learningMetadata = learningMetadata
self.kind = kind
self.createdAt = createdAt
self.updatedAt = updatedAt ?? createdAt
@@ -70,7 +105,7 @@ public struct PolishStylePack: Codable, Equatable, Identifiable, Sendable {
}
private enum CodingKeys: String, CodingKey {
case id, name, prompt, allowsAddedEmoji, kind, createdAt, updatedAt
case id, name, prompt, allowsAddedEmoji, learningMetadata, kind, createdAt, updatedAt
}
public init(from decoder: Decoder) throws {
@@ -80,6 +115,11 @@ public struct PolishStylePack: Codable, Equatable, Identifiable, Sendable {
prompt = try container.decode(String.self, forKey: .prompt)
// Older synced packs omit the key stay emoji-strict.
allowsAddedEmoji = try container.decodeIfPresent(Bool.self, forKey: .allowsAddedEmoji) ?? false
// V1 packs have no learning provenance and remain fully decodable.
learningMetadata = try container.decodeIfPresent(
LearningMetadata.self,
forKey: .learningMetadata
)
kind = try container.decode(Kind.self, forKey: .kind)
createdAt = try container.decode(Date.self, forKey: .createdAt)
updatedAt = try container.decode(Date.self, forKey: .updatedAt)
@@ -251,6 +251,18 @@ public final class ProviderConfig: ObservableObject, @unchecked Sendable {
}
}
/// Whether Reply may return several sendable variants. Default is on.
@Published public var multipleReplyVariantsEnabled: Bool {
didSet {
guard !isApplyingConfiguration,
multipleReplyVariantsEnabled != configuration.multipleReplyVariantsEnabled else {
return
}
configuration.multipleReplyVariantsEnabled = multipleReplyVariantsEnabled
persistConfiguration(postConfigChanged: true)
}
}
/// Whether the pipeline should run translate-and-polish (not just
/// polish). Both engines honour the selected target locale.
public var isTranslationEffective: Bool {
@@ -438,6 +450,7 @@ public final class ProviderConfig: ObservableObject, @unchecked Sendable {
keyboardHapticIntensity = configuration.keyboardHapticIntensity
polishIntensity = configuration.polishIntensity
aiResponseLength = configuration.aiResponseLength
multipleReplyVariantsEnabled = configuration.multipleReplyVariantsEnabled
llmThinkingEnabled = configuration.llmThinkingEnabled
clipboardHistoryEnabled = configuration.clipboardHistoryEnabled
clipboardCandidateBarEnabled = configuration.clipboardCandidateBarEnabled
@@ -469,6 +482,7 @@ public final class ProviderConfig: ObservableObject, @unchecked Sendable {
keyboardHapticIntensity = .default
polishIntensity = .default
aiResponseLength = .default
multipleReplyVariantsEnabled = true
localASRCustomLanguageModelEnabled = true
llmThinkingEnabled = false
clipboardHistoryEnabled = false
@@ -485,6 +499,7 @@ public final class ProviderConfig: ObservableObject, @unchecked Sendable {
configuration.keyboardHapticIntensity = .default
configuration.polishIntensity = .default
configuration.aiResponseLength = .default
configuration.multipleReplyVariantsEnabled = true
configuration.localASRCustomLanguageModelEnabled = true
configuration.llmThinkingEnabled = false
configuration.clipboardHistoryEnabled = false
@@ -543,6 +558,7 @@ public final class ProviderConfig: ObservableObject, @unchecked Sendable {
keyboardHapticIntensity = fresh.keyboardHapticIntensity
polishIntensity = fresh.polishIntensity
aiResponseLength = fresh.aiResponseLength
multipleReplyVariantsEnabled = fresh.multipleReplyVariantsEnabled
llmThinkingEnabled = fresh.llmThinkingEnabled
clipboardHistoryEnabled = fresh.clipboardHistoryEnabled
clipboardCandidateBarEnabled = fresh.clipboardCandidateBarEnabled
@@ -29,6 +29,7 @@ public struct SyncedAppSettingsV2: Codable, Equatable, Sendable {
public var keyboardHapticIntensity: SyncedField<KeyboardHapticIntensity>
public var polishIntensity: SyncedField<PolishIntensity>
public var aiResponseLength: SyncedField<AIResponseLength>
public var multipleReplyVariantsEnabled: SyncedField<Bool>
public var activePolishStyleId: SyncedField<String>
public var llmThinkingEnabled: SyncedField<Bool>
public var flowSkipAppSwitch: SyncedField<Bool>
@@ -53,6 +54,7 @@ public struct SyncedAppSettingsV2: Codable, Equatable, Sendable {
keyboardHapticIntensity: SyncedField<KeyboardHapticIntensity>,
polishIntensity: SyncedField<PolishIntensity>? = nil,
aiResponseLength: SyncedField<AIResponseLength>? = nil,
multipleReplyVariantsEnabled: SyncedField<Bool>? = nil,
activePolishStyleId: SyncedField<String>,
llmThinkingEnabled: SyncedField<Bool>,
clipboardHistoryEnabled: SyncedField<Bool>? = nil,
@@ -86,6 +88,11 @@ public struct SyncedAppSettingsV2: Codable, Equatable, Sendable {
updatedAt: keyboardHapticIntensity.updatedAt,
deviceID: keyboardHapticIntensity.deviceID
)
self.multipleReplyVariantsEnabled = multipleReplyVariantsEnabled ?? SyncedField(
value: true,
updatedAt: keyboardHapticIntensity.updatedAt,
deviceID: keyboardHapticIntensity.deviceID
)
self.activePolishStyleId = activePolishStyleId
self.llmThinkingEnabled = llmThinkingEnabled
// Kept as optional parameters so old call sites and payload fixtures
@@ -115,6 +122,7 @@ public struct SyncedAppSettingsV2: Codable, Equatable, Sendable {
case keyboardHapticIntensity
case polishIntensity
case aiResponseLength
case multipleReplyVariantsEnabled
case activePolishStyleId
case llmThinkingEnabled
case clipboardHistoryEnabled
@@ -174,6 +182,14 @@ public struct SyncedAppSettingsV2: Codable, Equatable, Sendable {
updatedAt: keyboardHapticIntensity.updatedAt,
deviceID: keyboardHapticIntensity.deviceID
)
multipleReplyVariantsEnabled = try container.decodeIfPresent(
SyncedField<Bool>.self,
forKey: .multipleReplyVariantsEnabled
) ?? SyncedField(
value: true,
updatedAt: keyboardHapticIntensity.updatedAt,
deviceID: keyboardHapticIntensity.deviceID
)
activePolishStyleId = try container.decodeIfPresent(
SyncedField<String>.self,
forKey: .activePolishStyleId
@@ -251,6 +267,7 @@ public struct SyncedAppSettingsV2: Codable, Equatable, Sendable {
keyboardHapticIntensity.updatedAt,
polishIntensity.updatedAt,
aiResponseLength.updatedAt,
multipleReplyVariantsEnabled.updatedAt,
activePolishStyleId.updatedAt,
llmThinkingEnabled.updatedAt,
flowSkipAppSwitch.updatedAt,
@@ -278,6 +295,7 @@ public struct SyncedAppSettingsV2: Codable, Equatable, Sendable {
try container.encode(keyboardHapticIntensity, forKey: .keyboardHapticIntensity)
try container.encode(polishIntensity, forKey: .polishIntensity)
try container.encode(aiResponseLength, forKey: .aiResponseLength)
try container.encode(multipleReplyVariantsEnabled, forKey: .multipleReplyVariantsEnabled)
try container.encode(activePolishStyleId, forKey: .activePolishStyleId)
try container.encode(llmThinkingEnabled, forKey: .llmThinkingEnabled)
try container.encode(flowSkipAppSwitch, forKey: .flowSkipAppSwitch)
@@ -317,6 +335,7 @@ public extension SyncedAppSettingsV2 {
keyboardHapticIntensity: field(configuration.keyboardHapticIntensity),
polishIntensity: field(configuration.polishIntensity),
aiResponseLength: field(configuration.aiResponseLength),
multipleReplyVariantsEnabled: field(configuration.multipleReplyVariantsEnabled),
activePolishStyleId: field(configuration.activePolishStyleId),
llmThinkingEnabled: field(configuration.llmThinkingEnabled),
flowSkipAppSwitch: field(configuration.flowSkipAppSwitch),
@@ -349,6 +368,7 @@ public extension SyncedAppSettingsV2 {
keyboardHapticIntensity: field(KeyboardHapticIntensity.default),
polishIntensity: field(PolishIntensity.default),
aiResponseLength: field(AIResponseLength.default),
multipleReplyVariantsEnabled: field(true),
activePolishStyleId: field(PolishStylePackCatalog.defaultID),
llmThinkingEnabled: field(false),
flowSkipAppSwitch: field(legacy.flowSkipAppSwitch),
@@ -393,6 +413,10 @@ public extension SyncedAppSettingsV2 {
local: local.aiResponseLength,
remote: remote.aiResponseLength
),
multipleReplyVariantsEnabled: .merge(
local: local.multipleReplyVariantsEnabled,
remote: remote.multipleReplyVariantsEnabled
),
activePolishStyleId: .merge(
local: local.activePolishStyleId,
remote: remote.activePolishStyleId
@@ -424,6 +448,7 @@ public extension SyncedAppSettingsV2 {
configuration.keyboardHapticIntensity = keyboardHapticIntensity.value
configuration.polishIntensity = polishIntensity.value
configuration.aiResponseLength = aiResponseLength.value
configuration.multipleReplyVariantsEnabled = multipleReplyVariantsEnabled.value
configuration.activePolishStyleId = activePolishStyleId.value
configuration.llmThinkingEnabled = llmThinkingEnabled.value
configuration.flowSkipAppSwitch = flowSkipAppSwitch.value
@@ -454,6 +479,7 @@ public extension SyncedAppSettingsV2 {
patch(&copy.keyboardHapticIntensity, value: configuration.keyboardHapticIntensity)
patch(&copy.polishIntensity, value: configuration.polishIntensity)
patch(&copy.aiResponseLength, value: configuration.aiResponseLength)
patch(&copy.multipleReplyVariantsEnabled, value: configuration.multipleReplyVariantsEnabled)
patch(&copy.activePolishStyleId, value: configuration.activePolishStyleId)
patch(&copy.llmThinkingEnabled, value: configuration.llmThinkingEnabled)
patch(&copy.flowSkipAppSwitch, value: configuration.flowSkipAppSwitch)
@@ -487,6 +513,7 @@ public extension SyncedAppSettingsV2 {
touch(&copy.keyboardHapticIntensity, value: configuration.keyboardHapticIntensity)
touch(&copy.polishIntensity, value: configuration.polishIntensity)
touch(&copy.aiResponseLength, value: configuration.aiResponseLength)
touch(&copy.multipleReplyVariantsEnabled, value: configuration.multipleReplyVariantsEnabled)
touch(&copy.activePolishStyleId, value: configuration.activePolishStyleId)
touch(&copy.llmThinkingEnabled, value: configuration.llmThinkingEnabled)
touch(&copy.flowSkipAppSwitch, value: configuration.flowSkipAppSwitch)