Skip to content

Commit 997946c

Browse files
authored
fix(mcpapps): prompt-browser inline shell height at first paint + always-present brand bar (#1051)
Match the platform-info app's proven sizing and branding structure. Height: claude.ai sizes the app iframe by reading documentElement height directly at first paint and ignores ui/notifications/size-changed (anthropics/claude-ai-mcp #69); at that read only inline attributes on <html> are honored, not the stylesheet. Declare the 640px shell height as an inline style on <html> so it is correct before the prompt data arrives, and signal the size at init (watchSize in onInitialized) instead of deferring it until after content renders. Removes the lockSize path, which set the height only on the first content paint and left the host reading the pre-render spinner. Brand bar: render it unconditionally instead of shipping it as class="brandbar hidden" and revealing it from JS, matching the always-present logo container in the platform-info app.
1 parent ff4d522 commit 997946c

1 file changed

Lines changed: 33 additions & 40 deletions

File tree

apps/prompt-browser/index.html

Lines changed: 33 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<!DOCTYPE html>
2-
<html lang="en">
2+
<html lang="en" style="height: 640px">
33
<head>
44
<meta charset="UTF-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -49,15 +49,17 @@
4949

5050
* { box-sizing: border-box; margin: 0; padding: 0; }
5151

52-
/* Fixed-height app shell, tall enough for a human to actually browse.
52+
/* Fixed 640px app shell, tall enough for a human to actually browse.
5353
Claude sizes the iframe by reading documentElement's height directly
54-
and ignores ui/notifications/size-changed (anthropics/claude-ai-mcp
55-
#69), so the height is declared as an INLINE style on <html> (present
56-
at first paint, which is what Claude snapshots) in addition to this
57-
rule and the size-changed message the app still sends for
58-
spec-compliant hosts. The brand bar and each view's head are pinned;
59-
the body scrolls inside the shell. Keep this value in sync with the
60-
inline style="height:..." on the <html> element above. */
54+
at first paint and ignores ui/notifications/size-changed
55+
(anthropics/claude-ai-mcp #69). At that read only inline attributes on
56+
<html> are honored, not this stylesheet, so the height is ALSO declared
57+
as an INLINE style="height:640px" on the <html> element above; that
58+
inline value is what Claude reads, independent of when the prompt data
59+
arrives. This rule keeps the shell sized in spec-compliant hosts, and
60+
the app still emits size-changed for them. The brand bar and each
61+
view's head are pinned; the body scrolls inside the shell. Keep this
62+
value in sync with the inline style on <html> and APP_HEIGHT below. */
6163
html, body { height: 640px; }
6264
body {
6365
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans',
@@ -373,7 +375,7 @@
373375
</style>
374376
</head>
375377
<body>
376-
<div id="brandbar" class="brandbar hidden">
378+
<div id="brandbar" class="brandbar">
377379
<span id="brand-logo" class="brand-logo"></span>
378380
<span id="brand-name" class="brand-name"></span>
379381
</div>
@@ -471,7 +473,6 @@
471473
} else {
472474
nameEl.textContent = brandName;
473475
}
474-
document.getElementById('brandbar').classList.remove('hidden');
475476
}
476477
renderBrand();
477478

@@ -500,38 +501,33 @@
500501
function send(msg) { window.parent.postMessage(msg, '*'); }
501502

502503
// ---------------------------------------------------------------------
503-
// MCP Apps height contract (#1043). The host sizes the iframe from the
504-
// last ui/notifications/size-changed the view sends; without one the app
505-
// renders in the host's default cramped window. The view declares a fixed
506-
// 500px viewport (its shell height) and re-reports on any reflow via a
507-
// ResizeObserver, mirroring the SDK's useAutoResize pattern. Kept
508-
// byte-identical to the platform-info app so both speak one contract.
504+
// MCP Apps height contract (#1043). The shell is a fixed 640px viewport
505+
// declared inline on <html> (present at first paint) and in the stylesheet;
506+
// that inline height is what Claude reads when it sizes the iframe from the
507+
// documentElement directly, ignoring ui/notifications/size-changed
508+
// (anthropics/claude-ai-mcp #69). Because the height is fixed and does not
509+
// depend on the prompt data, it is correct at first paint even though this
510+
// browser needs two round trips (handshake, then the prompt list) before it
511+
// has content to show; the spinner renders inside the 640px shell, not as
512+
// the shell's height. size-changed is still emitted, at init, for
513+
// spec-compliant hosts that honor it. Kept in step with the platform-info
514+
// app so both speak one contract.
509515
// ---------------------------------------------------------------------
510516
var APP_HEIGHT = 640;
511-
var sizeLocked = false;
512517
function sendSize() {
513518
// Report height only. These apps are laid out at the host's conversation
514519
// width, so width stays host-controlled (fluid); we own only the height.
515520
send({ jsonrpc: '2.0', method: 'ui/notifications/size-changed',
516521
params: { height: APP_HEIGHT } });
517522
}
518-
// lockSize declares the app's height AFTER its real content has rendered.
519-
// Claude sizes the iframe by reading documentElement's height directly and
520-
// snapshots it once, early; a size signal (a set height or a ResizeObserver
521-
// firing) sent BEFORE content renders locks the frame at the pre-render
522-
// spinner height (anthropics/claude-ai-mcp #69). This browser needs two round
523-
// trips (handshake, then the prompt list), so at init it is still a spinner,
524-
// which is why setting the height at init did nothing. The height and the
525-
// observer are therefore established only here, on the first content paint.
526-
// size-changed is also sent for spec-compliant hosts that honor it.
527-
function lockSize() {
528-
document.documentElement.style.height = APP_HEIGHT + 'px';
523+
// watchSize signals the shell height and re-reports on any reflow. The shell
524+
// is pinned to APP_HEIGHT (inline + stylesheet), so the ResizeObserver
525+
// reports that height whether the app is still a spinner or fully rendered;
526+
// there is no pre-render spinner height to lock onto. Called once at init.
527+
function watchSize() {
529528
sendSize();
530-
if (!sizeLocked) {
531-
sizeLocked = true;
532-
if (typeof ResizeObserver === 'function') {
533-
new ResizeObserver(function() { sendSize(); }).observe(document.documentElement);
534-
}
529+
if (typeof ResizeObserver === 'function') {
530+
new ResizeObserver(function() { sendSize(); }).observe(document.documentElement);
535531
}
536532
}
537533

@@ -569,6 +565,9 @@
569565
isLegacy = !hostCaps;
570566
canMessage = !!(hostCaps && hostCaps.message);
571567
send({ jsonrpc: '2.0', method: 'ui/notifications/initialized', params: {} });
568+
// Signal the fixed shell height now, at init. The shell is a fixed size
569+
// (inline on <html>), so it is correct before the prompt data loads.
570+
watchSize();
572571
boot();
573572
}
574573

@@ -886,17 +885,11 @@
886885
state.ranked = !!query && d.ranking !== undefined;
887886
hide('loading');
888887
renderBrowser();
889-
// Real content is now on screen; declare the height so Claude's
890-
// DOM-height read lands on the rendered library, not the spinner.
891-
lockSize();
892888
})
893889
.catch(function(err) {
894890
if (seq !== listSeq || isCanceled(err)) { return; }
895891
hide('loading');
896892
showError(err.message, function() { loadList(state.query); });
897-
// Even an error state is a real render; size to it rather than
898-
// leaving the frame locked at the spinner height.
899-
lockSize();
900893
});
901894
}
902895

0 commit comments

Comments
 (0)