|
1 | 1 | <!DOCTYPE html> |
2 | | -<html lang="en"> |
| 2 | +<html lang="en" style="height: 640px"> |
3 | 3 | <head> |
4 | 4 | <meta charset="UTF-8"> |
5 | 5 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
49 | 49 |
|
50 | 50 | * { box-sizing: border-box; margin: 0; padding: 0; } |
51 | 51 |
|
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. |
53 | 53 | 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. */ |
61 | 63 | html, body { height: 640px; } |
62 | 64 | body { |
63 | 65 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', |
|
373 | 375 | </style> |
374 | 376 | </head> |
375 | 377 | <body> |
376 | | - <div id="brandbar" class="brandbar hidden"> |
| 378 | + <div id="brandbar" class="brandbar"> |
377 | 379 | <span id="brand-logo" class="brand-logo"></span> |
378 | 380 | <span id="brand-name" class="brand-name"></span> |
379 | 381 | </div> |
|
471 | 473 | } else { |
472 | 474 | nameEl.textContent = brandName; |
473 | 475 | } |
474 | | - document.getElementById('brandbar').classList.remove('hidden'); |
475 | 476 | } |
476 | 477 | renderBrand(); |
477 | 478 |
|
|
500 | 501 | function send(msg) { window.parent.postMessage(msg, '*'); } |
501 | 502 |
|
502 | 503 | // --------------------------------------------------------------------- |
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. |
509 | 515 | // --------------------------------------------------------------------- |
510 | 516 | var APP_HEIGHT = 640; |
511 | | - var sizeLocked = false; |
512 | 517 | function sendSize() { |
513 | 518 | // Report height only. These apps are laid out at the host's conversation |
514 | 519 | // width, so width stays host-controlled (fluid); we own only the height. |
515 | 520 | send({ jsonrpc: '2.0', method: 'ui/notifications/size-changed', |
516 | 521 | params: { height: APP_HEIGHT } }); |
517 | 522 | } |
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() { |
529 | 528 | 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); |
535 | 531 | } |
536 | 532 | } |
537 | 533 |
|
|
569 | 565 | isLegacy = !hostCaps; |
570 | 566 | canMessage = !!(hostCaps && hostCaps.message); |
571 | 567 | 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(); |
572 | 571 | boot(); |
573 | 572 | } |
574 | 573 |
|
|
886 | 885 | state.ranked = !!query && d.ranking !== undefined; |
887 | 886 | hide('loading'); |
888 | 887 | 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(); |
892 | 888 | }) |
893 | 889 | .catch(function(err) { |
894 | 890 | if (seq !== listSeq || isCanceled(err)) { return; } |
895 | 891 | hide('loading'); |
896 | 892 | 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(); |
900 | 893 | }); |
901 | 894 | } |
902 | 895 |
|
|
0 commit comments