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.
182 lines
5.2 KiB
TypeScript
182 lines
5.2 KiB
TypeScript
/**
|
|
* SessionContextMenu - Right-click context menu for sidebar session items.
|
|
* Supports opening in current pane, new tab, and split right.
|
|
* Shows keyboard shortcut hints for actions that have them.
|
|
*/
|
|
|
|
import { useEffect, useRef, useState } from 'react';
|
|
|
|
import { MAX_PANES } from '@renderer/types/panes';
|
|
import { formatShortcut } from '@renderer/utils/stringUtils';
|
|
import { Check, ClipboardCopy, Eye, EyeOff, Pin, PinOff, Terminal } from 'lucide-react';
|
|
|
|
interface SessionContextMenuProps {
|
|
x: number;
|
|
y: number;
|
|
sessionId: string;
|
|
projectId: string;
|
|
sessionLabel: string;
|
|
paneCount: number;
|
|
isPinned: boolean;
|
|
isHidden: boolean;
|
|
onClose: () => void;
|
|
onOpenInCurrentPane: () => void;
|
|
onOpenInNewTab: () => void;
|
|
onSplitRightAndOpen: () => void;
|
|
onTogglePin: () => void;
|
|
onToggleHide: () => void;
|
|
}
|
|
|
|
export const SessionContextMenu = ({
|
|
x,
|
|
y,
|
|
sessionId,
|
|
paneCount,
|
|
isPinned,
|
|
isHidden,
|
|
onClose,
|
|
onOpenInCurrentPane,
|
|
onOpenInNewTab,
|
|
onSplitRightAndOpen,
|
|
onTogglePin,
|
|
onToggleHide,
|
|
}: SessionContextMenuProps): React.JSX.Element => {
|
|
const menuRef = useRef<HTMLDivElement>(null);
|
|
const [copiedField, setCopiedField] = useState<'id' | 'command' | null>(null);
|
|
|
|
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]);
|
|
|
|
const menuWidth = 240;
|
|
const menuHeight = 290;
|
|
const clampedX = Math.min(x, window.innerWidth - menuWidth - 8);
|
|
const clampedY = Math.min(y, window.innerHeight - menuHeight - 8);
|
|
|
|
const handleClick = (action: () => void) => () => {
|
|
action();
|
|
onClose();
|
|
};
|
|
|
|
const handleCopy = (text: string, field: 'id' | 'command') => async () => {
|
|
try {
|
|
await navigator.clipboard.writeText(text);
|
|
setCopiedField(field);
|
|
setTimeout(() => {
|
|
setCopiedField(null);
|
|
onClose();
|
|
}, 600);
|
|
} catch {
|
|
// Silently fail
|
|
}
|
|
};
|
|
|
|
const atMaxPanes = paneCount >= MAX_PANES;
|
|
|
|
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)',
|
|
}}
|
|
>
|
|
<MenuItem label="Open in Current Pane" onClick={handleClick(onOpenInCurrentPane)} />
|
|
<MenuItem
|
|
label="Open in New Tab"
|
|
shortcut={`${formatShortcut('')}Click`}
|
|
onClick={handleClick(onOpenInNewTab)}
|
|
/>
|
|
<div className="mx-2 my-1 border-t" style={{ borderColor: 'var(--color-border)' }} />
|
|
<MenuItem
|
|
label="Split Right and Open"
|
|
onClick={handleClick(onSplitRightAndOpen)}
|
|
disabled={atMaxPanes}
|
|
/>
|
|
<div className="mx-2 my-1 border-t" style={{ borderColor: 'var(--color-border)' }} />
|
|
<MenuItem
|
|
label={isPinned ? 'Unpin Session' : 'Pin Session'}
|
|
icon={isPinned ? <PinOff className="size-4" /> : <Pin className="size-4" />}
|
|
onClick={handleClick(onTogglePin)}
|
|
/>
|
|
<MenuItem
|
|
label={isHidden ? 'Unhide Session' : 'Hide Session'}
|
|
icon={isHidden ? <Eye className="size-4" /> : <EyeOff className="size-4" />}
|
|
onClick={handleClick(onToggleHide)}
|
|
/>
|
|
<div className="mx-2 my-1 border-t" style={{ borderColor: 'var(--color-border)' }} />
|
|
<MenuItem
|
|
label={copiedField === 'id' ? 'Copied!' : 'Copy Session ID'}
|
|
icon={
|
|
copiedField === 'id' ? (
|
|
<Check className="size-4 text-green-400" />
|
|
) : (
|
|
<ClipboardCopy className="size-4" />
|
|
)
|
|
}
|
|
onClick={handleCopy(sessionId, 'id')}
|
|
/>
|
|
<MenuItem
|
|
label={copiedField === 'command' ? 'Copied!' : 'Copy Resume Command'}
|
|
icon={
|
|
copiedField === 'command' ? (
|
|
<Check className="size-4 text-green-400" />
|
|
) : (
|
|
<Terminal className="size-4" />
|
|
)
|
|
}
|
|
onClick={handleCopy(`claude --resume ${sessionId}`, 'command')}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const MenuItem = ({
|
|
label,
|
|
shortcut,
|
|
icon,
|
|
onClick,
|
|
disabled,
|
|
}: {
|
|
label: string;
|
|
shortcut?: string;
|
|
icon?: React.ReactNode;
|
|
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 className="flex items-center gap-2">
|
|
{icon}
|
|
{label}
|
|
</span>
|
|
{shortcut && (
|
|
<span className="ml-4 text-xs" style={{ color: 'var(--color-text-muted)' }}>
|
|
{shortcut}
|
|
</span>
|
|
)}
|
|
</button>
|
|
);
|
|
};
|