From 4434bb09c1e58c82fbdaf275919a76a20de2cca2 Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Mon, 6 Jul 2026 14:41:56 -0700 Subject: [PATCH] Read App Intents Documentation Nucleic-Session: 56C5F781-4444-466B-91CE-A7734A8E117F Co-authored-by: Nucleic --- .../NucleicRemote/Models/RemoteStore.swift | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/NucleicRemote/NucleicRemote/Models/RemoteStore.swift b/NucleicRemote/NucleicRemote/Models/RemoteStore.swift index f7fe423..3510394 100644 --- a/NucleicRemote/NucleicRemote/Models/RemoteStore.swift +++ b/NucleicRemote/NucleicRemote/Models/RemoteStore.swift @@ -1225,6 +1225,59 @@ final class RemoteStore: ObservableObject { func setSessionAutoShip(_ id: SessionID, _ autoShip: Bool) { send(.setSessionAutoShip(id, autoShip)) } func setSessionShipBranch(_ id: SessionID, _ branch: String?) { send(.setSessionShipBranch(id, branch)) } + // MARK: - App Intents support + + /// Bring networking online and dial the paired host(s) for an App Intent that runs in a *cold* + /// background process (Siri / Shortcuts / a widget or Live Activity button), where the SwiftUI + /// scene never mounts and `onAppear` never fires. Mirrors the push handler's silent-launch + /// bootstrap (`startNetworkingIfNeeded` + `reconnect`). Idempotent; a no-op in demo mode. + func bootstrapForIntent() { + guard !demoMode else { return } + startNetworkingIfNeeded() + if isPaired { reconnect() } + } + + /// Whether any paired Mac currently has a live channel. + var hasLiveConnection: Bool { connections.values.contains { $0.connectivity.isLive } } + + /// Await a live connection to any paired Mac, up to `timeout` seconds — an intent must act over a + /// *live* channel or fail clean (UX_IOS §6, §3.1). Brings networking up first if it's cold, then + /// polls until a link comes up or the deadline passes. Returns whether a link is live. + func awaitLiveConnection(timeout: TimeInterval = 6) async -> Bool { + if demoMode || hasLiveConnection { return true } + bootstrapForIntent() + let deadline = Date().addingTimeInterval(timeout) + while Date() < deadline { + try? await Task.sleep(for: .milliseconds(200)) + if hasLiveConnection { return true } + } + return hasLiveConnection + } + + /// Resolve an approval by id from an App Intent with a full `Decision`. Like + /// `respondFromNotification`, it prefers the owning Mac, else broadcasts to every live Mac (the + /// owner resolves; the rest see an unknown / `alreadyResolved` id and no-op), and queues briefly + /// on a dropped link so a decision made just as the socket blips still lands. `sessionID` (when + /// the surface knows it) routes directly. Returns whether it went out over a live channel now. + @discardableResult + func respondToApproval(id: ApprovalID, sessionID: SessionID?, decision: Decision) -> Bool { + if demoMode { demoHandle(.approvalRespond(id, decision)); return true } + if let sessionID, let conn = connection(owningSession: sessionID), conn.connectivity.isLive { + conn.send(.approvalRespond(id, decision)) + openApprovals.removeAll { $0.id == id } + return true + } + let live = connections.values.filter { $0.connectivity.isLive } + if !live.isEmpty { + for conn in live { conn.send(.approvalRespond(id, decision)) } + openApprovals.removeAll { $0.id == id } + return true + } + pendingNotificationDecision = (id, decision, Date()) + reconnect() + return false + } + // MARK: - Plumbing /// Route an intent to the Mac that owns its target (mesh P3). Sessions/projects/to-dos are shown