fix(header): inactive-tab rename actually starts; snappier tab state change

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.
This commit is contained in:
Bohdan Triapitsyn
2026-08-24 20:18:45 +03:00
parent b1023b87e7
commit 4b2ad01b4a
2 changed files with 28 additions and 15 deletions
+26 -14
View File
@@ -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<string | null>(null);
const [headerSessionTitleDraft, setHeaderSessionTitleDraft] = React.useState('');
const [pendingHeaderRetentionAction, setPendingHeaderRetentionAction] = React.useState<{ action: 'archive' | 'delete'; sessionId: string } | null>(null);
const headerRenameFormRef = React.useRef<HTMLFormElement | null>(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 (
<>
<Item onClick={() => { if (!isActive) select(); pendingHeaderRenameRef.current = true; }}>
<Item onClick={() => { if (!isActive) select(); pendingHeaderRenameRef.current = session.id; }}>
<Icon name="pencil-ai" className="mr-2 size-4" />{t('sessions.sidebar.session.menu.rename')}
</Item>
<Item onClick={() => 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 = () => {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="min-w-[190px]">
<DropdownMenuItem onClick={() => { pendingHeaderRenameRef.current = true; }}><Icon name="pencil-ai" className="mr-2 size-4" />{t('sessions.sidebar.session.menu.rename')}</DropdownMenuItem>
<DropdownMenuItem onClick={() => { pendingHeaderRenameRef.current = currentSessionId; }}><Icon name="pencil-ai" className="mr-2 size-4" />{t('sessions.sidebar.session.menu.rename')}</DropdownMenuItem>
<DropdownMenuItem onClick={() => currentSessionId && copySessionIdFor(currentSessionId)}><Icon name="file-copy" className="mr-2 size-4" />{t('sessions.sidebar.session.menu.copyId')}</DropdownMenuItem>
<DropdownMenuSeparator />
{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();
}
}}
@@ -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',
),
)}