agent-ecosystem/src/renderer/components/layout/TabContextMenu.tsx
iliya 06bf5d4381 merge: sync with upstream/main — session reports, cost calculation, Linux title bar, auto-expand AI groups
Brings in upstream changes:
- Session analysis reports (overview, cost, tokens, tools, git, quality)
- Unified cost calculation with LiteLLM pricing data
- Custom title bar for Linux with native toggle
- Auto-expand AI response groups setting
- MoreMenu toolbar component
- Various fixes (window drag, Ctrl+R, notification guard)

All merge conflicts resolved preserving both fork features (team management,
agent language, fullscreen, diff view) and upstream additions.
2026-02-24 21:04:23 +02:00

171 lines
4.9 KiB
TypeScript

/**
* TabContextMenu - Right-click context menu for tab actions.
* Supports close, close others, close all, bulk close for multi-select,
* and split left/right for pane management.
* Shows keyboard shortcut hints for actions that have them.
*/
import { useEffect, useRef } from 'react';
import { formatShortcut } from '@renderer/utils/stringUtils';
interface TabContextMenuProps {
x: number;
y: number;
tabId: string;
paneId: string;
selectedCount: number;
onClose: () => void;
onCloseTab: () => void;
onCloseOtherTabs: () => void;
onCloseAllTabs: () => void;
onCloseSelectedTabs?: () => void;
onSplitRight: () => void;
onSplitLeft: () => void;
disableSplit: boolean;
/** Whether this tab is a session tab (pin only applies to sessions) */
isSessionTab?: boolean;
/** Whether this session is currently pinned in the sidebar */
isPinned?: boolean;
/** Callback to toggle pin state */
onTogglePin?: () => void;
/** Whether this session is currently hidden from the sidebar */
isHidden?: boolean;
/** Callback to toggle hide state */
onToggleHide?: () => void;
}
export const TabContextMenu = ({
x,
y,
selectedCount,
onClose,
onCloseTab,
onCloseOtherTabs,
onCloseAllTabs,
onCloseSelectedTabs,
onSplitRight,
onSplitLeft,
disableSplit,
isSessionTab,
isPinned,
onTogglePin,
isHidden,
onToggleHide,
}: TabContextMenuProps): React.JSX.Element => {
const menuRef = useRef<HTMLDivElement>(null);
// Close on click-outside and Escape
useEffect(() => {
const handleMouseDown = (e: MouseEvent): void => {
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
onClose();
}
};
const handleKeyDown = (e: KeyboardEvent): void => {
if (e.key === 'Escape') onClose();
};
document.addEventListener('mousedown', handleMouseDown);
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('mousedown', handleMouseDown);
document.removeEventListener('keydown', handleKeyDown);
};
}, [onClose]);
// Viewport clamping
const menuWidth = 240;
const menuHeight = selectedCount > 1 ? 220 : 196;
const clampedX = Math.min(x, window.innerWidth - menuWidth - 8);
const clampedY = Math.min(y, window.innerHeight - menuHeight - 8);
const handleClick = (action: () => void) => () => {
action();
onClose();
};
return (
<div
ref={menuRef}
className="fixed z-50 min-w-[220px] overflow-hidden rounded-md border py-1 shadow-lg"
style={{
left: clampedX,
top: clampedY,
backgroundColor: 'var(--color-surface-overlay)',
borderColor: 'var(--color-border-emphasis)',
color: 'var(--color-text)',
}}
>
{selectedCount > 1 && onCloseSelectedTabs ? (
<MenuItem
label={`Close ${selectedCount} Tabs`}
onClick={handleClick(onCloseSelectedTabs)}
/>
) : (
<MenuItem
label="Close Tab"
shortcut={formatShortcut('W')}
onClick={handleClick(onCloseTab)}
/>
)}
<MenuItem label="Close Other Tabs" onClick={handleClick(onCloseOtherTabs)} />
<div className="mx-2 my-1 border-t" style={{ borderColor: 'var(--color-border)' }} />
<MenuItem
label="Split Right"
shortcut={formatShortcut('\\')}
onClick={handleClick(onSplitRight)}
disabled={disableSplit}
/>
<MenuItem label="Split Left" onClick={handleClick(onSplitLeft)} disabled={disableSplit} />
{isSessionTab && onTogglePin && (
<>
<div className="mx-2 my-1 border-t" style={{ borderColor: 'var(--color-border)' }} />
<MenuItem
label={isPinned ? 'Unpin from Sidebar' : 'Pin to Sidebar'}
onClick={handleClick(onTogglePin)}
/>
</>
)}
{isSessionTab && onToggleHide && (
<MenuItem
label={isHidden ? 'Unhide from Sidebar' : 'Hide from Sidebar'}
onClick={handleClick(onToggleHide)}
/>
)}
<div className="mx-2 my-1 border-t" style={{ borderColor: 'var(--color-border)' }} />
<MenuItem
label="Close All Tabs"
shortcut={formatShortcut('W', { shift: true })}
onClick={handleClick(onCloseAllTabs)}
/>
</div>
);
};
const MenuItem = ({
label,
shortcut,
onClick,
disabled,
}: {
label: string;
shortcut?: string;
onClick: () => void;
disabled?: boolean;
}): React.JSX.Element => {
return (
<button
className="flex w-full items-center justify-between px-3 py-1.5 text-left text-sm transition-colors hover:bg-[var(--color-surface-raised)]"
onClick={onClick}
disabled={disabled}
style={{ opacity: disabled ? 0.4 : 1 }}
>
<span>{label}</span>
{shortcut && (
<span className="ml-4 text-xs" style={{ color: 'var(--color-text-muted)' }}>
{shortcut}
</span>
)}
</button>
);
};