feat(keyboard): ship AI mode surface with streaming search answers

Add the AI keyboard tab, Agent settings, and user-owned LLM key path for 1.7.0, including streaming answers and web-search transports without the built-in DeepSeek fallback.
This commit is contained in:
Rocky
2026-08-11 01:06:27 +08:00
parent f6212dcd2c
commit 3de665d254
81 changed files with 4467 additions and 398 deletions
@@ -0,0 +1,58 @@
// AIResponseLength.swift
// OSGKeyboard · Shared
//
// Soft response-length preference for AI keyboard mode. Guidance is
// applied through the system prompt not a hard character gate.
import Foundation
public enum AIResponseLength: String, Codable, CaseIterable, Sendable {
case short
case medium
case detailed
public static let `default`: AIResponseLength = .medium
public var labelKey: String {
switch self {
case .short: return "ai.responseLength.short"
case .medium: return "ai.responseLength.medium"
case .detailed: return "ai.responseLength.detailed"
}
}
/// Soft length guidance injected into the AI-mode system prompt.
public var promptGuidance: String {
switch self {
case .short:
return "Keep the answer brief: about 23 sentences."
case .medium:
return "Keep the answer moderately long: roughly within 500 characters."
case .detailed:
return "You may answer in more detail: roughly within 3000 characters."
}
}
public static func resolve(storedRawValue rawValue: String?) -> AIResponseLength {
switch rawValue {
case AIResponseLength.short.rawValue:
return .short
case AIResponseLength.detailed.rawValue:
return .detailed
case AIResponseLength.medium.rawValue:
return .medium
default:
return .default
}
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
self = Self.resolve(storedRawValue: try container.decode(String.self))
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(rawValue)
}
}