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
This commit is contained in:
Justin Chen
2026-03-27 16:59:47 -07:00
committed by GitHub
parent 5be9778feb
commit 3f19f148dc
4 changed files with 65 additions and 3 deletions

View File

@@ -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,

View File

@@ -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);

View File

@@ -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;

View File

@@ -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';
}