Skip to content
Draft
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
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ BrowserCode is a browser-based coding sandbox. It's a working example of [Browse

BrowserCode started out as a way to run AI coding CLIs entirely client-side. It's since grown into a full IDE: alongside the CLIs, it can boot and preview web frameworks directly in the browser, so you can prototype an agent's output without ever leaving the tab.

BrowserCode 0.6.0 is our latest beta release. This preview launches with an unmodified version of Claude Code, running completely client-side. Gemini CLI is available as well.
BrowserCode 1.0.0 is our latest beta release. This preview launches with an unmodified version of Claude Code, running completely client-side.

<h2 id="quickstart">Quickstart</h2>

Expand Down Expand Up @@ -99,12 +99,12 @@ This is BrowserCode beta. Don't be kind to it. Stretch it, bend it, find out wha

<h2 id="roadmap">Roadmap</h2>

| | CLI | Status |
| :--------------------------------------------------------------------------------: | --------------- | ---------------- |
| <img src="./static/readme/gemini.webp" alt="Gemini CLI" width="32" height="32" /> | **Gemini CLI** | ✅ Beta open now |
| <img src="./static/readme/claude.webp" alt="Claude Code" width="32" height="32" /> | **Claude Code** | ✅ Beta open now |
| <img src="./static/readme/codex.webp" alt="Codex" width="32" height="32" /> | **Codex** | 🚧 Coming soon |
| <img src="./static/readme/opencode.webp" alt="OpenCode" width="32" height="32" /> | **OpenCode** | 🚧 Coming soon |
| | CLI | Status |
| :------------------------------------------------------------------------------------: | --------------- | ---------------- |
| <img src="./static/readme/claude.webp" alt="Claude Code" width="32" height="32" /> | **Claude Code** | ✅ Beta open now |
| <img src="./static/readme/codex.webp" alt="Codex" width="32" height="32" /> | **Codex** | 🚧 Coming soon |
| <img src="./static/readme/antigravity.svg" alt="Antigravity" width="32" height="32" /> | **Antigravity** | 🚧 Coming soon |
| <img src="./static/readme/opencode.webp" alt="OpenCode" width="32" height="32" /> | **OpenCode** | 🚧 Coming soon |

Also coming up: cloning GitHub repos directly into your BrowserCode workspace.

