From 4b2ad01b4add8b08aca6c98e3d99e13e5c76cf27 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Mon, 24 Aug 2026 20:18:45 +0300 Subject: [PATCH] fix(header): inactive-tab rename actually starts; snappier tab state change MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename from another tab's menu stored only a boolean that the session-switch reset effect wiped before the menu finished closing, so the tab activated but rename never began. The pending rename now carries the target session id and begins exactly when that session becomes active (whether the menu closes before or after the switch), without the reset effect cancelling it — and without an in-flight rename being cancelled by unrelated re-renders. Tab background/text state changes animate at 75ms so activation reads immediate. --- packages/ui/src/components/layout/Header.tsx | 40 ++++++++++++------- .../components/layout/SessionTabsStrip.tsx | 3 +- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/packages/ui/src/components/layout/Header.tsx b/packages/ui/src/components/layout/Header.tsx index c7000a9d..265f2354 100644 --- a/packages/ui/src/components/layout/Header.tsx +++ b/packages/ui/src/components/layout/Header.tsx @@ -926,18 +926,13 @@ export const Header: React.FC = () => { const deleteSessions = useSessionUIStore((state) => state.deleteSessions); const [isRenamingHeaderSession, setIsRenamingHeaderSession] = React.useState(false); const [isHeaderSessionMenuOpen, setIsHeaderSessionMenuOpen] = React.useState(false); - const pendingHeaderRenameRef = React.useRef(false); + /** Session id whose rename was requested from a tab menu; survives the + activation that a Rename on an inactive tab performs first. */ + const pendingHeaderRenameRef = React.useRef(null); const [headerSessionTitleDraft, setHeaderSessionTitleDraft] = React.useState(''); const [pendingHeaderRetentionAction, setPendingHeaderRetentionAction] = React.useState<{ action: 'archive' | 'delete'; sessionId: string } | null>(null); const headerRenameFormRef = React.useRef(null); - React.useEffect(() => { - pendingHeaderRenameRef.current = false; - setIsHeaderSessionMenuOpen(false); - setIsRenamingHeaderSession(false); - setHeaderSessionTitleDraft(''); - setPendingHeaderRetentionAction(null); - }, [currentSessionId]); const beginHeaderSessionRename = React.useCallback(() => { if (!currentSessionId) return; @@ -945,6 +940,23 @@ export const Header: React.FC = () => { setIsRenamingHeaderSession(true); }, [currentSession?.title, currentSessionId, currentSessionTitle]); + const beginHeaderSessionRenameRef = React.useRef(beginHeaderSessionRename); + beginHeaderSessionRenameRef.current = beginHeaderSessionRename; + + React.useEffect(() => { + setIsHeaderSessionMenuOpen(false); + setPendingHeaderRetentionAction(null); + if (currentSessionId && pendingHeaderRenameRef.current === currentSessionId) { + // Rename on an inactive tab activates it first; the switch itself is + // when the rename can begin (the menu may close before or after it). + pendingHeaderRenameRef.current = null; + beginHeaderSessionRenameRef.current(); + return; + } + setIsRenamingHeaderSession(false); + setHeaderSessionTitleDraft(''); + }, [currentSessionId]); + const saveHeaderSessionRename = React.useCallback(async () => { if (!currentSessionId) return; const title = headerSessionTitleDraft.trim(); @@ -1536,7 +1548,7 @@ export const Header: React.FC = () => { const canMoveToWorktree = isActive && !isVSCode && !isChatContext && currentSession && !currentSession.parentId; return ( <> - { if (!isActive) select(); pendingHeaderRenameRef.current = true; }}> + { if (!isActive) select(); pendingHeaderRenameRef.current = session.id; }}> {t('sessions.sidebar.session.menu.rename')} copySessionIdFor(session.id)}> @@ -1734,8 +1746,8 @@ export const Header: React.FC = () => { open={isHeaderSessionMenuOpen} onOpenChange={setIsHeaderSessionMenuOpen} onOpenChangeComplete={(open) => { - if (!open && pendingHeaderRenameRef.current) { - pendingHeaderRenameRef.current = false; + if (!open && pendingHeaderRenameRef.current && pendingHeaderRenameRef.current === currentSessionId) { + pendingHeaderRenameRef.current = null; beginHeaderSessionRename(); } }} @@ -1746,7 +1758,7 @@ export const Header: React.FC = () => { - { pendingHeaderRenameRef.current = true; }}>{t('sessions.sidebar.session.menu.rename')} + { pendingHeaderRenameRef.current = currentSessionId; }}>{t('sessions.sidebar.session.menu.rename')} currentSessionId && copySessionIdFor(currentSessionId)}>{t('sessions.sidebar.session.menu.copyId')} {currentSession?.shareUrl ? ( @@ -1806,8 +1818,8 @@ export const Header: React.FC = () => { renderMenu={renderSessionTabMenu} suppressActiveTabControls={isRenamingHeaderSession} onMenuOpenChangeComplete={(open) => { - if (!open && pendingHeaderRenameRef.current) { - pendingHeaderRenameRef.current = false; + if (!open && pendingHeaderRenameRef.current && pendingHeaderRenameRef.current === currentSessionId) { + pendingHeaderRenameRef.current = null; beginHeaderSessionRename(); } }} diff --git a/packages/ui/src/components/layout/SessionTabsStrip.tsx b/packages/ui/src/components/layout/SessionTabsStrip.tsx index 45018366..f4bbe964 100644 --- a/packages/ui/src/components/layout/SessionTabsStrip.tsx +++ b/packages/ui/src/components/layout/SessionTabsStrip.tsx @@ -222,10 +222,11 @@ const SessionTabItem: React.FC<{ data-controls-open={overlayVisible ? 'true' : 'false'} className={cn( 'session-tab group/session-tab relative flex h-7 w-full min-w-0 select-none items-center rounded-md px-2', + 'transition-colors duration-75', isActive ? 'bg-interactive-selection' : cn( - 'cursor-pointer text-muted-foreground transition-colors duration-150 hover:bg-interactive-hover hover:text-foreground', + 'cursor-pointer text-muted-foreground hover:bg-interactive-hover hover:text-foreground', overlayVisible && 'bg-interactive-hover text-foreground', ), )}