chore(release): bump version to 1.6.5 (build 51)

Ship automatic low-profile PiP keep-alive so background dictation stays ready without visible startup UI or idle audio work.
This commit is contained in:
Rocky
2026-08-06 21:39:31 +08:00
parent 9b02e8948c
commit 77504facc8
10 changed files with 193 additions and 110 deletions
@@ -197,6 +197,11 @@ public final class KeyboardViewController: UIInputViewController {
disableSystemGestureDelays()
keyboardHeightConstraint?.constant = targetKeyboardHeight
refreshReturnKeyRole()
// Run after the extension is fully presented so UIKit accepts the
// containing-app handoff even when typing mode is the default surface.
DispatchQueue.main.async { [weak self] in
self?.flowCoordinator.ensurePiPReadyOnKeyboardOpen()
}
OSGDiag.log(
"KVC.viewDidAppear done height=\(targetKeyboardHeight) \(OSGDiag.memoryTag())",
category: "boot"
@@ -116,6 +116,33 @@ final class KeyboardFlowCoordinator {
stopHostReadyWait()
}
/// Ensure the containing app has armed its low-profile PiP even when the
/// keyboard opens directly into Pinyin/English typing mode. The mic stays
/// visually ready; if the host contract is missing, one automatic handoff
/// prepares PiP so the next press does not need another app switch.
func ensurePiPReadyOnKeyboardOpen() {
guard FlowHandoffPolicy.allowsProactiveHostAutoLaunch,
FlowSessionPolicy.keepAliveMode() == .pictureInPicture,
state.hasCompletedOnboarding,
hasFullAccess(),
AppGroup.isAvailable,
!isPendingFlowStart,
!isFlowRecording,
!isAwaitingFlowResult else { return }
FlowSessionBridge.reloadFromDisk()
if FlowSessionBridge.isHostReady() { return }
if let reason = FlowSessionBridge.readySnapshot()?.reason,
reason == .recording || reason == .processing || reason == .awaitingDelivery {
return
}
detectAndStoreAppContext()
beginFlowStart(recordAfterHandoff: false)
traceState("keyboardOpen.autoArmPiP")
}
func refreshSessionState() {
FlowSessionBridge.reloadFromDisk()
refreshConfigFromAppGroup()
@@ -527,11 +554,7 @@ final class KeyboardFlowCoordinator {
isPendingFlowStart = true
isFlowRecording = false
flowStartDeadline = Date().timeIntervalSince1970 + FlowWatchdog.startTimeout
state.lastTranscript = ExtL10n.string(
FlowSessionPolicy.keepAliveMode() == .pictureInPicture
? "keyboard.flow.startingSession.pip"
: "keyboard.flow.startingSession"
)
state.lastTranscript = ""
recomputeMicVoiceAvailability()
OSGDiag.log(
"beginFlowStart → openHostApp(startflow) recordAfterHandoff=\(recordAfterHandoff) "
+16 -17
View File
@@ -280,17 +280,15 @@ public struct KeyboardRootView: View {
}
private var buttonPhase: RecordButton.Phase {
if case .error = state.phase { return .error }
if case .denied = state.phase { return .error }
switch state.micVoiceAvailability {
case .ready:
return .idleReady
case .unavailable:
return .idleUnavailable
case .recording:
return .recording
case .processing:
return .processing
case .ready, .unavailable:
// Host/PiP readiness is handled by an automatic app handoff. Keep
// the idle mic visually green instead of exposing startup state.
return .idleReady
}
}
}
@@ -409,7 +407,16 @@ private struct TranscriptLine: View {
@ViewBuilder
private var idleHint: some View {
let isWarning = micVoiceAvailability.isUnavailable
let isWarning: Bool = {
switch micVoiceAvailability {
case .unavailable(.hostNotReady), .unavailable(.preparingSession):
return false
case .unavailable:
return true
case .ready, .recording, .processing:
return false
}
}()
Group {
switch micVoiceAvailability {
case .ready:
@@ -417,17 +424,9 @@ private struct TranscriptLine: View {
case .unavailable(.missingAPIKey):
Text(micDisabledHint)
case .unavailable(.hostNotReady):
if FlowSessionPolicy.keepAliveMode() == .pictureInPicture {
ExtL10n.text("keyboard.flow.sessionInactive.pip")
} else {
ExtL10n.text("keyboard.flow.sessionInactive")
}
ExtL10n.text("keyboard.placeholder.idle")
case .unavailable(.preparingSession):
if FlowSessionPolicy.keepAliveMode() == .pictureInPicture {
ExtL10n.text("keyboard.flow.startingSession.pip")
} else {
ExtL10n.text("keyboard.flow.startingSession")
}
ExtL10n.text("keyboard.placeholder.idle")
case .unavailable(.noFullAccess):
ExtL10n.text("keyboard.error.fullAccessRequired")
case .unavailable(.appGroupUnavailable):