feat(keyboard): add Notes skill and system-style typing
Add save-to-Notes export and direct map handoff while aligning multi-touch typing, period shortcuts, return actions, and navigation visuals with system behavior.
This commit is contained in:
@@ -70,13 +70,18 @@ final class AIKeyboardCoordinator {
|
||||
func submitClipboardSkill(_ skill: AIClipboardSkill) {
|
||||
guard canAcceptIdleSubmit else { return }
|
||||
enterIfNeeded()
|
||||
state.pendingClipboardSkillID = skill.kind == .export ? skill.id : nil
|
||||
let material = ClipboardHistoryStore.shared.newestAIHintEligibleEntry()?.text
|
||||
if skill.kind == .export {
|
||||
state.pendingClipboardSkillID = skill.id
|
||||
state.pendingClipboardSkillSource = material
|
||||
} else {
|
||||
clearPendingExportSkill()
|
||||
}
|
||||
let instruction = AIClipboardSkillCatalog.instruction(
|
||||
for: skill,
|
||||
locale: AIHintLocaleResolver.packLocale(),
|
||||
translationTargetLocaleId: state.translationTargetLocaleId
|
||||
)
|
||||
let material = ClipboardHistoryStore.shared.newestAIHintEligibleEntry()?.text
|
||||
AIAgentShortcutRun.trace("keyboard.submit skill=\(skill.id) kind=\(skill.kind)")
|
||||
if let material {
|
||||
AIAgentShortcutRun.traceBody("keyboard.clipboard", material)
|
||||
@@ -120,7 +125,7 @@ final class AIKeyboardCoordinator {
|
||||
) {
|
||||
guard case .ready(let prompt) = resolution else {
|
||||
// The clipboard window closed between rendering and this tap.
|
||||
state.pendingClipboardSkillID = nil
|
||||
clearPendingExportSkill()
|
||||
state.aiSession.fail(
|
||||
ExtL10n.string("keyboard.ai.error.clipboardUnavailable"),
|
||||
utteranceID: nil
|
||||
@@ -128,7 +133,7 @@ final class AIKeyboardCoordinator {
|
||||
return
|
||||
}
|
||||
guard let conversationID = state.aiSession.conversationID else {
|
||||
state.pendingClipboardSkillID = nil
|
||||
clearPendingExportSkill()
|
||||
return
|
||||
}
|
||||
let disposition = flow.submitAIQuestion(
|
||||
@@ -137,14 +142,14 @@ final class AIKeyboardCoordinator {
|
||||
thinkingEnabled: thinkingEnabled
|
||||
)
|
||||
if case .rejected(let rejection) = disposition {
|
||||
state.pendingClipboardSkillID = nil
|
||||
clearPendingExportSkill()
|
||||
state.aiSession.fail(message(for: rejection), utteranceID: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
guard state.aiSession.isBusy else { return }
|
||||
state.pendingClipboardSkillID = nil
|
||||
clearPendingExportSkill()
|
||||
flow.cancelAIRecording()
|
||||
state.aiSession.cancelCurrentWork()
|
||||
}
|
||||
@@ -153,7 +158,7 @@ final class AIKeyboardCoordinator {
|
||||
if state.aiSession.canInsert, let answer = state.aiSession.answer {
|
||||
guard insertAnswer(answer) else { return }
|
||||
state.aiSession.markAnswerInserted(
|
||||
offersSend: state.returnKeyRole == .send
|
||||
offersSend: state.returnKeyRole.usesActionFill
|
||||
)
|
||||
} else if state.aiSession.canSend {
|
||||
state.aiSession.markAnswerSent()
|
||||
@@ -228,16 +233,21 @@ final class AIKeyboardCoordinator {
|
||||
}
|
||||
|
||||
func fail(_ message: String, utteranceID: UUID?) {
|
||||
state.pendingClipboardSkillID = nil
|
||||
clearPendingExportSkill()
|
||||
state.aiSession.fail(message, utteranceID: utteranceID)
|
||||
}
|
||||
|
||||
private func endConversationIfNeeded() {
|
||||
state.pendingClipboardSkillID = nil
|
||||
clearPendingExportSkill()
|
||||
guard let conversationID = state.aiSession.conversationID else { return }
|
||||
flow.endAIConversation(conversationID)
|
||||
}
|
||||
|
||||
private func clearPendingExportSkill() {
|
||||
state.pendingClipboardSkillID = nil
|
||||
state.pendingClipboardSkillSource = nil
|
||||
}
|
||||
|
||||
private var isPendingExportSkill: Bool {
|
||||
guard let id = state.pendingClipboardSkillID else { return false }
|
||||
return resolvedSkill(id: id)?.kind == .export
|
||||
@@ -251,9 +261,10 @@ final class AIKeyboardCoordinator {
|
||||
}
|
||||
|
||||
/// Parse an export skill. Empty → in-keyboard tip, stay in the host app.
|
||||
/// Lines → hand off to the host to run the companion Shortcut.
|
||||
/// Lines → hand off to the host (Shortcut, Maps, or Didi).
|
||||
private func finishExportSkill(answer: String) {
|
||||
let source = ClipboardHistoryStore.shared.newestAIHintEligibleEntry()?.text
|
||||
let source = state.pendingClipboardSkillSource
|
||||
?? ClipboardHistoryStore.shared.newestAIHintEligibleEntry()?.text
|
||||
let skillID = state.pendingClipboardSkillID
|
||||
let items: [String]
|
||||
let emptyTipKey: String
|
||||
@@ -267,6 +278,13 @@ final class AIKeyboardCoordinator {
|
||||
case AIClipboardSkillCatalog.navigateID:
|
||||
items = AIAddressExtraction.lines(from: answer, sourceClipboard: source)
|
||||
emptyTipKey = "keyboard.ai.skill.noAddress"
|
||||
case AIClipboardSkillCatalog.saveToNotesID:
|
||||
items = AINoteExport.items(
|
||||
from: answer,
|
||||
sourceClipboard: source,
|
||||
locale: AIHintLocaleResolver.packLocale()
|
||||
)
|
||||
emptyTipKey = "keyboard.ai.skill.noNote"
|
||||
default:
|
||||
items = AIGenericSkillExport.items(from: answer)
|
||||
emptyTipKey = "keyboard.ai.skill.noExportItems"
|
||||
@@ -280,7 +298,7 @@ final class AIKeyboardCoordinator {
|
||||
AIAgentShortcutRun.traceBody("keyboard.parsedTitles", items.joined(separator: "\n"))
|
||||
}
|
||||
#endif
|
||||
state.pendingClipboardSkillID = nil
|
||||
clearPendingExportSkill()
|
||||
if state.aiSession.isBusy {
|
||||
state.aiSession.cancelCurrentWork()
|
||||
}
|
||||
@@ -292,14 +310,25 @@ final class AIKeyboardCoordinator {
|
||||
state.skillTipText = ExtL10n.string(emptyTipKey)
|
||||
return
|
||||
}
|
||||
guard let skillID,
|
||||
resolvedSkill(id: skillID)?.shortcutName != nil else {
|
||||
AIAgentShortcutRun.trace("keyboard.parse missingShortcut skill=\(skillID ?? "nil")")
|
||||
guard let skillID, let skill = resolvedSkill(id: skillID) else {
|
||||
AIAgentShortcutRun.trace("keyboard.parse missingSkill")
|
||||
state.skillTipText = ExtL10n.string("keyboard.ai.skill.shortcutMissing")
|
||||
return
|
||||
}
|
||||
if skill.requiresShortcut, skill.shortcutName == nil {
|
||||
AIAgentShortcutRun.trace("keyboard.parse missingShortcut skill=\(skillID)")
|
||||
state.skillTipText = ExtL10n.string("keyboard.ai.skill.shortcutMissing")
|
||||
return
|
||||
}
|
||||
AIAgentShortcutRun.trace("keyboard.handoffToHost skill=\(skillID) items=\(items.count)")
|
||||
state.skillTipText = ExtL10n.string("keyboard.ai.skill.runningShortcut")
|
||||
let tipKey: String
|
||||
switch skillID {
|
||||
case AIClipboardSkillCatalog.navigateID:
|
||||
tipKey = "keyboard.ai.skill.openingMaps"
|
||||
default:
|
||||
tipKey = "keyboard.ai.skill.runningShortcut"
|
||||
}
|
||||
state.skillTipText = ExtL10n.string(tipKey)
|
||||
state.runClipboardExportSkill(skillID, items)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user