mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
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:
@@ -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,
|
||||
|
||||
|
||||
Reference in New Issue
Block a user