= {
}
}
},
- 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
}
};
diff --git a/src/lib/stores/diskImageWarning.svelte.ts b/src/lib/stores/diskImageWarning.svelte.ts
new file mode 100644
index 0000000..c6d1387
--- /dev/null
+++ b/src/lib/stores/diskImageWarning.svelte.ts
@@ -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 = () => {};
+}
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index c3a61f2..93e7bc8 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -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';
@@ -89,6 +90,8 @@
Help flyout and the Home page both need to trigger it from anywhere via stepperState. -->
+
+
{#if !zenState.on}
{/if}
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index d97d62a..177dfa7 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -134,7 +134,7 @@
Run coding agents
- Claude Code and Gemini CLI, sandboxed in your browser
+ Claude Code, sandboxed in your browser
@@ -202,7 +202,7 @@
editor, file tree, terminals, and live preview for popular frameworks, and a set of
AI coding CLIs
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.
diff --git a/src/routes/[tool]/+page.ts b/src/routes/[tool]/+page.ts
index 82cd0de..19b07a4 100644
--- a/src/routes/[tool]/+page.ts
+++ b/src/routes/[tool]/+page.ts
@@ -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}`);
diff --git a/src/routes/agents/+page.svelte b/src/routes/agents/+page.svelte
index c0242b9..89315ac 100644
--- a/src/routes/agents/+page.svelte
+++ b/src/routes/agents/+page.svelte
@@ -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
diff --git a/src/routes/agents/[tool]/+page.svelte b/src/routes/agents/[tool]/+page.svelte
index 4237bcd..3d3b00b 100644
--- a/src/routes/agents/[tool]/+page.svelte
+++ b/src/routes/agents/[tool]/+page.svelte
@@ -5,13 +5,16 @@
import opencodeLogoSrc from '$lib/assets/opencode-logo.svg';
import { onMount } from 'svelte';
+ import { fade } from 'svelte/transition';
import { page } from '$app/stores';
import { bootCLI } from '$lib/agents/boot';
+ import LoadingScene from '$lib/components/LoadingScene.svelte';
import { openTour } from '$lib/stores/stepper.svelte';
- import { toolItems } from '$lib/config/tools';
+ import { cliConfigs, toolItems } from '$lib/config/tools';
import { requestSingleTabLock } from '$lib/utils/tabLock';
import { watchIsMobile } from '$lib/utils/viewport';
import { navigateWithLeaveGuard, installLeaveGuard } from '$lib/stores/leaveWarning.svelte';
+ import { openAgentWithSizeWarning } from '$lib/stores/diskImageWarning.svelte';
import { PortalState } from '$lib/stores/portals.svelte';
import ZenToggle from '$lib/components/ZenToggle.svelte';
import { zenState } from '$lib/stores/zen.svelte';
@@ -104,6 +107,17 @@
// Tool switching is a full page load, so the active tool is fixed for this page's lifetime
const activeTool = getActiveTool();
+ const activeLabel = toolItems.find((item) => item.id === activeTool)?.label ?? activeTool;
+
+ // Two lines only: BrowserPod.boot() reports no progress, so there's nothing finer to show.
+ const BOOT_LINES = [`loading ${activeLabel} runtime`, `starting ${activeLabel}`];
+
+ // Shown every boot: whether a repeat visit re-streams the image is unconfirmed.
+ const sizeMB = cliConfigs[activeTool]?.approxSizeMB;
+ const BOOT_NOTE = sizeMB ? `downloading a ${sizeMB} MB runtime` : '';
+
+ let cliStarted = $state(false);
+ let loaderVisible = $state(true);
function toggleToolMenu() {
showToolMenu = !showToolMenu;
@@ -111,8 +125,10 @@
function selectTool(id: string) {
if (validToolIds.has(id)) {
- // Already an active agent session here — always confirm before tearing it down.
- navigateWithLeaveGuard(`/agents/${id}`, true);
+ openAgentWithSizeWarning(id, () =>
+ // Already an active agent session here — always confirm before tearing it down.
+ navigateWithLeaveGuard(`/agents/${id}`, true)
+ );
}
showToolMenu = false;
}
@@ -139,15 +155,12 @@
// The duplicate-tab case above has nothing booted, so there's no work to lose.
disposeLeaveGuard = installLeaveGuard();
- // Shown on every boot, not just the first ever visit.
- showTerminalTip = true;
-
if (!consoleEl) {
console.error('Terminal container is not ready yet');
return;
}
- bootCLI(tool, consoleEl, portal.apply);
+ bootCLI(tool, consoleEl, portal.apply, () => (cliStarted = true));
});
return () => {
@@ -210,6 +223,23 @@