From 9fdca1bc2b6337ddd2c103c407ebd4d910fba46a Mon Sep 17 00:00:00 2001 From: User Date: Thu, 29 Jan 2026 02:09:39 +0000 Subject: [PATCH] feat: add ability to edit queued messages with pause for top item Implements ENG-583: Users can now edit queued messages before they are sent. When editing the first item in the queue, processing is automatically paused to prevent the message from being sent while being edited. Changes: - Add updateQueueItem, setEditingItem, isItemEditing to message-queue-store - Add EditQueuedMessageDialog component for editing message content - Update QueueProcessor to pause when editing first queue item - Add edit button (pencil icon) to queue item rows in AgentQueueIndicator - Pass subChatId to AgentQueueIndicator for edit dialog context Co-Authored-By: Claude Opus 4.5 --- .../components/edit-queued-message-dialog.tsx | 217 ++++++++++++++++++ .../agents/components/queue-processor.tsx | 20 ++ .../features/agents/main/active-chat.tsx | 1 + .../agents/stores/message-queue-store.ts | 41 ++++ .../agents/ui/agent-queue-indicator.tsx | 55 ++++- 5 files changed, 332 insertions(+), 2 deletions(-) create mode 100644 src/renderer/features/agents/components/edit-queued-message-dialog.tsx 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 */} +