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 baseHref = htmlBaseHref(url, currentHref);
const baseElement = `<base href="${escapeHtmlAttribute(baseHref)}">`; const baseElement = `<base href="${escapeHtmlAttribute(baseHref)}">`;
if (/<head[^>]*>/i.exec(content)) { // "(?:\s[^>]*)?" keeps the tag-name boundary so `<header>` (a common
return content.replace(/<head([^>]*)>/i, `<head$1>${baseElement}`); // 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}`; 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", () => { test("does not duplicate HTML scroll restoration script", () => {
const html = appendHtmlPreviewScrollRestoration( const html = appendHtmlPreviewScrollRestoration(
"<html><body>x</body></html>", "<html><body>x</body></html>",