export interface AgentPromptAttachment {
name: string;
type?: string;
size?: number;
text?: string;
dataUrl?: string;
}
export function escapePromptAttachmentAttribute(value: string): string {
return value.replace(/&/g, "&").replace(/"/g, """);
}
export function formatPromptWithAttachments(
prompt: string,
attachments: readonly AgentPromptAttachment[],
): string {
if (attachments.length === 0) return prompt;
const attachmentText = attachments
.map((attachment) => {
const size = attachment.size ? ` size="${attachment.size}"` : "";
const type = attachment.type
? ` type="${escapePromptAttachmentAttribute(attachment.type)}"`
: "";
if (attachment.dataUrl) {
return `\n${attachment.dataUrl}\n`;
}
const body =
attachment.text?.trim() ||
"Selected in the UI. If this file is needed, inspect it from the workspace or ask for a readable copy.";
return `\n${body}\n`;
})
.join("\n\n");
return `${prompt.trimEnd()}\n\nAttached context:\n${attachmentText}`;
}
export const AGENT_PROMPT_MAX_INLINE_TEXT_CHARS = 60_000;
export const AGENT_PROMPT_MAX_INLINE_IMAGE_BYTES = 2 * 1024 * 1024;
export interface ReadAgentPromptAttachmentOptions {
maxInlineTextChars?: number;
maxInlineImageBytes?: number;
}
export async function readAgentPromptAttachment(
file: File,
options: ReadAgentPromptAttachmentOptions = {},
): Promise {
const maxInlineTextChars =
options.maxInlineTextChars ?? AGENT_PROMPT_MAX_INLINE_TEXT_CHARS;
const maxInlineImageBytes =
options.maxInlineImageBytes ?? AGENT_PROMPT_MAX_INLINE_IMAGE_BYTES;
const attachment: AgentPromptAttachment = {
name: file.name,
type: file.type || undefined,
size: file.size,
};
if (isInlineableAgentPromptFile(file) && file.size <= maxInlineTextChars) {
try {
attachment.text = await file.text();
} catch {
// Keep the filename-only attachment if the browser cannot read it.
}
} else if (
file.type.startsWith("image/") &&
file.size <= maxInlineImageBytes
) {
try {
attachment.dataUrl = await readFileAsDataUrl(file);
} catch {
// Keep the filename-only attachment if the browser cannot read it.
}
}
return attachment;
}
export function isInlineableAgentPromptFile(file: File): boolean {
if (file.type.startsWith("text/")) return true;
return /\.(cjs|css|csv|html|js|json|jsx|md|mdx|mjs|sql|tsx?|txt|xml|yaml|yml)$/i.test(
file.name,
);
}
function readFileAsDataUrl(file: File): Promise {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(String(reader.result ?? ""));
reader.onerror = () =>
reject(reader.error ?? new Error("Could not read file"));
reader.readAsDataURL(file);
});
}