Skip to content

fix(web): no stale-title flash after renaming a thread - #7822

Closed
UtkarshUsername wants to merge 17 commits into
pingdotgg:mainfrom
UtkarshUsername:fix/rename-flash-old-title
Closed

fix(web): no stale-title flash after renaming a thread#7822
UtkarshUsername wants to merge 17 commits into
pingdotgg:mainfrom
UtkarshUsername:fix/rename-flash-old-title

Conversation

@UtkarshUsername

@UtkarshUsername UtkarshUsername commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

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

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes

Built by ox-alpha in opencode.

Note

Fix stale-title flash after renaming a thread in Sidebar and ChatHeader

  • Adds optimistic title handling to the sidebar and chat header so the newly committed title shows immediately after a rename and persists until the store catches up; on failure the title reverts to the stored value
  • Introduces withOptimisticTitle helper in Sidebar.tsx that overrides a thread shell's title with the optimistic entry when available
  • Rewrites commitThreadRename and ChatHeader.commitRename to close the editor immediately, set a baseline-anchored optimistic title, and call updateThreadMetadata; failures (including interrupts) clear the optimistic override and show an error toast
  • Sets renameCommittedRef.current = true before onCommitRename in SidebarThreadRow to prevent duplicate submissions from blur followed by refocus
  • Risk: optimistic title state is anchored to a baseline title; if multiple rapid renames race on the same thread before the store updates, an earlier baseline mismatch could retire the wrong optimistic entry. Review commitThreadRename and the retiring effect in Sidebar.tsx

Macroscope 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.

@coderabbitai

coderabbitai Bot commented Aug 21, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: bde34467-6772-436b-80a5-293baf20e5dc

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:S 10-29 changed lines (additions + deletions). labels Aug 21, 2026
Comment thread apps/web/src/components/chat/ChatHeader.tsx Outdated
Comment thread apps/web/src/components/Sidebar.tsx Outdated
Comment thread apps/web/src/components/Sidebar.tsx Outdated

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread apps/web/src/components/Sidebar.tsx Outdated
Comment thread apps/web/src/components/chat/ChatHeader.tsx Outdated
@macroscopeapp

macroscopeapp Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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.

@github-actions github-actions Bot added size:M 30-99 changed lines (additions + deletions). and removed size:S 10-29 changed lines (additions + deletions). labels Aug 21, 2026

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread apps/web/src/components/Sidebar.tsx Outdated
Comment thread apps/web/src/components/chat/ChatHeader.tsx Outdated
…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.
@github-actions github-actions Bot added size:L 100-499 changed lines (additions + deletions). and removed size:M 30-99 changed lines (additions + deletions). labels Aug 21, 2026
Comment thread apps/web/src/components/Sidebar.tsx Outdated
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.

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread apps/web/src/components/chat/ChatHeader.tsx Outdated
Comment thread apps/web/src/components/Sidebar.tsx Outdated

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread apps/web/src/components/Sidebar.tsx Outdated
Comment thread apps/web/src/components/chat/ChatHeader.tsx Outdated
Comment thread apps/web/src/components/chat/ChatHeader.tsx Outdated
Comment thread apps/web/src/components/Sidebar.tsx Outdated
…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.

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread apps/web/src/components/Sidebar.tsx Outdated
Comment thread apps/web/src/components/chat/ChatHeader.tsx Outdated
Comment thread apps/web/src/components/Sidebar.tsx Outdated
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.

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread apps/web/src/components/Sidebar.tsx Outdated
@github-actions github-actions Bot added size:M 30-99 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Aug 21, 2026

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread apps/web/src/components/chat/ChatHeader.tsx Outdated
Comment thread apps/web/src/components/chat/ChatHeader.tsx Outdated
Comment thread apps/web/src/components/Sidebar.tsx Outdated
Comment thread apps/web/src/components/Sidebar.tsx Outdated
@github-actions github-actions Bot removed the size:M 30-99 changed lines (additions + deletions). label Aug 21, 2026
@github-actions github-actions Bot added the size:L 100-499 changed lines (additions + deletions). label Aug 21, 2026

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread apps/web/src/components/Sidebar.tsx Outdated
Comment thread apps/web/src/components/chat/ChatHeader.tsx Outdated
Comment thread apps/web/src/components/Sidebar.tsx Outdated

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread apps/web/src/components/Sidebar.tsx Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread apps/web/src/components/Sidebar.tsx
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.

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread apps/web/src/components/chat/ChatHeader.tsx Outdated
@UtkarshUsername
UtkarshUsername marked this pull request as draft August 21, 2026 18:45

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread apps/web/src/components/Sidebar.tsx Outdated
Comment thread packages/client-runtime/src/state/threadShell.ts Outdated

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread apps/web/src/components/chat/ChatHeader.tsx
environmentId: threadRef.environmentId,
input: { threadId: threadRef.threadId, title: trimmed },
}).then((result) => {
if (result._tag !== "Failure") return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 BC: 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

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread apps/web/src/components/Sidebar.tsx Outdated
Comment thread apps/web/src/components/Sidebar.tsx
…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.

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread apps/web/src/components/Sidebar.tsx Outdated
Comment thread apps/web/src/components/chat/ChatHeader.tsx Outdated
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.

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread packages/client-runtime/src/state/threadShell.ts Outdated

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant