Merge remote-tracking branch 'origin/main' into fix/ui-thinking-effort-draft-project-rename
This commit is contained in:
@@ -186,7 +186,6 @@ const MAX_MOBILE_COMPOSER_LINES = 16;
|
||||
*/
|
||||
const MOBILE_COMPOSER_BOUND_GAP_PX = 4;
|
||||
const EMPTY_QUEUE: QueuedMessage[] = [];
|
||||
const EMPTY_SENDING_IDS: string[] = [];
|
||||
const COMPACT_CHAT_PLACEHOLDER_MAX_WIDTH = 560;
|
||||
const renameFileForAttachmentCitation = (file: File, filename: string): File => {
|
||||
if (file.name === filename) {
|
||||
@@ -774,8 +773,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
|
||||
)
|
||||
);
|
||||
const addToQueue = useMessageQueueStore((state) => state.addToQueue);
|
||||
const clearQueue = useMessageQueueStore((state) => state.clearQueue);
|
||||
const removeFromQueue = useMessageQueueStore((state) => state.removeFromQueue);
|
||||
const takeForSend = useMessageQueueStore((state) => state.takeForSend);
|
||||
|
||||
// Inline comment drafts
|
||||
const inlineDraftSessionKey = currentSessionId ?? (newSessionDraftOpen ? 'draft' : '');
|
||||
@@ -929,9 +927,15 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
|
||||
// queue consumes them and attaches them as structured context parts.
|
||||
const messageToQueue = inputSnapshot.message.replace(/^\n+|\n+$/g, '');
|
||||
const attachmentsToQueue = sanitizeAttachmentsForSend(attachedFiles);
|
||||
// Resolved now, not at delivery: the server that sends a queued
|
||||
// message has no agent list, and the mention must match what was
|
||||
// visible when the user typed it.
|
||||
const { sanitizedText, mention } = parseAgentMentions(messageToQueue, agents);
|
||||
|
||||
addToQueue(messageQueueTarget, {
|
||||
content: messageToQueue,
|
||||
text: sanitizedText,
|
||||
agentMention: mention?.name,
|
||||
attachments: attachmentsToQueue.length > 0 ? attachmentsToQueue : undefined,
|
||||
sendConfig: currentProviderId && currentModelId ? {
|
||||
providerID: currentProviderId,
|
||||
@@ -939,6 +943,20 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
|
||||
agent: currentAgentName ?? undefined,
|
||||
variant: currentVariant ?? undefined,
|
||||
} : undefined,
|
||||
}).catch((error) => {
|
||||
console.warn('[queue] failed to queue message:', error);
|
||||
toast.error(t('chat.queuedMessage.toast.queueFailed'));
|
||||
// The composer was cleared on queueing; give the text back unless
|
||||
// the user has already typed something new.
|
||||
const currentInput = composerRef.current?.getValue() ?? messageRef.current;
|
||||
if (!currentInput) {
|
||||
setMessage(messageToQueue);
|
||||
} else {
|
||||
useInputStore.getState().setPendingInputText(messageToQueue, 'append');
|
||||
}
|
||||
if (attachmentsToQueue.length > 0) {
|
||||
useInputStore.getState().setAttachedFiles([...useInputStore.getState().attachedFiles, ...attachmentsToQueue]);
|
||||
}
|
||||
});
|
||||
|
||||
// Sending while the agent works must still take the reader to the
|
||||
@@ -959,7 +977,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
|
||||
if (!isMobile) {
|
||||
composerRef.current?.focus();
|
||||
}
|
||||
}, [getCurrentInputSnapshot, currentSessionId, messageQueueTarget, attachedFiles, sanitizeAttachmentsForSend, addToQueue, clearAttachedFiles, isMobile, currentProviderId, currentModelId, currentAgentName, currentVariant, scrollToLatest]);
|
||||
}, [getCurrentInputSnapshot, currentSessionId, messageQueueTarget, attachedFiles, sanitizeAttachmentsForSend, addToQueue, clearAttachedFiles, isMobile, currentProviderId, currentModelId, currentAgentName, currentVariant, scrollToLatest, agents, t]);
|
||||
|
||||
const handleQueuedMessageEdit = React.useCallback((content: string) => {
|
||||
setMessage(content);
|
||||
@@ -1023,28 +1041,23 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
|
||||
hasContent: options.presetText.trim().length > 0 || attachedFiles.length > 0 || hasDrafts,
|
||||
}
|
||||
: getCurrentInputSnapshot();
|
||||
// A queued item stays in the queue until its own send resolves, so the
|
||||
// auto-send hook may already be delivering one of these. Merging it here
|
||||
// would send the same message twice (the window is seconds over a relay).
|
||||
const sendingIds = messageQueueTarget
|
||||
? useMessageQueueStore.getState().sendingIds[getMessageQueueKey(messageQueueTarget)] ?? EMPTY_SENDING_IDS
|
||||
: EMPTY_SENDING_IDS;
|
||||
const queuedMessagesToSend = (queuedMessageId
|
||||
? queuedMessages.filter((message) => message.id === queuedMessageId)
|
||||
: queuedMessages
|
||||
).filter((message) => !sendingIds.includes(message.id));
|
||||
|
||||
if (queuedOnly && autoReviewRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (queuedOnly) {
|
||||
if (queuedMessagesToSend.length === 0 || !currentSessionId) return;
|
||||
if (!queuedMessages.some((message) => !queuedMessageId || message.id === queuedMessageId) || !currentSessionId) return;
|
||||
} else if ((!inputSnapshot.hasContent && !hasQueuedMessages) || (!currentSessionId && !newSessionDraftOpen)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const capturedSendConfig = queuedOnly ? queuedMessagesToSend[0]?.sendConfig : undefined;
|
||||
// The projection knows the captured send configuration; the full
|
||||
// messages are taken from the queue only once nothing below can still
|
||||
// bail out, so an early return leaves the queue untouched.
|
||||
const queuedProjection = queuedMessageId
|
||||
? queuedMessages.filter((message) => message.id === queuedMessageId)
|
||||
: queuedMessages;
|
||||
const capturedSendConfig = queuedOnly ? queuedProjection[0]?.sendConfig : undefined;
|
||||
const providerIdToSend = capturedSendConfig?.providerID ?? currentProviderId;
|
||||
const modelIdToSend = capturedSendConfig?.modelID ?? currentModelId;
|
||||
const agentNameToSend = capturedSendConfig?.agent ?? currentAgentName;
|
||||
@@ -1115,10 +1128,10 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
|
||||
const preparedDocumentMentions = new Map<string, AttachedFile[]>();
|
||||
const reservedFilenames = new Set([
|
||||
...attachedFiles.map((attachment) => attachment.filename),
|
||||
...queuedMessagesToSend.flatMap((queued) => queued.attachments?.map((attachment) => attachment.filename) ?? []),
|
||||
...queuedProjection.flatMap((queued) => queued.attachments?.map((attachment) => attachment.filename) ?? []),
|
||||
]);
|
||||
const mentionTexts = [
|
||||
...queuedMessagesToSend.map((queued) => queued.content),
|
||||
...queuedProjection.map((queued) => queued.content),
|
||||
...(!queuedOnly && inputSnapshot.hasContent ? [inputSnapshot.message] : []),
|
||||
];
|
||||
for (const rawText of mentionTexts) {
|
||||
@@ -1150,6 +1163,22 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
// The composer delivers these itself, so they leave the queue now — the
|
||||
// queue's own delivery (server-side, or the auto-send hook in VS Code)
|
||||
// skips anything already in flight, and a message already being
|
||||
// delivered stays out of this send so it cannot go out twice.
|
||||
let queuedMessagesToSend: QueuedMessage[] = [];
|
||||
if (capturedTarget && hasQueuedMessages) {
|
||||
try {
|
||||
queuedMessagesToSend = await takeForSend(capturedTarget, queuedMessageId);
|
||||
} catch (error) {
|
||||
console.warn('[queue] failed to take queued messages for sending:', error);
|
||||
toast.error(t('chat.queuedMessage.toast.takeFailed'));
|
||||
return;
|
||||
}
|
||||
if (queuedOnly && queuedMessagesToSend.length === 0) return;
|
||||
}
|
||||
|
||||
// Inline review comments and synthetic context are consumed before
|
||||
// assembly so a failed send can restore exactly what it took. Context
|
||||
// drafts ride with whichever send goes out next, including queued
|
||||
@@ -1201,12 +1230,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
|
||||
|
||||
if (outgoing.isEmpty) return;
|
||||
|
||||
// Clear queue and input
|
||||
if (capturedTarget && queuedMessageId) {
|
||||
removeFromQueue(capturedTarget, queuedMessageId);
|
||||
} else if (capturedTarget && hasQueuedMessages) {
|
||||
clearQueue(capturedTarget);
|
||||
}
|
||||
// Clear input (the queue was taken above)
|
||||
if (!queuedOnly) {
|
||||
setMessage('');
|
||||
confirmedMentionsRef.current.clear();
|
||||
|
||||
@@ -20,6 +20,7 @@ import { useInputStore } from '@/sync/input-store';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { Icon } from "@/components/icon/Icon";
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { toast } from '@/components/ui';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface QueuedMessageChipProps {
|
||||
@@ -149,16 +150,21 @@ export const QueuedMessageChips = memo(({ onEditMessage, onSendMessage }: Queued
|
||||
|
||||
const handleEdit = React.useCallback((message: QueuedMessage) => {
|
||||
if (!target) return;
|
||||
|
||||
const popped = popToInput(target, message.id);
|
||||
if (popped) {
|
||||
|
||||
// The full message (attachments included) comes back from the queue's
|
||||
// owner; the chip itself only knows the summary.
|
||||
void popToInput(target, message.id).then((popped) => {
|
||||
if (!popped) return;
|
||||
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]);
|
||||
}).catch((error) => {
|
||||
console.warn('[queue] failed to take queued message for editing:', error);
|
||||
toast.error(t('chat.queuedMessage.toast.takeFailed'));
|
||||
});
|
||||
}, [target, popToInput, onEditMessage, t]);
|
||||
|
||||
const handleSend = React.useCallback((message: QueuedMessage) => {
|
||||
onSendMessage(message.id);
|
||||
|
||||
Reference in New Issue
Block a user