fix(frontend): stop matching <header> as <head> in HTML preview base injection (#4625)

appendHtmlPreviewBaseHref detected the head tag with /<head[^>]*>/i,
which also matches <header ...>. For a fragment with no <head> that
opens with <header> - a common shape in agent-generated report pages -
the <base> element was injected after the <header> opening tag instead
of being prepended, so relative assets appearing before that point
(e.g. a leading <img>) resolved without the base and failed to load in
the sandboxed iframe.

Use the word-boundary-safe /<head(?:\s[^>]*)?>/i that the sibling
appendHtmlPreviewScrollRestoration already uses, keeping the two
injectors consistent.
This commit is contained in:
Baldwinzc 2026-08-01 21:59:27 +08:00 committed by GitHub
parent 5bf3e3960d
commit 2f6cadb8b0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 41 additions and 2 deletions

View File

@ -175,8 +175,14 @@ export function appendHtmlPreviewBaseHref(
const baseHref = htmlBaseHref(url, currentHref);
const baseElement = `<base href="${escapeHtmlAttribute(baseHref)}">`;
if (/<head[^>]*>/i.exec(content)) {
return content.replace(/<head([^>]*)>/i, `<head$1>${baseElement}`);
// "(?:\s[^>]*)?" keeps the tag-name boundary so `<header>` (a common
// leading tag in agent-generated fragments) is not mistaken for `<head>`;
// mirrors appendHtmlPreviewScrollRestoration below.
if (/<head(?:\s[^>]*)?>/i.test(content)) {
return content.replace(
/<head(?:\s[^>]*)?>/i,
(headTag) => `${headTag}${baseElement}`,
);
}
return `${baseElement}${content}`;
}

View File

@ -282,6 +282,39 @@ test("preserves existing head elements when injecting scroll restoration", () =>
);
});
test("does not mistake <header> for <head> when injecting the base href", () => {
// Agent-generated fragments often have no <head> but open with <header>;
// the base must then be prepended so assets before the tag resolve too.
const html =
'<img src="logo.png"><header class="top">Report</header><main>content</main>';
const result = appendHtmlPreviewBaseHref(
html,
"/demo/threads/thread-1/user-data/outputs/report.html",
"http://localhost/workspace/chats/thread-1",
);
expect(result).toBe(
'<base href="http://localhost/demo/threads/thread-1/user-data/outputs/">' +
html,
);
});
test("injects the base href inside an attributed <head> tag", () => {
const html =
'<!doctype html><html><head lang="en"><meta charset="utf-8"></head><body>x</body></html>';
const result = appendHtmlPreviewBaseHref(
html,
"/demo/threads/thread-1/user-data/outputs/report.html",
"http://localhost/workspace/chats/thread-1",
);
expect(result).toContain(
'<head lang="en"><base href="http://localhost/demo/threads/thread-1/user-data/outputs/">',
);
});
test("does not duplicate HTML scroll restoration script", () => {
const html = appendHtmlPreviewScrollRestoration(
"<html><body>x</body></html>",