From dbbdef0f7899bad7a011bd6c4e7dc260f2997516 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Fri, 2 Oct 2026 14:49:45 -0700 Subject: [PATCH] Offer fullscreen in the panel, and hold images in inserted HTML Where the host offers fullscreen, an Expand button asks for it; the page fills the room it is given. Images in markup the app inserts as HTML text now wait for their files instead of requesting the panel host's site, which ended the mic.svg and Cesium credit 404s. --- src/app/globePanelRuntime.js | 58 ++++++++++++++++++++++++++++++++++- src/tools/globePanel.js | 14 ++++++--- src/tools/globePanel.test.mjs | 3 ++ 3 files changed, 69 insertions(+), 6 deletions(-) diff --git a/src/app/globePanelRuntime.js b/src/app/globePanelRuntime.js index 708b29e01..879630c19 100644 --- a/src/app/globePanelRuntime.js +++ b/src/app/globePanelRuntime.js @@ -13,6 +13,7 @@ export function panelRuntime(config) { const status = document.getElementById('status'); const open = document.getElementById('open'); + const expand = document.getElementById('expand'); const say = (text) => { if (status.isConnected) status.textContent = text; }; @@ -340,6 +341,40 @@ export function panelRuntime(config) { }; window.Worker.prototype = NativeWorker.prototype; + // Markup the app adds as HTML text would request its images from this + // page's site at once; hold their addresses until the files are here. + const htmlWithHeldImages = (html) => + String(html).replace( + /(<(?:img|source)\b[^>]*?\s)src=(["'])([^"']*)\2/gi, + (tag, before, quote, value) => + appPath(value) === null + ? tag + : `${before}data-gev-src=${quote}${value}${quote}`, + ); + const fillHeldImages = (root) => { + for (const element of root?.querySelectorAll?.('[data-gev-src]') ?? []) { + const path = appPath(element.getAttribute('data-gev-src')); + element.removeAttribute('data-gev-src'); + appObjectUrl(path).then((url) => element.setAttribute('src', url)); + } + }; + const innerHtml = Object.getOwnPropertyDescriptor( + Element.prototype, + 'innerHTML', + ); + Object.defineProperty(Element.prototype, 'innerHTML', { + ...innerHtml, + set(value) { + innerHtml.set.call(this, htmlWithHeldImages(value)); + fillHeldImages(this); + }, + }); + const insertHtml = Element.prototype.insertAdjacentHTML; + Element.prototype.insertAdjacentHTML = function (position, html) { + insertHtml.call(this, position, htmlWithHeldImages(html)); + fillHeldImages(this.parentNode ?? this); + }; + // Markup and styles the app adds later name app files too. new MutationObserver((records) => { for (const record of records) @@ -545,6 +580,26 @@ export function panelRuntime(config) { return; } if (message.method === 'ui/notifications/tool-result') show(message.params); + if (message.method === 'ui/notifications/host-context-changed') + showDisplayMode(message.params); + }); + + // Fullscreen, where the host offers it: the globe gets the room, and some + // hosts only keep a panel on screen up to date outside the conversation. + let displayMode = 'inline'; + const showDisplayMode = (context) => { + if (context?.displayMode) displayMode = context.displayMode; + if (context?.availableDisplayModes) + expand.hidden = !context.availableDisplayModes.includes('fullscreen'); + expand.textContent = displayMode === 'fullscreen' ? 'Collapse' : 'Expand'; + }; + expand.addEventListener('click', () => { + request('ui/request-display-mode', { + mode: displayMode === 'fullscreen' ? 'inline' : 'fullscreen', + }).then( + (result) => showDisplayMode({ displayMode: result?.mode }), + () => {}, + ); }); open.addEventListener('click', () => { @@ -565,7 +620,8 @@ export function panelRuntime(config) { }, protocolVersion: config.protocolVersion, }).then( - () => { + (result) => { + showDisplayMode(result?.hostContext); send({ method: 'ui/notifications/initialized' }); notify('ui/notifications/size-changed', { width: document.body.clientWidth, diff --git a/src/tools/globePanel.js b/src/tools/globePanel.js index 2cdd1f219..04badb390 100644 --- a/src/tools/globePanel.js +++ b/src/tools/globePanel.js @@ -75,18 +75,22 @@ function panelHtml(runtime) { God's Eye View
Waiting for a view…
+
+ +
diff --git a/src/tools/globePanel.test.mjs b/src/tools/globePanel.test.mjs index bc7504081..ea67bb6e3 100644 --- a/src/tools/globePanel.test.mjs +++ b/src/tools/globePanel.test.mjs @@ -31,6 +31,9 @@ test('the globe panel is an MCP Apps resource that loads the app through its ser // The MCP Apps SDK's initialize parameters; hosts reject anything else. assert.match(resource.text, /appInfo: \{/); assert.doesNotMatch(resource.text, /clientInfo/); + // Fullscreen where the host offers it. + assert.match(resource.text, /'ui\/request-display-mode'/); + assert.match(resource.text, /