Expand Down
8 changes: 6 additions & 2 deletions src/lib/agents/boot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ import { trackEvent } from '$lib/utils/useLazyTracking';
export async function bootCLI(
tool: keyof typeof cliConfigs,
terminalEl: HTMLElement,
onPortalUpdate?: (update: PortalUpdate) => void
onPortalUpdate?: (update: PortalUpdate) => void,
onCliStart?: () => void
) {
const { BrowserPod } = await import('@leaningtech/browserpod');

const config = cliConfigs[tool] ?? cliConfigs.gemini;
const config = cliConfigs[tool] ?? cliConfigs.claude;
const toolLabel = toolItems.find((item) => item.id === tool)?.label ?? tool;

if (isIos()) {
Expand Down Expand Up @@ -67,6 +68,9 @@ export async function bootCLI(

trackEvent('Booted', { tool: toolLabel });

// Before the await: `run` only settles when the CLI exits.
onCliStart?.();

await pod.run(config.command, config.args, {
env: ['COLORTERM=truecolor'],
terminal,
Expand Down
58 changes: 58 additions & 0 deletions src/lib/components/DiskImageWarningModal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<script lang="ts">
import Icon from '@iconify/svelte';
import {
diskImageWarningState,
confirmDiskImageWarning,
cancelDiskImageWarning
} from '$lib/stores/diskImageWarning.svelte';

function handleKeydown(event: KeyboardEvent) {
if (diskImageWarningState.open && event.key === 'Escape') {
event.preventDefault();
cancelDiskImageWarning();
}
}
</script>

<svelte:window onkeydown={handleKeydown} />

{#if diskImageWarningState.open}
<div
class="fixed inset-0 z-[70] flex items-center justify-center bg-black/70 p-4 backdrop-blur-sm"
role="presentation"
onclick={(e) => e.target === e.currentTarget && cancelDiskImageWarning()}
>
<div
class="glass-panel max-w-sm rounded-xl border border-bc-mist/15 px-6 py-7 text-center shadow-2xl"
role="alertdialog"
aria-modal="true"
aria-labelledby="disk-image-warning-title"
>
<div
class="mx-auto mb-4 flex h-10 w-10 items-center justify-center rounded-lg bg-bc-gold/10 text-bc-gold"
>
<Icon icon="mingcute:download-line" width="22" height="22" />
</div>
<h3 id="disk-image-warning-title" class="mb-2 text-sm font-semibold text-zinc-50">
{diskImageWarningState.label} is a {diskImageWarningState.sizeMB} MB download
</h3>
<p class="mb-5 text-[12.5px] leading-relaxed text-zinc-400">
It downloads when you open the session. Nothing runs until it finishes.
</p>
<div class="flex justify-center gap-2">
<button
onclick={cancelDiskImageWarning}
class="rounded-md bg-white/5 px-4 py-2 text-[13px] font-medium text-zinc-300 transition hover:bg-white/10"
>
Cancel
</button>
<button
onclick={confirmDiskImageWarning}
class="rounded-md bg-bc-azure/90 px-4 py-2 text-[13px] font-medium text-white transition hover:bg-bc-azure"
>
Continue
</button>
</div>
</div>
</div>
{/if}
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@
let {
lines = [],
activeLine = -1,
note = '',
flash = false,
onFlashComplete
}: {
/** Ordered boot-log messages, dmesg-style (lowercase). */
lines?: string[];
/** Highest line index real progress justifies revealing (-1 before boot starts). */
activeLine?: number;
/** Aside under the log, for a wait that needs explaining. */
note?: string;
flash?: boolean;
onFlashComplete?: () => void;
} = $props();
Expand Down Expand Up @@ -310,6 +313,9 @@
{#if active && sinceStage >= 2}<span class="wait">{Math.floor(sinceStage)}s</span>{/if}
</div>
{/each}
{#if note}
<div class="note">{note}</div>
{/if}
</div>

<span class="sr-only" role="status" aria-live="polite">{srStatus}</span>
Expand Down Expand Up @@ -387,6 +393,14 @@
font-variant-numeric: tabular-nums;
}

/* Indented past the [OK] gate so it doesn't read as another step. */
.note {
margin-top: 5px;
padding-left: 33px;
color: rgba(255, 255, 255, 0.38);
animation: log-in 0.22s ease both;
}

@keyframes log-in {
from {
opacity: 0;
Expand Down
5 changes: 4 additions & 1 deletion src/lib/components/Sidebar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import { openTour } from '$lib/stores/stepper.svelte';
import { NEW_ISSUE_URL } from '$lib/utils/bug-report';
import { navigateWithLeaveGuard } from '$lib/stores/leaveWarning.svelte';
import { openAgentWithSizeWarning } from '$lib/stores/diskImageWarning.svelte';

let isHome = $derived($page.route.id === '/');
let isAgentsSection = $derived($page.route.id?.startsWith('/agents') ?? false);
Expand Down Expand Up @@ -189,7 +190,9 @@
{@const isRunning = activeTool?.id === item.id}
<button
type="button"
onclick={() => !item.disabled && navigate(`/agents/${item.id}`)}
onclick={() =>
!item.disabled &&
openAgentWithSizeWarning(item.id, () => navigate(`/agents/${item.id}`))}
disabled={item.disabled}
class="flex w-full items-center gap-2.5 rounded-md px-2 py-1.5 text-left text-[12.5px] transition
{item.disabled
Expand Down
6 changes: 3 additions & 3 deletions src/lib/components/Stepper.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,8 @@
Welcome to BrowserCode
</h1>
<p class="text-sm leading-relaxed text-zinc-400">
Run AI coding agents like Claude Code and Gemini CLI, or spin up a full IDE playground
for popular frameworks, with everything sandboxed right in this browser tab.
Run AI coding agents like Claude Code, or spin up a full IDE playground for popular
frameworks, with everything sandboxed right in this browser tab.
</p>
{:else if stepperState.step === 2}
<h1 id="stepper-title" class="mb-3 font-display text-3xl font-bold text-zinc-100">
Expand Down Expand Up @@ -201,7 +201,7 @@
Or run AI agents from the sidebar
</h1>
<p class="text-sm leading-relaxed text-zinc-400">
Claude Code and Gemini CLI are available now. Codex CLI and OpenCode are coming soon.
Claude Code and Codex CLI are available now. Antigravity and OpenCode are coming soon.
</p>

<div class="mt-6 grid grid-cols-2 gap-2">
Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/ide/IdeShell.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import FileTreePanel from '$lib/components/ide/FileTreePanel.svelte';
import SearchPanel from '$lib/components/ide/SearchPanel.svelte';
import TerminalTabs from '$lib/components/ide/TerminalTabs.svelte';
import LoadingScene from '$lib/components/ide/LoadingScene.svelte';
import LoadingScene from '$lib/components/LoadingScene.svelte';
import { fade } from 'svelte/transition';
import type { BootStage, IdeSession } from '$lib/ide/session.svelte';
import { downloadProject } from '$lib/ide/download';
Expand Down
37 changes: 22 additions & 15 deletions src/lib/config/tools.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type ToolId = 'claude' | 'gemini' | 'codex' | 'opencode';
export type ToolId = 'claude' | 'antigravity' | 'codex' | 'opencode';

export type ToolItem = {
id: ToolId;
Expand All @@ -21,22 +21,22 @@ export const toolItems: ToolItem[] = [
accentClass: 'bg-orange-500/10 text-orange-400',
dotClass: 'bg-orange-400'
},
{
id: 'gemini',
icon: 'simple-icons:googlegemini',
label: 'Gemini CLI',
disabled: false,
accentClass: 'bg-blue-500/10 text-blue-400',
dotClass: 'bg-blue-400'
},
{
id: 'codex',
icon: 'hugeicons:chat-gpt',
label: 'Codex CLI',
disabled: true,
disabled: false,
accentClass: 'bg-bc-orchid/10 text-bc-orchid',
dotClass: 'bg-bc-orchid'
},
{
id: 'antigravity',
icon: 'bxl:google-antigravity',
label: 'Antigravity',
disabled: true,
accentClass: 'bg-blue-500/10 text-blue-400',
dotClass: 'bg-blue-400'
},
{
id: 'opencode',
icon: null,
Expand All @@ -54,6 +54,11 @@ export type CLIConfig = {
args: string[];
projectFile?: string;
openCallback?: (urlOrPath: string) => void;
/**
* Download size of `userImage`, in MB. Set only for images worth warning about;
* leaving it unset is what suppresses the warning.
*/
approxSizeMB?: number;
};

export const cliConfigs: Record<string, CLIConfig> = {
Expand All @@ -77,11 +82,13 @@ export const cliConfigs: Record<string, CLIConfig> = {
}
}
},
gemini: {
userImage: 'wss://disks.browserpod.io/gemini_20260430_2.ext2',
storageKey: 'gemini_20260430_2',
// PLACEHOLDER: these are Claude's values, so this boots Claude Code. Swap userImage, command,
// args and approxSizeMB when the Codex image ships; the storage key is already Codex's own.
codex: {
userImage: 'wss://disks.browserpod.io/claude_20260506.ext2',
storageKey: 'codex_placeholder',
command: 'node',
args: ['/home/user/node_modules/@google/gemini-cli/bundle/gemini.js'],
projectFile: '/project/gemini/GEMINI.md'
args: ['/home/user/claude-extracted/src/entrypoints/cli.js'],
approxSizeMB: 100
}
};
56 changes: 56 additions & 0 deletions src/lib/stores/diskImageWarning.svelte.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { cliConfigs, toolItems } from '$lib/config/tools';

export const diskImageWarningState = $state<{ open: boolean; label: string; sizeMB: number }>({
open: false,
label: '',
sizeMB: 0
});

// Module-level: only one warning is ever open at a time.
let proceed: () => void = () => {};
let pendingKey = '';

// Keyed by storage key, not tool id: it carries the image version, so a new image asks again.
const ACK_PREFIX = 'agent-size-ack:';

function acknowledged(storageKey: string): boolean {
try {
return localStorage.getItem(ACK_PREFIX + storageKey) !== null;
} catch {
// Blocked storage: warn every time rather than never.
return false;
}
}

/**
* Runs `open` once the agent's download size is acknowledged, or immediately if it already was.
* Agents with no `approxSizeMB` never warn, so every tool navigation can wrap this.
*/
export function openAgentWithSizeWarning(tool: string, open: () => void) {
const config = cliConfigs[tool];
if (!config?.approxSizeMB || acknowledged(config.storageKey)) {
open();
return;
}

proceed = open;
pendingKey = config.storageKey;
diskImageWarningState.label = toolItems.find((item) => item.id === tool)?.label ?? tool;
diskImageWarningState.sizeMB = config.approxSizeMB;
diskImageWarningState.open = true;
}

export function confirmDiskImageWarning() {
try {
localStorage.setItem(ACK_PREFIX + pendingKey, '1');
} catch {
// Never block the session over a failed hint.
}
diskImageWarningState.open = false;
proceed();
}

export function cancelDiskImageWarning() {
diskImageWarningState.open = false;
proceed = () => {};
}
3 changes: 3 additions & 0 deletions src/routes/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import Stepper from '$lib/components/Stepper.svelte';
import LeaveWarningModal from '$lib/components/LeaveWarningModal.svelte';
import IosUnsupportedModal from '$lib/components/IosUnsupportedModal.svelte';
import DiskImageWarningModal from '$lib/components/DiskImageWarningModal.svelte';
import { page } from '$app/stores';
import { toolItems } from '$lib/config/tools';
import { stepperState } from '$lib/stores/stepper.svelte';
Expand Down Expand Up @@ -89,6 +90,8 @@
Help flyout and the Home page both need to trigger it from anywhere via stepperState. -->
<Stepper />
<LeaveWarningModal />
<!-- Global: agents open from several routes. -->
<DiskImageWarningModal />
{#if !zenState.on}
<Sidebar />
{/if}
Expand Down
4 changes: 2 additions & 2 deletions src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@
<div>
<div class="text-[14px] font-medium text-zinc-100">Run coding agents</div>
<div class="mt-0.5 text-[12.5px] leading-relaxed text-white/45">
Claude Code and Gemini CLI, sandboxed in your browser
Claude Code, sandboxed in your browser
</div>
</div>
</button>
Expand Down Expand Up @@ -202,7 +202,7 @@
editor, file tree, terminals, and live preview for popular frameworks, and a set of
<span class="text-zinc-200">AI coding CLIs</span>
that you can run unmodified, with real file access and networking, without touching your local
machine, including Claude Code and Gemini CLI today, with more on the way.
machine, including Claude Code today, with more on the way.
</p>

<p class="mb-10 text-[14.5px] leading-relaxed text-white/60">
Expand Down
4 changes: 2 additions & 2 deletions src/routes/[tool]/+page.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { error, redirect } from '@sveltejs/kit';
import { toolItems } from '$lib/config/tools';

// Legacy URLs: the agent pages lived at /claude, /gemini, … before the IDE
// moved in at the top level. Keep them working permanently.
// Legacy URLs: the agent pages lived at /claude, … before the IDE moved in at the
// top level. Keep them working permanently for tools still in the registry.
export function load({ params }: { params: { tool: string } }) {
if (toolItems.some((item) => item.id === params.tool)) {
redirect(308, `/agents/${params.tool}`);
Expand Down
3 changes: 2 additions & 1 deletion src/routes/agents/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
import opencodeLogoSrc from '$lib/assets/opencode-logo.svg';
import { toolItems } from '$lib/config/tools';
import WavyGridBackground from '$lib/components/WavyGridBackground.svelte';
import { openAgentWithSizeWarning } from '$lib/stores/diskImageWarning.svelte';

function openTool(id: string, disabled: boolean) {
if (disabled) return;
window.location.href = `/agents/${id}`;
openAgentWithSizeWarning(id, () => (window.location.href = `/agents/${id}`));
}

// Fades the glass panel in over the wavy grid on arrival — full coverage from the start (not
Expand Down
Loading