From e62c0614910f87891918dc6410b519dabef68cf4 Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Mon, 6 Jul 2026 12:40:19 -0700 Subject: [PATCH 01/65] iOS Live Activity Updates Nucleic-Session: 665980BA-E8A5-4E84-9DF4-1757CF792353 Co-authored-by: Nucleic --- NucleicRemote/NucleicRemote/LiveActivityManager.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/NucleicRemote/NucleicRemote/LiveActivityManager.swift b/NucleicRemote/NucleicRemote/LiveActivityManager.swift index 0ed2631..ee99985 100644 --- a/NucleicRemote/NucleicRemote/LiveActivityManager.swift +++ b/NucleicRemote/NucleicRemote/LiveActivityManager.swift @@ -88,6 +88,12 @@ final class LiveActivityManager { if let existing = Activity.activities.first { activity = existing observePushToken(existing) + // Dismiss any duplicates so only the adopted Activity renders. iOS stacks multiple + // Activities of one type in the Dynamic Island — an orphan (from a prior launch that + // was killed before `end()`) then shows *its* stale state in the collapsed pill while + // expanding reveals the fresh one, and `end()` on completion would leave it lingering + // on the last "needs attention" glance. Keep exactly one. + endStrays(keeping: existing.id) enqueue(state) } else { // `pushType: .token` opts the activity into APNs updates — the Macs push new From b128256cd39ea17adcfb0fbc0cc79a2b65d25244 Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Mon, 6 Jul 2026 12:40:34 -0700 Subject: [PATCH 02/65] iOS Live Activity Updates Nucleic-Session: 665980BA-E8A5-4E84-9DF4-1757CF792353 Co-authored-by: Nucleic --- .../NucleicRemote/LiveActivityManager.swift | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/NucleicRemote/NucleicRemote/LiveActivityManager.swift b/NucleicRemote/NucleicRemote/LiveActivityManager.swift index ee99985..7831ce0 100644 --- a/NucleicRemote/NucleicRemote/LiveActivityManager.swift +++ b/NucleicRemote/NucleicRemote/LiveActivityManager.swift @@ -143,13 +143,31 @@ final class LiveActivityManager { tokenObservation = nil pendingState = nil lastState = nil - guard let activity else { return } + let tracked = activity self.activity = nil - onActivityEnded?(activity.id) // let the Macs stop pushing to this token + if let tracked { onActivityEnded?(tracked.id) } // let the Macs stop pushing to this token + // Dismiss the tracked Activity *and any strays*: ending only the one we track would leave an + // orphan (from a prior launch killed before `end()`) rendering a stale "needs attention" + // glance in the Dynamic Island after the work it described has finished. Clear them all so + // "everything idle" can't get stuck on screen. Task { - await activity.end( - ActivityContent(state: activity.content.state, staleDate: nil), - dismissalPolicy: .immediate) + for activity in Activity.activities { + await activity.end( + ActivityContent(state: activity.content.state, staleDate: nil), + dismissalPolicy: .immediate) + } + } + } + + /// Dismiss every live Activity except `keep` — collapses accidental duplicates down to one so + /// iOS can't render a stale orphan alongside the tracked glance. + private func endStrays(keeping keep: String) { + for stray in Activity.activities where stray.id != keep { + Task { + await stray.end( + ActivityContent(state: stray.content.state, staleDate: nil), + dismissalPolicy: .immediate) + } } } From 547d2a0c29eb1cb1c5c514cfa23eb032c5d8c178 Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Mon, 6 Jul 2026 12:45:07 -0700 Subject: [PATCH 03/65] Network Reconnection Delay Nucleic-Session: 4BE3E40C-E939-444C-8AED-93C0AFE13C07 Co-authored-by: Nucleic --- NucleicRemote/NucleicRemote/Models/HostConnection.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/NucleicRemote/NucleicRemote/Models/HostConnection.swift b/NucleicRemote/NucleicRemote/Models/HostConnection.swift index d3234e9..a0a929e 100644 --- a/NucleicRemote/NucleicRemote/Models/HostConnection.swift +++ b/NucleicRemote/NucleicRemote/Models/HostConnection.swift @@ -115,6 +115,7 @@ final class HostConnection { private var eventTask: Task? private var connectTask: Task? private var retryTask: Task? + private var revalidateTask: Task? private var lanConnectTimeout: Task? private var reconnectAttempts = 0 private var seenSeq: Set = [] From 88a004ca1ea728e50452caf24a7a631350341b54 Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Mon, 6 Jul 2026 12:45:08 -0700 Subject: [PATCH 04/65] iOS Live Activity Updates Nucleic-Session: 665980BA-E8A5-4E84-9DF4-1757CF792353 Co-authored-by: Nucleic --- NucleicRemote/NucleicRemote/LiveActivityManager.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/NucleicRemote/NucleicRemote/LiveActivityManager.swift b/NucleicRemote/NucleicRemote/LiveActivityManager.swift index 7831ce0..25bd6e8 100644 --- a/NucleicRemote/NucleicRemote/LiveActivityManager.swift +++ b/NucleicRemote/NucleicRemote/LiveActivityManager.swift @@ -15,6 +15,13 @@ final class LiveActivityManager { /// still summarize everything, this just bounds the per-session list. private static let maxLines = 3 + /// How long the terminal "all clear / Done" glance lingers before the Activity dismisses. Long + /// enough to read at a glance, short enough not to loiter on the lock screen. + private static let doneLinger: Duration = .seconds(4) + /// The delayed dismissal after the Done glance is shown, cancelled if work resumes first — so a + /// completed run flashes "Done" and then clears instead of vanishing the instant it finishes. + private var dismissTask: Task? + private var activity: Activity? /// The last content we pushed. Updates that don't change it are skipped so we don't spend From 810d791bf945e2301362ba5d4d447fc80e95775e Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Mon, 6 Jul 2026 12:45:16 -0700 Subject: [PATCH 05/65] Network Reconnection Delay Nucleic-Session: 4BE3E40C-E939-444C-8AED-93C0AFE13C07 Co-authored-by: Nucleic --- .../NucleicRemote/Models/HostConnection.swift | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/NucleicRemote/NucleicRemote/Models/HostConnection.swift b/NucleicRemote/NucleicRemote/Models/HostConnection.swift index a0a929e..c58860d 100644 --- a/NucleicRemote/NucleicRemote/Models/HostConnection.swift +++ b/NucleicRemote/NucleicRemote/Models/HostConnection.swift @@ -689,6 +689,33 @@ final class HostConnection { } } + /// Foreground liveness check — the fast-reconnect path. iOS suspends the app when it's + /// backgrounded, which silently kills the socket; but no `.closed`/`.failed` reaches us while + /// we're frozen, so this connection can wake still marked `.connected`. Trusting that stale + /// state means nothing reconnects until the 55s keepalive deadline (or ~15s TCP keepalive) + /// finally notices — the several-second (or worse) stall on return. Instead: if we're already + /// not live, re-dial now with no backoff; if we *look* live, actively ping and re-dial the + /// instant the link fails to answer within a tight deadline. A pinned host only (a host + /// mid-pairing runs its own one-shot chain and has no `pinnedHost`). + func revalidate() { + guard let host = pinnedHost else { return } + guard connectivity.isLive, let client else { + reconnectAttempts = 0 + reconnect(to: host) + return + } + revalidateTask?.cancel() + revalidateTask = Task { [weak self] in + let alive = await client.probeAlive(within: .milliseconds(800)) + guard !Task.isCancelled, let self, self.client === client, + self.connectivity.isLive else { return } + if !alive { + self.reconnectAttempts = 0 + self.reconnect(to: host) + } + } + } + private func scheduleRetry(host: PairedHost?) { guard let host else { return } reconnectAttempts += 1 From 8c52dbdda0a3ca18dfba1814e6f0048e8d5ce905 Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Mon, 6 Jul 2026 12:45:18 -0700 Subject: [PATCH 06/65] iOS Live Activity Updates Nucleic-Session: 665980BA-E8A5-4E84-9DF4-1757CF792353 Co-authored-by: Nucleic --- NucleicRemote/NucleicRemote/LiveActivityManager.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/NucleicRemote/NucleicRemote/LiveActivityManager.swift b/NucleicRemote/NucleicRemote/LiveActivityManager.swift index 25bd6e8..4dd1dc3 100644 --- a/NucleicRemote/NucleicRemote/LiveActivityManager.swift +++ b/NucleicRemote/NucleicRemote/LiveActivityManager.swift @@ -51,9 +51,13 @@ final class LiveActivityManager { let needsYou = live.filter { $0.status.needsYou($0.disposition) } guard !running.isEmpty || !needsYou.isEmpty else { - end() + finishWithDoneGlance(hostName: hostName, live: live) return } + // Work is active again — abort any pending "Done" dismissal so the glance doesn't clear + // out from under a run that just resumed (or a new one that just started). + dismissTask?.cancel() + dismissTask = nil // Everything in flight or waiting on the user, attention-first (approvals, then waiting // input, then running), freshest within a rank. This is both the detail-row source and From 0bb4e453b774f1fe2be78ec7cf1c0e8bed63509b Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Mon, 6 Jul 2026 12:45:21 -0700 Subject: [PATCH 07/65] Network Reconnection Delay Nucleic-Session: 4BE3E40C-E939-444C-8AED-93C0AFE13C07 Co-authored-by: Nucleic --- NucleicRemote/NucleicRemote/Models/HostConnection.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/NucleicRemote/NucleicRemote/Models/HostConnection.swift b/NucleicRemote/NucleicRemote/Models/HostConnection.swift index c58860d..0644e40 100644 --- a/NucleicRemote/NucleicRemote/Models/HostConnection.swift +++ b/NucleicRemote/NucleicRemote/Models/HostConnection.swift @@ -730,6 +730,7 @@ final class HostConnection { func teardown() { retryTask?.cancel(); retryTask = nil + revalidateTask?.cancel(); revalidateTask = nil connectTask?.cancel(); connectTask = nil connectPlan = nil teardownClient() From ecfec2690f02ebf110f8e7581345ecee0be41555 Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Mon, 6 Jul 2026 12:45:27 -0700 Subject: [PATCH 08/65] Network Reconnection Delay Nucleic-Session: 4BE3E40C-E939-444C-8AED-93C0AFE13C07 Co-authored-by: Nucleic --- NucleicRemote/NucleicRemote/Models/RemoteStore.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/NucleicRemote/NucleicRemote/Models/RemoteStore.swift b/NucleicRemote/NucleicRemote/Models/RemoteStore.swift index 8260c82..50b4c47 100644 --- a/NucleicRemote/NucleicRemote/Models/RemoteStore.swift +++ b/NucleicRemote/NucleicRemote/Models/RemoteStore.swift @@ -3,6 +3,9 @@ import Network import SwiftUI import NucleicProtocol import NucleicTailnet +#if canImport(UIKit) +import UIKit +#endif /// On the phone there's no app-side `SessionSummary` view-model to collide with, so the wire /// type *is* the model. Alias it under the host's name so the shared vocabulary reads the same. From 41f0f62beb4b7ededd09b9b9cb2b843a2b607856 Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Mon, 6 Jul 2026 12:45:31 -0700 Subject: [PATCH 09/65] iOS Live Activity Updates Nucleic-Session: 665980BA-E8A5-4E84-9DF4-1757CF792353 Co-authored-by: Nucleic --- .../NucleicRemote/LiveActivityManager.swift | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) 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) { From 7b11a1862870bf3b034c71daf9bcc58d2ab2bd2f Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Mon, 6 Jul 2026 12:45:39 -0700 Subject: [PATCH 10/65] iOS Live Activity Updates Nucleic-Session: 665980BA-E8A5-4E84-9DF4-1757CF792353 Co-authored-by: Nucleic --- NucleicRemote/NucleicRemote/LiveActivityManager.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NucleicRemote/NucleicRemote/LiveActivityManager.swift b/NucleicRemote/NucleicRemote/LiveActivityManager.swift index 1507e4c..19ece0d 100644 --- a/NucleicRemote/NucleicRemote/LiveActivityManager.swift +++ b/NucleicRemote/NucleicRemote/LiveActivityManager.swift @@ -187,6 +187,8 @@ final class LiveActivityManager { updateTask = nil tokenObservation?.cancel() tokenObservation = nil + dismissTask?.cancel() + dismissTask = nil pendingState = nil lastState = nil let tracked = activity From f257e4d66fce3e5280558d15e72e2bcb755a5f35 Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Mon, 6 Jul 2026 12:45:40 -0700 Subject: [PATCH 11/65] Network Reconnection Delay Nucleic-Session: 4BE3E40C-E939-444C-8AED-93C0AFE13C07 Co-authored-by: Nucleic --- .../NucleicRemote/Models/RemoteStore.swift | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/NucleicRemote/NucleicRemote/Models/RemoteStore.swift b/NucleicRemote/NucleicRemote/Models/RemoteStore.swift index 50b4c47..6decb92 100644 --- a/NucleicRemote/NucleicRemote/Models/RemoteStore.swift +++ b/NucleicRemote/NucleicRemote/Models/RemoteStore.swift @@ -320,12 +320,48 @@ final class RemoteStore: ObservableObject { } /// Returning to the foreground: the network may have changed while the app was suspended (and - /// path/keepalive callbacks were frozen). Re-read the path and re-dial anything not live so the - /// transport is correct by the time the UI is on screen, rather than after a backoff. + /// path/keepalive callbacks were frozen). Re-read the path, re-dial anything not live, then + /// actively revalidate the connections that still *look* live — a socket the OS killed while we + /// were suspended wakes marked `.connected`, and trusting that would stall the reconnect until + /// the 55s keepalive deadline. `revalidate` pings each such link and re-dials the instant it + /// fails to answer, so the transport is correct (and fast) by the time the UI is on screen. func onForeground() { + endBackgroundHold() guard !demoMode, isPaired else { return } pathMonitor.refresh() + // Ensure a connection exists for every paired host (and drop unpaired); this re-dials the + // ones already known to be offline. reconnect() + // The zombies from suspension still read `.connected` — probe and re-dial the dead ones. + for conn in connections.values where conn.connectivity.isLive { conn.revalidate() } + } + + #if canImport(UIKit) + /// Held while backgrounded so iOS keeps the process (and thus the live sockets + 20s keepalive) + /// running for its short grant, instead of suspending us the moment we background. + private var backgroundTask: UIBackgroundTaskIdentifier = .invalid + #endif + + /// Entering the background: ask iOS to keep us running briefly so a quick app-switch (glance at + /// another app, tap a Live Activity, answer a banner) doesn't tear the connection down and force + /// a cold reconnect on return. The grant is short (~30s) and best-effort; once it lapses the OS + /// suspends us and the socket dies, which the next `onForeground` revalidation reconnects fast. + func onBackground() { + guard !demoMode else { return } + #if canImport(UIKit) + endBackgroundHold() + backgroundTask = UIApplication.shared.beginBackgroundTask(withName: "nucleic.sync.hold") { + [weak self] in self?.endBackgroundHold() + } + #endif + } + + private func endBackgroundHold() { + #if canImport(UIKit) + guard backgroundTask != .invalid else { return } + UIApplication.shared.endBackgroundTask(backgroundTask) + backgroundTask = .invalid + #endif } private func seedDemo() { From 468af9dd4a649c59fe4799175381672d6fe7bce0 Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Mon, 6 Jul 2026 12:45:55 -0700 Subject: [PATCH 12/65] Network Reconnection Delay Nucleic-Session: 4BE3E40C-E939-444C-8AED-93C0AFE13C07 Co-authored-by: Nucleic --- .../NucleicRemote/Models/RemoteStore.swift | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/NucleicRemote/NucleicRemote/Models/RemoteStore.swift b/NucleicRemote/NucleicRemote/Models/RemoteStore.swift index 6decb92..2cda3d5 100644 --- a/NucleicRemote/NucleicRemote/Models/RemoteStore.swift +++ b/NucleicRemote/NucleicRemote/Models/RemoteStore.swift @@ -76,6 +76,19 @@ final class RemoteStore: ObservableObject { private var openSessionHostID: String? @Published private(set) var openEvents: [AgentEvent] = [] @Published private(set) var openApprovals: [ApprovalRequest] = [] + + /// The open session is blocked on an approval whose details haven't arrived yet — the state + /// right after opening from a Live Activity or notification while the channel is still + /// (re)connecting. The summary says `.awaitingApproval`, but `openApprovals` (which is filled + /// from the live snapshot) is empty because there's no live channel yet. Drives a lightweight + /// "loading approval" placeholder so a tap from the lock screen lands on the request-in-progress + /// instead of a blank transcript; it flips off the instant the real card arrives (sub-second, + /// thanks to the fast foreground reconnect). Never shown once live — then the snapshot is truth. + var openApprovalLoading: Bool { + guard !connectivity.isLive, openApprovals.isEmpty, let id = openSessionID, + let summary = sessions.first(where: { $0.sessionID == id }) else { return false } + return summary.status == .awaitingApproval || summary.pendingApprovalCount > 0 + } /// Whether the compact (iPhone) Sessions tab currently has a session detail pushed. Distinct /// from `openSessionID`, which is the *data* subscription and is only torn down on the detail's /// `onDisappear` — and SwiftUI fires that at the *end* of the pop transition, so keying the From 9a0a46bc8baf02df822e64ad270aa9d3caf013ae Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Mon, 6 Jul 2026 12:46:02 -0700 Subject: [PATCH 13/65] Network Reconnection Delay Nucleic-Session: 4BE3E40C-E939-444C-8AED-93C0AFE13C07 Co-authored-by: Nucleic --- NucleicRemote/NucleicRemote/NucleicRemoteApp.swift | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/NucleicRemote/NucleicRemote/NucleicRemoteApp.swift b/NucleicRemote/NucleicRemote/NucleicRemoteApp.swift index 3546d9a..0337176 100644 --- a/NucleicRemote/NucleicRemote/NucleicRemoteApp.swift +++ b/NucleicRemote/NucleicRemote/NucleicRemoteApp.swift @@ -31,11 +31,18 @@ struct NucleicRemoteApp: App { // activity flag current so it only notifies for background transitions. .onChange(of: scenePhase) { _, phase in store.setScenePhaseActive(phase == .active) - if phase == .active { + switch phase { + case .active: // Re-plan transports immediately on foreground — the network may have // changed while suspended, so don't wait for a dead socket to notice. store.onForeground() Task { await HeartbeatReporter.reportIfDue() } + case .background: + // Hold the process (and its live sockets) briefly so a quick app-switch + // doesn't drop the connection and force a cold reconnect on return. + store.onBackground() + default: + break } } } From 303cdcc86d73f9eed7a90603de00d68ad35df620 Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Mon, 6 Jul 2026 12:46:13 -0700 Subject: [PATCH 14/65] Network Reconnection Delay Nucleic-Session: 4BE3E40C-E939-444C-8AED-93C0AFE13C07 Co-authored-by: Nucleic --- NucleicRemote/NucleicRemote/Views/SessionDetailView.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/NucleicRemote/NucleicRemote/Views/SessionDetailView.swift b/NucleicRemote/NucleicRemote/Views/SessionDetailView.swift index 7bbcefd..0c6c60a 100644 --- a/NucleicRemote/NucleicRemote/Views/SessionDetailView.swift +++ b/NucleicRemote/NucleicRemote/Views/SessionDetailView.swift @@ -310,6 +310,12 @@ struct SessionDetailView: View { } else { ApprovalCardView(approval: approval, availableHeight: availableHeight) } + } else if store.openApprovalLoading { + // Opened from a Live Activity / notification for a session blocked on approval, but + // the channel is still (re)connecting — bridge the gap with a placeholder so the tap + // lands on the request-in-progress, not a blank transcript. Replaced by the real + // ApprovalCardView the instant the snapshot arrives (sub-second reconnect). + approvalLoadingCard } else if let status = summary?.status { let running = status == .running let canCompose = running || status == .awaitingInput || status.isTerminal From d07ab7ec982bc096c0563a239910c399ad271b0e Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Mon, 6 Jul 2026 12:46:24 -0700 Subject: [PATCH 15/65] Network Reconnection Delay Nucleic-Session: 4BE3E40C-E939-444C-8AED-93C0AFE13C07 Co-authored-by: Nucleic --- .../Views/SessionDetailView.swift | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/NucleicRemote/NucleicRemote/Views/SessionDetailView.swift b/NucleicRemote/NucleicRemote/Views/SessionDetailView.swift index 0c6c60a..8247caf 100644 --- a/NucleicRemote/NucleicRemote/Views/SessionDetailView.swift +++ b/NucleicRemote/NucleicRemote/Views/SessionDetailView.swift @@ -375,6 +375,28 @@ struct SessionDetailView: View { } } + /// The placeholder shown while an approval's details are still loading — mirrors the approval + /// card's frame (orange warning header, glass surface) so the real `ApprovalCardView` swaps in + /// without a jump. Non-actionable: the real approval id isn't known yet, so there's nothing to + /// Allow/Deny until the snapshot lands. + private var approvalLoadingCard: some View { + VStack(alignment: .leading, spacing: 10) { + HStack(spacing: 6) { + Image(systemName: "exclamationmark.triangle.fill").foregroundStyle(.orange) + Text("Permission requested").font(.subheadline.weight(.semibold)) + } + HStack(spacing: 8) { + ProgressView().controlSize(.small) + Text("Connecting to your Mac to load this approval…") + .font(.footnote).foregroundStyle(.secondary) + Spacer(minLength: 0) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 14).padding(.vertical, 12) + .glassSurface(cornerRadius: 26) + } + /// A queued follow-up shown above the composer: its text (or attachment names) with the /// attachments as chips, and — at control scope — a ✕ that cancels just this message. @ViewBuilder From 5248bc0ff486fdce706b8a21f3de1d7e2531d7ae Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Mon, 6 Jul 2026 12:49:20 -0700 Subject: [PATCH 16/65] Network Reconnection Delay Nucleic-Session: 4BE3E40C-E939-444C-8AED-93C0AFE13C07 Co-authored-by: Nucleic --- NucleicRemote/NucleicRemote/Models/HostConnection.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/NucleicRemote/NucleicRemote/Models/HostConnection.swift b/NucleicRemote/NucleicRemote/Models/HostConnection.swift index 0644e40..7d8ce76 100644 --- a/NucleicRemote/NucleicRemote/Models/HostConnection.swift +++ b/NucleicRemote/NucleicRemote/Models/HostConnection.swift @@ -706,7 +706,10 @@ final class HostConnection { } revalidateTask?.cancel() revalidateTask = Task { [weak self] in - let alive = await client.probeAlive(within: .milliseconds(800)) + // A generous bound on a pong round-trip (LAN <10ms, relay <200ms) — only paid in full + // when the link is actually dead; a live one answers and exits in ~1 RTT. Kept well + // under the follow-on re-dial so the whole dead-link reconnect stays sub-second. + let alive = await client.probeAlive(within: .milliseconds(600)) guard !Task.isCancelled, let self, self.client === client, self.connectivity.isLive else { return } if !alive { From 7e61921a4a8327072797f016bc3f2b5c8bb0b15a Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Mon, 6 Jul 2026 12:53:22 -0700 Subject: [PATCH 17/65] Adjust Push Notification Logic Nucleic-Session: 0F065CD6-20FE-4941-ACF5-C57A4572A5A3 Co-authored-by: Nucleic --- NucleicRemote/NucleicRemote/Info.plist | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/NucleicRemote/NucleicRemote/Info.plist b/NucleicRemote/NucleicRemote/Info.plist index dccb57d..d5d5e4b 100644 --- a/NucleicRemote/NucleicRemote/Info.plist +++ b/NucleicRemote/NucleicRemote/Info.plist @@ -20,6 +20,14 @@ target's "Stamp git commit into Info.plist" build phase (git rev-parse --short HEAD). --> NucleicCommit + + UIBackgroundModes + + remote-notification + NSSupportsLiveActivities