feat(keyboard): expand contextual skills and managed flows
Add local clipboard intent recommendations, webpage and phone actions, and safer host handoffs. Refine managed gateway, catalog refresh, onboarding, and adaptive polish behavior.
This commit is contained in:
@@ -43,8 +43,12 @@ public struct AIAgentSkillLayout: Codable, Equatable, Sendable {
|
||||
) -> AIAgentSkillLayout {
|
||||
let known = Dictionary(uniqueKeysWithValues: catalog.map { ($0.id, $0) })
|
||||
var seenEnabled = Set<String>()
|
||||
let enabled = enabledIDs.filter { id in
|
||||
known[id] != nil && seenEnabled.insert(id).inserted
|
||||
let enabled = enabledIDs.compactMap { id -> String? in
|
||||
let canonical = AIClipboardSkillCatalog.canonicalID(for: id)
|
||||
guard known[canonical] != nil, seenEnabled.insert(canonical).inserted else {
|
||||
return nil
|
||||
}
|
||||
return canonical
|
||||
}
|
||||
|
||||
var seenConfirmed = Set<String>()
|
||||
|
||||
@@ -46,6 +46,9 @@ public struct AIHintCard: Codable, Equatable, Identifiable, Sendable {
|
||||
public var locale: String
|
||||
public var conditions: [String]
|
||||
public var metadata: AIHintMetadata?
|
||||
/// Server policy intent. Ordinary cards match hold-to-talk AI; current
|
||||
/// information cards additionally require online search.
|
||||
public var taskKind: ManagedGatewayTaskKind
|
||||
|
||||
public init(
|
||||
id: String,
|
||||
@@ -56,7 +59,8 @@ public struct AIHintCard: Codable, Equatable, Identifiable, Sendable {
|
||||
source: String = "local",
|
||||
locale: String = "zh",
|
||||
conditions: [String] = [],
|
||||
metadata: AIHintMetadata? = nil
|
||||
metadata: AIHintMetadata? = nil,
|
||||
taskKind: ManagedGatewayTaskKind = .aiQuestion
|
||||
) {
|
||||
self.id = id
|
||||
self.displayText = displayText
|
||||
@@ -67,6 +71,7 @@ public struct AIHintCard: Codable, Equatable, Identifiable, Sendable {
|
||||
self.locale = locale
|
||||
self.conditions = conditions
|
||||
self.metadata = metadata
|
||||
self.taskKind = taskKind
|
||||
}
|
||||
|
||||
public var requiresClipboard30s: Bool {
|
||||
@@ -83,7 +88,7 @@ public struct AIHintCard: Codable, Equatable, Identifiable, Sendable {
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, displayText, text, prompt, category, priority, source, locale, conditions, metadata
|
||||
case id, displayText, text, prompt, category, priority, source, locale, conditions, metadata, taskKind
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
@@ -96,6 +101,10 @@ public struct AIHintCard: Codable, Equatable, Identifiable, Sendable {
|
||||
locale = try container.decodeIfPresent(String.self, forKey: .locale) ?? "zh"
|
||||
conditions = try container.decodeIfPresent([String].self, forKey: .conditions) ?? []
|
||||
metadata = try container.decodeIfPresent(AIHintMetadata.self, forKey: .metadata)
|
||||
taskKind = try container.decodeIfPresent(
|
||||
ManagedGatewayTaskKind.self,
|
||||
forKey: .taskKind
|
||||
) ?? .aiQuestion
|
||||
if let display = try container.decodeIfPresent(String.self, forKey: .displayText),
|
||||
!display.isEmpty {
|
||||
displayText = display
|
||||
@@ -115,6 +124,7 @@ public struct AIHintCard: Codable, Equatable, Identifiable, Sendable {
|
||||
try container.encode(locale, forKey: .locale)
|
||||
try container.encode(conditions, forKey: .conditions)
|
||||
try container.encodeIfPresent(metadata, forKey: .metadata)
|
||||
try container.encode(taskKind, forKey: .taskKind)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ public struct AppGroupConfiguration: Sendable, Equatable {
|
||||
public static let localASRCustomLanguageModelEnabled = "config.localASR.customLanguageModelEnabled"
|
||||
/// Enabled AI Agent skill IDs (order) + confirmed companion Shortcuts.
|
||||
public static let agentSkillLayout = "config.aiAgentSkills.layout.v1"
|
||||
/// One-shot: append semantic built-ins introduced with the unlimited layout.
|
||||
/// One-shot migrations for newly default-installed built-in and official skills.
|
||||
public static let agentSkillDefaultsMigrationVersion =
|
||||
"config.aiAgentSkills.defaultsMigrationVersion"
|
||||
/// User-created clipboard skills (no cloud sync; App Group only).
|
||||
|
||||
@@ -20,8 +20,8 @@ public struct FlowCommand: Codable, Equatable, Sendable {
|
||||
case submitAIQuestion
|
||||
}
|
||||
|
||||
/// Wire version that includes a strongly typed OOBE feature.
|
||||
public static let currentProtocolVersion = 8
|
||||
/// Wire version that includes host-side webpage summary extraction.
|
||||
public static let currentProtocolVersion = 10
|
||||
|
||||
public let protocolVersion: Int
|
||||
public let sessionId: UUID
|
||||
@@ -41,8 +41,12 @@ public struct FlowCommand: Codable, Equatable, Sendable {
|
||||
public let aiConversationID: UUID?
|
||||
/// Prefilled question used only by `.submitAIQuestion`.
|
||||
public let aiQuestionText: String?
|
||||
/// Public webpage fetched by the host for an explicit summary skill.
|
||||
public let aiWebPageURL: URL?
|
||||
/// Fine-grained managed-gateway intent for AI question submissions.
|
||||
public let aiTaskKind: ManagedGatewayTaskKind?
|
||||
/// Audited product entry point for managed usage attribution.
|
||||
public let managedRequestSource: ManagedGatewayRequestSource?
|
||||
/// Optional server-audited purpose for managed gateway billing policy.
|
||||
public let managedRequestPurpose: ManagedGatewayRequestPurpose?
|
||||
/// Required feature discriminator when `managedRequestPurpose == .oobe`.
|
||||
@@ -68,7 +72,9 @@ public struct FlowCommand: Codable, Equatable, Sendable {
|
||||
sourceHistoryEntryRevision: Int64? = nil,
|
||||
aiConversationID: UUID? = nil,
|
||||
aiQuestionText: String? = nil,
|
||||
aiWebPageURL: URL? = nil,
|
||||
aiTaskKind: ManagedGatewayTaskKind? = nil,
|
||||
managedRequestSource: ManagedGatewayRequestSource? = nil,
|
||||
managedRequestPurpose: ManagedGatewayRequestPurpose? = nil,
|
||||
managedOOBEFeature: ManagedGatewayOOBEFeature? = nil,
|
||||
aiThinkingEnabled: Bool? = nil,
|
||||
@@ -89,7 +95,9 @@ public struct FlowCommand: Codable, Equatable, Sendable {
|
||||
self.sourceHistoryEntryRevision = sourceHistoryEntryRevision
|
||||
self.aiConversationID = aiConversationID
|
||||
self.aiQuestionText = aiQuestionText
|
||||
self.aiWebPageURL = aiWebPageURL
|
||||
self.aiTaskKind = aiTaskKind
|
||||
self.managedRequestSource = managedRequestSource
|
||||
self.managedRequestPurpose = managedRequestPurpose
|
||||
self.managedOOBEFeature = managedOOBEFeature
|
||||
self.aiThinkingEnabled = aiThinkingEnabled
|
||||
|
||||
@@ -13,8 +13,12 @@ public struct FlowUtteranceRequest: Equatable, Sendable {
|
||||
public let aiConversationID: UUID?
|
||||
/// When set with `.aiQuestion`, host skips ASR and answers this text.
|
||||
public let aiQuestionText: String?
|
||||
/// Public HTTPS page to fetch in the host before a webpage-summary request.
|
||||
public let aiWebPageURL: URL?
|
||||
/// Fine-grained managed-gateway intent. Regular questions keep the default.
|
||||
public let aiTaskKind: ManagedGatewayTaskKind?
|
||||
/// Audited product entry point for managed usage attribution.
|
||||
public let managedRequestSource: ManagedGatewayRequestSource?
|
||||
/// Optional server-audited purpose for managed gateway billing policy.
|
||||
public let managedRequestPurpose: ManagedGatewayRequestPurpose?
|
||||
/// Required feature discriminator when `managedRequestPurpose == .oobe`.
|
||||
@@ -31,7 +35,9 @@ public struct FlowUtteranceRequest: Equatable, Sendable {
|
||||
sourceHistoryEntryRevision: Int64? = nil,
|
||||
aiConversationID: UUID? = nil,
|
||||
aiQuestionText: String? = nil,
|
||||
aiWebPageURL: URL? = nil,
|
||||
aiTaskKind: ManagedGatewayTaskKind? = nil,
|
||||
managedRequestSource: ManagedGatewayRequestSource? = nil,
|
||||
managedRequestPurpose: ManagedGatewayRequestPurpose? = nil,
|
||||
managedOOBEFeature: ManagedGatewayOOBEFeature? = nil,
|
||||
aiThinkingEnabled: Bool? = nil
|
||||
@@ -42,7 +48,9 @@ public struct FlowUtteranceRequest: Equatable, Sendable {
|
||||
self.sourceHistoryEntryRevision = sourceHistoryEntryRevision
|
||||
self.aiConversationID = aiConversationID
|
||||
self.aiQuestionText = aiQuestionText
|
||||
self.aiWebPageURL = aiWebPageURL
|
||||
self.aiTaskKind = aiTaskKind
|
||||
self.managedRequestSource = managedRequestSource
|
||||
self.managedRequestPurpose = managedRequestPurpose
|
||||
self.managedOOBEFeature = managedOOBEFeature
|
||||
self.aiThinkingEnabled = aiThinkingEnabled
|
||||
@@ -66,14 +74,18 @@ public struct FlowUtteranceRequest: Equatable, Sendable {
|
||||
conversationID: UUID,
|
||||
prefilledQuestion: String? = nil,
|
||||
taskKind: ManagedGatewayTaskKind = .aiQuestion,
|
||||
requestSource: ManagedGatewayRequestSource? = nil,
|
||||
oobeFeature: ManagedGatewayOOBEFeature? = nil,
|
||||
thinkingEnabled: Bool? = nil
|
||||
thinkingEnabled: Bool? = nil,
|
||||
webPageURL: URL? = nil
|
||||
) -> FlowUtteranceRequest {
|
||||
FlowUtteranceRequest(
|
||||
mode: .aiQuestion,
|
||||
aiConversationID: conversationID,
|
||||
aiQuestionText: prefilledQuestion,
|
||||
aiWebPageURL: webPageURL,
|
||||
aiTaskKind: taskKind,
|
||||
managedRequestSource: requestSource,
|
||||
managedRequestPurpose: oobeFeature == nil ? nil : .oobe,
|
||||
managedOOBEFeature: oobeFeature,
|
||||
aiThinkingEnabled: thinkingEnabled
|
||||
|
||||
@@ -85,7 +85,7 @@ public struct OfficialSkillDefinition: Codable, Equatable, Identifiable, Sendabl
|
||||
cardTitleKey: "",
|
||||
descriptionKey: "",
|
||||
kind: kind,
|
||||
isDefault: false,
|
||||
isDefault: true,
|
||||
customName: localization.name,
|
||||
customSummary: localization.summary,
|
||||
customPrompt: localization.prompt,
|
||||
|
||||
@@ -114,3 +114,76 @@ public enum TranslationLanguageCatalog {
|
||||
return all.first { $0.id == offLocaleId } ?? all[0]
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves the device's first preferred language for clipboard translation.
|
||||
/// This intentionally ignores the optional post-dictation translation target.
|
||||
public enum SystemLanguageResolver {
|
||||
public static func primaryIdentifier(
|
||||
preferredLanguages: [String] = Locale.preferredLanguages
|
||||
) -> String {
|
||||
normalizedIdentifier(
|
||||
preferredLanguages.first ?? Locale.autoupdatingCurrent.identifier
|
||||
)
|
||||
}
|
||||
|
||||
public static func promptLanguageName(
|
||||
preferredLanguages: [String] = Locale.preferredLanguages
|
||||
) -> String {
|
||||
let identifier = primaryIdentifier(preferredLanguages: preferredLanguages)
|
||||
if let known = TranslationLanguageCatalog.all.first(where: { $0.id == identifier }) {
|
||||
return known.promptLanguageName
|
||||
}
|
||||
return Locale(identifier: "en").localizedString(forIdentifier: identifier)
|
||||
?? identifier
|
||||
}
|
||||
|
||||
public static func displayLanguageName(
|
||||
uiLanguage: AppUILanguage,
|
||||
preferredLanguages: [String] = Locale.preferredLanguages
|
||||
) -> String {
|
||||
let identifier = primaryIdentifier(preferredLanguages: preferredLanguages)
|
||||
let displayLocale = Locale(identifier: uiLanguage.resolvedLanguageCode())
|
||||
return displayLocale.localizedString(forIdentifier: identifier)
|
||||
?? promptLanguageName(preferredLanguages: preferredLanguages)
|
||||
}
|
||||
|
||||
public static func isSameLanguage(
|
||||
sourceIdentifier: String,
|
||||
targetIdentifier: String
|
||||
) -> Bool {
|
||||
let source = normalizedIdentifier(sourceIdentifier)
|
||||
let target = normalizedIdentifier(targetIdentifier)
|
||||
if source.hasPrefix("zh"), target.hasPrefix("zh") {
|
||||
let sourceScript = chineseScript(in: source)
|
||||
let targetScript = chineseScript(in: target)
|
||||
return sourceScript == nil || targetScript == nil || sourceScript == targetScript
|
||||
}
|
||||
return source == target
|
||||
}
|
||||
|
||||
private static func normalizedIdentifier(_ identifier: String) -> String {
|
||||
let language = Locale.Language(identifier: identifier)
|
||||
guard let rawCode = language.languageCode?.identifier else {
|
||||
return identifier.lowercased()
|
||||
}
|
||||
let code = rawCode.lowercased()
|
||||
guard code == "zh" || code == "yue" else { return code }
|
||||
|
||||
let script = language.script?.identifier.lowercased()
|
||||
let region = Locale(identifier: identifier).region?.identifier.uppercased()
|
||||
if script == "hant" || ["HK", "MO", "TW"].contains(region) {
|
||||
return "zh-Hant"
|
||||
}
|
||||
if script == "hans" || ["CN", "MY", "SG"].contains(region) {
|
||||
return "zh-Hans"
|
||||
}
|
||||
return "zh"
|
||||
}
|
||||
|
||||
private static func chineseScript(in identifier: String) -> String? {
|
||||
let normalized = identifier.lowercased()
|
||||
if normalized.contains("hant") { return "Hant" }
|
||||
if normalized.contains("hans") { return "Hans" }
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user