fix(desktop): fullscreen header bar + always-visible close controls (#8033)

Signed-off-by: Andrew Harvard <aharvard@squareup.com>
Signed-off-by: Douwe Osinga <douwe@squareup.com>
Co-authored-by: Goose <opensource@block.xyz>
Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
Andrew Harvard
2026-03-20 12:15:17 -04:00
committed by GitHub
parent 8df8642660
commit 80f3e50d0d
3 changed files with 61 additions and 31 deletions
@@ -192,12 +192,10 @@ var McpAppBridge = (function () {
};
sendRequest("ui/initialize", {
protocolVersion: "2026-01-26",
appInfo: appIdentity,
clientInfo: appIdentity,
appCapabilities: {
availableDisplayModes: ["inline", "fullscreen"],
},
capabilities: {},
})
.then(function (result) {
applyTheme(result.hostContext || result);
@@ -33,6 +33,7 @@ import { cn } from '../../utils';
import { errorMessage } from '../../utils/conversionUtils';
import { getProtocol, isProtocolSafe } from '../../utils/urlSecurity';
import FlyingBird from '../FlyingBird';
import { formatExtensionName } from '../settings/extensions/subcomponents/ExtensionList';
import {
GooseDisplayMode,
SandboxPermissions,
@@ -54,6 +55,7 @@ import {
} from './useDisplayMode';
const DEFAULT_IFRAME_HEIGHT = 200;
const FULLSCREEN_HEADER_HEIGHT = 48;
const DISPLAY_MODE_LAYOUTS: Record<GooseDisplayMode, DimensionLayout> = {
inline: { width: 'fixed', height: 'unbounded' },
@@ -246,6 +248,7 @@ export default function McpAppRenderer({
onDisplayModeChange,
}: McpAppRendererProps) {
const containerRef = useRef<HTMLDivElement>(null);
const contentRef = useRef<HTMLDivElement>(null);
const dm = useDisplayMode({ displayMode, onDisplayModeChange, containerRef });
const {
@@ -258,6 +261,7 @@ export default function McpAppRenderer({
isInline,
appSupportsFullscreen,
appSupportsPip,
appTitle,
changeDisplayMode,
inlineHeight,
pipPosition,
@@ -672,7 +676,7 @@ export default function McpAppRenderer({
containerDimensions: getContainerDimensions(
activeDisplayMode,
containerWidth,
containerHeight
isFullscreen ? containerHeight - FULLSCREEN_HEADER_HEIGHT : containerHeight
),
locale: navigator.language,
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
@@ -695,6 +699,7 @@ export default function McpAppRenderer({
resolvedTheme,
mcpHostStyles,
activeDisplayMode,
isFullscreen,
isStandalone,
containerWidth,
containerHeight,
@@ -754,36 +759,53 @@ export default function McpAppRenderer({
);
};
const showControls = !isStandalone && !isError && (appSupportsFullscreen || appSupportsPip);
const showControls =
!isStandalone && !isError && (appSupportsFullscreen || appSupportsPip || isFullscreen || isPip);
const fullscreenTitle = useMemo(() => {
if (appTitle) return appTitle;
if (extensionName) return formatExtensionName(extensionName);
return 'App';
}, [appTitle, extensionName]);
const renderFullscreenHeader = () => (
<div
className="flex shrink-0 items-center border-b border-border-primary bg-background-primary px-3"
style={{ height: `${FULLSCREEN_HEADER_HEIGHT}px` }}
>
<div className="min-w-0 flex-1" />
<span className="truncate px-4 text-sm font-medium text-text-secondary">
{fullscreenTitle}
</span>
<div className="flex flex-1 items-center justify-end gap-1">
{appSupportsPip && (
<button
onClick={() => changeDisplayMode('pip')}
className="no-drag cursor-pointer rounded-md p-1.5 text-text-secondary transition-colors hover:bg-black/10 hover:text-text-primary dark:hover:bg-white/10"
title="Picture-in-Picture"
aria-label="Picture-in-Picture"
>
<PictureInPicture2 size={16} />
</button>
)}
<button
ref={fullscreenCloseRef}
onClick={() => changeDisplayMode('inline')}
className="no-drag cursor-pointer rounded-md p-1.5 text-text-secondary transition-colors hover:bg-black/10 hover:text-text-primary dark:hover:bg-white/10"
title="Exit fullscreen (Esc)"
aria-label="Exit fullscreen"
>
<X size={16} />
</button>
</div>
</div>
);
const renderDisplayModeControls = () => {
if (!showControls) return null;
if (activeDisplayMode === 'fullscreen') {
return (
<div className="no-drag absolute top-3 right-3 z-[60] flex gap-1">
{appSupportsPip && (
<button
onClick={() => changeDisplayMode('pip')}
className="cursor-pointer rounded-md bg-black/50 p-1.5 text-white backdrop-blur-sm transition-opacity hover:bg-black/70"
title="Picture-in-Picture"
aria-label="Picture-in-Picture"
>
<PictureInPicture2 size={16} />
</button>
)}
<button
ref={fullscreenCloseRef}
onClick={() => changeDisplayMode('inline')}
className="cursor-pointer rounded-md bg-black/50 p-1.5 text-white backdrop-blur-sm transition-opacity hover:bg-black/70"
title="Exit fullscreen (Esc)"
aria-label="Exit fullscreen"
>
<X size={16} />
</button>
</div>
);
}
// Fullscreen controls are rendered by renderFullscreenHeader instead.
if (activeDisplayMode === 'fullscreen') return null;
if (activeDisplayMode === 'pip') {
return (
@@ -893,9 +915,10 @@ export default function McpAppRenderer({
{/* Stable app container — never unmounted, only repositioned via CSS */}
<div
ref={containerRef}
className={cn(containerClasses, isPip && 'group/pip')}
className={cn(containerClasses, isFillsViewport && 'flex flex-col', isPip && 'group/pip')}
style={containerStyle}
>
{isFullscreen && renderFullscreenHeader()}
{isPip && (
<div className="pointer-events-none sticky top-1 z-20 flex h-0 items-start justify-between px-1 opacity-0 transition-opacity group-hover/pip:pointer-events-auto group-hover/pip:opacity-100 focus-within:pointer-events-auto focus-within:opacity-100">
<div
@@ -914,7 +937,7 @@ export default function McpAppRenderer({
<div className="flex gap-1">{renderDisplayModeControls()}</div>
</div>
)}
<div className={cn('relative w-full', !isPip && 'h-full')}>
<div ref={contentRef} className={cn('relative w-full', !isPip && 'flex-1 min-h-0')}>
{!isPip && renderDisplayModeControls()}
{renderContent()}
</div>
@@ -36,6 +36,7 @@ export interface DisplayModeState {
isInline: boolean;
appSupportsFullscreen: boolean;
appSupportsPip: boolean;
appTitle: string | null;
changeDisplayMode: (mode: GooseDisplayMode) => void;
@@ -77,6 +78,9 @@ export function useDisplayMode({
// null = not yet known (controls stay hidden until initialize), empty = app didn't declare any.
const [appDeclaredModes, setAppDeclaredModes] = useState<string[] | null>(null);
// App-declared title from ui/initialize (highest priority in the title fallback chain).
const [appTitle, setAppTitle] = useState<string | null>(null);
const effectiveDisplayModes = useMemo((): McpUiDisplayMode[] => {
if (!appDeclaredModes) return [];
return AVAILABLE_DISPLAY_MODES.filter((m) => appDeclaredModes.includes(m));
@@ -263,6 +267,10 @@ export function useDisplayMode({
if (caps?.availableDisplayModes && Array.isArray(caps.availableDisplayModes)) {
setAppDeclaredModes(caps.availableDisplayModes);
}
const title = data.params.clientInfo?.name;
if (typeof title === 'string' && title.trim()) {
setAppTitle(title.trim());
}
}
// After initialize, only allow modes both host and app agree on.
@@ -319,6 +327,7 @@ export function useDisplayMode({
isInline,
appSupportsFullscreen,
appSupportsPip,
appTitle,
changeDisplayMode,