Stop a mid-turn rename offering to switch to the conversation on screen - #4857
Stop a mid-turn rename offering to switch to the conversation on screen#4857springfall2008 wants to merge 5 commits into
Conversation
The "Replying in 'X' - switch to it" banner exists to say a reply is running
in a conversation the user is NOT looking at. setBusy() got that right, but
handleTitle() restated the same rule inverted:
if (state.busy && state.busy.conversation_id === state.conversation) {
showBanner(state.busy.conversation_id, data.title);
}
'title' events are scoped server-side to the conversation being viewed, so
that branch is true exactly when the busy conversation is the one already
open - the case setBusy() deliberately hides the banner for. The moment the
model named a new conversation with set_chat_title, the banner appeared
offering to switch the user to the transcript in front of them.
The same handler also never updated state.titles, which updateChatTitle()
reads, so the header went on saying "New chat" while the row in the list
already showed the real name - both halves of the reported screenshot.
Fixed by giving the decision a single owner, refreshBanner(), which setBusy()
and handleTitle() both route through; restating it per call site is what let
the two drift apart. handleTitle() now refreshes the header and keeps
state.busy.title current, so switching away afterwards shows the banner with
the name the conversation actually has, rather than the one it was created
with.
Verified before and after by running the real generated script in a DOM
shim: on the same conversation a title event took the banner from visible to
hidden, the header title from undefined to the new name, and a reply in a
different conversation still raises the banner.
The existing banner test asserted the rule inside setBusy(), so it is
retargeted at refreshBanner() and additionally checks setBusy() still
delegates - the coverage moves with the rule rather than being dropped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🟢 Approval recommended
The behavioral fix is localized, aligns setBusy() and handleTitle() behind a shared decision point, and is covered by targeted regression tests (only a minor comment wording nit noted).
Pull request overview
This PR fixes a UI logic mismatch in the Chat tab where a mid-turn conversation rename could incorrectly display the “Replying in ‘X’ — switch to it” banner for the currently open conversation, and could also leave the header title stale. The change centralizes the banner visibility decision and ensures title events update both the conversation list row and the header state.
Changes:
- Introduce
refreshBanner()as the single owner of the banner show/hide decision and routesetBusy()/handleTitle()through it. - Update
handleTitle()to keepstate.titles(and thus the header viaupdateChatTitle()) in sync with server title events, and to keepstate.busy.titlecurrent when applicable. - Add/retarget tests to assert the banner rule is centralized and that title events don’t trigger a “switch to it” banner for the open conversation.
File summaries
| File | Description |
|---|---|
| apps/predbat/web_chat.py | Centralizes busy-banner visibility logic and updates title-event handling to keep header/list/banner state consistent. |
| apps/predbat/tests/test_web_chat.py | Updates existing banner rule test to target refreshBanner() and adds a regression test for mid-turn title events. |
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…true Review follow-up. The comment claimed every caller goes through refreshBanner() rather than reaching for showBanner/hideBanner itself, while setIdle() still called hideBanner() directly. Rewording the comment would have documented the exception. Removing it is better: setIdle() clears state.busy immediately before, so refreshBanner() takes the hide branch and the outcome is identical, with one fewer place that decides for itself. A direct call is precisely the shape the inverted rule grew in - whoever calls one is deciding, and that decision drifted. refreshBanner() is now the only caller of either, and the comment says what it actually owns: the decision, not every path that ends in a hidden banner. The test asserts the same for setBusy() and setIdle(), and strips line comments before checking - naming the old call while explaining why it is no longer made must not read as making it, which the first version of the assertion got wrong. Mutation-checked: reverting setIdle() to hideBanner() fails both assertions. Behaviour confirmed unchanged in a DOM shim - busy elsewhere raises the banner, setIdle clears it, busy on the open conversation leaves it down. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The test added in 11f54ba returned None on success, so the runner's 'failed |= test(...)' raised TypeError and the whole web_chat module aborted after the first test - the cause of the red pre-commit CI check. Co-Authored-By: Claude Code <noreply@anthropic.com>
|
The red pre-commit check was a test-harness bug, not a code regression: |
Summary
The "Replying in 'X' — switch to it" banner exists to say a reply is running in a conversation the user is not looking at.
setBusy()got that right.handleTitle()restated the same rule inverted:titleevents are scoped server-side to the conversation being viewed, so that branch is true exactly when the busy conversation is the one already open — the casesetBusy()deliberately hides the banner for. So the moment the model named a new conversation withset_chat_title, a banner appeared offering to switch the user to the transcript already in front of them.The same handler also never updated
state.titles, whichupdateChatTitle()reads, so the header went on saying "New chat" while the row in the list already showed the real name — both halves of the reported screenshot, one handler.Fix
The decision gets a single owner,
refreshBanner(), whichsetBusy()andhandleTitle()both route through. Restating the rule at each call site is precisely what let the two drift apart.handleTitle()now refreshes the header and keepsstate.busy.titlecurrent, so switching away afterwards shows the banner with the name the conversation actually has rather than the one it was created with.Testing
Written test-first; the assertions were watched failing before the fix existed.
Beyond the string assertions this repo uses for its JS, I ran the real generated script in a DOM shim, before and after:
setBusyon the conversation being viewedundefined"Octopus saving sessions today"That reproduces the reported screenshot and shows both defects fixed with no regression on the case the banner is actually for.
./run_pre_commit— exit 0, all hooks passed, full--quicksuite green.Note on existing coverage
test_busy_banner_only_points_at_another_conversationasserted the rule insidesetBusy(). Rather than dropping it, it is retargeted atrefreshBanner()and additionally checks thatsetBusy()still delegates — so the coverage moves with the rule, and the rule cannot quietly migrate back into one caller and drift again.