Playground
Edit a zidane agent and run it in your browser, with nothing to install.
The playground runs a zidane agent in your browser tab. The agent is a set of files you edit: its code, its tools and its prompt. The agent, the sandbox its tools use and the model all run on your machine: no server and no API key. For a bigger hosted model, add an OpenRouter key.
What runs where#
A WebContainer is a Node.js runtime that runs in the browser. A page can boot only one, so each one lives in its own iframe.
| Piece | Runs in | Does |
|---|---|---|
| Agent | WebContainer in iframe 1 | Runs your agent's files on Node with zidane |
| Sandbox | WebContainer in iframe 2 | Runs the agent's shell commands and file operations |
| Model | Web Worker, on WebGPU | Runs the model with transformers.js, or the page calls OpenRouter |
| Page | Your tab | Shows the workspace and the chat, and relays between the three |
The agent uses zidane's local provider. Its model requests and tool calls go out as lines on the process's stdout. The page answers them from the model worker or the sandbox, and writes the answers to stdin.
The workspace#
The playground fills the window under the site's header. What the panes hold never makes them bigger: a long prompt scrolls inside the composer, and only the edges below change a pane's size.
| Part | What you do there |
|---|---|
| Agent and Sandbox tabs | Switch between the agent's files and the sandbox's files |
| File tree | Click a file to open it. It updates when the agent, a terminal or a save changes a file |
| Editor | Edit with syntax highlighting. CmdS or CtrlS saves. TypeScript files in the agent project are type-checked (below) |
| Tabs | Drag a tab, or press AltShift← / → on it, to move it. Middle-click closes a tab. A dot marks unsaved changes |
| Preview | The page a server in the sandbox serves, alone or beside the editor (below) |
| Terminals | A jsh shell in each container, one tab each |
| Chat | Send a prompt, pick the model, read the run. A file you attach also lands in the sandbox under attachments/, and the prompt tells the agent where |
A file the agent changes while you have it open reloads in the editor. If you have unsaved edits, a bar asks you to Reload (take the new file) or Overwrite (keep yours).
On a phone the parts are tabs: Files, Editor, Terminal and Chat.
Resize and hide the panes#
On a desktop, drag the edge between two panes to resize them:
| Edge | Resizes |
|---|---|
| Right of the file tree | The tree's width |
| Above the terminals | The terminals' height |
| Left of the chat | The chat's width |
| Left of the preview, beside the editor | The preview's width |
Double-click an edge to put its pane back to the default size. The editor always keeps at least 360 px by 200 px: in a narrow window the tree gives way first, then the chat.
The buttons in the workspace's top row hide and show the file tree, the terminals and the chat. The terminals' own row has a button that hides them too.
The playground remembers the sizes and the hidden panes in this browser, and opens with them the next time.
Check types#
The editor checks the agent's TypeScript files as you type, about 300 ms after you stop. The TypeScript compiler runs in a Web Worker, against zidane's type declarations and Node's. It downloads (about 2 MB compressed) the first time you open a .ts file.
| Action | Result |
|---|---|
| Write a type error | The line shows the error under it, and the chip on the tab row counts errors |
| Hover a name | A card shows its type and docs |
| Cmd-click or Ctrl-click a name | Opens its definition. A definition in zidane or Node opens read-only |
| Find references in the hover card | Lists every use, click one to open it |
Node still runs your files without checking them: a type error does not stop a run.
Preview a server#
When a server in the sandbox starts answering, its page opens in the Preview, beside the editor (Split, in the workspace's top row). Preview in the same row shows it alone. The preview has a reload button, an address bar with the site's icon, and a button that opens the page in a new tab. Type a path such as /about in the address bar to go there.
Switching between the Agent, Sandbox and Preview views keeps the page as it was. The preview shows the last server that started answering. With more than one, the ports menu (the plug button) picks another. The page reloads when a server starts again on its port, and goes when its server stops. While nothing answers with a page, the preview tries again a few times, then says it waits for the server, with a reload button. A server in the agent container gets no preview: both containers share one preview address, which the sandbox owns.
To try it, pick Start from > Scaffold a Vite app and run this in the Sandbox terminal:
npm create vite@latest app -- --template vanilla
cd app && npm install && npm run devThe agent starts a server the same way, with its run_in_terminal tool. Its shell tool waits for the command to end, so the default prompt tells the agent not to start servers with it.
Edit the agent#
The agent container holds these files. Each prompt runs the files as they are saved at that moment.
| File | Holds |
|---|---|
agent.ts | A function that returns the agent, with createAgent |
system.md | The system prompt |
zidane.config.json | name, tools (the tool files to load), codemode, mini, and an optional maxTurns (without it, a prompt runs until the model answers) |
tools/<name>.ts | One tool each: its default export is a zidane tool |
agent.ts gets everything it needs from the playground. The default file:
import type { PlaygroundEnv } from 'zidane-playground'
import { createAgent, localReliabilityBehavior } from 'zidane'
import { capToolOutput, MINI_BEHAVIOR } from 'zidane-playground'
export default function agent({ config, system, tools, provider, execution, session }: PlaygroundEnv) {
const agent = createAgent({
name: config.name,
system,
provider,
execution,
session,
tools,
behavior: {
...localReliabilityBehavior(config.mini ? MINI_BEHAVIOR : {}),
maxTurns: config.maxTurns,
codemode: config.codemode ? { mode: 'only' } : false,
},
})
if (config.mini)
capToolOutput(agent)
return agent
}provider is the model you picked, execution is the sandbox and session keeps the conversation between prompts. Node runs the TypeScript itself: it strips the types and does not check them. A syntax or runtime error fails the run: the error shows in the transcript with its file and line, and in the agent's terminal. So does anything your code logs with console.
Write a tool#
Add tool (the + in the agent's file tree) asks for a name, writes tools/<name>.ts and adds the name to tools in zidane.config.json. A tool is a spec the model reads and an execute that runs when the model calls it:
import type { ToolDef } from 'zidane'
const tool: ToolDef = {
spec: {
name: 'line_count',
description: 'Counts the lines of a file in the sandbox.',
inputSchema: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] },
},
async execute(input, ctx) {
const text = await ctx.execution.readFile(ctx.handle, String(input.path))
return `${text.split('\n').length} lines`
},
}
export default toolMost tool files start as zidane's own tools (basicTools from zidane/presets). Change a description, or wrap execute to change what the tool returns. See Tools for the full tool shape.
Two tool files belong to the playground:
| Tool | Does |
|---|---|
run_in_terminal | Types a command into the sandbox terminal and returns what it printed after wait_ms (default 5,000 ms, at most 30,000). The command keeps running, so the agent uses it for servers. |
agent_files | Lists, reads and writes the agent's own files (agent.ts, system.md, zidane.config.json, tools/<name>.ts). A change applies from the next prompt. |
Start from a template#
Start from replaces the files in both containers. It asks first when it will replace files you changed. It leaves the model and the chat as they are. Your saved projects show above the templates, under Your projects.
| Template | Agent | Sandbox |
|---|---|---|
| Empty | The default agent: eight tools, codemode on, no turn cap | Empty |
| Explain a project | Five tools (no list_files, edit or multi_edit) | A word-count script to explain |
| Fix a failing test | The default agent | sum.js with a bug and its test |
| Scaffold a Vite app | The default agent | A README asking for a Vite app |
| Mini | Small prompt and tools for small windows (below) | Empty |
Start the sandbox project#
When files come in (Start from, a saved project or a share link), the playground starts the sandbox's project as you would by hand. If the sandbox has a package.json, the playground types a command into the Sandbox terminal, from what the file lists:
package.json has | Command |
|---|---|
Dependencies and a dev script | npm install && npm run dev |
A start script and no dev | npm run start (after npm install when it lists dependencies) |
Dependencies, no dev or start | npm install |
| Neither | Nothing |
The Sandbox terminal shows (it opens if you hid it), and the keyboard stays where it was. Whatever ran in that terminal stops first (CtrlC). A server that starts answering opens its page in the Preview. Of the templates, only Explain a project has something to run: its start script prints the word count.
npm install writes package-lock.json. It does not count as a file you changed: Start from replaces it without asking.
Pick a model#
The model pill in the composer lists the models with their window, download size and a Cached mark. The first send with a model downloads it, with the progress under the composer, and keeps it in the browser's cache. Later visits load it from there.
These numbers come from an Apple M4 Pro (20-core GPU) in Chrome 156. The window is the number of tokens the conversation can grow to, prompt and replies together. Prefill is the wait while the model reads the prompt, before the first word of its reply.
| Model | Download | Window | Speed | Prefill at 8K tokens | In real runs |
|---|---|---|---|---|---|
| Qwen3 4B Instruct 2507 | 2.9 GB | 16,384 tokens | 43 tokens/s | 33 s | Default. Created and ran a file in 3 of 3 runs, fixed the failing test in 3 of 3 |
| Qwen3 1.7B | 1.1 GB | 32,768 tokens | 96 tokens/s | 13 s | With Mini, creates and runs a file in about 2 s. With codemode, its scripts often print instead of writing the file |
| Qwen3 0.6B | 570 MB | 32,768 tokens | 119 tokens/s | 9 s | Writes the file but rarely runs it |
onnxruntime-web fails on a single pass over about 2,000 prompt tokens, so the worker reads the prompt in 1,024-token chunks. The models measured and left out, and why, are listed in website/src/playground/models.ts. The playground also downloads the ONNX runtime once (27 MB, 7 MB compressed).
Run OpenRouter models#
OpenRouter serves hosted models from many vendors behind one API. With your OpenRouter API key, the agent runs on them instead of your GPU.
- Open Providers in the status row and turn on OpenRouter.
- Paste your key (create one at openrouter.ai/keys) and click Test key. The panel shows the key's credit and how many models it added.
- Pick a model in the OpenRouter group of the model pill and send. Nothing downloads.
The picker lists the OpenRouter models that support tool calling, with their window, a Reasoning mark and the price per million tokens (prompt, then reply). The agent needs tools, so the panel counts the models it leaves out. The page fetches the list at most once an hour.
| Question | Answer |
|---|---|
| Where is the key? | In this browser's localStorage, under zidane-playground:openrouter:v1. Clear key removes it |
| Who gets it? | Only openrouter.ai. The page sends each request from your browser to OpenRouter. The agent's container sends its requests to the page, which adds the key, so the container never sees it |
| What does it cost? | OpenRouter bills your account at the model's price. Free models cost nothing and have tighter rate limits |
| What if a request fails? | The transcript and the panel show OpenRouter's error: a refused key (401), no credits (402), a rate limit (429) |
Fit a small window#
The meter next to the project tabs shows the tokens of the agent's first request against the model's window, for example 3,157 / 16,384 tokens. It counts your files: the agent container builds the request as a run would, and the page counts it with the model's tokenizer. It recounts after every save. The page has no tokenizer for an OpenRouter model: it estimates 4 characters a token and marks the count with ≈.
When the prompt and a full reply (1,024 tokens) do not fit, the meter turns red and offers Start from Mini. The Mini template shrinks what the model reads:
| Part | Default | Mini |
|---|---|---|
system.md | Six lines on how to work | One line |
| Tools | Eight, with zidane's descriptions | shell, read_file, write_file, with one-line descriptions and only their main parameters |
| Codemode | On | Off |
"mini": true | No | Tool output cut to 1,200 characters, reads of 120 lines or 4 KB at most, old tool results stubbed past 4 KB |
| Max turns | No cap | 4 |
Codemode's own instructions take about 800 tokens. Small models also tend to call it without a description or with a broken script, which is why Mini turns it off. A small model can also repeat the same tool call forever, and Mini's compaction keeps the window from ever filling, so Mini stops a prompt after 4 turns.
Save and share#
Save (next to Start from) keeps both containers' files and the conversation in this browser (IndexedDB). A model-made title names the project, or the first prompt does. Start from › Your projects opens, renames and deletes saved projects. Opening one leaves the model you picked as it is. If the browser refuses a save, an open, a rename or a delete, the reason shows next to the buttons.
Share in the chat header uploads the conversation and both containers' files to the site's share API (/api/share). It gives a link, /playground?s=<id>, that anyone can open in their own playground. A share holds 2 MB at most.
Save and Share keep the agent's own files and the sandbox's files, at most 500 per container. They leave out node_modules, .git, binary files and files over 200,000 characters.
| Action | Result |
|---|---|
| Create link | Uploads the session as it is now. Later messages are not in the link. |
| Update link | Uploads the session again. The old link stops working. |
| Unshare | Deletes the share. Only the browser that made the link can delete it. |
A restored or shared conversation shows in full, but the agent starts without its memory of that session.
Limits#
| Limit | Detail |
|---|---|
| Browsers | Chrome and Edge on a desktop. Firefox only with WebGPU turned on. Safari lacks the credentialless isolation policy WebContainers need here. Phones lack the GPU memory for the model |
| Models | Small models pick the wrong tool, loop, or claim a fix they did not make. Treat a run as a demo of the agent loop |
| Types | Node strips TypeScript types without checking them, and refuses syntax it cannot strip (an enum). The editor's check leaves out Node modules an agent rarely uses (http2, inspector, sqlite, test and a few more): their types read as any |
| Preview | The address bar follows the page through links and history changes. A reload loads the page again from its server |
| Sandbox shell | WebContainer's jsh: node, npm and ls, but no grep, find or sed. It merges stderr into stdout |
| Tools | zidane's basic set without background tasks and sub-agents |
| Sessions | Unsaved files and chats end when you reload the page. Save keeps them in this browser (below) |
The Browser chip in the status row checks for WebGPU and cross-origin isolation on load. When one is missing, the chip says which, and the chat column explains it. Without WebGPU the files, terminals and preview still work. Without isolation the containers cannot start: the editor shows the starter files, read-only.
Host the playground#
WebContainers need an API key on a production origin. The site reads WEBCONTAINER_API_KEY from website/.env at build time and puts it in the bundle of the two container iframes, so anyone can read it there. Restrict the key to the site's origins in your StackBlitz settings. On localhost the page boots WebContainers without the key.
Cross-origin isolation applies to /playground and its files only:
| Header | Value |
|---|---|
Cross-Origin-Opener-Policy | same-origin |
Cross-Origin-Embedder-Policy | credentialless |
vite dev and vite preview set them. For hosting, website/public/_headers lists them in the format Netlify and Cloudflare Pages read. On another host, set the same headers on /playground and /playground/*.
To run it locally, build zidane at the repo root (the agent container mounts that build), then start the site and open localhost:4321/playground. Add ?model=scripted to the URL for a scripted model with no real download.
bun run build
cd website && bun run dev