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,25 @@ final class FlowSessionPolicyTests: XCTestCase {
XCTAssertEqual(FlowInactivityDuration.tenMinutes.timeInterval, 10 * 60)
}
func testKeepAliveModeDefaultsToLiveActivity() {
let defaults = makeDefaults()
XCTAssertEqual(FlowSessionPolicy.keepAliveMode(defaults: defaults), .liveActivity)
XCTAssertTrue(FlowSessionPolicy.usesInactivityExpiry(defaults: defaults))
}
func testPiPSessionHasNoInactivityExpiry() {
let defaults = makeDefaults()
defaults.set(FlowKeepAliveMode.pictureInPicture.rawValue,
forKey: AppGroupConfiguration.Keys.flowKeepAliveMode)
FlowSessionBridge.markSessionActive(sessionId: UUID(), defaults: defaults)
XCTAssertTrue(FlowSessionBridge.isSessionActive(defaults: defaults))
XCTAssertNil(FlowSessionBridge.sessionExpiresAt(defaults: defaults))
FlowSessionBridge.touchLastActivity(defaults: defaults)
XCTAssertNil(FlowSessionBridge.sessionExpiresAt(defaults: defaults))
}
func testTouchLastActivityExtendsExpiry() {
let defaults = makeDefaults()
defaults.set(FlowInactivityDuration.tenMinutes.rawValue, forKey: AppGroupConfiguration.Keys.flowInactivityDuration)