authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-09-20 20:34:06-03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-20 20:34:06-03:00
logc789cd174ae50f50ecb0a34b96a81e00b0f5c823
tree72fdf10f5c0a22adf5d53b325714fda10d0a662a
parent67285e4b8719000eecb9de2a424609ca5c6f0274
signature Signed by PGP key B5690EEEBB952194

fix(web): skip image inline chip when composer is empty (#12528)


1 files changed, 20 insertions(+), 2 deletions(-)

apps/web/src/components/chat/ChatComposer.tsx+20-2
...@@ -206,6 +206,7 @@ import {...@@ -206,6 +206,7 @@ import {
206import { useOpenPrLink } from "~/lib/openPullRequestLink";206import { useOpenPrLink } from "~/lib/openPullRequestLink";
207import {207import {
208 collectInlineContextIds,208 collectInlineContextIds,
209 stripInlineContextReferences,
209 type ComposerContextReference,210 type ComposerContextReference,
210 ensureInlineContextReferences,211 ensureInlineContextReferences,
211 formatInlineContextReference,212 formatInlineContextReference,
...@@ -5241,6 +5242,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -5241,6 +5242,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
5241 options?: {5242 options?: {
5242 readonly source?: ChatFileAttachment["source"];5243 readonly source?: ChatFileAttachment["source"];
5243 readonly selection?: { start: number; end: number };5244 readonly selection?: { start: number; end: number };
5245 readonly skipImageInlineChip?: boolean;
5244 },5246 },
5245 ): Promise<boolean> => {5247 ): Promise<boolean> => {
5246 if (!activeThreadId || files.length === 0 || isRevertingCheckpointRef.current) return false;5248 if (!activeThreadId || files.length === 0 || isRevertingCheckpointRef.current) return false;
...@@ -5260,6 +5262,20 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -5260,6 +5262,20 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
5260 // large image is being compressed, and the attachments and errors belong5262 // large image is being compressed, and the attachments and errors belong
5261 // to the thread the paste happened in.5263 // to the thread the paste happened in.
5262 const threadId = activeThreadId;5264 const threadId = activeThreadId;
5265 // Images landing with no prose live on the shelf with no chip. Read before
5266 // the awaits below: compression is async and the prompt may change while it
5267 // runs. An explicit selection replace and states where the editor refuses
5268 // input (connecting, approval, pending questions, project selection) still
5269 // get chips so the image is never invisible, unless paste-as-text explicitly
5270 // requests no inline image chip.
5271 const imageAttachmentsGetChips =
5272 !options?.skipImageInlineChip &&
5273 (options?.selection !== undefined ||
5274 isConnecting ||
5275 isComposerApprovalState ||
5276 pendingUserInputs.length > 0 ||
5277 projectSelectionRequired ||
5278 stripInlineContextReferences(promptRef.current).trim().length > 0);
52635279
5264 // Validation happens synchronously so concurrent pastes see each other:5280 // Validation happens synchronously so concurrent pastes see each other:
5265 // accepted files reserve their attachment slots (via the pending counter)5281 // accepted files reserve their attachment slots (via the pending counter)
...@@ -5416,7 +5432,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -5416,7 +5432,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
5416 : [],5432 : [],
5417 );5433 );
5418 const storedImages = nextImages.filter((image) => storedImageIds.has(image.id));5434 const storedImages = nextImages.filter((image) => storedImageIds.has(image.id));
5419 if (storedImages.length > 0) {5435 if (storedImages.length > 0 && imageAttachmentsGetChips) {
5420 insertedAny =5436 insertedAny =
5421 insertAttachmentReferences(storedImages.map(imageContextReference)) || insertedAny;5437 insertAttachmentReferences(storedImages.map(imageContextReference)) || insertedAny;
5422 }5438 }
...@@ -5444,6 +5460,8 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -5444,6 +5460,8 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
5444 /**5460 /**
5445 * Chips for freshly attached files land at the caret; when the editor cannot take5461 * Chips for freshly attached files land at the caret; when the editor cannot take
5446 * input (approval, pending questions) they are appended so the file is never invisible.5462 * input (approval, pending questions) they are appended so the file is never invisible.
5463 * Images skip this when they land with no prose and the editor takes input:
5464 * the shelf thumbnail is enough.
5447 */5465 */
5448 const insertAttachmentReferences = (5466 const insertAttachmentReferences = (
5449 references: ReadonlyArray<ComposerContextReference>,5467 references: ReadonlyArray<ComposerContextReference>,
...@@ -5582,7 +5600,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -5582,7 +5600,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
5582 ) {5600 ) {
5583 event.preventDefault();5601 event.preventDefault();
5584 event.stopPropagation();5602 event.stopPropagation();
5585 void addComposerAttachments(files);5603 void addComposerAttachments(files, { skipImageInlineChip: bypassAutoAttachment });
5586 return;5604 return;
5587 }5605 }
55885606