import React, { memo } from 'react'; import { DndContext, MouseSensor, TouchSensor, useSensor, useSensors, closestCenter, type DragEndEvent, } from '@dnd-kit/core'; import { SortableContext, useSortable, verticalListSortingStrategy, } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { createMessageQueueTarget, getMessageQueueKey, useMessageQueueStore, type MessageQueueTarget, type QueuedMessage } from '@/stores/messageQueueStore'; import { useSessionUIStore } from '@/sync/session-ui-store'; import { useInputStore } from '@/sync/input-store'; import { useI18n } from '@/lib/i18n'; import { Icon } from "@/components/icon/Icon"; import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; interface QueuedMessageChipProps { message: QueuedMessage; target: MessageQueueTarget; onEdit: (message: QueuedMessage) => void; onSend: (message: QueuedMessage) => void; } const QueuedMessageChip = memo(({ message, target, onEdit, onSend }: QueuedMessageChipProps) => { const { t } = useI18n(); const removeFromQueue = useMessageQueueStore((state) => state.removeFromQueue); const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: message.id }); // Get first line of message, truncated const firstLine = React.useMemo(() => { const lines = message.content.split('\n'); const first = lines[0] || ''; const maxLength = 100; if (first.length > maxLength) { return first.substring(0, maxLength) + '...'; } return first + (lines.length > 1 ? '...' : ''); }, [message.content]); const attachmentCount = message.attachments?.length ?? 0; return (
{firstLine || t('chat.queuedMessage.empty')} {attachmentCount > 0 && ( {t('chat.queuedMessage.attachments', { count: attachmentCount })} )}
); }); QueuedMessageChip.displayName = 'QueuedMessageChip'; interface QueuedMessageChipsProps { onEditMessage: (content: string, attachments?: QueuedMessage['attachments']) => void; onSendMessage: (messageId: string) => void; } const EMPTY_QUEUE: QueuedMessage[] = []; export const QueuedMessageChips = memo(({ onEditMessage, onSendMessage }: QueuedMessageChipsProps) => { const { t } = useI18n(); const currentSessionId = useSessionUIStore((state) => state.currentSessionId); // Must use the same resolution the composer used to build the queue key — // reading currentSessionDirectory raw can key the chips to a different // directory than the one the messages were queued under. const currentSessionDirectory = useSessionUIStore( React.useCallback( (state) => (currentSessionId ? state.getDirectoryForSession(currentSessionId) : null), [currentSessionId], ), ); const target = currentSessionId ? createMessageQueueTarget(currentSessionId, currentSessionDirectory) : null; const queueKey = target ? getMessageQueueKey(target) : null; const queuedMessages = useMessageQueueStore( React.useCallback( (state) => { if (!queueKey) return EMPTY_QUEUE; return state.queuedMessages[queueKey] ?? EMPTY_QUEUE; }, [queueKey] ) ); const popToInput = useMessageQueueStore((state) => state.popToInput); const reorderQueue = useMessageQueueStore((state) => state.reorderQueue); const sensors = useSensors( // Desktop: drag after a small move so other clicks still register. useSensor(MouseSensor, { activationConstraint: { distance: 8 } }), // Touch: long-press to drag (tap still hits buttons, swipe scrolls). useSensor(TouchSensor, { activationConstraint: { delay: 200, tolerance: 6 } }), ); const handleDragEnd = React.useCallback((event: DragEndEvent) => { const { active, over } = event; if (!over || active.id === over.id || !target) return; reorderQueue(target, String(active.id), String(over.id)); }, [target, reorderQueue]); const handleEdit = React.useCallback((message: QueuedMessage) => { if (!target) return; const popped = popToInput(target, message.id); if (popped) { if (popped.attachments && popped.attachments.length > 0) { const currentAttachments = useInputStore.getState().attachedFiles; useInputStore.getState().setAttachedFiles([...currentAttachments, ...popped.attachments]); } onEditMessage(popped.content, popped.attachments); } }, [target, popToInput, onEditMessage]); const handleSend = React.useCallback((message: QueuedMessage) => { onSendMessage(message.id); }, [onSendMessage]); if (queuedMessages.length === 0 || !target) { return null; } return (
{t('chat.queuedMessage.title')} {queuedMessages.length}
m.id)} strategy={verticalListSortingStrategy} >
{queuedMessages.map((message) => ( ))}
); }); QueuedMessageChips.displayName = 'QueuedMessageChips';