From d8a9c17dae8dbff598866336752989a26200d56e Mon Sep 17 00:00:00 2001 From: brooklyn! Date: Thu, 30 Jul 2026 05:32:55 -0500 Subject: [PATCH] fix(tui): keep the pending model shown at turn end instead of blipping back (#74766) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A model picked mid-turn is applied at the next turn start, but the end-of-turn session.info (_emit_settled_session_info) reads the still-live agent — the OLD model — and clobbered the optimistic paint, so the UI showed the new model, then snapped back to the old one when the turn ended, then switched for real on the next turn. Report the queued pick's model/provider in _session_info while a switch is pending (it IS the model the next turn runs), so the display stays on the user's choice through the settle. Cleared once the switch applies. --- tests/test_tui_gateway_server.py | 24 ++++++++++++++++++++++++ tui_gateway/server.py | 24 +++++++++++++++++++++--- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/tests/test_tui_gateway_server.py b/tests/test_tui_gateway_server.py index 0904c237002..ada479fafbb 100644 --- a/tests/test_tui_gateway_server.py +++ b/tests/test_tui_gateway_server.py @@ -8439,6 +8439,30 @@ def test_session_info_includes_session_title(monkeypatch): assert info["title"] == "Dashboard title" +def test_session_info_reports_pending_model_switch(monkeypatch): + """A model queued mid-turn shows as the session's model in session.info, so + the end-of-turn settle doesn't blip the UI back to the still-live old model + before the switch applies at the next turn start.""" + agent = types.SimpleNamespace(tools=[], model="old/model", provider="openai") + session = { + "session_key": "", + "history": [], + "pending_model_switch": { + "raw": "new/model --provider anthropic", + "display_model": "new/model", + "display_provider": "anthropic", + }, + } + + info = server._session_info(agent, session) + assert info["model"] == "new/model" + assert info["provider"] == "anthropic" + + # With nothing queued the live agent model wins, as before. + session.pop("pending_model_switch") + assert server._session_info(agent, session)["model"] == "old/model" + + # --------------------------------------------------------------------------- # History-mutating commands must reject while session.running is True. # Without these guards, prompt.submit's post-run history write either diff --git a/tui_gateway/server.py b/tui_gateway/server.py index b3be3ee1b0d..ca5876ac00b 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -4768,9 +4768,18 @@ def _session_info(agent, session: dict | None = None) -> dict: yolo = bool(_YOLO_MODE_FROZEN) or session_yolo or approval_mode == "off" except Exception: yolo = False + # A model switch queued mid-turn (pending_model_switch) applies at the next + # turn start, so agent.model still reads the OLD model until then. Report the + # pending pick instead — it's the model the next turn will run, and it stops + # the end-of-turn settle from blipping the UI back to the old model before + # the switch lands. Cleared once _apply_pending_model_switch consumes it. + pending_switch = (session or {}).get("pending_model_switch") or {} + pending_model = str(pending_switch.get("display_model") or "").strip() + pending_provider = str(pending_switch.get("display_provider") or "").strip() info: dict = { - "model": mirror.get("model", getattr(agent, "model", "")), - "provider": mirror.get("provider", getattr(agent, "provider", "")), + "model": pending_model or mirror.get("model", getattr(agent, "model", "")), + "provider": pending_provider + or mirror.get("provider", getattr(agent, "provider", "")), "reasoning_effort": reasoning_effort, "service_tier": service_tier, "fast": service_tier == "priority", @@ -10012,8 +10021,9 @@ def _(rid, params: dict) -> dict: # The user gets to pick, keep typing, and send the next turn on # the new model without waiting for the swap or interrupting. if session.get("running"): + parsed = parse_model_switch_args(value) try: - pending_model = parse_model_switch_args(value).model_input + pending_model = parsed.model_input except Exception: pending_model = str(value) session["pending_model_switch"] = { @@ -10021,6 +10031,14 @@ def _(rid, params: dict) -> dict: "confirm_expensive_model": bool( params.get("confirm_expensive_model", False) ), + # The resolved model/provider the next turn will run on. + # _session_info reports these while the switch is pending + # so the end-of-turn settle keeps showing the user's pick + # instead of blipping back to the still-live old model. + "display_model": pending_model, + "display_provider": ( + getattr(parsed, "explicit_provider", "") or "" + ).strip(), } return _ok( rid,