# Orchard Studio: Implementation Roadmap This roadmap converts the vision into executable phases with clear deliverables. --- ## Phase 0: Engine Validation (Current) **Goal:** Prove the SDLC engine can autonomously build complex systems. **Status:** In Progress (2 of 4 trees passing) ### Slackpath Verification Results | Tree | Infrastructure | Build | Status | Notes | |------|----------------|-------|--------|-------| | `slackpath-1` | ✅ Pass | ✅ Pass | ✅ Complete | 5 polls (~30s) | | `slackpath-2` | ✅ Pass | ✅ Pass | ✅ Complete | 111 polls (~9 min) | | `slackpath-3` | ✅ Pass | ⚠️ Timeout | ❌ Incomplete | Build stayed "pending" - worker capacity | | `slackpath-4` | ✅ Pass | ⚠️ Timeout | ❌ Incomplete | Build stayed "running" - long task | ### Key Findings 1. **Infrastructure provisioning works reliably** - all 4 trees pass `wait-infra` 2. **SDLC execution works** - slackpath-1 and slackpath-2 complete successfully 3. **Worker capacity issue** - slackpath-3 never started (stayed "pending") 4. **Long-running task issue** - slackpath-4 timed out while still "running" ### Remaining Work | Task | Status | Notes | |------|--------|-------| | Worker pool scaling | 🔄 In Progress | Need capacity for parallel builds | | Long-running task handling | 🔄 In Progress | Extend timeouts or checkpoint | | Component templates complete | 🔄 In Progress | service ✅, worker 🔄, app-react 🔄 | ### Success Criteria ```bash # All 4 slackpath trees complete successfully ./cookbooks/scripts/tree-runner.sh cookbooks/trees/slackpath-1-authenticated-service.yaml # ✅ ./cookbooks/scripts/tree-runner.sh cookbooks/trees/slackpath-2-*.yaml # ✅ ./cookbooks/scripts/tree-runner.sh cookbooks/trees/slackpath-3-*.yaml # 🔄 ./cookbooks/scripts/tree-runner.sh cookbooks/trees/slackpath-4-*.yaml # 🔄 ``` ### Exit Condition All 4 slackpath trees complete autonomously without manual intervention. --- ## Phase 1: Blueprint API (Week 1-2) **Goal:** Add Blueprint storage and chat endpoint for Architect conversations. ### Week 1: Database & CRUD #### Day 1-2: Schema & Migrations ```sql -- migrations/000X_blueprints.up.sql CREATE TABLE blueprints ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), project_id UUID NOT NULL REFERENCES projects(id), feature_name TEXT NOT NULL, summary TEXT, sections JSONB NOT NULL DEFAULT '{}', open_questions JSONB NOT NULL DEFAULT '[]', assumptions JSONB NOT NULL DEFAULT '[]', ready_to_build BOOLEAN NOT NULL DEFAULT FALSE, blockers JSONB NOT NULL DEFAULT '[]', status TEXT NOT NULL DEFAULT 'draft', built_feature_slug TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE blueprint_messages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), blueprint_id UUID NOT NULL REFERENCES blueprints(id) ON DELETE CASCADE, role TEXT NOT NULL, content TEXT NOT NULL, blueprint_snapshot JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_blueprints_project ON blueprints(project_id); CREATE INDEX idx_blueprint_messages_blueprint ON blueprint_messages(blueprint_id); ``` #### Day 3-4: Domain & Repository ```go // internal/domain/blueprint.go type Blueprint struct { ID uuid.UUID ProjectID uuid.UUID FeatureName string Summary string Sections BlueprintSections OpenQuestions []OpenQuestion Assumptions []Assumption ReadyToBuild bool Blockers []string Status string // draft, building, built, archived CreatedAt time.Time UpdatedAt time.Time } type BlueprintSections struct { DataModel DataModelSection APIEndpoints APIEndpointsSection UIComponents UIComponentsSection Dependencies DependenciesSection } // internal/port/blueprint.go type BlueprintRepository interface { Create(ctx context.Context, bp *domain.Blueprint) error Get(ctx context.Context, id uuid.UUID) (*domain.Blueprint, error) Update(ctx context.Context, bp *domain.Blueprint) error Delete(ctx context.Context, id uuid.UUID) error ListByProject(ctx context.Context, projectID uuid.UUID) ([]domain.Blueprint, error) AddMessage(ctx context.Context, msg *domain.BlueprintMessage) error GetMessages(ctx context.Context, blueprintID uuid.UUID) ([]domain.BlueprintMessage, error) } ``` #### Day 5: Handler & Routes ```go // internal/handlers/blueprints.go func (h *BlueprintHandler) Mount(r chi.Router) { r.Route("/projects/{projectId}/blueprints", func(r chi.Router) { r.With(auth.RequireScope(auth.ScopeProjectsRead)).Get("/", h.List) r.With(auth.RequireScope(auth.ScopeProjectsRead)).Get("/{blueprintId}", h.Get) r.With(auth.RequireScope(auth.ScopeProjectsExecute)).Post("/", h.Create) r.With(auth.RequireScope(auth.ScopeProjectsExecute)).Delete("/{blueprintId}", h.Delete) r.With(auth.RequireScope(auth.ScopeProjectsExecute)).Post("/{blueprintId}/chat", h.Chat) r.With(auth.RequireScope(auth.ScopeProjectsExecute)).Post("/{blueprintId}/build", h.Build) }) } ``` ### Week 2: Architect Integration + Design References #### Day 6-7: Architect Service + Reference Handling The Architect service handles both text conversations and design reference processing. **Reference Capture (parallel track):** ```go // internal/service/reference_service.go type ReferenceService struct { verifyExecutor port.VerifyExecutor // Reuse Playwright infrastructure storage port.FileStorage logger *slog.Logger } func (s *ReferenceService) CaptureURL(ctx context.Context, blueprintID uuid.UUID, url string) (*domain.Reference, error) { // 1. Screenshot the URL using Playwright pod result, err := s.verifyExecutor.Capture(ctx, domain.VerifySpec{ URL: url, Viewports: []string{"1920x1080"}, // Desktop only for references FullPage: true, }) // 2. Store screenshot path := fmt.Sprintf("/references/%s/%s.png", blueprintID, uuid.New()) s.storage.Save(ctx, path, result.Screenshots["1920x1080"]) // 3. Return reference metadata return &domain.Reference{ ID: uuid.New(), Type: "url", Source: url, Thumbnail: path, }, nil } func (s *ReferenceService) ProcessUpload(ctx context.Context, blueprintID uuid.UUID, data []byte) (*domain.Reference, error) { // Handle user-uploaded screenshots path := fmt.Sprintf("/references/%s/%s.png", blueprintID, uuid.New()) s.storage.Save(ctx, path, data) return &domain.Reference{ ID: uuid.New(), Type: "screenshot", Source: path, Thumbnail: path, }, nil } ``` #### Day 6-7: Architect Service ```go // internal/service/architect_service.go type ArchitectService struct { blueprintRepo port.BlueprintRepository llmClient port.LLMClient logger *slog.Logger } func (s *ArchitectService) Chat(ctx context.Context, blueprintID uuid.UUID, message string) (*ChatResponse, error) { // 1. Load blueprint and conversation history bp, _ := s.blueprintRepo.Get(ctx, blueprintID) messages, _ := s.blueprintRepo.GetMessages(ctx, blueprintID) // 2. Build prompt with Architect persona prompt := s.buildArchitectPrompt(bp, messages, message) // 3. Call LLM response, _ := s.llmClient.Complete(ctx, prompt) // 4. Parse structured response reply, blueprintUpdate := s.parseResponse(response) // 5. Apply blueprint update s.applyBlueprintUpdate(bp, blueprintUpdate) // 6. Save message and updated blueprint s.blueprintRepo.AddMessage(ctx, &domain.BlueprintMessage{...}) s.blueprintRepo.Update(ctx, bp) return &ChatResponse{Reply: reply, Blueprint: bp}, nil } ``` #### Day 8-9: Architect Prompt Engineering ````markdown # Architect Agent You are the Architect for Orchard Studio. Your job is requirements engineering. ## Response Format ALWAYS respond with valid JSON in this exact format: ```json { "reply": "Your conversational response to the user", "blueprint": { "feature": "Feature name", "summary": "One-line summary", "sections": { "dataModel": { "status": "empty|partial|complete", "entities": [...] }, "designSystem": { "status": "empty|partial|complete", "colors": [...], "typography": [...], "spacing": [...], "inspirationNotes": "..." }, ... }, "references": { "items": [...] }, "openQuestions": [...], "assumptions": [...], "readyToBuild": false, "blockers": [...] } } ``` ```` ## Behavior Rules 1. Start with the current blueprint (provided below) 2. Update it based on the user's message 3. Ask 1-2 clarifying questions if gaps exist 4. Suggest building when the plan is complete 5. NEVER write implementation code ## Handling Design References When the user provides a URL or screenshot: 1. **Describe what you observe** (conversational, natural language): - Layout structure, visual hierarchy - Color palette, theme (light/dark) - Component patterns you recognize - Typography and spacing feel 2. **Ask clarifying questions about intent**: - "Match exactly or use as inspiration?" - "Keep these colors or use your brand?" - "Include all elements or simplify for v1?" 3. **Extract structured tokens into designSystem**: - Colors: Primary, secondary, accent, background, text - Typography: Font families, sizes, weights - Spacing: Observed rhythm (4px, 8px, 16px, etc.) - Border radius, shadows, other patterns 4. **Document in inspirationNotes**: - Which elements came from which reference - What was adapted vs. copied - User's stated preferences The conversation stays loose. The Blueprint stays precise. ## Building the Plan Agentively The transition from loose conversation to structured Blueprint is the core challenge. This is NOT a simple extraction—it requires judgment, interpretation, and iteration. Key principles: 1. **Don't rush to structure.** Let the conversation develop naturally. 2. **Show your work.** "I'm inferring spacing of 16px from the card padding I see." 3. **Invite correction.** "Does this match what you had in mind?" 4. **Iterate incrementally.** Each turn refines the Blueprint slightly. 5. **Distinguish confidence levels.** "I'm confident about the 3-tier layout, less sure about the accent color." The Architect is not a form-filler. It's a collaborator that builds shared understanding. ```` #### Day 10: Testing & Verification ```bash # Test conversation flow curl -X POST $RDEV_API_URL/projects/$PROJECT_ID/blueprints \ -H "X-API-Key: $RDEV_API_KEY" \ -d '{"featureName": "Cat Photos"}' # Returns blueprint_id curl -X POST $RDEV_API_URL/projects/$PROJECT_ID/blueprints/$BLUEPRINT_ID/chat \ -H "X-API-Key: $RDEV_API_KEY" \ -d '{"message": "I want users to post cat photos"}' # Returns {reply: "Should photos be public or...", blueprint: {...}} # Test with design reference curl -X POST $RDEV_API_URL/projects/$PROJECT_ID/blueprints/$BLUEPRINT_ID/chat \ -H "X-API-Key: $RDEV_API_KEY" \ -d '{ "message": "Build a photo grid like this", "references": [{"type": "url", "source": "https://unsplash.com/"}] }' # Returns {reply: "I see a masonry grid with rounded corners...", blueprint: {...}} # Blueprint now includes references.items[] and sections.designSystem ```` #### Cookbook Tree: design-from-reference ```yaml # cookbooks/trees/design-from-reference.yaml name: design-from-reference description: "E2E test: Build feature from visual inspiration" vars: project_name: "" reference_url: "https://stripe.com/pricing" steps: create-project: action: api method: POST endpoint: /project body: name: "{{ .vars.project_name }}" outputs: - project_id: .data.name start-blueprint: depends_on: [create-project] action: api method: POST endpoint: "/projects/{{ .outputs.create-project.project_id }}/blueprints" body: featureName: "Pricing Page" outputs: - blueprint_id: .data.id provide-reference: depends_on: [start-blueprint] action: api method: POST endpoint: "/projects/{{ .outputs.create-project.project_id }}/blueprints/{{ .outputs.start-blueprint.blueprint_id }}/chat" body: message: "Build a pricing page inspired by this" references: - type: url source: "{{ .vars.reference_url }}" outputs: - has_design_system: .blueprint.sections.designSystem.status != "empty" clarify-intent: depends_on: [provide-reference] action: api method: POST endpoint: "/projects/{{ .outputs.create-project.project_id }}/blueprints/{{ .outputs.start-blueprint.blueprint_id }}/chat" body: message: "Use as inspiration. Match the 3-tier layout but use a light theme with blue accents." confirm-plan: depends_on: [clarify-intent] action: api method: POST endpoint: "/projects/{{ .outputs.create-project.project_id }}/blueprints/{{ .outputs.start-blueprint.blueprint_id }}/chat" body: message: "Looks good, build it" trigger-build: depends_on: [confirm-plan] action: api method: POST endpoint: "/projects/{{ .outputs.create-project.project_id }}/blueprints/{{ .outputs.start-blueprint.blueprint_id }}/build" outputs: - operation_id: .data.operationId wait-build: depends_on: [trigger-build] action: wait_build build_id: "{{ .outputs.trigger-build.operation_id }}" max_attempts: 120 teardown: - action: api method: DELETE endpoint: "/project/{{ .outputs.create-project.project_id }}" ``` ### Phase 1 Exit Criteria - [ ] Blueprints table created and migrated - [ ] CRUD endpoints working - [ ] Chat endpoint returns structured responses - [ ] Chat endpoint accepts `references[]` array (URLs and uploads) - [ ] URL references auto-screenshot via Playwright - [ ] Architect describes visual references in conversation - [ ] Design tokens extracted into `sections.designSystem` - [ ] Conversation history persisted - [ ] `readyToBuild` computed correctly --- ## Phase 2: Operation Tracking (Week 2-3) **Goal:** Move tree/build orchestration from shell scripts to database-tracked operations. ### Week 2 (overlap with Phase 1): Schema & Core #### Operations Schema ```sql -- migrations/000Y_operations.up.sql CREATE TABLE operations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), project_id UUID NOT NULL REFERENCES projects(id), blueprint_id UUID REFERENCES blueprints(id), operation_type TEXT NOT NULL, tree_name TEXT, status TEXT NOT NULL DEFAULT 'pending', current_phase TEXT, progress JSONB NOT NULL DEFAULT '{}', result JSONB, error TEXT, started_at TIMESTAMPTZ, completed_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE operation_events ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), operation_id UUID NOT NULL REFERENCES operations(id) ON DELETE CASCADE, event_type TEXT NOT NULL, payload JSONB NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_operations_project ON operations(project_id); CREATE INDEX idx_operations_status ON operations(status) WHERE status IN ('pending', 'running'); CREATE INDEX idx_operation_events_operation ON operation_events(operation_id); ``` #### Orchestrator Service ```go // internal/service/orchestrator_service.go type OrchestratorService struct { operationRepo port.OperationRepository eventRepo port.OperationEventRepository sdlcService *SDLCService workQueue port.WorkQueue logger *slog.Logger } func (s *OrchestratorService) StartBuild(ctx context.Context, blueprintID uuid.UUID) (*domain.Operation, error) { // 1. Create operation record op := &domain.Operation{ ProjectID: projectID, BlueprintID: &blueprintID, OperationType: "build", Status: "pending", } s.operationRepo.Create(ctx, op) // 2. Convert blueprint to SDLC feature spec spec := s.blueprintToSpec(blueprint) // 3. Enqueue work item with operation ID s.workQueue.Enqueue(ctx, &domain.WorkItem{ Type: "sdlc", OperationID: op.ID, Spec: spec, }) return op, nil } func (s *OrchestratorService) EmitEvent(ctx context.Context, opID uuid.UUID, event OperationEvent) error { // 1. Persist event s.eventRepo.Create(ctx, opID, event) // 2. Update operation status if event.Type == "phase" { s.operationRepo.UpdatePhase(ctx, opID, event.Phase, event.Status) } return nil } ``` ### Week 3: SSE Streaming #### SSE Handler ```go // internal/handlers/operations_stream.go func (h *OperationsHandler) Stream(w http.ResponseWriter, r *http.Request) { opID := chi.URLParam(r, "operationId") // Set SSE headers w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") flusher, ok := w.(http.Flusher) if !ok { api.WriteInternalError(w, "streaming not supported") return } // Get existing events (for reconnection) events, _ := h.eventRepo.GetByOperation(r.Context(), opID) for _, e := range events { fmt.Fprintf(w, "event: %s\ndata: %s\n\n", e.Type, e.Payload) flusher.Flush() } // Subscribe to new events ch := h.eventBus.Subscribe(opID) defer h.eventBus.Unsubscribe(opID, ch) for { select { case event := <-ch: fmt.Fprintf(w, "event: %s\ndata: %s\n\n", event.Type, event.Payload) flusher.Flush() if event.Type == "complete" || event.Type == "error" { return } case <-r.Context().Done(): return } } } ``` #### SDLC Executor Instrumentation ```go // internal/worker/sdlc_executor.go func (e *SDLCExecutor) Execute(ctx context.Context, task *domain.WorkItem) error { // Emit start event e.emitEvent(ctx, task.OperationID, OperationEvent{ Type: "phase", Phase: "started", Status: "in_progress", Message: "Starting build", }) // ... existing execution logic ... // Emit phase transitions for _, phase := range []string{"spec", "design", "implement", "test", "deploy"} { e.emitEvent(ctx, task.OperationID, OperationEvent{ Type: "phase", Phase: phase, Status: "in_progress", Message: fmt.Sprintf("Starting %s phase", phase), }) // Execute phase... e.emitEvent(ctx, task.OperationID, OperationEvent{ Type: "phase", Phase: phase, Status: "complete", Message: fmt.Sprintf("Completed %s phase", phase), }) } // Emit completion e.emitEvent(ctx, task.OperationID, OperationEvent{ Type: "complete", Status: "success", Message: "Build complete", URL: deployedURL, }) return nil } ``` ### Phase 2 Exit Criteria - [ ] Operations table created and migrated - [ ] `POST /operations` starts tracked operation - [ ] `GET /operations/{id}` returns current status - [ ] `GET /operations/{id}/stream` returns SSE events - [ ] SDLC executor emits phase events - [ ] Events persist for replay on reconnect --- ## Phase 3: Blueprint → Build Integration (Week 3) **Goal:** Connect "Build It" button to full SDLC execution. ### Blueprint to Spec Conversion ```go // internal/service/architect_service.go func (s *ArchitectService) Build(ctx context.Context, blueprintID uuid.UUID) (*domain.Operation, error) { bp, _ := s.blueprintRepo.Get(ctx, blueprintID) // Validate readiness if !bp.ReadyToBuild { return nil, errors.New("blueprint not ready: " + strings.Join(bp.Blockers, ", ")) } // Convert to spec markdown spec := s.renderSpec(bp) // Create SDLC feature feature, _ := s.sdlcService.CreateFeature(ctx, bp.ProjectID, domain.FeatureRequest{ Name: bp.FeatureName, Spec: spec, }) // Link blueprint to feature bp.Status = "building" bp.BuiltFeatureSlug = feature.Slug s.blueprintRepo.Update(ctx, bp) // Start operation op, _ := s.orchestratorService.StartBuild(ctx, bp.ID, feature.Slug) return op, nil } func (s *ArchitectService) renderSpec(bp *domain.Blueprint) string { tmpl := `# Feature: {{.FeatureName}} ## Summary {{.Summary}} ## Data Model {{range .Sections.DataModel.Entities}} ### {{.Name}} | Field | Type | |-------|------| {{range .Fields}}| {{.Name}} | {{.Type}} | {{end}} {{end}} ## API Endpoints {{range .Sections.APIEndpoints.Endpoints}} - ` + "`{{.Method}} {{.Path}}`" + ` - {{.Description}} {{end}} ## UI Components {{range .Sections.UIComponents.Components}} - **{{.Name}}**: {{.Purpose}} {{end}} ` // ... render template ... } ``` ### Phase 3 Exit Criteria - [ ] `POST /blueprints/{id}/build` triggers SDLC feature creation - [ ] Spec markdown generated from blueprint sections - [ ] Operation created and trackable - [ ] Blueprint status updated to "building" → "built" --- ## Phase 4: Frontend (Week 4-5) **Goal:** Build the three-pane Studio interface. ### Week 4: Project Setup & Core Components #### Project Structure ``` apps/studio/ ├── app/ │ ├── layout.tsx │ ├── page.tsx # Template selection │ ├── auth/ │ │ └── callback/page.tsx # OAuth callback │ └── projects/ │ └── [id]/ │ └── page.tsx # Workspace ├── components/ │ ├── templates/ │ │ └── TemplateCard.tsx │ ├── workspace/ │ │ ├── ChatPane.tsx │ │ ├── PlanPane.tsx │ │ ├── PreviewPane.tsx │ │ └── BuildProgress.tsx │ └── ui/ # shadcn components ├── lib/ │ ├── api.ts # rdev-api client │ ├── sse.ts # SSE connection │ └── store.ts # Zustand state ├── tailwind.config.ts └── package.json ``` #### Core State Management ```typescript // lib/store.ts import { create } from "zustand"; interface StudioState { // Blueprint state blueprint: Blueprint | null; messages: Message[]; // Operation state operation: Operation | null; events: OperationEvent[]; // Actions sendMessage: (message: string) => Promise; startBuild: () => Promise; subscribeToOperation: (operationId: string) => void; } export const useStudio = create((set, get) => ({ blueprint: null, messages: [], operation: null, events: [], sendMessage: async (message) => { const { blueprint } = get(); const response = await api.chat(blueprint.id, message); set({ blueprint: response.blueprint, messages: [ ...get().messages, { role: "user", content: message }, { role: "architect", content: response.reply }, ], }); }, startBuild: async () => { const { blueprint } = get(); const operation = await api.build(blueprint.id); set({ operation }); get().subscribeToOperation(operation.id); }, subscribeToOperation: (operationId) => { const eventSource = new EventSource( `${API_URL}/operations/${operationId}/stream`, ); eventSource.onmessage = (e) => { const event = JSON.parse(e.data); set({ events: [...get().events, event] }); if (event.type === "complete") { eventSource.close(); } }; }, })); ``` #### Chat Pane Component ```typescript // components/workspace/ChatPane.tsx export function ChatPane() { const { messages, sendMessage, blueprint } = useStudio(); const [input, setInput] = useState(''); const handleSubmit = async (e: FormEvent) => { e.preventDefault(); if (!input.trim()) return; await sendMessage(input); setInput(''); }; return (
{messages.map((msg, i) => (
{msg.content}
))}
setInput(e.target.value)} placeholder="Describe what you want to build..." className="w-full p-2 border rounded" />
); } ``` #### Plan Pane Component ```typescript // components/workspace/PlanPane.tsx export function PlanPane() { const { blueprint } = useStudio(); if (!blueprint) return ; return (

