fix(client/apple): the iOS slice hit the type-checker wall, and tvOS couldn't be checked at all

`ContentView.body` grew two modifiers and stopped type-checking on the iOS slice —
"unable to type-check this expression in reasonable time", a failure macOS builds
never show and the one Apple CI never builds. Split into the screen plus its
lifecycle drivers, then the prompt chain, with each alert's presentation Binding
lifted out the way the deep-link one already was.

tvOS couldn't be checked from the command line at all: HomeView imports the slide
transition, which ships with the Xcode project only because its manifest breaks
SwiftPM's whole-graph validation. `canImport` instead of a bare `os(tvOS)` gate
makes the tvOS sources compile without it, and the app still gets the transition
because there the module is present. Both slices now typecheck by hand.

The scope switcher is tvOS-gated with them: a name prompt and a nested management
menu are not what a remote does well, and §5.4 keeps profile EDITING off
controller-first surfaces in v1 — they honor bindings and render pinned cards.

Also: release the session's settings latch when a connect fails, is refused, or is
abandoned mid-handshake. Only `disconnect` cleared it, so a failed dial left the
would-be session's resolution latched over the globals.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-29 12:06:41 +02:00
co-authored by Claude Opus 5
parent 25b127803f
commit 0a32f4eab1
5 changed files with 123 additions and 87 deletions
@@ -361,6 +361,9 @@ final class SessionModel: ObservableObject {
if case .success(let conn) = result {
Task.detached { conn.close() } // joins Rust threads off-main
}
// A LATER connect has already latched its own settings; only release the
// latch when nothing took over, or this would blank the live session's.
if self.phase == .idle { SessionSettings.end() }
return
}
switch result {
@@ -383,12 +386,14 @@ final class SessionModel: ObservableObject {
Task.detached { conn.close() } // joins Rust threads off-main
self.phase = .idle
self.activeHost = nil
SessionSettings.end() // no session, so nothing may keep its settings latched
self.errorMessage = "\(host.displayName) is not paired yet. "
+ "Pair with its PIN before streaming."
}
case .failure(let error):
self.phase = .idle
self.activeHost = nil
SessionSettings.end() // the dial failed back to the plain globals
if case PunktfunkClientError.rejected(let rejection) = error {
// The host answered and stated its reason (declined / approval timed
// out / busy / versions differ) show that, and never wake-retry a