diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..daba01d --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "dev", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "dev"], + "port": 5173 + } + ] +} diff --git a/.prettierignore b/.prettierignore index 7d74fe2..2293a05 100644 --- a/.prettierignore +++ b/.prettierignore @@ -7,3 +7,4 @@ bun.lockb # Miscellaneous /static/ +.claude/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..405b6f4 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,218 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What is BrowserCode? + +BrowserCode is a web-based IDE that runs entirely in the browser using WebAssembly. It has two experiences: a **playground IDE** at `/ide` (editor + file tree + terminals + live preview, with per-framework starter templates) and **AI coding CLIs** (Claude Code, Gemini CLI) at `/agents/[tool]`. Both are built on BrowserPod, which provides a sandboxed Node.js environment with persistent filesystem storage, POSIX CLI tools (bash, git, npm), and instant app previews via WebAssembly-based portal URLs. + +See `docs/codebase-guide.md` for a full structural tour. + +## Quick Start + +```bash +npm install # Install dependencies +npm run dev # Start development server at http://localhost:5173 +npm run check # Run type checking and Svelte validation +npm run lint # Check code style with ESLint and Prettier +npm run format # Auto-format code with Prettier +npm run build # Build for production +npm run preview # Preview production build locally +``` + +## Tech Stack + +- **Framework**: SvelteKit 2.50.2 (Svelte 5.51.0) +- **Build**: Vite 7.3.1 +- **Styling**: Tailwind CSS 4.1.18 +- **Runtime**: BrowserPod 2.8.0 (WebAssembly Node.js environment) +- **Type System**: TypeScript 5.9.3 (strict mode) +- **Icons**: Iconify + mingcute icon set +- **Code Quality**: ESLint 9.39.2, Prettier 3.8.1 + +## High-Level Architecture + +### User-Facing Flow + +``` +User visits browsercode.io + ↓ +/ redirects to /ide (playground); agents live at /agents/claude, /agents/gemini + ↓ ++layout.svelte renders: Sidebar + Main Content Area + ↓ +/ide: ide/+page.svelte → IdeSession.boot() hydrates a framework template into a pod +/agents/[tool]: agents/[tool]/+page.svelte → bootCLI() boots the tool's disk image + ↓ +Terminal renders stdio output; Portal iframes live previews +``` + +### Routing & Tool Selection + +The app is a client-rendered SPA (`adapter-static`, `ssr = false`, `200.html` fallback), so all redirects below run client-side. + +- **Root**: `/` redirects to `/ide` (`src/routes/+page.ts`) +- **Playground IDE**: `/ide`, framework selected via `?framework=` (registry in `lib/config/frameworks.ts`; templates in `static/templates/`) +- **Agent Pages**: `/agents/[tool]` (`routes/agents/[tool]/+page.svelte`) +- **Legacy URLs**: `/claude`, `/gemini`, … redirect to `/agents/` (`routes/[tool]/+page.ts`) +- **Supported Tools** (lib/config/tools.ts): + - Claude Code: enabled, uses claude_20260506 disk image + - Gemini CLI: enabled, uses gemini_20260430_2 disk image + - Codex, OpenCode: disabled (coming soon) + +Tool/IDE switching uses full page loads (`window.location.href`) on purpose — that is the teardown mechanism for the running pod. + +### Component Hierarchy + +**+layout.svelte** (root layout) + +- Renders Sidebar (left) + main content (right) +- Manages tool validation and active tool state +- Sets up OG meta tags for social sharing + +**agents/[tool]/+page.svelte** (agent application) + +- Terminal component (simple div#console container) +- Portal component (preview iframe + controls) +- Desktop: split layout with drag-to-resize (portalFraction state) +- Mobile: tab navigation between Terminal and Preview views +- Handles portal updates from BrowserPod callbacks + +**ide/+page.svelte** (playground application) + +- Dispatches to IdeLanding (bare `/ide`) or IdeShell (framework/GitHub boot); IdeShell composes the icon rail, resizable FileTreePanel side panel, a header project switcher (template + GitHub clone), EditorPane (CodeMirror), TerminalTabs (Output + Bash), Portal preview +- Pod lifecycle and file/portal state live in `lib/ide/session.svelte.ts` (IdeSession); pod file I/O helpers in `lib/ide/pod-fs.ts` +- `lib/components/ide/EditorPane.svelte` is the only file importing `@codemirror/*` — it is designed to be replaced by VS Code Web later; session/pod-fs/frameworks survive that migration + +**Portal.svelte** + +- iFrame displaying preview URLs +- Port selector (if multiple services running) +- Menu for copy URL, QR code, open in new tab +- QR code generation using qrcode library + +**Sidebar.svelte** + +- Fixed-width nav (desktop only, hidden mobile) +- Tool buttons, GitHub/Discord links, Help button +- Uses Iconify icons with hover tooltips + +**Stepper.svelte** + +- Onboarding tutorial modal +- State managed via stepperState store + +### Data Flow: BrowserPod & Portals + +``` +lib/utils/main.ts: bootCLI() + 1. Detects iOS (unsupported platform) + 2. Initializes BrowserPod with VITE_API_KEY env var + 3. Creates terminal linked to #console div + 4. Sets up portal listener (pod.onPortal callback) + 5. Creates /home/user/project directory + 6. Copies optional project file (CLAUDE.md or GEMINI.md) + 7. Runs CLI: node /path/to/cli.js in /home/user/project + +BrowserPod callbacks: + - Portal events: { port, url } → portalUpdate state → Portal component + - Open events: Optional custom handler (e.g., OAuth URL rewriting for Claude) +``` + +**Portal Updates**: When a service starts on a port, BrowserPod emits `{ port, url, active: true }`. This is collected in the `portals` array (state in [tool]/+page.svelte), and the selected portal renders in the iFrame. + +### Storage & Persistence + +- Each tool has a `storageKey` (e.g., `claude_20260506`) that maps to a BrowserPod disk image URL +- BrowserPod caches the WebAssembly filesystem locally (IndexedDB) +- Changes persist across sessions within the same storage key +- Disk images are versioned; updating the version syncs a new baseline + +### Configuration + +**lib/config/tools.ts** defines: + +- Tool metadata (id, label, icon, disabled flag) +- CLI configurations: disk image URL, storage key, command, args, optional project file, optional open callback + +To add a new tool: + +1. Add to `toolItems` array +2. Add config to `cliConfigs` object +3. Ensure disk image is available at the WSS URL + +## Development Workflow + +### Adding a Component + +1. Create a `.svelte` file in `src/lib/components/` +2. Use ` -{#snippet navButton(item: { id: string; icon: string | null; label: string; disabled: boolean })} - {@const isActive = activePanel === item.id} -
- -
+ {label} + + {/snippet} diff --git a/src/lib/components/Stepper.svelte b/src/lib/components/Stepper.svelte index f404d7f..38c90a5 100644 --- a/src/lib/components/Stepper.svelte +++ b/src/lib/components/Stepper.svelte @@ -3,13 +3,14 @@ import Icon from '@iconify/svelte'; import favicon from '$lib/assets/favicon.svg'; import opencodeLogoSrc from '$lib/assets/opencode-logo.svg'; + import { page } from '$app/stores'; import { stepperState } from '$lib/stores/stepper.svelte'; + import { toolItems } from '$lib/config/tools'; + import { frameworkRailItems } from '$lib/config/frameworks'; let currentStep = 1; const totalSteps = 7; - let highlightedAgent: 'codex' | 'opencode' = 'codex'; - let agentCycleTimer: ReturnType | null = null; let copied = false; let copyTimer: ReturnType | null = null; @@ -17,92 +18,53 @@ const dispatch = createEventDispatcher(); - const agents = { - codex: { - label: 'Codex CLI', - icon: 'hugeicons:chat-gpt', - helper: 'Codex CLI — coming soon', - useIcon: true - }, - opencode: { - label: 'OpenCode', - icon: null, - helper: 'OpenCode — coming soon', - useIcon: false - } - } as const; - - // Sidebar order: [Claude, Gemini, Codex, OpenCode]. Each nav button is 40px tall with 2px gap. - // Nav starts ~93px from viewport top (favicon header + divider + pt-2). Button N center ≈ 93 + (N-1)*42. - // Codex is button 3 → ~177px; OpenCode is button 4 → ~219px. - const agentButtonOffsets = { - codex: 177, - opencode: 219 - }; - - // GitHub icon is in the bottom section of the sidebar. - // We use a bottom offset from the viewport bottom. - // Bottom section: py-2 (8px) + 3 buttons (40px each) + gap-0.5 (2px) between each. - // From bottom: 8px padding + 8px (half of last question button) → GitHub is 3rd from bottom. - // GitHub button center from bottom ≈ 8 + 40 + 2 + 40 + 2 + 20 = 112px - const githubButtonBottomOffset = 112; + // Measured from the real sidebar buttons (via data-tour-target) rather than hand-computed + // pixel math, so the pointers stay accurate if the sidebar's layout ever changes again. + // These are just sane fallbacks in case a target isn't found for some reason. + let agentsTop = 113; + let ideTop = 155; + let helpBottom = 28; + + function centerOf(selector: string): DOMRect | null { + return document.querySelector(selector)?.getBoundingClientRect() ?? null; + } + + function measureTourTargets() { + const agentsRect = centerOf('[data-tour-target="agents"]'); + if (agentsRect) agentsTop = agentsRect.top + agentsRect.height / 2; + + const ideRect = centerOf('[data-tour-target="ide"]'); + if (ideRect) ideTop = ideRect.top + ideRect.height / 2; + + const helpRect = centerOf('[data-tour-target="help"]'); + if (helpRect) helpBottom = window.innerHeight - (helpRect.top + helpRect.height / 2); + } onMount(() => { + measureTourTargets(); + + // The tour only auto-opens the first time someone lands on Home — deep-linking straight + // into /ide or /agents/[tool] on a first visit shouldn't interrupt with the modal. const isFirstTime = !localStorage.getItem('hasVisited'); - if (isFirstTime) { + if (isFirstTime && $page.route.id === '/') { stepperState.open = true; localStorage.setItem('hasVisited', 'true'); } }); onDestroy(() => { - if (agentCycleTimer) clearInterval(agentCycleTimer); if (copyTimer) clearTimeout(copyTimer); }); - const agentOrder: Array<'codex' | 'opencode'> = ['codex', 'opencode']; - - function startAgentCycle() { - if (agentCycleTimer) return; - agentCycleTimer = setInterval(() => { - const idx = agentOrder.indexOf(highlightedAgent); - highlightedAgent = agentOrder[(idx + 1) % agentOrder.length]; - }, 1800); - } - - function stopAgentCycle() { - if (agentCycleTimer) { - clearInterval(agentCycleTimer); - agentCycleTimer = null; - } - } - function nextStep() { - if (currentStep < totalSteps) { - currentStep += 1; - if (currentStep === 4) { - highlightedAgent = 'codex'; - startAgentCycle(); - } else { - stopAgentCycle(); - } - } + if (currentStep < totalSteps) currentStep += 1; } function prevStep() { - if (currentStep > 1) { - currentStep -= 1; - if (currentStep === 4) { - highlightedAgent = 'codex'; - startAgentCycle(); - } else { - stopAgentCycle(); - } - } + if (currentStep > 1) currentStep -= 1; } function finish() { - stopAgentCycle(); stepperState.open = false; dispatch('close'); } @@ -131,18 +93,18 @@ console.error('Failed to copy prompt:', err); } } + + // Steps 3-5 point at sidebar buttons, so the backdrop leaves the sidebar uncovered for those. + const sidebarSteps = new Set([3, 4, 5]); {#if stepperState.open} - +