feat: wire native Codex UI as /console entry, keep classic console at /console-classic
ci / test (push) Successful in 45m25s
ci / test (push) Successful in 45m25s
- Vendor codex-ui-stack host/ as cloud/codex-ui-host/ (original/ not vendored, 650MB) - Bridge emulates 7 methods missing in codex 0.164.0-alpha.9, exposes 29 experimental-protocol methods (150 -> 179), Linux binary candidates, read-only POST /__codex_git - GET /console 302s to the UI host (:8095) when reachable, else falls back to the classic React console; GET /console-classic always serves it - Alignment tables: docs/codex-native-ui-alignment.md - Tests: test_console 7, new test_codex_ui_host 4, handler method pin 247 -> 249
This commit is contained in:
1 parent
41f3985248
commit
3e0ff898e0
20 files changed
+3018
-24
No files matched your search
Executable
+109
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Load the local web host in the headless browser, capture console + exceptions,
|
||||
* then dump the resulting page state and a screenshot.
|
||||
*
|
||||
* node host/cdp-load.mjs --port 9333 --url http://127.0.0.1:8095/ --out /tmp/shot.png
|
||||
*/
|
||||
|
||||
import fs from "node:fs";
|
||||
|
||||
const argv = process.argv.slice(2);
|
||||
const argOf = (name, fallback) => {
|
||||
const i = argv.indexOf(name);
|
||||
return i !== -1 && argv[i + 1] ? argv[i + 1] : fallback;
|
||||
};
|
||||
const PORT = argOf("--port", "9333");
|
||||
const URL_ = argOf("--url", "http://127.0.0.1:8095/");
|
||||
const OUT = argOf("--out", "/tmp/codexweb.png");
|
||||
const SETTLE = Number(argOf("--settle", "6000"));
|
||||
|
||||
const version = await (await fetch(`http://127.0.0.1:${PORT}/json/version`)).json();
|
||||
const ws = new WebSocket(version.webSocketDebuggerUrl);
|
||||
let seq = 0;
|
||||
const pending = new Map();
|
||||
const events = [];
|
||||
|
||||
const send = (method, params = {}, sessionId) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const id = ++seq;
|
||||
pending.set(id, { resolve, reject });
|
||||
const msg = { id, method, params };
|
||||
if (sessionId) msg.sessionId = sessionId;
|
||||
ws.send(JSON.stringify(msg));
|
||||
});
|
||||
|
||||
ws.addEventListener("message", (ev) => {
|
||||
const msg = JSON.parse(ev.data);
|
||||
if (msg.method) {
|
||||
if (msg.method === "Runtime.consoleAPICalled") {
|
||||
const text = (msg.params.args || [])
|
||||
.map((a) => a.value ?? a.description ?? a.unserializableValue ?? "")
|
||||
.join(" ")
|
||||
.slice(0, 600);
|
||||
events.push(`[${msg.params.type}] ${text}`);
|
||||
} else if (msg.method === "Runtime.exceptionThrown") {
|
||||
const d = msg.params.exceptionDetails;
|
||||
events.push(`[EXCEPTION] ${d.exception?.description ?? d.text}`.slice(0, 2200));
|
||||
} else if (msg.method === "Log.entryAdded") {
|
||||
const e = msg.params.entry;
|
||||
events.push(`[log:${e.level}] ${e.text}`.slice(0, 500));
|
||||
}
|
||||
}
|
||||
if (!msg.id || !pending.has(msg.id)) return;
|
||||
const { resolve, reject } = pending.get(msg.id);
|
||||
pending.delete(msg.id);
|
||||
msg.error ? reject(new Error(JSON.stringify(msg.error).slice(0, 400))) : resolve(msg.result);
|
||||
});
|
||||
|
||||
await new Promise((r) => ws.addEventListener("open", r, { once: true }));
|
||||
|
||||
const { targetInfos } = await send("Target.getTargets");
|
||||
const pages = targetInfos.filter((t) => t.type === "page");
|
||||
const main = pages.find((t) => t.url.includes("127.0.0.1")) ?? pages[0];
|
||||
if (!main) {
|
||||
console.error("no page target");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { sessionId } = await send("Target.attachToTarget", { targetId: main.targetId, flatten: true });
|
||||
await send("Page.enable", {}, sessionId);
|
||||
await send("Runtime.enable", {}, sessionId);
|
||||
await send("Log.enable", {}, sessionId);
|
||||
|
||||
await send("Page.navigate", { url: URL_ }, sessionId);
|
||||
// Runtime.enable replays the previous page's console buffer; drop it so the
|
||||
// report only covers this load.
|
||||
events.length = 0;
|
||||
await new Promise((r) => setTimeout(r, SETTLE));
|
||||
|
||||
const state = await send(
|
||||
"Runtime.evaluate",
|
||||
{
|
||||
expression: `(() => {
|
||||
const root = document.querySelector('#root');
|
||||
const text = (root?.innerText || '').replace(/\\s+/g, ' ').trim();
|
||||
return JSON.stringify({
|
||||
url: location.href,
|
||||
title: document.title,
|
||||
rootChildren: root ? root.children.length : -1,
|
||||
rootBytes: root ? root.innerHTML.length : 0,
|
||||
textLength: text.length,
|
||||
textSample: text.slice(0, 500),
|
||||
bootstrapped: !!globalThis.__codexBootstrapped,
|
||||
hasShim: typeof globalThis.__codexHost,
|
||||
}, null, 1);
|
||||
})()`,
|
||||
returnByValue: true,
|
||||
},
|
||||
sessionId,
|
||||
);
|
||||
|
||||
console.log("STATE:", state.result.value);
|
||||
console.log(`\nEVENTS (${events.length}):`);
|
||||
for (const e of events.slice(0, 60)) console.log(" " + e);
|
||||
|
||||
const shot = await send("Page.captureScreenshot", { format: "png" }, sessionId);
|
||||
fs.writeFileSync(OUT, Buffer.from(shot.data, "base64"));
|
||||
console.log(`\nscreenshot: ${OUT} (${fs.statSync(OUT).size} bytes)`);
|
||||
ws.close();
|
||||
Reference in new issue
Block a user