Merge nucleic/warm-coral-viper-sn0m into dev

This commit is contained in:
2026-08-04 04:18:59 -07:00
parent 53e2b8bce4
commit ef48cce36f
2 changed files with 11 additions and 5 deletions
@@ -352,10 +352,13 @@ struct SessionDetailView: View {
@ViewBuilder @ViewBuilder
private func modelEffortMenu(_ summary: WireSessionSummary) -> some View { private func modelEffortMenu(_ summary: WireSessionSummary) -> some View {
if !store.routesIntelligence { 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) ModelMenu(model: modelBinding, catalog: store.modelCatalog, backend: summary.backend)
.id(summary.backend)
EffortMenu(effort: effortBinding, catalog: store.modelCatalog, 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. // shows that backend's effort range rather than collapsing to one level.
modelSKU: summary.model ?? store.modelCatalog.models(for: summary.backend).first?.sku, modelSKU: summary.model ?? store.modelCatalog.models(for: summary.backend).first?.sku,
controlled: controlled, controlled: controlled,
@@ -368,7 +371,7 @@ struct SessionDetailView: View {
// MARK: - The in-session Intelligence rail // 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 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. /// 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. /// `pendingIntelligence` covers only the round trip, and `intelligencePreview` the drag.
private var sessionIntelligence: Int { private var sessionIntelligence: Int {
@@ -433,6 +436,7 @@ struct SessionDetailView: View {
.onChange(of: summary.routedLevel) { _, _ in pendingIntelligence = nil } .onChange(of: summary.routedLevel) { _, _ in pendingIntelligence = nil }
.onChange(of: summary.model) { _, _ in pendingIntelligence = nil } .onChange(of: summary.model) { _, _ in pendingIntelligence = nil }
.onChange(of: summary.effort) { _, _ 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 — // 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 // 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 // rail would stay parked on a stop the chat never reached, so it springs back here
@@ -477,8 +477,10 @@ extension WireModelCatalog {
/// A compact model selector driven by the wire catalog (SYNC §5.2). `backend == nil` — the /// 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 /// 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 /// dividers; an in-session `backend` shows only the models for the session summary's *current*
/// provider's models. Selecting sets the SKU; the binding's `nil` means "host default". /// 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 { struct ModelMenu: View {
@Binding var model: String? @Binding var model: String?
let catalog: WireModelCatalog let catalog: WireModelCatalog