From ef48cce36f5a443aeb8179c202b34669cce6eaf9 Mon Sep 17 00:00:00 2001 From: Nucleic Date: Tue, 4 Aug 2026 04:18:59 -0700 Subject: [PATCH] Merge nucleic/warm-coral-viper-sn0m into dev --- .../NucleicRemote/Views/SessionDetailView.swift | 10 +++++++--- NucleicRemote/NucleicRemote/Views/Theme.swift | 6 ++++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/NucleicRemote/NucleicRemote/Views/SessionDetailView.swift b/NucleicRemote/NucleicRemote/Views/SessionDetailView.swift index 061b1ef..edf9891 100644 --- a/NucleicRemote/NucleicRemote/Views/SessionDetailView.swift +++ b/NucleicRemote/NucleicRemote/Views/SessionDetailView.swift @@ -352,10 +352,13 @@ struct SessionDetailView: View { @ViewBuilder private func modelEffortMenu(_ summary: WireSessionSummary) -> some View { if !store.routesIntelligence { - // A session's backend is fixed at creation, so only same-backend models are offered. + // Context Switch can replace the backend between turns. The summary is the live source + // of truth, and the explicit identity rebuilds a native Menu that happened to be open + // across the update instead of retaining its old provider's snapshot. ModelMenu(model: modelBinding, catalog: store.modelCatalog, backend: summary.backend) + .id(summary.backend) EffortMenu(effort: effortBinding, catalog: store.modelCatalog, - // No explicit model yet → use the session backend's default, so the menu + // No explicit model yet → use the current backend's default, so the menu // shows that backend's effort range rather than collapsing to one level. modelSKU: summary.model ?? store.modelCatalog.models(for: summary.backend).first?.sku, controlled: controlled, @@ -368,7 +371,7 @@ struct SessionDetailView: View { // MARK: - The in-session Intelligence rail /// The stop this chat's rail sits at. The host owns it — moving the rail sends the stop and - /// the host re-routes within the chat's fixed backend lane, then broadcasts the resulting + /// the host re-routes within the chat's current backend lane, then broadcasts the resulting /// model/effort — so the committed value is *derived* from the summary rather than held here. /// `pendingIntelligence` covers only the round trip, and `intelligencePreview` the drag. private var sessionIntelligence: Int { @@ -433,6 +436,7 @@ struct SessionDetailView: View { .onChange(of: summary.routedLevel) { _, _ in pendingIntelligence = nil } .onChange(of: summary.model) { _, _ in pendingIntelligence = nil } .onChange(of: summary.effort) { _, _ in pendingIntelligence = nil } + .onChange(of: summary.backend) { _, _ in pendingIntelligence = nil } // A refused move — every candidate for that stop is inside a reached quota window — // changes nothing at all; the host answers with an error instead. Without this the // rail would stay parked on a stop the chat never reached, so it springs back here diff --git a/NucleicRemote/NucleicRemote/Views/Theme.swift b/NucleicRemote/NucleicRemote/Views/Theme.swift index 1b4c85c..cba05fd 100644 --- a/NucleicRemote/NucleicRemote/Views/Theme.swift +++ b/NucleicRemote/NucleicRemote/Views/Theme.swift @@ -477,8 +477,10 @@ extension WireModelCatalog { /// A compact model selector driven by the wire catalog (SYNC §5.2). `backend == nil` — the /// home composer, where the model also selects the backend — shows every provider group with -/// dividers; a fixed `backend` (in-session, where the backend is immutable) shows only that -/// provider's models. Selecting sets the SKU; the binding's `nil` means "host default". +/// dividers; an in-session `backend` shows only the models for the session summary's *current* +/// provider. Context Switch may change that field between turns, so callers pass the live summary +/// rather than retaining the creation-time lane. Selecting sets the SKU; the binding's `nil` means +/// "host default". struct ModelMenu: View { @Binding var model: String? let catalog: WireModelCatalog