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';