diff --git a/src/renderer/features/agents/components/edit-queued-message-dialog.tsx b/src/renderer/features/agents/components/edit-queued-message-dialog.tsx new file mode 100644 index 00000000..26f1df34 --- /dev/null +++ b/src/renderer/features/agents/components/edit-queued-message-dialog.tsx @@ -0,0 +1,217 @@ +"use client" + +import { AnimatePresence, motion } from "motion/react" +import { useEffect, useState, useRef, useCallback } from "react" +import { createPortal } from "react-dom" +import { Button } from "../../../components/ui/button" +import { Textarea } from "../../../components/ui/textarea" +import type { AgentQueueItem } from "../lib/queue-utils" +import { useMessageQueueStore } from "../stores/message-queue-store" + +interface EditQueuedMessageDialogProps { + isOpen: boolean + onClose: () => void + item: AgentQueueItem | null + subChatId: string + isFirstInQueue?: boolean +} + +const EASING_CURVE = [0.55, 0.055, 0.675, 0.19] as const +const INTERACTION_DELAY_MS = 250 + +export function EditQueuedMessageDialog({ + isOpen, + onClose, + item, + subChatId, + isFirstInQueue = false, +}: EditQueuedMessageDialogProps) { + const [mounted, setMounted] = useState(false) + const [message, setMessage] = useState("") + const openAtRef = useRef(0) + const textareaRef = useRef(null) + + const updateQueueItem = useMessageQueueStore((s) => s.updateQueueItem) + const setEditingItem = useMessageQueueStore((s) => s.setEditingItem) + + useEffect(() => { + setMounted(true) + }, []) + + useEffect(() => { + if (isOpen && item) { + openAtRef.current = performance.now() + setMessage(item.message) + // If editing the first item in queue, pause processing + if (isFirstInQueue) { + setEditingItem(item.id, true) + } + } + }, [isOpen, item, isFirstInQueue, setEditingItem]) + + const handleAnimationComplete = () => { + if (isOpen) { + textareaRef.current?.focus() + // Select all text + textareaRef.current?.select() + } + } + + const handleClose = useCallback(() => { + const canInteract = performance.now() - openAtRef.current > INTERACTION_DELAY_MS + if (!canInteract) return + // Clear editing state when closing + if (item && isFirstInQueue) { + setEditingItem(item.id, false) + } + onClose() + }, [item, isFirstInQueue, setEditingItem, onClose]) + + const handleSave = useCallback(() => { + const trimmedMessage = message.trim() + if (!trimmedMessage || !item) { + handleClose() + return + } + + // Only update if changed + if (trimmedMessage !== item.message) { + updateQueueItem(subChatId, item.id, { message: trimmedMessage }) + } + + // Clear editing state + if (isFirstInQueue) { + setEditingItem(item.id, false) + } + onClose() + }, [message, item, subChatId, updateQueueItem, isFirstInQueue, setEditingItem, onClose, handleClose]) + + useEffect(() => { + if (!isOpen) return + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") { + event.preventDefault() + handleClose() + } + // Cmd/Ctrl + Enter to save + if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) { + event.preventDefault() + handleSave() + } + } + + document.addEventListener("keydown", handleKeyDown) + return () => document.removeEventListener("keydown", handleKeyDown) + }, [isOpen, handleClose, handleSave]) + + if (!mounted) return null + + const portalTarget = typeof document !== "undefined" ? document.body : null + if (!portalTarget) return null + + const hasAttachments = + (item?.images && item.images.length > 0) || + (item?.files && item.files.length > 0) || + (item?.textContexts && item.textContexts.length > 0) || + (item?.diffTextContexts && item.diffTextContexts.length > 0) + + const attachmentCount = + (item?.images?.length || 0) + + (item?.files?.length || 0) + + (item?.textContexts?.length || 0) + + (item?.diffTextContexts?.length || 0) + + return createPortal( + + {isOpen && item && ( + <> + {/* Overlay */} + + + {/* Main Dialog */} +
+ e.stopPropagation()} + > +
+
+

+ Edit queued message +

+ {isFirstInQueue && ( +

+ Queue processing is paused while editing +

+ )} + {!isFirstInQueue && ( +

+ Edit the message before it's sent +

+ )} + + {/* Textarea for message */} +