diff --git a/apps/server/src/mcp/toolkits/html/tools.ts b/apps/server/src/mcp/toolkits/html/tools.ts index b550e3b1299c..6160b9b1a3ab 100644 --- a/apps/server/src/mcp/toolkits/html/tools.ts +++ b/apps/server/src/mcp/toolkits/html/tools.ts @@ -68,7 +68,7 @@ export const HtmlPreviewTool = Tool.make("html_preview", { // touches no workspace, so plan mode and read-only sandboxes can use it. // Open-world, since the page may load remote resources, as in a preview. const HtmlRenderTool = Tool.make(HTML_RENDER_TOOL_NAME, { - description: `Show a finished HTML page (chart, table, diagram, collage, mockup) inline in this thread, above your final text reply; call it before writing that reply. The reader already sees the page, so the reply should not announce it, say where it is, or restate it: add only what the page doesn't say. Preview with html_preview first. T3 fits the frame to the page's height at each reader's width, up to height; anything taller scrolls inside the frame. ${PAGE_RULES} ${HTML_RENDER_LAYOUT_GUIDE} ${HTML_RENDER_THEME_GUIDE}`, + description: `Show a finished HTML page (chart, table, diagram, collage, mockup) inline in this thread, above your final text reply; call it before writing that reply. The reader already sees the page, so the reply should not announce it, say where it is, or restate it: add only what the page doesn't say. Preview with html_preview first. T3 fits the frame to the page's height at each reader's width. A height below the page's contentHeight caps the frame there, and the rest scrolls inside it. ${PAGE_RULES} ${HTML_RENDER_LAYOUT_GUIDE} ${HTML_RENDER_THEME_GUIDE}`, parameters: Schema.Struct({ html: Html, title: Schema.String.check( @@ -76,7 +76,7 @@ const HtmlRenderTool = Tool.make(HTML_RENDER_TOOL_NAME, { Schema.isMaxLength(HTML_RENDER_MAX_TITLE_LENGTH), ).annotate({ description: "Short name for the page." }), height: Schema.Int.annotate({ - description: `The most the frame may take, in CSS pixels, ${HTML_RENDER_MIN_HEIGHT}-${HTML_RENDER_MAX_HEIGHT}. Use html_preview's contentHeight, or less to make long content scroll inside the frame.`, + description: `The frame height in CSS pixels, ${HTML_RENDER_MIN_HEIGHT}-${HTML_RENDER_MAX_HEIGHT}. Use html_preview's contentHeight, or less to make long content scroll inside the frame.`, }), }), success: Schema.Struct({ diff --git a/apps/web/src/components/chat/HtmlRenderFrame.tsx b/apps/web/src/components/chat/HtmlRenderFrame.tsx index b92c2d60aa58..9661f59249d9 100644 --- a/apps/web/src/components/chat/HtmlRenderFrame.tsx +++ b/apps/web/src/components/chat/HtmlRenderFrame.tsx @@ -20,8 +20,9 @@ const MIN_URL_LIFE_MS = 5 * 60_000; /** * An agent's HTML render inline in the thread: the page itself on the thread's - * own background, at its measured height for this width (capped at the agent's - * height). Loading and failure hold the same box so nothing below it moves. + * own background, at the server's measured height for this width until the + * page reports its own. Loading and failure hold the same box so nothing below + * it moves. */ export function HtmlRenderFrame(props: { readonly environmentId: EnvironmentId; @@ -43,7 +44,10 @@ export function HtmlRenderFrame(props: { observer.observe(box); return () => observer.disconnect(); }, []); - const height = htmlRenderFrameHeight(props.htmlRender, width); + // Client fonts can wrap a page taller than the server measured it; a frame + // left short would scroll inside the thread and take the reader's scroll. + const [contentHeight, setContentHeight] = useState(); + const height = htmlRenderFrameHeight(props.htmlRender, width, contentHeight); const fileName = htmlRenderFileName(title); const resource = useMemo( () => ({ @@ -91,7 +95,12 @@ export function HtmlRenderFrame(props: {
{src !== null ? ( <> - +
void; }) { const theme = useHtmlRenderTheme(); const frameRef = useRef(null); @@ -102,6 +105,20 @@ export function HtmlRenderDocument(props: { window.addEventListener("message", openLink); return () => window.removeEventListener("message", openLink); }, []); + const { onContentHeight } = props; + // A page posts its height once per change, so listen from the commit that + // inserts the frame; a passive effect could run after a fast page's first post. + useLayoutEffect(() => { + if (onContentHeight === undefined) return; + const resize = (event: MessageEvent) => { + const height = readHtmlRenderContentHeight(event.data); + if (height !== undefined && event.source === frameRef.current?.contentWindow) { + onContentHeight(height); + } + }; + window.addEventListener("message", resize); + return () => window.removeEventListener("message", resize); + }, [onContentHeight]); return (