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 (
- <>
-
+
+
-
+
+
+ {({ hovered, open }) => (
+
+ )}
+
+
{tooltipContent}
-
-
+
+
This action cannot be undone
-
- {items.map((item) => (
-
- ))}
-
-
- >
+
+ {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,
},
}));