mirror of
https://github.com/anomalyco/opencode.git
synced 2026-04-22 13:54:50 +00:00
Compare commits
2 Commits
kit/file-h
...
opencode/s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32ba9287b6 | ||
|
|
3c8b069bba |
@@ -1,5 +1,6 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import { attachmentMime } from "./files"
|
import { attachmentMime } from "./files"
|
||||||
|
import { MAX_ATTACHMENT_BYTES, estimateAttachment, totalAttachments, wouldExceedAttachmentLimit } from "./limit"
|
||||||
import { pasteMode } from "./paste"
|
import { pasteMode } from "./paste"
|
||||||
|
|
||||||
describe("attachmentMime", () => {
|
describe("attachmentMime", () => {
|
||||||
@@ -42,3 +43,19 @@ describe("pasteMode", () => {
|
|||||||
expect(pasteMode("x".repeat(8000))).toBe("manual")
|
expect(pasteMode("x".repeat(8000))).toBe("manual")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("attachment limit", () => {
|
||||||
|
test("estimates encoded attachment size", () => {
|
||||||
|
expect(estimateAttachment({ size: 3 }, "image/png")).toBe("data:image/png;base64,".length + 4)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("totals current attachments", () => {
|
||||||
|
expect(totalAttachments([{ dataUrl: "abc" }, { dataUrl: "de" }])).toBe(5)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("flags uploads that exceed the total limit", () => {
|
||||||
|
const list = [{ dataUrl: "a".repeat(MAX_ATTACHMENT_BYTES - 4) }]
|
||||||
|
expect(wouldExceedAttachmentLimit(list, { size: 3 }, "image/png")).toBe(true)
|
||||||
|
expect(wouldExceedAttachmentLimit([], { size: 3 }, "image/png")).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useLanguage } from "@/context/language"
|
|||||||
import { uuid } from "@/utils/uuid"
|
import { uuid } from "@/utils/uuid"
|
||||||
import { getCursorPosition } from "./editor-dom"
|
import { getCursorPosition } from "./editor-dom"
|
||||||
import { attachmentMime } from "./files"
|
import { attachmentMime } from "./files"
|
||||||
|
import { wouldExceedAttachmentLimit } from "./limit"
|
||||||
import { normalizePaste, pasteMode } from "./paste"
|
import { normalizePaste, pasteMode } from "./paste"
|
||||||
|
|
||||||
function dataUrl(file: File, mime: string) {
|
function dataUrl(file: File, mime: string) {
|
||||||
@@ -33,6 +34,8 @@ type PromptAttachmentsInput = {
|
|||||||
readClipboardImage?: () => Promise<File | null>
|
readClipboardImage?: () => Promise<File | null>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AddState = "added" | "failed" | "unsupported" | "limit"
|
||||||
|
|
||||||
export function createPromptAttachments(input: PromptAttachmentsInput) {
|
export function createPromptAttachments(input: PromptAttachmentsInput) {
|
||||||
const prompt = usePrompt()
|
const prompt = usePrompt()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
@@ -44,18 +47,27 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const add = async (file: File, toast = true) => {
|
const warnLimit = () => {
|
||||||
|
showToast({
|
||||||
|
title: language.t("prompt.toast.attachmentLimit.title"),
|
||||||
|
description: language.t("prompt.toast.attachmentLimit.description"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const add = async (file: File): Promise<AddState> => {
|
||||||
const mime = await attachmentMime(file)
|
const mime = await attachmentMime(file)
|
||||||
if (!mime) {
|
if (!mime) {
|
||||||
if (toast) warn()
|
return "unsupported" as const
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const editor = input.editor()
|
const editor = input.editor()
|
||||||
if (!editor) return false
|
if (!editor) return "failed" as const
|
||||||
|
|
||||||
|
const images = prompt.current().filter((part): part is ImageAttachmentPart => part.type === "image")
|
||||||
|
if (wouldExceedAttachmentLimit(images, file, mime)) return "limit" as const
|
||||||
|
|
||||||
const url = await dataUrl(file, mime)
|
const url = await dataUrl(file, mime)
|
||||||
if (!url) return false
|
if (!url) return "failed" as const
|
||||||
|
|
||||||
const attachment: ImageAttachmentPart = {
|
const attachment: ImageAttachmentPart = {
|
||||||
type: "image",
|
type: "image",
|
||||||
@@ -66,23 +78,26 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
|
|||||||
}
|
}
|
||||||
const cursor = prompt.cursor() ?? getCursorPosition(editor)
|
const cursor = prompt.cursor() ?? getCursorPosition(editor)
|
||||||
prompt.set([...prompt.current(), attachment], cursor)
|
prompt.set([...prompt.current(), attachment], cursor)
|
||||||
return true
|
return "added" as const
|
||||||
}
|
}
|
||||||
|
|
||||||
const addAttachment = (file: File) => add(file)
|
const addAttachments = async (list: File[]) => {
|
||||||
|
const result = { added: false, unsupported: false }
|
||||||
const addAttachments = async (files: File[], toast = true) => {
|
for (const file of list) {
|
||||||
let found = false
|
const state = await add(file)
|
||||||
|
if (state === "limit") {
|
||||||
for (const file of files) {
|
warnLimit()
|
||||||
const ok = await add(file, false)
|
return result.added
|
||||||
if (ok) found = true
|
}
|
||||||
|
result.added = result.added || state === "added"
|
||||||
|
result.unsupported = result.unsupported || state === "unsupported"
|
||||||
}
|
}
|
||||||
|
if (!result.added && result.unsupported) warn()
|
||||||
if (!found && files.length > 0 && toast) warn()
|
return result.added
|
||||||
return found
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addAttachment = (file: File) => addAttachments([file])
|
||||||
|
|
||||||
const removeAttachment = (id: string) => {
|
const removeAttachment = (id: string) => {
|
||||||
const current = prompt.current()
|
const current = prompt.current()
|
||||||
const next = current.filter((part) => part.type !== "image" || part.id !== id)
|
const next = current.filter((part) => part.type !== "image" || part.id !== id)
|
||||||
|
|||||||
15
packages/app/src/components/prompt-input/limit.ts
Normal file
15
packages/app/src/components/prompt-input/limit.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
const MB = 1024 * 1024
|
||||||
|
|
||||||
|
export const MAX_ATTACHMENT_BYTES = 5 * MB
|
||||||
|
|
||||||
|
export function estimateAttachment(file: { size: number }, mime: string) {
|
||||||
|
return `data:${mime};base64,`.length + Math.ceil(file.size / 3) * 4
|
||||||
|
}
|
||||||
|
|
||||||
|
export function totalAttachments(list: Array<{ dataUrl: string }>) {
|
||||||
|
return list.reduce((sum, part) => sum + part.dataUrl.length, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function wouldExceedAttachmentLimit(list: Array<{ dataUrl: string }>, file: { size: number }, mime: string) {
|
||||||
|
return totalAttachments(list) + estimateAttachment(file, mime) > MAX_ATTACHMENT_BYTES
|
||||||
|
}
|
||||||
@@ -281,6 +281,8 @@ export const dict = {
|
|||||||
"prompt.action.send": "Send",
|
"prompt.action.send": "Send",
|
||||||
"prompt.action.stop": "Stop",
|
"prompt.action.stop": "Stop",
|
||||||
|
|
||||||
|
"prompt.toast.attachmentLimit.title": "Attachment limit reached",
|
||||||
|
"prompt.toast.attachmentLimit.description": "Attachments can total up to 5 MB. Try smaller or fewer files.",
|
||||||
"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.modelAgentRequired.title": "Select an agent and model",
|
"prompt.toast.modelAgentRequired.title": "Select an agent and model",
|
||||||
|
|||||||
Reference in New Issue
Block a user