feat(app): disallow duplicate attachments (#39464)
This commit is contained in:
parent
5b11635d97
commit
eca5e68a5e
21 changed files with 124 additions and 3 deletions
|
|
@ -368,6 +368,7 @@ export function usePromptInputV2Controller(props: PromptInputV2ControllerProps):
|
||||||
title: language.t("prompt.toast.pasteUnsupported.title"),
|
title: language.t("prompt.toast.pasteUnsupported.title"),
|
||||||
description: language.t("prompt.toast.pasteUnsupported.description"),
|
description: language.t("prompt.toast.pasteUnsupported.description"),
|
||||||
}),
|
}),
|
||||||
|
duplicate: () => showToast({ title: language.t("prompt.toast.attachmentDuplicate.title") }),
|
||||||
onError: (error) =>
|
onError: (error) =>
|
||||||
showToast({
|
showToast({
|
||||||
variant: "error",
|
variant: "error",
|
||||||
|
|
|
||||||
|
|
@ -277,6 +277,7 @@ export const dict = {
|
||||||
"prompt.action.send": "إرسال",
|
"prompt.action.send": "إرسال",
|
||||||
"prompt.action.stop": "توقف",
|
"prompt.action.stop": "توقف",
|
||||||
"prompt.toast.pasteUnsupported.title": "مرفق غير مدعوم",
|
"prompt.toast.pasteUnsupported.title": "مرفق غير مدعوم",
|
||||||
|
"prompt.toast.attachmentDuplicate.title": "تم تحميل هذا الملف بالفعل",
|
||||||
"prompt.toast.pasteUnsupported.description": "يمكن إرفاق الصور أو ملفات PDF أو الملفات النصية فقط هنا.",
|
"prompt.toast.pasteUnsupported.description": "يمكن إرفاق الصور أو ملفات PDF أو الملفات النصية فقط هنا.",
|
||||||
"prompt.toast.modelAgentRequired.title": "حدد وكيلاً ونموذجاً",
|
"prompt.toast.modelAgentRequired.title": "حدد وكيلاً ونموذجاً",
|
||||||
"prompt.toast.modelAgentRequired.description": "اختر وكيلاً ونموذجاً قبل إرسال الموجه.",
|
"prompt.toast.modelAgentRequired.description": "اختر وكيلاً ونموذجاً قبل إرسال الموجه.",
|
||||||
|
|
|
||||||
|
|
@ -277,6 +277,7 @@ export const dict = {
|
||||||
"prompt.action.send": "Enviar",
|
"prompt.action.send": "Enviar",
|
||||||
"prompt.action.stop": "Parar",
|
"prompt.action.stop": "Parar",
|
||||||
"prompt.toast.pasteUnsupported.title": "Anexo não suportado",
|
"prompt.toast.pasteUnsupported.title": "Anexo não suportado",
|
||||||
|
"prompt.toast.attachmentDuplicate.title": "Este arquivo já foi enviado",
|
||||||
"prompt.toast.pasteUnsupported.description": "Apenas imagens, PDFs ou arquivos de texto podem ser anexados aqui.",
|
"prompt.toast.pasteUnsupported.description": "Apenas imagens, PDFs ou arquivos de texto podem ser anexados aqui.",
|
||||||
"prompt.toast.modelAgentRequired.title": "Selecione um agente e modelo",
|
"prompt.toast.modelAgentRequired.title": "Selecione um agente e modelo",
|
||||||
"prompt.toast.modelAgentRequired.description": "Escolha um agente e modelo antes de enviar um prompt.",
|
"prompt.toast.modelAgentRequired.description": "Escolha um agente e modelo antes de enviar um prompt.",
|
||||||
|
|
|
||||||
|
|
@ -298,6 +298,7 @@ export const dict = {
|
||||||
"prompt.action.stop": "Zaustavi",
|
"prompt.action.stop": "Zaustavi",
|
||||||
|
|
||||||
"prompt.toast.pasteUnsupported.title": "Nepodržan prilog",
|
"prompt.toast.pasteUnsupported.title": "Nepodržan prilog",
|
||||||
|
"prompt.toast.attachmentDuplicate.title": "Ova datoteka je već učitana",
|
||||||
"prompt.toast.pasteUnsupported.description": "Ovdje se mogu priložiti samo slike, PDF-ovi ili tekstualne datoteke.",
|
"prompt.toast.pasteUnsupported.description": "Ovdje se mogu priložiti samo slike, PDF-ovi ili tekstualne datoteke.",
|
||||||
"prompt.toast.modelAgentRequired.title": "Odaberi agenta i model",
|
"prompt.toast.modelAgentRequired.title": "Odaberi agenta i model",
|
||||||
"prompt.toast.modelAgentRequired.description": "Odaberi agenta i model prije slanja upita.",
|
"prompt.toast.modelAgentRequired.description": "Odaberi agenta i model prije slanja upita.",
|
||||||
|
|
|
||||||
|
|
@ -296,6 +296,7 @@ export const dict = {
|
||||||
"prompt.action.stop": "Stop",
|
"prompt.action.stop": "Stop",
|
||||||
|
|
||||||
"prompt.toast.pasteUnsupported.title": "Ikke understøttet vedhæftning",
|
"prompt.toast.pasteUnsupported.title": "Ikke understøttet vedhæftning",
|
||||||
|
"prompt.toast.attachmentDuplicate.title": "Denne fil er allerede uploadet",
|
||||||
"prompt.toast.pasteUnsupported.description": "Kun billeder, PDF'er eller tekstfiler kan vedhæftes her.",
|
"prompt.toast.pasteUnsupported.description": "Kun billeder, PDF'er eller tekstfiler kan vedhæftes her.",
|
||||||
"prompt.toast.modelAgentRequired.title": "Vælg en agent og model",
|
"prompt.toast.modelAgentRequired.title": "Vælg en agent og model",
|
||||||
"prompt.toast.modelAgentRequired.description": "Vælg en agent og model før du sender en forespørgsel.",
|
"prompt.toast.modelAgentRequired.description": "Vælg en agent og model før du sender en forespørgsel.",
|
||||||
|
|
|
||||||
|
|
@ -282,6 +282,7 @@ export const dict = {
|
||||||
"prompt.action.send": "Senden",
|
"prompt.action.send": "Senden",
|
||||||
"prompt.action.stop": "Stopp",
|
"prompt.action.stop": "Stopp",
|
||||||
"prompt.toast.pasteUnsupported.title": "Nicht unterstützter Anhang",
|
"prompt.toast.pasteUnsupported.title": "Nicht unterstützter Anhang",
|
||||||
|
"prompt.toast.attachmentDuplicate.title": "Diese Datei wurde bereits hochgeladen",
|
||||||
"prompt.toast.pasteUnsupported.description": "Hier können nur Bilder, PDFs oder Textdateien angehängt werden.",
|
"prompt.toast.pasteUnsupported.description": "Hier können nur Bilder, PDFs oder Textdateien angehängt werden.",
|
||||||
"prompt.toast.modelAgentRequired.title": "Wählen Sie einen Agenten und ein Modell",
|
"prompt.toast.modelAgentRequired.title": "Wählen Sie einen Agenten und ein Modell",
|
||||||
"prompt.toast.modelAgentRequired.description":
|
"prompt.toast.modelAgentRequired.description":
|
||||||
|
|
|
||||||
|
|
@ -301,6 +301,7 @@ export const dict = {
|
||||||
|
|
||||||
"prompt.toast.pasteUnsupported.title": "Unsupported attachment",
|
"prompt.toast.pasteUnsupported.title": "Unsupported attachment",
|
||||||
"prompt.toast.pasteUnsupported.description": "Only images, PDFs, or text files can be attached here.",
|
"prompt.toast.pasteUnsupported.description": "Only images, PDFs, or text files can be attached here.",
|
||||||
|
"prompt.toast.attachmentDuplicate.title": "This file has already been uploaded",
|
||||||
"prompt.toast.modelAgentRequired.title": "Select an agent and model",
|
"prompt.toast.modelAgentRequired.title": "Select an agent and model",
|
||||||
"prompt.toast.modelAgentRequired.description": "Choose an agent and model before sending a prompt.",
|
"prompt.toast.modelAgentRequired.description": "Choose an agent and model before sending a prompt.",
|
||||||
"prompt.toast.worktreeCreateFailed.title": "Failed to create worktree",
|
"prompt.toast.worktreeCreateFailed.title": "Failed to create worktree",
|
||||||
|
|
|
||||||
|
|
@ -297,6 +297,7 @@ export const dict = {
|
||||||
"prompt.action.stop": "Detener",
|
"prompt.action.stop": "Detener",
|
||||||
|
|
||||||
"prompt.toast.pasteUnsupported.title": "Adjunto no compatible",
|
"prompt.toast.pasteUnsupported.title": "Adjunto no compatible",
|
||||||
|
"prompt.toast.attachmentDuplicate.title": "Este archivo ya se ha subido",
|
||||||
"prompt.toast.pasteUnsupported.description": "Solo se pueden adjuntar imágenes, PDFs o archivos de texto aquí.",
|
"prompt.toast.pasteUnsupported.description": "Solo se pueden adjuntar imágenes, PDFs o archivos de texto aquí.",
|
||||||
"prompt.toast.modelAgentRequired.title": "Selecciona un agente y modelo",
|
"prompt.toast.modelAgentRequired.title": "Selecciona un agente y modelo",
|
||||||
"prompt.toast.modelAgentRequired.description": "Elige un agente y modelo antes de enviar un prompt.",
|
"prompt.toast.modelAgentRequired.description": "Elige un agente y modelo antes de enviar un prompt.",
|
||||||
|
|
|
||||||
|
|
@ -277,6 +277,7 @@ export const dict = {
|
||||||
"prompt.action.send": "Envoyer",
|
"prompt.action.send": "Envoyer",
|
||||||
"prompt.action.stop": "Arrêter",
|
"prompt.action.stop": "Arrêter",
|
||||||
"prompt.toast.pasteUnsupported.title": "Pièce jointe non prise en charge",
|
"prompt.toast.pasteUnsupported.title": "Pièce jointe non prise en charge",
|
||||||
|
"prompt.toast.attachmentDuplicate.title": "Ce fichier a déjà été téléversé",
|
||||||
"prompt.toast.pasteUnsupported.description":
|
"prompt.toast.pasteUnsupported.description":
|
||||||
"Seules les images, les PDF ou les fichiers texte peuvent être joints ici.",
|
"Seules les images, les PDF ou les fichiers texte peuvent être joints ici.",
|
||||||
"prompt.toast.modelAgentRequired.title": "Sélectionnez un agent et un modèle",
|
"prompt.toast.modelAgentRequired.title": "Sélectionnez un agent et un modèle",
|
||||||
|
|
|
||||||
|
|
@ -276,6 +276,7 @@ export const dict = {
|
||||||
"prompt.action.send": "送信",
|
"prompt.action.send": "送信",
|
||||||
"prompt.action.stop": "停止",
|
"prompt.action.stop": "停止",
|
||||||
"prompt.toast.pasteUnsupported.title": "サポートされていない添付ファイル",
|
"prompt.toast.pasteUnsupported.title": "サポートされていない添付ファイル",
|
||||||
|
"prompt.toast.attachmentDuplicate.title": "このファイルはすでにアップロードされています",
|
||||||
"prompt.toast.pasteUnsupported.description": "画像、PDF、またはテキストファイルのみ添付できます。",
|
"prompt.toast.pasteUnsupported.description": "画像、PDF、またはテキストファイルのみ添付できます。",
|
||||||
"prompt.toast.modelAgentRequired.title": "エージェントとモデルを選択",
|
"prompt.toast.modelAgentRequired.title": "エージェントとモデルを選択",
|
||||||
"prompt.toast.modelAgentRequired.description": "プロンプトを送信する前にエージェントとモデルを選択してください。",
|
"prompt.toast.modelAgentRequired.description": "プロンプトを送信する前にエージェントとモデルを選択してください。",
|
||||||
|
|
|
||||||
|
|
@ -264,6 +264,7 @@ export const dict = {
|
||||||
"prompt.action.send": "전송",
|
"prompt.action.send": "전송",
|
||||||
"prompt.action.stop": "중지",
|
"prompt.action.stop": "중지",
|
||||||
"prompt.toast.pasteUnsupported.title": "지원되지 않는 첨부 파일",
|
"prompt.toast.pasteUnsupported.title": "지원되지 않는 첨부 파일",
|
||||||
|
"prompt.toast.attachmentDuplicate.title": "이 파일은 이미 업로드되었습니다",
|
||||||
"prompt.toast.pasteUnsupported.description": "이미지, PDF 또는 텍스트 파일만 첨부할 수 있습니다.",
|
"prompt.toast.pasteUnsupported.description": "이미지, PDF 또는 텍스트 파일만 첨부할 수 있습니다.",
|
||||||
"prompt.toast.modelAgentRequired.title": "에이전트 및 모델 선택",
|
"prompt.toast.modelAgentRequired.title": "에이전트 및 모델 선택",
|
||||||
"prompt.toast.modelAgentRequired.description": "프롬프트를 보내기 전에 에이전트와 모델을 선택하세요.",
|
"prompt.toast.modelAgentRequired.description": "프롬프트를 보내기 전에 에이전트와 모델을 선택하세요.",
|
||||||
|
|
|
||||||
|
|
@ -288,6 +288,7 @@ export const dict = {
|
||||||
"prompt.action.stop": "Stopp",
|
"prompt.action.stop": "Stopp",
|
||||||
|
|
||||||
"prompt.toast.pasteUnsupported.title": "Ikke støttet vedlegg",
|
"prompt.toast.pasteUnsupported.title": "Ikke støttet vedlegg",
|
||||||
|
"prompt.toast.attachmentDuplicate.title": "Denne filen er allerede lastet opp",
|
||||||
"prompt.toast.pasteUnsupported.description": "Kun bilder, PDF-er eller tekstfiler kan legges ved her.",
|
"prompt.toast.pasteUnsupported.description": "Kun bilder, PDF-er eller tekstfiler kan legges ved her.",
|
||||||
"prompt.toast.modelAgentRequired.title": "Velg en agent og modell",
|
"prompt.toast.modelAgentRequired.title": "Velg en agent og modell",
|
||||||
"prompt.toast.modelAgentRequired.description": "Velg en agent og modell før du sender en forespørsel.",
|
"prompt.toast.modelAgentRequired.description": "Velg en agent og modell før du sender en forespørsel.",
|
||||||
|
|
|
||||||
|
|
@ -278,6 +278,7 @@ export const dict = {
|
||||||
"prompt.action.send": "Wyślij",
|
"prompt.action.send": "Wyślij",
|
||||||
"prompt.action.stop": "Zatrzymaj",
|
"prompt.action.stop": "Zatrzymaj",
|
||||||
"prompt.toast.pasteUnsupported.title": "Nieobsługiwany załącznik",
|
"prompt.toast.pasteUnsupported.title": "Nieobsługiwany załącznik",
|
||||||
|
"prompt.toast.attachmentDuplicate.title": "Ten plik został już przesłany",
|
||||||
"prompt.toast.pasteUnsupported.description": "Można tutaj załączać tylko obrazy, pliki PDF lub pliki tekstowe.",
|
"prompt.toast.pasteUnsupported.description": "Można tutaj załączać tylko obrazy, pliki PDF lub pliki tekstowe.",
|
||||||
"prompt.toast.modelAgentRequired.title": "Wybierz agenta i model",
|
"prompt.toast.modelAgentRequired.title": "Wybierz agenta i model",
|
||||||
"prompt.toast.modelAgentRequired.description": "Wybierz agenta i model przed wysłaniem zapytania.",
|
"prompt.toast.modelAgentRequired.description": "Wybierz agenta i model przed wysłaniem zapytania.",
|
||||||
|
|
|
||||||
|
|
@ -297,6 +297,7 @@ export const dict = {
|
||||||
"prompt.action.stop": "Остановить",
|
"prompt.action.stop": "Остановить",
|
||||||
|
|
||||||
"prompt.toast.pasteUnsupported.title": "Неподдерживаемое вложение",
|
"prompt.toast.pasteUnsupported.title": "Неподдерживаемое вложение",
|
||||||
|
"prompt.toast.attachmentDuplicate.title": "Этот файл уже загружен",
|
||||||
"prompt.toast.pasteUnsupported.description": "Здесь можно прикрепить только изображения, PDF или текстовые файлы.",
|
"prompt.toast.pasteUnsupported.description": "Здесь можно прикрепить только изображения, PDF или текстовые файлы.",
|
||||||
"prompt.toast.modelAgentRequired.title": "Выберите агента и модель",
|
"prompt.toast.modelAgentRequired.title": "Выберите агента и модель",
|
||||||
"prompt.toast.modelAgentRequired.description": "Выберите агента и модель перед отправкой запроса.",
|
"prompt.toast.modelAgentRequired.description": "Выберите агента и модель перед отправкой запроса.",
|
||||||
|
|
|
||||||
|
|
@ -297,6 +297,7 @@ export const dict = {
|
||||||
"prompt.action.stop": "หยุด",
|
"prompt.action.stop": "หยุด",
|
||||||
|
|
||||||
"prompt.toast.pasteUnsupported.title": "ไฟล์แนบที่ไม่รองรับ",
|
"prompt.toast.pasteUnsupported.title": "ไฟล์แนบที่ไม่รองรับ",
|
||||||
|
"prompt.toast.attachmentDuplicate.title": "ไฟล์นี้ถูกอัปโหลดแล้ว",
|
||||||
"prompt.toast.pasteUnsupported.description": "แนบได้เฉพาะรูปภาพ, PDF หรือไฟล์ข้อความเท่านั้น",
|
"prompt.toast.pasteUnsupported.description": "แนบได้เฉพาะรูปภาพ, PDF หรือไฟล์ข้อความเท่านั้น",
|
||||||
"prompt.toast.modelAgentRequired.title": "เลือกเอเจนต์และโมเดล",
|
"prompt.toast.modelAgentRequired.title": "เลือกเอเจนต์และโมเดล",
|
||||||
"prompt.toast.modelAgentRequired.description": "เลือกเอเจนต์และโมเดลก่อนส่งพร้อมท์",
|
"prompt.toast.modelAgentRequired.description": "เลือกเอเจนต์และโมเดลก่อนส่งพร้อมท์",
|
||||||
|
|
|
||||||
|
|
@ -302,6 +302,7 @@ export const dict = {
|
||||||
"prompt.action.stop": "Durdur",
|
"prompt.action.stop": "Durdur",
|
||||||
|
|
||||||
"prompt.toast.pasteUnsupported.title": "Desteklenmeyen ek",
|
"prompt.toast.pasteUnsupported.title": "Desteklenmeyen ek",
|
||||||
|
"prompt.toast.attachmentDuplicate.title": "Bu dosya zaten yüklendi",
|
||||||
"prompt.toast.pasteUnsupported.description": "Buraya yalnızca resimler, PDF'ler veya metin dosyaları eklenebilir.",
|
"prompt.toast.pasteUnsupported.description": "Buraya yalnızca resimler, PDF'ler veya metin dosyaları eklenebilir.",
|
||||||
"prompt.toast.modelAgentRequired.title": "Bir ajan ve model seçin",
|
"prompt.toast.modelAgentRequired.title": "Bir ajan ve model seçin",
|
||||||
"prompt.toast.modelAgentRequired.description": "Komut göndermeden önce bir ajan ve model seçin.",
|
"prompt.toast.modelAgentRequired.description": "Komut göndermeden önce bir ajan ve model seçin.",
|
||||||
|
|
|
||||||
|
|
@ -299,6 +299,7 @@ export const dict = {
|
||||||
"prompt.action.stop": "Зупинити",
|
"prompt.action.stop": "Зупинити",
|
||||||
|
|
||||||
"prompt.toast.pasteUnsupported.title": "Непідтримуване вкладення",
|
"prompt.toast.pasteUnsupported.title": "Непідтримуване вкладення",
|
||||||
|
"prompt.toast.attachmentDuplicate.title": "Цей файл уже завантажено",
|
||||||
"prompt.toast.pasteUnsupported.description": "Сюди можна прикріплювати лише зображення, PDF або текстові файли.",
|
"prompt.toast.pasteUnsupported.description": "Сюди можна прикріплювати лише зображення, PDF або текстові файли.",
|
||||||
"prompt.toast.modelAgentRequired.title": "Виберіть агента та модель",
|
"prompt.toast.modelAgentRequired.title": "Виберіть агента та модель",
|
||||||
"prompt.toast.modelAgentRequired.description": "Виберіть агента та модель перед надсиланням запиту.",
|
"prompt.toast.modelAgentRequired.description": "Виберіть агента та модель перед надсиланням запиту.",
|
||||||
|
|
|
||||||
|
|
@ -316,6 +316,7 @@ export const dict = {
|
||||||
"prompt.action.send": "发送",
|
"prompt.action.send": "发送",
|
||||||
"prompt.action.stop": "停止",
|
"prompt.action.stop": "停止",
|
||||||
"prompt.toast.pasteUnsupported.title": "不支持的附件",
|
"prompt.toast.pasteUnsupported.title": "不支持的附件",
|
||||||
|
"prompt.toast.attachmentDuplicate.title": "此文件已上传",
|
||||||
"prompt.toast.pasteUnsupported.description": "此处仅能附加图片、PDF 或文本文件。",
|
"prompt.toast.pasteUnsupported.description": "此处仅能附加图片、PDF 或文本文件。",
|
||||||
"prompt.toast.modelAgentRequired.title": "请选择智能体和模型",
|
"prompt.toast.modelAgentRequired.title": "请选择智能体和模型",
|
||||||
"prompt.toast.modelAgentRequired.description": "发送提示前请先选择智能体和模型。",
|
"prompt.toast.modelAgentRequired.description": "发送提示前请先选择智能体和模型。",
|
||||||
|
|
|
||||||
|
|
@ -297,6 +297,7 @@ export const dict = {
|
||||||
"prompt.action.stop": "停止",
|
"prompt.action.stop": "停止",
|
||||||
|
|
||||||
"prompt.toast.pasteUnsupported.title": "不支援的附件",
|
"prompt.toast.pasteUnsupported.title": "不支援的附件",
|
||||||
|
"prompt.toast.attachmentDuplicate.title": "此檔案已上傳",
|
||||||
"prompt.toast.pasteUnsupported.description": "此處僅能附加圖片、PDF 或文字檔案。",
|
"prompt.toast.pasteUnsupported.description": "此處僅能附加圖片、PDF 或文字檔案。",
|
||||||
"prompt.toast.modelAgentRequired.title": "請選擇代理程式和模型",
|
"prompt.toast.modelAgentRequired.title": "請選擇代理程式和模型",
|
||||||
"prompt.toast.modelAgentRequired.description": "傳送提示前請先選擇代理程式和模型。",
|
"prompt.toast.modelAgentRequired.description": "傳送提示前請先選擇代理程式和模型。",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import { createRoot } from "solid-js"
|
import { createRoot } from "solid-js"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
import { createPromptAttachmentsCore } from "@/components/prompt-input/attachments"
|
import { createPromptAttachmentsCore } from "@/components/prompt-input/attachments"
|
||||||
import { createPromptState } from "@/context/prompt"
|
import { createPromptState } from "@/context/prompt"
|
||||||
|
import { createPromptInputV2Attachments } from "../../session-ui/src/v2/components/prompt-input/attachments"
|
||||||
|
import type { PromptInputV2Prompt } from "../../session-ui/src/v2/components/prompt-input/types"
|
||||||
|
|
||||||
describe("prompt attachment session ownership", () => {
|
describe("prompt attachment session ownership", () => {
|
||||||
test("adds an asynchronously read image to the session where the read started", async () => {
|
test("adds an asynchronously read image to the session where the read started", async () => {
|
||||||
|
|
@ -83,6 +86,89 @@ describe("prompt attachment session ownership", () => {
|
||||||
dispose()
|
dispose()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
test("rejects a duplicate native clipboard attachment in the V2 prompt store", async () => {
|
||||||
|
await createRoot(async (dispose) => {
|
||||||
|
const [state, setState] = createStore({ prompt: [] as PromptInputV2Prompt })
|
||||||
|
const duplicate = Promise.withResolvers<void>()
|
||||||
|
const files = [
|
||||||
|
new File(["hello"], "clipboard-1.txt", { type: "text/plain" }),
|
||||||
|
new File(["hello"], "clipboard-2.txt", { type: "text/plain" }),
|
||||||
|
]
|
||||||
|
const attachments = createPromptInputV2Attachments({
|
||||||
|
capture: () => ({
|
||||||
|
current: () => state.prompt,
|
||||||
|
cursor: () => 0,
|
||||||
|
set: (prompt) => setState("prompt", prompt),
|
||||||
|
}),
|
||||||
|
editor: () => document.createElement("div"),
|
||||||
|
focusEditor: () => undefined,
|
||||||
|
addPart: () => false,
|
||||||
|
setDraggingType: () => undefined,
|
||||||
|
directory: () => "/",
|
||||||
|
isDialogActive: () => false,
|
||||||
|
warn: () => undefined,
|
||||||
|
duplicate: duplicate.resolve,
|
||||||
|
onError: () => undefined,
|
||||||
|
readClipboardImage: async () => files.shift() ?? null,
|
||||||
|
})
|
||||||
|
const event = {
|
||||||
|
clipboardData: { items: [], getData: () => "" },
|
||||||
|
preventDefault: () => undefined,
|
||||||
|
stopPropagation: () => undefined,
|
||||||
|
} as unknown as ClipboardEvent
|
||||||
|
|
||||||
|
await attachments.handlePaste(event)
|
||||||
|
await attachments.handlePaste(event)
|
||||||
|
await duplicate.promise
|
||||||
|
|
||||||
|
expect(state.prompt).toHaveLength(1)
|
||||||
|
dispose()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("rejects desktop duplicates and keeps changed files in the V2 prompt store", async () => {
|
||||||
|
await createRoot(async (dispose) => {
|
||||||
|
const [state, setState] = createStore({ prompt: [] as PromptInputV2Prompt })
|
||||||
|
const duplicates: string[] = []
|
||||||
|
const attachments = createPromptInputV2Attachments({
|
||||||
|
capture: () => ({
|
||||||
|
current: () => state.prompt,
|
||||||
|
cursor: () => 0,
|
||||||
|
set: (prompt) => setState("prompt", prompt),
|
||||||
|
}),
|
||||||
|
editor: () => document.createElement("div"),
|
||||||
|
focusEditor: () => undefined,
|
||||||
|
addPart: () => false,
|
||||||
|
setDraggingType: () => undefined,
|
||||||
|
directory: () => "/",
|
||||||
|
isDialogActive: () => false,
|
||||||
|
warn: () => undefined,
|
||||||
|
duplicate: () => duplicates.push("duplicate"),
|
||||||
|
onError: () => undefined,
|
||||||
|
getPathForFile: (file) => (file.name === "browser.txt" ? "" : `/tmp/${file.name}`),
|
||||||
|
})
|
||||||
|
const first = new File(["first"], "a.txt", { type: "text/plain" })
|
||||||
|
const second = new File(["second"], "b.txt", { type: "text/plain" })
|
||||||
|
|
||||||
|
await attachments.addAttachments([first, second])
|
||||||
|
await attachments.addAttachments([first, second])
|
||||||
|
expect(state.prompt).toHaveLength(2)
|
||||||
|
expect(duplicates).toEqual(["duplicate", "duplicate"])
|
||||||
|
|
||||||
|
await attachments.addAttachments([new File(["edited"], "a.txt", { type: "text/plain" })])
|
||||||
|
expect(state.prompt).toHaveLength(3)
|
||||||
|
|
||||||
|
await attachments.addAttachments([
|
||||||
|
new File(["same"], "browser.txt", { type: "text/plain" }),
|
||||||
|
new File(["same"], "browser.txt", { type: "text/plain" }),
|
||||||
|
])
|
||||||
|
expect(state.prompt).toHaveLength(4)
|
||||||
|
expect(duplicates).toHaveLength(3)
|
||||||
|
dispose()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
function images(prompt: ReturnType<typeof createPromptState>) {
|
function images(prompt: ReturnType<typeof createPromptState>) {
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,7 @@ export type PromptInputV2AttachmentConfig = {
|
||||||
directory: () => string
|
directory: () => string
|
||||||
isDialogActive: () => boolean
|
isDialogActive: () => boolean
|
||||||
warn: () => void
|
warn: () => void
|
||||||
|
duplicate: () => void
|
||||||
onError: (error: unknown) => void
|
onError: (error: unknown) => void
|
||||||
readClipboardImage?: () => Promise<File | null>
|
readClipboardImage?: () => Promise<File | null>
|
||||||
getPathForFile?: (file: File) => string
|
getPathForFile?: (file: File) => string
|
||||||
|
|
@ -94,7 +95,7 @@ export function createPromptInputV2Attachments(
|
||||||
if (!editor) return
|
if (!editor) return
|
||||||
return { prompt, cursor: prompt.cursor() ?? cursorPosition(editor) }
|
return { prompt, cursor: prompt.cursor() ?? cursorPosition(editor) }
|
||||||
}
|
}
|
||||||
const add = async (file: File, toast = true, target = capture()) => {
|
const add = async (file: File, toast = true, target = capture(), clipboard = false) => {
|
||||||
if (!target) return false
|
if (!target) return false
|
||||||
const mime = await attachmentMime(file)
|
const mime = await attachmentMime(file)
|
||||||
if (!mime) {
|
if (!mime) {
|
||||||
|
|
@ -103,11 +104,26 @@ export function createPromptInputV2Attachments(
|
||||||
}
|
}
|
||||||
const url = await dataUrl(file, mime)
|
const url = await dataUrl(file, mime)
|
||||||
if (!url) return false
|
if (!url) return false
|
||||||
|
const sourcePath = input.getPathForFile?.(file) || undefined
|
||||||
|
// Native clipboard images arrive with a fresh timestamped filename on every paste, so identical
|
||||||
|
// clipboard content is matched on bytes alone.
|
||||||
|
const duplicate = target.prompt
|
||||||
|
.current()
|
||||||
|
.some(
|
||||||
|
(part) =>
|
||||||
|
part.type === "image" &&
|
||||||
|
part.dataUrl === url &&
|
||||||
|
(sourcePath ? part.sourcePath === sourcePath : !part.sourcePath && (clipboard || part.filename === file.name)),
|
||||||
|
)
|
||||||
|
if (duplicate) {
|
||||||
|
input.duplicate()
|
||||||
|
return true
|
||||||
|
}
|
||||||
const attachment: PromptInputV2Attachment = {
|
const attachment: PromptInputV2Attachment = {
|
||||||
type: "image",
|
type: "image",
|
||||||
id: globalThis.crypto?.randomUUID?.() ?? Math.random().toString(16).slice(2),
|
id: globalThis.crypto?.randomUUID?.() ?? Math.random().toString(16).slice(2),
|
||||||
filename: file.name,
|
filename: file.name,
|
||||||
sourcePath: input.getPathForFile?.(file) || undefined,
|
sourcePath,
|
||||||
mime,
|
mime,
|
||||||
dataUrl: url,
|
dataUrl: url,
|
||||||
}
|
}
|
||||||
|
|
@ -141,7 +157,7 @@ export function createPromptInputV2Attachments(
|
||||||
const plainText = clipboardData.getData("text/plain") ?? ""
|
const plainText = clipboardData.getData("text/plain") ?? ""
|
||||||
if (input.readClipboardImage && !plainText) {
|
if (input.readClipboardImage && !plainText) {
|
||||||
const file = await input.readClipboardImage()
|
const file = await input.readClipboardImage()
|
||||||
if (file && (await add(file, true, target))) return
|
if (file && (await add(file, true, target, true))) return
|
||||||
}
|
}
|
||||||
if (!plainText) return
|
if (!plainText) return
|
||||||
const text = plainText.includes("\r") ? plainText.replace(/\r\n?/g, "\n") : plainText
|
const text = plainText.includes("\r") ? plainText.replace(/\r\n?/g, "\n") : plainText
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue