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
}
diff --git a/apps/web/src/components/files/BrowserDocumentFrame.browser.test.tsx b/apps/web/src/components/files/BrowserDocumentFrame.browser.test.tsx
index 9785a6764b2b6..3915ecce9a4ca 100644
--- a/apps/web/src/components/files/BrowserDocumentFrame.browser.test.tsx
+++ b/apps/web/src/components/files/BrowserDocumentFrame.browser.test.tsx
@@ -1,3 +1,4 @@
+import { injectHtmlRenderBootstrap } from "@t3tools/shared/htmlRender";
import { describe, expect, it, vi } from "vite-plus/test";
import "~/index.css";
@@ -34,3 +35,109 @@ describe("HtmlRenderDocument", () => {
expect(onContentHeight).toHaveBeenCalledTimes(1);
});
});
+
+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 572441ffe5366..12af0f39336b2 100644
--- a/packages/shared/src/htmlRender.ts
+++ b/packages/shared/src/htmlRender.ts
@@ -232,6 +232,7 @@ export const HTML_RENDER_THEME_GUIDE = [
export const HTML_RENDER_LAYOUT_GUIDE = [
`Frame width is reply width (${HTML_RENDER_COLUMN_WIDTH}px desktop, about 360px phones), on the thread background.`,
"Leave html, body, and the outer element with no background color, no outer card, border, banner title, or side padding. This wins over any rule that sets a page background.",
+ "T3 adds an overridable 8px html gutter for selection rings and shadows. Use fluid widths within it.",
"A box that needs its own background gets 16px padding and var(--radius) corners. Give charts fixed pixel heights.",
"Let content set the height. Do not use vh units or height:100% on html or body. Both are rejected or loop.",
].join(" ");
@@ -353,7 +354,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}" +