Better parsing of pasted html as markdown so agents understand (#9190)

Signed-off-by: Douwe Osinga <douwe@squareup.com>
Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
Douwe Osinga
2026-05-14 12:57:18 -04:00
committed by GitHub
parent 4671053985
commit ffd2349b0e
3 changed files with 88 additions and 25 deletions
+54 -1
View File
@@ -44,6 +44,29 @@ import { UserInput, ImageData } from '../types/message';
import { compressImageDataUrl } from '../utils/conversionUtils';
import { fetchCanonicalModelInfo } from '../utils/canonical';
import { defineMessages, useIntl } from '../i18n';
import TurndownService from 'turndown';
const turndown = new TurndownService({
headingStyle: 'atx',
bulletListMarker: '-',
codeBlockStyle: 'fenced',
});
turndown.addRule('complexLinks', {
filter: (node) => {
return (
node.nodeName === 'A' &&
!!node.getAttribute('href') &&
/\n/.test(node.textContent || '')
);
},
replacement: (content, node) => {
const el = node as HTMLElement;
const href = el.getAttribute('href')!;
const label = content.replace(/\n+/g, ' ').trim();
return `[${label}](${href})`;
},
});
interface PastedImage {
id: string;
@@ -805,10 +828,40 @@ export default function ChatInput({
}, [droppedFiles.length, localDroppedFiles.length, onFilesProcessed, setLocalDroppedFiles]);
const handlePaste = async (evt: React.ClipboardEvent<HTMLTextAreaElement>) => {
if (isRecording) return;
const files = Array.from(evt.clipboardData.files || []);
const imageFiles = files.filter((file) => file.type.startsWith('image/'));
if (imageFiles.length === 0) return;
if (imageFiles.length === 0) {
const html = evt.clipboardData.getData('text/html');
if (html) {
const doc = new DOMParser().parseFromString(html, 'text/html');
const hasLinks = doc.querySelectorAll('a[href]').length > 0;
if (hasLinks) {
const markdown = turndown.turndown(doc.body).trim();
if (markdown) {
evt.preventDefault();
const textarea = textAreaRef.current;
if (textarea) {
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const newValue =
displayValue.substring(0, start) + markdown + displayValue.substring(end);
const cursorPos = start + markdown.length;
setDisplayValue(newValue);
updateValue(newValue);
setHasUserTyped(true);
checkForMentionOrSlash(newValue, cursorPos, textarea);
requestAnimationFrame(() => {
textarea.selectionStart = textarea.selectionEnd = cursorPos;
});
}
}
}
}
return;
}
// Check if adding these images would exceed the limit
if (pastedImages.length + imageFiles.length > MAX_IMAGES_PER_MESSAGE) {