From 3f19f148dca9b1f01bb78c3f4fd5ba2fa65b1dbc Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Fri, 27 Mar 2026 16:59:47 -0700 Subject: [PATCH] show a warning when there are more than 20 images in a request (#305817) * show a warning when there are more than 20 images in a request * address comments --- .../attachments/chatAttachmentWidgets.ts | 11 +++++++- .../chatAttachmentsContentPart.ts | 25 ++++++++++++++++++- .../browser/widget/input/chatInputPart.ts | 25 ++++++++++++++++++- .../common/attachments/chatVariableEntries.ts | 7 ++++++ 4 files changed, 65 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts b/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts index 849d78a520a..aabd7fe45ad 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts @@ -63,7 +63,7 @@ import { ITerminalService } from '../../../terminal/browser/terminal.js'; import { IChatContentReference } from '../../common/chatService/chatService.js'; import { coerceImageBuffer } from '../../common/chatImageExtraction.js'; import { ChatConfiguration } from '../../common/constants.js'; -import { IChatRequestPasteVariableEntry, IChatRequestVariableEntry, IElementVariableEntry, INotebookOutputVariableEntry, IPromptFileVariableEntry, IPromptTextVariableEntry, ISCMHistoryItemVariableEntry, OmittedState, PromptFileVariableKind, ChatRequestToolReferenceEntry, ISCMHistoryItemChangeVariableEntry, ISCMHistoryItemChangeRangeVariableEntry, ITerminalVariableEntry, isStringVariableEntry } from '../../common/attachments/chatVariableEntries.js'; +import { IChatRequestPasteVariableEntry, IChatRequestVariableEntry, IElementVariableEntry, INotebookOutputVariableEntry, IPromptFileVariableEntry, IPromptTextVariableEntry, ISCMHistoryItemVariableEntry, MAX_IMAGES_PER_REQUEST, OmittedState, PromptFileVariableKind, ChatRequestToolReferenceEntry, ISCMHistoryItemChangeVariableEntry, ISCMHistoryItemChangeRangeVariableEntry, ITerminalVariableEntry, isStringVariableEntry } from '../../common/attachments/chatVariableEntries.js'; import { ILanguageModelChatMetadataAndIdentifier, ILanguageModelsService } from '../../common/languageModels.js'; import { IChatEntitlementService } from '../../../../services/chat/common/chatEntitlementService.js'; import { ILanguageModelToolsService, isToolSet } from '../../common/tools/languageModelToolsService.js'; @@ -432,6 +432,8 @@ export class ImageAttachmentWidget extends AbstractChatAttachmentWidget { ariaLabel = localize('chat.omittedImageAttachment', "Omitted this image: {0}", attachment.name); } else if (attachment.omittedState === OmittedState.Partial) { ariaLabel = localize('chat.partiallyOmittedImageAttachment', "Partially omitted this image: {0}", attachment.name); + } else if (attachment.omittedState === OmittedState.ImageLimitExceeded) { + ariaLabel = localize('chat.imageLimitExceededAttachment', "Image not sent due to limit: {0}", attachment.name); } else { ariaLabel = localize('chat.imageAttachment', "Attached image, {0}", attachment.name); } @@ -519,6 +521,13 @@ function createImageElements(resource: URI | undefined, name: string, fullName: content: hoverElement, style: HoverStyle.Pointer, })); + } else if (omittedState === OmittedState.ImageLimitExceeded) { + element.classList.add('warning'); + hoverElement.textContent = localize('chat.imageLimitExceededHover', "This image was not sent because the maximum of {0} images per request was exceeded.", MAX_IMAGES_PER_REQUEST); + disposable.add(hoverService.setupDelayedHover(element, { + content: hoverElement, + style: HoverStyle.Pointer, + })); } else { disposable.add(hoverService.setupDelayedHover(element, { content: hoverElement, diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAttachmentsContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAttachmentsContentPart.ts index 6cff3c02973..f4f48e3743f 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAttachmentsContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAttachmentsContentPart.ts @@ -11,7 +11,7 @@ import { URI } from '../../../../../../base/common/uri.js'; import { Range } from '../../../../../../editor/common/core/range.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { ResourceLabels } from '../../../../../browser/labels.js'; -import { IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry, isSCMHistoryItemChangeRangeVariableEntry, isSCMHistoryItemChangeVariableEntry, isSCMHistoryItemVariableEntry, isTerminalVariableEntry, isWorkspaceVariableEntry, OmittedState } from '../../../common/attachments/chatVariableEntries.js'; +import { IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry, isSCMHistoryItemChangeRangeVariableEntry, isSCMHistoryItemChangeVariableEntry, isSCMHistoryItemVariableEntry, isTerminalVariableEntry, isWorkspaceVariableEntry, MAX_IMAGES_PER_REQUEST, OmittedState } from '../../../common/attachments/chatVariableEntries.js'; import { ChatResponseReferencePartStatusKind, IChatContentReference } from '../../../common/chatService/chatService.js'; import { DefaultChatAttachmentWidget, ElementChatAttachmentWidget, FileAttachmentWidget, ImageAttachmentWidget, NotebookCellOutputChatAttachmentWidget, PasteAttachmentWidget, PromptFileAttachmentWidget, PromptTextAttachmentWidget, SCMHistoryItemAttachmentWidget, SCMHistoryItemChangeAttachmentWidget, SCMHistoryItemChangeRangeAttachmentWidget, TerminalCommandAttachmentWidget, ToolSetOrToolItemAttachmentWidget } from '../../attachments/chatAttachmentWidgets.js'; import { IChatAttachmentWidgetRegistry } from '../../attachments/chatAttachmentWidgetRegistry.js'; @@ -73,6 +73,8 @@ export class ChatAttachmentsContentPart extends Disposable { const visibleAttachments = this.getVisibleAttachments(); const hasMoreAttachments = this.limit && this._variables.length > this.limit && !this._showingAll; + this.markImageLimitExceeded(this._variables); + for (const attachment of visibleAttachments) { this.renderAttachment(attachment, container); } @@ -89,6 +91,27 @@ export class ChatAttachmentsContentPart extends Disposable { return this._variables.slice(0, this.limit); } + /** + * When the total number of image attachments exceeds the per-request limit, + * mark the oldest images (those that will be dropped by the backend) with + * {@link OmittedState.ImageLimitExceeded}. + */ + private markImageLimitExceeded(attachments: readonly IChatRequestVariableEntry[]): void { + const imageAttachments = attachments.filter(isImageVariableEntry); + if (imageAttachments.length <= MAX_IMAGES_PER_REQUEST) { + return; + } + + // The backend keeps the most-recent images, so mark the oldest ones as exceeded. + // Only overwrite NotOmitted or ImageLimitExceeded to avoid clobbering other states (e.g. Partial for GIFs). + const excessCount = imageAttachments.length - MAX_IMAGES_PER_REQUEST; + for (let i = 0; i < excessCount; i++) { + if (imageAttachments[i].omittedState === OmittedState.NotOmitted || imageAttachments[i].omittedState === OmittedState.ImageLimitExceeded) { + imageAttachments[i].omittedState = OmittedState.ImageLimitExceeded; + } + } + } + private renderShowMoreButton(container: HTMLElement) { const remainingCount = this._variables.length - (this.limit ?? 0); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index 08a4ed41ac9..77a3a5da12e 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -82,7 +82,7 @@ import { getSimpleCodeEditorWidgetOptions, getSimpleEditorOptions, setupSimpleEd import { InlineChatConfigKeys } from '../../../../inlineChat/common/inlineChat.js'; import { IChatViewTitleActionContext } from '../../../common/actions/chatActions.js'; import { ChatContextKeys } from '../../../common/actions/chatContextKeys.js'; -import { ChatRequestVariableSet, IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry, isSCMHistoryItemChangeRangeVariableEntry, isSCMHistoryItemChangeVariableEntry, isSCMHistoryItemVariableEntry, isStringVariableEntry } from '../../../common/attachments/chatVariableEntries.js'; +import { ChatRequestVariableSet, IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry, isSCMHistoryItemChangeRangeVariableEntry, isSCMHistoryItemChangeVariableEntry, isSCMHistoryItemVariableEntry, isStringVariableEntry, MAX_IMAGES_PER_REQUEST, OmittedState } from '../../../common/attachments/chatVariableEntries.js'; import { ChatMode, getModeNameForTelemetry, IChatMode, IChatModeService } from '../../../common/chatModes.js'; import { IChatFollowup, IChatQuestionCarousel, IChatService, IChatSessionContext } from '../../../common/chatService/chatService.js'; import { agentOptionId, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem, IChatSessionsService, isIChatSessionFileChange2, localChatSessionType } from '../../../common/chatSessionsService.js'; @@ -2564,6 +2564,29 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._indexOfLastOpenedContext = -1; } + // Mark images that exceed the per-request limit so they render with a warning + const imageAttachments = attachments.filter(([, a]) => isImageVariableEntry(a)); + if (imageAttachments.length > MAX_IMAGES_PER_REQUEST) { + const excessCount = imageAttachments.length - MAX_IMAGES_PER_REQUEST; + for (let i = 0; i < excessCount; i++) { + const attachment = imageAttachments[i][1]; + if (attachment.omittedState === OmittedState.NotOmitted || attachment.omittedState === OmittedState.ImageLimitExceeded) { + attachment.omittedState = OmittedState.ImageLimitExceeded; + } + } + for (let i = excessCount; i < imageAttachments.length; i++) { + if (imageAttachments[i][1].omittedState === OmittedState.ImageLimitExceeded) { + imageAttachments[i][1].omittedState = OmittedState.NotOmitted; + } + } + } else { + for (const [, a] of imageAttachments) { + if (a.omittedState === OmittedState.ImageLimitExceeded) { + a.omittedState = OmittedState.NotOmitted; + } + } + } + for (const [index, attachment] of attachments) { const resource = URI.isUri(attachment.value) ? attachment.value : isLocation(attachment.value) ? attachment.value.uri : undefined; diff --git a/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts b/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts index 30f30a4c60b..6ce816b06cd 100644 --- a/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts +++ b/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts @@ -56,8 +56,15 @@ export const enum OmittedState { NotOmitted, Partial, Full, + ImageLimitExceeded, } +/** + * The maximum number of images allowed per request. + * Claude has an upstream limit where more than 20 images causes issues. + */ +export const MAX_IMAGES_PER_REQUEST = 20; + export interface IChatRequestToolEntry extends IBaseChatRequestVariableEntry { readonly kind: 'tool'; }