Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 22 additions & 14 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1391,22 +1391,30 @@ export class App extends ProtocolWithEvents<
scheduled = true;
requestAnimationFrame(() => {
scheduled = false;
const html = document.documentElement;
const body = document.body;

// Measure actual content height by temporarily overriding html sizing.
// Height uses max-content because fit-content would clamp to the viewport
// height when content is taller than the iframe, causing internal scrolling.
// Measure content height via body.scrollHeight + vertical body margins.
//
// Width uses window.innerWidth instead of measuring via fit-content.
// Setting html.style.width to fit-content forces a synchronous reflow at
// 0px width for responsive apps (whose content derives width from the
// container rather than having intrinsic width). This causes the browser
// to clamp scrollLeft on any horizontal scroll containers to 0, permanently
// destroying their scroll positions.
const originalHeight = html.style.height;
html.style.height = "max-content";
const height = Math.ceil(html.getBoundingClientRect().height);
html.style.height = originalHeight;
// scrollHeight captures overflow (so the iframe grows when content is
// taller than the viewport — see #525) and equals the content height for
// a default-styled body (so the iframe shrinks to fit — see #57).
//
// We previously forced html.style.height = "max-content" to measure
// intrinsic height, but that makes height:100% descendants resolve to
// auto during measurement. Apps that set html,body{height:100%} with a
// viewport-filling child reported a collapsed (spinner-sized) height
// and then never recovered — see #143. Reading scrollHeight without a
// style override leaves those layouts intact.
//
// Tradeoff: an app that sets body{height:100%} won't shrink below the
// current viewport height. That's the semantically correct behavior for
// a viewport-filling layout; apps that need explicit control should
// pass {autoResize: false} and call sendSizeChanged() manually.
const bodyStyle = getComputedStyle(body);
const bodyMarginY =
(parseFloat(bodyStyle.marginTop) || 0) +
(parseFloat(bodyStyle.marginBottom) || 0);
const height = Math.ceil(body.scrollHeight + bodyMarginY);

const width = Math.ceil(window.innerWidth);

Expand Down
17 changes: 12 additions & 5 deletions src/react/useApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@ export * from "../app";
/**
* Options for configuring the {@link useApp `useApp`} hook.
*
* Note: This interface does NOT expose {@link App `App`} options like `autoResize`.
* The hook creates the `App` with default options (`autoResize: true`). If you
* need custom `App` options, create the `App` manually instead of using this hook.
*
* @see {@link useApp `useApp`} for the hook that uses these options
* @see {@link useAutoResize `useAutoResize`} for manual auto-resize control with custom `App` options
*/
Expand All @@ -21,6 +17,16 @@ export interface UseAppOptions {
* Declares what features this app supports.
*/
capabilities: McpUiAppCapabilities;
/**
* Automatically report size changes to the host.
*
* Disable this for apps that manage their own height (canvases, editors,
* diagrams) or that use viewport-relative sizing like `100vh`, and call
* {@link App.sendSizeChanged `app.sendSizeChanged`} manually instead.
*
* @default true
*/
autoResize?: boolean;
/**
* Called after {@link App `App`} is created but before connection.
*
Expand Down Expand Up @@ -120,6 +126,7 @@ export interface AppState {
export function useApp({
appInfo,
capabilities,
autoResize = true,
onAppCreated,
}: UseAppOptions): AppState {
const [app, setApp] = useState<App | null>(null);
Expand All @@ -135,7 +142,7 @@ export function useApp({
window.parent,
window.parent,
);
const app = new App(appInfo, capabilities);
const app = new App(appInfo, capabilities, { autoResize });

// Register handlers BEFORE connecting
onAppCreated?.(app);
Expand Down
Loading