215 lines · 7.8 KB · Swift
RawDownload
import CodexBarCoreimport Foundationimport Observation struct AgentSessionRemoteRefreshGate {    private(set) var generation = 0    private(set) var isInFlight = false    private(set) var isPending = false     mutating func settingsDidChange() {        self.generation += 1        self.isPending = self.isInFlight    }     mutating func begin() -> Int? {        guard !self.isInFlight else {            return nil        }        self.isInFlight = true        self.isPending = false        return self.generation    }     mutating func finish(generation: Int) -> (shouldPublish: Bool, shouldRetry: Bool) {        self.isInFlight = false        let outcome = (generation == self.generation, self.isPending)        self.isPending = false        return outcome    }} @MainActor@Observablefinal class AgentSessionsStore {    typealias LocalScan = @Sendable (_ includeFileOnlySessions: Bool) async -> [AgentSession]     private let settings: SettingsStore    private let localScan: LocalScan    private let remoteFetcher: RemoteSessionFetcher    @ObservationIgnored private var localRefreshTask: Task<Void, Never>?    @ObservationIgnored private var remoteRefreshTask: Task<Void, Never>?    @ObservationIgnored private var localRefreshInFlight = false    @ObservationIgnored private var remoteRefreshGate = AgentSessionRemoteRefreshGate()    @ObservationIgnored var onUpdate: (@MainActor () -> Void)?     private(set) var localSessions: [AgentSession] = []    private(set) var remoteHosts: [RemoteSessionHostResult] = []    private(set) var lastUpdatedAt: Date?    private(set) var latestLocalActivityAt: Date?     init(        settings: SettingsStore,        localScanner: LocalAgentSessionScanner = LocalAgentSessionScanner(),        remoteFetcher: RemoteSessionFetcher = RemoteSessionFetcher())    {        self.settings = settings        self.localScan = { includeFileOnlySessions in            await localScanner.scan(includeFileOnlySessions: includeFileOnlySessions)        }        self.remoteFetcher = remoteFetcher    }     init(        settings: SettingsStore,        localScan: @escaping LocalScan,        remoteFetcher: RemoteSessionFetcher = RemoteSessionFetcher())    {        self.settings = settings        self.localScan = localScan        self.remoteFetcher = remoteFetcher    }     var totalCount: Int {        self.localSessions.count + self.remoteHosts.reduce(0) { $0 + $1.sessions.count }    }     /// Adaptive refresh uses local metadata only after explicit consent. Remote sessions remain    /// behind the Agent Sessions setting because they can involve Tailscale discovery and SSH.    var localMonitoringEnabled: Bool {        self.settings.agentSessionsEnabled || self.settings.adaptiveActivityScanningEnabled    }     nonisolated static func latestActivityAt(in sessions: [AgentSession]) -> Date? {        sessions.compactMap(\.lastActivityAt).max()    }     nonisolated static func shouldScanLocally(        agentSessionsEnabled: Bool,        adaptiveActivityScanningEnabled: Bool,        lowPowerModeEnabled: Bool,        thermalState: ProcessInfo.ThermalState) -> Bool    {        if agentSessionsEnabled {            return true        }        guard adaptiveActivityScanningEnabled, !lowPowerModeEnabled else { return false }        return thermalState != .serious && thermalState != .critical    }     func start() {        guard self.localRefreshTask == nil, self.remoteRefreshTask == nil else { return }        self.localRefreshTask = Task { [weak self] in            while !Task.isCancelled {                await self?.refreshLocal()                try? await Task.sleep(for: .seconds(30))            }        }        self.remoteRefreshTask = Task { [weak self] in            while !Task.isCancelled {                await self?.refreshRemote()                try? await Task.sleep(for: .seconds(60))            }        }    }     func stop() {        self.localRefreshTask?.cancel()        self.remoteRefreshTask?.cancel()        self.localRefreshTask = nil        self.remoteRefreshTask = nil    }     func settingsDidChange(remoteConfigurationChanged: Bool = true) {        if remoteConfigurationChanged {            self.remoteRefreshGate.settingsDidChange()        }        if !self.settings.agentSessionsEnabled {            // Adaptive keeps only the timestamp signal. Retained session paths and identities            // remain scoped to the explicitly enabled Agent Sessions UI.            self.localSessions = []            self.remoteHosts = []        }        guard self.localMonitoringEnabled else {            self.latestLocalActivityAt = nil            self.onUpdate?()            return        }        guard !SettingsStore.isRunningTests else { return }        Task { [weak self] in            await self?.refreshLocal()            if remoteConfigurationChanged, self?.settings.agentSessionsEnabled == true {                await self?.refreshRemote()            }        }    }     func refreshOnMenuOpen() {        guard self.localMonitoringEnabled, !SettingsStore.isRunningTests else { return }        Task { [weak self] in            await self?.refreshLocal()            if self?.settings.agentSessionsEnabled == true {                await self?.refreshRemote()            }        }    }     func focus(_ session: AgentSession, remoteHost: String?) {        if let remoteHost {            Task {                await self.remoteFetcher.focus(sessionID: session.id, host: remoteHost)            }        } else {            _ = SessionWindowFocuser.focus(session)        }    }     func refreshLocal() async {        guard self.localMonitoringEnabled, !self.localRefreshInFlight else { return }        let processInfo = ProcessInfo.processInfo        guard Self.shouldScanLocally(            agentSessionsEnabled: self.settings.agentSessionsEnabled,            adaptiveActivityScanningEnabled: self.settings.adaptiveActivityScanningEnabled,            lowPowerModeEnabled: processInfo.isLowPowerModeEnabled,            thermalState: processInfo.thermalState)        else { return }        self.localRefreshInFlight = true        let sessions = await self.localScan(self.settings.agentSessionsEnabled)        self.localRefreshInFlight = false        guard !Task.isCancelled, self.localMonitoringEnabled else { return }        self.applyLocalScanResult(sessions)    }     func applyLocalScanResult(_ sessions: [AgentSession], updatedAt: Date = Date()) {        self.latestLocalActivityAt = Self.latestActivityAt(in: sessions)        self.localSessions = self.settings.agentSessionsEnabled ? sessions : []        self.lastUpdatedAt = updatedAt        self.onUpdate?()    }     private func refreshRemote() async {        guard self.settings.agentSessionsEnabled else { return }        guard var generation = self.remoteRefreshGate.begin() else { return }        while self.settings.agentSessionsEnabled {            var hosts = self.manualHosts            await hosts.append(contentsOf: self.remoteFetcher.discoveredHosts())            let results = await self.remoteFetcher.fetch(hosts: hosts)            let outcome = self.remoteRefreshGate.finish(generation: generation)            guard !Task.isCancelled, self.settings.agentSessionsEnabled else { return }            if outcome.shouldPublish {                self.remoteHosts = results                self.lastUpdatedAt = Date()                self.onUpdate?()            }            guard outcome.shouldRetry, let nextGeneration = self.remoteRefreshGate.begin() else { return }            generation = nextGeneration        }    }     private var manualHosts: [String] {        self.settings.agentSessionsManualHosts            .split(separator: ",")            .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }            .filter { !$0.isEmpty }    }}