diff --git a/packages/ui/src/components/comments/InlineCommentCard.tsx b/packages/ui/src/components/comments/InlineCommentCard.tsx index 6eae933e..326b1695 100644 --- a/packages/ui/src/components/comments/InlineCommentCard.tsx +++ b/packages/ui/src/components/comments/InlineCommentCard.tsx @@ -7,6 +7,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; +import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@/components/ui/context-menu'; import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; @@ -32,6 +33,7 @@ export function InlineCommentCard({ const themeContext = useOptionalThemeSystem(); const currentTheme = themeContext?.currentTheme; const [isOpen, setIsOpen] = useState(false); + const [isContextMenuOpen, setIsContextMenuOpen] = useState(false); const draftText = typeof draft.text === 'string' ? draft.text : ''; // Check if content is long enough to warrant collapsing (rough estimate) @@ -39,18 +41,27 @@ export function InlineCommentCard({ const isLongContent = draftText.length > 150 || draftText.split('\n').length > 3; return ( -
+ + { + event.preventDefault(); + setIsContextMenuOpen(true); + }} + /> + } + >
@@ -117,6 +128,17 @@ export function InlineCommentCard({
-
+ + + + + {t('inlineComment.actions.editComment')} + + + + {t('inlineComment.actions.deleteComment')} + + + ); } diff --git a/packages/ui/src/components/layout/SidebarFilesTree.tsx b/packages/ui/src/components/layout/SidebarFilesTree.tsx index 1b007147..e27e0aaf 100644 --- a/packages/ui/src/components/layout/SidebarFilesTree.tsx +++ b/packages/ui/src/components/layout/SidebarFilesTree.tsx @@ -16,6 +16,13 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuSeparator, + ContextMenuTrigger, +} from '@/components/ui/context-menu'; import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; @@ -136,6 +143,8 @@ interface FileRowProps { downloadFile?: (path: string) => Promise; contextMenuPath: string | null; setContextMenuPath: (path: string | null) => void; + rightClickMenuPath: string | null; + setRightClickMenuPath: (path: string | null) => void; onSelect: (node: FileNode) => void; onToggle: (path: string) => void; onRevealPath: (path: string) => void; @@ -153,6 +162,8 @@ const FileRow: React.FC = ({ downloadFile, contextMenuPath, setContextMenuPath, + rightClickMenuPath, + setRightClickMenuPath, onSelect, onToggle, onRevealPath, @@ -165,8 +176,8 @@ const FileRow: React.FC = ({ const handleContextMenu = React.useCallback((event?: React.MouseEvent) => { if (!canRename && !canCreateFile && !canCreateFolder && !canDelete && !canReveal) return; event?.preventDefault(); - setContextMenuPath(node.path); - }, [canRename, canCreateFile, canCreateFolder, canDelete, canReveal, node.path, setContextMenuPath]); + setRightClickMenuPath(node.path); + }, [canRename, canCreateFile, canCreateFolder, canDelete, canReveal, node.path, setRightClickMenuPath]); const handleInteraction = React.useCallback(() => { if (isDir) { @@ -178,8 +189,76 @@ const FileRow: React.FC = ({ const handleMenuButtonClick = React.useCallback((event: React.MouseEvent) => { event.stopPropagation(); + setRightClickMenuPath(null); setContextMenuPath(node.path); - }, [node.path, setContextMenuPath]); + }, [node.path, setContextMenuPath, setRightClickMenuPath]); + + const renderMenuItems = ({ + Item, + Separator, + }: { + Item: React.ElementType; + Separator: React.ElementType; + }) => ( + <> + {canRename && ( + { e.stopPropagation(); onOpenDialog('rename', node); }}> + {t('sidebarFilesTree.menu.rename')} + + )} + { + e.stopPropagation(); + void copyTextToClipboard(node.path).then((result) => { + if (result.ok) { + toast.success(t('sidebarFilesTree.toast.pathCopied')); + return; + } + toast.error(t('sidebarFilesTree.toast.copyFailed')); + }); + }}> + {t('sidebarFilesTree.menu.copyPath')} + + {!isDir && downloadFile && ( + { + e.stopPropagation(); + void downloadFile(node.path); + }}> + {t('sidebarFilesTree.menu.save')} + + )} + {canReveal && ( + { e.stopPropagation(); onRevealPath(node.path); }}> + {t(getRevealLabelKey())} + + )} + {isDir && (canCreateFile || canCreateFolder) && ( + <> + + {canCreateFile && ( + { e.stopPropagation(); onOpenDialog('createFile', node); }}> + {t('sidebarFilesTree.menu.newFile')} + + )} + {canCreateFolder && ( + { e.stopPropagation(); onOpenDialog('createFolder', node); }}> + {t('sidebarFilesTree.menu.newFolder')} + + )} + + )} + {canDelete && ( + <> + + { e.stopPropagation(); onOpenDialog('delete', node); }} + className="text-destructive focus:text-destructive" + > + {t('sidebarFilesTree.menu.delete')} + + + )} + + ); const handleDragStart = React.useCallback((e: React.DragEvent) => { const path = getRelativePath(root, node.path); @@ -189,10 +268,8 @@ const FileRow: React.FC = ({ }, [node.path, root]); return ( -
+ setRightClickMenuPath(open ? node.path : null)}> + }>
)} -
+
+ + {renderMenuItems({ Item: ContextMenuItem, Separator: ContextMenuSeparator })} + +
); }; @@ -357,6 +383,7 @@ export const SidebarFilesTree: React.FC = () => { // Context menu state const [contextMenuPath, setContextMenuPath] = React.useState(null); + const [rightClickMenuPath, setRightClickMenuPath] = React.useState(null); // Dialog state for CRUD operations const [activeDialog, setActiveDialog] = React.useState<'createFile' | 'createFolder' | 'rename' | 'delete' | null>(null); @@ -801,6 +828,8 @@ export const SidebarFilesTree: React.FC = () => { downloadFile={files.downloadFile} contextMenuPath={contextMenuPath} setContextMenuPath={setContextMenuPath} + rightClickMenuPath={rightClickMenuPath} + setRightClickMenuPath={setRightClickMenuPath} onSelect={handleOpenFile} onToggle={toggleDirectory} onRevealPath={handleRevealPath} diff --git a/packages/ui/src/components/sections/agents/AgentsSidebar.tsx b/packages/ui/src/components/sections/agents/AgentsSidebar.tsx index 51710384..9155eef8 100644 --- a/packages/ui/src/components/sections/agents/AgentsSidebar.tsx +++ b/packages/ui/src/components/sections/agents/AgentsSidebar.tsx @@ -17,6 +17,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; +import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@/components/ui/context-menu'; import { useAgentsStore, isAgentBuiltIn, isAgentHidden, type AgentScope, type AgentDraft } from '@/stores/useAgentsStore'; import { useShallow } from 'zustand/react/shallow'; import { cn } from '@/lib/utils'; @@ -537,18 +538,37 @@ const AgentListItem: React.FC = ({ const { t } = useI18n(); const extAgent = agent as Agent & { scope?: AgentScope }; const isMobile = isMobileDeviceViaCSS(); + const [isContextMenuOpen, setIsContextMenuOpen] = React.useState(false); + const renderMenuItems = (Item: React.ElementType) => ( + <> + {onRename && ( + { e.stopPropagation(); onRename(); }}> + + {t('settings.common.actions.rename')} + + )} + { e.stopPropagation(); onDuplicate(); }}> + + {t('settings.common.actions.duplicate')} + + {onReset && ( + { e.stopPropagation(); onReset(); }}> + + {t('settings.common.actions.reset')} + + )} + {onDelete && ( + { e.stopPropagation(); onDelete(); }} className="text-destructive focus:text-destructive"> + + {t('settings.common.actions.delete')} + + )} + + ); return ( -
{ - e.preventDefault(); - onMenuOpenChange(true); - } : undefined} - > + + { e.preventDefault(); setIsContextMenuOpen(true); } : undefined} />}>
- + { if (open) setIsContextMenuOpen(false); onMenuOpenChange(open); }}> - {onRename && ( - { - e.stopPropagation(); - onRename(); - }} - > - - {t('settings.common.actions.rename')} - - )} - - { - e.stopPropagation(); - onDuplicate(); - }} - > - - {t('settings.common.actions.duplicate')} - - - {onReset && ( - { - e.stopPropagation(); - onReset(); - }} - > - - {t('settings.common.actions.reset')} - - )} - - {onDelete && ( - { - e.stopPropagation(); - onDelete(); - }} - className="text-destructive focus:text-destructive" - > - - {t('settings.common.actions.delete')} - - )} + {renderMenuItems(DropdownMenuItem)}
-
+ + + {renderMenuItems(ContextMenuItem)} + + ); }; diff --git a/packages/ui/src/components/sections/commands/CommandsSidebar.tsx b/packages/ui/src/components/sections/commands/CommandsSidebar.tsx index 816b70bf..96807d29 100644 --- a/packages/ui/src/components/sections/commands/CommandsSidebar.tsx +++ b/packages/ui/src/components/sections/commands/CommandsSidebar.tsx @@ -17,6 +17,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; +import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@/components/ui/context-menu'; import { useCommandsStore, isCommandBuiltIn, type Command } from '@/stores/useCommandsStore'; import { useSkillsStore } from '@/stores/useSkillsStore'; import { useShallow } from 'zustand/react/shallow'; @@ -398,17 +399,36 @@ const CommandListItem: React.FC = ({ }) => { const { t } = useI18n(); const isMobile = isMobileDeviceViaCSS(); - return ( -
( + <> + {onRename && ( + { e.stopPropagation(); onRename(); }}> + + {t('settings.common.actions.rename')} + )} - onContextMenu={!isMobile ? (e) => { - e.preventDefault(); - onMenuOpenChange(true); - } : undefined} - > + { e.stopPropagation(); onDuplicate(); }}> + + {t('settings.common.actions.duplicate')} + + {onReset && ( + { e.stopPropagation(); onReset(); }}> + + {t('settings.common.actions.reset')} + + )} + {onDelete && ( + { e.stopPropagation(); onDelete(); }} className="text-destructive focus:text-destructive"> + + {t('settings.common.actions.delete')} + + )} + + ); + return ( + + { e.preventDefault(); setIsContextMenuOpen(true); } : undefined} />}>
- + { if (open) setIsContextMenuOpen(false); onMenuOpenChange(open); }}> - {onRename && ( - { - e.stopPropagation(); - onRename(); - }} - > - - {t('settings.common.actions.rename')} - - )} - - { - e.stopPropagation(); - onDuplicate(); - }} - > - - {t('settings.common.actions.duplicate')} - - - {onReset && ( - { - e.stopPropagation(); - onReset(); - }} - > - - {t('settings.common.actions.reset')} - - )} - - {onDelete && ( - { - e.stopPropagation(); - onDelete(); - }} - className="text-destructive focus:text-destructive" - > - - {t('settings.common.actions.delete')} - - )} + {renderMenuItems(DropdownMenuItem)}
-
+ + + {renderMenuItems(ContextMenuItem)} + + ); }; diff --git a/packages/ui/src/components/sections/git-identities/GitPage.tsx b/packages/ui/src/components/sections/git-identities/GitPage.tsx index 78a22158..cb592958 100644 --- a/packages/ui/src/components/sections/git-identities/GitPage.tsx +++ b/packages/ui/src/components/sections/git-identities/GitPage.tsx @@ -15,6 +15,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; +import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@/components/ui/context-menu'; import { useGitIdentitiesStore, type GitIdentityProfile, type DiscoveredGitCredential } from '@/stores/useGitIdentitiesStore'; import { useShallow } from 'zustand/react/shallow'; import { GitSettings } from '@/components/sections/openchamber/GitSettings'; @@ -248,6 +249,7 @@ const IdentityRow: React.FC = ({ hasBorder, }) => { const { t } = useI18n(); + const [contextMenuOpen, setContextMenuOpen] = React.useState(false); const iconName = ICON_MAP[profile.icon || 'branch'] || 'git-branch'; const iconColor = COLOR_MAP[profile.color || '']; const authType = profile.authType || 'ssh'; @@ -260,17 +262,43 @@ const IdentityRow: React.FC = ({ onEdit(); }; - return ( -
( + <> + { e.stopPropagation(); onToggleDefault(); }}> + {isDefault ? t('settings.gitIdentities.page.actions.unsetDefault') : t('settings.gitIdentities.page.actions.setAsDefault')} + + {!isReadOnly && onDelete && ( + { e.stopPropagation(); onDelete(); }} + className="text-destructive focus:text-destructive" + > + + {t('settings.common.actions.delete')} + )} - onClick={onEdit} - role="button" - tabIndex={0} - onKeyDown={handleKeyDown} - > + + ); + + return ( + + { + event.preventDefault(); + setContextMenuOpen(true); + }} + /> + } + >
@@ -308,21 +336,14 @@ const IdentityRow: React.FC = ({ - { e.stopPropagation(); onToggleDefault(); }}> - {isDefault ? t('settings.gitIdentities.page.actions.unsetDefault') : t('settings.gitIdentities.page.actions.setAsDefault')} - - {!isReadOnly && onDelete && ( - { e.stopPropagation(); onDelete(); }} - className="text-destructive focus:text-destructive" - > - - {t('settings.common.actions.delete')} - - )} + {renderMenuItems(DropdownMenuItem)} -
+ + + {renderMenuItems(ContextMenuItem)} + + ); }; diff --git a/packages/ui/src/components/sections/mcp/McpSidebar.tsx b/packages/ui/src/components/sections/mcp/McpSidebar.tsx index f408fd8b..f7b11c42 100644 --- a/packages/ui/src/components/sections/mcp/McpSidebar.tsx +++ b/packages/ui/src/components/sections/mcp/McpSidebar.tsx @@ -23,6 +23,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; +import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@/components/ui/context-menu'; import { useI18n } from '@/lib/i18n'; interface McpSidebarProps { @@ -81,6 +82,7 @@ export const McpSidebar: React.FC = ({ onItemSelect }) => { const [deleteTarget, setDeleteTarget] = React.useState(null); const [isDeleting, setIsDeleting] = React.useState(false); const [openMenuMcp, setOpenMenuMcp] = React.useState(null); + const [rightClickMenuMcp, setRightClickMenuMcp] = React.useState(null); const [isRefreshingStatus, setIsRefreshingStatus] = React.useState(false); const projectServers = React.useMemo( @@ -164,6 +166,19 @@ export const McpSidebar: React.FC = ({ onItemSelect }) => { setIsDeleting(false); }; + const renderMcpMenuItems = (server: McpServerConfig, Item: React.ElementType) => ( + { + e.stopPropagation(); + setDeleteTarget(server); + }} + className="text-destructive focus:text-destructive" + > + + {t('settings.common.actions.delete')} + + ); + return (
@@ -219,17 +234,8 @@ export const McpSidebar: React.FC = ({ onItemSelect }) => { const isMobile = isMobileDeviceViaCSS(); return ( -
{ - e.preventDefault(); - setOpenMenuMcp(server.name); - } : undefined} - > + setRightClickMenuMcp(open ? server.name : null)}> + { e.preventDefault(); setRightClickMenuMcp(server.name); } : undefined} />}>
- setOpenMenuMcp(open ? server.name : null)}> + { if (open) setRightClickMenuMcp(null); setOpenMenuMcp(open ? server.name : null); }}> - { - e.stopPropagation(); - setDeleteTarget(server); - }} - className="text-destructive focus:text-destructive" - > - - {t('settings.common.actions.delete')} - + {renderMcpMenuItems(server, DropdownMenuItem)} -
+ + + {renderMcpMenuItems(server, ContextMenuItem)} + + ); })} @@ -296,17 +297,8 @@ export const McpSidebar: React.FC = ({ onItemSelect }) => { const isMobile = isMobileDeviceViaCSS(); return ( -
{ - e.preventDefault(); - setOpenMenuMcp(server.name); - } : undefined} - > + setRightClickMenuMcp(open ? server.name : null)}> + { e.preventDefault(); setRightClickMenuMcp(server.name); } : undefined} />}>
- setOpenMenuMcp(open ? server.name : null)}> + { if (open) setRightClickMenuMcp(null); setOpenMenuMcp(open ? server.name : null); }}> - { - e.stopPropagation(); - setDeleteTarget(server); - }} - className="text-destructive focus:text-destructive" - > - - {t('settings.common.actions.delete')} - + {renderMcpMenuItems(server, DropdownMenuItem)} -
+ + + {renderMcpMenuItems(server, ContextMenuItem)} + + ); })} diff --git a/packages/ui/src/components/sections/skills/SkillsSidebar.tsx b/packages/ui/src/components/sections/skills/SkillsSidebar.tsx index b62d99aa..fabdedc0 100644 --- a/packages/ui/src/components/sections/skills/SkillsSidebar.tsx +++ b/packages/ui/src/components/sections/skills/SkillsSidebar.tsx @@ -17,6 +17,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; +import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@/components/ui/context-menu'; import { useSkillsStore, type DiscoveredSkill } from '@/stores/useSkillsStore'; import { useShallow } from 'zustand/react/shallow'; import { cn } from '@/lib/utils'; @@ -461,17 +462,26 @@ const SkillListItem: React.FC = ({ : t('settings.skills.sidebar.badge.opencode'); const badgeClassName = 'typography-micro text-muted-foreground bg-[var(--surface-muted)] px-1 rounded flex-shrink-0 leading-none pb-px border border-[var(--interactive-border)]/50'; const isBuiltIn = isBuiltInSkill(skill); + const [isContextMenuOpen, setIsContextMenuOpen] = React.useState(false); + const renderMenuItems = (Item: React.ElementType) => ( + <> + { e.stopPropagation(); onRename(); }}> + + {t('settings.common.actions.rename')} + + { e.stopPropagation(); onDuplicate(); }}> + + {t('settings.common.actions.duplicate')} + + { e.stopPropagation(); onDelete(); }} className="text-destructive focus:text-destructive"> + + {t('settings.common.actions.delete')} + + + ); return ( -
{ - e.preventDefault(); - onMenuOpenChange(true); - } : undefined} - > + + { e.preventDefault(); setIsContextMenuOpen(true); } : undefined} />}>
- {!isBuiltIn ? + {!isBuiltIn ? { if (open) setIsContextMenuOpen(false); onMenuOpenChange(open); }}> - { - e.stopPropagation(); - onRename(); - }} - > - - {t('settings.common.actions.rename')} - - - { - e.stopPropagation(); - onDuplicate(); - }} - > - - {t('settings.common.actions.duplicate')} - - - { - e.stopPropagation(); - onDelete(); - }} - className="text-destructive focus:text-destructive" - > - - {t('settings.common.actions.delete')} - + {renderMenuItems(DropdownMenuItem)} : null}
-
+
+ {!isBuiltIn ? ( + + {renderMenuItems(ContextMenuItem)} + + ) : null} +
); }; diff --git a/packages/ui/src/components/sections/snippets/SnippetsSidebar.tsx b/packages/ui/src/components/sections/snippets/SnippetsSidebar.tsx index 48697481..bd09a310 100644 --- a/packages/ui/src/components/sections/snippets/SnippetsSidebar.tsx +++ b/packages/ui/src/components/sections/snippets/SnippetsSidebar.tsx @@ -3,6 +3,7 @@ import { Button } from '@/components/ui/button'; import { toast } from '@/components/ui'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; +import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@/components/ui/context-menu'; import { useSnippetsStore } from '@/stores/useSnippetsStore'; import { useShallow } from 'zustand/react/shallow'; import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; @@ -19,6 +20,7 @@ export const SnippetsSidebar: React.FC = ({ onItemSelect } const { t } = useI18n(); const [confirmDeleteSnippet, setConfirmDeleteSnippet] = React.useState(null); const [openMenuName, setOpenMenuName] = React.useState(null); + const [rightClickMenuName, setRightClickMenuName] = React.useState(null); const { selectedSnippetName, snippets, setSelectedSnippet, setSnippetDraft, deleteSnippet, loadSnippets } = useSnippetsStore(useShallow((s) => ({ selectedSnippetName: s.selectedSnippetName, snippets: s.snippets, @@ -71,7 +73,8 @@ export const SnippetsSidebar: React.FC = ({ onItemSelect } {sortedSnippets.map((snippet) => ( -
+ setRightClickMenuName(open ? snippet.name : null)}> + { event.preventDefault(); setRightClickMenuName(snippet.name); }} />}> - setOpenMenuName(open ? snippet.name : null)}> + { if (open) setRightClickMenuName(null); setOpenMenuName(open ? snippet.name : null); }}>
+ + + { e.stopPropagation(); setConfirmDeleteSnippet(snippet); }} className="text-destructive focus:text-destructive"> + + {t('settings.common.actions.delete')} + + + ))}
diff --git a/packages/ui/src/components/session/sidebar/SessionNodeItem.tsx b/packages/ui/src/components/session/sidebar/SessionNodeItem.tsx index 258793ca..669d082f 100644 --- a/packages/ui/src/components/session/sidebar/SessionNodeItem.tsx +++ b/packages/ui/src/components/session/sidebar/SessionNodeItem.tsx @@ -1,5 +1,6 @@ import React from 'react'; import type { Session } from '@opencode-ai/sdk/v2'; +import { ContextMenu } from '@base-ui/react/context-menu'; import { DropdownMenu, DropdownMenuContent, @@ -10,6 +11,7 @@ import { DropdownMenuSubTrigger, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; +import { dropdownMenuItemClass, dropdownMenuPopupClass, dropdownMenuSeparatorClass, dropdownMenuSubTriggerClass } from '@/components/ui/dropdown-menu.styles'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { cn } from '@/lib/utils'; import { canUseElectronDesktopIPC, invokeDesktop, isVSCodeRuntime } from '@/lib/desktop'; @@ -352,6 +354,8 @@ function SessionNodeItemComponent(props: Props): React.ReactNode { const sessionUpdatedLabel = formatSessionDateLabel(sessionTimestamp); const sessionCompactUpdatedLabel = formatSessionCompactDateLabel(sessionTimestamp); const isMenuOpen = openSidebarMenuKey === menuInstanceKey; + const [isContextMenuOpen, setIsContextMenuOpen] = React.useState(false); + const isSessionMenuOpen = isMenuOpen || isContextMenuOpen; const isMultiRunLikeSession = React.useMemo(() => parseMultiRunSessionTitle(resolvedSession.title) !== null, [resolvedSession.title]); const [fusionDialogOpen, setFusionDialogOpen] = React.useState(false); @@ -602,6 +606,9 @@ function SessionNodeItemComponent(props: Props): React.ReactNode { : null; const handleMenuOpenChange = (open: boolean) => { + if (open) { + setIsContextMenuOpen(false); + } setOpenSidebarMenuKey(open ? menuInstanceKey : null); }; @@ -614,6 +621,10 @@ function SessionNodeItemComponent(props: Props): React.ReactNode { } }; + const handleContextMenuOpenChange = (open: boolean) => { + setIsContextMenuOpen(open); + }; + const handleMenuTriggerClick = (event: React.MouseEvent) => { event.preventDefault(); event.stopPropagation(); @@ -706,9 +717,21 @@ function SessionNodeItemComponent(props: Props): React.ReactNode { } }; - const sessionMenuContent = ( - (renamingFolderId || editingIdRef.current) ? false : true}> - ( + <> + { // Defer rename until dropdown close transition completes. // onOpenChangeComplete fires after animation + focus cleanup are done, @@ -719,38 +742,38 @@ function SessionNodeItemComponent(props: Props): React.ReactNode { > {t('sessions.sidebar.session.menu.rename')} - - togglePinnedSession(session.id)} className="[&>svg]:mr-1"> + + togglePinnedSession(session.id)} className="[&>svg]:mr-1"> {isPinnedSession ? : } {isPinnedSession ? t('sessions.sidebar.session.menu.unpin') : t('sessions.sidebar.session.menu.pin')} - + {!resolvedSession.share ? ( - handleShareSession(resolvedSession)} className="[&>svg]:mr-1"> + handleShareSession(resolvedSession)} className="[&>svg]:mr-1"> {t('sessions.sidebar.session.menu.share')} - + ) : ( <> - { if (resolvedSession.share?.url) handleCopyShareUrl(resolvedSession.share.url, session.id); }} className="[&>svg]:mr-1"> + { if (resolvedSession.share?.url) handleCopyShareUrl(resolvedSession.share.url, session.id); }} className="[&>svg]:mr-1"> {copiedSessionId === session.id ? <>{t('sessions.sidebar.session.menu.copied')} : <>{t('sessions.sidebar.session.menu.copyLink')}} - - handleUnshareSession(session.id)} className="[&>svg]:mr-1"> + + handleUnshareSession(session.id)} className="[&>svg]:mr-1"> {t('sessions.sidebar.session.menu.unshare')} - + )} - { void handleExportSession(); }} className="[&>svg]:mr-1"> + { void handleExportSession(); }} className="[&>svg]:mr-1"> {t('sessions.sidebar.session.menu.exportMarkdown')} - + {isMultiRunLikeSession ? ( - setFusionDialogOpen(true)} className="[&>svg]:mr-1"> + setFusionDialogOpen(true)} className="[&>svg]:mr-1"> {t('sessions.sidebar.session.menu.runFusion')} - + ) : null} {sessionDirectory && !archivedBucket ? (() => { @@ -758,39 +781,39 @@ function SessionNodeItemComponent(props: Props): React.ReactNode { const currentFolderId = getSessionFolderId(sessionDirectory, session.id); return ( <> - - - {t('sessions.sidebar.folders.moveToFolder')} - + + + {t('sessions.sidebar.folders.moveToFolder')} + {scopeFolders.length === 0 ? ( - {t('sessions.sidebar.folders.none')} + {t('sessions.sidebar.folders.none')} ) : ( scopeFolders.map((folder) => ( - { if (currentFolderId === folder.id) removeSessionFromFolder(sessionDirectory, session.id); else addSessionToFolder(sessionDirectory, folder.id, session.id); }}> + { if (currentFolderId === folder.id) removeSessionFromFolder(sessionDirectory, session.id); else addSessionToFolder(sessionDirectory, folder.id, session.id); }}> {folder.name} {currentFolderId === folder.id ? : null} - + )) )} - - { const newFolder = createFolderAndStartRename(sessionDirectory); if (!newFolder) return; addSessionToFolder(sessionDirectory, newFolder.id, session.id); }}> + + { const newFolder = createFolderAndStartRename(sessionDirectory); if (!newFolder) return; addSessionToFolder(sessionDirectory, newFolder.id, session.id); }}> {t('sessions.sidebar.folders.newFolderEllipsis')} - + {currentFolderId ? ( - { removeSessionFromFolder(sessionDirectory, session.id); }} className="text-destructive focus:text-destructive"> + { removeSessionFromFolder(sessionDirectory, session.id); }} className="text-destructive focus:text-destructive"> {t('sessions.sidebar.folders.removeFromFolder')} - + ) : null} - - + + ); })() : null} {!isVSCode ? ( - { if (!sessionDirectory) return; @@ -805,42 +828,108 @@ function SessionNodeItemComponent(props: Props): React.ReactNode { {t('sessions.sidebar.session.menu.openInSidePanel')} {t('sessions.sidebar.session.menu.betaBadge')} - + ) : null} {isElectron ? ( - {t('sessions.sidebar.session.menu.openMiniChatWindow')} - + ) : null} - - handleDeleteSession(session, { archivedBucket })}> + + handleDeleteSession(session, { archivedBucket })}> {archivedBucket ? t('sessions.sidebar.bulkActions.delete') : t('sessions.sidebar.bulkActions.archive')} - + + + ); + + const sessionMenuContent = ( + (renamingFolderId || editingIdRef.current) ? false : true}> + {renderSessionMenuItems({ + Item: DropdownMenuItem, + Separator: DropdownMenuSeparator, + Sub: DropdownMenuSub, + SubTrigger: DropdownMenuSubTrigger, + SubContent: DropdownMenuSubContent, + })} ); + const contextMenuContent = ( + + + (renamingFolderId || editingIdRef.current) ? false : true} + style={{ + backgroundColor: 'var(--surface-elevated)', + color: 'var(--surface-elevated-foreground)', + }} + className={cn(dropdownMenuPopupClass, 'min-w-[180px]')} + > + {renderSessionMenuItems({ + Item: ({ className, ...itemProps }: React.ComponentProps) => ( + + ), + Separator: ({ className, ...separatorProps }: React.ComponentProps) => ( + + ), + Sub: ContextMenu.SubmenuRoot, + SubTrigger: ({ className, children, ...triggerProps }: React.ComponentProps) => ( + + {children} + + + ), + SubContent: ({ className, children, ...popupProps }: React.ComponentProps) => ( + + + + {children} + + + + ), + })} + + + + ); + return ( -
0 && 'pl-[20px]', - isRowSelected && 'bg-primary/15', - )} - > + + 0 && 'pl-[20px]', + isRowSelected && 'bg-primary/15', + )} + /> + } + > {leadingIndicators} {subsessionChevron}
@@ -874,7 +963,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
@@ -963,7 +1052,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
-
+ + {contextMenuContent} + {hasChildren && isExpanded ? node.children.map((child) => renderSessionNode(child, depth + 1, sessionDirectory ?? groupDirectory, projectId, archivedBucket, undefined, renderContext)) diff --git a/packages/ui/src/components/session/sidebar/sortableItems.tsx b/packages/ui/src/components/session/sidebar/sortableItems.tsx index 7ea4b9be..797e15da 100644 --- a/packages/ui/src/components/session/sidebar/sortableItems.tsx +++ b/packages/ui/src/components/session/sidebar/sortableItems.tsx @@ -7,6 +7,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; +import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@/components/ui/context-menu'; import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'; import { Icon } from "@/components/icon/Icon"; import { cn } from '@/lib/utils'; @@ -89,14 +90,35 @@ export const SortableProjectItem: React.FC = ({ const suppressNextToggleRef = React.useRef(false); const menuInstanceKey = `project:${id}`; const isMenuOpen = openSidebarMenuKey === menuInstanceKey; + const [isContextMenuOpen, setIsContextMenuOpen] = React.useState(false); const projectIconName = projectIcon ? PROJECT_ICON_MAP[projectIcon] : null; const iconColor = projectColor ? (PROJECT_COLOR_MAP[projectColor] ?? null) : null; const handleMenuOpenChange = React.useCallback((open: boolean) => { + if (open) setIsContextMenuOpen(false); setOpenSidebarMenuKey(open ? menuInstanceKey : null); }, [menuInstanceKey, setOpenSidebarMenuKey]); + const renderProjectMenuItems = (Item: React.ElementType) => ( + <> + {showCreateButtons && !isRepo && !hideDirectoryControls && onNewSession && ( + + + {t('sessions.sidebar.project.actions.newSession')} + + )} + + + {t('sessions.sidebar.session.menu.rename')} + + + + {t('sessions.sidebar.project.actions.closeProject')} + + + ); + const handleMenuTriggerClick = React.useCallback((event: React.MouseEvent) => { event.stopPropagation(); }, []); @@ -140,12 +162,19 @@ export const SortableProjectItem: React.FC = ({ /> )} -
+ + { + event.preventDefault(); + setIsContextMenuOpen(true); + }} + /> + } + >
@@ -261,23 +290,7 @@ export const SortableProjectItem: React.FC = ({ - {showCreateButtons && !isRepo && !hideDirectoryControls && onNewSession && ( - - - {t('sessions.sidebar.project.actions.newSession')} - - )} - - - {t('sessions.sidebar.session.menu.rename')} - - - - {t('sessions.sidebar.project.actions.closeProject')} - + {renderProjectMenuItems(DropdownMenuItem)}
@@ -312,7 +325,11 @@ export const SortableProjectItem: React.FC = ({
) : null}
-
+ + + {renderProjectMenuItems(ContextMenuItem)} + + ) : null} diff --git a/packages/ui/src/components/ui/context-menu.tsx b/packages/ui/src/components/ui/context-menu.tsx new file mode 100644 index 00000000..15c38a4e --- /dev/null +++ b/packages/ui/src/components/ui/context-menu.tsx @@ -0,0 +1,99 @@ +import * as React from "react"; +import { ContextMenu as BaseContextMenu } from "@base-ui/react/context-menu"; + +import { Icon } from "@/components/icon/Icon"; +import { cn } from "@/lib/utils"; +import { + dropdownMenuItemClass, + dropdownMenuPopupClass, + dropdownMenuSeparatorClass, + dropdownMenuSubTriggerClass, +} from "./dropdown-menu.styles"; + +function ContextMenu({ ...props }: React.ComponentProps) { + return ; +} + +function ContextMenuTrigger({ ...props }: React.ComponentProps) { + return ; +} + +type ContentProps = { + className?: string; + positionerClassName?: string; + children?: React.ReactNode; +} & React.ComponentProps; + +function ContextMenuContent({ className, positionerClassName, children, style, ...props }: ContentProps) { + return ( + + + + {children} + + + + ); +} + +function ContextMenuItem({ className, ...props }: React.ComponentProps) { + return ; +} + +function ContextMenuSeparator({ className, ...props }: React.ComponentProps) { + return ; +} + +function ContextMenuSub({ ...props }: React.ComponentProps) { + return ; +} + +function ContextMenuSubTrigger({ className, children, ...props }: React.ComponentProps) { + return ( + + {children} + + + ); +} + +function ContextMenuSubContent({ className, positionerClassName, children, style, ...props }: ContentProps) { + return ( + + + + {children} + + + + ); +} + +export { + ContextMenu, + ContextMenuTrigger, + ContextMenuContent, + ContextMenuItem, + ContextMenuSeparator, + ContextMenuSub, + ContextMenuSubTrigger, + ContextMenuSubContent, +}; diff --git a/packages/ui/src/components/ui/dropdown-menu.styles.ts b/packages/ui/src/components/ui/dropdown-menu.styles.ts new file mode 100644 index 00000000..520dee1d --- /dev/null +++ b/packages/ui/src/components/ui/dropdown-menu.styles.ts @@ -0,0 +1,4 @@ +export const dropdownMenuPopupClass = "app-region-no-drag transition-all duration-150 ease-out data-[starting-style]:opacity-0 data-[starting-style]:scale-95 data-[ending-style]:opacity-0 data-[ending-style]:scale-95 z-50 max-h-[var(--available-height)] min-w-[8rem] origin-[var(--transform-origin)] overflow-visible rounded-xl p-1 shadow-[inset_0_1px_0_0_rgba(255,255,255,0.8),inset_0_0_0_1px_rgba(0,0,0,0.04),0_0_0_1px_rgba(0,0,0,0.10),0_1px_2px_-0.5px_rgba(0,0,0,0.08),0_4px_8px_-2px_rgba(0,0,0,0.08),0_12px_20px_-4px_rgba(0,0,0,0.08)] dark:shadow-[inset_0_1px_0_0_rgba(255,255,255,0.12),inset_0_0_0_1px_rgba(255,255,255,0.08),0_0_0_1px_rgba(0,0,0,0.36),0_1px_1px_-0.5px_rgba(0,0,0,0.22),0_3px_3px_-1.5px_rgba(0,0,0,0.20),0_6px_6px_-3px_rgba(0,0,0,0.16)]"; +export const dropdownMenuItemClass = "data-[highlighted]:bg-interactive-hover hover:bg-interactive-hover data-[variant=destructive]:text-destructive data-[variant=destructive]:hover:bg-destructive/10 dark:data-[variant=destructive]:hover:bg-destructive/20 data-[variant=destructive]:hover:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-pointer items-center gap-2 rounded-lg px-2 py-1 typography-ui-label outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5"; +export const dropdownMenuSubTriggerClass = "data-[highlighted]:bg-interactive-hover hover:bg-interactive-hover [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-pointer items-center gap-2 rounded-lg px-2 py-1 typography-ui-label outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5"; +export const dropdownMenuSeparatorClass = "bg-border -mx-1 my-0.5 h-px"; diff --git a/packages/ui/src/components/ui/dropdown-menu.tsx b/packages/ui/src/components/ui/dropdown-menu.tsx index 124ac39b..12d49e42 100644 --- a/packages/ui/src/components/ui/dropdown-menu.tsx +++ b/packages/ui/src/components/ui/dropdown-menu.tsx @@ -3,6 +3,7 @@ import { Menu as BaseMenu } from "@base-ui/react/menu" import { cn } from "@/lib/utils" import { Icon } from "@/components/icon/Icon"; +import { dropdownMenuItemClass, dropdownMenuPopupClass, dropdownMenuSeparatorClass, dropdownMenuSubTriggerClass } from "./dropdown-menu.styles"; type AsChildProps = { asChild?: boolean }; type AsChildRenderProps = { @@ -134,7 +135,7 @@ function DropdownMenuContent({ ...style, }} className={cn( - "app-region-no-drag transition-all duration-150 ease-out data-[starting-style]:opacity-0 data-[starting-style]:scale-95 data-[ending-style]:opacity-0 data-[ending-style]:scale-95 z-50 max-h-[var(--available-height)] min-w-[8rem] origin-[var(--transform-origin)] overflow-visible rounded-xl p-1 shadow-[inset_0_1px_0_0_rgba(255,255,255,0.8),inset_0_0_0_1px_rgba(0,0,0,0.04),0_0_0_1px_rgba(0,0,0,0.10),0_1px_2px_-0.5px_rgba(0,0,0,0.08),0_4px_8px_-2px_rgba(0,0,0,0.08),0_12px_20px_-4px_rgba(0,0,0,0.08)] dark:shadow-[inset_0_1px_0_0_rgba(255,255,255,0.12),inset_0_0_0_1px_rgba(255,255,255,0.08),0_0_0_1px_rgba(0,0,0,0.36),0_1px_1px_-0.5px_rgba(0,0,0,0.22),0_3px_3px_-1.5px_rgba(0,0,0,0.20),0_6px_6px_-3px_rgba(0,0,0,0.16)]", + dropdownMenuPopupClass, className )} {...props} @@ -177,7 +178,7 @@ function DropdownMenuItem({ data-inset={inset} data-variant={variant} className={cn( - "data-[highlighted]:bg-interactive-hover hover:bg-interactive-hover data-[variant=destructive]:text-destructive data-[variant=destructive]:hover:bg-destructive/10 dark:data-[variant=destructive]:hover:bg-destructive/20 data-[variant=destructive]:hover:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-pointer items-center gap-2 rounded-lg px-2 py-1 typography-ui-label outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5", + dropdownMenuItemClass, className )} {...props} @@ -270,7 +271,7 @@ function DropdownMenuSeparator({ return ( ) @@ -311,7 +312,7 @@ function DropdownMenuSubTrigger({ data-slot="dropdown-menu-sub-trigger" data-inset={inset} className={cn( - "data-[highlighted]:bg-interactive-hover hover:bg-interactive-hover [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-pointer items-center gap-2 rounded-lg px-2 py-1 typography-ui-label outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5", + dropdownMenuSubTriggerClass, className )} {...props} @@ -338,7 +339,7 @@ function DropdownMenuSubContent({ color: 'var(--surface-elevated-foreground)', }} className={cn( - "transition-all duration-150 ease-out data-[starting-style]:opacity-0 data-[starting-style]:scale-95 data-[ending-style]:opacity-0 data-[ending-style]:scale-95 z-50 min-w-[8rem] origin-[var(--transform-origin)] overflow-visible rounded-xl p-1 shadow-[inset_0_1px_0_0_rgba(255,255,255,0.8),inset_0_0_0_1px_rgba(0,0,0,0.04),0_0_0_1px_rgba(0,0,0,0.10),0_1px_2px_-0.5px_rgba(0,0,0,0.08),0_4px_8px_-2px_rgba(0,0,0,0.08),0_12px_20px_-4px_rgba(0,0,0,0.08)] dark:shadow-[inset_0_1px_0_0_rgba(255,255,255,0.12),inset_0_0_0_1px_rgba(255,255,255,0.08),0_0_0_1px_rgba(0,0,0,0.36),0_1px_1px_-0.5px_rgba(0,0,0,0.22),0_3px_3px_-1.5px_rgba(0,0,0,0.20),0_6px_6px_-3px_rgba(0,0,0,0.16)]", + dropdownMenuPopupClass, className )} {...props} diff --git a/packages/ui/src/components/views/FilesView.tsx b/packages/ui/src/components/views/FilesView.tsx index f8bf1385..422ed88c 100644 --- a/packages/ui/src/components/views/FilesView.tsx +++ b/packages/ui/src/components/views/FilesView.tsx @@ -10,7 +10,14 @@ import { DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; +} from '@/components/ui/dropdown-menu'; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuSeparator, + ContextMenuTrigger, +} from '@/components/ui/context-menu'; import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; @@ -373,8 +380,10 @@ interface FileRowProps { canReveal: boolean; }; downloadFile?: (path: string) => Promise; - contextMenuPath: string | null; - setContextMenuPath: (path: string | null) => void; + contextMenuPath: string | null; + setContextMenuPath: (path: string | null) => void; + rightClickMenuPath: string | null; + setRightClickMenuPath: (path: string | null) => void; onSelect: (node: FileNode) => void; onToggle: (path: string) => void; onRevealPath: (path: string) => void; @@ -392,8 +401,10 @@ const FileRow: React.FC = ({ badge, permissions, downloadFile, - contextMenuPath, - setContextMenuPath, + contextMenuPath, + setContextMenuPath, + rightClickMenuPath, + setRightClickMenuPath, onSelect, onToggle, onRevealPath, @@ -406,10 +417,10 @@ const FileRow: React.FC = ({ const handleContextMenu = React.useCallback((event?: React.MouseEvent) => { if (!canRename && !canCreateFile && !canCreateFolder && !canDelete && !canReveal) { return; - } - event?.preventDefault(); - setContextMenuPath(node.path); - }, [canRename, canCreateFile, canCreateFolder, canDelete, canReveal, node.path, setContextMenuPath]); + } + event?.preventDefault(); + setRightClickMenuPath(node.path); + }, [canRename, canCreateFile, canCreateFolder, canDelete, canReveal, node.path, setRightClickMenuPath]); const handleInteraction = React.useCallback(() => { if (isDir) { @@ -420,15 +431,94 @@ const FileRow: React.FC = ({ }, [isDir, node, onSelect, onToggle]); const handleMenuButtonClick = React.useCallback((event: React.MouseEvent) => { - event.stopPropagation(); - setContextMenuPath(node.path); - }, [node.path, setContextMenuPath]); + event.stopPropagation(); + setRightClickMenuPath(null); + setContextMenuPath(node.path); + }, [node.path, setContextMenuPath, setRightClickMenuPath]); + + const renderMenuItems = ({ + Item, + Separator, + }: { + Item: React.ElementType; + Separator: React.ElementType; + }) => ( + <> + {canRename && ( + { e.stopPropagation(); onOpenDialog('rename', node); }}> + {t('sidebarFilesTree.menu.rename')} + + )} + { + e.stopPropagation(); + void copyTextToClipboard(node.path).then((result) => { + if (result.ok) { + toast.success(t('sidebarFilesTree.toast.pathCopied')); + return; + } + toast.error(t('sidebarFilesTree.toast.copyFailed')); + }); + }}> + {t('sidebarFilesTree.menu.copyPath')} + + { + e.stopPropagation(); + const relativePath = getDisplayPath(root, node.path) || node.path; + void copyTextToClipboard(relativePath).then((result) => { + if (result.ok) { + toast.success(t('filesView.toast.relativePathCopied')); + return; + } + toast.error(t('sidebarFilesTree.toast.copyFailed')); + }); + }}> + {t('filesView.tree.menu.copyRelativePath')} + + {!isDir && downloadFile && ( + { + e.stopPropagation(); + void downloadFile(node.path); + }}> + {t('sidebarFilesTree.menu.save')} + + )} + {canReveal && ( + { e.stopPropagation(); onRevealPath(node.path); }}> + {t(getRevealLabelKey())} + + )} + {isDir && (canCreateFile || canCreateFolder) && ( + <> + + {canCreateFile && ( + { e.stopPropagation(); onOpenDialog('createFile', node); }}> + {t('sidebarFilesTree.menu.newFile')} + + )} + {canCreateFolder && ( + { e.stopPropagation(); onOpenDialog('createFolder', node); }}> + {t('sidebarFilesTree.menu.newFolder')} + + )} + + )} + {canDelete && ( + <> + + { e.stopPropagation(); onOpenDialog('delete', node); }} + className="text-destructive focus:text-destructive" + > + {t('sidebarFilesTree.menu.delete')} + + + )} + + ); return ( -
+ setRightClickMenuPath(open ? node.path : null)}> + }> - setContextMenuPath(null)}> - {canRename && ( - { e.stopPropagation(); onOpenDialog('rename', node); }}> - {t('sidebarFilesTree.menu.rename')} - - )} - { - e.stopPropagation(); - void copyTextToClipboard(node.path).then((result) => { - if (result.ok) { - toast.success(t('sidebarFilesTree.toast.pathCopied')); - return; - } - toast.error(t('sidebarFilesTree.toast.copyFailed')); - }); - }}> - {t('sidebarFilesTree.menu.copyPath')} - - { - e.stopPropagation(); - const relativePath = getDisplayPath(root, node.path) || node.path; - void copyTextToClipboard(relativePath).then((result) => { - if (result.ok) { - toast.success(t('filesView.toast.relativePathCopied')); - return; - } - toast.error(t('sidebarFilesTree.toast.copyFailed')); - }); - }}> - {t('filesView.tree.menu.copyRelativePath')} - - {!isDir && downloadFile && ( - { - e.stopPropagation(); - void downloadFile(node.path); - }}> - {t('sidebarFilesTree.menu.save')} - - )} - {canReveal && ( - { e.stopPropagation(); onRevealPath(node.path); }}> - {t(getRevealLabelKey())} - - )} - {isDir && (canCreateFile || canCreateFolder) && ( - <> - - {canCreateFile && ( - { e.stopPropagation(); onOpenDialog('createFile', node); }}> - {t('sidebarFilesTree.menu.newFile')} - - )} - {canCreateFolder && ( - { e.stopPropagation(); onOpenDialog('createFolder', node); }}> - {t('sidebarFilesTree.menu.newFolder')} - - )} - - )} - {canDelete && ( - <> - - { e.stopPropagation(); onOpenDialog('delete', node); }} - className="text-destructive focus:text-destructive" - > - {t('sidebarFilesTree.menu.delete')} - - - )} - + setContextMenuPath(null)}> + {renderMenuItems({ Item: DropdownMenuItem, Separator: DropdownMenuSeparator })} +
)} -
- ); -}; + + + {renderMenuItems({ Item: ContextMenuItem, Separator: ContextMenuSeparator })} + + + ); +}; interface DialogsProps { activeDialog: 'createFile' | 'createFolder' | 'rename' | 'delete' | null; @@ -840,7 +866,8 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { const [dialogData, setDialogData] = React.useState<{ path: string; name?: string; type?: 'file' | 'directory' } | null>(null); const [dialogInputValue, setDialogInputValue] = React.useState(''); const [isDialogSubmitting, setIsDialogSubmitting] = React.useState(false); - const [contextMenuPath, setContextMenuPath] = React.useState(null); + const [contextMenuPath, setContextMenuPath] = React.useState(null); + const [rightClickMenuPath, setRightClickMenuPath] = React.useState(null); const [copiedContent, setCopiedContent] = React.useState(false); const [copiedPath, setCopiedPath] = React.useState(false); const [isGoToLineOpen, setIsGoToLineOpen] = React.useState(false); @@ -2122,8 +2149,10 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { badge={isDir ? getFolderBadge(node.path) : undefined} permissions={fileRowPermissions} downloadFile={files.downloadFile} - contextMenuPath={contextMenuPath} - setContextMenuPath={setContextMenuPath} + contextMenuPath={contextMenuPath} + setContextMenuPath={setContextMenuPath} + rightClickMenuPath={rightClickMenuPath} + setRightClickMenuPath={setRightClickMenuPath} onSelect={handleSelectFile} onToggle={toggleDirectory} onRevealPath={handleRevealPath} diff --git a/packages/ui/src/components/views/agent-manager/AgentManagerSidebar.tsx b/packages/ui/src/components/views/agent-manager/AgentManagerSidebar.tsx index 5ed17d8b..def5b964 100644 --- a/packages/ui/src/components/views/agent-manager/AgentManagerSidebar.tsx +++ b/packages/ui/src/components/views/agent-manager/AgentManagerSidebar.tsx @@ -17,6 +17,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; +import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@/components/ui/context-menu'; import { Icon } from "@/components/icon/Icon"; import { cn } from '@/lib/utils'; import { useAgentGroupsStore, type AgentGroup } from '@/stores/useAgentGroupsStore'; @@ -47,6 +48,7 @@ interface AgentGroupItemProps { const AgentGroupItem: React.FC = ({ group, isSelected, isBusy, onSelect }) => { const { t } = useI18n(); const [menuOpen, setMenuOpen] = React.useState(false); + const [contextMenuOpen, setContextMenuOpen] = React.useState(false); const [confirmOpen, setConfirmOpen] = React.useState(false); const [isDeleting, setIsDeleting] = React.useState(false); const deleteGroupSessions = useAgentGroupsStore((s) => s.deleteGroupSessions); @@ -66,16 +68,38 @@ const AgentGroupItem: React.FC = ({ group, isSelected, isBu }, [deleteGroupSessions, group.name, group.sessions, isDeleting, t]); const relativeTime = formatRelativeTime(group.lastActive); + const renderGroupMenuItems = (Item: React.ElementType) => ( + { + e.stopPropagation(); + setMenuOpen(false); + setContextMenuOpen(false); + setConfirmOpen(true); + }} + > + {t('agentManager.sidebar.item.delete')} + + ); return ( <> -
+ + { + event.preventDefault(); + setContextMenuOpen(true); + }} + /> + } + >
- + { if (open) setContextMenuOpen(false); setMenuOpen(open); }}> - { - e.stopPropagation(); - setMenuOpen(false); - setConfirmOpen(true); - }} - > - {t('agentManager.sidebar.item.delete')} - + {renderGroupMenuItems(DropdownMenuItem)}
-
+ + + {renderGroupMenuItems(ContextMenuItem)} + +