From dc667126e7e1ec30b4fd7e1b06c1064fd82c40b7 Mon Sep 17 00:00:00 2001 From: Stevan Borus Date: Thu, 22 Oct 2026 19:40:57 +0200 Subject: [PATCH 1/2] fix(web): keep HTML prototype selection rings visible Generated HTML pages start flush against the iframe viewport, so selected cards can paint their rings outside the visible page. Reserve an overridable 8px gutter and include it in normal document sizing, with an iframe regression covering both edge cards. --- .../BrowserDocumentFrame.browser.test.tsx | 111 ++++++++++++++++++ packages/shared/src/htmlRender.ts | 6 +- 2 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 apps/web/src/components/files/BrowserDocumentFrame.browser.test.tsx diff --git a/apps/web/src/components/files/BrowserDocumentFrame.browser.test.tsx b/apps/web/src/components/files/BrowserDocumentFrame.browser.test.tsx new file mode 100644 index 0000000000000..1a6061b29f9f2 --- /dev/null +++ b/apps/web/src/components/files/BrowserDocumentFrame.browser.test.tsx @@ -0,0 +1,111 @@ +import { injectHtmlRenderBootstrap } from "@t3tools/shared/htmlRender"; +import { expect, it, vi } from "vite-plus/test"; + +import { render } from "../../../test/render"; +import { HtmlRenderDocument } from "./BrowserDocumentFrame"; + +it("keeps the first and last selected card rings inside the fitted renderer frame", async () => { + const html = injectHtmlRenderBootstrap(`
+ + + +
`); + const src = URL.createObjectURL(new Blob([html], { type: "text/html" })); + using _url = { [Symbol.dispose]: () => URL.revokeObjectURL(src) }; + const container = document.createElement("div"); + container.style.width = "728px"; + const reports = new Map< + string, + { + selected: boolean; + ring: number; + left: number; + right: number; + top: number; + bottom: number; + pageHeight: number; + viewportHeight: number; + viewportWidth: number; + scrollWidth: number; + } + >(); + const messages = new AbortController(); + using _messages = { [Symbol.dispose]: () => messages.abort() }; + window.addEventListener( + "message", + (event) => { + const frame = container.querySelector("iframe"); + if (event.source === frame?.contentWindow && event.data?.type === "selected-card-bounds") { + reports.set(event.data.id, event.data); + } + }, + { signal: messages.signal }, + ); + const contentHeight = vi.fn((height: number) => { + container.style.setProperty("--content-height", `${height}px`); + }); + await using screen = await render( + <> + + + , + { container }, + ); + + await vi.waitFor(() => expect(contentHeight).toHaveBeenCalled()); + const frame = screen.getByTitle("Edge cards").element() as HTMLIFrameElement; + await vi.waitFor(() => { + expect(frame.getBoundingClientRect().height).toBe(contentHeight.mock.lastCall?.[0]); + }); + + for (const id of ["first", "last"]) { + frame.contentWindow?.postMessage({ type: "select-card", id }, "*"); + await vi.waitFor(() => expect(reports.get(id)?.selected).toBe(true)); + const bounds = reports.get(id)!; + expect(bounds.ring).toBe(5); + expect(bounds.viewportWidth).toBe(728); + expect(bounds.pageHeight).toBe(contentHeight.mock.lastCall?.[0]); + expect(bounds.viewportHeight).toBe(bounds.pageHeight); + expect(bounds.left).toBeGreaterThanOrEqual(0); + expect(bounds.top).toBeGreaterThanOrEqual(0); + expect(bounds.right).toBeLessThanOrEqual(bounds.viewportWidth); + expect(bounds.bottom).toBeLessThanOrEqual(bounds.viewportHeight); + expect(bounds.scrollWidth).toBeLessThanOrEqual(bounds.viewportWidth); + } +}); diff --git a/packages/shared/src/htmlRender.ts b/packages/shared/src/htmlRender.ts index cb01eab9601df..9c3fd98f3bbcb 100644 --- a/packages/shared/src/htmlRender.ts +++ b/packages/shared/src/htmlRender.ts @@ -236,13 +236,13 @@ export const HTML_RENDER_THEME_GUIDE = [ "--destructive, --destructive-foreground, --destructive-surface, --warning, --warning-foreground, --warning-surface,", "--success, --success-foreground, --info, --info-foreground, --code-background, --code-foreground,", "--chart-1 … --chart-6 (categorical series for charts), --radius, --font-sans, --font-mono.", - "The base stylesheet sets html background/color/font from these, body margin to 0, and hides the page's scrollbar; your own CSS overrides it.", + "The base stylesheet sets html background/color/font from these, adds an 8px safety gutter on html for selection rings and shadows, sets body margin to 0, and hides the page's scrollbar; your own CSS overrides it.", ].join(" "); export const HTML_RENDER_LAYOUT_GUIDE = [ `The frame is borderless on the thread's background, as wide as the reply column (${HTML_RENDER_COLUMN_WIDTH}px on desktop by default, wider if the reader widens chat, about 360px on phones), and its left edge lines up with your reply text.`, "The page sits on the thread's own background, so by default leave html, body, and the outermost element with no background color. This overrides general style preferences such as a fixed black page background.", - "Use a fluid width with no horizontal padding on the outermost element, and no outer card, border, or banner title: the page is part of your reply.", + "Use a fluid width with no extra horizontal padding on the outermost element. T3 already adds an 8px safety gutter for selection rings and shadows. Add no outer card, border, or banner title: the page is part of your reply.", "If a box needs its own background (a mock of a specific screen, a panel that must stand apart), give it at least 16px of padding on every side and var(--radius) corners, so content never touches its edge.", "Give charts fixed pixel heights rather than heights that scale with width.", "Let content set the page's height. The frame grows to fit the page, so a height that follows the viewport makes it grow again and again: html_preview and html_render reject vh, svh, lvh, and dvh units, and height:100% on html or body has the same problem.", @@ -373,7 +373,7 @@ export function htmlRenderThemeMessage(theme: HtmlRenderTheme) { } const BASE_CSS = - "html{background:var(--background);color:var(--foreground);font-family:var(--font-sans);font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased;-webkit-text-size-adjust:100%;scrollbar-width:none}" + + "html{box-sizing:border-box;padding:8px;background:var(--background);color:var(--foreground);font-family:var(--font-sans);font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased;-webkit-text-size-adjust:100%;scrollbar-width:none}" + "html::-webkit-scrollbar{display:none}body{margin:0}code,kbd,pre,samp{font-family:var(--font-mono)}" + // Focus outlines paint inside the element, so the frame edge never clips them. ":where(:focus-visible){outline:2px solid var(--ring)}:focus-visible{outline-width:2px!important;outline-offset:-2px!important}" + From 4a24f02e26329dc09dc721faa5405ad3ee902cdf Mon Sep 17 00:00:00 2001 From: maria-rcks <254055478+maria-rcks@users.noreply.github.com> Date: Thu, 29 Oct 2026 03:51:49 +0000 Subject: [PATCH 2/2] chore(server): update the agent context budget for the html gutter note --- apps/server/src/provider/agentContextBudget.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/provider/agentContextBudget.json b/apps/server/src/provider/agentContextBudget.json index d5496ea8cfc57..1711e148d4ca4 100644 --- a/apps/server/src/provider/agentContextBudget.json +++ b/apps/server/src/provider/agentContextBudget.json @@ -1,5 +1,5 @@ { "claudeSystemAppendBytes": 5135, "codexAdditionalContextBytes": 6865, - "mcpToolListJsonBytes": 45907 + "mcpToolListJsonBytes": 46007 }