feat(flow): add Picture in Picture keep-alive mode

- Add FlowKeepAliveMode (Dynamic Island vs PiP) with iCloud sync
- Settings: keep-alive picker; inactivity timeout only for Live Activity
- PiP: waveform sample-buffer controller, mic released between utterances
- PiP sessions have no idle expiry; user closing PiP ends the session
- FlowSessionManager branches hostReady, session start, and utterance paths
- UIBackgroundModes picture-in-picture; bilingual strings and tests

Co-authored-by: Rocky <hkgood@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-07-26 10:34:52 +00:00
parent e6f99d2744
commit 70ba3a4359
18 changed files with 801 additions and 36 deletions
@@ -29,6 +29,7 @@ public struct SyncedAppSettingsV2: Codable, Equatable, Sendable {
public var polishIntensity: SyncedField<PolishIntensity>
public var llmThinkingEnabled: SyncedField<Bool>
public var flowSkipAppSwitch: SyncedField<Bool>
public var flowKeepAliveMode: SyncedField<FlowKeepAliveMode>
public var flowInactivityDuration: SyncedField<FlowInactivityDuration>
public init(
@@ -50,6 +51,7 @@ public struct SyncedAppSettingsV2: Codable, Equatable, Sendable {
polishIntensity: SyncedField<PolishIntensity>,
llmThinkingEnabled: SyncedField<Bool>,
flowSkipAppSwitch: SyncedField<Bool>,
flowKeepAliveMode: SyncedField<FlowKeepAliveMode>,
flowInactivityDuration: SyncedField<FlowInactivityDuration>
) {
self.schemaVersion = schemaVersion
@@ -70,6 +72,7 @@ public struct SyncedAppSettingsV2: Codable, Equatable, Sendable {
self.polishIntensity = polishIntensity
self.llmThinkingEnabled = llmThinkingEnabled
self.flowSkipAppSwitch = flowSkipAppSwitch
self.flowKeepAliveMode = flowKeepAliveMode
self.flowInactivityDuration = flowInactivityDuration
}
@@ -92,6 +95,7 @@ public struct SyncedAppSettingsV2: Codable, Equatable, Sendable {
case polishIntensity
case llmThinkingEnabled
case flowSkipAppSwitch
case flowKeepAliveMode
case flowInactivityDuration
}
@@ -124,6 +128,14 @@ public struct SyncedAppSettingsV2: Codable, Equatable, Sendable {
forKey: .llmThinkingEnabled
) ?? SyncedField(value: false, updatedAt: polishIntensity.updatedAt, deviceID: polishIntensity.deviceID)
flowSkipAppSwitch = try container.decode(SyncedField<Bool>.self, forKey: .flowSkipAppSwitch)
flowKeepAliveMode = try container.decodeIfPresent(
SyncedField<FlowKeepAliveMode>.self,
forKey: .flowKeepAliveMode
) ?? SyncedField(
value: .liveActivity,
updatedAt: flowSkipAppSwitch.updatedAt,
deviceID: flowSkipAppSwitch.deviceID
)
flowInactivityDuration = try container.decode(
SyncedField<FlowInactivityDuration>.self,
forKey: .flowInactivityDuration
@@ -171,6 +183,7 @@ public struct SyncedAppSettingsV2: Codable, Equatable, Sendable {
polishIntensity.updatedAt,
llmThinkingEnabled.updatedAt,
flowSkipAppSwitch.updatedAt,
flowKeepAliveMode.updatedAt,
flowInactivityDuration.updatedAt,
].max() ?? .distantPast
}
@@ -208,6 +221,7 @@ public extension SyncedAppSettingsV2 {
polishIntensity: field(configuration.polishIntensity),
llmThinkingEnabled: field(configuration.llmThinkingEnabled),
flowSkipAppSwitch: field(configuration.flowSkipAppSwitch),
flowKeepAliveMode: field(configuration.flowKeepAliveMode),
flowInactivityDuration: field(configuration.flowInactivityDuration)
)
}
@@ -237,6 +251,7 @@ public extension SyncedAppSettingsV2 {
polishIntensity: field(legacy.polishIntensity),
llmThinkingEnabled: field(false),
flowSkipAppSwitch: field(legacy.flowSkipAppSwitch),
flowKeepAliveMode: field(.liveActivity),
flowInactivityDuration: field(legacy.flowInactivityDuration)
)
}
@@ -269,6 +284,7 @@ public extension SyncedAppSettingsV2 {
polishIntensity: .merge(local: local.polishIntensity, remote: remote.polishIntensity),
llmThinkingEnabled: .merge(local: local.llmThinkingEnabled, remote: remote.llmThinkingEnabled),
flowSkipAppSwitch: .merge(local: local.flowSkipAppSwitch, remote: remote.flowSkipAppSwitch),
flowKeepAliveMode: .merge(local: local.flowKeepAliveMode, remote: remote.flowKeepAliveMode),
flowInactivityDuration: .merge(
local: local.flowInactivityDuration,
remote: remote.flowInactivityDuration
@@ -294,6 +310,7 @@ public extension SyncedAppSettingsV2 {
configuration.polishIntensity = polishIntensity.value
configuration.llmThinkingEnabled = llmThinkingEnabled.value
configuration.flowSkipAppSwitch = flowSkipAppSwitch.value
configuration.flowKeepAliveMode = flowKeepAliveMode.value
configuration.flowInactivityDuration = flowInactivityDuration.value
}
@@ -321,6 +338,7 @@ public extension SyncedAppSettingsV2 {
patch(&copy.polishIntensity, value: configuration.polishIntensity)
patch(&copy.llmThinkingEnabled, value: configuration.llmThinkingEnabled)
patch(&copy.flowSkipAppSwitch, value: configuration.flowSkipAppSwitch)
patch(&copy.flowKeepAliveMode, value: configuration.flowKeepAliveMode)
patch(&copy.flowInactivityDuration, value: configuration.flowInactivityDuration)
return copy
}
@@ -351,6 +369,7 @@ public extension SyncedAppSettingsV2 {
touch(&copy.polishIntensity, value: configuration.polishIntensity)
touch(&copy.llmThinkingEnabled, value: configuration.llmThinkingEnabled)
touch(&copy.flowSkipAppSwitch, value: configuration.flowSkipAppSwitch)
touch(&copy.flowKeepAliveMode, value: configuration.flowKeepAliveMode)
touch(&copy.flowInactivityDuration, value: configuration.flowInactivityDuration)
return copy
}