Mesh P3: host switcher (switch active Mac) + 2-host demo

The safe, verifiable slice of iOS multi-host: switch which paired Mac the phone views, reusing the
existing (proven) single-connection reconnect — no rewrite of the connection state machine. The
simultaneous [HostID: HostConnection] multiplexer stays deferred (it needs two real Macs to verify).

RemoteStore: an `activeHostID` (the paired Mac the flat projection reflects); `hostChoices` (paired
registry live, mock hosts in demo); `switchHost(to:)` — live re-points via reconnect(), demo swaps
the mock host preserving in-demo edits; `reconnect()`/`pair()` set the active host; `unpair()` now
forgets the *active* Mac and switches to a remaining one if any (identical for a single host). Demo
seeds two mock Macs ("Andrew's Mac", "Studio Mac") with distinct sessions/dashboards.

SessionsView: a host-switcher menu in the toolbar, shown only when >1 Mac.

Verified in the iOS Simulator (demo mode): the switcher lists both Macs, and switching swaps the
whole session projection + the tab badge (screenshots taken). Single-host behavior is unchanged
(one host ⇒ switcher hidden ⇒ flat state exactly as before).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
2026-07-04 17:40:00 -07:00
co-authored by Claude Opus 4.8
parent 630380085e
commit 7b7e13aa20
2 changed files with 139 additions and 11 deletions
@@ -79,6 +79,27 @@ struct SessionsView: View {
// A notification tap while this tab is already up.
.onChange(of: store.pendingRoute) { consumeRoute() }
.toolbar {
// Host switcher (mesh P3): pick which paired Mac to view. Hidden with a single Mac.
ToolbarItem(placement: .topBarLeading) {
if store.hostChoices.count > 1 {
Menu {
ForEach(store.hostChoices) { choice in
Button {
store.switchHost(to: choice.id)
} label: {
Label(choice.name,
systemImage: choice.isActive ? "checkmark" : "desktopcomputer")
}
}
} label: {
HStack(spacing: 4) {
Image(systemName: "desktopcomputer")
Text(store.hostName).lineLimit(1)
Image(systemName: "chevron.down").font(.caption2)
}
}
}
}
ToolbarItem(placement: .topBarTrailing) {
Button {
showArchived.toggle()