feat: refresh queued message controls
Queued messages now match the reverted message dock style Added per-message edit and send actions Sending one queued message no longer sends the full queue
This commit is contained in:
@@ -1332,6 +1332,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
);
|
||||
const addToQueue = useMessageQueueStore((state) => state.addToQueue);
|
||||
const clearQueue = useMessageQueueStore((state) => state.clearQueue);
|
||||
const removeFromQueue = useMessageQueueStore((state) => state.removeFromQueue);
|
||||
|
||||
// Inline comment drafts
|
||||
const draftCount = useInlineCommentDraftStore(
|
||||
@@ -1579,6 +1580,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
// Keep a ref to handleSubmit so callbacks don't depend on it.
|
||||
type SubmitOptions = {
|
||||
queuedOnly?: boolean;
|
||||
queuedMessageId?: string;
|
||||
};
|
||||
const handleSubmitRef = React.useRef<(options?: SubmitOptions) => Promise<void>>(async () => {});
|
||||
|
||||
@@ -1627,6 +1629,10 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
}, 0);
|
||||
}, []);
|
||||
|
||||
const handleQueuedMessageSend = React.useCallback((messageId: string) => {
|
||||
void handleSubmitRef.current({ queuedOnly: true, queuedMessageId: messageId });
|
||||
}, []);
|
||||
|
||||
const handleOpenAgentPanel = React.useCallback(() => {
|
||||
setMobileControlsPanel('agent');
|
||||
}, []);
|
||||
@@ -1645,15 +1651,25 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
|
||||
const handleSubmit = async (options?: SubmitOptions) => {
|
||||
const queuedOnly = options?.queuedOnly ?? false;
|
||||
const queuedMessageId = options?.queuedMessageId;
|
||||
const inputSnapshot = getCurrentInputSnapshot();
|
||||
const queuedMessagesToSend = queuedMessageId
|
||||
? queuedMessages.filter((message) => message.id === queuedMessageId)
|
||||
: queuedMessages;
|
||||
|
||||
if (queuedOnly) {
|
||||
if (!hasQueuedMessages || !currentSessionId) return;
|
||||
if (queuedMessagesToSend.length === 0 || !currentSessionId) return;
|
||||
} else if ((!inputSnapshot.hasContent && !hasQueuedMessages) || (!currentSessionId && !newSessionDraftOpen)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentProviderId || !currentModelId) {
|
||||
const capturedSendConfig = queuedOnly ? queuedMessagesToSend[0]?.sendConfig : undefined;
|
||||
const providerIdToSend = capturedSendConfig?.providerID ?? currentProviderId;
|
||||
const modelIdToSend = capturedSendConfig?.modelID ?? currentModelId;
|
||||
const agentNameToSend = capturedSendConfig?.agent ?? currentAgentName;
|
||||
const variantToSend = capturedSendConfig?.variant ?? currentVariant;
|
||||
|
||||
if (!providerIdToSend || !modelIdToSend) {
|
||||
console.warn('Cannot send message: provider or model not selected');
|
||||
return;
|
||||
}
|
||||
@@ -1675,8 +1691,8 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
const syntheticParts = consumePendingSyntheticParts();
|
||||
|
||||
// Process queued messages first
|
||||
for (let i = 0; i < queuedMessages.length; i++) {
|
||||
const queuedMsg = queuedMessages[i];
|
||||
for (let i = 0; i < queuedMessagesToSend.length; i++) {
|
||||
const queuedMsg = queuedMessagesToSend[i];
|
||||
const { sanitizedText, mention } = parseAgentMentions(queuedMsg.content, agents);
|
||||
const { sanitizedText: queuedText, attachments: mentionAttachments } = extractInlineFileMentions(sanitizedText);
|
||||
addMentionedSkills(queuedText);
|
||||
@@ -1715,7 +1731,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
agentMentionName = mention.name;
|
||||
}
|
||||
|
||||
if (queuedMessages.length === 0) {
|
||||
if (queuedMessagesToSend.length === 0) {
|
||||
// No queue - current input is primary
|
||||
primaryText = messageText;
|
||||
primaryAttachments = [...attachmentsToSend, ...mentionAttachments];
|
||||
@@ -1735,7 +1751,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
}
|
||||
|
||||
if (drafts.length > 0) {
|
||||
if (queuedMessages.length === 0) {
|
||||
if (queuedMessagesToSend.length === 0) {
|
||||
primaryText = appendInlineComments(primaryText, drafts);
|
||||
} else if (additionalParts.length > 0) {
|
||||
const lastPart = additionalParts[additionalParts.length - 1];
|
||||
@@ -1786,7 +1802,9 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
if (!primaryText && primaryAttachments.length === 0 && additionalParts.length === 0) return;
|
||||
|
||||
// Clear queue and input
|
||||
if (currentSessionId && hasQueuedMessages) {
|
||||
if (currentSessionId && queuedMessageId) {
|
||||
removeFromQueue(currentSessionId, queuedMessageId);
|
||||
} else if (currentSessionId && hasQueuedMessages) {
|
||||
clearQueue(currentSessionId);
|
||||
}
|
||||
if (!queuedOnly) {
|
||||
@@ -1862,13 +1880,13 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
const instructionsText = await renderMagicPrompt('session.summary.instructions', { topic_block: topicBlock });
|
||||
await sendMessage(
|
||||
visibleText,
|
||||
currentProviderId,
|
||||
currentModelId,
|
||||
currentAgentName,
|
||||
providerIdToSend,
|
||||
modelIdToSend,
|
||||
agentNameToSend,
|
||||
[],
|
||||
agentMentionName,
|
||||
[{ text: instructionsText, synthetic: true }],
|
||||
currentVariant,
|
||||
variantToSend,
|
||||
inputMode,
|
||||
);
|
||||
scrollToBottom?.();
|
||||
@@ -1884,13 +1902,13 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
const instructionsText = await renderMagicPrompt('session.review.instructions');
|
||||
await sendMessage(
|
||||
visibleText,
|
||||
currentProviderId,
|
||||
currentModelId,
|
||||
currentAgentName,
|
||||
providerIdToSend,
|
||||
modelIdToSend,
|
||||
agentNameToSend,
|
||||
[],
|
||||
agentMentionName,
|
||||
[{ text: instructionsText, synthetic: true }],
|
||||
currentVariant,
|
||||
variantToSend,
|
||||
inputMode,
|
||||
);
|
||||
scrollToBottom?.();
|
||||
@@ -1933,13 +1951,13 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
|
||||
const sendPromise = sendMessage(
|
||||
primaryText,
|
||||
currentProviderId,
|
||||
currentModelId,
|
||||
currentAgentName,
|
||||
providerIdToSend,
|
||||
modelIdToSend,
|
||||
agentNameToSend,
|
||||
primaryAttachments,
|
||||
agentMentionName,
|
||||
additionalParts.length > 0 ? additionalParts : undefined,
|
||||
currentVariant,
|
||||
variantToSend,
|
||||
inputMode
|
||||
);
|
||||
|
||||
@@ -3772,6 +3790,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
<AttachedFilesList onShowPopup={handleShowAttachmentPreview} />
|
||||
<QueuedMessageChips
|
||||
onEditMessage={handleQueuedMessageEdit}
|
||||
onSendMessage={handleQueuedMessageSend}
|
||||
/>
|
||||
{hasDrafts && (
|
||||
<div className="flex flex-wrap items-center gap-2 pb-2">
|
||||
|
||||
@@ -4,14 +4,16 @@ 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';
|
||||
|
||||
interface QueuedMessageChipProps {
|
||||
message: QueuedMessage;
|
||||
sessionId: string;
|
||||
onEdit: (message: QueuedMessage) => void;
|
||||
onSend: (message: QueuedMessage) => void;
|
||||
}
|
||||
|
||||
const QueuedMessageChip = memo(({ message, sessionId, onEdit }: QueuedMessageChipProps) => {
|
||||
const QueuedMessageChip = memo(({ message, sessionId, onEdit, onSend }: QueuedMessageChipProps) => {
|
||||
const { t } = useI18n();
|
||||
const removeFromQueue = useMessageQueueStore((state) => state.removeFromQueue);
|
||||
|
||||
@@ -29,24 +31,31 @@ const QueuedMessageChip = memo(({ message, sessionId, onEdit }: QueuedMessageChi
|
||||
const attachmentCount = message.attachments?.length ?? 0;
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-center gap-1.5 text-sm h-5 px-1">
|
||||
<button
|
||||
<div className="flex min-w-0 items-center gap-2 py-1">
|
||||
<span className="min-w-0 flex-1 truncate typography-ui-label text-foreground">
|
||||
{firstLine || t('chat.queuedMessage.empty')}
|
||||
{attachmentCount > 0 && (
|
||||
<span className="ml-1 text-muted-foreground">{t('chat.queuedMessage.attachments', { count: attachmentCount })}</span>
|
||||
)}
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="xs"
|
||||
onClick={() => onEdit(message)}
|
||||
className="flex min-w-0 flex-1 items-center gap-1.5 text-left hover:opacity-80 transition-opacity"
|
||||
>
|
||||
<Icon name="message-2" className="h-4 w-4 flex-shrink-0 text-muted-foreground"
|
||||
/>
|
||||
<span className="text-muted-foreground flex-shrink-0">
|
||||
Queued
|
||||
{attachmentCount > 0 && (
|
||||
<span className="ml-1">{t('chat.queuedMessage.attachments', { count: attachmentCount })}</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="text-foreground truncate">
|
||||
{firstLine || t('chat.queuedMessage.empty')}
|
||||
</span>
|
||||
</button>
|
||||
<Icon name="edit" className="h-3 w-3" aria-hidden="true" />
|
||||
{t('chat.queuedMessage.edit')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="xs"
|
||||
onClick={() => onSend(message)}
|
||||
>
|
||||
<Icon name="send-plane" className="h-3 w-3" aria-hidden="true" />
|
||||
{t('chat.queuedMessage.send')}
|
||||
</Button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeFromQueue(sessionId, message.id)}
|
||||
@@ -63,11 +72,13 @@ 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 }: QueuedMessageChipsProps) => {
|
||||
export const QueuedMessageChips = memo(({ onEditMessage, onSendMessage }: QueuedMessageChipsProps) => {
|
||||
const { t } = useI18n();
|
||||
const currentSessionId = useSessionUIStore((state) => state.currentSessionId);
|
||||
const queuedMessages = useMessageQueueStore(
|
||||
React.useCallback(
|
||||
@@ -93,20 +104,35 @@ export const QueuedMessageChips = memo(({ onEditMessage }: QueuedMessageChipsPro
|
||||
}
|
||||
}, [currentSessionId, popToInput, onEditMessage]);
|
||||
|
||||
const handleSend = React.useCallback((message: QueuedMessage) => {
|
||||
onSendMessage(message.id);
|
||||
}, [onSendMessage]);
|
||||
|
||||
if (queuedMessages.length === 0 || !currentSessionId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pb-2 w-full px-1 space-y-1">
|
||||
{queuedMessages.map((message) => (
|
||||
<QueuedMessageChip
|
||||
key={message.id}
|
||||
message={message}
|
||||
sessionId={currentSessionId}
|
||||
onEdit={handleEdit}
|
||||
/>
|
||||
))}
|
||||
<div className="pb-2 w-full px-1">
|
||||
<div className="rounded-xl border border-border/60 bg-[var(--surface-elevated)] text-[var(--surface-elevated-foreground)] shadow-sm overflow-hidden">
|
||||
<div className="flex w-full items-center gap-2 px-3 py-2 text-left">
|
||||
<span className="typography-ui-label font-medium text-foreground flex-shrink-0">
|
||||
{t('chat.queuedMessage.title')} {queuedMessages.length}
|
||||
</span>
|
||||
<Icon name="time" className="ml-auto h-4 w-4 text-muted-foreground" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="px-3 pb-3 flex flex-col gap-1.5 max-h-[10.5rem] overflow-y-auto">
|
||||
{queuedMessages.map((message) => (
|
||||
<QueuedMessageChip
|
||||
key={message.id}
|
||||
message={message}
|
||||
sessionId={currentSessionId}
|
||||
onEdit={handleEdit}
|
||||
onSend={handleSend}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1519,6 +1519,9 @@ export const dict = {
|
||||
'chat.fileMentionAutocomplete.empty': 'No matches found',
|
||||
'chat.queuedMessage.attachments': '+{count} file(s)',
|
||||
'chat.queuedMessage.empty': '(empty)',
|
||||
'chat.queuedMessage.title': 'Queued messages',
|
||||
'chat.queuedMessage.edit': 'edit',
|
||||
'chat.queuedMessage.send': 'send',
|
||||
'chat.queuedMessage.removeAria': 'Remove from queue',
|
||||
'chat.container.returnToParent.aria': 'Return to parent session',
|
||||
'chat.container.returnToParent.titleNamed': 'Return to: {title}',
|
||||
|
||||
@@ -1485,6 +1485,9 @@ export const dict: Record<I18nKey, string> = {
|
||||
"chat.fileMentionAutocomplete.empty": "No se encontraron coincidencias",
|
||||
"chat.queuedMessage.attachments": "+{count} archivo(s)",
|
||||
"chat.queuedMessage.empty": "(vacío)",
|
||||
"chat.queuedMessage.title": "Queued messages",
|
||||
"chat.queuedMessage.edit": "edit",
|
||||
"chat.queuedMessage.send": "send",
|
||||
"chat.queuedMessage.removeAria": "Eliminar de la cola",
|
||||
"chat.container.returnToParent.aria": "Volver a la sesión principal",
|
||||
"chat.container.returnToParent.titleNamed": "Volver a: {title}",
|
||||
|
||||
@@ -1521,6 +1521,9 @@ export const dict: Record<I18nKey, string> = {
|
||||
'chat.fileMentionAutocomplete.empty': '일치하는 항목 없음',
|
||||
'chat.queuedMessage.attachments': '+{count} 파일(s)',
|
||||
'chat.queuedMessage.empty': '(비어 있음)',
|
||||
'chat.queuedMessage.title': 'Queued messages',
|
||||
'chat.queuedMessage.edit': 'edit',
|
||||
'chat.queuedMessage.send': 'send',
|
||||
'chat.queuedMessage.removeAria': '큐에서 제거',
|
||||
'chat.container.returnToParent.aria': '상위 세션으로 돌아가기',
|
||||
'chat.container.returnToParent.titleNamed': '돌아가기: {title}',
|
||||
|
||||
@@ -512,6 +512,9 @@ export const dict: Record<I18nKey, string> = {
|
||||
'chat.fileMentionAutocomplete.empty': 'Nie znaleziono pasujących',
|
||||
'chat.queuedMessage.attachments': '+{count} plik(i)',
|
||||
'chat.queuedMessage.empty': '(puste)',
|
||||
'chat.queuedMessage.title': 'Queued messages',
|
||||
'chat.queuedMessage.edit': 'edit',
|
||||
'chat.queuedMessage.send': 'send',
|
||||
'chat.queuedMessage.removeAria': 'Usuń z kolejki',
|
||||
'chat.container.returnToParent.aria': 'Powrót do sesji nadrzędnej',
|
||||
'chat.container.returnToParent.titleNamed': 'Powrót do: {title}',
|
||||
|
||||
@@ -1485,6 +1485,9 @@ export const dict: Record<I18nKey, string> = {
|
||||
"chat.fileMentionAutocomplete.empty": "Nenhuma correspondência encontrada",
|
||||
"chat.queuedMessage.attachments": "+{count} arquivo(s)",
|
||||
"chat.queuedMessage.empty": "(vazio)",
|
||||
"chat.queuedMessage.title": "Queued messages",
|
||||
"chat.queuedMessage.edit": "edit",
|
||||
"chat.queuedMessage.send": "send",
|
||||
"chat.queuedMessage.removeAria": "Excluir da fila",
|
||||
"chat.container.returnToParent.aria": "Voltar para a sessão principal",
|
||||
"chat.container.returnToParent.titleNamed": "Voltar para: {title}",
|
||||
|
||||
@@ -1485,6 +1485,9 @@ export const dict: Record<I18nKey, string> = {
|
||||
"chat.fileMentionAutocomplete.empty": "Збігів не знайдено",
|
||||
"chat.queuedMessage.attachments": "+{count} файл(и)",
|
||||
"chat.queuedMessage.empty": "(порожній)",
|
||||
"chat.queuedMessage.title": "Повідомлення в черзі",
|
||||
"chat.queuedMessage.edit": "edit",
|
||||
"chat.queuedMessage.send": "send",
|
||||
"chat.queuedMessage.removeAria": "Видалити з черги",
|
||||
"chat.container.returnToParent.aria": "Повернутися до батьківської сесії",
|
||||
"chat.container.returnToParent.titleNamed": "Повернутися до: {title}",
|
||||
|
||||
@@ -1485,6 +1485,9 @@ export const dict: Record<I18nKey, string> = {
|
||||
'chat.fileMentionAutocomplete.empty': '未找到匹配项',
|
||||
'chat.queuedMessage.attachments': '+{count} 个文件',
|
||||
'chat.queuedMessage.empty': '(空)',
|
||||
'chat.queuedMessage.title': 'Queued messages',
|
||||
'chat.queuedMessage.edit': 'edit',
|
||||
'chat.queuedMessage.send': 'send',
|
||||
'chat.queuedMessage.removeAria': '从队列移除',
|
||||
'chat.container.returnToParent.aria': '返回父会话',
|
||||
'chat.container.returnToParent.titleNamed': '返回到:{title}',
|
||||
|
||||
@@ -1482,6 +1482,9 @@ export const dict: Record<I18nKey, string> = {
|
||||
'chat.fileMentionAutocomplete.empty': '找不到符合項目',
|
||||
'chat.queuedMessage.attachments': '+{count} 個檔案',
|
||||
'chat.queuedMessage.empty': '(空)',
|
||||
'chat.queuedMessage.title': 'Queued messages',
|
||||
'chat.queuedMessage.edit': 'edit',
|
||||
'chat.queuedMessage.send': 'send',
|
||||
'chat.queuedMessage.removeAria': '從佇列移除',
|
||||
'chat.container.returnToParent.aria': '返回父會話',
|
||||
'chat.container.returnToParent.titleNamed': '返回到:{title}',
|
||||
|
||||
Reference in New Issue
Block a user