Skip to content

Commit 0787101

Browse files
lawrencecchenclaude
andcommitted
Auto-attach: restore manual-supersede under main's validation split; enable presence-online-preferred via live presence (#5792)
Two post-rebase changes: 1. Manual pairing must supersede an in-flight auto-attach at the TOP of connectPairingURLResult, before validation, so even an invalid code parks the background attempt (the documented invariant the manualPairingSupersedes* / invalidManualHost* tests assert). Main's #6028 refactor split the post-validation beginPairingAttempt(method: "qr") out of the early path, so the supersede no longer rode on it. Call supersedeInFlightAutoAttach() explicitly; it is guarded on autoAttachInFlight, so it is a no-op when nothing is parked. 2. Presence #5792 has landed, so wire its live signal into auto-attach target selection (deferred when the PR was opened). The shell already owns a live PresenceMap from the device-tree subscription; derive online device ids from it (new PresenceMap.onlineDeviceIDs, same rollup rule as deviceSummary) when no explicit MobileAutoAttachPresence provider is injected and presence data exists. The external provider seam still wins if set, and an empty map (no presence yet) keeps the recency-only path unchanged, so existing behavior and tests are preserved. This stays behind the auto-attach flag. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent fb2b2b0 commit 0787101

3 files changed

Lines changed: 52 additions & 4 deletions

File tree

Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1665,7 +1665,19 @@ public final class MobileShellComposite: MobileTerminalOutputSinking {
16651665
await loadRegistryDevices()
16661666
guard stillCurrent() else { return false }
16671667

1668-
let presenceOnline = await autoAttachPresence?.onlineDeviceIDs()
1668+
// Prefer an explicitly injected presence provider; otherwise fall back to
1669+
// the shell's own live presence stream (#5792), which the device tree
1670+
// already subscribes. Either way the selector prefers a single online Mac
1671+
// and treats multiple online Macs as ambiguous. `nil` (no provider AND no
1672+
// presence data yet) keeps the recency-only path unchanged.
1673+
let presenceOnline: Set<String>?
1674+
if let provided = await autoAttachPresence?.onlineDeviceIDs() {
1675+
presenceOnline = provided
1676+
} else if !presenceMap.isEmpty {
1677+
presenceOnline = presenceMap.onlineDeviceIDs()
1678+
} else {
1679+
presenceOnline = nil
1680+
}
16691681
guard stillCurrent() else { return false }
16701682

16711683
// On a physical phone, reject loopback routes: a `127.0.0.1` route names
@@ -2530,9 +2542,13 @@ public final class MobileShellComposite: MobileTerminalOutputSinking {
25302542
_ rawValue: String? = nil,
25312543
acceptedVersionWarning: Bool
25322544
) async -> MobilePairingURLConnectionResult {
2533-
// `beginPairingAttempt` below supersedes any in-flight auto-attach (and
2534-
// resolves its restoring gate), so a background attempt that resumes
2535-
// mid-pairing cannot invalidate this manual attempt.
2545+
// A user-initiated manual pairing supersedes any in-flight auto-attach
2546+
// (and resolves its restoring gate) here at the top, BEFORE validation, so
2547+
// even an invalid code parks the background attempt and a later resume
2548+
// cannot invalidate this manual attempt. (Main split the post-validation
2549+
// `beginPairingAttempt(method: "qr")` out of this early path, so the
2550+
// supersede no longer rides on it and must fire explicitly.)
2551+
supersedeInFlightAutoAttach()
25362552
let rawURL = Self.normalizedPairingURL(rawValue ?? pairingCode)
25372553
_ = beginPairingValidationAttempt()
25382554
connectionAttemptGeneration = UUID()

Packages/CmuxMobileShell/Sources/CmuxMobileShell/PresenceMap.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,19 @@ public struct PresenceMap: Equatable, Sendable {
8686
lastSeenAt: Date(timeIntervalSince1970: lastSeenMs / 1000)
8787
)
8888
}
89+
90+
/// The device ids the live presence stream currently reports online (any
91+
/// instance online), using the same rollup rule as ``deviceSummary``. This
92+
/// is the auto-attach target selector's presence signal: it prefers a single
93+
/// live Mac and treats 2+ live Macs as ambiguous instead of guessing on
94+
/// recency. Empty when no presence data has streamed yet, which the caller
95+
/// distinguishes from "data exists, nobody online" via ``isEmpty``.
96+
public func onlineDeviceIDs() -> Set<String> {
97+
var ids: Set<String> = []
98+
for (deviceId, instances) in instancesByDevice
99+
where instances.values.contains(where: { $0.online }) {
100+
ids.insert(deviceId)
101+
}
102+
return ids
103+
}
89104
}

Packages/CmuxMobileShell/Tests/CmuxMobileShellTests/PresenceMapTests.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,4 +116,21 @@ import Testing
116116
#expect(map.instance(deviceId: "mac-a", tag: "ghost") == nil)
117117
#expect(map.deviceSummary(deviceId: "mac-z") == nil)
118118
}
119+
120+
@Test func onlineDeviceIDsRollsUpAnyOnlineInstancePerDevice() {
121+
var map = PresenceMap()
122+
// mac-a: one online instance; mac-b: only offline instances; mac-c: a
123+
// mix (offline default + online build tag) so the device counts as online.
124+
map.apply(snapshot([
125+
instance(deviceId: "mac-a", tag: "default", online: true),
126+
instance(deviceId: "mac-b", tag: "default", online: false),
127+
instance(deviceId: "mac-c", tag: "default", online: false),
128+
instance(deviceId: "mac-c", tag: "beta", online: true),
129+
]))
130+
#expect(map.onlineDeviceIDs() == ["mac-a", "mac-c"])
131+
// An empty map yields no online ids; the auto-attach caller distinguishes
132+
// this "no presence data yet" case from "data exists, nobody online" via
133+
// `isEmpty`, so it can fall back to recency only when truly unseeded.
134+
#expect(PresenceMap().onlineDeviceIDs().isEmpty)
135+
}
119136
}

0 commit comments

Comments
 (0)