Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 0.3.7

### Patch Changes

- 2447315: Rebuild the browser-hosted Vibe platform example with React and the shared MCP-B design system while preserving its real actor, editor, preview, export, reload, and namespace-typing coverage.

## 0.3.6

### Patch Changes
Expand Down
7 changes: 6 additions & 1 deletion examples/vibe-platform/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ SDK external to the browser host; `agent.worker.ts` blob-imports the result and
class through `createActorContainer`. Saving any `server/*` file drains requests, releases storage,
and places a new instance over the same SQLite files, so code is volatile while state is durable.

**The studio uses the MCP-B design system.** React 19 owns the shell and state; the shared React
FileTree, Button, WebPreview, and Terminal components provide the product UI, while the shared Lit
CodeMirror custom element remains the editor.

**The sandbox has one narrow capability.** The preview keeps `sandbox="allow-scripts"`. Its `fetch`
wrapper sends only `/api/*` requests to the parent over a one-request `MessageChannel`; the page
forwards the flattened request to the authored actor and returns its real status, headers, and body.
Expand All @@ -63,7 +67,8 @@ Agents SDK's `nodejs_compat` flag; `package.json` pins the same Agents SDK versi

| Path | What it is |
| --- | --- |
| `index.html`, `src/main.ts` | The page: supervisor, editor, two builds, preview bridge, export. Plain DOM. |
| `index.html`, `src/main.tsx` | The React supervisor: design-system shell, two builds, preview bridge, export. |
| `src/studio.css` | Page-grid composition around the shared design-system components. |
| `src/wire.ts` | The page↔actor protocol, and why it is shaped for capnweb. |
| `src/worker/host.worker.ts` | The workspace host: pool, ports, container, RPC. |
| `src/worker/agent.worker.ts` | The authored-class host: evaluation, stable pool, container, RPC. |
Expand Down
136 changes: 2 additions & 134 deletions examples/vibe-platform/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,141 +4,9 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>vibe platform — a Durable Object in a page</title>
<style>
:root {
color-scheme: light dark;
--edge: color-mix(in oklab, currentColor 15%, transparent);
--muted: color-mix(in oklab, currentColor 55%, transparent);
--panel: color-mix(in oklab, canvas 92%, canvastext);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
height: 100vh;
display: flex;
flex-direction: column;
font:
13px/1.5 ui-monospace,
SFMono-Regular,
Menlo,
monospace;
}
header {
display: flex;
gap: 0.75rem;
align-items: center;
padding: 0.5rem 0.75rem;
border-bottom: 1px solid var(--edge);
}
header h1 {
font-size: 13px;
margin: 0 0.5rem 0 0;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.spacer {
flex: 1;
}
#status {
color: var(--muted);
}
button {
font: inherit;
padding: 0.25rem 0.75rem;
border: 1px solid var(--edge);
border-radius: 0.25rem;
background: var(--panel);
color: inherit;
cursor: pointer;
}
button:hover:not(:disabled) {
border-color: currentColor;
}
button:disabled {
opacity: 0.5;
cursor: default;
}
#banner {
margin: 0;
padding: 0.75rem;
background: #fde7e7;
color: #7a1c1c;
border-bottom: 1px solid #f0b8b8;
}
main {
flex: 1;
min-height: 0;
display: grid;
grid-template-columns: 15rem minmax(18rem, 2fr) 3fr;
grid-template-rows: 1fr 10rem;
}
#files {
grid-row: 1 / 3;
border-right: 1px solid var(--edge);
overflow: auto;
padding: 0.5rem 0;
}
#files button {
display: block;
width: 100%;
text-align: left;
border: 0;
border-radius: 0;
background: none;
padding: 0.2rem 0.75rem;
}
#files button[aria-current="true"] {
background: var(--panel);
font-weight: 700;
}
#editor {
grid-row: 1 / 3;
border: 0;
border-right: 1px solid var(--edge);
padding: 0.75rem;
font: inherit;
tab-size: 2;
resize: none;
background: none;
color: inherit;
}
#preview {
border: 0;
width: 100%;
height: 100%;
background: white;
}
#log {
margin: 0;
padding: 0.5rem 0.75rem;
overflow: auto;
border-top: 1px solid var(--edge);
white-space: pre-wrap;
color: var(--muted);
}
#log .err {
color: #d33c3c;
}
</style>
</head>
<body>
<header>
<h1>vibe platform</h1>
<button id="save">save</button>
<button id="build">build &amp; run</button>
<button id="export">export</button>
<span class="spacer"></span>
<span id="status">booting…</span>
</header>
<p id="banner" hidden></p>
<main>
<nav id="files" aria-label="workspace files"></nav>
<textarea id="editor" spellcheck="false" aria-label="file contents"></textarea>
<iframe id="preview" title="preview" sandbox="allow-scripts"></iframe>
<pre id="log"></pre>
</main>
<script type="module" src="/src/main.ts"></script>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
8 changes: 8 additions & 0 deletions examples/vibe-platform/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,22 @@
},
"dependencies": {
"@mcp-b/do-runtime": "workspace:*",
"@mcp-b/interactive-components": "0.2.16",
"@mcp-b/react-components": "0.46.3",
"@rolldown/browser": "^1.1.5",
"@sqlite.org/sqlite-wasm": "3.53.0-build1",
"agents": "0.21.0",
"capnweb": "0.10.0",
"lit": "3.3.1",
"lit-html": "3.3.1",
"react": "19.3.0-canary-711c445b-20260722",
"react-dom": "19.3.0-canary-711c445b-20260722",
"unenv": "2.0.0-rc.24"
},
"devDependencies": {
"@cloudflare/workers-types": "5.20260820.1",
"@types/react": "19.2.18",
"@types/react-dom": "19.2.4",
"vite": "^8.2.1"
}
}
30 changes: 16 additions & 14 deletions examples/vibe-platform/scripts/e2e.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,14 @@ page.on("console", (message) => {
});

