diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index 8f91f08c7093..a739f70bc5d0 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -205,6 +205,7 @@ import { import { useOpenPrLink } from "~/lib/openPullRequestLink"; import { collectInlineContextIds, + stripInlineContextReferences, type ComposerContextReference, ensureInlineContextReferences, formatInlineContextReference, @@ -5234,6 +5235,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) options?: { readonly source?: ChatFileAttachment["source"]; readonly selection?: { start: number; end: number }; + readonly skipImageInlineChip?: boolean; }, ): Promise => { if (!activeThreadId || files.length === 0 || isRevertingCheckpointRef.current) return false; @@ -5253,6 +5255,20 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) // large image is being compressed, and the attachments and errors belong // to the thread the paste happened in. const threadId = activeThreadId; + // Images landing with no prose live on the shelf with no chip. Read before + // the awaits below: compression is async and the prompt may change while it + // runs. An explicit selection replace and states where the editor refuses + // input (connecting, approval, pending questions, project selection) still + // get chips so the image is never invisible, unless paste-as-text explicitly + // requests no inline image chip. + const imageAttachmentsGetChips = + !options?.skipImageInlineChip && + (options?.selection !== undefined || + isConnecting || + isComposerApprovalState || + pendingUserInputs.length > 0 || + projectSelectionRequired || + stripInlineContextReferences(promptRef.current).trim().length > 0); // Validation happens synchronously so concurrent pastes see each other: // accepted files reserve their attachment slots (via the pending counter) @@ -5409,7 +5425,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) : [], ); const storedImages = nextImages.filter((image) => storedImageIds.has(image.id)); - if (storedImages.length > 0) { + if (storedImages.length > 0 && imageAttachmentsGetChips) { insertedAny = insertAttachmentReferences(storedImages.map(imageContextReference)) || insertedAny; } @@ -5437,6 +5453,8 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) /** * Chips for freshly attached files land at the caret; when the editor cannot take * input (approval, pending questions) they are appended so the file is never invisible. + * Images skip this when they land with no prose and the editor takes input: + * the shelf thumbnail is enough. */ const insertAttachmentReferences = ( references: ReadonlyArray, @@ -5575,7 +5593,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) ) { event.preventDefault(); event.stopPropagation(); - void addComposerAttachments(files); + void addComposerAttachments(files, { skipImageInlineChip: bypassAutoAttachment }); return; }