From ff605a3997d07aa4b1b362ef3c19d021314437dc Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Sat, 4 Jul 2026 23:46:00 -0700 Subject: [PATCH] Live Activity Content Density Nucleic-Session: B3643133-163C-4C5D-BE20-1A6B7FB83DA9 Co-authored-by: Nucleic --- .../NucleicRemote/LiveActivityManager.swift | 47 +++++++++++++++---- 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/NucleicRemote/NucleicRemote/LiveActivityManager.swift b/NucleicRemote/NucleicRemote/LiveActivityManager.swift index 4052afa..00592cc 100644 --- a/NucleicRemote/NucleicRemote/LiveActivityManager.swift +++ b/NucleicRemote/NucleicRemote/LiveActivityManager.swift @@ -68,23 +68,52 @@ final class LiveActivityManager { linesRemoved: removed, lines: Array(lines)) - if let activity { - Task { await activity.update(ActivityContent(state: state, staleDate: nil)) } - } else { - // Recover an Activity that survived an app relaunch before starting a new one. + push(state, hostName: hostName) + } + + /// Apply `state` to the Activity — deduped against the last push and coalesced through one + /// serial task so the newest state always wins. + private func push(_ state: NucleicSessionAttributes.ContentState, hostName: String) { + guard state != lastState else { return } + lastState = state + + guard let activity else { + // Recover an Activity that survived an app relaunch before starting a new one; the + // recovered one still needs the fresh state, so fall through to the update pipeline. if let existing = Activity.activities.first { activity = existing - Task { await existing.update(ActivityContent(state: state, staleDate: nil)) } - return + enqueue(state) + } else { + activity = try? Activity.request( + attributes: NucleicSessionAttributes(hostName: hostName), + content: ActivityContent(state: state, staleDate: nil)) } - activity = try? Activity.request( - attributes: NucleicSessionAttributes(hostName: hostName), - content: ActivityContent(state: state, staleDate: nil)) + return + } + _ = activity + enqueue(state) + } + + /// Hand the newest state to the serial drainer (starting it if idle). + private func enqueue(_ state: NucleicSessionAttributes.ContentState) { + pendingState = state + guard updateTask == nil else { return } // the running drainer will pick this up + updateTask = Task { @MainActor [weak self] in + guard let self else { return } + while let next = self.pendingState { + self.pendingState = nil + await self.activity?.update(ActivityContent(state: next, staleDate: nil)) + } + self.updateTask = nil } } /// End the Activity (all idle, or unpaired). func end() { + updateTask?.cancel() + updateTask = nil + pendingState = nil + lastState = nil guard let activity else { return } self.activity = nil Task {