mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
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:
1 parent
996b95eb45
commit
62468c547f
3 files changed
+63
-17
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user