Playground#309
Conversation
BrowserPod currently requires the usage of is own openssl build for networking. This will change in the future and this can be reverted then.
linux-raw-sys does not support the BrowserPod target. In the future a patched version will be provided with the BrowserPod rust toolchain, and this could be reverted.
BrowserPod matches target_arch = "wasm64"
✅ Deploy Preview for yarn-v6 ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: API key logged to console
- Removed console.log statement that was exposing the BrowserPod API key to browser devtools.
- ✅ Fixed: Share always shows Copied
- Removed error handler and fallback that incorrectly showed 'Copied' on clipboard failures, now only shows on success.
Or push these changes by commenting:
@cursor push b12af016ac
Preview (b12af016ac)
diff --git a/website/src/components/playground/PlaygroundTerminal.tsx b/website/src/components/playground/PlaygroundTerminal.tsx
--- a/website/src/components/playground/PlaygroundTerminal.tsx
+++ b/website/src/components/playground/PlaygroundTerminal.tsx
@@ -224,7 +224,6 @@
writeLines(term, [`${cyan}[browserpod]${reset} Booting pod...`]);
- console.log(`Booting pod...`, apiKey);
const pod = await BrowserPod.boot({apiKey});
if (disposed || !term)
diff --git a/website/src/pages/playground.astro b/website/src/pages/playground.astro
--- a/website/src/pages/playground.astro
+++ b/website/src/pages/playground.astro
@@ -56,9 +56,7 @@
};
if (navigator.clipboard && window.isSecureContext)
- navigator.clipboard.writeText(location.href).then(done, done);
- else
- done();
+ navigator.clipboard.writeText(location.href).then(done);
});
})();
</script>You can send follow-ups to the cloud agent here.
⏱️ Benchmark Resultsgatsby install-full-cold
📊 Raw benchmark data (gatsby install-full-cold)Base times: 4.344s, 4.496s, 4.429s, 4.306s, 4.237s, 4.365s, 4.422s, 4.331s, 4.422s, 4.395s, 4.429s, 4.418s, 4.396s, 4.343s, 4.434s, 4.447s, 4.297s, 4.368s, 4.320s, 4.379s, 4.408s, 4.457s, 4.409s, 4.374s, 4.442s, 4.417s, 4.398s, 4.375s, 4.450s, 4.453s Head times: 4.402s, 4.401s, 4.377s, 4.348s, 4.313s, 4.398s, 4.350s, 4.395s, 4.400s, 4.318s, 4.343s, 4.377s, 4.378s, 4.378s, 4.361s, 4.449s, 4.475s, 4.353s, 4.362s, 4.191s, 4.356s, 4.361s, 4.357s, 4.310s, 4.378s, 4.226s, 4.400s, 4.462s, 4.366s, 4.461s gatsby install-cache-only
📊 Raw benchmark data (gatsby install-cache-only)Base times: 1.273s, 1.280s, 1.282s, 1.289s, 1.275s, 1.276s, 1.260s, 1.274s, 1.282s, 1.279s, 1.268s, 1.287s, 1.296s, 1.287s, 1.272s, 1.277s, 1.284s, 1.273s, 1.280s, 1.290s, 1.291s, 1.280s, 1.312s, 1.280s, 1.273s, 1.273s, 1.259s, 1.258s, 1.267s, 1.273s Head times: 1.275s, 1.265s, 1.267s, 1.279s, 1.290s, 1.279s, 1.283s, 1.278s, 1.285s, 1.281s, 1.285s, 1.269s, 1.266s, 1.280s, 1.265s, 1.258s, 1.282s, 1.266s, 1.262s, 1.275s, 1.280s, 1.288s, 1.279s, 1.262s, 1.272s, 1.278s, 1.264s, 1.269s, 1.278s, 1.272s gatsby install-cache-and-lock (warm, with lockfile)
📊 Raw benchmark data (gatsby install-cache-and-lock (warm, with lockfile))Base times: 0.348s, 0.351s, 0.349s, 0.350s, 0.346s, 0.348s, 0.343s, 0.341s, 0.348s, 0.343s, 0.345s, 0.342s, 0.344s, 0.343s, 0.338s, 0.347s, 0.347s, 0.352s, 0.338s, 0.339s, 0.339s, 0.344s, 0.343s, 0.343s, 0.343s, 0.346s, 0.339s, 0.344s, 0.338s, 0.341s Head times: 0.346s, 0.342s, 0.343s, 0.343s, 0.341s, 0.340s, 0.344s, 0.342s, 0.346s, 0.355s, 0.344s, 0.342s, 0.342s, 0.344s, 0.343s, 0.341s, 0.343s, 0.342s, 0.342s, 0.343s, 0.347s, 0.343s, 0.344s, 0.340s, 0.347s, 0.344s, 0.343s, 0.345s, 0.343s, 0.342s |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: BrowserPod sessions leak on remount
- Added disposed check after writeProjectFiles and stored pod reference for cleanup to prevent multiple BrowserPod instances from running simultaneously.
Or push these changes by commenting:
@cursor push 1370c6a18b
Preview (1370c6a18b)
diff --git a/website/src/components/playground/PlaygroundTerminal.tsx b/website/src/components/playground/PlaygroundTerminal.tsx
--- a/website/src/components/playground/PlaygroundTerminal.tsx
+++ b/website/src/components/playground/PlaygroundTerminal.tsx
@@ -1,6 +1,6 @@
-import '@xterm/xterm/css/xterm.css';
-import type {Terminal as XtermTerminal} from '@xterm/xterm';
-import {useEffect, useRef} from 'react';
+import "@xterm/xterm/css/xterm.css";
+import type { Terminal as XtermTerminal } from "@xterm/xterm";
+import { useEffect, useRef } from "react";
interface Props {
files: Array<PlaygroundFile>;
@@ -20,14 +20,18 @@
rows?: number;
onOutput: (buffer: ArrayBuffer) => void;
}): Promise<BrowserPodTerminal>;
- createDirectory(path: string, opts?: {recursive?: boolean}): Promise<void>;
+ createDirectory(path: string, opts?: { recursive?: boolean }): Promise<void>;
createFile(path: string, mode: `binary` | `utf-8`): Promise<BrowserPodFile>;
- run(executable: string, args: Array<string>, opts: {
- cwd?: string;
- echo?: boolean;
- env?: Array<string>;
- terminal: BrowserPodTerminal;
- }): Promise<unknown>;
+ run(
+ executable: string,
+ args: Array<string>,
+ opts: {
+ cwd?: string;
+ echo?: boolean;
+ env?: Array<string>;
+ terminal: BrowserPodTerminal;
+ },
+ ): Promise<unknown>;
};
type BrowserPodFile = {
@@ -65,7 +69,12 @@
function getBrowserPodApiKey() {
const env = import.meta.env as Record<string, string | undefined>;
- return env.PUBLIC_BROWSERPOD_API_KEY ?? env.VITE_BPAPIKEY ?? env.VITE_BP_APIKEY ?? ``;
+ return (
+ env.PUBLIC_BROWSERPOD_API_KEY ??
+ env.VITE_BPAPIKEY ??
+ env.VITE_BP_APIKEY ??
+ ``
+ );
}
function dirname(path: string) {
@@ -74,11 +83,9 @@
}
function formatUnknownError(error: unknown) {
- if (error instanceof Error)
- return error.message;
+ if (error instanceof Error) return error.message;
- if (typeof error === `string`)
- return error;
+ if (typeof error === `string`) return error;
if (error === undefined)
return `BrowserPod rejected without an error message`;
@@ -90,11 +97,16 @@
}
}
-async function writeProjectFiles(pod: BrowserPodInstance, files: Array<PlaygroundFile>) {
+async function writeProjectFiles(
+ pod: BrowserPodInstance,
+ files: Array<PlaygroundFile>,
+) {
try {
- await pod.createDirectory(PROJECT_PATH, {recursive: true});
+ await pod.createDirectory(PROJECT_PATH, { recursive: true });
} catch (error) {
- throw new Error(`Failed to create ${PROJECT_PATH}: ${formatUnknownError(error)}`);
+ throw new Error(
+ `Failed to create ${PROJECT_PATH}: ${formatUnknownError(error)}`,
+ );
}
const directories = new Set<string>();
@@ -102,15 +114,14 @@
for (const file of files) {
const directory = dirname(file.path);
- if (directory)
- directories.add(directory);
+ if (directory) directories.add(directory);
}
for (const directory of directories) {
const path = `${PROJECT_PATH}/${directory}`;
try {
- await pod.createDirectory(path, {recursive: true});
+ await pod.createDirectory(path, { recursive: true });
} catch (error) {
throw new Error(`Failed to create ${path}: ${formatUnknownError(error)}`);
}
@@ -132,10 +143,9 @@
async function writeYarnBinary(pod: BrowserPodInstance) {
const response = await fetch(YARN_BIN_ASSET);
- if (!response.ok)
- return false;
+ if (!response.ok) return false;
- await pod.createDirectory(YARN_BIN_DIR, {recursive: true});
+ await pod.createDirectory(YARN_BIN_DIR, { recursive: true });
const podFile = await pod.createFile(YARN_BIN_PATH, `binary`);
await podFile.write(await response.arrayBuffer());
@@ -146,40 +156,41 @@
async function writeShellConfig(pod: BrowserPodInstance) {
const podFile = await pod.createFile(BASHRC_PATH, `utf-8`);
- await podFile.write([
- `export PATH="${YARN_BIN_DIR}:$PATH"`,
- `export npm_config_user_agent="yarn-playground"`,
- `export PS1="\\[\\e[38;2;134;239;172m\\]yarn-playground\\[\\e[0m\\] \\[\\e[38;2;148;163;184m\\]\\w\\[\\e[0m\\] $ "`,
- ``,
- `cd ${PROJECT_PATH}`,
- ``,
- ].join(`\n`));
+ await podFile.write(
+ [
+ `export PATH="${YARN_BIN_DIR}:$PATH"`,
+ `export npm_config_user_agent="yarn-playground"`,
+ `export PS1="\\[\\e[38;2;134;239;172m\\]yarn-playground\\[\\e[0m\\] \\[\\e[38;2;148;163;184m\\]\\w\\[\\e[0m\\] $ "`,
+ ``,
+ `cd ${PROJECT_PATH}`,
+ ``,
+ ].join(`\n`),
+ );
await podFile.close();
}
-export function PlaygroundTerminal({files, version}: Props) {
+export function PlaygroundTerminal({ files, version }: Props) {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const container = containerRef.current;
- if (!container)
- return undefined;
+ if (!container) return undefined;
let disposed = false;
let term: XtermTerminal | null = null;
let browserPodTerminal: BrowserPodTerminal | null = null;
let resizeObserver: ResizeObserver | null = null;
let focusTerm: (() => void) | null = null;
+ let pod: BrowserPodInstance | null = null;
async function start() {
- const [{Terminal}, {FitAddon}] = await Promise.all([
+ const [{ Terminal }, { FitAddon }] = await Promise.all([
import(`@xterm/xterm`),
import(`@xterm/addon-fit`),
]);
- if (disposed)
- return;
+ if (disposed) return;
const fitAddon = new FitAddon();
term = new Terminal({
@@ -218,22 +229,20 @@
term.loadAddon(fitAddon);
term.open(container);
- term.onData(data => browserPodTerminal?.readData(data));
+ term.onData((data) => browserPodTerminal?.readData(data));
focusTerm = () => term?.focus();
container.addEventListener(`pointerdown`, focusTerm);
requestAnimationFrame(() => {
- if (!term || disposed)
- return;
+ if (!term || disposed) return;
fitAddon.fit();
term.focus();
});
resizeObserver = new ResizeObserver(() => {
- if (!term || disposed)
- return;
+ if (!term || disposed) return;
fitAddon.fit();
});
@@ -261,22 +270,21 @@
}
try {
- const {BrowserPod} = await import(/* @vite-ignore */ BROWSERPOD_RUNTIME_URL) as {BrowserPod: BrowserPodApi | null};
+ const { BrowserPod } = (await import(
+ /* @vite-ignore */ BROWSERPOD_RUNTIME_URL
+ )) as { BrowserPod: BrowserPodApi | null };
- if (!BrowserPod)
- throw new Error(`BrowserPod runtime failed to load`);
+ if (!BrowserPod) throw new Error(`BrowserPod runtime failed to load`);
- const pod = await BrowserPod.boot({apiKey});
+ pod = await BrowserPod.boot({ apiKey });
- if (disposed || !term)
- return;
+ if (disposed || !term) return;
browserPodTerminal = await pod.createCustomTerminal({
cols: term.cols,
rows: term.rows,
- onOutput: buffer => {
- if (!term || disposed)
- return;
+ onOutput: (buffer) => {
+ if (!term || disposed) return;
term.write(new Uint8Array(buffer));
},
@@ -284,6 +292,8 @@
await writeProjectFiles(pod, files);
+ if (disposed || !term) return;
+
if (await writeYarnBinary(pod)) {
writeLines(term, [
`${cyan}[browserpod]${reset} Mounted ${YARN_BIN_ASSET}`,
@@ -304,11 +314,13 @@
`${cyan}[browserpod]${reset} Opening BrowserPod bash in the mounted project instead.`,
]);
- await pod.run(`bash`, [], {cwd: PROJECT_PATH, terminal: browserPodTerminal});
+ await pod.run(`bash`, [], {
+ cwd: PROJECT_PATH,
+ terminal: browserPodTerminal,
+ });
}
} catch (error) {
- if (!term || disposed)
- return;
+ if (!term || disposed) return;
writeLines(term, [
`${red}[browserpod]${reset} ${formatUnknownError(error)}`,
@@ -321,12 +333,18 @@
return () => {
disposed = true;
+ pod = null;
+ browserPodTerminal = null;
resizeObserver?.disconnect();
- if (focusTerm)
- container.removeEventListener(`pointerdown`, focusTerm);
+ if (focusTerm) container.removeEventListener(`pointerdown`, focusTerm);
term?.dispose();
};
}, [files, version]);
- return <div ref={containerRef} className={`playground-terminal-mount absolute inset-[20px_22px] min-h-0 min-w-0 rounded-xl max-[560px]:inset-3.5`} />;
+ return (
+ <div
+ ref={containerRef}
+ className={`playground-terminal-mount absolute inset-[20px_22px] min-h-0 min-w-0 rounded-xl max-[560px]:inset-3.5`}
+ />
+ );
}You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit 4d90be3. Configure here.
| if (focusTerm) | ||
| container.removeEventListener(`pointerdown`, focusTerm); | ||
| term?.dispose(); | ||
| }; |
There was a problem hiding this comment.
BrowserPod sessions leak on remount
Medium Severity
The terminal effect calls BrowserPod.boot and pod.run but cleanup only disposes xterm. Changing presets or when files/version change retriggers the effect without stopping the prior pod or shell, so multiple BrowserPod instances and bash sessions can run and keep consuming memory and API quota.
Reviewed by Cursor Bugbot for commit 4d90be3. Configure here.



Work in progress
Note
Medium Risk
Touches shared HTTP/TLS client configuration across
zpmandzpm-switchwith target-specific behavior; native installs should stay on rustls while browserpod is a separate path, but regressions in TLS or client cert handling are possible.Overview
Adds a Playground page on the docs site where users browse preset sample projects in Monaco and run commands in an xterm terminal backed by BrowserPod, with optional mounting of a Yarn
yarn-binWASM artifact fromwebsite/public/browserpod/.Rust changes enable compiling Yarn for the BrowserPod target (
wasm64+target_vendor = "browserpod"):reqwestTLS is split so browserpod builds use default/native TLS while native builds keep rustls; HTTP client setup and PEM client identity follow the same split (identity certs error on browserpod).Cpu::Wasm64is recognized in system metadata, andbuild:browserpod-yarninstalls the BrowserPod toolchain and emitsyarn-bin.wasmplus a manifest.The playground route is served with COOP/COEP headers (Astro middleware, Vite dev/preview plugin, and static
_headers) so SharedArrayBuffer works. Nav gains a Playground link;dialogueris trimmed to password-only features to reduce transitive deps in the lockfile.Reviewed by Cursor Bugbot for commit 4d90be3. Bugbot is set up for automated code reviews on this repo. Configure here.