Skip to content

Playground#309

Open
arcanis wants to merge 7 commits into
mainfrom
mael/playground
Open

Playground#309
arcanis wants to merge 7 commits into
mainfrom
mael/playground

Conversation

@arcanis

@arcanis arcanis commented Jun 12, 2026

Copy link
Copy Markdown
Member

Work in progress


Note

Medium Risk
Touches shared HTTP/TLS client configuration across zpm and zpm-switch with 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-bin WASM artifact from website/public/browserpod/.

Rust changes enable compiling Yarn for the BrowserPod target (wasm64 + target_vendor = "browserpod"): reqwest TLS 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::Wasm64 is recognized in system metadata, and build:browserpod-yarn installs the BrowserPod toolchain and emits yarn-bin.wasm plus 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; dialoguer is 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.

arcanis and others added 6 commits June 12, 2026 19:06
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"
@netlify

netlify Bot commented Jun 12, 2026

Copy link
Copy Markdown

Deploy Preview for yarn-v6 ready!

Name Link
🔨 Latest commit 4d90be3
🔍 Latest deploy log https://app.netlify.com/projects/yarn-v6/deploys/6a2c84cd9fd3c7000833e4d2
😎 Deploy Preview https://deploy-preview-309--yarn-v6.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Create PR

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.

Comment thread website/src/components/playground/PlaygroundTerminal.tsx Outdated
Comment thread website/src/pages/playground.astro
@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown

⏱️ Benchmark Results

gatsby install-full-cold

Metric Base Head Difference
Mean 4.392s 4.368s -0.54% ✅
Median 4.403s 4.371s -0.71% ✅
Min 4.237s 4.191s
Max 4.496s 4.475s
Std Dev 0.056s 0.060s
📊 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

Metric Base Head Difference
Mean 1.278s 1.274s -0.31% ✅
Median 1.278s 1.276s -0.13% ✅
Min 1.258s 1.258s
Max 1.312s 1.290s
Std Dev 0.011s 0.009s
📊 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)

Metric Base Head Difference
Mean 0.344s 0.344s -0.13% ✅
Median 0.343s 0.343s -0.12% ✅
Min 0.338s 0.340s
Max 0.352s 0.355s
Std Dev 0.004s 0.003s
📊 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

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

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.

Create PR

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();
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4d90be3. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants