Both MCP servers serve six ui:// views (a circuit, a result with its verdict, a receipt, a critique, an estimate's frontier, a device target's map) through the protocol's extension for interactive views, version 2026-01-26, to a host that says it draws them: each tool names its view in _meta.ui.resourceUri, the view's data (the engine's layout and the circuit in words, a target's map) rides in the result's _meta, and the service's poll_quantum_job, which a view calls while a job runs, is kept from the model. The service keeps no session, so the client's word travels in the session id it is given at initialize. Every host gets each such answer in plain words. The views are one self-contained page (736 KB, held under 1 MiB) built from the Studio's components, carrying the engine (wai-quantum-views) as WebAssembly and translated to JavaScript. Under the extension's default content-security policy the browser refuses WebAssembly, so the JavaScript form checks the receipt there; a scripted host in a headless browser draws every view from both servers under both policies and holds the check, a changed result, the size and no outside request. The page rebuilds to the same bytes in the CI image. Co-Authored-By: Claude Opus 5.5 <[email protected]>
99 lines
3.7 KiB
JavaScript
99 lines
3.7 KiB
JavaScript
// A view of WAI Quantum's engine inside an agent host: one page, the view it
|
||
// draws named by the server in <meta name="wai-view">, the tool's answer
|
||
// given by the host (src/views/host.js). The page loads nothing and connects
|
||
// nowhere; a job the service is still running is followed through the
|
||
// server's own tool for that, which the host keeps from the model.
|
||
import { el } from '../dom.js';
|
||
import { connect } from './host.js';
|
||
import { render, META } from './render.js';
|
||
|
||
const VIEW = document.querySelector('meta[name="wai-view"]')?.content || 'result';
|
||
const POLL = 'poll_quantum_job';
|
||
const root = document.getElementById('wq');
|
||
const body = document.body;
|
||
body.dataset.view = VIEW;
|
||
body.dataset.state = 'waiting';
|
||
|
||
const TITLES = { result: 'Result', receipt: 'Receipt', circuit: 'Circuit', critique: 'Critique', estimate: 'Resource estimate', target: 'Device target' };
|
||
let args = {};
|
||
|
||
function head(sub) {
|
||
return el('header', { class: 'wq-head' }, el('span', { class: 'wq-name', text: 'WAI Quantum' }), el('span', { class: 'wq-what', text: TITLES[VIEW] || VIEW }), sub ? el('span', { class: 'qs-note', text: sub }) : null);
|
||
}
|
||
|
||
function waiting(text) {
|
||
root.replaceChildren(head(), el('p', { class: 'qs-note', id: 'wq-waiting', text }));
|
||
}
|
||
|
||
const host = connect({
|
||
name: 'wai-quantum-views',
|
||
version: '1',
|
||
onInput: (a) => {
|
||
args = a || {};
|
||
waiting('The tool is running; its answer appears here.');
|
||
},
|
||
onResult: (res) => show(res),
|
||
onCancelled: (why) => {
|
||
body.dataset.state = 'cancelled';
|
||
waiting(`The call was cancelled${why ? `: ${why}` : ''}.`);
|
||
},
|
||
onContext: (ctx) => {
|
||
if (ctx?.theme === 'light' || ctx?.theme === 'dark') document.documentElement.dataset.theme = ctx.theme;
|
||
},
|
||
onTeardown: () => { stopped = true; },
|
||
});
|
||
|
||
let stopped = false;
|
||
let polling = null;
|
||
|
||
async function show(res) {
|
||
const data = res?._meta?.[META] || null;
|
||
const out = render(VIEW, { args, res: res || {}, data });
|
||
root.replaceChildren(head(), ...out.blocks);
|
||
body.dataset.state = out.state;
|
||
if (out.poll) follow(out.poll);
|
||
if (out.checked) {
|
||
const v = await out.checked;
|
||
if (!v.error) {
|
||
const what = VIEW === 'estimate' ? 'the estimate’s seal' : VIEW === 'circuit' ? 'the compile’s certificate' : 'the receipt';
|
||
host.tellModel(`The ${TITLES[VIEW].toLowerCase()} view checked ${what} on the person’s machine with the engine: ${v.valid ? 'it verifies' : 'it does NOT verify'}${v.signer ? `, signed by ${v.signer}` : ''}.`).catch(() => {});
|
||
}
|
||
}
|
||
}
|
||
|
||
// A job the service is still running: asked after, more slowly each time, until it ends.
|
||
function follow(id) {
|
||
if (polling) return;
|
||
let wait = 1000;
|
||
polling = (async () => {
|
||
while (!stopped) {
|
||
await new Promise((r) => setTimeout(r, wait));
|
||
wait = Math.min(5000, wait * 1.5);
|
||
let res;
|
||
try { res = await host.callTool(POLL, { id }); } catch (e) {
|
||
waiting(`This host would not let the view follow the job (${e.message}); get_quantum_job reads it.`);
|
||
break;
|
||
}
|
||
const s = res?.structuredContent?.status;
|
||
if (s !== 'queued' && s !== 'running') { polling = null; show(res); return; }
|
||
}
|
||
polling = null;
|
||
})();
|
||
}
|
||
|
||
// The host sizes the frame to the page.
|
||
let sized = 0;
|
||
new ResizeObserver(() => {
|
||
cancelAnimationFrame(sized);
|
||
sized = requestAnimationFrame(() => {
|
||
// The content's own size, not the frame's: the host fits the frame to it.
|
||
const r = root.getBoundingClientRect();
|
||
host.sizeChanged(Math.ceil(r.width), Math.ceil(r.bottom));
|
||
});
|
||
}).observe(root);
|
||
|
||
waiting('Waiting for the host.');
|
||
host.start().catch((e) => {
|
||
body.dataset.state = 'no-host';
|
||
waiting(`No host answered (${e.message}). This page is a view for an agent host; it draws what a tool answers.`);
|
||
});
|