{blueprint.feature}

{/* Design References */} {blueprint.references?.items?.length > 0 && (
{blueprint.references.items.map(ref => ( ))}
)} {/* Design System (extracted from references) */} {blueprint.sections.designSystem?.status !== 'empty' && (
{blueprint.sections.designSystem.inspirationNotes && (

{blueprint.sections.designSystem.inspirationNotes}

)}
)}
{blueprint.sections.dataModel.entities.map(entity => ( ))}
{blueprint.sections.apiEndpoints.endpoints.map(ep => ( ))}
{blueprint.sections.uiComponents.components.map(comp => ( ))}
{blueprint.openQuestions.length > 0 && (

Open Questions

    {blueprint.openQuestions.map(q => (
  • {q.question}
  • ))}
)}
); } function ReferenceCard({ reference }: { reference: Reference }) { return (
{reference.source}

{reference.type === 'url' ? new URL(reference.source).hostname : 'Uploaded'}

); } function DesignTokens({ tokens }: { tokens: DesignSystem }) { return (
{tokens.colors?.length > 0 && (

Colors

{tokens.colors.map(c => (
))}
)} {tokens.spacing?.length > 0 && (

Spacing

{tokens.spacing.join('px, ')}px

)} {tokens.borderRadius && (

Border Radius

{tokens.borderRadius}

)}
); } ``` ### Week 5: Polish & Integration #### Preview Pane with Build Progress ```typescript // components/workspace/PreviewPane.tsx export function PreviewPane() { const { operation, events } = useStudio(); const [previewUrl, setPreviewUrl] = useState(); // Update preview URL when build completes useEffect(() => { const completeEvent = events.find(e => e.type === 'complete'); if (completeEvent?.url) { setPreviewUrl(completeEvent.url); } }, [events]); return (
{/* Preview iframe */}
{previewUrl ? (