const preview = () => page.frameLocator("#preview");
const file = (path) => page.locator(`#files button[data-path="${path}"]`);
const file = (path) => page.locator(`#files [data-path="${path}"] button`);
const editorValue = () => page.locator("#editor").evaluate((element) => element.value);
const fillEditor = (value) => page.locator("#editor .cm-content").fill(value);
const builtOk = () =>
page.waitForFunction(() => /^built in \d+ms$/.test(document.querySelector("#status").textContent), {
timeout: TIMEOUT,
});
page.waitForFunction(
() => /^built in \d+ms$/.test(document.querySelector("#status")?.textContent ?? ""),
{ timeout: TIMEOUT },
);

const previewApi = (method = "GET") =>
preview()
Expand Down Expand Up @@ -244,10 +247,10 @@ try {
undefined,
{ timeout: TIMEOUT },
);
const source = await page.locator("#editor").inputValue();
const source = await editorValue();
agentSourceForExport = source.replace("a quiet hello", "a cheerful hello");
if (agentSourceForExport === source) throw new Error("agent edit marker was missing");
await page.locator("#editor").fill(agentSourceForExport);
await fillEditor(agentSourceForExport);
await page.locator("#save").click();
await builtOk();
await page.waitForFunction(
Expand All @@ -264,7 +267,7 @@ try {

await step("a user DO syntax error reaches the log and recovers after it is fixed", async () => {
const broken = agentSourceForExport.replace("export class", "export clss");
await page.locator("#editor").fill(broken);
await fillEditor(broken);
await page.locator("#save").click();
await page.waitForFunction(
() => document.querySelector("#status").textContent === "saving failed",
Expand All @@ -276,7 +279,7 @@ try {
throw new Error("the syntax error message did not reach the UI log");
}

await page.locator("#editor").fill(agentSourceForExport);
await fillEditor(agentSourceForExport);
await page.locator("#save").click();
await builtOk();
const state = await previewApi();
Expand All @@ -288,7 +291,7 @@ try {
"export class MyAgent extends Agent {",
'export class MyAgent extends Agent {\n constructor(ctx, env) {\n super(ctx, env);\n throw new Error("e2e constructor failed");\n }',
);
await page.locator("#editor").fill(broken);
await fillEditor(broken);
await page.locator("#save").click();
await page.waitForFunction(
() => document.querySelector("#status").textContent === "saving failed",
Expand All @@ -301,7 +304,7 @@ try {
{ timeout: TIMEOUT },
);

await page.locator("#editor").fill(agentSourceForExport);
await fillEditor(agentSourceForExport);
await page.locator("#save").click();
await builtOk();
const state = await previewApi();
Expand All @@ -313,14 +316,13 @@ try {
// 5 -----------------------------------------------------------------------
await step("editing a file in the UI, saving it, and rebuilding", async () => {
await file("/src/App.tsx").click();
const editor = page.locator("#editor");
await page.waitForFunction(
(marker) => document.querySelector("#editor").value.includes(marker),
TITLE_BEFORE,
{ timeout: TIMEOUT },
);
const source = await editor.inputValue();
await editor.fill(source.replace(TITLE_BEFORE, TITLE_AFTER));
const source = await editorValue();
await fillEditor(source.replace(TITLE_BEFORE, TITLE_AFTER));
await page.locator("#save").click();
await builtOk();
});
Expand All @@ -347,7 +349,7 @@ try {
undefined,
{ timeout: TIMEOUT },
);
const source = await page.locator("#editor").inputValue();
const source = await editorValue();
if (!source.includes(TITLE_AFTER)) {
throw new Error("the reloaded workspace does not contain the edit");
}
Expand Down
Loading