refactor(header): drop session tab tooltips
The hover info card (project/branch/PR/time) added noise without pull — the tab title plus the sidebar already cover it. The tooltip body component, its per-hover subscriptions, and the native title attribute are gone; right-click/menu and the status dot are untouched.
This commit is contained in:
+1
-1
@@ -13,7 +13,7 @@ All notable changes to this project will be documented in this file.
|
||||
- **Terminal:** terminals no longer vanish or die behind your back. Opening the app in another browser tab, on another device, or after a reload now shows the terminals already running on the server instead of an empty list, and terminals sitting in background tabs are no longer closed by the server's idle cleanup while the app is open.
|
||||
- Chat: @ file mentions now rank files and directories together by how well they match, so the file you typed is at the top instead of below unrelated directories. Multi-word queries match in any order, and long paths keep the folder next to the file name visible so identical-looking index.md rows are distinguishable.
|
||||
- Search: Ctrl/Cmd+P now matches the whole file path, not just the file name — searching a folder name like "solo-is-a" finds the file inside it.
|
||||
- **Session tabs (opt-in):** the web/desktop header can show your open sessions as browser-style tabs — turn them on in Settings → General → Navigation → Session tabs. Every session you open joins the strip, clicking a tab switches the whole workspace (chat, project, panels), and closing one (its × button, middle-click, or Alt+W — rebindable in Shortcuts) never touches the session itself. Tabs reorder by drag, scroll behind the header buttons when there are many, carry the sidebar's running/unread dot, and show the sidebar's info tooltip (project, branch, PR status) on hover. Each tab has the full session menu — on the "..." button or right-click — plus Close other tabs; renaming works right in the tab.
|
||||
- **Session tabs (opt-in):** the web/desktop header can show your open sessions as browser-style tabs — turn them on in Settings → General → Navigation → Session tabs. Every session you open joins the strip, clicking a tab switches the whole workspace (chat, project, panels), and closing one (its × button, middle-click, or Alt+W — rebindable in Shortcuts) never touches the session itself. Tabs reorder by drag, scroll behind the header buttons when there are many and carry the sidebar's running/unread dot. Each tab has the full session menu — on the "..." button or right-click — plus Close other tabs; renaming works right in the tab.
|
||||
- Files: the editor toolbar is now always docked under the file tabs; the floating hover toolbar and its setting were removed.
|
||||
- **Search in dropdowns:** every searchable picker — branches, projects, agents, models, providers, stashes, SSH hosts, skills, archived sessions — now uses one matcher: best matches come first, multi-word queries match in any order, and punctuation doesn't matter (so "gpt4o" finds "gpt-4o"). The git branch and gitmoji pickers also stopped silently dropping rows that a second, built-in filter didn't like. Sidebar session search and the Todos/Memory/Plans/Notes filters match the same way now.
|
||||
- Terminal: mobile keyboards no longer capitalize the first letter of every command on iOS and Android.
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { dropdownMenuItemClass, dropdownMenuPopupClass, dropdownMenuSeparatorClass } from '@/components/ui/dropdown-menu.styles';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { Icon } from '@/components/icon/Icon';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
@@ -36,15 +35,6 @@ import { useGlobalSessionsStore, resolveGlobalSessionDirectory } from '@/stores/
|
||||
import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||
import { useGlobalSessionStatus } from '@/sync/sync-context';
|
||||
import { useSessionUnseenCount } from '@/sync/notification-store';
|
||||
import { useProjectsStore } from '@/stores/useProjectsStore';
|
||||
import { useGitAllBranches } from '@/stores/useGitStore';
|
||||
import { getGitHubPrStatusKey, usePrVisualSummary } from '@/stores/useGitHubPrStatusStore';
|
||||
import {
|
||||
formatProjectLabel,
|
||||
formatSessionCompactDateLabel,
|
||||
formatSessionDateLabel,
|
||||
normalizePath,
|
||||
} from '@/components/session/sidebar/utils';
|
||||
|
||||
const restrictToXAxis: Modifier = ({ transform }) => ({ ...transform, y: 0 });
|
||||
|
||||
@@ -83,96 +73,6 @@ const contextComponents: SessionTabMenuComponents = {
|
||||
),
|
||||
};
|
||||
|
||||
/** Resolve the project a session directory belongs to, for the hover tooltip. */
|
||||
const useTabProjectLabel = (directory: string | null): string | null =>
|
||||
useProjectsStore(React.useCallback((state) => {
|
||||
if (!directory) return null;
|
||||
const dir = normalizePath(directory);
|
||||
if (!dir) return null;
|
||||
for (const project of state.projects) {
|
||||
const path = normalizePath(project.path);
|
||||
if (path && (dir === path || dir.startsWith(`${path}/`))) {
|
||||
return formatProjectLabel(project.label?.trim() || path.split('/').pop() || path);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}, [directory]));
|
||||
|
||||
/**
|
||||
* Tooltip body for one tab. Lives in its own component so the branch,
|
||||
* worktree, PR and project subscriptions exist only while the tooltip is
|
||||
* open — the resting tab pays only for its status dot.
|
||||
*/
|
||||
const SessionTabTooltipBody: React.FC<{ tab: SessionTab; title: string }> = ({ tab, title }) => {
|
||||
const { t } = useI18n();
|
||||
const directory = normalizePath(resolveGlobalSessionDirectory(tab.session) ?? null);
|
||||
const projectLabel = useTabProjectLabel(directory);
|
||||
const worktreeMetadata = useSessionUIStore((state) => state.worktreeMetadata);
|
||||
const allBranches = useGitAllBranches();
|
||||
const branchLabel = React.useMemo(() => {
|
||||
const meta = worktreeMetadata.get(tab.id);
|
||||
if (meta?.branch?.trim()) return meta.branch.trim();
|
||||
if (directory) return allBranches.get(directory)?.trim() || null;
|
||||
return null;
|
||||
}, [worktreeMetadata, allBranches, tab.id, directory]);
|
||||
const prSummary = usePrVisualSummary(directory && branchLabel ? getGitHubPrStatusKey(directory, branchLabel) : null);
|
||||
const prIconColor = prSummary ? `var(--pr-${prSummary.visualState})` : undefined;
|
||||
const prStatusLabel = React.useMemo(() => {
|
||||
if (!prSummary) return null;
|
||||
switch (prSummary.visualState) {
|
||||
case 'merged':
|
||||
return t('sessions.sidebar.group.pr.status.merged');
|
||||
case 'open':
|
||||
return (prSummary.canMerge === true || prSummary.mergeableState === 'clean' || prSummary.checks?.state === 'success')
|
||||
? t('sessions.sidebar.group.pr.status.readyToMerge')
|
||||
: t('sessions.sidebar.group.pr.status.open');
|
||||
case 'blocked':
|
||||
return prSummary.mergeableState === 'dirty'
|
||||
? t('sessions.sidebar.group.pr.status.mergeConflicts')
|
||||
: t('sessions.sidebar.group.pr.status.mergeBlocked');
|
||||
case 'draft':
|
||||
return t('sessions.sidebar.group.pr.status.draft');
|
||||
case 'closed':
|
||||
return t('sessions.sidebar.group.pr.status.closed');
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}, [prSummary, t]);
|
||||
const sessionTimestamp = tab.session.time?.updated || tab.session.time?.created || 0;
|
||||
return (
|
||||
<div className="flex min-w-44 flex-col gap-1.5 text-left text-xs">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="min-w-0 truncate font-medium text-foreground">{title}</span>
|
||||
{sessionTimestamp ? (
|
||||
<span className="flex-shrink-0 text-muted-foreground" title={formatSessionDateLabel(sessionTimestamp)}>
|
||||
{formatSessionCompactDateLabel(sessionTimestamp)}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{projectLabel ? (
|
||||
<div className="flex min-w-0 items-center gap-1.5 text-muted-foreground">
|
||||
<Icon name="folder" className="h-3 w-3 flex-shrink-0" />
|
||||
<span className="min-w-0 truncate">{projectLabel}</span>
|
||||
</div>
|
||||
) : null}
|
||||
{branchLabel ? (
|
||||
<div className="flex min-w-0 items-center gap-1.5 text-muted-foreground">
|
||||
<Icon name="git-branch" className="h-3 w-3 flex-shrink-0" style={prIconColor ? { color: prIconColor } : undefined} />
|
||||
<span className="min-w-0 truncate">{branchLabel}</span>
|
||||
</div>
|
||||
) : null}
|
||||
{prSummary && prStatusLabel ? (
|
||||
<div className="flex min-w-0 items-center gap-1.5">
|
||||
<Icon name="git-pull-request" className="h-3 w-3 flex-shrink-0" style={prIconColor ? { color: prIconColor } : undefined} />
|
||||
<span className="min-w-0 truncate" style={prIconColor ? { color: prIconColor } : undefined}>
|
||||
#{prSummary.number} · {prStatusLabel}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* One tab, active or not. The tab drags to reorder; the menu and close
|
||||
* controls sit in a hover-revealed overlay at the tab's end (menu first,
|
||||
@@ -203,7 +103,6 @@ const SessionTabItem: React.FC<{
|
||||
|
||||
const title = tab.session.title?.trim() || t('sessions.sidebar.session.untitled');
|
||||
const overlayVisible = !suppressControls && (menuOpen || menuVisible);
|
||||
const anyMenuOpen = menuOpen || contextMenuOpen;
|
||||
|
||||
// Session state for the dot and the hover tooltip.
|
||||
const sessionStatus = useGlobalSessionStatus(tab.id);
|
||||
@@ -238,9 +137,7 @@ const SessionTabItem: React.FC<{
|
||||
onOpenChange={setContextMenuOpen}
|
||||
onOpenChangeComplete={(open) => onMenuOpenChangeComplete?.(open)}
|
||||
>
|
||||
<Tooltip delayDuration={700}>
|
||||
<TooltipTrigger asChild>
|
||||
<ContextMenu.Trigger
|
||||
<ContextMenu.Trigger
|
||||
render={(triggerProps) => (
|
||||
<div
|
||||
{...triggerProps}
|
||||
@@ -346,14 +243,7 @@ const SessionTabItem: React.FC<{
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
{!anyMenuOpen && !isDragging ? (
|
||||
<TooltipContent side="bottom" sideOffset={8} className="max-w-xs text-left">
|
||||
<SessionTabTooltipBody tab={tab} title={title} />
|
||||
</TooltipContent>
|
||||
) : null}
|
||||
</Tooltip>
|
||||
/>
|
||||
<ContextMenu.Portal>
|
||||
<ContextMenu.Positioner className="app-region-no-drag z-50">
|
||||
<ContextMenu.Popup
|
||||
|
||||
Reference in New Issue
Block a user