Add sidebar hover menu for recent creations.#192
Conversation
Lets users open, rename, or delete creations from the sidebar with shared mutations that keep History and recent lists in sync.
|
@adoodevv is attempting to deploy a commit to the Adam Team on Vercel. A member of the Team first needs to authorize it. |
Greptile SummaryAdds a per-item hover menu (Open / Rename / Delete) to the sidebar's recent-creations list and extracts the delete/rename logic into shared
Confidence Score: 3/5Two real defects need fixing before this lands: the dropdown stays open after the delete confirmation resolves, and an in-flight refetch can silently repopulate the cache for a resource that was just deleted. The AlertDialog-inside-DropdownMenu pattern with onSelect.preventDefault() prevents the dropdown from closing before the dialog opens, so after the user confirms or cancels deletion the dropdown reappears and must be manually dismissed — every time. Separately, onMutate in useDeleteConversation clears the individual conversation and messages from the cache without first canceling in-flight requests for those keys; if the editor is open and a refetch is in flight, it completes after the removal and quietly restores stale data for a deleted resource. SidebarConversationItem.tsx (dialog/dropdown interaction) and conversationService.ts (optimistic update completeness) Important Files Changed
Reviews (1): Last reviewed commit: "Add sidebar hover menu for recent creati..." | Re-trigger Greptile |
| <AlertDialog> | ||
| <DropdownMenu> | ||
| <DropdownMenuTrigger asChild> | ||
| <Button | ||
| type="button" | ||
| variant="ghost" | ||
| size="icon" | ||
| className="h-6 w-6 p-0 text-adam-neutral-400 hover:bg-adam-neutral-950 hover:text-adam-neutral-100" | ||
| onClick={(event) => event.preventDefault()} | ||
| > | ||
| <MoreVertical className="h-3.5 w-3.5" /> | ||
| <span className="sr-only">Creation options</span> | ||
| </Button> | ||
| </DropdownMenuTrigger> | ||
| <DropdownMenuContent | ||
| align="end" | ||
| className="min-w-[10rem] border-adam-neutral-700 bg-[#191A1A]" | ||
| > | ||
| <DropdownMenuItem | ||
| asChild | ||
| className="text-adam-neutral-50 hover:cursor-pointer hover:bg-adam-neutral-950 focus:bg-adam-neutral-950" | ||
| > | ||
| <Link | ||
| to="/editor/$id" | ||
| params={{ id: conversation.id }} | ||
| onClick={onNavigate} | ||
| > | ||
| <ExternalLink className="mr-2 h-3.5 w-3.5" /> | ||
| Open | ||
| </Link> | ||
| </DropdownMenuItem> | ||
| <DropdownMenuItem | ||
| className="text-adam-neutral-50 hover:cursor-pointer hover:bg-adam-neutral-950 focus:bg-adam-neutral-950" | ||
| onClick={(event) => { | ||
| event.preventDefault(); | ||
| onRename(conversation.id, conversation.title); | ||
| }} | ||
| > | ||
| <Pencil className="mr-2 h-3.5 w-3.5" /> | ||
| Rename | ||
| </DropdownMenuItem> | ||
| <AlertDialogTrigger asChild> | ||
| <DropdownMenuItem | ||
| className="text-adam-neutral-50 hover:cursor-pointer hover:bg-adam-neutral-950 hover:text-red-500 focus:bg-adam-neutral-950 focus:text-red-500" | ||
| onSelect={(event) => event.preventDefault()} | ||
| > | ||
| <Trash2 className="mr-2 h-3.5 w-3.5" /> | ||
| Delete | ||
| </DropdownMenuItem> | ||
| </AlertDialogTrigger> | ||
| </DropdownMenuContent> | ||
| </DropdownMenu> | ||
| <AlertDialogContent className="border-[2px] border-adam-neutral-700 bg-adam-background-1"> | ||
| <AlertDialogHeader> | ||
| <AlertDialogTitle className="text-adam-neutral-100"> | ||
| Delete creation | ||
| </AlertDialogTitle> | ||
| <AlertDialogDescription> | ||
| Are you sure you want to delete “ | ||
| {conversation.title || 'Untitled creation'}”? This action | ||
| cannot be undone. | ||
| </AlertDialogDescription> | ||
| </AlertDialogHeader> | ||
| <AlertDialogFooter> | ||
| <AlertDialogCancel>Cancel</AlertDialogCancel> | ||
| <AlertDialogAction | ||
| className="bg-red-600 hover:bg-red-700 dark:bg-red-900 dark:hover:bg-red-800" | ||
| onClick={() => onDelete(conversation.id)} | ||
| > | ||
| Delete | ||
| </AlertDialogAction> | ||
| </AlertDialogFooter> | ||
| </AlertDialogContent> | ||
| </AlertDialog> |
There was a problem hiding this comment.
DropdownMenu stays open after AlertDialog dismissal
onSelect={(event) => event.preventDefault()} on the Delete item intentionally suppresses the dropdown's auto-close behaviour when the item is selected. This lets the AlertDialogTrigger fire, but it also means the DropdownMenu remains open while the AlertDialog is shown. Once the user confirms or cancels the dialog, focus returns to the DropdownMenuItem that is still live inside the open dropdown — the dropdown reappears visually and the user must click away to dismiss it.
The standard fix for Radix UI is to manage state manually: hold open state on the dropdown and an open state for the confirm dialog, then close the dropdown first (setDropdownOpen(false)) in the Delete onClick, and open the dialog (setDeleteDialogOpen(true)) separately — no AlertDialogTrigger inside the menu at all.
| onMutate: async (conversationId) => { | ||
| await queryClient.cancelQueries({ queryKey: ['conversations'] }); | ||
| await queryClient.cancelQueries({ | ||
| queryKey: ['conversations', 'recent'], | ||
| }); | ||
| const previousConversations = queryClient.getQueryData(['conversations']); | ||
| const previousRecent = queryClient.getQueryData([ | ||
| 'conversations', | ||
| 'recent', | ||
| ]); | ||
| patchConversationListCaches(queryClient, conversationId, 'remove'); | ||
| return { previousConversations, previousRecent }; |
There was a problem hiding this comment.
Missing
cancelQueries for individual conversation/messages caches before optimistic removal
onMutate cancels ['conversations'] and ['conversations', 'recent'], but patchConversationListCaches then calls removeQueries on ['conversation', conversationId] and ['messages', conversationId] without first canceling those queries. If either key has an in-flight request at the moment of deletion (e.g., the editor is open and refetching), the response will arrive after removeQueries and silently repopulate the cache with stale data for a deleted resource. The fix is to add two more await queryClient.cancelQueries calls for those keys before invoking patchConversationListCaches. The same gap exists in useRenameConversation.onMutate for ['conversation', conversationId] (line 146–163).
| onMutate: async (conversationId) => { | ||
| await queryClient.cancelQueries({ queryKey: ['conversations'] }); | ||
| await queryClient.cancelQueries({ | ||
| queryKey: ['conversations', 'recent'], | ||
| }); |
There was a problem hiding this comment.
The first
cancelQueries({ queryKey: ['conversations'] }) already matches ['conversations', 'recent'] via prefix matching (default exact: false), so the second cancel call is redundant. Same pattern repeats in useRenameConversation.onMutate.
| onMutate: async (conversationId) => { | |
| await queryClient.cancelQueries({ queryKey: ['conversations'] }); | |
| await queryClient.cancelQueries({ | |
| queryKey: ['conversations', 'recent'], | |
| }); | |
| onMutate: async (conversationId) => { | |
| await queryClient.cancelQueries({ queryKey: ['conversations'] }); |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
1 issue found across 4 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/components/sidebar/SidebarConversationItem.tsx">
<violation number="1" location="src/components/sidebar/SidebarConversationItem.tsx:48">
P1: Hover-gated visibility makes sidebar item actions inaccessible on touch devices. The menu button uses `[@media(hover:hover)]` for both `group-hover` and `focus-within`, which never matches on touch-only devices, leaving the controls permanently invisible while still occupying layout space. This breaks mobile sidebar parity claimed in the PR test plan.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| {conversation.title || 'Untitled creation'} | ||
| </span> | ||
| </Link> | ||
| <div className="absolute right-0 top-1/2 -translate-y-1/2 opacity-0 transition-opacity duration-150 [@media(hover:hover)]:focus-within:opacity-100 [@media(hover:hover)]:group-hover:opacity-100"> |
There was a problem hiding this comment.
P1: Hover-gated visibility makes sidebar item actions inaccessible on touch devices. The menu button uses [@media(hover:hover)] for both group-hover and focus-within, which never matches on touch-only devices, leaving the controls permanently invisible while still occupying layout space. This breaks mobile sidebar parity claimed in the PR test plan.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/components/sidebar/SidebarConversationItem.tsx, line 48:
<comment>Hover-gated visibility makes sidebar item actions inaccessible on touch devices. The menu button uses `[@media(hover:hover)]` for both `group-hover` and `focus-within`, which never matches on touch-only devices, leaving the controls permanently invisible while still occupying layout space. This breaks mobile sidebar parity claimed in the PR test plan.</comment>
<file context>
@@ -0,0 +1,126 @@
+ {conversation.title || 'Untitled creation'}
+ </span>
+ </Link>
+ <div className="absolute right-0 top-1/2 -translate-y-1/2 opacity-0 transition-opacity duration-150 [@media(hover:hover)]:focus-within:opacity-100 [@media(hover:hover)]:group-hover:opacity-100">
+ <AlertDialog>
+ <DropdownMenu>
</file context>
Summary
useDeleteConversation/useRenameConversationhooks used by Sidebar and HistoryTest plan
//historySummary by cubic
Add a hover actions menu to recent creations in the sidebar so you can Open, Rename, or Delete without leaving the page. Shared mutations keep the sidebar and History in sync, with proper redirects and empty states.
New Features
/Refactors
useDeleteConversationanduseRenameConversationtoconversationServicewith optimistic cache updates and image cleanupHistoryViewnow uses the shared hooksWritten for commit fe7535f. Summary will update on new commits.