feat(header): session tab tooltips, status dots, cursor context menu

Right-click now opens the session menu under the cursor (the sidebar's
context-menu pattern; the same header-supplied items back both the
"..." dropdown and the context menu via injected menu primitives) and
still never changes the active tab. Hovering a tab shows the sidebar's
session tooltip — title, last-activity time, project, branch and PR
status — after a delay, suppressed while a menu is open or a drag is in
flight. Each tab carries the sidebar's status dot at its end (accent
while the session runs, info-blue for unread), hidden while the hover
controls overlay it. Tab titles fade out instead of ending in "...",
and while the active tab is renaming its hover controls stay hidden so
only the rename controls show.
This commit is contained in:
Bohdan Triapitsyn
2026-08-24 20:02:59 +03:00
parent 338dc73e96
commit c47c7190f4
5 changed files with 366 additions and 139 deletions
+26 -24
View File
@@ -1530,49 +1530,50 @@ export const Header: React.FC = () => {
const showMiniChatHeaderAction = hasElectronDesktopIPC && (isNewSessionDraftOpen || Boolean(currentSessionId));
const renderSessionTabMenu = React.useCallback(({ session, isActive, select, closeOtherTabs }: SessionTabMenuArgs) => {
const renderSessionTabMenu = React.useCallback(({ session, isActive, select, closeOtherTabs, components }: SessionTabMenuArgs) => {
const { Item, Separator } = components;
const shareUrl = session.share?.url ?? null;
const canMoveToWorktree = isActive && !isVSCode && !isChatContext && currentSession && !currentSession.parentId;
return (
<>
<DropdownMenuItem onClick={() => { if (!isActive) select(); pendingHeaderRenameRef.current = true; }}>
<Item onClick={() => { if (!isActive) select(); pendingHeaderRenameRef.current = true; }}>
<Icon name="pencil-ai" className="mr-2 size-4" />{t('sessions.sidebar.session.menu.rename')}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => copySessionIdFor(session.id)}>
</Item>
<Item onClick={() => copySessionIdFor(session.id)}>
<Icon name="file-copy" className="mr-2 size-4" />{t('sessions.sidebar.session.menu.copyId')}
</DropdownMenuItem>
<DropdownMenuSeparator />
</Item>
<Separator />
{shareUrl ? (
<>
<DropdownMenuItem onClick={() => copySessionShareUrl(shareUrl)}>
<Item onClick={() => copySessionShareUrl(shareUrl)}>
<Icon name="file-copy" className="mr-2 size-4" />{t('sessions.sidebar.session.menu.copyLink')}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => void unshareSessionFor(session.id)}>
</Item>
<Item onClick={() => void unshareSessionFor(session.id)}>
<Icon name="link-unlink-m" className="mr-2 size-4" />{t('sessions.sidebar.session.menu.unshare')}
</DropdownMenuItem>
</Item>
</>
) : (
<DropdownMenuItem onClick={() => void shareSessionFor(session.id)}>
<Item onClick={() => void shareSessionFor(session.id)}>
<Icon name="share-2" className="mr-2 size-4" />{t('sessions.sidebar.session.menu.share')}
</DropdownMenuItem>
</Item>
)}
{isActive ? (
<DropdownMenuItem onClick={() => void exportCurrentSession()}>
<Item onClick={() => void exportCurrentSession()}>
<Icon name="download" className="mr-2 size-4" />{t('sessions.sidebar.session.menu.exportMarkdown')}
</DropdownMenuItem>
</Item>
) : null}
{canMoveToWorktree ? (
<Tooltip>
<TooltipTrigger asChild>
<span className="block">
<DropdownMenuItem
<Item
disabled={!sessionDirectory || isCurrentSessionActive || isCurrentSessionMovingToWorktree}
onClick={moveCurrentSessionToWorktree}
className="w-full"
>
<Icon name="folder-shared" className="mr-2 size-4" />
{t('sessions.sidebar.session.menu.moveToWorktree')}
</DropdownMenuItem>
</Item>
</span>
</TooltipTrigger>
<TooltipContent side="right" className="max-w-72">
@@ -1584,17 +1585,17 @@ export const Header: React.FC = () => {
</TooltipContent>
</Tooltip>
) : null}
<DropdownMenuSeparator />
<DropdownMenuItem onClick={closeOtherTabs}>
<Separator />
<Item onClick={closeOtherTabs}>
<Icon name="close-circle" className="mr-2 size-4" />{t('header.sessionTabs.closeOtherTabs')}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => setPendingHeaderRetentionAction({ action: 'archive', sessionId: session.id })}>
</Item>
<Separator />
<Item onClick={() => setPendingHeaderRetentionAction({ action: 'archive', sessionId: session.id })}>
<Icon name="inbox-archive" className="mr-2 size-4" />{t('sessions.sidebar.bulkActions.archive')}
</DropdownMenuItem>
<DropdownMenuItem className="text-destructive focus:text-destructive" onClick={() => setPendingHeaderRetentionAction({ action: 'delete', sessionId: session.id })}>
</Item>
<Item className="text-destructive focus:text-destructive" onClick={() => setPendingHeaderRetentionAction({ action: 'delete', sessionId: session.id })}>
<Icon name="delete-bin" className="mr-2 size-4" />{t('sessions.sidebar.bulkActions.delete')}
</DropdownMenuItem>
</Item>
</>
);
}, [copySessionIdFor, copySessionShareUrl, currentSession, exportCurrentSession, isChatContext, isCurrentSessionActive, isCurrentSessionMovingToWorktree, isVSCode, moveCurrentSessionToWorktree, sessionDirectory, shareSessionFor, t, unshareSessionFor]);
@@ -1803,6 +1804,7 @@ export const Header: React.FC = () => {
) : null}
<SessionTabsStrip
renderMenu={renderSessionTabMenu}
suppressActiveTabControls={isRenamingHeaderSession}
onMenuOpenChangeComplete={(open) => {
if (!open && pendingHeaderRenameRef.current) {
pendingHeaderRenameRef.current = false;