fix(web): no stale-title flash after renaming a thread - #7822
fix(web): no stale-title flash after renaming a thread#7822UtkarshUsername wants to merge 17 commits into
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Reviewed the rename-commit timing change in Sidebar.tsx and ChatHeader.tsx. Keeping the editor mounted until the server confirms does fix the stale-title flash, but the reset was moved into an unconditional deferred callback (finally), so a completing request now clears whatever rename session is current at that moment — not necessarily the one it started. One comment per file with the smallest guard.
Posted via Macroscope — UI Consistency
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — This PR adds optimistic title state in two separate components, and unresolved review comments identify that this creates UI inconsistencies — the sidebar and header can show different titles for the same thread, and delete/archive dialogs don't use the optimistic title. The architectural approach warrants human review. You can add or adjust custom eligibility rules. Learn more. |
There was a problem hiding this comment.
Reviewed the rename-flash change in Sidebar.tsx and ChatHeader.tsx. The epoch guard does fix the stale-completion problem flagged earlier, but the underlying premise — that the store holds the new title once updateMetadata settles — does not hold for this data flow, so the flash is only shortened while a new "editor open across a network round trip" state is introduced. Details inline.
Posted via Macroscope — UI Consistency
…itor open The rename response resolves before clients receive the updated title (the shell stream coalesces on the server), so closing on the response still flashed the old title for a beat. Close the editor immediately and render the committed title until the store catches up; revert it if the server rejects the rename.
The optimistic title only cleared when the store matched it exactly, so a divergent title change (like title regeneration) landing in the same coalesced window as the rename left the typed title stuck on screen. Baseline each override against the stored title at commit time and retire it once the store shows anything else.
There was a problem hiding this comment.
Reviewed the optimistic rename overlays in Sidebar.tsx and ChatHeader.tsx. The immediate-close + shadowed-title approach removes the flash on the surface that owns the edit, and the sidebar's prop-level shadowing (rowThread) keeps the row's own rename entry points consistent. Two issues with how the pending title is retired, plus the fact that the overlay is duplicated per surface — details inline.
Posted via Macroscope — UI Consistency
There was a problem hiding this comment.
Two findings on the new optimistic-title overrides: the retire baseline drops the override when a second rename is still in flight (re-introducing the old-title flash), and the override now lives twice — once per surface — so the sidebar row and the chat header can show different titles for the same thread.
Posted via Macroscope — UI Consistency
…chain Renaming twice within the coalescing window baselined both overrides against the same stored title, so the first title landing retired the second override and briefly flashed the intermediate title. Track the chain of committed titles per thread instead: the override stays up while the store walks it and retires when the store reaches the latest title or anything outside the chain.
There was a problem hiding this comment.
Reviewed the optimistic-title override in Sidebar.tsx and ChatHeader.tsx. The chain baseline fixes the second-rename case flagged earlier, but the retire rule still drops the override in one sequence (rename, then rename back before the stream lands), which reintroduces the intermediate-title flash. One smaller consistency gap in the sidebar context-menu dialogs.
Posted via Macroscope — UI Consistency
Renaming back to the current title retired the optimistic override immediately, so the first commit's frame still flashed the intermediate title. Retire only when the store reaches the final chain link or leaves the chain entirely. Also read the override-aware title in the archive and delete confirmations so dialogs match the row right after a rename.
There was a problem hiding this comment.
One finding: the new threadTitlesByKey dependency on commitThreadRename gives every sidebar row a fresh onCommitRename prop on each thread-shell event, which defeats the row memoization this file explicitly maintains via refs.
Posted via Macroscope — UI Consistency
There was a problem hiding this comment.
Reviewed the rename-flash change against the two rename surfaces. The new "hold the editor open until the store catches up" model removes the flash on the happy path, but it leaves the editor interactive (and in one case permanently open) during the awaiting window. Four inline findings.
Posted via Macroscope — UI Consistency
There was a problem hiding this comment.
Two remaining findings on the "hold the editor open until the store catches up" approach. The double-submit and interrupt holes from the previous revision look closed; what's left is that both surfaces keep a live editor mounted after commit, so post-commit keystrokes and pointer interactions still run pre-commit semantics.
Posted via Macroscope — UI Consistency
There was a problem hiding this comment.
One finding on the sidebar's optimistic-title baseline; the header's equivalent logic is correct and is used as the reference below.
Posted via Macroscope — UI Consistency
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 2e99cc9. Configure here.
Search results bypassed the optimistic rename overlay so a just-renamed thread could still show its old name there. A second rename committed before the store caught up also baselined against the displayed title, retiring its own override on the next frame; both renames now anchor to the same stored-title baseline.
There was a problem hiding this comment.
One consistency issue: the header's optimistic title is not visible to the thread menu's destructive confirmations, so the header and its own menu can name the same thread differently during the override window. The sidebar half of this PR already handles that case.
Posted via Macroscope — UI Consistency
There was a problem hiding this comment.
Reviewed the rename/optimistic-title changes in Sidebar.tsx, ChatHeader.tsx and useThreadActionMenu.ts for surface consistency (row, search, tooltip, header, menu confirmations). The menu/dialog titles are now consistent with what each surface renders. One remaining consistency hole in the retire rule is noted inline.
Posted via Macroscope — UI Consistency
There was a problem hiding this comment.
One finding: the optimistic-title entry is only removed on failure, so its baseline goes stale after the first successful rename of a thread and the stale-title flash comes back for every later rename in the same session.
Posted via Macroscope — UI Consistency
| environmentId: threadRef.environmentId, | ||
| input: { threadId: threadRef.threadId, title: trimmed }, | ||
| }).then((result) => { | ||
| if (result._tag !== "Failure") return; |
There was a problem hiding this comment.
Success returns without touching the map, so the entry survives forever and its baseline freezes at the first pre-rename title — every later rename of the same thread in the session gets no optimism at all.
Stored A, rename → B: entry {title:"B", baseline:"A"}, row shadows to B, B lands, entry stays. Rename B → C: nextOptimisticThreadTitles inherits current.get(key).baseline = "A" (threadShell.ts:44), so the entry is {title:"C", baseline:"A"} while the store reads "B"; the shadow condition source.title === optimistic.baseline (threadShell.ts:157) is false, the row keeps rendering B until the coalesced shell frame arrives — the flash this PR removes. The dead entry is also a latent wrong title: if the stored title ever returns to "A" (rename from another client, regenerate, undo), the shadow re-activates and pins "C" indefinitely, and the map grows one permanent entry per renamed thread.
Smallest fix is to baseline on the raw stored title rather than the displayed one (e.g. read environmentThreadShells.environmentThreadIndexAtom(threadRef.environmentId) for the unshadowed title), or prune the entry once the store has moved off baseline so the next rename starts clean. Same call shape in ChatHeader.tsx.
Posted via Macroscope — UI Consistency
There was a problem hiding this comment.
Reviewed the rename-optimism changes for cross-surface consistency. Centralizing the override on the shared shell atom is the right shape — every title reader (sidebar rows, sidebar title search, ChatHeader, and the action-menu confirmations via readThreadShell) now resolves the same value, so the surfaces can no longer disagree, and the derived filter keeps chained and rename-back sequences from flashing an intermediate title.
Two findings remain on the changed call sites: the registry plumbing is duplicated verbatim in both surfaces instead of living behind a named helper, and fulfilled entries are never removed from the backing map, which lets a later store title revive a retired override.
Posted via Macroscope — UI Consistency
…y plumbing Filtered title overlay hid fulfilled/diverged entries but left them in the raw map, so a later store value that re-entered the chain revived the stale optimistic title. The filtered atom now schedules a microtask write-back to actually delete pruned entries from the raw map. Both rename surfaces duplicated the same get/set plumbing against the shared shell atom. Export setOptimisticThreadTitle and clearOptimisticThreadTitle helpers from threadShell and call them from Sidebar and ChatHeader.
There was a problem hiding this comment.
One blocking finding: both rename surfaces write the optimistic-title entry under a key format the thread-shell reader never uses, so the optimistic override cannot be applied (and makes the shell's filtered view throw while an entry exists). Details inline.
Posted via Macroscope — UI Consistency
scopedThreadKey uses colon separator while threadShell atom family keys use null separator via threadKey. Writing optimistic entries with the wrong key meant no shell reader ever matched them and parseThreadKey threw on the colon format. Helpers now take ScopedThreadRef and derive the key internally.
There was a problem hiding this comment.
One convention issue found in packages/client-runtime/src/state/threadShell.ts: the new registry-accepting helpers use an ad-hoc any-typed structural registry shape instead of the canonical AtomRegistry.AtomRegistry type used everywhere else in this package.
Posted via Macroscope — Effect Service Conventions
There was a problem hiding this comment.
One finding: the shared optimistic-title entry retires as soon as the stored title equals the newly committed title, which still flashes an intermediate title when a thread is renamed back inside the coalescing window. Details inline.
Posted via Macroscope — UI Consistency
| return; | ||
| } | ||
| setRenamingThreadKey(null); | ||
| environmentThreadShells.setOptimisticThreadTitle( |
There was a problem hiding this comment.
Renaming back to the still-stored title inside the coalescing window retires the entry immediately, so the intermediate title still flashes.
Stored A, rename to B → entry {title:"B", chain:[A,B]}, row shows B. Rename back to A before B's shell frame lands: the row passes its displayed title (B) as originalTitle, so nextOptimisticThreadTitles produces {title:"A", chain:[A,B,A]} — but the filtered view (packages/client-runtime/src/state/threadShell.ts:116) prunes on source.title === entry.title, and the store is still at A. The entry dies on the next read, and when B's frame arrives every shell reader (row, header, search, menu confirmations) renders B until A's frame lands.
The retire rule can't distinguish "store hasn't moved yet" from "store reached the final title". Smallest fix is to make fulfillment depend on the rename actually having settled — e.g. carry a pending-commit count on the entry, incremented here and decremented when updateThreadMetadata settles (success as well as failure), and only apply the source.title === entry.title prune when that count is zero. ChatHeader.tsx:191 goes through the same helper, so one change covers both surfaces.
Posted via Macroscope — UI Consistency

What Changed
After confirming a thread rename (Enter or clicking away), the new name now appears immediately instead of a split second later. Previously the old name could flash briefly while the app caught up.
If a rename fails (for example a connection drop), the title reverts to what it was and an error message explains what happened. Renaming again immediately after a rename works cleanly, and the archive/delete confirmations show the newest name too.
Applies to renaming from the sidebar and from the chat header.
Why
The visible title updates a beat after you hit Enter, because the change is confirmed by the server first. Closing the rename box instantly meant briefly showing the old name during that gap. Now the app keeps showing the name you typed until the real one has arrived, so the title never goes backwards.
UI Changes
No static visual change; only how quickly the title updates after confirming a rename differs.
Checklist
Built by ox-alpha in opencode.
Note
Fix stale-title flash after renaming a thread in
SidebarandChatHeaderwithOptimisticTitlehelper in Sidebar.tsx that overrides a thread shell's title with the optimistic entry when availablecommitThreadRenameandChatHeader.commitRenameto close the editor immediately, set a baseline-anchored optimistic title, and callupdateThreadMetadata; failures (including interrupts) clear the optimistic override and show an error toastrenameCommittedRef.current = truebeforeonCommitRenameinSidebarThreadRowto prevent duplicate submissions from blur followed by refocuscommitThreadRenameand the retiring effect in Sidebar.tsxMacroscope summarized 656a84e. (Automatic summaries will resume when PR exits draft mode or review begins).
Note
Low Risk
Client-only optimistic title display around rename; no auth or persistence changes. Worst case is a stale title until the next store update or a failed rename revert.
Overview
Stops the old thread title from flashing after a rename. The editor closes immediately, but the committed name stays on screen until the coalesced store stream actually moves off the pre-rename value.
Sidebar and chat header both keep a short-lived override (
optimisticTitles/pendingTitle) keyed to a baseline stored title. Search, list rows, and archive/delete confirmations use that name. Failures (including interrupts) drop the override and toast; a second rename before the stream lands reuses the same baseline so it does not retire early.Also marks the sidebar rename as committed on blur so a refocus during catch-up cannot submit twice.
Reviewed by Cursor Bugbot for commit 656a84e. Bugbot is set up for automated code reviews on this repo. Configure here.