5d7fcdf24e
Keep marketing version at 1.6.6 and bump build to 59. Strengthen never-answer polish safeguards, clipboard reply-intent continuity, voice undo UX, device UITests harness, eval fixtures, and What's New assets.
194 lines
5.6 KiB
Swift
194 lines
5.6 KiB
Swift
// ClipboardPreparingPolicy.swift
|
|
// OSGKeyboard · Shared
|
|
//
|
|
// Pure decisions for clipboard「准备录音…」so paste-alert restore / double-start
|
|
// / host-failure recovery stay hermetic and regression-tested.
|
|
|
|
import Foundation
|
|
|
|
// MARK: - Restore after paste-alert / cold-start recreate
|
|
|
|
public enum ClipboardRestoreAction: Equatable, Sendable {
|
|
/// Mid-flight claim exists — reattach preparing/recording, never pressBegan again.
|
|
case awaitExistingStart
|
|
/// Intent exists but start is not issued — resume acquisition / host warm-up automatically.
|
|
case resumeIntent
|
|
/// Already in a live clipboard phase — only refresh UI / recover.
|
|
case refreshOnly
|
|
}
|
|
|
|
/// Whether a clipboard intent may start now or must warm the host first.
|
|
public enum ClipboardHostGateAction: Equatable, Sendable {
|
|
case startRecordingNow
|
|
case openHostColdStart
|
|
case waitForHost
|
|
case ignore
|
|
}
|
|
|
|
/// Mic chrome while a clipboard round is live.
|
|
public enum ClipboardMicChrome: Equatable, Sendable {
|
|
/// Grey / spinner / tappable to cancel — acquiring paste or waiting for host.
|
|
case preparingCancelable
|
|
/// Blue recording chrome + side captions.
|
|
case recordingBlue
|
|
/// Not a clipboard recording chrome state.
|
|
case none
|
|
}
|
|
|
|
public enum ClipboardPreparingPolicy: Sendable {
|
|
|
|
public static func restoreAction(
|
|
hasStartIssued: Bool,
|
|
phase: ClipboardPreparingPhase
|
|
) -> ClipboardRestoreAction {
|
|
switch phase {
|
|
case .idle, .denied, .error:
|
|
return hasStartIssued ? .awaitExistingStart : .resumeIntent
|
|
case .requestingPermissions, .recording, .processing:
|
|
return .refreshOnly
|
|
}
|
|
}
|
|
|
|
/// Map the shared mic handoff decision onto the auto-resuming clipboard intent.
|
|
public static func hostGateAction(
|
|
micPressAction: FlowMicPressAction
|
|
) -> ClipboardHostGateAction {
|
|
switch micPressAction {
|
|
case .startRecording:
|
|
return .startRecordingNow
|
|
case .openHostColdStart:
|
|
return .openHostColdStart
|
|
case .waitForHostReady:
|
|
// The coordinator keeps the same intent and auto-records once ready.
|
|
return .waitForHost
|
|
case .ignore:
|
|
return .ignore
|
|
}
|
|
}
|
|
|
|
public static func micChrome(
|
|
isClipboardUtterance: Bool,
|
|
phase: ClipboardPreparingPhase,
|
|
awaitingHostConfirm: Bool
|
|
) -> ClipboardMicChrome {
|
|
guard isClipboardUtterance else { return .none }
|
|
switch phase {
|
|
case .requestingPermissions:
|
|
return .preparingCancelable
|
|
case .recording:
|
|
return awaitingHostConfirm ? .preparingCancelable : .recordingBlue
|
|
case .processing:
|
|
return .preparingCancelable
|
|
case .idle, .denied, .error:
|
|
return .none
|
|
}
|
|
}
|
|
|
|
// MARK: - Stop while preparing
|
|
|
|
public static func stopWhilePreparing(
|
|
awaitingHostConfirm: Bool
|
|
) -> ClipboardPreparingStopAction {
|
|
awaitingHostConfirm ? .abortPreparing : .requestStop
|
|
}
|
|
|
|
// MARK: - Host moved on while preparing
|
|
|
|
public static func recoverWhilePreparing(
|
|
awaitingHostConfirm: Bool,
|
|
currentUtteranceId: UUID?,
|
|
hostBusyUtteranceId: UUID?,
|
|
hostReason: ClipboardHostBusyReason?,
|
|
hasTerminalFailureForCurrent: Bool
|
|
) -> ClipboardPreparingRecoverAction {
|
|
guard awaitingHostConfirm else { return .none }
|
|
|
|
if hasTerminalFailureForCurrent {
|
|
return .abortForHostFailure
|
|
}
|
|
|
|
guard let hostReason, let busyId = hostBusyUtteranceId else {
|
|
return .none
|
|
}
|
|
|
|
switch hostReason {
|
|
case .recording:
|
|
if busyId == currentUtteranceId {
|
|
return .confirmRecording
|
|
}
|
|
return .adoptSibling(busyId)
|
|
case .processing:
|
|
if busyId == currentUtteranceId {
|
|
return .wait
|
|
}
|
|
return .adoptSibling(busyId)
|
|
}
|
|
}
|
|
|
|
// MARK: - Ensure at most one startRecording
|
|
|
|
public static func ensureStartAction(
|
|
issuedUtteranceId: UUID?,
|
|
isFlowRecording: Bool,
|
|
currentUtteranceId: UUID?,
|
|
hostBusyUtteranceId: UUID?,
|
|
hostReason: ClipboardHostBusyReason?,
|
|
hostReadyWithSession: Bool
|
|
) -> ClipboardEnsureStartAction {
|
|
guard let issued = issuedUtteranceId else { return .none }
|
|
|
|
if let busyId = hostBusyUtteranceId, let hostReason {
|
|
switch hostReason {
|
|
case .recording, .processing:
|
|
return .adoptBusy(busyId, hostReason)
|
|
}
|
|
}
|
|
|
|
if isFlowRecording, currentUtteranceId == issued {
|
|
return .alreadyInFlight
|
|
}
|
|
|
|
if hostReadyWithSession {
|
|
return .writeStart(issued)
|
|
}
|
|
|
|
return .waitForHost
|
|
}
|
|
}
|
|
|
|
/// Keyboard phase subset relevant to clipboard prepare/restore.
|
|
public enum ClipboardPreparingPhase: Equatable, Sendable {
|
|
case idle
|
|
case denied
|
|
case error
|
|
case requestingPermissions
|
|
case recording
|
|
case processing
|
|
}
|
|
|
|
public enum ClipboardPreparingStopAction: Equatable, Sendable {
|
|
case abortPreparing
|
|
case requestStop
|
|
}
|
|
|
|
public enum ClipboardHostBusyReason: Equatable, Sendable {
|
|
case recording
|
|
case processing
|
|
}
|
|
|
|
public enum ClipboardPreparingRecoverAction: Equatable, Sendable {
|
|
case none
|
|
case wait
|
|
case confirmRecording
|
|
case adoptSibling(UUID)
|
|
case abortForHostFailure
|
|
}
|
|
|
|
public enum ClipboardEnsureStartAction: Equatable, Sendable {
|
|
case none
|
|
case alreadyInFlight
|
|
case adoptBusy(UUID, ClipboardHostBusyReason)
|
|
case writeStart(UUID)
|
|
case waitForHost
|
|
}
|