feat(keyboard): unify assistant voice and AI workflows
Merge dictation and AI controls into one assistant surface, preserve safe insertion and clipboard actions, and align settings and tests with the new flow.
This commit is contained in:
@@ -190,6 +190,18 @@ public struct AISessionState: Equatable, Sendable {
|
||||
phase = .sent
|
||||
}
|
||||
|
||||
/// Drops a result retained because the insertion target changed.
|
||||
/// The conversation remains active so the user can immediately ask again.
|
||||
public mutating func discardReadyAnswer() {
|
||||
guard phase == .ready else { return }
|
||||
answer = nil
|
||||
activeUtteranceID = nil
|
||||
draftAnswerText = nil
|
||||
transcript = ""
|
||||
errorMessage = nil
|
||||
phase = .idle
|
||||
}
|
||||
|
||||
public mutating func cancelCurrentWork() {
|
||||
guard isBusy else { return }
|
||||
activeUtteranceID = nil
|
||||
|
||||
@@ -60,6 +60,22 @@ public struct EditableInputReference: Codable, Equatable, Sendable {
|
||||
now >= expiresAt
|
||||
}
|
||||
|
||||
/// Same-process verification uses the exact insertion kept in memory.
|
||||
/// This path intentionally does not depend on the field fingerprint,
|
||||
/// because UITextDocumentProxy may publish that context one callback after
|
||||
/// the insertion while the in-memory record is already authoritative.
|
||||
public func matchesLiveInsertion(
|
||||
extensionInstanceID currentInstanceID: UUID,
|
||||
lastInsertedText: String?,
|
||||
contextBeforeInput: String?
|
||||
) -> Bool {
|
||||
!isExpired()
|
||||
&& isWithinLengthBudget
|
||||
&& extensionInstanceID == currentInstanceID
|
||||
&& lastInsertedText == insertedText
|
||||
&& contextBeforeInput?.hasSuffix(insertedText) == true
|
||||
}
|
||||
|
||||
/// Rebuilt extensions must match the complete inserted string at the caret
|
||||
/// and, when captured, the same field fingerprint; a suffix sample is insufficient.
|
||||
public func isFullyVerified(
|
||||
|
||||
@@ -8,6 +8,8 @@ import CoreGraphics
|
||||
public enum KeyboardChromeLayout {
|
||||
public static let totalHeight: CGFloat = 281
|
||||
public static let actionKeyHeight: CGFloat = 50
|
||||
/// Shared capsule action height for unified Send and edit-mode controls.
|
||||
public static let assistantActionCapsuleHeight: CGFloat = actionKeyHeight
|
||||
public static let actionKeyCornerRadius: CGFloat = 10
|
||||
/// Shared spacing for every bottom action row. iPad uses a custom globe
|
||||
/// slot; iPhone relies on the system-provided switch below the keyboard.
|
||||
|
||||
@@ -216,15 +216,16 @@ public final class TypingInputConfiguration: ObservableObject {
|
||||
let store = defaults ?? AppGroup.defaultsIfAvailable
|
||||
guard let store else { return (.voice, nil) }
|
||||
|
||||
// AI is an explicit product surface. Restore it as an empty temporary
|
||||
// conversation even when the general "remember surface" toggle is off.
|
||||
// Older builds persisted a dedicated AI surface. It now migrates to
|
||||
// the unified assistant surface.
|
||||
if store.string(forKey: Key.lastSurface) == KeyboardState.Surface.ai.rawValue {
|
||||
return (.ai, nil)
|
||||
return (.voice, nil)
|
||||
}
|
||||
|
||||
if store.bool(forKey: Key.rememberLastSurface),
|
||||
let raw = store.string(forKey: Key.lastSurface),
|
||||
let surface = KeyboardState.Surface(rawValue: raw) {
|
||||
let persistedSurface = KeyboardState.Surface(rawValue: raw) {
|
||||
let surface: KeyboardState.Surface = persistedSurface == .ai ? .voice : persistedSurface
|
||||
let language: TypingInputLanguage? = surface == .typing
|
||||
? persistedTypingLanguage(defaults: store) ?? .chinese
|
||||
: nil
|
||||
|
||||
Reference in New Issue
Block a user