diff --git a/docs/apps-architecture/00-vision.md b/docs/apps-architecture/00-vision.md new file mode 100644 index 0000000..ab4cdb5 --- /dev/null +++ b/docs/apps-architecture/00-vision.md @@ -0,0 +1,258 @@ +# Iris Apps: Vision Document + +## The Problem + +Today's software development is fractured: + +1. **IDEs are separate from runtime** - You write code in VS Code, but your app runs elsewhere. Context switching kills flow. + +2. **AI assistants are disconnected** - AI can help write code, but it can't truly interact with your running application. It suggests; it doesn't collaborate. + +3. **Extensions are second-class** - VS Code extensions can't really "do" things in your application. They're limited to IDE chrome, not application logic. + +4. **Mobile is an afterthought** - Most development tools barely work on tablets, let alone phones. Yet mobile is where users spend their time. + +5. **UI and logic are entangled** - Building an app means maintaining separate frontend and backend codebases, build systems, and deployment pipelines. + +## The Vision + +**Iris Apps** is a new paradigm where: + +> *The application you're building runs inside the tool you're using to build it, the UI is defined by your server code, and an AI agent can interact with everything.* + +### Core Principles + +#### 1. Server-Defined Rendering (SDR) + +The breakthrough insight: **UI is just data**. + +Instead of maintaining a separate frontend codebase, your server returns a component tree that the client renders. Change your server code, and the UI updates instantly. + +```typescript +// Your entire app - server AND UI - in one file +export default defineApp({ + state: { + query: state(''), + results: state([]), + }, + + tools: [ + defineTool({ + name: 'run_query', + description: 'Execute a SQL query', + parameters: z.object({ sql: z.string() }), + execute: async ({ sql }, ctx) => { + const rows = await db.query(sql); + ctx.state.results.set(rows); + return { rowCount: rows.length }; + }, + }), + ], + + ui: (ctx) => ( + Stack({ padding: 16, gap: 12 }, [ + Heading({}, 'Database Explorer'), + Input({ + value: ctx.state.query, + onChangeText: ctx.state.query.set, + placeholder: 'SELECT * FROM ...', + }), + Button({ onPress: () => ctx.runTool('run_query', { sql: ctx.state.query }) }, + 'Execute' + ), + ctx.state.results.length > 0 && DataTable({ data: ctx.state.results }), + ]) + ), +}); +``` + +No separate frontend. No build step for UI. No bundler configuration. Just describe what you want, and it renders—on web AND mobile. + +#### 2. Self-Hosted Development + +You build Iris Apps inside Iris. The app runs as a tab alongside your code. Edit your server file, see the UI update instantly. + +``` +┌─────────────────────────────────────────────────────────────┐ +│ IRIS │ +├─────────────┬───────────────────────────────────────────────┤ +│ Files │ [server.ts] [▶ My App] │ +│ │ ┌─────────────────────────────────────────┐ │ +│ ├─ app.json │ │ │ │ +│ └─ server.ts│ │ Your app running live here │ │ +│ │ │ │ │ +│ │ │ Edit code on left │ │ +│ │ │ See changes instantly on right │ │ +│ │ │ │ │ +│ │ └─────────────────────────────────────────┘ │ +└─────────────┴───────────────────────────────────────────────┘ +``` + +Notice: there's no `ui/` folder. No Vite. No React build. The UI IS the server code. + +#### 3. AI-Native Architecture + +Apps expose **tools** that AI agents can use. The agent doesn't just help you write code; it can *use* your app to accomplish tasks. + +```typescript +// The AI agent can now: +// 1. Understand what your app does (from tool descriptions) +// 2. Actually use it ("Run a query to find all users") +// 3. See and interpret the results +// 4. Help users interact with YOUR app +``` + +#### 4. True Mobile-First + +Because UI is rendered from component definitions, the same app works natively on mobile: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ │ +│ WEB MOBILE (React Native) │ +│ │ +│ Stack({ padding: 16 }) → │ +│ Text({ size: 'lg' }) → │ +│ Button({ onPress }) → │ +│ Input({ value }) → │ +│ │ +│ Same code. Native components. No WebView. │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +#### 5. Graceful Degradation + +During development, code breaks. That's normal. Iris Apps are resilient: + +- **Server error?** Show the error with stack trace and source link +- **Invalid UI tree?** Render what's valid, highlight what's broken +- **Tool fails?** Return error state, don't crash the app + +The development experience should feel like pair programming with a safety net. + +#### 6. Universal Runtime + +An Iris App runs in three modes from the same codebase: + +| Mode | Context | Use Case | +|------|---------|----------| +| **Development** | Inside Iris IDE | Building the app | +| **Installed** | In any Iris project | Using the app as a tool | +| **Standalone** | Independent server | Production deployment | + +Write once. Run everywhere. Not as a compromise, but as a feature. + +## What This Enables + +### For Individual Developers +- Build custom tools in a single file +- See changes instantly—no build step +- AI can use your tools immediately +- Works on your phone and tablet + +### For Teams +- Share internal tools as installable apps +- Consistent UI via shared component library +- AI agents get team-specific capabilities +- Same app works on all devices + +### For the Ecosystem +- Apps are tiny (just server code) +- Easy to review, audit, and trust +- Component library evolves independently +- Mobile and web parity by default + +## Why Server-Defined Rendering? + +The traditional approach: + +``` +Server Code → API → Frontend Code → Build → Bundle → Browser + │ │ │ │ + └─── Two codebases ─────┘ └── Slow ──┘ +``` + +The SDR approach: + +``` +Server Code → Component Tree → Render + │ │ │ + └── One file ───┴── Instant ────┘ +``` + +**Benefits:** +- **Instant updates** - No build step, no bundling +- **Single source of truth** - UI logic lives with business logic +- **Mobile parity** - Same components render natively +- **Smaller apps** - No frontend bundle to ship +- **AI-friendly** - UI is introspectable data + +**Trade-offs:** +- Limited to the component library (by design) +- Can't use arbitrary npm packages in UI (but can in server) +- Less flexibility for highly custom UIs + +For the 90% of apps that are forms, lists, and data displays, SDR is dramatically simpler. For the 10% that need full control, we provide an escape hatch. + +## The Escape Hatch + +Some apps genuinely need full React control: +- 3D visualization (Three.js) +- Rich text editors (ProseMirror, TipTap) +- Canvas-based drawing +- Complex animations + +For these, apps can opt into **Custom UI Mode**: + +```json +{ + "ui": { + "mode": "custom", + "entry": "ui/index.html" + } +} +``` + +Custom UI runs in a sandboxed context with a bridge to the server. It's more work to build but provides full flexibility when needed. + +## Success Metrics + +We'll know we've succeeded when: + +1. **Most apps are single-file** - No separate UI codebase needed + +2. **Mobile works automatically** - Apps built on desktop work on phones + +3. **AI agents use apps naturally** - Tool invocation feels seamless + +4. **Instant feedback** - Edit → See change is under 100ms + +5. **Errors don't break flow** - Every error has a recovery path + +## What We're Not Building + +To stay focused, we explicitly exclude: + +- **A general-purpose IDE** - Iris is for Iris Apps +- **A no-code platform** - This is for developers who write code +- **Arbitrary React apps** - SDR is the primary path; custom UI is the escape hatch +- **A web-only solution** - Mobile parity is a requirement + +## The Name + +**Iris** - The messenger goddess, the rainbow bridge between worlds. + +We're building the bridge between: +- Server and UI (SDR) +- Development and runtime +- Human intent and machine execution +- Desktop and mobile + +**Iris Apps** - Applications that see, understand, and respond. + +--- + +*"The best way to predict the future is to invent it." — Alan Kay* + +*We're not predicting. We're building.* diff --git a/docs/apps-architecture/01-architecture.md b/docs/apps-architecture/01-architecture.md new file mode 100644 index 0000000..ab5eb65 --- /dev/null +++ b/docs/apps-architecture/01-architecture.md @@ -0,0 +1,675 @@ +# Iris Apps: System Architecture + +## Overview + +Iris Apps use **Server-Defined Rendering (SDR)** as the primary UI approach. The server defines the UI as a component tree, and the client renders it using a shared component library. This enables instant updates, mobile parity, and dramatically simpler app development. + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ IRIS PLATFORM │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │ +│ │ App Manager │ │ Tool Registry │ │ Service Runner │ │ +│ │ │ │ │ │ │ │ +│ │ • Discovery │ │ • AI tools │ │ • Background │ │ +│ │ • Lifecycle │ │ • App tools │ │ • Health │ │ +│ │ • Hot reload │ │ • Permissions │ │ • Logging │ │ +│ └───────┬────────┘ └───────┬────────┘ └───────┬────────┘ │ +│ │ │ │ │ +│ └───────────────────┼───────────────────┘ │ +│ │ │ +│ ┌─────────▼─────────┐ │ +│ │ App Runtime │ │ +│ │ │ │ +│ │ • State mgmt │ │ +│ │ • UI generation │◄──── SDR: UI is data │ +│ │ • Tool exec │ │ +│ │ • Event bridge │ │ +│ └─────────┬─────────┘ │ +│ │ │ +│ ┌───────────────────┼───────────────────┐ │ +│ │ │ │ │ +│ ┌───────▼────────┐ ┌───────▼────────┐ ┌──────▼───────┐ │ +│ │ SDR Client │ │ AI Agent │ │ Protocol │ │ +│ │ (Renderer) │ │ Integration │ │ Layer │ │ +│ │ │ │ │ │ │ │ +│ │ • Component │ │ • Tool calls │ │ • WebSocket │ │ +│ │ registry │ │ • Context │ │ • State sync│ │ +│ │ • Web + RN │ │ • Results │ │ • Events │ │ +│ └────────────────┘ └────────────────┘ └──────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +## SDR Architecture + +### How Server-Defined Rendering Works + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ SDR DATA FLOW │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ SERVER CLIENT │ +│ (Bun) (Web / React Native) │ +│ │ +│ ┌──────────────────┐ ┌──────────────────┐ │ +│ │ │ │ │ │ +│ │ App State │───────────────│ State Store │ │ +│ │ { count: 5 } │ WebSocket │ { count: 5 } │ │ +│ │ │ │ │ │ +│ └────────┬─────────┘ └────────┬─────────┘ │ +│ │ │ │ +│ ▼ ▼ │ +│ ┌──────────────────┐ ┌──────────────────┐ │ +│ │ │ │ │ │ +│ │ ui(ctx) │───────────────│ SDR Renderer │ │ +│ │ returns tree │ Component │ renders tree │ │ +│ │ │ Tree (JSON) │ │ │ +│ └──────────────────┘ └────────┬─────────┘ │ +│ │ │ +│ Component Tree: ▼ │ +│ { ┌──────────────────┐ │ +│ type: 'Stack', │ │ │ +│ props: { gap: 12 }, │ Native UI │ │ +│ children: [ │ (React / RN) │ │ +│ { │ │ │ +│ type: 'Text', └──────────────────┘ │ +│ props: { size: 'xl' }, │ +│ children: ['Count: 5'] │ +│ }, │ +│ { │ +│ type: 'Button', │ +│ props: { onPress: { $action: 'increment' } }, │ +│ children: ['+1'] │ +│ } │ +│ ] │ +│ } │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### Component Tree Model + +The UI is represented as a JSON-serializable tree: + +```typescript +// Component node in the tree +interface ComponentNode { + $: 'component'; + type: string; // Component name from registry + key?: string; // React key for reconciliation + props: Record; + children?: UINode[]; +} + +// Different node types +type UINode = + | ComponentNode // { $: 'component', type: 'Button', ... } + | string // Plain text + | number // Numbers render as text + | boolean // true/false (false = don't render) + | null // Don't render + | UINode[]; // Fragment (array of nodes) + +// Props can be values or special objects +type PropValue = + | string | number | boolean | null + | PropValue[] + | { [key: string]: PropValue } + | ActionRef // { $action: 'toolName', args?: {...} } + | StateRef // { $state: 'stateName' } + | StyleValue; // { $style: {...} } +``` + +### Client-Side Rendering + +The SDR client renders the tree using a component registry: + +```typescript +// Component registry maps type names to React components +const ComponentRegistry = { + // Layout + 'Stack': StackComponent, + 'Row': RowComponent, + 'Box': BoxComponent, + 'ScrollView': ScrollViewComponent, + + // Typography + 'Text': TextComponent, + 'Heading': HeadingComponent, + 'Code': CodeComponent, + + // Forms + 'Button': ButtonComponent, + 'Input': InputComponent, + 'TextArea': TextAreaComponent, + 'Select': SelectComponent, + 'Checkbox': CheckboxComponent, + 'Switch': SwitchComponent, + + // Data Display + 'DataTable': DataTableComponent, + 'List': ListComponent, + 'Card': CardComponent, + + // Feedback + 'Alert': AlertComponent, + 'Spinner': SpinnerComponent, + 'Progress': ProgressComponent, + + // ...100+ more components +}; + +// Renderer walks the tree and creates React elements +function renderNode(node: UINode): React.ReactNode { + if (node === null || node === false) return null; + if (typeof node === 'string' || typeof node === 'number') return node; + if (Array.isArray(node)) return node.map(renderNode); + + const Component = ComponentRegistry[node.type]; + if (!Component) { + return ; + } + + const props = resolveProps(node.props); + const children = node.children?.map(renderNode); + + return {children}; +} +``` + +## Subsystem Details + +### 1. App Manager + +The App Manager handles discovery, loading, and lifecycle of apps. + +``` +┌─────────────────────────────────────────────────────────────┐ +│ APP MANAGER │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ Discovery Loading Lifecycle │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ Scan for │ │ Validate │ │ Activate │ │ +│ │ app.json │─────▶│ manifest │─────▶│ runtime │ │ +│ └──────────┘ │ + code │ │ │ │ +│ └──────────┘ └──────────┘ │ +│ │ │ +│ ┌──────────┐ ┌──────────┐ │ │ +│ │ Watch │ │ Hot │◀───────────┘ │ +│ │ files │─────▶│ reload │ │ +│ └──────────┘ └──────────┘ │ +│ │ +│ For SDR apps: │ +│ • No separate UI build needed │ +│ • File change → re-run ui() → push new tree │ +│ • State preserved across reloads │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +**Key Data Structures:** + +```typescript +interface ManagedApp { + // Identity + id: string; + manifest: AppManifest; + projectId: string; + + // Paths + rootPath: string; + serverPath: string; + + // Runtime state + status: AppStatus; + runtime: AppRuntime; + + // UI mode + uiMode: 'sdr' | 'custom'; + + // For SDR: current UI tree + currentUI?: UINode; + + // For custom UI: sandbox info + customUI?: { + entry: string; + devPort?: number; + }; + + // Hot reload + watcher: FSWatcher; +} +``` + +### 2. App Runtime + +The runtime executes app code and manages the UI generation. + +``` +┌─────────────────────────────────────────────────────────────┐ +│ APP RUNTIME │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ Server Module │ │ +│ │ │ │ +│ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │ +│ │ │ State │ │ Tools │ │ UI fn │ │ │ +│ │ │ │ │ │ │ │ │ │ +│ │ │ Reactive │ │ AI- │ │ Returns │ │ │ +│ │ │ values │ │ callable │ │ tree │ │ │ +│ │ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ │ │ +│ │ │ │ │ │ │ +│ │ └──────────────┼──────────────┘ │ │ +│ │ │ │ │ +│ └───────────────────────┼───────────────────────────────┘ │ +│ │ │ +│ ┌───────▼───────┐ │ +│ │ UI Generator │ │ +│ │ │ │ +│ │ • Run ui(ctx)│ │ +│ │ • Diff trees │ │ +│ │ • Push delta │ │ +│ └───────┬───────┘ │ +│ │ │ +│ ┌─────────────┼─────────────┐ │ +│ │ │ │ │ +│ ┌───────▼───────┐ ┌───▼───┐ ┌───────▼───────┐ │ +│ │ State Sync │ │ Tools │ │ Persistence │ │ +│ │ (WebSocket) │ │ │ │ (optional) │ │ +│ └───────────────┘ └───────┘ └───────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +**UI Generation Flow:** + +```typescript +class AppRuntime { + private ui: UINode | null = null; + + // Called when state changes or on reload + async regenerateUI(): Promise { + // Create context with current state + const ctx = this.createUIContext(); + + // Run the UI function + const newUI = await this.module.ui(ctx); + + // Validate the tree + const validated = validateUITree(newUI); + + // Diff against current + const delta = diffTrees(this.ui, validated); + + // Store new tree + this.ui = validated; + + // Push delta to connected clients + this.broadcast({ type: 'ui:update', delta }); + } + + private createUIContext(): UIContext { + return { + state: this.createStateProxy(), + runTool: (name, args) => this.executeTool(name, args), + // ... other context methods + }; + } +} +``` + +### 3. Component Registry + +The component registry maps type strings to actual React components. + +``` +┌─────────────────────────────────────────────────────────────┐ +│ COMPONENT REGISTRY │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ @iris/ui-kit │ │ +│ │ │ │ +│ │ LAYOUT FORMS DATA DISPLAY │ │ +│ │ ──────── ───── ──────────── │ │ +│ │ Stack Button DataTable │ │ +│ │ Row Input List │ │ +│ │ Box TextArea Card │ │ +│ │ ScrollView Select Badge │ │ +│ │ Divider Checkbox Avatar │ │ +│ │ Switch Image │ │ +│ │ TYPOGRAPHY Radio Code │ │ +│ │ ────────── Slider │ │ +│ │ Text DatePicker FEEDBACK │ │ +│ │ Heading ──────── │ │ +│ │ Paragraph NAVIGATION Alert │ │ +│ │ Label ────────── Toast │ │ +│ │ Code Tabs Spinner │ │ +│ │ Link Menu Progress │ │ +│ │ Breadcrumb Skeleton │ │ +│ │ OVERLAYS Link │ │ +│ │ ──────── SPECIALIZED │ │ +│ │ Dialog ICONS ─────────── │ │ +│ │ Sheet ───── CodeEditor │ │ +│ │ Tooltip Icon Terminal │ │ +│ │ Popover (lucide) FileTree │ │ +│ │ DiffViewer │ │ +│ │ Markdown │ │ +│ │ │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +│ Web: React components with Tailwind/CSS │ +│ Mobile: React Native components with StyleSheet │ +│ Same API, platform-native rendering │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +**Cross-Platform Implementation:** + +```typescript +// @iris/ui-kit/src/Button.tsx (web) +export function Button({ onPress, variant, size, children, ...props }) { + return ( + + ); +} + +// @iris/ui-kit/src/Button.native.tsx (React Native) +export function Button({ onPress, variant, size, children, ...props }) { + return ( + + {children} + + ); +} + +// Same import, different implementation per platform +``` + +### 4. Tool Registry + +Tools are the primary way AI agents interact with apps. + +``` +┌─────────────────────────────────────────────────────────────┐ +│ TOOL REGISTRY │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ Tool Index │ │ +│ │ │ │ +│ │ BUILTIN TOOLS (Iris Core) │ │ +│ │ ├─ read_file Read file contents │ │ +│ │ ├─ write_file Write file contents │ │ +│ │ ├─ bash Execute shell command │ │ +│ │ └─ ... │ │ +│ │ │ │ +│ │ APP TOOLS (from Iris Apps) │ │ +│ │ ├─ app:database-explorer/query │ │ +│ │ ├─ app:database-explorer/list_tables │ │ +│ │ ├─ app:api-tester/send_request │ │ +│ │ └─ ... │ │ +│ │ │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +│ Tool Execution Flow: │ +│ 1. AI requests tool call │ +│ 2. Registry finds tool by name │ +│ 3. Permission check │ +│ 4. Execute tool with context │ +│ 5. Tool updates app state (triggers UI regeneration) │ +│ 6. Return result to AI │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 5. SDR Client + +The client receives UI trees and renders them. + +``` +┌─────────────────────────────────────────────────────────────┐ +│ SDR CLIENT │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ Connection │ │ +│ │ │ │ +│ │ WebSocket to Iris server │ │ +│ │ ├─ Receive: ui:sync, ui:update, state:update │ │ +│ │ └─ Send: action, state:set │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ Renderer │ │ +│ │ │ │ +│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ +│ │ │ Receive │──▶│ Validate │──▶│ Render │ │ │ +│ │ │ tree │ │ tree │ │ tree │ │ │ +│ │ └──────────┘ └──────────┘ └────┬─────┘ │ │ +│ │ │ │ │ +│ │ ▼ │ │ +│ │ ┌──────────────┐ │ │ +│ │ │ React / │ │ │ +│ │ │ React Native│ │ │ +│ │ └──────────────┘ │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ Action Handler │ │ +│ │ │ │ +│ │ When user interacts (button press, input change): │ │ +│ │ 1. Serialize action: { $action: 'increment' } │ │ +│ │ 2. Send to server via WebSocket │ │ +│ │ 3. Server executes tool/updates state │ │ +│ │ 4. Server pushes new UI tree │ │ +│ │ 5. Client re-renders │ │ +│ │ │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Data Flow + +### State Change → UI Update + +``` +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ User │ │ Server │ │ Client │ +│ (or AI) │ │ │ │ │ +└──────┬──────┘ └──────┬──────┘ └──────┬──────┘ + │ │ │ + │ Tool call: │ │ + │ increment() │ │ + │──────────────────▶│ │ + │ │ │ + │ │ Update state │ + │ │ count = count + 1│ + │ │ │ + │ │ Re-run ui(ctx) │ + │ │ │ + │ │ UI tree update │ + │ │──────────────────▶│ + │ │ │ + │ │ │ Re-render + │ │ │ + │ Result │ │ + │◀──────────────────│ │ + │ │ │ +``` + +### Hot Reload Flow + +``` +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ File Edit │ │ Server │ │ Client │ +│ │ │ │ │ │ +└──────┬──────┘ └──────┬──────┘ └──────┬──────┘ + │ │ │ + │ server.ts saved │ │ + │──────────────────▶│ │ + │ │ │ + │ │ Snapshot state │ + │ │ │ + │ │ Reload module │ + │ │ (Bun hot reload) │ + │ │ │ + │ │ Restore state │ + │ │ │ + │ │ Re-run ui(ctx) │ + │ │ │ + │ │ Push new tree │ + │ │──────────────────▶│ + │ │ │ + │ │ │ Re-render + │ │ │ (instant) + │ │ │ +``` + +## Custom UI Mode (Escape Hatch) + +For apps that need full React control: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ CUSTOM UI MODE │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ App opts in via manifest: │ +│ { │ +│ "ui": { │ +│ "mode": "custom", │ +│ "entry": "ui/index.html" │ +│ } │ +│ } │ +│ │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ Web Platform │ │ +│ │ │ │ +│ │ Options: │ │ +│ │ • Shadow DOM (same context, style isolation) │ │ +│ │ • iframe (full isolation, security sandbox) │ │ +│ │ │ │ +│ │ Communication via postMessage bridge │ │ +│ │ │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ Mobile Platform │ │ +│ │ │ │ +│ │ WebView with bridge to React Native │ │ +│ │ (Acknowledged limitation - not native) │ │ +│ │ │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +│ Trade-offs vs SDR: │ +│ ✗ Requires separate build system (Vite) │ +│ ✗ Slower hot reload │ +│ ✗ Mobile uses WebView, not native │ +│ ✓ Full control over UI │ +│ ✓ Can use any npm packages │ +│ ✓ Complex visualizations possible │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Deployment Architecture + +### Standalone Mode + +``` +┌─────────────────────────────────────────────────────────────┐ +│ STANDALONE APP │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ @iris/runtime │ │ +│ │ │ │ +│ │ Minimal runtime that provides: │ │ +│ │ • App loading and execution │ │ +│ │ • State management │ │ +│ │ • WebSocket server for SDR clients │ │ +│ │ • HTTP API for tool invocation │ │ +│ │ • Optional: static file serving for web client │ │ +│ │ │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +│ Deployment options: │ +│ • Server + Web client (serve from same process) │ +│ • Server + Mobile app (connect via WebSocket) │ +│ • API-only (tools accessible via REST) │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Technology Choices + +| Component | Technology | Rationale | +|-----------|------------|-----------| +| **Backend Runtime** | Bun | Fast startup, native TS, hot reload | +| **Component Library** | React + React Native | Cross-platform, ecosystem | +| **State Management** | Custom signals | Simple, serializable, reactive | +| **Communication** | WebSocket | Real-time, bidirectional | +| **Schema Validation** | Zod | Runtime validation, TS inference | +| **Styling (Web)** | Tailwind CSS | Utility-first, consistent | +| **Styling (Mobile)** | StyleSheet | Native performance | + +## Security Boundaries + +``` +┌─────────────────────────────────────────────────────────────┐ +│ SECURITY ZONES │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ ZONE 1: Iris Core (Trusted) │ │ +│ │ │ │ +│ │ • Full system access │ │ +│ │ • Manages all apps │ │ +│ │ • Controls permissions │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ ZONE 2: App Server (Permission-gated) │ │ +│ │ │ │ +│ │ • Declared permissions only │ │ +│ │ • Scoped file access │ │ +│ │ • Scoped network access │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ ZONE 3: SDR Client (Data-only) │ │ +│ │ │ │ +│ │ • Only renders data from server │ │ +│ │ • No direct system access │ │ +│ │ • Components are from trusted registry │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +│ Note: SDR simplifies security! │ +│ • No arbitrary JS execution on client │ +│ • No need for iframe sandbox │ +│ • All logic runs on server (controlled environment) │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +*Next: [02-app-model.md](./02-app-model.md) - Detailed app model and lifecycle* diff --git a/docs/apps-architecture/02-app-model.md b/docs/apps-architecture/02-app-model.md new file mode 100644 index 0000000..22f4b31 --- /dev/null +++ b/docs/apps-architecture/02-app-model.md @@ -0,0 +1,670 @@ +# Iris Apps: App Model & Lifecycle + +## The App Model + +An Iris App is a single-file application that combines: + +1. **State** - Reactive values that drive the UI +2. **Tools** - Functions exposed to AI agents and the UI +3. **UI** - A function that returns a component tree (SDR) +4. **Services** - Optional background processes + +``` +┌─────────────────────────────────────────────────────────────┐ +│ IRIS APP │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ Manifest │ │ +│ │ (app.json) │ │ +│ │ │ │ +│ │ • Identity (name, version, description) │ │ +│ │ • Capabilities (tools, services) │ │ +│ │ • Requirements (permissions) │ │ +│ │ • UI mode (sdr or custom) │ │ +│ └──────────────────────────────────────────────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ Server Module │ │ +│ │ (server.ts) │ │ +│ │ │ │ +│ │ export default defineApp({ │ │ +│ │ state: { ... }, // Reactive state │ │ +│ │ tools: [ ... ], // AI-callable functions │ │ +│ │ ui: (ctx) => ..., // SDR UI function │ │ +│ │ services: { ... }, // Background services │ │ +│ │ onActivate: ..., // Lifecycle hooks │ │ +│ │ }) │ │ +│ │ │ │ +│ └──────────────────────────────────────────────────────┘ │ +│ │ +│ For most apps: NO SEPARATE UI FOLDER │ +│ The ui() function IS the UI │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +## App Structure + +### Minimal App (Single File) + +``` +my-app/ +├── app.json # Manifest +└── server.ts # Everything else +``` + +### App with Assets + +``` +my-app/ +├── app.json # Manifest +├── server.ts # App logic + UI +└── assets/ # Static files (optional) + ├── icon.png + └── data.json +``` + +### App with Custom UI (Escape Hatch) + +``` +my-app/ +├── app.json # Manifest with ui.mode: "custom" +├── server.ts # Tools, state, services +└── ui/ # Full React app + ├── index.html + ├── vite.config.ts + └── src/ + └── App.tsx +``` + +## Manifest Schema + +### SDR App (Default) + +```json +{ + "$schema": "https://iris.dev/schemas/app.json", + + "name": "database-explorer", + "displayName": "Database Explorer", + "version": "1.0.0", + "description": "Browse and query SQLite databases", + "icon": "database", + + "server": "server.ts", + + "tools": [ + { + "name": "query", + "description": "Execute a SQL query against the database" + }, + { + "name": "list_tables", + "description": "List all tables in the database" + } + ], + + "permissions": [ + "filesystem:read:$PROJECT", + "ai:chat" + ] +} +``` + +### Custom UI App + +```json +{ + "name": "3d-visualizer", + "displayName": "3D Visualizer", + "version": "1.0.0", + + "server": "server.ts", + + "ui": { + "mode": "custom", + "entry": "ui/index.html", + "devPort": 5174 + }, + + "tools": [...], + "permissions": [...] +} +``` + +### Manifest Fields + +| Field | Required | Description | +|-------|----------|-------------| +| `name` | Yes | Unique identifier (lowercase, hyphens) | +| `displayName` | Yes | Human-readable name | +| `version` | Yes | Semver version | +| `description` | No | Brief description | +| `icon` | No | Icon name (from Lucide icons) | +| `server` | Yes | Path to server module | +| `ui` | No | Custom UI config (omit for SDR) | +| `ui.mode` | No | `"sdr"` (default) or `"custom"` | +| `ui.entry` | Only for custom | Path to UI entry point | +| `tools` | No | Tools exposed to AI agent | +| `services` | No | Background services | +| `permissions` | Yes | Required permissions | + +## Complete App Example + +```typescript +// server.ts +import { defineApp, defineTool, state } from '@iris/app-sdk'; +import { Stack, Heading, Input, Button, DataTable, Text, Alert } from '@iris/ui'; +import { z } from 'zod'; + +export default defineApp({ + // Reactive state + state: { + query: state('SELECT * FROM users LIMIT 10'), + results: state([]), + error: state(null), + isLoading: state(false), + }, + + // Tools exposed to AI agent + tools: [ + defineTool({ + name: 'query', + description: 'Execute a SQL query against the database', + parameters: z.object({ + sql: z.string().describe('The SQL query to execute'), + }), + execute: async ({ sql }, ctx) => { + ctx.state.isLoading.set(true); + ctx.state.error.set(null); + ctx.state.query.set(sql); + + try { + const db = await ctx.getService('database'); + const results = await db.query(sql); + ctx.state.results.set(results); + return { success: true, rowCount: results.length }; + } catch (e) { + ctx.state.error.set(e.message); + return { success: false, error: e.message }; + } finally { + ctx.state.isLoading.set(false); + } + }, + }), + + defineTool({ + name: 'list_tables', + description: 'List all tables in the database', + parameters: z.object({}), + execute: async (_, ctx) => { + const db = await ctx.getService('database'); + const tables = await db.query( + "SELECT name FROM sqlite_master WHERE type='table'" + ); + return { tables: tables.map(t => t.name) }; + }, + }), + ], + + // Services + services: { + database: { + start: async (ctx) => { + const dbPath = await ctx.getConfig('databasePath'); + return new Database(dbPath); + }, + stop: async (db) => { + await db.close(); + }, + autoStart: true, + }, + }, + + // UI function - returns component tree + ui: (ctx) => { + const { query, results, error, isLoading } = ctx.state; + + return Stack({ padding: 16, gap: 16 }, [ + // Header + Heading({ level: 1 }, 'Database Explorer'), + + // Query input + Stack({ gap: 8 }, [ + Input({ + value: query, + onChangeText: query.set, + placeholder: 'Enter SQL query...', + multiline: true, + rows: 3, + }), + Button({ + onPress: () => ctx.runTool('query', { sql: query.get() }), + disabled: isLoading, + }, isLoading ? 'Running...' : 'Execute Query'), + ]), + + // Error display + error && Alert({ variant: 'error' }, error), + + // Results table + results.length > 0 && DataTable({ + data: results, + columns: Object.keys(results[0] || {}), + }), + + // Empty state + results.length === 0 && !error && !isLoading && + Text({ color: 'muted' }, 'Run a query to see results'), + ]); + }, + + // Lifecycle + onActivate: async (ctx) => { + ctx.log.info('Database Explorer activated'); + }, +}); +``` + +## SDR UI Model + +### Component Functions + +UI is built with component functions that return component nodes: + +```typescript +// Component function signature +function ComponentName(props: Props, children?: Children): ComponentNode + +// Examples +Heading({ level: 1 }, 'Hello World') +Button({ onPress: handler, variant: 'primary' }, 'Click Me') +Stack({ gap: 8, padding: 16 }, [child1, child2, child3]) +``` + +### Component Node Structure + +```typescript +// What the component function returns +interface ComponentNode { + $: 'component'; + type: string; // 'Button', 'Stack', etc. + props: Record; + children?: UINode[]; +} + +// Example +Heading({ level: 1 }, 'Hello') +// Returns: +{ + $: 'component', + type: 'Heading', + props: { level: 1 }, + children: ['Hello'] +} +``` + +### Conditional Rendering + +```typescript +// Using && for conditional +error && Alert({ variant: 'error' }, error) + +// Using ternary +isLoading ? Spinner({}) : Button({}, 'Submit') + +// Filtering arrays +items.filter(item => item.visible).map(item => Card({}, item.name)) +``` + +### Lists and Keys + +```typescript +// Map over data with keys +items.map(item => + Card({ key: item.id }, [ + Text({ weight: 'bold' }, item.title), + Text({ color: 'muted' }, item.description), + ]) +) +``` + +### Event Handlers + +```typescript +// Direct state update +Input({ + value: ctx.state.query, + onChangeText: ctx.state.query.set, +}) + +// Tool call +Button({ + onPress: () => ctx.runTool('submit', { data: ctx.state.form.get() }), +}, 'Submit') + +// Custom handler +Button({ + onPress: { $action: 'increment', args: { amount: 5 } }, +}, '+5') +``` + +### State References + +```typescript +// Reading state in UI +Text({}, `Count: ${ctx.state.count}`) + +// The ctx.state.count is reactive - UI updates when it changes +``` + +## State Model + +### State Types + +```typescript +import { state, computed, query } from '@iris/app-sdk'; + +// Primitive state - simple reactive value +const count = state(0); +const name = state(''); +const items = state([]); + +// Computed state - derived from other state +const doubled = computed((get) => get(count) * 2); +const filtered = computed((get) => + get(items).filter(i => i.active) +); + +// Query state - async data with caching +const users = query(async (ctx) => { + const response = await ctx.fetch('/api/users'); + return response.json(); +}, { + staleTime: 60_000, // Fresh for 1 minute +}); +``` + +### State Operations + +```typescript +// Get current value +const current = count.get(); + +// Set new value +count.set(5); + +// Update based on previous +count.update(n => n + 1); + +// For arrays +items.update(list => [...list, newItem]); +items.update(list => list.filter(i => i.id !== id)); + +// Subscribe to changes (rarely needed - UI auto-subscribes) +const unsubscribe = count.subscribe(value => { + console.log('Count changed:', value); +}); +``` + +### State in UI Context + +```typescript +ui: (ctx) => { + // Access state values (reactive) + const { count, items } = ctx.state; + + // Use in UI + return Stack({}, [ + Text({}, `Count: ${count}`), + Text({}, `Items: ${items.length}`), + + // Update state + Button({ onPress: () => count.update(n => n + 1) }, '+1'), + ]); +} +``` + +## Tool Model + +### Tool Definition + +```typescript +defineTool({ + // Required + name: 'query', + description: 'Execute a SQL query', + parameters: z.object({ + sql: z.string().describe('SQL query to execute'), + params: z.array(z.unknown()).optional(), + }), + execute: async (args, ctx) => { + // Implementation + return { success: true, data: results }; + }, + + // Optional + examples: [ + { + description: 'Get all users', + input: { sql: 'SELECT * FROM users' }, + }, + ], +}) +``` + +### Tool Context + +```typescript +interface ToolContext { + // App identity + appId: string; + projectId: string; + + // State access + state: StateMap; + + // Services + getService(name: string): Promise; + + // Platform access + iris: { + ai: { chat, embed }; + fs: { read, write, list }; + tools: { call }; + }; + + // Logging + log: Logger; +} +``` + +### Calling Tools from UI + +```typescript +ui: (ctx) => { + return Button({ + onPress: async () => { + const result = await ctx.runTool('query', { + sql: ctx.state.query.get() + }); + if (!result.success) { + ctx.state.error.set(result.error); + } + } + }, 'Run Query'); +} +``` + +## Service Model + +### Internal Service + +```typescript +services: { + database: { + start: async (ctx) => { + const config = await ctx.getConfig('database'); + const db = new Database(config.path); + return db; + }, + stop: async (db) => { + await db.close(); + }, + autoStart: true, + }, +} + +// Usage in tools +const db = await ctx.getService('database'); +``` + +### Process Service + +```typescript +services: { + 'dev-server': { + type: 'process', + command: 'npm', + args: ['run', 'dev'], + cwd: './server', + env: { PORT: '3001' }, + autoStart: false, + }, +} +``` + +## App Lifecycle + +### State Machine + +``` + ┌─────────────┐ + │ DISCOVERED │ + └──────┬──────┘ + │ load() + ▼ + ┌─────────────┐ + │ LOADING │ + └──────┬──────┘ + │ + ┌────┴────┐ + ▼ ▼ + ┌─────────┐ ┌─────────┐ + │ ERROR │ │ LOADED │ + └─────────┘ └────┬────┘ + │ activate() + ▼ + ┌─────────────┐ + │ ACTIVE │◄──── Hot reload + └──────┬──────┘ (re-run ui()) + │ + deactivate() + │ + ▼ + ┌─────────────┐ + │ STOPPED │ + └─────────────┘ +``` + +### Lifecycle Hooks + +```typescript +export default defineApp({ + // Called when app becomes active + onActivate: async (ctx) => { + await ctx.startService('database'); + ctx.log.info('App activated'); + }, + + // Called when app is deactivated + onDeactivate: async (ctx) => { + await ctx.stopService('database'); + ctx.log.info('App deactivated'); + }, + + // Called on hot reload (state is preserved) + onReload: async (ctx, previousState) => { + ctx.log.info('App reloaded'); + }, + + // Called when an error occurs + onError: async (ctx, error) => { + ctx.log.error('App error:', error); + ctx.state.lastError.set(error.message); + return true; // Error handled + }, +}); +``` + +## Configuration + +### Config Schema in Manifest + +```json +{ + "configuration": { + "properties": { + "databasePath": { + "type": "string", + "description": "Path to SQLite database file", + "default": "./data.db" + }, + "maxResults": { + "type": "number", + "default": 100 + } + } + } +} +``` + +### Accessing Config + +```typescript +// In tools or lifecycle hooks +const dbPath = await ctx.getConfig('databasePath'); +await ctx.setConfig('maxResults', 50); + +// Watch for changes +ctx.onConfigChange('databasePath', async (newPath) => { + await ctx.restartService('database'); +}); +``` + +## Permission Model + +### Permission Declaration + +```json +{ + "permissions": [ + "filesystem:read:$PROJECT", + "filesystem:write:$APP/data", + "network:localhost", + "ai:chat" + ] +} +``` + +### Permission Scopes + +| Scope | Description | +|-------|-------------| +| `$PROJECT` | Current project directory | +| `$APP` | App's directory | +| `$DATA` | App's data directory | +| Domain pattern | e.g., `*.example.com` | + +### Permission Categories + +- `filesystem:read`, `filesystem:write` - File access +- `network:*`, `network:localhost` - Network access +- `ai:chat`, `ai:embed` - AI model access +- `iris:tools` - Call other Iris tools +- `process:spawn` - Run shell commands + +--- + +*Next: [03-sdk-design.md](./03-sdk-design.md) - SDK API design* diff --git a/docs/apps-architecture/03-sdk-design.md b/docs/apps-architecture/03-sdk-design.md new file mode 100644 index 0000000..7f7c352 --- /dev/null +++ b/docs/apps-architecture/03-sdk-design.md @@ -0,0 +1,708 @@ +# Iris Apps: SDK Design + +## Overview + +The Iris App SDK enables developers to build apps with Server-Defined Rendering. The SDK is intentionally simple—most apps are a single file. + +``` +@iris/app-sdk +├── defineApp() Define an app +├── defineTool() Define a tool +├── state() Create reactive state +├── computed() Create derived state +├── query() Create async query state +└── ... + +@iris/ui +├── Stack, Row, Box Layout components +├── Text, Heading Typography +├── Button, Input Form elements +├── DataTable, List Data display +└── 100+ components Full UI kit +``` + +## Quick Start + +```typescript +// server.ts - A complete Iris App +import { defineApp, defineTool, state } from '@iris/app-sdk'; +import { Stack, Text, Button } from '@iris/ui'; +import { z } from 'zod'; + +export default defineApp({ + state: { + count: state(0), + }, + + tools: [ + defineTool({ + name: 'increment', + description: 'Increment the counter', + parameters: z.object({ + amount: z.number().default(1), + }), + execute: async ({ amount }, ctx) => { + ctx.state.count.update(n => n + amount); + return { newValue: ctx.state.count.get() }; + }, + }), + ], + + ui: (ctx) => ( + Stack({ padding: 24, gap: 16, align: 'center' }, [ + Text({ size: '4xl', weight: 'bold' }, ctx.state.count), + Button({ + onPress: () => ctx.state.count.update(n => n + 1), + size: 'lg', + }, '+1'), + ]) + ), +}); +``` + +## Core API + +### defineApp() + +Creates an app definition: + +```typescript +import { defineApp } from '@iris/app-sdk'; + +export default defineApp({ + // Reactive state (optional) + state: { + count: state(0), + items: state([]), + }, + + // Computed values (optional) + computed: { + total: computed((get) => get('items').reduce((sum, i) => sum + i.price, 0)), + }, + + // Async queries (optional) + queries: { + users: query(async (ctx) => { + return ctx.fetch('/api/users').then(r => r.json()); + }), + }, + + // Tools for AI and UI (optional) + tools: [ + defineTool({ ... }), + ], + + // Background services (optional) + services: { + database: { ... }, + }, + + // UI function - returns component tree + ui: (ctx) => { + return Stack({}, [...]); + }, + + // Lifecycle hooks (optional) + onActivate: async (ctx) => { ... }, + onDeactivate: async (ctx) => { ... }, + onReload: async (ctx) => { ... }, + onError: async (ctx, error) => { ... }, +}); +``` + +### defineTool() + +Creates a tool that AI agents and the UI can call: + +```typescript +import { defineTool } from '@iris/app-sdk'; +import { z } from 'zod'; + +const queryTool = defineTool({ + // Required + name: 'query', + description: 'Execute a SQL query against the database', + parameters: z.object({ + sql: z.string().describe('The SQL query'), + limit: z.number().optional().default(100), + }), + execute: async (args, ctx) => { + const db = await ctx.getService('database'); + const results = await db.query(args.sql, args.limit); + ctx.state.lastQuery.set(args.sql); + ctx.state.results.set(results); + return { rowCount: results.length, rows: results }; + }, + + // Optional + examples: [ + { description: 'Get all users', input: { sql: 'SELECT * FROM users' } }, + ], +}); +``` + +### state() + +Creates reactive state: + +```typescript +import { state } from '@iris/app-sdk'; + +// Primitive values +const count = state(0); +const name = state(''); +const isActive = state(false); + +// Complex values +const items = state([]); +const user = state(null); +const form = state({ name: '', email: '' }); + +// With options +const history = state([], { + persist: true, // Save to disk + persistKey: 'history', // Custom key + maxLength: 100, // Limit array length +}); +``` + +**State Operations:** + +```typescript +// Read +const value = count.get(); + +// Write +count.set(5); + +// Update +count.update(n => n + 1); +items.update(list => [...list, newItem]); +items.update(list => list.filter(i => i.id !== targetId)); + +// In UI context, state is reactive +ui: (ctx) => { + // This auto-updates when count changes + return Text({}, `Count: ${ctx.state.count}`); +} +``` + +### computed() + +Creates derived state: + +```typescript +import { computed } from '@iris/app-sdk'; + +// Simple computation +const doubled = computed((get) => get('count') * 2); + +// Derived from multiple states +const summary = computed((get) => { + const items = get('items'); + const filter = get('filter'); + return items + .filter(i => i.name.includes(filter)) + .map(i => i.name) + .join(', '); +}); + +// Async computed (becomes a query) +const filteredUsers = computed(async (get, ctx) => { + const filter = get('filter'); + const users = await ctx.queries.users.get(); + return users.filter(u => u.name.includes(filter)); +}); +``` + +### query() + +Creates async data with caching: + +```typescript +import { query } from '@iris/app-sdk'; + +const users = query( + async (ctx) => { + const response = await ctx.fetch('/api/users'); + return response.json(); + }, + { + staleTime: 60_000, // Fresh for 1 minute + cacheTime: 300_000, // Keep in cache for 5 minutes + refetchOnMount: true, // Refetch when app opens + retry: 3, // Retry failed requests + } +); + +// Usage +const data = users.get(); // Current data (may be undefined) +await users.load(); // Force load +users.invalidate(); // Clear cache +const isLoading = users.loading; // Loading state +const error = users.error; // Error state +``` + +## UI Components (@iris/ui) + +### Layout Components + +```typescript +import { Stack, Row, Box, ScrollView, Divider } from '@iris/ui'; + +// Vertical stack +Stack({ gap: 8, padding: 16 }, [ + child1, + child2, +]) + +// Horizontal row +Row({ gap: 8, justify: 'space-between' }, [ + left, + right, +]) + +// Generic container +Box({ padding: 16, background: 'muted', rounded: 'lg' }, content) + +// Scrollable area +ScrollView({ maxHeight: 400 }, longContent) + +// Separator +Divider({ orientation: 'horizontal' }) +``` + +### Typography + +```typescript +import { Text, Heading, Code, Link } from '@iris/ui'; + +// Basic text +Text({}, 'Hello world') +Text({ size: 'lg', weight: 'bold', color: 'primary' }, 'Important') + +// Headings +Heading({ level: 1 }, 'Page Title') +Heading({ level: 2 }, 'Section') + +// Code +Code({ language: 'typescript' }, 'const x = 1') + +// Links +Link({ href: '/other-page' }, 'Click here') +``` + +### Form Elements + +```typescript +import { Button, Input, TextArea, Select, Checkbox, Switch } from '@iris/ui'; + +// Button +Button({ onPress: handler, variant: 'primary', size: 'lg' }, 'Submit') +Button({ onPress: handler, disabled: true }, 'Disabled') + +// Text input +Input({ + value: ctx.state.name, + onChangeText: ctx.state.name.set, + placeholder: 'Enter name...', +}) + +// Number input +Input({ + type: 'number', + value: ctx.state.amount, + onChangeText: v => ctx.state.amount.set(Number(v)), +}) + +// Text area +TextArea({ + value: ctx.state.description, + onChangeText: ctx.state.description.set, + rows: 4, +}) + +// Select dropdown +Select({ + value: ctx.state.category, + onValueChange: ctx.state.category.set, + options: [ + { label: 'Option 1', value: 'opt1' }, + { label: 'Option 2', value: 'opt2' }, + ], +}) + +// Checkbox +Checkbox({ + checked: ctx.state.agreed, + onCheckedChange: ctx.state.agreed.set, + label: 'I agree to terms', +}) + +// Switch/toggle +Switch({ + checked: ctx.state.enabled, + onCheckedChange: ctx.state.enabled.set, +}) +``` + +### Data Display + +```typescript +import { DataTable, List, Card, Badge, Avatar } from '@iris/ui'; + +// Data table +DataTable({ + data: ctx.state.users, + columns: [ + { key: 'name', header: 'Name' }, + { key: 'email', header: 'Email' }, + { key: 'role', header: 'Role', render: (role) => Badge({}, role) }, + ], + onRowClick: (row) => ctx.state.selected.set(row.id), +}) + +// List +List({ + data: ctx.state.items, + renderItem: (item) => ( + Row({ gap: 8 }, [ + Avatar({ src: item.avatar }), + Text({}, item.name), + ]) + ), +}) + +// Card +Card({ padding: 16 }, [ + Heading({ level: 3 }, 'Card Title'), + Text({}, 'Card content'), +]) + +// Badge +Badge({ variant: 'success' }, 'Active') +Badge({ variant: 'error' }, 'Failed') +``` + +### Feedback + +```typescript +import { Alert, Spinner, Progress, Toast } from '@iris/ui'; + +// Alert +Alert({ variant: 'info', title: 'Note' }, 'This is informational') +Alert({ variant: 'error' }, 'Something went wrong') + +// Loading spinner +Spinner({ size: 'lg' }) + +// Progress bar +Progress({ value: 0.7, max: 1 }) + +// Toast (triggered via context) +ctx.toast({ message: 'Saved!', variant: 'success' }) +``` + +### Overlays + +```typescript +import { Dialog, Sheet, Tooltip, Popover } from '@iris/ui'; + +// Dialog +Dialog({ + open: ctx.state.dialogOpen, + onOpenChange: ctx.state.dialogOpen.set, + title: 'Confirm', +}, [ + Text({}, 'Are you sure?'), + Row({ gap: 8, justify: 'end' }, [ + Button({ onPress: () => ctx.state.dialogOpen.set(false) }, 'Cancel'), + Button({ onPress: handleConfirm, variant: 'primary' }, 'Confirm'), + ]), +]) + +// Tooltip +Tooltip({ content: 'More information' }, + Button({}, 'Hover me') +) +``` + +### Icons + +```typescript +import { Icon } from '@iris/ui'; + +// Uses Lucide icons +Icon({ name: 'database', size: 24 }) +Icon({ name: 'check', color: 'green' }) +Icon({ name: 'x', color: 'red' }) +``` + +### Specialized Components + +```typescript +import { CodeEditor, Terminal, FileTree, Markdown } from '@iris/ui'; + +// Code editor +CodeEditor({ + value: ctx.state.code, + onChange: ctx.state.code.set, + language: 'typescript', + theme: 'dark', +}) + +// Terminal output +Terminal({ + lines: ctx.state.logs, + autoScroll: true, +}) + +// File tree +FileTree({ + files: ctx.state.files, + onSelect: (path) => ctx.state.selectedFile.set(path), +}) + +// Markdown renderer +Markdown({}, ctx.state.readme) +``` + +## UI Context + +The `ui` function receives a context with everything needed: + +```typescript +ui: (ctx) => { + // State access + const { count, items, user } = ctx.state; + + // Computed values + const { total, filteredItems } = ctx.computed; + + // Query data + const users = ctx.queries.users; + + // Run tools + const handleSubmit = () => ctx.runTool('submit', { data: form.get() }); + + // Access Iris platform (if permitted) + const handleAI = async () => { + const response = await ctx.iris.ai.chat([ + { role: 'user', content: 'Summarize this' } + ]); + }; + + // Show notifications + ctx.toast({ message: 'Saved!', variant: 'success' }); + + // Theme info + const { theme, isDark } = ctx.theme; + + // App info + const { appId, projectId } = ctx.app; + + return Stack({}, [...]); +} +``` + +## Platform Access + +Apps can access Iris platform features through the context: + +### AI Access + +```typescript +// Requires: "ai:chat" permission +const response = await ctx.iris.ai.chat([ + { role: 'system', content: 'You are a helpful assistant' }, + { role: 'user', content: userQuestion }, +], { + model: 'claude-sonnet', + maxTokens: 1000, +}); + +// Requires: "ai:embed" permission +const embeddings = await ctx.iris.ai.embed(['text1', 'text2']); +``` + +### File System + +```typescript +// Requires: "filesystem:read:$PROJECT" permission +const content = await ctx.iris.fs.read('src/index.ts'); +const files = await ctx.iris.fs.list('src'); +const exists = await ctx.iris.fs.exists('package.json'); + +// Requires: "filesystem:write:$APP/data" permission +await ctx.iris.fs.write('data/output.json', JSON.stringify(data)); +``` + +### Other Tools + +```typescript +// Requires: "iris:tools" permission +const result = await ctx.iris.tools.call('bash', { + command: 'npm run build', +}); +``` + +### Navigation + +```typescript +// Requires: "iris:navigation" permission (usually implicit) +ctx.iris.navigate('/files/src/index.ts'); +ctx.iris.openTab({ type: 'file', path: 'src/index.ts' }); +``` + +## Type Safety + +The SDK provides full TypeScript support: + +```typescript +// Types are inferred from state definitions +const count = state(0); // StateHandle +const items = state([]); // StateHandle + +// Tool parameters are validated at runtime AND compile time +defineTool({ + parameters: z.object({ + id: z.string(), + count: z.number(), + }), + execute: async (args, ctx) => { + // args is typed as { id: string; count: number } + args.id; // string + args.count; // number + }, +}); + +// Component props are typed +Button({ + onPress: () => {}, // required + variant: 'primary', // 'primary' | 'secondary' | 'ghost' | ... + size: 'lg', // 'sm' | 'md' | 'lg' + disabled: false, // boolean +}, 'Click'); +``` + +## Custom UI Mode + +For apps that need full React control, opt into custom UI mode: + +```json +{ + "ui": { + "mode": "custom", + "entry": "ui/index.html" + } +} +``` + +Then create a full React app in `ui/`: + +```typescript +// ui/src/App.tsx +import { useIrisApp, IrisAppProvider } from '@iris/app-sdk/react'; + +function App() { + return ( + + + + ); +} + +function CustomUI() { + const { state, runTool, iris } = useIrisApp(); + + // Full React control + return ( +
+ + +
+ ); +} +``` + +**Custom UI Hooks:** + +```typescript +import { + useIrisApp, // Full app context + useAppState, // Single state value + useRunTool, // Tool execution + useIrisAI, // AI access + useIrisFS, // File system +} from '@iris/app-sdk/react'; + +function MyComponent() { + const [count, setCount] = useAppState('count'); + const runTool = useRunTool(); + const ai = useIrisAI(); + + // Use like regular React hooks +} +``` + +## Error Handling + +### In Tools + +```typescript +defineTool({ + execute: async (args, ctx) => { + try { + const result = await riskyOperation(args); + return { success: true, data: result }; + } catch (error) { + // Return error - don't throw + return { success: false, error: error.message }; + } + }, +}); +``` + +### In UI + +```typescript +ui: (ctx) => { + const { error, isLoading, data } = ctx.state; + + // Handle error states in UI + if (error) { + return Alert({ variant: 'error' }, error); + } + + if (isLoading) { + return Stack({ align: 'center', padding: 24 }, [ + Spinner({}), + Text({}, 'Loading...'), + ]); + } + + return DataTable({ data }); +} +``` + +### Global Error Handler + +```typescript +export default defineApp({ + onError: async (ctx, error) => { + ctx.log.error('App error:', error); + ctx.state.lastError.set(error.message); + + // Return true to indicate error was handled + // Return false or throw to propagate + return true; + }, +}); +``` + +--- + +*Next: [04-security-model.md](./04-security-model.md) - Security and permissions* diff --git a/docs/apps-architecture/04-security-model.md b/docs/apps-architecture/04-security-model.md new file mode 100644 index 0000000..87e7502 --- /dev/null +++ b/docs/apps-architecture/04-security-model.md @@ -0,0 +1,441 @@ +# Iris Apps: Security Model + +## Overview + +SDR (Server-Defined Rendering) dramatically simplifies security compared to traditional approaches. Because UI is rendered from a trusted component registry—not arbitrary JavaScript—we eliminate an entire class of client-side vulnerabilities. + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ SDR SECURITY ADVANTAGE │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ TRADITIONAL (iframe/JS) SDR APPROACH │ +│ │ +│ ┌─────────────────────┐ ┌─────────────────────┐ │ +│ │ Untrusted JS │ │ Component Tree │ │ +│ │ Can do anything │ │ (JSON data) │ │ +│ │ XSS, data theft │ │ Can only render │ │ +│ │ Needs sandbox │ │ trusted components │ │ +│ └─────────────────────┘ └─────────────────────┘ │ +│ │ │ │ +│ ▼ ▼ │ +│ ┌─────────────────────┐ ┌─────────────────────┐ │ +│ │ iframe sandbox │ │ Direct render │ │ +│ │ CSP headers │ │ No sandbox needed │ │ +│ │ postMessage only │ │ Same context │ │ +│ │ Performance cost │ │ Fast & simple │ │ +│ └─────────────────────┘ └─────────────────────┘ │ +│ │ +│ SDR: The UI can only do what the component registry allows │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +## Security Zones + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ SECURITY ZONES │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ ZONE 1: Iris Core (Trusted) │ │ +│ │ │ │ +│ │ • Full system access │ │ +│ │ • Permission enforcement │ │ +│ │ • App lifecycle management │ │ +│ │ • API credentials storage │ │ +│ │ │ │ +│ │ Access: Only Iris core code │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ ZONE 2: App Server (Permission-Gated) │ │ +│ │ │ │ +│ │ • App's server.ts code runs here │ │ +│ │ • Has declared permissions only │ │ +│ │ • All platform access checked at runtime │ │ +│ │ • Can access state, tools, services │ │ +│ │ │ │ +│ │ Access: Via ctx.iris.* with permission checks │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ ZONE 3: SDR Renderer (Data-Only) │ │ +│ │ │ │ +│ │ • Receives JSON component trees │ │ +│ │ • Renders using trusted @iris/ui components │ │ +│ │ • Cannot execute arbitrary code │ │ +│ │ • Actions sent back to server for execution │ │ +│ │ │ │ +│ │ Access: Read-only rendering of data │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +## Permission System + +### Permission Declaration + +Apps declare required permissions in `app.json`: + +```json +{ + "permissions": [ + "filesystem:read:$PROJECT", + "filesystem:write:$APP/data", + "network:api.example.com", + "ai:chat" + ] +} +``` + +### Permission Categories + +| Category | Examples | Risk Level | +|----------|----------|------------| +| **filesystem** | `filesystem:read:$PROJECT` | Medium-High | +| **network** | `network:localhost`, `network:*.api.com` | Medium | +| **ai** | `ai:chat`, `ai:embed` | Medium (cost) | +| **iris** | `iris:tools`, `iris:navigation` | Low-High | +| **process** | `process:spawn:$APP` | High | + +### Permission Scopes + +| Scope | Resolves To | +|-------|-------------| +| `$PROJECT` | Current project directory | +| `$APP` | App's own directory | +| `$DATA` | App's data directory | +| `$CONFIG` | App's config directory | +| Domain glob | e.g., `*.example.com` | + +### Permission Reference + +#### Filesystem + +``` +filesystem:read # Read any file (DANGEROUS) +filesystem:read:$PROJECT # Read within project only +filesystem:read:$APP # Read within app directory +filesystem:write # Write any file (DANGEROUS) +filesystem:write:$PROJECT # Write within project +filesystem:write:$APP # Write within app directory +filesystem:write:$APP/data # Write to specific subdirectory +``` + +#### Network + +``` +network:* # Any network access +network:localhost # Localhost only +network:example.com # Specific domain +network:*.example.com # Domain with subdomains +``` + +#### AI + +``` +ai:chat # Use chat completions +ai:chat:limited # Rate-limited chat +ai:embed # Use embeddings +``` + +#### Iris Platform + +``` +iris:tools # Call built-in tools +iris:tools:read # Read-only tools only +iris:navigation # Navigate UI +iris:notifications # Show notifications +iris:apps # Cross-app communication +``` + +#### Process + +``` +process:spawn # Run any command (DANGEROUS) +process:spawn:$APP # Run commands in app dir only +process:env # Access env variables +process:env:PUBLIC_* # Only PUBLIC_* vars +``` + +### Permission Enforcement + +```typescript +// Every platform access checks permissions at runtime +async function read(path: string, ctx: AppContext): Promise { + const resolvedPath = resolvePath(path, ctx); + const permission = `filesystem:read:${resolvedPath}`; + + if (!ctx.permissions.check(permission)) { + throw new PermissionDeniedError(permission); + } + + return Bun.file(resolvedPath).text(); +} +``` + +## Trust Levels + +Apps have different trust levels based on their source: + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ TRUST LEVELS │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ LEVEL 3: Development App (Highest Trust) │ +│ ├── Source: App being developed in current project │ +│ ├── Trust: User is the developer │ +│ ├── Permissions: Can request any, granted interactively │ +│ └── All source code visible │ +│ │ +│ LEVEL 2: Verified App │ +│ ├── Source: Iris App Registry, verified publisher │ +│ ├── Trust: Signed by known entity │ +│ ├── Permissions: Declared, approved at install │ +│ └── Automated security scanning │ +│ │ +│ LEVEL 1: Community App │ +│ ├── Source: Iris App Registry, unverified │ +│ ├── Trust: User accepts risk │ +│ ├── Permissions: Restricted by default │ +│ └── Warning shown at install │ +│ │ +│ LEVEL 0: Unknown App (Lowest Trust) │ +│ ├── Source: Unknown URL or path │ +│ ├── Trust: None │ +│ ├── Permissions: Minimal only │ +│ └── Explicit user approval required │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +## SDR Security Benefits + +### No Client-Side Code Execution + +Traditional apps run arbitrary JavaScript. SDR apps can only: + +1. Render components from the trusted registry +2. Send actions back to the server +3. Display data provided by the server + +```typescript +// This UI tree is just data - no code runs on the client +{ + type: 'Button', + props: { + onPress: { $action: 'delete', args: { id: '123' } } + }, + children: ['Delete'] +} + +// The client renders a Button component +// When pressed, it sends { action: 'delete', args: { id: '123' } } to server +// Server decides whether to execute based on permissions +``` + +### Actions Are Validated + +Every action from the UI goes through the server: + +```typescript +// Client sends action +{ action: 'delete', args: { id: '123' } } + +// Server validates and executes +async function handleAction(action: Action, ctx: AppContext) { + // Check if action exists + const tool = ctx.tools.get(action.name); + if (!tool) { + throw new Error(`Unknown action: ${action.name}`); + } + + // Validate arguments + const parsed = tool.parameters.safeParse(action.args); + if (!parsed.success) { + throw new ValidationError(parsed.error); + } + + // Execute with permission checks + return tool.execute(parsed.data, ctx); +} +``` + +### Component Registry is Trusted + +The `@iris/ui` component registry is: +- Maintained by Iris team +- Audited for security +- Cannot execute arbitrary code +- Cannot access system resources directly + +```typescript +// Components can only do what they're designed to do +function Button({ onPress, children }) { + return ( + + ); +} +``` + +## Custom UI Security + +When apps opt into custom UI mode, additional security measures apply: + +### Sandboxing Options + +```typescript +// App manifest +{ + "ui": { + "mode": "custom", + "entry": "ui/index.html", + "sandbox": "strict" // or "relaxed" + } +} +``` + +**Strict (default):** +- Shadow DOM isolation +- CSP headers restricting scripts/styles +- No direct DOM access to parent + +**Relaxed:** +- Same React context as Iris +- Shared state directly accessible +- Trust the app developer + +### Bridge Security + +Custom UI communicates via a bridge: + +```typescript +// All messages validated +class SecureBridge { + handleMessage(message: unknown) { + // Validate structure + const validated = BridgeMessageSchema.parse(message); + + // Check permissions for requested action + if (validated.type === 'iris:action') { + this.checkPermission(validated.action); + } + + // Rate limit + if (!this.rateLimiter.allow()) { + throw new RateLimitError(); + } + + // Process message + this.process(validated); + } +} +``` + +## Audit Logging + +All sensitive operations are logged: + +```typescript +interface AuditEvent { + timestamp: number; + appId: string; + action: string; + target?: string; + allowed: boolean; + result?: 'success' | 'error'; +} + +// Example log +{ + timestamp: 1705234567890, + appId: 'database-explorer', + action: 'filesystem:read', + target: '/project/data.db', + allowed: true, + result: 'success' +} +``` + +### Audit UI + +Users can review what apps have accessed: + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Audit Log: database-explorer │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ Today │ +│ ─────── │ +│ 10:32 ✓ filesystem:read /project/data.db │ +│ 10:32 ✓ ai:chat 3 messages │ +│ 10:31 ✓ filesystem:read /project/schema.sql │ +│ │ +│ Yesterday │ +│ ───────── │ +│ 15:45 ✗ filesystem:read /etc/passwd (DENIED) │ +│ 15:44 ✓ network:fetch api.example.com/users │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +## Rate Limiting + +Prevent resource abuse: + +```typescript +const rateLimits = { + 'ai:chat': { requests: 100, window: '1h' }, + 'filesystem:read': { requests: 1000, window: '1m' }, + 'network:fetch': { requests: 100, window: '1m' }, +}; +``` + +## Development Mode Security + +During development, apps get relaxed security: + +```typescript +const developmentDefaults = { + // Implicit permissions (no declaration needed) + implicit: [ + 'filesystem:read:$PROJECT', + 'filesystem:write:$PROJECT', + 'network:localhost', + 'iris:navigation', + 'iris:notifications', + ], + + // Prompted interactively + prompted: [ + 'network:*', + 'ai:*', + 'process:spawn', + ], + + // Always denied + denied: [ + 'filesystem:read:/', + 'process:env:*_SECRET', + 'process:env:*_KEY', + ], +}; +``` + +--- + +*Next: [05-resilience.md](./05-resilience.md) - Error handling and recovery* diff --git a/docs/apps-architecture/05-resilience.md b/docs/apps-architecture/05-resilience.md new file mode 100644 index 0000000..999f93c --- /dev/null +++ b/docs/apps-architecture/05-resilience.md @@ -0,0 +1,362 @@ +# Iris Apps: Resilience & Error Handling + +## Philosophy + +> **Broken code is normal. Broken experiences are not.** + +During development, apps will constantly be in broken states. SDR makes error handling simpler because all logic runs on the server—we have full control over error recovery. + +## Error Categories + +| Category | Example | Handling | +|----------|---------|----------| +| **Server Load** | Syntax error in server.ts | Show error overlay, wait for fix | +| **Server Runtime** | Tool throws exception | Catch, show error in UI, don't crash | +| **UI Generation** | ui() returns invalid tree | Render valid parts, highlight broken | +| **Action Execution** | User action fails | Return error result, show message | + +## SDR Error Flow + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ SDR ERROR HANDLING │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ 1. Server Error (server.ts won't load) │ +│ ┌────────────────────────────────────────────────────────┐ │ +│ │ Show full error overlay: │ │ +│ │ • Syntax error message │ │ +│ │ • File + line number │ │ +│ │ • "Open in Editor" button │ │ +│ │ • Auto-reload when file changes │ │ +│ └────────────────────────────────────────────────────────┘ │ +│ │ +│ 2. UI Generation Error (ui() throws) │ +│ ┌────────────────────────────────────────────────────────┐ │ +│ │ Show last valid UI + error banner: │ │ +│ │ • Error message at top │ │ +│ │ • Stack trace in details │ │ +│ │ • App still partially usable │ │ +│ └────────────────────────────────────────────────────────┘ │ +│ │ +│ 3. Invalid UI Tree (ui() returns bad data) │ +│ ┌────────────────────────────────────────────────────────┐ │ +│ │ Validate tree, render valid parts: │ │ +│ │ • Unknown components → show placeholder │ │ +│ │ • Invalid props → use defaults │ │ +│ │ • Show warnings in dev tools │ │ +│ └────────────────────────────────────────────────────────┘ │ +│ │ +│ 4. Action Error (tool execution fails) │ +│ ┌────────────────────────────────────────────────────────┐ │ +│ │ Return error to UI, let app handle: │ │ +│ │ • { success: false, error: "message" } │ │ +│ │ • App shows error via Alert component │ │ +│ │ • No crash, no white screen │ │ +│ └────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +## Server Load Errors + +When server.ts fails to load (syntax error, missing import, etc.): + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ ⚠️ App Error: database-explorer │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ SyntaxError: Unexpected token '}' at line 42 │ +│ │ +│ ┌────────────────────────────────────────────────────────┐ │ +│ │ 40 │ const result = await db.query(sql); │ │ +│ │ 41 │ return { success: true, data: result │ │ +│ │ 42 │ } // ← Missing comma before this line │ │ +│ │ 43 │ }); │ │ +│ └────────────────────────────────────────────────────────┘ │ +│ │ +│ server.ts:42:3 │ +│ │ +│ [Open in Editor] [Retry] │ +│ │ +│ Watching for changes... │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +**Behavior:** +- App shows error overlay instead of UI +- File watcher active—auto-retries on save +- User can click to open file at error location + +## UI Generation Errors + +When ui() throws during rendering: + +```typescript +// This will throw +ui: (ctx) => { + const user = ctx.state.user.get(); + return Stack({}, [ + Text({}, user.name), // Error if user is null! + ]); +} +``` + +**Error Display:** + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ ⚠️ UI Error [Dismiss] │ +│ TypeError: Cannot read property 'name' of null │ +│ at ui (server.ts:45) │ +└─────────────────────────────────────────────────────────────────┘ +┌─────────────────────────────────────────────────────────────────┐ +│ │ +│ [Last valid UI rendered here] │ +│ │ +│ User can still interact with existing UI │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +**Fix in Code:** + +```typescript +ui: (ctx) => { + const user = ctx.state.user.get(); + + // Guard against null + if (!user) { + return Stack({ padding: 24 }, [ + Text({}, 'Please select a user'), + ]); + } + + return Stack({}, [ + Text({}, user.name), + ]); +} +``` + +## Invalid UI Tree + +When ui() returns invalid data: + +```typescript +// Returns unknown component +ui: (ctx) => { + return Stack({}, [ + MyCustomThing({ data: 'test' }), // Not in registry! + ]); +} +``` + +**Rendered:** + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ [Valid Stack renders] │ +│ │ +│ ┌────────────────────────────────────────────────────────┐ │ +│ │ ⚠️ Unknown component: MyCustomThing │ │ +│ │ This component is not in the @iris/ui registry │ │ +│ └────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +**Console Warning:** +``` +[Iris] Unknown component "MyCustomThing" at path root.children[0] +Available components: Stack, Row, Text, Button, ... +``` + +## Action Execution Errors + +Tools should return errors, not throw: + +```typescript +// GOOD: Return error state +defineTool({ + name: 'query', + execute: async (args, ctx) => { + try { + const result = await db.query(args.sql); + return { success: true, data: result }; + } catch (error) { + return { success: false, error: error.message }; + } + }, +}); + +// Handle in UI +ui: (ctx) => { + const { error, results } = ctx.state; + + return Stack({}, [ + // Show error if present + error && Alert({ variant: 'error' }, error), + + // Results + results && DataTable({ data: results }), + ]); +} +``` + +## Hot Reload Resilience + +When code changes, state is preserved: + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ HOT RELOAD FLOW │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ 1. File change detected │ +│ │ +│ 2. Snapshot current state │ +│ { count: 5, items: [...], selectedId: 'abc' } │ +│ │ +│ 3. Unload old module │ +│ │ +│ 4. Load new module │ +│ ├── Success → Continue │ +│ └── Error → Show overlay, keep old module running │ +│ │ +│ 5. Restore state │ +│ ├── State exists in new module → Restore │ +│ └── State removed → Drop (with warning) │ +│ │ +│ 6. Re-run ui() with restored state │ +│ │ +│ 7. Push new UI to client │ +│ (Client sees instant update, state preserved) │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### State Migration + +If state shape changes: + +```typescript +export default defineApp({ + state: { + // Renamed from 'count' to 'counter' + counter: state(0), + }, + + onReload: async (ctx, previousState) => { + // Migrate state + if ('count' in previousState) { + ctx.state.counter.set(previousState.count); + } + }, +}); +``` + +## Global Error Handler + +Catch errors at the app level: + +```typescript +export default defineApp({ + onError: async (ctx, error) => { + // Log error + ctx.log.error('App error:', error); + + // Update UI state + ctx.state.lastError.set(error.message); + + // Return true = error handled, don't propagate + // Return false = propagate to Iris error overlay + return true; + }, +}); +``` + +## Service Resilience + +Services restart automatically on failure: + +```typescript +services: { + database: { + start: async (ctx) => { + return new Database(ctx.getConfig('dbPath')); + }, + stop: async (db) => { + await db.close(); + }, + + // Restart on crash + restartOnCrash: true, + maxRestarts: 5, + restartDelay: 1000, // Exponential backoff base + + // Health check + healthCheck: async (db) => { + await db.query('SELECT 1'); + return true; + }, + healthInterval: 30000, + }, +} +``` + +## Error Display Components + +Use built-in components for error states: + +```typescript +import { Alert, Spinner, Stack, Text, Button } from '@iris/ui'; + +ui: (ctx) => { + const { isLoading, error, data } = ctx.state; + + // Loading state + if (isLoading) { + return Stack({ align: 'center', justify: 'center', minHeight: 200 }, [ + Spinner({ size: 'lg' }), + Text({ color: 'muted' }, 'Loading...'), + ]); + } + + // Error state + if (error) { + return Stack({ padding: 16, gap: 12 }, [ + Alert({ variant: 'error', title: 'Error' }, error), + Button({ + onPress: () => ctx.runTool('retry'), + variant: 'outline', + }, 'Retry'), + ]); + } + + // Success state + return DataTable({ data }); +} +``` + +## Console Logging + +Apps can log to the Iris console: + +```typescript +// In tools or lifecycle +ctx.log.debug('Debug info'); +ctx.log.info('Something happened'); +ctx.log.warn('Warning'); +ctx.log.error('Error occurred', error); + +// Logs appear in: +// 1. Iris console panel +// 2. Server terminal +// 3. Browser dev tools (in dev mode) +``` + +--- + +*Next: [06-protocol.md](./06-protocol.md) - Communication protocols* diff --git a/docs/apps-architecture/06-protocol.md b/docs/apps-architecture/06-protocol.md new file mode 100644 index 0000000..ff6f5f5 --- /dev/null +++ b/docs/apps-architecture/06-protocol.md @@ -0,0 +1,636 @@ +# Iris Apps: Communication Protocols + +## Overview + +With Server-Defined Rendering (SDR), communication is simpler than traditional approaches. The server sends UI trees; the client renders them. Actions flow back to the server. + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ COMMUNICATION CHANNELS │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ Iris │◄───────►│ App │◄───────►│ SDR │ │ +│ │ Core │ WS │ Server │ WS │ Renderer │ │ +│ └──────────┘ └──────────┘ └──────────┘ │ +│ │ │ │ │ +│ │ │ │ │ +│ ▼ ▼ ▼ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ Other │ │ External │ │ User │ │ +│ │ Apps │ │ Services │ │ Actions │ │ +│ └──────────┘ └──────────┘ └──────────┘ │ +│ │ +│ Primary Flow: │ +│ 1. App server generates ui() → Component tree │ +│ 2. Tree sent via WebSocket → Client │ +│ 3. SDR Renderer maps tree → Native components │ +│ 4. User actions → Server → State change → New tree │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +## SDR Protocol (Server ↔ Client) + +### Message Format + +All messages use a simple JSON envelope: + +```typescript +interface SDRMessage { + // Message identity + id: string; + type: SDRMessageType; + + // Payload + payload: unknown; + + // Metadata + timestamp: number; + correlationId?: string; // Links request/response +} + +type SDRMessageType = + // UI synchronization + | 'ui:sync' // Full UI tree (on connect, after reload) + | 'ui:patch' // Partial UI update (optimized) + + // State + | 'state:sync' // Full state sync + | 'state:update' // Single state update + + // Actions (from client) + | 'action:call' // User triggered action + | 'action:result' // Action result + + // Lifecycle + | 'app:ready' // App initialized + | 'app:reload' // Hot reload triggered + | 'app:error' // App error occurred + + // Input events + | 'input:change' // Form input changed + | 'input:submit' // Form submitted +; +``` + +### UI Sync Flow + +The primary communication pattern: + +``` +┌──────────┐ ┌──────────┐ +│ App │ │ SDR │ +│ Server │ │ Renderer │ +└────┬─────┘ └────┬─────┘ + │ │ + │ UI:SYNC (on connect) │ + │ { │ + │ type: "ui:sync", │ + │ payload: { │ + │ tree: { │ + │ $: "component", │ + │ type: "Stack", │ + │ props: { padding: 16 }, │ + │ children: [ │ + │ { type: "Text", ... }, │ + │ { type: "Button", ... } │ + │ ] │ + │ }, │ + │ state: { │ + │ count: 0, │ + │ items: [] │ + │ } │ + │ } │ + │ } │ + │────────────────────────────────────────►│ + │ │ + │ │ Render tree + │ │ using registry + │ │ +``` + +### Action Flow + +When user interacts with the UI: + +``` +┌──────────┐ ┌──────────┐ +│ App │ │ SDR │ +│ Server │ │ Renderer │ +└────┬─────┘ └────┬─────┘ + │ │ + │ User clicks │ + │ Button │ + │ │ + │ ACTION:CALL │ + │ { │ + │ type: "action:call", │ + │ id: "act-123", │ + │ payload: { │ + │ action: "increment", │ + │ args: { amount: 1 } │ + │ } │ + │ } │ + │◄────────────────────────────────────────│ + │ │ + │ Execute tool │ + │ Update state │ + │ Re-run ui() │ + │ │ + │ UI:SYNC (new tree) │ + │ { │ + │ payload: { │ + │ tree: { ... }, │ + │ state: { count: 1 } │ + │ } │ + │ } │ + │────────────────────────────────────────►│ + │ │ + │ │ Diff & update + │ │ +``` + +### State Updates (Optimistic) + +For responsive UIs, state can update optimistically: + +``` +┌──────────┐ ┌──────────┐ +│ App │ │ SDR │ +│ Server │ │ Renderer │ +└────┬─────┘ └────┬─────┘ + │ │ + │ User types │ + │ in Input │ + │ │ + │ INPUT:CHANGE │ + │ { │ + │ type: "input:change", │ + │ payload: { │ + │ path: "query", │ + │ value: "SELECT *" │ + │ } │ + │ } │ + │◄────────────────────────────────────────│ + │ │ + │ STATE:UPDATE (confirmation) │ + │ { │ + │ type: "state:update", │ + │ payload: { │ + │ key: "query", │ + │ value: "SELECT *" │ + │ } │ + │ } │ + │────────────────────────────────────────►│ + │ │ + │ (UI already shows value - no flicker) │ + │ │ +``` + +### Component Tree Structure + +The UI tree is a JSON representation of components: + +```typescript +interface ComponentNode { + // Marker for component nodes + $: 'component'; + + // Component type (from registry) + type: string; + + // Props passed to component + props: Record; + + // Children (text, nodes, or mixed) + children?: UIChild[]; + + // Unique key for list diffing + key?: string; +} + +type UIChild = string | number | boolean | null | ComponentNode; + +type PropValue = + | string + | number + | boolean + | null + | PropValue[] + | { [key: string]: PropValue } + | ActionDescriptor; + +interface ActionDescriptor { + $action: string; // Tool/action name + args?: unknown; // Arguments + optimistic?: unknown; // Optimistic state update +} +``` + +Example tree: + +```json +{ + "$": "component", + "type": "Stack", + "props": { "padding": 16, "gap": 12 }, + "children": [ + { + "$": "component", + "type": "Text", + "props": { "size": "2xl", "weight": "bold" }, + "children": ["Counter: 5"] + }, + { + "$": "component", + "type": "Button", + "props": { + "variant": "primary", + "onPress": { "$action": "increment", "args": { "amount": 1 } } + }, + "children": ["+1"] + } + ] +} +``` + +## Internal RPC Protocol (Iris ↔ App Server) + +For Iris platform access (AI, filesystem, tools): + +```typescript +interface RPCMessage { + id: string; + type: 'request' | 'response' | 'event'; + + // For requests + method?: string; + params?: unknown; + + // For responses + result?: unknown; + error?: RPCError; + + // For events + event?: string; + data?: unknown; + + correlationId?: string; + timestamp: number; +} +``` + +### Method Namespaces + +``` +iris:* Iris platform access + iris:ai:chat + iris:ai:embed + iris:fs:read + iris:fs:write + iris:fs:list + iris:tools:call + iris:navigate + iris:notify + +lifecycle:* App lifecycle + lifecycle:activate + lifecycle:deactivate + lifecycle:reload + +service:* Service management + service:start + service:stop + service:status + +config:* Configuration + config:get + config:set +``` + +### Example: AI Chat Call + +``` +┌──────────┐ ┌──────────┐ +│ App │ │ Iris │ +│ Server │ │ Core │ +└────┬─────┘ └────┬─────┘ + │ │ + │ REQUEST │ + │ { │ + │ id: "rpc-456", │ + │ type: "request", │ + │ method: "iris:ai:chat", │ + │ params: { │ + │ messages: [ │ + │ { role: "user", content: "..." } │ + │ ], │ + │ model: "claude-sonnet" │ + │ } │ + │ } │ + │────────────────────────────────────────►│ + │ │ + │ Check permission│ + │ Execute AI call │ + │ │ + │ RESPONSE │ + │ { │ + │ type: "response", │ + │ correlationId: "rpc-456", │ + │ result: { │ + │ content: "Here's the answer...", │ + │ usage: { tokens: 150 } │ + │ } │ + │ } │ + │◄────────────────────────────────────────│ + │ │ +``` + +## Custom UI Bridge Protocol (iframe mode) + +For apps using custom UI mode (the escape hatch), communication uses postMessage: + +```typescript +interface BridgeMessage { + protocol: 'iris-bridge'; + version: 1; + type: BridgeMessageType; + id?: string; + payload: unknown; +} + +type BridgeMessageType = + | 'init' // Shell → iframe: Initialize + | 'ready' // iframe → Shell: Ready + | 'state:sync' // Shell → iframe: State sync + | 'state:update' // Shell → iframe: State change + | 'state:set' // iframe → Shell: Set state + | 'action:call' // iframe → Shell: Call tool + | 'action:result' // Shell → iframe: Tool result + | 'theme:change' // Shell → iframe: Theme changed +; +``` + +### Bridge Initialization + +``` +┌──────────┐ ┌──────────┐ +│ Shell │ │ iframe │ +│ (parent) │ │ (app) │ +└────┬─────┘ └────┬─────┘ + │ │ + │ INIT │ + │ { │ + │ type: "init", │ + │ payload: { │ + │ appId: "my-app", │ + │ projectId: "proj-123", │ + │ state: { count: 0 }, │ + │ theme: "dark", │ + │ config: { ... } │ + │ } │ + │ } │ + │────────────────────────────────────────►│ + │ │ + │ │ Initialize + │ │ React app + │ │ + │ READY │ + │◄────────────────────────────────────────│ + │ │ +``` + +### Security for Bridge + +```typescript +class SecureBridge { + private allowedOrigin: string; + + constructor(iframe: HTMLIFrameElement) { + this.allowedOrigin = new URL(iframe.src).origin; + window.addEventListener('message', this.handleMessage); + } + + private handleMessage = (event: MessageEvent) => { + // Verify origin + if (event.origin !== this.allowedOrigin) { + console.warn('Rejected message from:', event.origin); + return; + } + + // Validate message structure + if (!this.isValidBridgeMessage(event.data)) { + return; + } + + this.process(event.data); + }; + + send(message: BridgeMessage): void { + this.iframe.contentWindow?.postMessage( + { protocol: 'iris-bridge', version: 1, ...message }, + this.allowedOrigin + ); + } +} +``` + +## Hot Reload Protocol + +### Server Module Reload (SDR) + +``` +┌──────────┐ ┌──────────┐ ┌──────────┐ +│ Bun FS │ │ App │ │ SDR │ +│ Watcher │ │ Server │ │ Renderer │ +└────┬─────┘ └────┬─────┘ └────┬─────┘ + │ │ │ + │ File change │ │ + │───────────────►│ │ + │ │ │ + │ │ 1. Snapshot │ + │ │ state │ + │ │ │ + │ │ 2. Unload │ + │ │ module │ + │ │ │ + │ │ 3. Load new │ + │ │ module │ + │ │ │ + │ │ 4. Restore │ + │ │ state │ + │ │ │ + │ │ 5. Run ui() │ + │ │ │ + │ │ UI:SYNC │ + │ │───────────────►│ + │ │ │ + │ │ │ Re-render + │ │ │ (instant!) + │ │ │ +``` + +### State Migration + +If state shape changes during hot reload: + +```typescript +export default defineApp({ + state: { + // Renamed from 'count' to 'counter' + counter: state(0), + }, + + onReload: async (ctx, previousState) => { + // Migrate old state + if ('count' in previousState) { + ctx.state.counter.set(previousState.count); + } + }, +}); +``` + +## WebSocket Connection Management + +### Connection States + +```typescript +type ConnectionState = + | 'connecting' + | 'connected' + | 'disconnected' + | 'reconnecting'; + +class AppConnection { + private state: ConnectionState = 'disconnected'; + private reconnectAttempts = 0; + + connect(): void { + this.state = 'connecting'; + this.ws = new WebSocket(this.url); + + this.ws.onopen = () => { + this.state = 'connected'; + this.reconnectAttempts = 0; + this.requestFullSync(); + }; + + this.ws.onclose = (event) => { + if (event.code !== 1000) { + this.scheduleReconnect(); + } + }; + } + + private scheduleReconnect(): void { + this.state = 'reconnecting'; + const delay = Math.min( + 1000 * Math.pow(2, this.reconnectAttempts), + 30000 + ); + setTimeout(() => this.connect(), delay); + this.reconnectAttempts++; + } +} +``` + +### Heartbeat + +``` +┌──────────┐ ┌──────────┐ +│ Client │ │ Server │ +└────┬─────┘ └────┬─────┘ + │ │ + │ PING (every 30s) │ + │ { type: "ping" } │ + │────────────────────────────────────────►│ + │ │ + │ PONG │ + │ { type: "pong" } │ + │◄────────────────────────────────────────│ + │ │ +``` + +## Error Protocol + +### Error Message Format + +```typescript +interface ErrorMessage { + type: 'app:error'; + payload: { + category: 'server_load' | 'ui_generation' | 'action' | 'runtime'; + message: string; + stack?: string; + file?: string; + line?: number; + column?: number; + recoverable: boolean; + timestamp: number; + }; +} +``` + +### Error Flow + +``` +┌──────────┐ ┌──────────┐ +│ App │ │ SDR │ +│ Server │ │ Renderer │ +└────┬─────┘ └────┬─────┘ + │ │ + │ ui() throws error │ + │ │ + │ APP:ERROR │ + │ { │ + │ type: "app:error", │ + │ payload: { │ + │ category: "ui_generation", │ + │ message: "Cannot read 'name'...", │ + │ file: "server.ts", │ + │ line: 45, │ + │ recoverable: true │ + │ } │ + │ } │ + │────────────────────────────────────────►│ + │ │ + │ │ Show error + │ │ overlay with + │ │ last valid UI + │ │ +``` + +## Rate Limiting + +To prevent abuse, certain operations are rate-limited: + +```typescript +const rateLimits: Record = { + 'iris:ai:chat': { requests: 100, window: '1h' }, + 'iris:fs:read': { requests: 1000, window: '1m' }, + 'iris:fs:write': { requests: 100, window: '1m' }, + 'ui:sync': { requests: 60, window: '1s' }, // Max 60 FPS +}; +``` + +Rate limit errors return: + +```typescript +{ + type: 'response', + error: { + code: -32008, + message: 'Rate limit exceeded', + data: { + limit: 100, + window: '1h', + resetAt: 1705234567890 + } + } +} +``` + +--- + +*Next: [07-implementation-roadmap.md](./07-implementation-roadmap.md) - Implementation phases and milestones* diff --git a/docs/apps-architecture/07-implementation-roadmap.md b/docs/apps-architecture/07-implementation-roadmap.md new file mode 100644 index 0000000..f5ee8a6 --- /dev/null +++ b/docs/apps-architecture/07-implementation-roadmap.md @@ -0,0 +1,852 @@ +# Iris Apps: Implementation Roadmap + +## Overview + +This roadmap outlines a phased approach to implementing the Iris Apps system with Server-Defined Rendering (SDR). SDR simplifies the architecture significantly—most phases are smaller than they would be with an iframe-based approach. + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ IMPLEMENTATION PHASES │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Phase 1: Foundation │ +│ ├── App manifest & loader │ +│ ├── Basic app lifecycle │ +│ └── SDR component registry │ +│ │ │ +│ ▼ │ +│ Phase 2: SDR Core │ +│ ├── ui() function execution │ +│ ├── Component tree rendering │ +│ └── Action handlers │ +│ │ │ +│ ▼ │ +│ Phase 3: State & Tools │ +│ ├── Reactive state management │ +│ ├── Tool registration & execution │ +│ └── Hot reload with state preservation │ +│ │ │ +│ ▼ │ +│ Phase 4: SDK & DX │ +│ ├── @iris/app-sdk package │ +│ ├── @iris/ui component library │ +│ └── Development tooling │ +│ │ │ +│ ▼ │ +│ Phase 5: Platform Integration │ +│ ├── Iris AI access │ +│ ├── Filesystem access │ +│ └── Permission system (VS Code-like) │ +│ │ │ +│ ▼ │ +│ Phase 6: Production & Custom UI │ +│ ├── Custom UI mode (iframe escape hatch) │ +│ ├── Mobile support (React Native) │ +│ └── Standalone runtime & distribution │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Phase 1: Foundation + +### Objective +Get a basic app structure and component registry in place. + +### Deliverables + +#### 1.1 App Manifest Schema +```typescript +// src/apps/manifest.ts +import { z } from 'zod'; + +export const AppManifestSchema = z.object({ + name: z.string().regex(/^[a-z0-9-]+$/), + displayName: z.string(), + version: z.string(), + description: z.string().optional(), + icon: z.string().optional(), + + server: z.string().default('server.ts'), + + // Optional: Custom UI mode (escape hatch) + ui: z.object({ + mode: z.enum(['sdr', 'custom']).default('sdr'), + entry: z.string().optional(), // For custom mode + }).optional(), + + tools: z.array(z.object({ + name: z.string(), + description: z.string(), + })).default([]), + + permissions: z.array(z.string()).default([]), +}); + +export type AppManifest = z.infer; +``` + +#### 1.2 Component Registry +```typescript +// src/apps/sdr/registry.ts +import { Stack, Row, Text, Button, Input, ... } from '@iris/ui'; + +export const componentRegistry: Record = { + // Layout + Stack, + Row, + Box, + ScrollView, + Divider, + + // Typography + Text, + Heading, + Code, + Link, + + // Form + Button, + Input, + TextArea, + Select, + Checkbox, + Switch, + + // Data display + DataTable, + List, + Card, + Badge, + + // Feedback + Alert, + Spinner, + Progress, + + // More... +}; + +export function getComponent(type: string): ComponentType | undefined { + return componentRegistry[type]; +} +``` + +#### 1.3 App Manager +```typescript +// src/apps/manager.ts +export class AppManager { + private apps = new Map(); + + async discover(projectPath: string): Promise { + // Find app.json files in project + } + + async load(appPath: string): Promise { + // Load manifest, validate, create runtime + } + + async activate(appId: string): Promise { + // Start app, run onActivate + } + + get(appId: string): ManagedApp | undefined { + return this.apps.get(appId); + } +} +``` + +#### 1.4 App Tab Type +```typescript +// webui/src/lib/tabs.ts +interface AppTabData { + type: 'app'; + projectId: string; + appId: string; + appName: string; +} +``` + +### Milestone Criteria +- [ ] Can create `app.json` in a project +- [ ] Iris discovers app and shows in sidebar +- [ ] Component registry has core components +- [ ] Basic app lifecycle (load, activate, deactivate) + +--- + +## Phase 2: SDR Core + +### Objective +Implement the SDR rendering engine that turns ui() output into React components. + +### Deliverables + +#### 2.1 App Runtime (Server Side) +```typescript +// src/apps/runtime.ts +export class AppRuntime { + private module: AppModule; + private context: AppContext; + + async initialize(modulePath: string): Promise { + this.module = await import(modulePath); + this.context = this.createContext(); + } + + generateUI(): ComponentTree { + // Call the app's ui() function + return this.module.default.ui(this.context); + } + + handleAction(action: string, args: unknown): Promise { + // Execute tool or state update + } +} +``` + +#### 2.2 SDR Renderer (Client Side) +```typescript +// webui/src/components/apps/SDRRenderer.tsx +import { componentRegistry } from '@iris/ui'; + +interface SDRRendererProps { + tree: ComponentTree; + onAction: (action: string, args: unknown) => void; +} + +export function SDRRenderer({ tree, onAction }: SDRRendererProps) { + return renderNode(tree, onAction); +} + +function renderNode(node: UINode, onAction: ActionHandler): ReactNode { + // Primitive values + if (typeof node === 'string' || typeof node === 'number') { + return node; + } + + if (node === null || node === undefined || node === false) { + return null; + } + + // Component node + if (node.$ === 'component') { + const Component = componentRegistry[node.type]; + + if (!Component) { + return ; + } + + // Transform action props + const props = transformProps(node.props, onAction); + + // Render children + const children = node.children?.map((child, i) => + renderNode(child, onAction) + ); + + return {children}; + } + + return null; +} +``` + +#### 2.3 Action Handler +```typescript +// webui/src/components/apps/AppHost.tsx +export function AppHost({ app }: { app: AppInfo }) { + const [tree, setTree] = useState(null); + const ws = useWebSocket(app.wsUrl); + + useEffect(() => { + ws.on('ui:sync', (payload) => { + setTree(payload.tree); + }); + }, [ws]); + + const handleAction = useCallback(async (action: string, args: unknown) => { + ws.send({ + type: 'action:call', + payload: { action, args } + }); + }, [ws]); + + if (!tree) { + return ; + } + + return ; +} +``` + +### Milestone Criteria +- [ ] ui() function executes on server +- [ ] Component tree renders in browser +- [ ] Button clicks trigger actions +- [ ] State changes cause UI re-render + +--- + +## Phase 3: State & Tools + +### Objective +Implement reactive state and tool system with hot reload. + +### Deliverables + +#### 3.1 Reactive State +```typescript +// src/apps/state.ts +export function state(initial: T, options?: StateOptions): StateHandle { + let value = initial; + const listeners = new Set>(); + + return { + get: () => value, + set: (newValue: T) => { + value = newValue; + listeners.forEach(l => l(value)); + }, + update: (updater: (prev: T) => T) => { + value = updater(value); + listeners.forEach(l => l(value)); + }, + subscribe: (listener: Listener) => { + listeners.add(listener); + return () => listeners.delete(listener); + }, + }; +} +``` + +#### 3.2 Tool Execution +```typescript +// src/apps/tools.ts +export class ToolExecutor { + constructor(private runtime: AppRuntime) {} + + async execute(name: string, args: unknown): Promise { + const tool = this.runtime.tools.get(name); + if (!tool) { + return { success: false, error: `Unknown tool: ${name}` }; + } + + // Validate arguments + const parsed = tool.parameters.safeParse(args); + if (!parsed.success) { + return { success: false, error: formatZodError(parsed.error) }; + } + + // Execute + try { + const result = await tool.execute(parsed.data, this.runtime.context); + return { success: true, data: result }; + } catch (error) { + return { success: false, error: error.message }; + } + } +} +``` + +#### 3.3 Hot Reload +```typescript +// src/apps/hot-reload.ts +export class HotReloadManager { + async handleFileChange(appPath: string): Promise { + const runtime = this.appManager.getRuntime(appPath); + if (!runtime) return; + + // 1. Snapshot current state + const stateSnapshot = runtime.snapshotState(); + + // 2. Unload module (Bun cache invalidation) + delete require.cache[require.resolve(appPath)]; + + // 3. Reload module + try { + await runtime.reload(); + + // 4. Restore state + runtime.restoreState(stateSnapshot); + + // 5. Re-run ui() and push to clients + const tree = runtime.generateUI(); + runtime.broadcast({ type: 'ui:sync', payload: { tree } }); + + } catch (error) { + // Send error to client, keep old module running + runtime.broadcast({ + type: 'app:error', + payload: { + category: 'server_load', + message: error.message, + recoverable: true, + } + }); + } + } +} +``` + +### Milestone Criteria +- [ ] State persists across interactions +- [ ] Tools can be defined and called +- [ ] File changes trigger instant UI updates +- [ ] State preserved across hot reloads +- [ ] Errors show overlay, don't crash + +--- + +## Phase 4: SDK & Developer Experience + +### Objective +Create polished SDK packages for app development. + +### Deliverables + +#### 4.1 @iris/app-sdk Package +``` +packages/app-sdk/ +├── package.json +├── src/ +│ ├── index.ts # Main exports +│ ├── app.ts # defineApp +│ ├── tool.ts # defineTool +│ ├── state.ts # state, computed, query +│ └── context.ts # Type definitions +└── tsconfig.json +``` + +```typescript +// packages/app-sdk/src/index.ts +export { defineApp } from './app'; +export { defineTool } from './tool'; +export { state, computed, query } from './state'; +export type { + AppContext, + ToolContext, + StateHandle, + AppDefinition, +} from './context'; +``` + +#### 4.2 @iris/ui Package +``` +packages/ui/ +├── package.json +├── src/ +│ ├── index.ts # All component exports +│ ├── registry.ts # Component registry +│ ├── components/ +│ │ ├── layout/ # Stack, Row, Box, etc. +│ │ ├── typography/ # Text, Heading, Code +│ │ ├── form/ # Button, Input, Select +│ │ ├── data/ # DataTable, List, Card +│ │ └── feedback/ # Alert, Spinner, Progress +│ └── native/ # React Native variants +└── tsconfig.json +``` + +```typescript +// packages/ui/src/index.ts +// Layout +export { Stack, Row, Box, ScrollView, Divider } from './components/layout'; + +// Typography +export { Text, Heading, Code, Link } from './components/typography'; + +// Form +export { Button, Input, TextArea, Select, Checkbox, Switch } from './components/form'; + +// Data +export { DataTable, List, Card, Badge, Avatar } from './components/data'; + +// Feedback +export { Alert, Spinner, Progress, Toast } from './components/feedback'; + +// Specialized +export { CodeEditor, Terminal, FileTree, Markdown } from './components/specialized'; +``` + +#### 4.3 CLI Commands +```bash +# Create new app +iris app create my-app + +# Start development with hot reload +iris app dev + +# Validate app +iris app validate +``` + +### Milestone Criteria +- [ ] SDK packages published (npm or local) +- [ ] Apps can import from @iris/app-sdk and @iris/ui +- [ ] Full TypeScript support with autocomplete +- [ ] CLI commands work + +--- + +## Phase 5: Platform Integration + +### Objective +Enable apps to access Iris platform features with appropriate permissions. + +### Deliverables + +#### 5.1 Iris Context +```typescript +// Extension to AppContext +interface IrisContext { + ai: { + chat(messages: Message[], options?: ChatOptions): Promise; + embed(text: string | string[]): Promise; + }; + + fs: { + read(path: string): Promise; + write(path: string, content: string): Promise; + list(path: string): Promise; + }; + + tools: { + call(name: string, args: unknown): Promise; + }; + + navigate(path: string): void; + notify(message: string, options?: NotifyOptions): void; +} +``` + +#### 5.2 Permission System (VS Code-like) + +Like VS Code extensions, permissions are declared and trusted at install/development time: + +```typescript +// src/apps/permissions.ts +export class PermissionManager { + private granted: Set; + + constructor(manifest: AppManifest, trustLevel: TrustLevel) { + this.granted = new Set(manifest.permissions); + + // Development apps: automatically trust project permissions + if (trustLevel === 'development') { + this.granted.add('filesystem:read:$PROJECT'); + this.granted.add('filesystem:write:$PROJECT'); + this.granted.add('network:localhost'); + } + } + + check(permission: string): boolean { + // Exact match + if (this.granted.has(permission)) return true; + + // Wildcard/scope matching + for (const granted of this.granted) { + if (this.matches(granted, permission)) return true; + } + + return false; + } +} + +// Trust levels (similar to VS Code workspace trust) +type TrustLevel = 'development' | 'installed' | 'untrusted'; +``` + +#### 5.3 AI Integration +```typescript +// src/apps/platform/ai.ts +export class AppAIProvider { + constructor( + private appId: string, + private permissions: PermissionManager + ) {} + + async chat(messages: Message[], options?: ChatOptions): Promise { + if (!this.permissions.check('ai:chat')) { + throw new Error('Permission denied: ai:chat'); + } + + return this.aiService.chat(messages, { + ...options, + metadata: { appId: this.appId }, + }); + } +} +``` + +#### 5.4 Filesystem Integration +```typescript +// src/apps/platform/fs.ts +export class AppFSProvider { + constructor( + private permissions: PermissionManager, + private projectPath: string + ) {} + + async read(path: string): Promise { + const resolved = this.resolvePath(path); + + if (!this.permissions.check(`filesystem:read:${this.getScope(resolved)}`)) { + throw new Error(`Permission denied: filesystem:read:${resolved}`); + } + + return Bun.file(resolved).text(); + } + + private resolvePath(path: string): string { + // Handle $PROJECT, $APP, etc. + // Prevent path traversal + } +} +``` + +### Milestone Criteria +- [ ] Apps can call AI models (with permission) +- [ ] Apps can read/write files (within permitted scope) +- [ ] Permission violations throw clear errors +- [ ] Development apps have sensible defaults + +--- + +## Phase 6: Production & Custom UI + +### Objective +Add escape hatches, mobile support, and production features. + +### Deliverables + +#### 6.1 Custom UI Mode (iframe) +For apps that need full React control (3D, canvas, complex interactions): + +```json +{ + "name": "3d-visualizer", + "ui": { + "mode": "custom", + "entry": "ui/index.html" + } +} +``` + +```tsx +// webui/src/components/apps/CustomAppHost.tsx +export function CustomAppHost({ app }: { app: AppInfo }) { + const bridge = useBridge(app); + + return ( +