Conversation
UiBundle gains optional modules and diagnostics. ui-page turns a bundle into base64 data: URLs with a trailing sourceURL and an import map keyed in the gadget: namespace, shared by the live iframe and browser export.
Preview:
|
|
LGTM! |
buildUiBundle walks from client.js with es-module-lexer's /js build, rewrites each relative import to its gadget: key, and reports the import rules' violations as diagnostics. A use-role viewer receives only what client.js reaches, never server.js or modules only it imports.
The export page adds the bundle's import map, admitted by a per-render CSP nonce rather than 'unsafe-inline', and its runtime sets gadget, RpcTarget and RpcStub as globals instead of prepending them to client.js. Export fails with the first fatal diagnostic before launching a browser.
The prelude becomes a runtime module script ahead of the entry, setting gadget, RpcTarget and RpcStub as globals, so nothing is prepended to client.js and its line numbers are exact. Modules load through the import map; errors are forwarded with their file, line and column, including SyntaxErrors and failed module loads; diagnostics reach the gadget console, and a fatal one replaces the frame.
Covers the import rules, which ones apply only to client code, the client globals, and that everything client.js reaches is sent to anyone who can use the gadget.
| * above, for caching. Or... maybe we should actually serve over RPC, but also employ the | ||
| * Cache API in the browser? Or some other local storage? | ||
| */ | ||
| jsCode: string; |
There was a problem hiding this comment.
What if there are cyclic imports such that another module imports client.js?
Maybe we should say: If there are multiple files, we don't send jsCode at all. We only send modules, plus maybe mainModule to name the entrypoint?
(Eventually we can phase out jsCode and always send modules, though probably only after clients have updated -- an update which totally breaks gadgets until everyone refreshes would be too disruptive.)
| modules?: UiModule[]; | ||
|
|
||
| /** Problems found while collecting the UI's modules. Absent when there are none. */ | ||
| diagnostics?: UiDiagnostic[]; |
There was a problem hiding this comment.
This diagnostics representation strikes me as overcomplicated and YAGNI.
What if:
- Dynamic imports aren't supported for now. What's even the point of a dynamic import when the code is all downloaded upfront anyway?
- Any error just causes getUiBundle() itself to throw?
Then on the client side we catch the getUiBundle() error and provide a one-click button to report it to the agent.
| window.parent.postMessage("handshake", "*", [port2]); | ||
| gadget = newMessagePortRpcSession(port1); | ||
| // RPC stub to the gadget's server-side Durable Object. | ||
| globalThis.gadget = newMessagePortRpcSession(port1); |
There was a problem hiding this comment.
I've never really liked the hack of gadget simply being a global, and I like it less in a world of multiple modules and large applications that import npm libraries and so on. gadget is a capability. Some random npm library shouldn't just be able to grab it out of the global scope...
Relatedly, I want to introduce a second RPC stub that talks to the workshop UI, to do things like display a specific agent in the chat sidebar.
What if, when using modules, you have to export a main function:
export default {
async function main(gadget, workshopUi) {
...
}
}
And RpcStub and RpcTarget should of course be imported from "capnweb" instead of assumed to exist as globals? (We can special-case "capnweb" for now until we have proper npm imports.)
…dules
A multi-file UI's bundle is now {modules} with client.js first, and a
single-file one stays {jsCode}. buildUiBundle throws on the first import
that breaks the rules for UI code instead of returning diagnostics, so
UiDiagnostic, the warnings and export's separate fatal check are gone. A
bad string-literal import() still rejects only when it runs. The total
size cap is dropped: it fired well below any limit the bundle hits.
Matches the live iframe's policy instead of minting a per-render nonce. data: scripts already let gadget code run anything in the page.
A failed load shows its message in place of the frame and sends it to the gadget console, which the composer can attach for the agent. It counts as loaded, so the agent's next code change reloads the view.
cba84fa to
3d99557
Compare
|
LGTM! |
Eval resultsVerdict: ⚪ Unchanged. No task moved beyond what 10 runs can tell apart from noise.
Failed checks
|
🔬 Eval runs reviewPerformanceAcross 10 runs per side, pass rates went from 100% to 90% for change-calendar, 80% for chess, 100% for incident-desk, and 60% for worker-logs; comparison.json classifies every change as within noise. Mean run costs fell respectively from $0.0292 → $0.0252, $0.0582 → $0.0456, $0.0282 → $0.0245, and $0.0229 → $0.0166, with fewer steps on every task, but early failures truncate the candidate’s work and the differences overlap individual-run variation. Worker Logs lost the most runs, four at turn 1, while chess incurred the largest retry overhead, with unmatched edits rising from 6 to 26; cache hits barely moved, and significant cache-break changes were mixed rather than a consistent improvement. ⚪ VERDICT: NO REGRESSION FROM THIS PRThe observed failures do not follow from the diff’s multi-file UI support or import guidance, pass-rate changes remain within noise, and neither the lower costs nor the mixed caching changes establish an attributable improvement. TriageFailure modes
Tool errors
Prompt cache
What to do
|
client.jscan now import the gadget's other.jsfiles, asserver.jsalready can, so a UI no longer has to fit in one 512K-character file. This work in done in such a way that, if, in the future, we pursue TypeScript and bundling it can arrive without gadget code changing.client.jsreaches, so a use-role viewer gets nothing else (neverserver.js)gadget:key, since relative imports fail insidedata:modulesjsCode, so tabs opened before this change still load single-file gadgetsdata:URL through an import map, withsourceURLlast so errors name the right file and linegadget,RpcTargetandRpcStubglobals before any gadget module runs, with nothing prepended toclient.jsThe live iframe's CSP is unchanged. The export page adds a per-render nonce so its import map runs under
script-src data:. The agent prompt covers splitting files, the import rules, and that anyone who can use a gadget receives everythingclient.jsreaches.