Fit the panel to Codex's and Claude's panel rules

Images and stylesheet files load as data: URLs, which hosts allow where
some refuse blob: images; the scene-asset source gets an https app
address the panel serves, since Codex pages use their own scheme; and a
panel whose 3D context the browser takes back says so.
This commit is contained in:
Sameh Khamis committed 2026-10-02 18:29:42 -07:00
1 parent 996b95eb45
commit 62468c547f
3 files changed
+63 -17

No files matched your search

+51 -16
View File
@@ -130,9 +130,12 @@ export function panelRuntime(config) {
});
}
// The app's own paths: relative ones, and absolute ones on this page's
// site, matched by scheme and host since a host may use its own scheme.
// The app's own paths: relative ones, absolute ones on this page's site
// (matched by scheme and host, since a host may use its own scheme), and
// ones under the app's address in the page, an https address for code
// that needs one (`config.appBaseUrl`).
const page = new URL(document.baseURI);
const appOrigin = new URL(config.appBaseUrl).origin;
function appPath(value) {
let url;
try {
@@ -140,20 +143,33 @@ export function panelRuntime(config) {
} catch {
return null;
}
if (url.protocol !== page.protocol || url.host !== page.host) return null;
return url.pathname + url.search;
const own =
url.origin === appOrigin ||
(url.protocol === page.protocol && url.host === page.host);
return own ? url.pathname + url.search : null;
}
const blobUrls = new Map();
/** A blob: URL holding an app file, for elements that load by URL. */
const fileUrls = new Map();
/**
* A data: URL holding an app file, for elements that load by URL. Hosts
* allow data: images where some refuse blob: ones.
*/
function appObjectUrl(path) {
if (!blobUrls.has(path))
blobUrls.set(
if (!fileUrls.has(path))
fileUrls.set(
path,
appFetch(path)
.then((response) => response.blob())
.then((blob) => URL.createObjectURL(blob)),
.then(
(blob) =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject(reader.error);
reader.readAsDataURL(blob);
}),
),
);
return blobUrls.get(path);
return fileUrls.get(path);
}
function installRequests() {
@@ -311,12 +327,15 @@ export function panelRuntime(config) {
URL.createObjectURL(new Blob([BOOT], { type: 'text/javascript' })),
options,
);
appObjectUrl(path).then((script) =>
worker.postMessage({
__gevScript: script,
module: options?.type === 'module',
}),
);
appFetch(path)
.then((response) => response.blob())
.then((blob) => URL.createObjectURL(blob))
.then((script) =>
worker.postMessage({
__gevScript: script,
module: options?.type === 'module',
}),
);
return worker;
};
window.Worker.prototype = NativeWorker.prototype;
@@ -398,6 +417,7 @@ export function panelRuntime(config) {
*/
async function startApp(url) {
window.GEV_EMBED_INLINE = true;
window.GEV_APP_BASE_URL = config.appBaseUrl;
try {
history.replaceState(
null,
@@ -452,6 +472,21 @@ export function panelRuntime(config) {
})),
);
for (const { type, text } of scripts) await runScript(text, type);
// Browsers cap live 3D contexts across a conversation's panels and take
// one back when there are too many; say so rather than show no map.
document.addEventListener(
'webglcontextlost',
() => {
const notice = document.createElement('div');
notice.id = 'status';
notice.textContent =
"This panel lost its 3D graphics, likely because other God's Eye " +
"View panels in this conversation hold them. Use Open in God's " +
'Eye View above, or show it in a new conversation.';
document.body.appendChild(notice);
},
{ capture: true, once: true },
);
}
// Tool results: the first view starts the app, later ones move it.
+5 -1
View File
@@ -8,7 +8,11 @@ export function createStandaloneTools(options) {
sceneDataPacks: {
sources: {
assets: createAssetDirectorySource({
baseUrl: new URL('/scene-assets/', document.baseURI).href,
// A panel names the app's address; see src/tools/globePanel.js.
baseUrl: new URL(
'/scene-assets/',
globalThis.GEV_APP_BASE_URL ?? document.baseURI,
).href,
}),
},
},
+7
View File
@@ -41,6 +41,12 @@ export const MCP_APP_MIME_TYPE = 'text/html;profile=mcp-app';
const MCP_APPS_PROTOCOL_VERSION = '2026-01-26';
const PANEL_HEIGHT_PX = 520;
const LOAD_TIMEOUT_MS = 90_000;
/**
* The app's address inside the panel, for code that needs an https address
* (the page's own may use a host's scheme). The panel loads its paths
* through the MCP server; the name never resolves.
*/
const PANEL_APP_BASE_URL = 'https://app.gods-eye-view.invalid/';
/** The panel-only tool the panel loads the app through. */
const PANEL_REQUEST_TOOL = 'panel_request';
@@ -51,6 +57,7 @@ const PANEL_REQUEST_TOOL = 'panel_request';
*/
function panelHtml(runtime) {
const config = {
appBaseUrl: PANEL_APP_BASE_URL,
loadTimeoutMs: LOAD_TIMEOUT_MS,
panelBase: PANEL_BASE,
panelHeight: PANEL_HEIGHT_PX,