Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
194 lines
5.2 KiB
TypeScript
194 lines
5.2 KiB
TypeScript
export interface AgentRichTextPromptImage {
|
|
name: string;
|
|
mimeType: "image/png" | "image/jpeg" | "image/webp";
|
|
data: string;
|
|
}
|
|
|
|
export function imageFilesFromDataTransfer(
|
|
dataTransfer: DataTransfer | null
|
|
): File[] {
|
|
if (!dataTransfer) {
|
|
return [];
|
|
}
|
|
const files: File[] = [];
|
|
const items = (dataTransfer as { items?: DataTransferItemList | null }).items;
|
|
if (!items) {
|
|
return Array.from(dataTransfer.files ?? []).filter((file) =>
|
|
supportedPromptImageMimeType(file.type)
|
|
);
|
|
}
|
|
for (const item of Array.from(items)) {
|
|
if (item.kind !== "file" || !supportedPromptImageMimeType(item.type)) {
|
|
continue;
|
|
}
|
|
const file = item.getAsFile();
|
|
if (file) {
|
|
files.push(file);
|
|
}
|
|
}
|
|
if (files.length === 0) {
|
|
return Array.from(dataTransfer.files ?? []).filter((file) =>
|
|
supportedPromptImageMimeType(file.type)
|
|
);
|
|
}
|
|
return files;
|
|
}
|
|
|
|
export function nonImageFilesFromDataTransfer(
|
|
dataTransfer: DataTransfer | null
|
|
): File[] {
|
|
if (!dataTransfer) {
|
|
return [];
|
|
}
|
|
const files: File[] = [];
|
|
const items = (dataTransfer as { items?: DataTransferItemList | null }).items;
|
|
if (!items) {
|
|
return Array.from(dataTransfer.files ?? []).filter(
|
|
(file) =>
|
|
!supportedPromptImageMimeType(file.type) &&
|
|
!supportedPromptImageFileName(file.name)
|
|
);
|
|
}
|
|
for (const item of Array.from(items)) {
|
|
if (item.kind !== "file" || supportedPromptImageMimeType(item.type)) {
|
|
continue;
|
|
}
|
|
const file = item.getAsFile();
|
|
if (file && supportedPromptImageFileName(file.name)) {
|
|
continue;
|
|
}
|
|
if (file) {
|
|
files.push(file);
|
|
}
|
|
}
|
|
if (files.length === 0) {
|
|
return Array.from(dataTransfer.files ?? []).filter(
|
|
(file) =>
|
|
!supportedPromptImageMimeType(file.type) &&
|
|
!supportedPromptImageFileName(file.name)
|
|
);
|
|
}
|
|
return files;
|
|
}
|
|
|
|
export function classifyAgentRichTextExternalFiles(
|
|
dataTransfer: DataTransfer | null
|
|
): { imageFiles: File[]; regularFiles: File[] } {
|
|
const imageFiles = imageFilesFromDataTransfer(dataTransfer);
|
|
const imageFileSet = new Set(imageFiles);
|
|
return {
|
|
imageFiles,
|
|
regularFiles: nonImageFilesFromDataTransfer(dataTransfer).filter(
|
|
(file) => !imageFileSet.has(file)
|
|
)
|
|
};
|
|
}
|
|
|
|
export function routeAgentRichTextExternalFiles(
|
|
dataTransfer: DataTransfer | null,
|
|
options: {
|
|
externalFilesSupported: boolean;
|
|
promptImagesSupported: boolean;
|
|
}
|
|
): {
|
|
externalFiles: File[];
|
|
imageFiles: File[];
|
|
imagesHandledAsFiles: boolean;
|
|
} {
|
|
const { imageFiles, regularFiles } =
|
|
classifyAgentRichTextExternalFiles(dataTransfer);
|
|
const imagesHandledAsFiles =
|
|
imageFiles.length > 0 &&
|
|
!options.promptImagesSupported &&
|
|
options.externalFilesSupported;
|
|
return {
|
|
externalFiles: options.externalFilesSupported
|
|
? [...regularFiles, ...(imagesHandledAsFiles ? imageFiles : [])]
|
|
: [],
|
|
imageFiles,
|
|
imagesHandledAsFiles
|
|
};
|
|
}
|
|
|
|
export function systemFileDragInfoFromDataTransfer(
|
|
dataTransfer: DataTransfer | null
|
|
): { hasImageFiles: boolean; hasRegularFiles: boolean } {
|
|
if (!dataTransfer) {
|
|
return { hasImageFiles: false, hasRegularFiles: false };
|
|
}
|
|
const items = (dataTransfer as { items?: DataTransferItemList | null }).items;
|
|
if (items) {
|
|
let hasImageFiles = false;
|
|
let hasRegularFiles = false;
|
|
for (const item of Array.from(items)) {
|
|
if (item.kind !== "file") {
|
|
continue;
|
|
}
|
|
if (supportedPromptImageMimeType(item.type)) {
|
|
hasImageFiles = true;
|
|
} else {
|
|
hasRegularFiles = true;
|
|
}
|
|
}
|
|
return { hasImageFiles, hasRegularFiles };
|
|
}
|
|
let hasImageFiles = false;
|
|
let hasRegularFiles = false;
|
|
for (const file of Array.from(dataTransfer.files ?? [])) {
|
|
if (
|
|
supportedPromptImageMimeType(file.type) ||
|
|
supportedPromptImageFileName(file.name)
|
|
) {
|
|
hasImageFiles = true;
|
|
} else {
|
|
hasRegularFiles = true;
|
|
}
|
|
}
|
|
return { hasImageFiles, hasRegularFiles };
|
|
}
|
|
|
|
export function supportedPromptImageMimeType(
|
|
value: string
|
|
): value is AgentRichTextPromptImage["mimeType"] {
|
|
return (
|
|
value === "image/png" || value === "image/jpeg" || value === "image/webp"
|
|
);
|
|
}
|
|
|
|
function supportedPromptImageFileName(value: string): boolean {
|
|
return /\.(png|jpe?g|webp)$/i.test(value.trim());
|
|
}
|
|
|
|
export async function readAgentRichTextPromptImages(
|
|
files: readonly File[]
|
|
): Promise<AgentRichTextPromptImage[]> {
|
|
const images = await Promise.all(
|
|
files.map(async (file) => {
|
|
if (!supportedPromptImageMimeType(file.type)) {
|
|
return null;
|
|
}
|
|
const data = await fileToBase64(file);
|
|
return {
|
|
name: file.name || "clipboard-image",
|
|
mimeType: file.type,
|
|
data
|
|
};
|
|
})
|
|
);
|
|
return images.filter((image): image is AgentRichTextPromptImage =>
|
|
Boolean(image)
|
|
);
|
|
}
|
|
|
|
function fileToBase64(file: File): Promise<string> {
|
|
return new Promise((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
reader.onerror = () => reject(reader.error ?? new Error("read failed"));
|
|
reader.onload = () => {
|
|
const result = typeof reader.result === "string" ? reader.result : "";
|
|
resolve(result.includes(",") ? (result.split(",").pop() ?? "") : result);
|
|
};
|
|
reader.readAsDataURL(file);
|
|
});
|
|
}
|