From e5658654c041bc0fb722a43b5bcb17125db4d65c Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Mon, 25 May 2026 20:18:57 +0700 Subject: [PATCH] Open rewind in an anchored dropdown menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the full-screen rewind sheet with a dropdown anchored to the trigger, with a muted warning header and inline pending state on the chosen action. Also realign the user message action row so timestamp, rewind, and copy share a single 24px baseline — the copy button was inheriting assistant-footer paddings that pushed the row taller than the icons. --- .../app/e2e/rewind-menu.ui-contract.spec.ts | 2 - packages/app/src/components/message.tsx | 8 +- .../app/src/components/rewind/rewind-menu.tsx | 173 +++++++++--------- 3 files changed, 94 insertions(+), 89 deletions(-) diff --git a/packages/app/e2e/rewind-menu.ui-contract.spec.ts b/packages/app/e2e/rewind-menu.ui-contract.spec.ts index e6c74bcf8..3f0ebfb64 100644 --- a/packages/app/e2e/rewind-menu.ui-contract.spec.ts +++ b/packages/app/e2e/rewind-menu.ui-contract.spec.ts @@ -68,7 +68,6 @@ test.describe("Rewind sheet", () => { await page.getByTestId("rewind-menu-trigger").first().click(); const rewindSheet = page.getByTestId("rewind-menu-content"); await expect(rewindSheet).toBeVisible(); - await expect(rewindSheet.getByText("Rewind to this message", { exact: true })).toBeVisible(); await expect( rewindSheet.getByText("This action cannot be undone", { exact: true }), ).toBeVisible(); @@ -149,7 +148,6 @@ test.describe("Rewind sheet", () => { await page.getByTestId("rewind-menu-trigger").first().click(); const rewindSheet = page.getByTestId("rewind-menu-content"); await expect(rewindSheet).toBeVisible(); - await expect(rewindSheet.getByText("Rewind to this message", { exact: true })).toBeVisible(); await expect( rewindSheet.getByText("This action cannot be undone", { exact: true }), ).toBeVisible(); diff --git a/packages/app/src/components/message.tsx b/packages/app/src/components/message.tsx index 344992c0f..fd1300740 100644 --- a/packages/app/src/components/message.tsx +++ b/packages/app/src/components/message.tsx @@ -409,8 +409,14 @@ const userMessageStylesheet = StyleSheet.create((theme) => ({ fontSize: theme.fontSize.sm, }, copyButton: { - padding: theme.spacing[1], + alignSelf: "center", + width: 24, + height: 24, + padding: 0, + margin: 0, marginRight: -theme.spacing[1], + alignItems: "center", + justifyContent: "center", }, trailingRow: { alignSelf: "flex-end", diff --git a/packages/app/src/components/rewind/rewind-menu.tsx b/packages/app/src/components/rewind/rewind-menu.tsx index b28686271..05db2900d 100644 --- a/packages/app/src/components/rewind/rewind-menu.tsx +++ b/packages/app/src/components/rewind/rewind-menu.tsx @@ -1,15 +1,16 @@ -import { memo, useCallback, useMemo, useState, type ComponentType } from "react"; +import { memo, useCallback, useMemo, useState, type ReactElement } from "react"; import { Text, View } from "react-native"; import { FileText, Layers, MessageSquare, Undo2 } from "lucide-react-native"; -import { StyleSheet } from "react-native-unistyles"; -import { AdaptiveModalSheet, type SheetHeader } from "@/components/adaptive-modal-sheet"; -import { Button } from "@/components/ui/button"; -import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { - type RewindMenuItem, - type RewindMode, - useRewindCapabilities, -} from "./use-rewind-capabilities"; + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import { type RewindMode, useRewindCapabilities } from "./use-rewind-capabilities"; import type { AgentCapabilityFlags } from "@server/server/agent/agent-sdk-types"; export type { RewindMode }; @@ -22,17 +23,14 @@ interface RewindMenuProps { testID?: string; } -const REWIND_HEADER: SheetHeader = { title: "Rewind to this message" }; -const SNAP_POINTS: string[] = ["42%", "72%"]; - -function iconForItem(item: RewindMenuItem): ComponentType<{ color: string; size: number }> { - switch (item.mode) { +function getIcon(mode: RewindMode, color: string): ReactElement { + switch (mode) { case "conversation": - return MessageSquare; + return ; case "files": - return FileText; + return ; case "both": - return Layers; + return ; } } @@ -40,36 +38,44 @@ export const RewindMenu = memo(function RewindMenu({ capabilities, rewoundText, onRewind, - isPending = false, + isPending: isPendingProp = false, testID = "rewind-menu", }: RewindMenuProps) { + const { theme } = useUnistyles(); const items = useRewindCapabilities(capabilities); - const [isSheetVisible, setIsSheetVisible] = useState(false); + const [isOpen, setIsOpen] = useState(false); const [pendingMode, setPendingMode] = useState(null); - const handleOpen = useCallback(() => setIsSheetVisible(true), []); - const handleClose = useCallback(() => { - if (!isPending) { - setIsSheetVisible(false); - } - }, [isPending]); + const isLocked = isPendingProp || pendingMode !== null; + + const handleOpenChange = useCallback( + (next: boolean) => { + if (!next && pendingMode !== null) return; + setIsOpen(next); + }, + [pendingMode], + ); + const handleSelect = useCallback( (mode: RewindMode) => async () => { - if (isPending) { - return; - } + if (isLocked) return; setPendingMode(mode); try { await onRewind({ mode, rewoundText }); + setIsOpen(false); } catch { - // useRewindAgentMutation owns the toast; the sheet only owns flow state. + // useRewindAgentMutation owns the toast; the menu only owns flow state. } finally { setPendingMode(null); - setIsSheetVisible(false); } }, - [isPending, onRewind, rewoundText], + [isLocked, onRewind, rewoundText], ); - const triggerIcon = useCallback((color: string) => , []); + + const triggerStyle = useCallback( + () => [styles.trigger, isLocked ? styles.triggerDisabled : null], + [isLocked], + ); + const tooltipContent = useMemo( () => ( @@ -84,53 +90,48 @@ export const RewindMenu = memo(function RewindMenu({ } return ( - <> - + + - - ))} - - - + + {items.map((item) => ( + + {item.label} + + ))} + + ); }); @@ -138,27 +139,27 @@ const styles = StyleSheet.create((theme) => ({ trigger: { width: 24, height: 24, - minHeight: 24, - paddingHorizontal: 0, - paddingVertical: 0, alignItems: "center", justifyContent: "center", + backgroundColor: "transparent", + }, + triggerDisabled: { + opacity: theme.opacity[50], + }, + triggerSlot: { + alignSelf: "flex-start", }, tooltipText: { color: theme.colors.foreground, fontSize: theme.fontSize.xs, }, - content: { - gap: theme.spacing[4], + warningHeader: { + paddingHorizontal: theme.spacing[3], + paddingTop: theme.spacing[2], + paddingBottom: theme.spacing[2], }, warningText: { - color: theme.colors.statusWarning, - fontSize: theme.fontSize.sm, - }, - actions: { - gap: theme.spacing[2], - }, - actionButton: { - justifyContent: "flex-start", + color: theme.colors.foregroundMuted, + fontSize: theme.fontSize.xs, }, }));