diff --git a/crates/goose-mcp/src/autovisualiser/templates/assets/mcp-app-bridge.js b/crates/goose-mcp/src/autovisualiser/templates/assets/mcp-app-bridge.js index efbf7c184a..fca90bcbda 100644 --- a/crates/goose-mcp/src/autovisualiser/templates/assets/mcp-app-bridge.js +++ b/crates/goose-mcp/src/autovisualiser/templates/assets/mcp-app-bridge.js @@ -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); diff --git a/ui/desktop/src/components/McpApps/McpAppRenderer.tsx b/ui/desktop/src/components/McpApps/McpAppRenderer.tsx index 37251dc207..4d442e69fb 100644 --- a/ui/desktop/src/components/McpApps/McpAppRenderer.tsx +++ b/ui/desktop/src/components/McpApps/McpAppRenderer.tsx @@ -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 = { inline: { width: 'fixed', height: 'unbounded' }, @@ -246,6 +248,7 @@ export default function McpAppRenderer({ onDisplayModeChange, }: McpAppRendererProps) { const containerRef = useRef(null); + const contentRef = useRef(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 = () => ( +
+
+ + {fullscreenTitle} + +
+ {appSupportsPip && ( + + )} + +
+
+ ); const renderDisplayModeControls = () => { if (!showControls) return null; - if (activeDisplayMode === 'fullscreen') { - return ( -
- {appSupportsPip && ( - - )} - -
- ); - } + // 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 */}
+ {isFullscreen && renderFullscreenHeader()} {isPip && (
{renderDisplayModeControls()}
)} -
+
{!isPip && renderDisplayModeControls()} {renderContent()}
diff --git a/ui/desktop/src/components/McpApps/useDisplayMode.ts b/ui/desktop/src/components/McpApps/useDisplayMode.ts index 9f644ffdd9..f6d7ac1d6b 100644 --- a/ui/desktop/src/components/McpApps/useDisplayMode.ts +++ b/ui/desktop/src/components/McpApps/useDisplayMode.ts @@ -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(null); + // App-declared title from ui/initialize (highest priority in the title fallback chain). + const [appTitle, setAppTitle] = useState(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,