nvrsion: Add: Global Icon For Remote Projects to display globe icon next to remote projects in sidebar, matching local projects in appearance, Fix: Chat: Remove Face ID Permission Text, Refactor: Chat: Live Activity Content Density to open app directly into waiting sessions when tapped, Refactor: Chat: Live Activity Content Density to refresh live activity info significantly faster, Refactor: Chat: Live Activity Content Density to refresh live activity info significantly faster, Fix: Chat: Live Activity Content Density to refresh live activity info significantly faster, Fix: Chat: Network Type Transition Optimization to reduce delay during network type switches, Feature Enhancement Planning: All devices in a mesh group should see and be aware of each other; “pair” reframed into “join”; scanning

Nucleic-Promote: 1
Co-authored-by: Nucleic <[email protected]>
This commit is contained in:
2026-07-05 00:28:21 -07:00
co-authored by Nucleic
parent 88329de96a
commit 368978bea8
15 changed files with 731 additions and 158 deletions
@@ -60,6 +60,9 @@ final class HostConnection {
var wireError: (WireError) -> Void = { _ in }
/// A pairing handshake succeeded — persist the pinned host record.
var didPair: (PairedHost) -> Void = { _ in }
/// The mesh roster changed (mesh "join"): a Mac was learned or revoked via gossip — the
/// registry was updated, so (re)connect to every paired Mac and drop any that left.
var meshRosterChanged: () -> Void = {}
/// The embedded Tailscale node's status changed (for Settings).
var tailnetStatus: (String?, URL?) -> Void = { _, _ in }
}
@@ -69,6 +72,7 @@ final class HostConnection {
private let identity: DeviceIdentity
private let discovery: LANDiscovery
private let pathMonitor: NetworkPathMonitor
// MARK: Connection machine (moved from RemoteStore, one per host)
@@ -99,12 +103,14 @@ final class HostConnection {
init(
hostID: String, hostName: String,
identity: DeviceIdentity, discovery: LANDiscovery, callbacks: Callbacks
identity: DeviceIdentity, discovery: LANDiscovery, pathMonitor: NetworkPathMonitor,
callbacks: Callbacks
) {
self.hostID = hostID
self.hostName = hostName
self.identity = identity
self.discovery = discovery
self.pathMonitor = pathMonitor
self.callbacks = callbacks
}
@@ -171,13 +177,50 @@ final class HostConnection {
callbacks.didUpdate()
}
/// Translate a gossiped mesh member (mesh "join") into a pinnable/dialable `PairedHost`. The
/// member carries the host static key + last-known addresses — everything IK reconnect needs.
/// No relay token yet (the phone never directly paired this Mac); it's adopted from the host's
/// `relayMembership` push after the first LAN/tailnet contact, so first contact needs one of
/// those paths.
static func pairedHost(from member: MeshMember) -> PairedHost {
let (lanHost, lanPort) = splitHostPort(member.addresses?.lanHint)
let tailnetHost = member.addresses?.tailnet
let hasTailnet = !(tailnetHost?.isEmpty ?? true)
return PairedHost(
deviceID: IdentityStore.deviceID(),
hostName: member.label,
hostStaticKey: member.staticPublicKey,
fingerprint: member.staticPublicKey.fingerprintHex,
lanHost: lanHost, lanPort: lanPort,
transport: (hasTailnet ? SyncTransportHint.tailnet : .lan).rawValue,
tailnetHost: hasTailnet ? tailnetHost : nil,
// The sync port on a tailnet is fixed protocol-wide (SyncTransportSetting.tailnetPort);
// a gossiped address carries only the IP.
tailnetPort: hasTailnet ? 43_753 : nil,
relayRoomID: member.addresses?.relayRoomID,
relayMembershipToken: nil, relayURL: nil)
}
/// Split a "host:port" hint (last-colon split so a bracketed IPv6 host survives).
private static func splitHostPort(_ hint: String?) -> (String?, UInt16?) {
guard let hint, let colon = hint.lastIndex(of: ":"),
let port = UInt16(hint[hint.index(after: colon)...]) else { return (nil, nil) }
var host = String(hint[..<colon])
if host.hasPrefix("["), host.hasSuffix("]") { host = String(host.dropFirst().dropLast()) }
return (host.isEmpty ? nil : host, port)
}
private func buildCandidates(
fingerprint: String?, lanHost: String?, lanPort: UInt16?,
tailnet: (host: String?, port: UInt16?)?,
relay: (membershipToken: String?, url: String?)? = nil
) -> [TransportAttempt] {
var candidates: [TransportAttempt] = []
if let endpoint = discovery.endpoint(forFingerprint: fingerprint, lanHost: lanHost, lanPort: lanPort) {
// Only offer LAN when the device actually has a LAN-capable path (Wi-Fi/wired). On cellular
// the pinned `lanHost:lanPort` is unreachable, so including it here would just burn the
// connect timeout before falling through — skip it and dial tailnet/relay immediately.
if pathMonitor.canUseLAN,
let endpoint = discovery.endpoint(forFingerprint: fingerprint, lanHost: lanHost, lanPort: lanPort) {
candidates.append(.lan(endpoint))
}
if let tailnet, let host = tailnet.host, let port = tailnet.port, TailnetSupport.isBuiltIn {
@@ -280,7 +323,10 @@ final class HostConnection {
channel: channel, identity: identity, hostStaticKey: hostStaticKey,
mode: mode, deviceID: deviceID,
deviceLabel: UIDevice.current.name, pushToken: PushRegistrar.shared.tokenHex,
releaseChannel: BuildInfo.current.channel.releaseChannel)
releaseChannel: BuildInfo.current.channel.releaseChannel,
// Mesh "join": advertise roster gossip so a host pushes its group view — the phone
// then auto-learns and connects to every Mac in the mesh, not just the one it scanned.
clientCaps: WireClientCapabilities(mesh: 1, canSyncRoster: true))
self.client = client
consume(client, pairingPayload: pairingPayload)
}
@@ -421,9 +467,37 @@ final class HostConnection {
case .peerList(let peers):
meshPeers = peers
callbacks.didUpdate()
case .meshRoster(let push):
// Mesh "join": the host gossiped its group view. Auto-learn every Mac member into the
// paired-hosts registry (so the multiplexer connects to all of them), and drop any the
// group revoked. Phone members are ignored — a phone doesn't dial other phones.
var changed = false
for member in push.members where member.kind == .mac || member.capabilities.canHost {
// A mac deviceID is `sha256(staticPublicKey)`; reject a forged (id, key) pair.
guard DeviceIdentity.hostID(ofStaticKey: member.staticPublicKey) == member.deviceID
else { continue }
let fingerprint = member.staticPublicKey.fingerprintHex
guard fingerprint != self.hostID else { continue } // that's the Mac we're on
if IdentityStore.pairedHost(id: fingerprint) == nil { changed = true }
IdentityStore.mergePairedHost(Self.pairedHost(from: member))
}
for tombstone in push.tombstones {
// fingerprint = first 16 hex of the hostID (sha256 prefix), the registry key.
let fingerprint = String(tombstone.deviceID.prefix(16))
if IdentityStore.pairedHost(id: fingerprint) != nil {
IdentityStore.removePairedHost(id: fingerprint)
changed = true
}
}
if changed { callbacks.meshRosterChanged() }
case .transferAccept, .transferReject, .transferReady, .transferCommitted, .transferChunkAck:
// Session-transfer replies (mesh P5) only reach a *source* Mac; a phone is never one.
break
case .transcriptChunk, .transcriptFetchComplete, .transcriptUnavailable:
// Full-transcript fetch replies (mesh sync) only arrive in response to a
// `fetchTranscript` this device sent. A phone pulls full history on demand via a
// dedicated fetch path (not this subscribe stream), so nothing to do here yet.
break
case .relayMembership(let membership):
// The host issued/refreshed this device's relay credential (mesh P2). Persist it
// in place (no reordering — this can arrive from a non-active Mac) so the relay
@@ -476,6 +550,27 @@ final class HostConnection {
Task { await client.send(msg) }
}
/// The device's network path changed (Wi-Fi ⇄ cellular, joined/left a network). React now rather
/// than waiting for a zombie LAN socket to time out or the reconnect backoff to elapse — this is
/// what makes the transport switch feel immediate. Only reconnect-managed hosts (a pinned host)
/// re-plan here; a host mid-pairing runs its own one-shot candidate chain.
func networkPathChanged() {
guard let host = pinnedHost else { return }
// Using (or dialing) LAN but LAN is no longer reachable → the socket is dead in all but name.
// Drop it and re-plan now; `buildCandidates` skips LAN and dials tailnet/relay.
if activeTransport == .lan, !pathMonitor.canUseLAN {
reconnectAttempts = 0
reconnect(to: host)
return
}
// Offline/reconnecting and the network just came back or changed → retry immediately instead
// of sitting out the remaining backoff.
if !connectivity.isLive, pathMonitor.isSatisfied {
reconnectAttempts = 0
reconnect(to: host)
}
}
private func scheduleRetry(host: PairedHost?) {
guard let host else { return }
reconnectAttempts += 1