feat(flow): implement ABCD session policy and host return whitelist
- Scheme A: on-demand session start, inactivity-based expiry, handoff auto-recording - Scheme B: cold-start overlay with swipe guidance and return alert - Scheme C+D: HostAppURLRegistry (20 apps) and sourceApplication capture - Settings: skip app switch toggle and inactivity duration picker - Add LSApplicationQueriesSchemes for canOpenURL checks Co-authored-by: Rocky <hkgood@users.noreply.github.com>
This commit is contained in:
@@ -30,6 +30,10 @@ public struct AppGroupConfiguration: Sendable, Equatable {
|
||||
public static let detectedAppContext = "config.detectedAppContext"
|
||||
public static let detectedAppContextAt = "config.detectedAppContextAt"
|
||||
public static let personalDictionary = "config.personalDictionary.v1"
|
||||
/// When true, the host app auto-returns to the source app after a cold-start handoff.
|
||||
public static let flowSkipAppSwitch = "config.flowSkipAppSwitch"
|
||||
/// Raw `FlowInactivityDuration` value; session expires after this idle window.
|
||||
public static let flowInactivityDuration = "config.flowInactivityDuration"
|
||||
}
|
||||
|
||||
// MARK: - Stored fields
|
||||
@@ -49,6 +53,10 @@ public struct AppGroupConfiguration: Sendable, Equatable {
|
||||
public var cursorDragNavigationEnabled: Bool
|
||||
public var polishIntensity: PolishIntensity
|
||||
public var personalDictionary: PersonalDictionary
|
||||
/// Auto-return to the host app after `startflow` cold start (default on).
|
||||
public var flowSkipAppSwitch: Bool
|
||||
/// Idle timeout before the Flow session ends; resets on each utterance.
|
||||
public var flowInactivityDuration: FlowInactivityDuration
|
||||
|
||||
// MARK: - Derived
|
||||
|
||||
@@ -145,7 +153,16 @@ public struct AppGroupConfiguration: Sendable, Equatable {
|
||||
return defaults.bool(forKey: Keys.cursorDragNavigationEnabled)
|
||||
}(),
|
||||
polishIntensity: resolvePolishIntensity(from: defaults),
|
||||
personalDictionary: decodePersonalDictionary(from: defaults)
|
||||
personalDictionary: decodePersonalDictionary(from: defaults),
|
||||
flowSkipAppSwitch: {
|
||||
if defaults.object(forKey: Keys.flowSkipAppSwitch) == nil {
|
||||
return true
|
||||
}
|
||||
return defaults.bool(forKey: Keys.flowSkipAppSwitch)
|
||||
}(),
|
||||
flowInactivityDuration: FlowInactivityDuration.fromStored(
|
||||
defaults.string(forKey: Keys.flowInactivityDuration)
|
||||
)
|
||||
)
|
||||
|
||||
let preset = LLMProvider.provider(id: config.providerId)
|
||||
@@ -193,6 +210,8 @@ public struct AppGroupConfiguration: Sendable, Equatable {
|
||||
defaults.set(handednessPreference.rawValue, forKey: Keys.handednessPreference)
|
||||
defaults.set(cursorDragNavigationEnabled, forKey: Keys.cursorDragNavigationEnabled)
|
||||
defaults.set(polishIntensity.rawValue, forKey: Keys.polishIntensity)
|
||||
defaults.set(flowSkipAppSwitch, forKey: Keys.flowSkipAppSwitch)
|
||||
defaults.set(flowInactivityDuration.rawValue, forKey: Keys.flowInactivityDuration)
|
||||
Self.encodePersonalDictionary(personalDictionary, to: defaults)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// FlowInactivityDuration.swift
|
||||
// OSGKeyboard · Shared
|
||||
//
|
||||
// User-selectable Flow session inactivity timeout. The timer resets after
|
||||
// each completed utterance (and on session start).
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum FlowInactivityDuration: String, CaseIterable, Identifiable, Sendable, Codable {
|
||||
case tenMinutes = "10m"
|
||||
case thirtyMinutes = "30m"
|
||||
case threeHours = "3h"
|
||||
case twelveHours = "12h"
|
||||
case twentyFourHours = "24h"
|
||||
|
||||
public var id: String { rawValue }
|
||||
|
||||
public static let `default`: FlowInactivityDuration = .twelveHours
|
||||
|
||||
public var timeInterval: TimeInterval {
|
||||
switch self {
|
||||
case .tenMinutes: return 10 * 60
|
||||
case .thirtyMinutes: return 30 * 60
|
||||
case .threeHours: return 3 * 60 * 60
|
||||
case .twelveHours: return 12 * 60 * 60
|
||||
case .twentyFourHours: return 24 * 60 * 60
|
||||
}
|
||||
}
|
||||
|
||||
public var labelKey: String {
|
||||
switch self {
|
||||
case .tenMinutes: return "settings.flow.inactivity.10m"
|
||||
case .thirtyMinutes: return "settings.flow.inactivity.30m"
|
||||
case .threeHours: return "settings.flow.inactivity.3h"
|
||||
case .twelveHours: return "settings.flow.inactivity.12h"
|
||||
case .twentyFourHours: return "settings.flow.inactivity.24h"
|
||||
}
|
||||
}
|
||||
|
||||
public static func fromStored(_ raw: String?) -> FlowInactivityDuration {
|
||||
guard let raw, let value = FlowInactivityDuration(rawValue: raw) else {
|
||||
return .default
|
||||
}
|
||||
return value
|
||||
}
|
||||
}
|
||||
@@ -171,6 +171,25 @@ public final class ProviderConfig: ObservableObject, @unchecked Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
/// When enabled, the host app tries to return to the source app after a cold-start handoff.
|
||||
@Published public var flowSkipAppSwitch: Bool {
|
||||
didSet {
|
||||
guard !isApplyingConfiguration, flowSkipAppSwitch != configuration.flowSkipAppSwitch else { return }
|
||||
configuration.flowSkipAppSwitch = flowSkipAppSwitch
|
||||
persistConfiguration()
|
||||
}
|
||||
}
|
||||
|
||||
/// Idle window before an active Flow session expires; resets on each utterance.
|
||||
@Published public var flowInactivityDuration: FlowInactivityDuration {
|
||||
didSet {
|
||||
guard !isApplyingConfiguration,
|
||||
flowInactivityDuration != configuration.flowInactivityDuration else { return }
|
||||
configuration.flowInactivityDuration = flowInactivityDuration
|
||||
persistConfiguration()
|
||||
}
|
||||
}
|
||||
|
||||
public var isConfigured: Bool {
|
||||
// Local engine uses on-device ASR + built-in DeepSeek polish and
|
||||
// does not need a user API key. Cloud needs base URL, key, and model.
|
||||
@@ -218,6 +237,8 @@ public final class ProviderConfig: ObservableObject, @unchecked Sendable {
|
||||
handednessPreference = configuration.handednessPreference
|
||||
cursorDragNavigationEnabled = configuration.cursorDragNavigationEnabled
|
||||
polishIntensity = configuration.polishIntensity
|
||||
flowSkipAppSwitch = configuration.flowSkipAppSwitch
|
||||
flowInactivityDuration = configuration.flowInactivityDuration
|
||||
isApplyingConfiguration = false
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user