feat(polish): allow mood emoji on custom styles and ship Flow/ASR fixes
Custom polish styles can opt in to emotion-matched emoji (default off), with prompt-level opt-in detection so paste-only styles keep model-added emoji. Also include Volcengine API-Key ASR auth, voice-processing capture, PiP flash fix, and related keyboard Shift/haptics reliability work.
This commit is contained in:
@@ -165,6 +165,9 @@ public final class KeyboardViewController: UIInputViewController {
|
||||
configSync.refreshConfigFromAppGroup()
|
||||
// Settings may have changed while the extension stayed alive.
|
||||
applyPreferredSurfaceOnOpen()
|
||||
// Re-warm Taptic after host app switches: SwiftUI `onAppear` often
|
||||
// skips when the extension process is reused, leaving generators cold.
|
||||
KeyboardHapticFeedback.prepare()
|
||||
if state.surface == .typing {
|
||||
OSGDiag.log("KVC.viewWillAppear enterTypingMode", category: "boot")
|
||||
typingSession.enterTypingMode()
|
||||
|
||||
@@ -41,6 +41,9 @@ final class KeyboardFlowCoordinator {
|
||||
/// stale sample never flashes the mic orange while the session is healthy.
|
||||
private var lastHostReadyAt: TimeInterval = 0
|
||||
private static let hostReadyGrace: TimeInterval = 4
|
||||
/// Once the host has published ready for this session, hold green through
|
||||
/// brief inter-utterance ready flaps instead of flashing preparingSession.
|
||||
private var sessionProvenReady = false
|
||||
private var flowSessionMonitorTask: Task<Void, Never>?
|
||||
private var isAwaitingFlowResult = false
|
||||
private var activeSessionId: UUID?
|
||||
@@ -168,9 +171,17 @@ final class KeyboardFlowCoordinator {
|
||||
// show red/white instead of a fake orange "starting" state.
|
||||
adoptHostBusyStateIfNeeded(snapshot: readySnapshot)
|
||||
|
||||
let hostReady = readySnapshot?.ready == true && FlowSessionBridge.isHostReady()
|
||||
let hostReadyRaw = readySnapshot?.ready == true && FlowSessionBridge.isHostReady()
|
||||
let sessionActive = FlowSessionBridge.isSessionActive()
|
||||
let now = Date().timeIntervalSince1970
|
||||
if hostReady { lastHostReadyAt = now }
|
||||
if hostReadyRaw {
|
||||
lastHostReadyAt = now
|
||||
sessionProvenReady = true
|
||||
}
|
||||
if !sessionActive {
|
||||
sessionProvenReady = false
|
||||
lastHostReadyAt = 0
|
||||
}
|
||||
// Grace window: the host was ready very recently, so treat a momentary
|
||||
// stale heartbeat read as "still warming" rather than an outright
|
||||
// failure. `isSessionActive` is heartbeat-independent, so it stays true
|
||||
@@ -181,19 +192,30 @@ final class KeyboardFlowCoordinator {
|
||||
// it as preparingSession was the orange-stuck bug after cold start:
|
||||
// host utt.rec=1 → ready=false → keyboard forever "正在启动…".
|
||||
let hostBusy = FlowKeyboardHostWarming.isHostBusy(reason: readySnapshot?.reason)
|
||||
// Hold green after the session already proved ready — PiP mic release /
|
||||
// ack lag must not flash yellow「正在启动画中画».
|
||||
let holdReady = FlowKeyboardHostWarming.shouldHoldReady(
|
||||
hostReady: hostReadyRaw,
|
||||
hostBusy: hostBusy,
|
||||
sessionActive: sessionActive,
|
||||
sessionProvenReady: sessionProvenReady,
|
||||
isPendingFlowStart: isPendingFlowStart,
|
||||
snapshotReason: readySnapshot?.reason
|
||||
)
|
||||
let hostReady = hostReadyRaw || holdReady
|
||||
// PiP sessions publish `reason=.starting` while the small window is
|
||||
// coming up — treat that as warming so the mic stays orange (wait)
|
||||
// instead of jumping into another cold start.
|
||||
let hostWarming = FlowKeyboardHostWarming.isHostWarming(
|
||||
hostReady: hostReady,
|
||||
hostBusy: hostBusy,
|
||||
sessionActive: FlowSessionBridge.isSessionActive(),
|
||||
sessionActive: sessionActive,
|
||||
hostReachable: FlowSessionBridge.isHostReachable(),
|
||||
isPendingFlowStart: isPendingFlowStart,
|
||||
withinReadyGrace: withinReadyGrace,
|
||||
snapshotReason: readySnapshot?.reason
|
||||
)
|
||||
state.flowSessionActive = FlowSessionBridge.isSessionActive()
|
||||
state.flowSessionActive = sessionActive
|
||||
state.debugPendingFlowStart = isPendingFlowStart
|
||||
state.debugFlowRecording = isFlowRecording
|
||||
state.debugAwaitingFlowResult = isAwaitingFlowResult
|
||||
@@ -626,6 +648,7 @@ final class KeyboardFlowCoordinator {
|
||||
"utterance=\(result.utteranceId.uuidString.prefix(8)) "
|
||||
+ "commandSeq=\(result.commandSeq) warning=\(result.warning == nil ? 0 : 1)"
|
||||
)
|
||||
recomputeMicVoiceAvailability()
|
||||
return
|
||||
}
|
||||
if let result = matchingResult(), isTerminalFailure(result) {
|
||||
|
||||
@@ -95,6 +95,8 @@ struct TypingRootView: View {
|
||||
if !hasContent { candidatePanelMounted = false }
|
||||
}
|
||||
.onAppear {
|
||||
// Primary warm-up lives in KVC.viewWillAppear (covers app switch).
|
||||
// Keep this for first mount / surface flip into typing.
|
||||
KeyboardHapticFeedback.prepare()
|
||||
}
|
||||
}
|
||||
@@ -489,8 +491,12 @@ struct TypingRootView: View {
|
||||
if !output.text.isEmpty {
|
||||
onInsert(output.text)
|
||||
}
|
||||
// Proxy context is up to date after inserts/deletes — refresh Shift.
|
||||
typing.syncAutocapitalization()
|
||||
// Notes / some hosts lag `documentContextBeforeInput` — pass the edit we
|
||||
// just applied so sentence Shift can arm after Return / "." immediately.
|
||||
typing.syncAutocapitalization(
|
||||
accountingForInsert: output.text,
|
||||
deleteCount: output.deleteCount
|
||||
)
|
||||
}
|
||||
|
||||
private func keyWeight(label: String, index: Int, rowIndex: Int) -> CGFloat {
|
||||
|
||||
@@ -234,14 +234,22 @@ public struct KeyboardRootView: View {
|
||||
}
|
||||
|
||||
private func bottomDeleteButton(disabled: Bool) -> some View {
|
||||
RepeatingDeleteButton(disabled: disabled) {
|
||||
RepeatingDeleteButton(
|
||||
disabled: disabled,
|
||||
hapticIntensity: state.keyboardHapticIntensity
|
||||
) {
|
||||
state.deleteBackward()
|
||||
}
|
||||
.frame(height: KeyboardLayoutMetrics.bottomActionRowHeight)
|
||||
}
|
||||
|
||||
private func bottomSpaceButton(disabled: Bool) -> some View {
|
||||
RectangularToolbarButton(spaceStyle: true, label: "space", disabled: disabled) {
|
||||
RectangularToolbarButton(
|
||||
spaceStyle: true,
|
||||
label: "space",
|
||||
disabled: disabled,
|
||||
hapticIntensity: state.keyboardHapticIntensity
|
||||
) {
|
||||
state.insertSpace()
|
||||
}
|
||||
.frame(height: KeyboardLayoutMetrics.bottomActionRowHeight)
|
||||
@@ -253,7 +261,8 @@ public struct KeyboardRootView: View {
|
||||
title: title,
|
||||
label: title,
|
||||
disabled: disabled,
|
||||
isSend: state.returnKeyRole == .send
|
||||
isSend: state.returnKeyRole == .send,
|
||||
hapticIntensity: state.keyboardHapticIntensity
|
||||
) {
|
||||
state.insertNewline()
|
||||
}
|
||||
|
||||
@@ -154,7 +154,10 @@ struct KeyboardTopControls: View {
|
||||
return
|
||||
}
|
||||
onInsert(output.text)
|
||||
typing.syncAutocapitalization()
|
||||
typing.syncAutocapitalization(
|
||||
accountingForInsert: output.text,
|
||||
deleteCount: output.deleteCount
|
||||
)
|
||||
}
|
||||
|
||||
private func accessibilityLabel(for tab: KeyboardInputTab) -> String {
|
||||
|
||||
@@ -94,7 +94,7 @@ struct RepeatingPressButton<Label: View>: View {
|
||||
var disabled: Bool = false
|
||||
/// Plays the system delete click on each fire (matches stock keyboard).
|
||||
var playsDeleteSound: Bool = true
|
||||
/// Typing-grid haptic strength; `.off` skips haptics (voice toolbar default).
|
||||
/// Settings → General → Haptics; `.off` skips impact feedback.
|
||||
var hapticIntensity: KeyboardHapticIntensity = .off
|
||||
let action: () -> Void
|
||||
@ViewBuilder let label: (_ isPressed: Bool) -> Label
|
||||
@@ -166,10 +166,16 @@ struct RepeatingDeleteButton: View {
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
let disabled: Bool
|
||||
/// Mirrors Settings → General → Haptics (same as typing delete).
|
||||
var hapticIntensity: KeyboardHapticIntensity = .off
|
||||
let action: () -> Void
|
||||
|
||||
var body: some View {
|
||||
RepeatingPressButton(disabled: disabled, action: action) { isPressed in
|
||||
RepeatingPressButton(
|
||||
disabled: disabled,
|
||||
hapticIntensity: hapticIntensity,
|
||||
action: action
|
||||
) { isPressed in
|
||||
ToolbarKeySurface(
|
||||
isPressed: isPressed,
|
||||
cornerRadius: ToolbarButtonMetrics.cornerRadius,
|
||||
@@ -227,15 +233,24 @@ struct RectangularToolbarButton: View {
|
||||
let label: String
|
||||
let disabled: Bool
|
||||
let isSend: Bool
|
||||
/// Settings → General → Haptics; space / return use `.action` role.
|
||||
var hapticIntensity: KeyboardHapticIntensity = .off
|
||||
let action: () -> Void
|
||||
|
||||
init(systemName: String, label: String, disabled: Bool = false, action: @escaping () -> Void) {
|
||||
init(
|
||||
systemName: String,
|
||||
label: String,
|
||||
disabled: Bool = false,
|
||||
hapticIntensity: KeyboardHapticIntensity = .off,
|
||||
action: @escaping () -> Void
|
||||
) {
|
||||
self.systemName = systemName
|
||||
self.spaceStyle = false
|
||||
self.title = nil
|
||||
self.label = label
|
||||
self.disabled = disabled
|
||||
self.isSend = false
|
||||
self.hapticIntensity = hapticIntensity
|
||||
self.action = action
|
||||
}
|
||||
|
||||
@@ -244,6 +259,7 @@ struct RectangularToolbarButton: View {
|
||||
label: String,
|
||||
disabled: Bool = false,
|
||||
isSend: Bool = false,
|
||||
hapticIntensity: KeyboardHapticIntensity = .off,
|
||||
action: @escaping () -> Void
|
||||
) {
|
||||
self.systemName = nil
|
||||
@@ -251,17 +267,25 @@ struct RectangularToolbarButton: View {
|
||||
self.label = label
|
||||
self.disabled = disabled
|
||||
self.isSend = isSend
|
||||
self.hapticIntensity = hapticIntensity
|
||||
self.action = action
|
||||
self.title = title
|
||||
}
|
||||
|
||||
init(spaceStyle: Bool, label: String, disabled: Bool = false, action: @escaping () -> Void) {
|
||||
init(
|
||||
spaceStyle: Bool,
|
||||
label: String,
|
||||
disabled: Bool = false,
|
||||
hapticIntensity: KeyboardHapticIntensity = .off,
|
||||
action: @escaping () -> Void
|
||||
) {
|
||||
self.systemName = nil
|
||||
self.spaceStyle = spaceStyle
|
||||
self.title = nil
|
||||
self.label = label
|
||||
self.disabled = disabled
|
||||
self.isSend = false
|
||||
self.hapticIntensity = hapticIntensity
|
||||
self.action = action
|
||||
}
|
||||
|
||||
@@ -299,13 +323,14 @@ struct RectangularToolbarButton: View {
|
||||
isSend ? .white : NativeKeyboardKeyColors.text(for: colorScheme)
|
||||
}
|
||||
|
||||
// 按下即响、按下即执行,与系统键盘保持一致(Button 默认松手才触发)。
|
||||
// 按下即响、即震、即执行,与系统键盘 / 打字面保持一致(Button 默认松手才触发)。
|
||||
private var pressGesture: some Gesture {
|
||||
DragGesture(minimumDistance: 0)
|
||||
.onChanged { _ in
|
||||
guard !disabled, !isPressing else { return }
|
||||
isPressing = true
|
||||
KeyboardSoundFeedback.keyClick()
|
||||
KeyboardHapticFeedback.play(role: .action, intensity: hapticIntensity)
|
||||
action()
|
||||
}
|
||||
.onEnded { _ in
|
||||
|
||||
Reference in New Issue
Block a user