Skip to content

Add sidebar hover menu for recent creations.#192

Open
adoodevv wants to merge 1 commit into
Adam-CAD:masterfrom
adoodevv:feat/sidebar-creation-actions
Open

Add sidebar hover menu for recent creations.#192
adoodevv wants to merge 1 commit into
Adam-CAD:masterfrom
adoodevv:feat/sidebar-creation-actions

Conversation

@adoodevv

@adoodevv adoodevv commented Jun 12, 2026

Copy link
Copy Markdown

Summary

  • Add hover dropdown on sidebar recent creations (Open, Rename, Delete)
  • Show empty state when there are no creations; hide "View all creations" when empty
  • Extract shared useDeleteConversation / useRenameConversation hooks used by Sidebar and History

Test plan

  • Hover a recent creation → ⋮ menu appears with Open / Rename / Delete
  • Rename from sidebar → title updates in sidebar and on History page
  • Delete from sidebar → item removed; deleting active editor redirects to /
  • Empty account → shows "No creations yet", no "View all creations" link
  • With creations → "View all creations" links to /history
  • Mobile sidebar sheet → same behavior works

Summary 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

    • "…" hover menu per creation with Open, Rename, Delete
    • Rename via dialog; updates both Sidebar and History
    • Delete removes item; deleting the active editor redirects to /
    • Empty state: shows “No creations yet”; hides “View all creations”
    • Mobile: navigating from the sidebar closes the sheet
  • Refactors

    • Extracted useDeleteConversation and useRenameConversation to conversationService with optimistic cache updates and image cleanup
    • HistoryView now uses the shared hooks

Written for commit fe7535f. Summary will update on new commits.

Review in cubic

Lets users open, rename, or delete creations from the sidebar with shared
mutations that keep History and recent lists in sync.
@vercel

vercel Bot commented Jun 12, 2026

Copy link
Copy Markdown

@adoodevv is attempting to deploy a commit to the Adam Team on Vercel.

A member of the Team first needs to authorize it.

@greptile-apps

greptile-apps Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds a per-item hover menu (Open / Rename / Delete) to the sidebar's recent-creations list and extracts the delete/rename logic into shared useDeleteConversation / useRenameConversation hooks consumed by both Sidebar and HistoryView.

  • SidebarConversationItem renders the ⋮ dropdown and confirmation dialog; the Sidebar gains empty-state copy, a "View all creations" footer link, and active-editor redirect on delete.
  • conversationService hosts the two new hooks with optimistic cache updates across both the ['conversations'] list and the ['conversations', 'recent'] sidebar query.
  • HistoryView drops its inline mutation definitions and delegates to the shared hooks — clean reduction with no logic regressions.

Confidence Score: 3/5

Two 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

Filename Overview
src/components/sidebar/SidebarConversationItem.tsx New component — the AlertDialog/DropdownMenu nesting pattern causes the dropdown to remain open after the delete confirmation dialog is dismissed, producing a visible UX defect.
src/services/conversationService.ts Extracted delete/rename hooks with optimistic updates — missing cancelQueries for individual conversation/messages caches before optimistic removal, leaving a race-condition window where stale data can repopulate the cache.
src/components/Sidebar.tsx Sidebar refactored to use new hooks and SidebarConversationItem; empty-state and "View all creations" logic is correct, active-editor redirect on delete is properly wired.
src/views/HistoryView.tsx Inline delete/rename mutations cleanly replaced with the shared hooks; no logic regressions introduced.

Reviews (1): Last reviewed commit: "Add sidebar hover menu for recent creati..." | Re-trigger Greptile

Comment on lines +49 to +122
<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 &ldquo;
{conversation.title || 'Untitled creation'}&rdquo;? 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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Comment on lines +86 to +97
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 };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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).

Comment on lines +86 to +90
onMutate: async (conversationId) => {
await queryClient.cancelQueries({ queryKey: ['conversations'] });
await queryClient.cancelQueries({
queryKey: ['conversations', 'recent'],
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Suggested change
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!

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant