Compare commits

...
3 changed files with 40 additions and 21 deletions

No files matched your search

+24 -19
View File
@@ -483,9 +483,7 @@ export function Prompt(props: PromptProps) {
})
return
}
if (content?.mime === "text/plain") {
await pasteInputText(content.data, changed)
}
if (content?.mime === "text/plain") pasteInputText(content.data)
})
},
},
@@ -1434,12 +1432,23 @@ export function Prompt(props: PromptProps) {
return true
}
async function pasteInputText(text: string, changed: () => boolean) {
function pasteInputText(text: string) {
// Windows ConPTY/Terminal often sends CR-only newlines in bracketed paste.
const normalizedText = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n")
const pastedContent = normalizedText.trim()
const attachments = await resolvePastedAttachments(pastedContent, terminalEnvironment.platform)
if (changed()) return
if (attachments) {
// Plain text inserts now so keys from the same input batch, such as punctuation
// that commits a multi-character IME candidate, land after it.
const pending = resolvePastedAttachments(normalizedText, terminalEnvironment.platform)
if (!pending) {
insertPastedText(normalizedText)
return
}
void enqueuePaste(async (changed) => {
const attachments = await pending
if (changed()) return
if (!attachments) {
insertPastedText(normalizedText)
return
}
attachments.forEach((attachment) => {
if (attachment.type === "text") {
pasteText(attachment.content, `[SVG: ${attachment.filename || "image"}]`)
@@ -1447,9 +1456,11 @@ export function Prompt(props: PromptProps) {
}
pasteAttachment(attachment)
})
return
}
})
}
function insertPastedText(normalizedText: string) {
const pastedContent = normalizedText.trim()
const lineCount = (pastedContent.match(/\n/g)?.length ?? 0) + 1
if ((lineCount >= 3 || pastedContent.length > 150) && config.prompt?.paste !== "full") {
const extmark = input.extmarks.getAllForTypeId(promptPartTypeId).find((extmark) => {
@@ -1776,11 +1787,6 @@ export function Prompt(props: PromptProps) {
return
}
// Normalize line endings at the boundary
// Windows ConPTY/Terminal often sends CR-only newlines in bracketed paste
// Replace CRLF first, then any remaining CR
const normalizedText = decodePasteBytes(event.bytes).replace(/\r\n/g, "\n").replace(/\r/g, "\n")
// Windows Terminal <1.25 can surface image-only clipboard as an
// empty bracketed paste. Windows Terminal 1.25+ does not.
if (event.bytes.byteLength === 0) {
@@ -1788,11 +1794,10 @@ export function Prompt(props: PromptProps) {
return
}
// Once we cross an async boundary below, the terminal may perform its
// default paste unless we suppress it first and handle insertion ourselves.
// pasteInputText may insert after an async attachment check, so suppress
// the default paste and let it handle insertion.
event.preventDefault()
void enqueuePaste((changed) => pasteInputText(normalizedText, changed))
pasteInputText(decodePasteBytes(event.bytes))
}}
ref={(r: TextareaRenderable) => {
input = r
@@ -26,15 +26,20 @@ export function readLocalAttachment(file: string, maxBytes = MAX_LOCAL_ATTACHMEN
)
}
export async function resolvePastedAttachments(text: string, platform: string) {
// Attachments are recognized by extension, so plain text resolves to undefined synchronously.
export function resolvePastedAttachments(text: string, platform: string) {
const pastedContent = text.trim()
const filepath = normalizePastedFilepath(pastedContent, platform)
if (/^(https?):\/\//.test(filepath)) return undefined
const filepaths = parsePastedFilepaths(pastedContent, platform)
if (![filepath, ...filepaths].some((file) => path.extname(file).toLowerCase() in mimeTypes)) return undefined
return readPastedAttachments(filepath, filepaths)
}
async function readPastedAttachments(filepath: string, filepaths: string[]) {
const attachment = await readLocalAttachment(filepath)
const attachments = attachment ? [{ filepath, attachment }] : []
if (!attachment) {
const filepaths = parsePastedFilepaths(pastedContent, platform)
if (filepaths.length <= 1) return undefined
let remaining = MAX_LOCAL_ATTACHMENT_BYTES
for (const candidate of filepaths) {
@@ -57,6 +57,15 @@ describe("prompt local attachments", () => {
).toEqual([])
})
test("resolves pastes without attachment paths synchronously", () => {
for (const input of ["你好", "plain\r\ntext", "https://example.com/image.png", "/tmp/notes.txt", ""]) {
expect(resolvePastedAttachments(input, "linux")).toBeUndefined()
}
for (const input of ["/tmp/image.PNG", "file:///tmp/file.pdf", "/tmp/one.png /tmp/two.webp"]) {
expect(resolvePastedAttachments(input, "linux")).toBeInstanceOf(Promise)
}
})
test("reads SVG attachments as text", async () => {
expect(await readLocalAttachmentWith(files({ mime: "image/svg+xml", text: "<svg />" }), "/tmp/image.svg")).toEqual({
type: "text",