agent-ecosystem/src/renderer/utils/attachmentRecipientCapabilities.ts
2026-05-25 23:43:29 +03:00

189 lines
5.7 KiB
TypeScript

import {
type AgentAttachmentCapability,
resolveAgentAttachmentCapability,
} from '@features/agent-attachments/renderer';
import { categorizeFile, getEffectiveMimeType, isImageMime } from '@shared/constants/attachments';
import { isLeadMember } from '@shared/utils/leadDetection';
import {
inferTeamProviderIdFromModel,
normalizeOptionalTeamProviderId,
} from '@shared/utils/teamProvider';
import type { AttachmentPayload, ResolvedTeamMember } from '@shared/types';
export interface MemberAttachmentCapabilityResult {
capability: AgentAttachmentCapability;
providerId: string;
model: string;
}
function getMemberProviderId(member: ResolvedTeamMember): string {
return (
normalizeOptionalTeamProviderId(member.providerId) ??
inferTeamProviderIdFromModel(member.model) ??
'unknown'
);
}
function isSupportedFileMime(mimeType: string, supported: readonly string[]): boolean {
return supported.some((candidate) =>
candidate.endsWith('/*') ? mimeType.startsWith(candidate.slice(0, -1)) : candidate === mimeType
);
}
function isSupportedImageMime(mimeType: string, supported: readonly string[]): boolean {
return supported.includes(mimeType);
}
function canReceiveAnyAttachment(capability: AgentAttachmentCapability): boolean {
return capability.supportsImages || capability.supportsFiles;
}
export function resolveMemberAttachmentCapability(
member: ResolvedTeamMember
): MemberAttachmentCapabilityResult {
const providerId = getMemberProviderId(member);
const model = member.model ?? '';
return {
providerId,
model,
capability: resolveAgentAttachmentCapability({ providerId, model }),
};
}
export function getMemberAttachmentUnavailableReason(
member: ResolvedTeamMember | null | undefined
): string | null {
if (!member) {
return 'Select a recipient before attaching files.';
}
const { capability } = resolveMemberAttachmentCapability(member);
if (canReceiveAnyAttachment(capability)) {
return null;
}
return capability.displayText;
}
export function getAttachmentInputAcceptForMember(
member: ResolvedTeamMember | null | undefined
): string {
if (!member) {
return '*/*';
}
const { capability } = resolveMemberAttachmentCapability(member);
if (capability.supportsImages && !capability.supportsFiles) {
return capability.supportedImageMimeTypes.join(',');
}
return '*/*';
}
export function validateAttachmentFilesForMember(input: {
member: ResolvedTeamMember | null | undefined;
files: FileList | File[];
}): string | null {
const member = input.member;
if (!member) {
return 'Select a recipient before attaching files.';
}
const files = Array.from(input.files);
if (files.length === 0) {
return null;
}
const { capability } = resolveMemberAttachmentCapability(member);
if (!canReceiveAnyAttachment(capability)) {
return capability.displayText;
}
for (const file of files) {
const category = categorizeFile(file);
if (category === 'unsupported') {
continue;
}
if (category === 'image') {
if (!capability.supportsImages) {
return capability.displayText;
}
const mimeType = getEffectiveMimeType(file);
if (!isSupportedImageMime(mimeType, capability.supportedImageMimeTypes)) {
return 'This image type is not supported by the selected model.';
}
continue;
}
if (!capability.supportsFiles) {
return capability.filesDisplayText;
}
const mimeType = getEffectiveMimeType(file);
if (!isSupportedFileMime(mimeType, capability.supportedFileMimeTypes)) {
return 'This file type is not supported by the selected model.';
}
}
return null;
}
export function validateAttachmentPayloadsForMember(input: {
member: ResolvedTeamMember | null | undefined;
attachments: readonly AttachmentPayload[];
}): string | null {
const member = input.member;
if (!member || input.attachments.length === 0) {
return null;
}
const { capability } = resolveMemberAttachmentCapability(member);
if (!canReceiveAnyAttachment(capability)) {
return capability.displayText;
}
let imageCount = 0;
let fileCount = 0;
let totalBytes = 0;
for (const attachment of input.attachments) {
totalBytes += attachment.size;
if (isImageMime(attachment.mimeType)) {
imageCount += 1;
if (!capability.supportsImages) {
return capability.displayText;
}
if (!isSupportedImageMime(attachment.mimeType, capability.supportedImageMimeTypes)) {
return 'This image type is not supported by the selected model.';
}
if (attachment.size > capability.maxBytesPerImage) {
return 'Image is too large for the selected model.';
}
continue;
}
fileCount += 1;
if (!capability.supportsFiles) {
return capability.filesDisplayText;
}
if (!isSupportedFileMime(attachment.mimeType, capability.supportedFileMimeTypes)) {
return 'This file type is not supported by the selected model.';
}
if (attachment.size > capability.maxBytesPerFile) {
return 'File is too large for the selected model.';
}
}
if (imageCount > capability.maxImages) {
return `Maximum ${capability.maxImages} image attachments for this model.`;
}
if (fileCount > capability.maxFiles) {
return `Maximum ${capability.maxFiles} file attachments for this model.`;
}
if (totalBytes > capability.maxBytesTotal) {
return 'Attachments exceed the selected model size limit.';
}
return null;
}
export function canMemberShowAttachmentControl(
member: ResolvedTeamMember | null | undefined
): boolean {
if (!member) {
return false;
}
const providerId = getMemberProviderId(member);
return isLeadMember(member) || providerId === 'opencode';
}