From 991de0437f86c28fa536f3cb64be360bd74bc1d0 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Nov 2025 06:32:34 +0000 Subject: [PATCH 1/2] docs: add comprehensive agent architecture documentation Add detailed technical documentation explaining how the autonomous agent system works in bolt.new, including: - Core concepts (artifacts, actions, action states, WebContainer) - Complete architecture diagrams showing data flow - Component breakdown (StreamingMessageParser, ActionRunner, etc.) - Execution flow from user input to WebContainer execution - State management with Nanostores - UI components (chat, workbench panel, code editor) - Real code examples and implementation patterns - File reference guide with line counts and purposes - Advanced topics (streaming chunks, abort patterns, error recovery) This document serves as a comprehensive guide for developers looking to understand the parser-driven autonomous execution system that creates the "agent" experience in bolt.new. --- AGENT_ARCHITECTURE.md | 1797 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1797 insertions(+) create mode 100644 AGENT_ARCHITECTURE.md diff --git a/AGENT_ARCHITECTURE.md b/AGENT_ARCHITECTURE.md new file mode 100644 index 0000000000..96bb78b1f2 --- /dev/null +++ b/AGENT_ARCHITECTURE.md @@ -0,0 +1,1797 @@ +# Bolt.new Agent Architecture - Complete Technical Guide + +## Table of Contents +1. [Overview](#overview) +2. [Core Concepts](#core-concepts) +3. [Architecture Diagram](#architecture-diagram) +4. [System Components](#system-components) +5. [Execution Flow](#execution-flow) +6. [State Management](#state-management) +7. [UI Components](#ui-components) +8. [Code Examples](#code-examples) +9. [Key Files Reference](#key-files-reference) +10. [Advanced Topics](#advanced-topics) + +--- + +## Overview + +### What is the "Agent" in Bolt.new? + +Bolt.new implements a **parser-driven autonomous execution system** where Claude AI acts as a code-generating agent. The "autonomous feel" comes from the fact that once Claude responds with instructions, the system automatically: + +1. Parses XML-wrapped actions from Claude's streaming response +2. Executes file operations and shell commands sequentially +3. Updates the UI in real-time to show progress +4. Syncs changes to a browser-based Node.js environment (WebContainer) + +**Key Insight**: The agent is not a separate entity - it's Claude AI responding with specially formatted XML that triggers automated execution through a sophisticated parser and runner system. + +--- + +## Core Concepts + +### 1. Artifacts + +An **artifact** represents a complete project or solution generated by Claude. + +```typescript +interface BoltArtifactData { + id: string; // Unique identifier (e.g., "react-todo-app") + title: string; // Human-readable title (e.g., "React Todo Application") +} +``` + +**Purpose**: Container for a collection of actions that together build an application. + +### 2. Actions + +An **action** is a single atomic operation that modifies the WebContainer environment. + +```typescript +type ActionType = 'file' | 'shell'; + +interface FileAction { + type: 'file'; + filePath: string; // e.g., "src/App.tsx" + content: string; // Full file contents +} + +interface ShellAction { + type: 'shell'; + content: string; // e.g., "npm install react" +} + +type BoltAction = FileAction | ShellAction; +``` + +### 3. Action States + +Actions progress through a lifecycle: + +```typescript +type ActionStatus = + | 'pending' // Queued but not started + | 'running' // Currently executing + | 'complete' // Successfully finished + | 'failed' // Execution error + | 'aborted'; // User cancelled +``` + +### 4. WebContainer + +A **browser-based Node.js runtime** that provides: +- Virtual file system +- Shell command execution +- Package manager (npm/pnpm) +- Dev server capabilities +- Network access + +**Constraints**: +- No native binaries +- No Python pip (only Pyodide) +- Limited system-level operations +- All in-browser execution + +--- + +## Architecture Diagram + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ USER INTERACTION │ +│ (Chat Input + Send Message) │ +└────────────────────────────────┬────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ CHAT.CLIENT.TSX │ +│ (useChat hook from ai/react) │ +└────────────────────────────────┬────────────────────────────────────┘ + │ + ▼ HTTP POST +┌─────────────────────────────────────────────────────────────────────┐ +│ API ROUTE: /api/chat │ +│ (app/routes/api.chat.ts) │ +│ │ +│ • Validates request │ +│ • Extracts conversation history │ +│ • Calls stream-text.ts │ +└────────────────────────────────┬────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ STREAM-TEXT.TS │ +│ (app/lib/.server/llm/stream-text.ts) │ +│ │ +│ • Loads system prompt from prompts.ts │ +│ • Builds conversation context (file contents + history) │ +│ • Calls Anthropic Claude API │ +│ • Streams response back │ +└────────────────────────────────┬────────────────────────────────────┘ + │ + ▼ Streaming Response +┌─────────────────────────────────────────────────────────────────────┐ +│ CLAUDE API RESPONSE │ +│ │ +│ Example Response: │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ I'll create a React app for you. │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ { "name": "app", ... } │ │ +│ │ │ │ +│ │ │ │ +│ │ npm install │ │ +│ │ │ │ +│ │ │ │ +│ └─────────────────────────────────────────────────────┘ │ +└────────────────────────────────┬────────────────────────────────────┘ + │ + ▼ Chunked Stream +┌─────────────────────────────────────────────────────────────────────┐ +│ STREAMING MESSAGE PARSER │ +│ (app/lib/runtime/message-parser.ts) │ +│ │ +│ • Parses incremental XML chunks │ +│ • Extracts tags │ +│ • Extracts tags │ +│ • Fires callbacks on open/close tags │ +│ │ +│ Callbacks: │ +│ onArtifactOpen(data) ────────────┐ │ +│ onArtifactClose() ────────────┤ │ +│ onActionOpen(data) ────────────┤ │ +│ onActionClose(data) ────────────┤ │ +└──────────────────────────────────────┼──────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ WORKBENCH STORE │ +│ (app/lib/stores/workbench.ts) │ +│ │ +│ State Management (Nanostores): │ +│ ┌────────────────────────────────────────────────────┐ │ +│ │ artifacts: Map │ │ +│ │ ├─ id: string │ │ +│ │ ├─ title: string │ │ +│ │ ├─ closed: boolean │ │ +│ │ └─ runner: ActionRunner ◄─────┐ │ │ +│ │ │ │ │ +│ │ showWorkbench: boolean │ │ │ +│ │ currentView: 'code' | 'preview' │ │ │ +│ │ unsavedFiles: Set │ │ │ +│ └────────────────────────────────────┼────────────────┘ │ +│ │ │ +│ Methods: │ │ +│ addArtifact() ─────────────────────┤ │ +│ addAction() ───────────────────────┤ │ +│ runAction() ───────────────────────┤ │ +└───────────────────────────────────────┼──────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ ACTION RUNNER │ +│ (app/lib/runtime/action-runner.ts) │ +│ │ +│ Manages sequential execution: │ +│ ┌────────────────────────────────────────────────────┐ │ +│ │ Actions Queue: [Action1, Action2, Action3, ...] │ │ +│ │ ▼ │ │ +│ │ #currentExecutionPromise │ │ +│ │ │ │ │ +│ │ ┌───────────┴───────────┐ │ │ +│ │ ▼ ▼ │ │ +│ │ #runFileAction() #runShellAction() │ │ +│ │ │ │ │ │ +│ │ └───────────┬───────────┘ │ │ +│ │ ▼ │ │ +│ │ Update Action State │ │ +│ │ (pending → running → complete) │ │ +│ └────────────────────────────────────────────────────┘ │ +│ │ +│ Key Methods: │ +│ • addAction(data): Registers action with AbortController │ +│ • runAction(data): Queues and executes action │ +│ • #executeAction(): Routes to file or shell executor │ +└────────────────────────────────┬────────────────────────────────────┘ + │ + ┌────────────┴────────────┐ + ▼ ▼ +┌──────────────────────────┐ ┌──────────────────────────┐ +│ FILE ACTION EXECUTION │ │ SHELL ACTION EXECUTION │ +│ │ │ │ +│ webcontainer.fs │ │ webcontainer.spawn() │ +│ .writeFile( │ │ ('jsh', ['-c', cmd]) │ +│ filePath, │ │ │ +│ content │ │ • Captures stdout │ +│ ) │ │ • Captures stderr │ +│ │ │ • Monitors exit code │ +└────────────┬─────────────┘ └────────────┬─────────────┘ + │ │ + └───────────┬───────────────────┘ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ WEBCONTAINER │ +│ (Browser-based Node.js) │ +│ │ +│ • Virtual File System (in-memory) │ +│ • Shell (jsh - JavaScript shell) │ +│ • npm/pnpm package managers │ +│ • Dev servers (Vite, webpack, etc.) │ +│ • Network access │ +└────────────────────────────────┬────────────────────────────────────┘ + │ + ▼ File System Changes +┌─────────────────────────────────────────────────────────────────────┐ +│ UI UPDATES │ +│ │ +│ Left Side (Chat): Right Side (Workbench): │ +│ ┌────────────────────┐ ┌──────────────────────┐ │ +│ │ Artifact Component │ │ Code Editor │ │ +│ │ • Title │ │ • File Tree │ │ +│ │ • Action List │ │ • Monaco Editor │ │ +│ │ ⟳ Running │ │ • Live Sync │ │ +│ │ ✓ Complete │ │ │ │ +│ │ ○ Pending │ │ Preview Panel │ │ +│ │ ✗ Failed │ │ • iframe │ │ +│ │ │ │ • Live Reload │ │ +│ └────────────────────┘ └──────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## System Components + +### 1. StreamingMessageParser + +**Location**: `app/lib/runtime/message-parser.ts` (286 lines) + +**Purpose**: Incrementally parse streaming XML responses from Claude to extract artifacts and actions. + +**Key Features**: +- Handles chunked/partial XML across multiple stream packets +- State machine for tag parsing +- Callback-based architecture for real-time processing +- Supports nested tags and attributes + +**Class Structure**: + +```typescript +class StreamingMessageParser { + private _text: string = ''; + private _currentAction: Partial | null = null; + private _currentArtifact: Partial | null = null; + + constructor(options: { + callbacks: { + onArtifactOpen?: (data: BoltArtifactData) => void; + onArtifactClose?: () => void; + onActionOpen?: (data: Partial) => void; + onActionClose?: (data: BoltAction) => void; + }; + }); + + parse(chunk: string): void; + // Incrementally processes text, firing callbacks when complete tags detected +} +``` + +**Example Usage**: + +```typescript +const parser = new StreamingMessageParser({ + callbacks: { + onArtifactOpen: (data) => { + console.log('Artifact started:', data.id, data.title); + }, + onActionClose: (action) => { + if (action.type === 'file') { + console.log('File action:', action.filePath); + } else { + console.log('Shell action:', action.content); + } + } + } +}); + +// As chunks arrive from API: +streamReader.on('data', (chunk) => { + parser.parse(chunk); +}); +``` + +**Parsing Algorithm**: + +1. Accumulate text in `_text` buffer +2. Search for ``, `` tags +3. Extract attributes using regex +4. Track opening/closing tags with stack +5. Fire callbacks when tags close (content is complete) +6. Handle escape sequences and special characters + +--- + +### 2. ActionRunner + +**Location**: `app/lib/runtime/action-runner.ts` (186 lines) + +**Purpose**: Execute file and shell actions sequentially in WebContainer with state tracking. + +**Key Features**: +- Sequential execution queue (prevents race conditions) +- AbortController support for cancellation +- State tracking (pending → running → complete/failed) +- Error handling and logging + +**Class Structure**: + +```typescript +class ActionRunner { + #webcontainer: Promise; + #currentExecutionPromise: Promise = Promise.resolve(); + runtimeLogger: () => ILogger; + + actions: MapStore>; + + constructor( + webcontainerPromise: Promise, + runtimeLogger: () => ILogger + ); + + addAction(data: ActionCallbackData): void; + // Registers action in 'pending' state with AbortController + + runAction(data: ActionCallbackData): void; + // Queues action for execution + + #executeAction(actionId: string): Promise; + // Routes to #runFileAction or #runShellAction + + #runFileAction(action: ActionState): Promise; + // Writes file to WebContainer filesystem + + #runShellAction(action: ActionState): Promise; + // Spawns shell process in WebContainer +} +``` + +**Action State Interface**: + +```typescript +interface ActionState { + action: BoltAction; + status: ActionStatus; + abort: () => void; // Triggers AbortController + executed: boolean; + abortSignal: AbortSignal; +} +``` + +**Execution Flow**: + +```typescript +// 1. Add action (called from parser callback) +actionRunner.addAction({ + messageId: 'msg-123', + actionId: 'action-456', + action: { + type: 'file', + filePath: 'src/App.tsx', + content: 'export default function App() { ... }' + } +}); + +// 2. Run action (called immediately after add) +actionRunner.runAction({ + messageId: 'msg-123', + actionId: 'action-456', + action: { /* same action */ } +}); + +// Internal execution: +// - Queued via #currentExecutionPromise.then(...) +// - Status updated to 'running' +// - File written or shell command executed +// - Status updated to 'complete' or 'failed' +``` + +**File Action Execution**: + +```typescript +async #runFileAction(action: ActionState): Promise { + const { filePath, content } = action.action as FileAction; + + try { + const webcontainer = await this.#webcontainer; + + // Ensure directory exists + const dirname = path.dirname(filePath); + await webcontainer.fs.mkdir(dirname, { recursive: true }); + + // Write file + await webcontainer.fs.writeFile(filePath, content); + + // Update state + this.#updateAction(action.id, { status: 'complete' }); + } catch (error) { + this.#updateAction(action.id, { status: 'failed' }); + throw error; + } +} +``` + +**Shell Action Execution**: + +```typescript +async #runShellAction(action: ActionState): Promise { + const { content: command } = action.action as ShellAction; + + try { + const webcontainer = await this.#webcontainer; + + // Spawn shell process + const process = await webcontainer.spawn('jsh', ['-c', command], { + signal: action.abortSignal + }); + + // Capture output + process.output.pipeTo(new WritableStream({ + write(data) { + console.log('Shell output:', data); + } + })); + + // Wait for exit + const exitCode = await process.exit; + + if (exitCode === 0) { + this.#updateAction(action.id, { status: 'complete' }); + } else { + this.#updateAction(action.id, { status: 'failed' }); + } + } catch (error) { + this.#updateAction(action.id, { status: 'failed' }); + throw error; + } +} +``` + +--- + +### 3. Workbench Store + +**Location**: `app/lib/stores/workbench.ts` (277 lines) + +**Purpose**: Central state management for artifacts, actions, files, and UI state. + +**Technology**: Nanostores (lightweight reactive state library) + +**Store Types**: + +```typescript +// Map store: artifacts by message ID +const artifacts = map>({}); + +interface ArtifactState { + id: string; // Artifact ID from Claude + title: string; // Human-readable title + closed: boolean; // Whether received + runner: ActionRunner; // Dedicated action executor +} + +// Atom stores: single values +const showWorkbench = atom(false); +const currentView = atom<'code' | 'preview'>('code'); +const unsavedFiles = atom>(new Set()); + +// File state +const files = atom(new Map()); +const selectedFile = atom(undefined); +``` + +**Key Methods**: + +```typescript +// Add new artifact (creates ActionRunner) +function addArtifact(data: { messageId: string; artifactId: string; title: string }) { + const runner = new ActionRunner(webcontainerPromise, runtimeLogger); + + artifacts.setKey(data.messageId, { + id: data.artifactId, + title: data.title, + closed: false, + runner + }); + + showWorkbench.set(true); // Open right panel +} + +// Add action to artifact's runner +function addAction(data: ActionCallbackData) { + const artifact = artifacts.get()[data.messageId]; + if (artifact) { + artifact.runner.addAction(data); + } +} + +// Execute action +function runAction(data: ActionCallbackData) { + const artifact = artifacts.get()[data.messageId]; + if (artifact) { + artifact.runner.runAction(data); + } +} + +// Track file changes +function setCurrentDocumentContent(filePath: string, content: string) { + const originalContent = files.get().get(filePath)?.content; + + if (originalContent !== undefined && originalContent !== content) { + unsavedFiles.set(new Set([...unsavedFiles.get(), filePath])); + } +} + +// Save file +async function saveFile(filePath: string) { + const webcontainer = await webcontainerPromise; + const content = currentDocument.get()?.value; + + if (content) { + await webcontainer.fs.writeFile(filePath, content); + + const newUnsaved = new Set(unsavedFiles.get()); + newUnsaved.delete(filePath); + unsavedFiles.set(newUnsaved); + } +} +``` + +**Reactive Updates**: + +```typescript +// Components subscribe to store updates +import { useStore } from '@nanostores/react'; + +function MyComponent() { + const currentArtifacts = useStore(artifacts); + const isWorkbenchVisible = useStore(showWorkbench); + + return ( +
+ {Object.values(currentArtifacts).map(artifact => ( +
{artifact.title}
+ ))} +
+ ); +} +``` + +--- + +### 4. System Prompt + +**Location**: `app/lib/.server/llm/prompts.ts` (279 lines) + +**Purpose**: Instruct Claude on how to format responses with `` and `` tags. + +**Key Instructions to Claude**: + +```markdown +# System Prompt Excerpts + +## Response Format + +You are Bolt, an AI assistant and expert web developer. When writing code, always use this format: + + + + // File contents here + + + + npm install package-name + + + +## Rules + +1. ALWAYS wrap complete solutions in tags +2. Each file must be in a separate tag +3. Each shell command in a separate tag +4. Create files BEFORE running commands that depend on them +5. Use two-space indentation +6. For package.json, always include necessary scripts +7. Think holistically - include all dependencies (HTML, CSS, JS) + +## WebContainer Constraints + +The code runs in a browser-based Node.js environment with limitations: + +- ❌ No native binaries (Python C extensions, node-gyp) +- ❌ No pip (use Pyodide for Python) +- ❌ No system commands (apt-get, brew, etc.) +- ✅ npm/pnpm work perfectly +- ✅ All pure JavaScript packages work +- ✅ Frontend frameworks (React, Vue, Svelte, etc.) +- ✅ Dev servers (Vite, webpack-dev-server, etc.) + +## Example Response + +User: "Create a React todo app" + +Claude Response: +I'll create a React todo application with Vite. + + + + { + "name": "todo-app", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.0.0", + "vite": "^4.3.0" + } + } + + + + npm install + + + + + + + + Todo App + + +
+ + + +
+ + + import React from 'react' + import ReactDOM from 'react-dom/client' + import App from './App' + + ReactDOM.createRoot(document.getElementById('root')).render( + + + , + ) + + + + import { useState } from 'react' + + export default function App() { + const [todos, setTodos] = useState([]) + const [input, setInput] = useState('') + + const addTodo = () => { + if (input.trim()) { + setTodos([...todos, { id: Date.now(), text: input, done: false }]) + setInput('') + } + } + + return ( +
+

Todo List

+ setInput(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && addTodo()} + /> + +
    + {todos.map(todo => ( +
  • {todo.text}
  • + ))} +
+
+ ) + } +
+ + + npm run dev + +
+``` + +**What the Prompt Achieves**: +1. Claude knows to wrap everything in `` tags +2. Each file is in its own `` tag +3. Shell commands are in `` tags +4. Files are created BEFORE `npm install` and `npm run dev` +5. The system automatically parses and executes these actions + +--- + +## Execution Flow + +### Detailed Step-by-Step Flow + +#### Phase 1: User Input → API Request + +```typescript +// 1. User types message in chat +// 2. Chat.client.tsx handles submission + +import { useChat } from 'ai/react'; + +function Chat() { + const { messages, input, handleSubmit, handleInputChange } = useChat({ + api: '/api/chat', + onResponse: (response) => { + // Response streaming starts + } + }); + + return ( +
+ + +
+ ); +} +``` + +#### Phase 2: API Route → Claude API + +```typescript +// app/routes/api.chat.ts + +export async function POST({ request }: ActionFunctionArgs) { + const { messages } = await request.json(); + + // Get unsaved files to send as context + const fileModifications = workbenchStore.getFileModifications(); + + // Call stream-text with conversation history + return streamText(messages, env, fileModifications); +} +``` + +#### Phase 3: Stream Processing + +```typescript +// app/lib/.server/llm/stream-text.ts + +export async function streamText( + messages: Message[], + env: Env, + fileModifications?: FileMap +) { + // 1. Build system prompt + const systemPrompt = getSystemPrompt(); + + // 2. Add file modifications to context + const enhancedMessages = [...messages]; + if (fileModifications && fileModifications.size > 0) { + enhancedMessages.push({ + role: 'user', + content: `Modified files:\n${Array.from(fileModifications.entries()) + .map(([path, content]) => `File: ${path}\n${content}`) + .join('\n\n')}` + }); + } + + // 3. Call Claude API + const anthropic = createAnthropic({ apiKey: env.ANTHROPIC_API_KEY }); + + const result = streamText({ + model: anthropic('claude-3-5-sonnet-20240620'), + system: systemPrompt, + messages: enhancedMessages, + maxTokens: 8192 + }); + + // 4. Return streaming response + return result.toDataStreamResponse(); +} +``` + +#### Phase 4: Parser Integration + +```typescript +// app/lib/hooks/useMessageParser.ts + +export function useMessageParser() { + const parser = useRef(); + + useEffect(() => { + parser.current = new StreamingMessageParser({ + callbacks: { + onArtifactOpen: (data) => { + // Add artifact to store + workbenchStore.addArtifact({ + messageId: currentMessageId, + artifactId: data.id, + title: data.title + }); + }, + + onArtifactClose: () => { + // Mark artifact as closed + workbenchStore.closeArtifact(currentMessageId); + }, + + onActionOpen: (data) => { + // For file actions, add immediately + if (data.type === 'file') { + workbenchStore.addAction({ + messageId: currentMessageId, + actionId: generateId(), + action: data as BoltAction + }); + } + }, + + onActionClose: (data) => { + // For shell actions, add when complete + // For file actions, run the action + const actionId = generateId(); + + if (data.type === 'shell') { + workbenchStore.addAction({ + messageId: currentMessageId, + actionId, + action: data + }); + } + + workbenchStore.runAction({ + messageId: currentMessageId, + actionId, + action: data + }); + } + } + }); + }, []); + + const parseChunk = useCallback((chunk: string) => { + parser.current?.parse(chunk); + }, []); + + return { parseChunk }; +} +``` + +#### Phase 5: Action Execution + +```typescript +// Sequential execution in ActionRunner + +runAction(data: ActionCallbackData) { + const { messageId, actionId, action } = data; + + // Queue execution (waits for previous to complete) + this.#currentExecutionPromise = this.#currentExecutionPromise + .then(() => this.#executeAction(actionId)) + .catch((error) => { + this.runtimeLogger().error('Action failed:', error); + }); +} + +async #executeAction(actionId: string): Promise { + const action = this.actions.get()[actionId]; + + if (!action || action.executed) { + return; + } + + // Mark as running + this.#updateAction(actionId, { status: 'running' }); + + try { + // Execute based on type + if (action.action.type === 'file') { + await this.#runFileAction(action); + } else { + await this.#runShellAction(action); + } + + // Mark as complete + this.#updateAction(actionId, { + status: 'complete', + executed: true + }); + } catch (error) { + this.#updateAction(actionId, { + status: 'failed', + executed: true + }); + } +} +``` + +--- + +## State Management + +### Nanostores Architecture + +Bolt.new uses **Nanostores**, a tiny (~300 bytes) state management library with atomic stores. + +#### Store Types + +```typescript +// Atom: Single value +import { atom } from 'nanostores'; + +const counter = atom(0); +counter.set(1); +console.log(counter.get()); // 1 + +// Map: Object with keys +import { map } from 'nanostores'; + +const user = map({ name: 'Alice', age: 30 }); +user.setKey('age', 31); +console.log(user.get().age); // 31 + +// Computed: Derived value +import { computed } from 'nanostores'; + +const doubled = computed(counter, value => value * 2); +console.log(doubled.get()); // 2 +``` + +#### Workbench Store Structure + +```typescript +// app/lib/stores/workbench.ts + +// Artifacts (map of message ID to artifact state) +export const artifacts = map>({}); + +// UI visibility +export const showWorkbench = atom(false); +export const currentView = atom<'code' | 'preview'>('code'); + +// File management +export const files = atom(new Map()); +export const selectedFile = atom(undefined); +export const unsavedFiles = atom>(new Set()); + +// Terminal +export const showTerminal = atom(false); +``` + +#### React Integration + +```tsx +import { useStore } from '@nanostores/react'; +import { artifacts, showWorkbench } from '~/lib/stores/workbench'; + +function ArtifactList() { + // Automatically re-renders when store changes + const currentArtifacts = useStore(artifacts); + const isVisible = useStore(showWorkbench); + + if (!isVisible) { + return null; + } + + return ( +
+ {Object.values(currentArtifacts).map(artifact => ( + + ))} +
+ ); +} +``` + +--- + +## UI Components + +### 1. Artifact Component + +**Location**: `app/components/chat/Artifact.tsx` + +**Purpose**: Display artifact metadata and action list within chat messages. + +```tsx +interface ArtifactProps { + messageId: string; +} + +export function Artifact({ messageId }: ArtifactProps) { + const artifact = useStore(artifacts)[messageId]; + const actions = useStore(artifact?.runner.actions); + + const [isExpanded, setIsExpanded] = useState(true); + + return ( +
+
+

{artifact.title}

+ +
+ + {isExpanded && ( +
+ {Object.values(actions).map(action => ( + + ))} +
+ )} + + +
+ ); +} +``` + +**Action Status Indicators**: + +```tsx +function ActionItem({ action }: { action: ActionState }) { + const statusIcon = { + pending: '○', + running: '⟳', + complete: '✓', + failed: '✗', + aborted: '✗' + }[action.status]; + + return ( +
+ {statusIcon} + + {action.action.type === 'file' ? ( + Create {action.action.filePath} + ) : ( + <> + Run command + {action.action.content} + + )} + + {action.status === 'running' && ( + + )} +
+ ); +} +``` + +### 2. Workbench Panel + +**Location**: `app/components/workbench/Workbench.client.tsx` + +**Purpose**: Right-side panel with code editor and preview. + +```tsx +export function Workbench() { + const isVisible = useStore(showWorkbench); + const view = useStore(currentView); + + if (!isVisible) { + return null; + } + + return ( +
+ {/* Header with view toggle */} + + + {/* View switcher */} +
+ + +
+ + {/* Main content area */} +
+ {view === 'code' ? ( + <> + + + + ) : ( + + )} +
+ + {/* Terminal (toggleable) */} + +
+ ); +} +``` + +**File Tree Component**: + +```tsx +function FileTree() { + const fileList = useStore(files); + const selected = useStore(selectedFile); + + return ( +
+ {Array.from(fileList.entries()).map(([path, file]) => ( + + ))} +
+ ); +} +``` + +**Code Editor Component** (Monaco Editor): + +```tsx +import Editor from '@monaco-editor/react'; + +function CodeEditor() { + const file = useStore(selectedFile); + const fileContent = useStore(files).get(file); + + return ( + { + if (file && value !== undefined) { + workbenchStore.setCurrentDocumentContent(file, value); + } + }} + options={{ + minimap: { enabled: false }, + fontSize: 14, + lineNumbers: 'on', + theme: 'vs-dark' + }} + /> + ); +} +``` + +**Preview Component** (iframe): + +```tsx +function Preview() { + const [url, setUrl] = useState(''); + + useEffect(() => { + webcontainerPromise.then(async (container) => { + // Listen for server ready event + container.on('server-ready', (port, url) => { + setUrl(url); + }); + }); + }, []); + + return ( +