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