diff --git a/NucleicRemote/NucleicRemote/LiveActivityManager.swift b/NucleicRemote/NucleicRemote/LiveActivityManager.swift index 4dd1dc3..1507e4c 100644 --- a/NucleicRemote/NucleicRemote/LiveActivityManager.swift +++ b/NucleicRemote/NucleicRemote/LiveActivityManager.swift @@ -87,6 +87,41 @@ final class LiveActivityManager { push(state, hostName: hostName) } + /// Nothing is running or waiting. If the glance was showing active work, flash a brief "Done" + /// summary of the just-completed sessions and then dismiss (UX_IOS §5.3 — a run should read as + /// *finished*, not just vanish). If nothing was on screen, there's nothing to close. + private func finishWithDoneGlance(hostName: String, live: [WireSessionSummary]) { + // Already flashing "Done" and scheduled to dismiss — let that run rather than restart it. + guard dismissTask == nil else { return } + // Nothing tracked on screen: only reach into ActivityKit if an untracked orphan is lingering. + guard activity != nil else { + if !Activity.activities.isEmpty { end() } + return + } + // The sessions that just finished — completed conversational turns and finished runs. These + // are exactly the ones the active-work filter above drops, surfaced now as `.done` rows. + let done = live + .filter { + $0.status == .finished + || ($0.status == .awaitingInput && $0.disposition == .completed) + } + .sorted(by: StatusStyle.attentionThenRecency) + .prefix(Self.maxLines) + .map(Self.line(for:)) + // Nothing to celebrate (e.g. the work was discarded/deleted) → just dismiss. + guard !done.isEmpty else { end(); return } + + let state = NucleicSessionAttributes.ContentState( + runningCount: 0, needsYouCount: 0, approvalCount: 0, + filesChanged: 0, linesAdded: 0, linesRemoved: 0, lines: Array(done)) + push(state, hostName: hostName) + dismissTask = Task { [weak self] in + try? await Task.sleep(for: Self.doneLinger) + guard !Task.isCancelled else { return } + self?.end() + } + } + /// 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) {