Session rename via sidebar pencil flips back to old title on next broadcast
Session rename via sidebar pencil flips back to old title on next broadcast
Affected: anthropic.claude-code 2.1.120 (Antigravity).
Likely affects all recent versions; the broken code path predates the
title-resolver and fork-discovery work tracked elsewhere.
Repro
- In the sidebar, click the pencil icon on a session and rename it.
- The new title appears in the sidebar list and tab title.
- Switch to a different session, or wait for any other event that
triggers a session-states broadcast (e.g. a busy-state flip on
another session).
- The renamed session's title reverts to the previous title in
both the sidebar and the editor tab.
The JSONL on disk has the new custom-title entry. Reloading the
webview (Developer: Reload Webviews) shows the new title, but it
will flip back again on the next broadcast.
I observed the same flip-back behavior on prior occasions when using
the /rename slash command, but the pencil-rename path is what I
instrumented for this report; whether /rename shares the exact same
root cause depends on its IPC path (which I didn't trace).
Root cause (traced)
The extension's sessionStates Map is the source of truth forbroadcastSessionStates() payloads. Its only writer isupdateSessionState(sessionId, state, title) on the manager class,
which the chat panel's per-session reactive in the webview triggers via
the update_session_state message. Title updates in the Map are
therefore a side effect of state updates.
The rename path doesn't go through that side effect:
q8.renameSession(the message handler) just delegates to
m1.renameSession (the storage class), which appends a custom-title
entry to JSONL. Neither layer touches sessionStates, calls
broadcastSessionStates(), or notifies any other component.
- For panel-triggered renames the per-session reactive in the panel's
webview happens to fire on summary.value change and sends
update_session_state with the new title, re-aligning the Map.
Sidebar-triggered renames have no such reactive, and the sidebar's
own q8 instance is constructed in resolveSessionListView with
void 0 for the onSessionStateChanged callback slot, so even if
the sidebar's webview did send update_session_state, the manager
would never receive it.
Net: after a sidebar rename, the Map still holds the previous title.
Any subsequent broadcastSessionStates() (focus change viasetActivePanel, busy-state flip on any session, panel disposal, etc.)
sends a session_states_update carrying the stale title. The sidebar's
update handler in the webview unconditionally writesN.summary.value = O.title if O.title is truthy, overwriting the
just-renamed local signal.
Empirical trace
Patching Object.defineProperty on each session's summary signal in
both webviews (panel + sidebar) and logging every write with a stack
trace, I get the following timeline for a single sidebar pencil rename
followed by a session switch:
| t (ms) | webview | session | old to new | source |
|--------|---------|---------|-----------|--------|
| 118820 | sidebar | target | A to B | Vn.renameSession (user pencil) |
| 121095 | sidebar | target | B to A | index.js:2044 (session_states_update handler) |
The 2.275s gap between the rename and the flip is the time between
hitting Enter and switching sessions. The flip is the inbound broadcast
from setActivePanel carrying the stale Map title.
The panel-side spy showed no flip in the same scenario when I drove the
rename through the panel's Vn.renameSession programmatically. That
path triggers the panel's per-session reactive, which sendsupdate_session_state(sessionId, state, newTitle) and re-aligns the
Map before the next broadcast. So the bug is specifically that
sidebar-side renames don't reach the Map.
Fix
Three coordinated splices, applied locally and verified working on
2.1.120:
- Make
updateSessionStatepreserve missing fields, so callers can
update only the title (or only the state) without clobbering the
other:
``js``
updateSessionState(sessionId, state, title) {
const prev = this.sessionStates.get(sessionId);
this.sessionStates.set(sessionId, {
sessionId,
state: state != null ? state : (prev?.state ?? "idle"),
title: title != null ? title : prev?.title,
});
this.broadcastSessionStates();
}
- Have
q8.renameSessioninvoke its existingonSessionStateChanged
callback after a successful storage write, passing undefined for
state (preserve) and the new title:
``js``
async renameSession(sessionId, title, isAi) {
const skipped = await (await m1.load(this.cwd, this.logger))
.renameSession(sessionId, title, isAi);
if (!skipped) this.onSessionStateChanged?.(sessionId, undefined, title);
return { type: "rename_session_response", skipped };
}
- Wire
onSessionStateChangedinto the sidebar'sq8instantiation
(currently constructed with void 0 and no further args). A minimal
forwarder is enough; the sidebar doesn't need the panel-mapping
bookkeeping the panel callback does:
``js``
new q8(/* ... */, /*panel*/ undefined, () => this.broadcastUsageUpdate(),
/*isFullEditor*/ false,
(id, state, title) => this.updateSessionState(id, state, title));
The deeper architectural fix would be to make rename atomic at the
storage layer: m1.renameSession emits an event, the manager
subscribes and updates the Map. That decouples title-broadcast from
the state-broadcast side channel and removes the cross-layer
dependency on the panel webview's reactive structure. The patch above
keeps the existing layering and just plugs the gaps.
Related
- #32150 (title resolver:
firstPromptshould outranklastPrompt):
independent. Affects which title the metadata parser returns from
JSONL; unrelated to the broadcast Map staleness.
- #48937 (forks invisible after compaction): independent. Different
layer (chain-walker / discovery), different code path.
- The "rename works until you switch sessions" workaround "reload
webviews" papers over this by re-running list_sessions_response
on the freshly-loaded JSONL, which then bypasses the stale Map for
the initial render. The fix above eliminates the need.
---
🤖 Diagnosis and patch written by Claude (claude-opus-4-7), posted by
@ojura. Live trace was captured by injecting Object.defineProperty
signal-write spies via Chrome DevTools Protocol (Electron's debug
port) into both the chat panel and sidebar webviews, walking the
React fiber tree to find the Vn sessions manager (it's thesessions prop on the Oe1 component, visible atOe1.memoizedProps.sessions), and driving the rename both
programmatically (via Vn.renameSession) and through the user's
pencil UI for comparison. The class names above (q8, m1,onSessionStateChanged, sessionStates) are the minified names in
the 2.1.120 bundle, located by grepping the single-lineextension.js directly, not symbols from the public TS source dump
at https://github.com/yasasbanukaofficial/claude-code. (That dump
helped me on a previous report, #32150, by surfacing two parallel
resolver sites that were easy to miss in the bundle. For this bug
the architecture was small enough that grep + reading the constructor
parameter list was the faster path.)
This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