fix(tui): keep the pending model shown at turn end instead of blipping back (#74766)

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.
This commit is contained in:
brooklyn! 2026-07-30 05:32:55 -05:00 committed by GitHub
parent 206eda50a5
commit d8a9c17dae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 45 additions and 3 deletions

View file

@ -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

View file

@ -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,