diff --git a/packages/ui/src/components/layout/Header.tsx b/packages/ui/src/components/layout/Header.tsx index 39245efd..526f6ee3 100644 --- a/packages/ui/src/components/layout/Header.tsx +++ b/packages/ui/src/components/layout/Header.tsx @@ -1733,8 +1733,68 @@ export const Header: React.FC = () => { ) : null} - -
+ { + if (!open && pendingHeaderRenameRef.current) { + pendingHeaderRenameRef.current = false; + beginHeaderSessionRename(); + } + }} + > + + + + + { pendingHeaderRenameRef.current = true; }}>{t('sessions.sidebar.session.menu.rename')} + {t('sessions.sidebar.session.menu.copyId')} + + {currentSession?.shareUrl ? ( + <> + {t('sessions.sidebar.session.menu.copyLink')} + void unshareCurrentSession()}>{t('sessions.sidebar.session.menu.unshare')} + + ) : ( + void shareCurrentSession()}>{t('sessions.sidebar.session.menu.share')} + )} + void exportCurrentSession()}>{t('sessions.sidebar.session.menu.exportMarkdown')} + {!isVSCode && !isChatContext && currentSession && !currentSession.parentId ? ( + + + + + + {t('sessions.sidebar.session.menu.moveToWorktree')} + + + + + {isCurrentSessionMovingToWorktree + ? t('sessions.sidebar.session.moveToWorktree.tooltipMoving') + : isCurrentSessionActive + ? t('sessions.sidebar.session.moveToWorktree.tooltipBusy') + : t('sessions.sidebar.session.moveToWorktree.tooltip')} + + + ) : null} + + setPendingHeaderRetentionAction('archive')}>{t('sessions.sidebar.bulkActions.archive')} + setPendingHeaderRetentionAction('delete')}>{t('sessions.sidebar.bulkActions.delete')} + + + ) : null} + > +
{isRenamingHeaderSession ? (
{ )}
-
- {currentSessionId && !isNewSessionDraftOpen && !isRenamingHeaderSession ? ( - { - if (!open && pendingHeaderRenameRef.current) { - pendingHeaderRenameRef.current = false; - beginHeaderSessionRename(); - } - }} - > - - - - - { pendingHeaderRenameRef.current = true; }}>{t('sessions.sidebar.session.menu.rename')} - {t('sessions.sidebar.session.menu.copyId')} - - {currentSession?.shareUrl ? ( - <> - {t('sessions.sidebar.session.menu.copyLink')} - void unshareCurrentSession()}>{t('sessions.sidebar.session.menu.unshare')} - - ) : ( - void shareCurrentSession()}>{t('sessions.sidebar.session.menu.share')} - )} - void exportCurrentSession()}>{t('sessions.sidebar.session.menu.exportMarkdown')} - {!isVSCode && !isChatContext && currentSession && !currentSession.parentId ? ( - - - - - - {t('sessions.sidebar.session.menu.moveToWorktree')} - - - - - {isCurrentSessionMovingToWorktree - ? t('sessions.sidebar.session.moveToWorktree.tooltipMoving') - : isCurrentSessionActive - ? t('sessions.sidebar.session.moveToWorktree.tooltipBusy') - : t('sessions.sidebar.session.moveToWorktree.tooltip')} - - - ) : null} - - setPendingHeaderRetentionAction('archive')}>{t('sessions.sidebar.bulkActions.archive')} - setPendingHeaderRetentionAction('delete')}>{t('sessions.sidebar.bulkActions.delete')} - - - ) : null} -
)} -
+ {activeSurfaceHeader || isVSCode ?
: null}
{showDesktopHeaderContextUsage && stableDesktopContextUsage ? ( diff --git a/packages/ui/src/components/layout/SessionTabsStrip.tsx b/packages/ui/src/components/layout/SessionTabsStrip.tsx index 01b77eca..51df891e 100644 --- a/packages/ui/src/components/layout/SessionTabsStrip.tsx +++ b/packages/ui/src/components/layout/SessionTabsStrip.tsx @@ -37,15 +37,24 @@ const restrictToXAxis: Modifier = ({ transform }) => ({ ...transform, y: 0 }); /** * Sortable shell for the active tab: the pill itself drags, while the * interactive content inside (rename form, menu) stops pointer-down so a text - * selection or menu click never starts a drag. + * selection or menu click never starts a drag. The close button and the + * session menu (passed down from the header) reveal on hover, exactly like on + * inactive tabs. */ -const ActiveTabShell: React.FC<{ id: string; children: React.ReactNode }> = ({ id, children }) => { +const ActiveTabShell: React.FC<{ + id: string; + menu: React.ReactNode; + menuOpen: boolean; + onClose: () => void; + closeLabel: string; + children: React.ReactNode; +}> = ({ id, menu, menuOpen, onClose, closeLabel, children }) => { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id }); return (
= ({ i
- {children} +
+ {children} +
+
event.stopPropagation()} + className={cn( + 'absolute right-1 top-1/2 hidden -translate-y-1/2 items-center gap-0.5', + 'opacity-0 transition-opacity duration-150', + 'group-hover/session-tab:flex group-hover/session-tab:opacity-100', + menuOpen && 'flex opacity-100', + )} + > + + {menu} +
); @@ -86,7 +116,7 @@ const InactiveSessionTab: React.FC<{
{title} +
event.stopPropagation()} + onPointerDown={(event) => event.stopPropagation()} + className={cn( + 'absolute right-1 top-1/2 hidden -translate-y-1/2 items-center gap-0.5', + 'opacity-0 transition-opacity duration-150', + 'group-hover/session-tab:flex group-hover/session-tab:opacity-100', + menuOpen && 'flex opacity-100', + )} + > + @@ -157,6 +198,7 @@ const InactiveSessionTab: React.FC<{ +
); @@ -173,7 +215,13 @@ const InactiveSessionTab: React.FC<{ * in the store but do not render, so a partial session list never destroys * the working set. */ -export const SessionTabsStrip: React.FC<{ children: React.ReactNode }> = ({ children }) => { +export const SessionTabsStrip: React.FC<{ + /** The header's session menu for the active tab (already a DropdownMenu). */ + menu?: React.ReactNode; + /** Whether that menu is open, so the hover overlay stays visible. */ + menuOpen?: boolean; + children: React.ReactNode; +}> = ({ menu = null, menuOpen = false, children }) => { const { t } = useI18n(); const tabIds = useSessionTabsStore((state) => state.tabIds); const ensureTab = useSessionTabsStore((state) => state.ensureTab); @@ -284,7 +332,18 @@ export const SessionTabsStrip: React.FC<{ children: React.ReactNode }> = ({ chil const renderTab = (tab: SessionTab) => { if (tab.id === currentSessionId) { - return {children}; + return ( + handleClose(tab.id)} + closeLabel={t('header.sessionTabs.closeTab')} + > + {children} + + ); } return ( = ({ chil
- {children} +
{children}
) : null}