diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..021c20a
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,148 @@
+# Changelog
+
+All notable changes to enowX Coder are documented here.
+
+---
+
+## [0.2.5] — 2026-04-23
+
+### Excalidraw Canvas — Collaborative Whiteboard
+- **New feature**: Full Excalidraw whiteboard integrated as a Canvas tab — switch between Chat and Canvas via segmented control in header
+- Drawings persist per project in SQLite — auto-save every 1 second, auto-load on project switch
+- Dark/light theme syncs with app theme toggle
+- Full Excalidraw toolbar: shapes, arrows, text, freehand, colors, layers, export to disk
+
+### AI Canvas — Generate Diagrams with AI
+- **New feature**: AI prompt bar floating at bottom of canvas — describe what to draw, AI generates Excalidraw elements
+- AI generates native Excalidraw JSON (rectangles, ellipses, diamonds, text, arrows with bindings)
+- **Context-aware editing**: Existing canvas elements are sent to AI as context — ask "change color of 2021 to yellow" and AI modifies only that element while preserving everything else
+- Provider and model selector in canvas prompt bar (shared with chat)
+- Merge behavior: AI returns full element set for edits, appends for new drawings
+
+### Conversation Memory — Smart Context Management
+- **New feature**: AI now remembers previous messages in the same chat session
+- Loads user messages + agent outputs from DB, interleaved chronologically
+- **Sliding window**: Max 20 recent message pairs to prevent token overflow
+- **Strip html:preview blocks**: Widget HTML (5-10KB each) compressed to ~300-500 byte summaries preserving chart data, labels, colors, metrics
+- **Token budget**: History capped at ~32K chars (~8K tokens), prioritizing recent messages
+- **Smart truncation**: User messages max 500 chars, assistant max 1500 chars, cut at word boundary
+- `summarize_html_widget()` extracts Chart.js datasets, labels, colors, metric values from HTML for targeted edits
+
+### Provider System Overhaul
+- **Enable/disable toggle**: Per-provider on/off switch in Settings detail panel — disabled providers hidden from chat dropdown
+- **Add Provider flow**: "Custom" replaced with "+ Add Provider" button — create unlimited custom providers with name, base URL, API key, model
+- User-created providers appear in sidebar list with enable/disable dot indicator
+- Delete provider resets selection to enowX Labs
+- `is_enabled` column added to providers table (migration)
+- **Manual model add**: Input field in Available Models section — type model name, click Add, auto-enabled
+- Manual models persist in DB and appear in list even after Settings reopen (merged with API-fetched models)
+
+### Token Optimization
+- **MAX_REACT_ITERATIONS reduced**: 20 → 10 — prevents excessive API calls per request while still allowing complex tasks
+- **Auto-select provider + model on app start**: If only 1 provider enabled, auto-selected as default. First available model auto-selected. No manual selection needed.
+- Provider dropdown shows as label when only 1 provider (no unnecessary dropdown)
+
+### Dark Mode — Warm Claude Palette
+- Replaced cold pure black (#0a0a0a) with warm Claude-inspired tones
+- Background: #1a1816 (warm dark brown), Surface: #201e1b, Text: #e8e4de (warm off-white)
+- Accent color: #D4845A (terracotta/copper) matching light mode
+- Borders, links, focus rings all use warm tones
+- Danger colors updated to Claude red palette (#E24B4A)
+
+### Markdown Table Fix
+- `fixMarkdownTables()` rewritten — fixes malformed tables from streaming
+- Auto-corrects separator row column count to match header
+- Splits crammed rows (multiple rows on one line)
+- Injects missing separator rows without duplicating between data rows
+- Applied to both agent streaming output and final chat messages
+
+### Scroll Bounce Fix
+- Removed gradient overlay conditional mount/unmount (caused layout shift loop)
+- Added `isAutoScrolling` guard — scroll events from auto-scroll ignored
+- Throttled streaming scroll to 80ms intervals
+- `requestAnimationFrame` for scroll-to-bottom to avoid layout thrashing
+
+### UI Polish
+- Left sidebar font sizes increased: project names, session titles, labels from 12px to 13px
+- Model dropdown: `max-height: 360px` with scroll for long model lists
+- Footer bar removed for cleaner layout
+- Right sidebar toggle button in header (hidden when sidebar open)
+- enowX Flux tooltip on hover with description and token usage info
+
+---
+
+## [0.2.0] — 2026-04-22
+
+### enowX Flux — Generative UI System
+- **New feature**: enowX Flux — AI generates interactive charts, diagrams, SVG illustrations, dashboards, and widgets inline in chat
+- Integrated Claude.ai's reverse-engineered design system (CSS variables, SVG color ramps, pre-styled form elements, light/dark mode)
+- `html:preview` code blocks render as live interactive iframes with full JS/CSS support
+- Chart.js, D3, Canvas, and CDN libraries supported inside previews
+- Debounced iframe rendering during streaming (400ms) to prevent flicker
+- Auto-resize iframe to match content height via direct DOM measurement + ResizeObserver
+- Re-measure on window resize for responsive behavior
+- Expand preview to fullscreen modal with source code view and copy button
+- Toggle button in header to enable/disable Flux (saves ~3K tokens per request when off)
+- Custom tooltip on hover explaining what enowX Flux does
+- Enhanced `PREVIEW_GUIDE` with full Claude guidelines: diagram types, illustrative patterns, interactive examples, UI components, art/illustration
+
+### Welcome Screen
+- Welcome screen with greeting + quick action chips (Write, Learn, Code, Personal, Brainstorm)
+- Chips prefill prompt in input bar on click
+- Auto-create project + session when user sends first message (no need to open folder first)
+- Input bar always enabled — no more "Open a folder to start chatting"
+
+### Chat Improvements
+- **Streaming output**: Agent responses now stream token-by-token with blinking cursor, instead of appearing all at once
+- **Auto-rename chat**: After first AI response, LLM generates a short 2-5 word title (e.g. "Greeting", "React Auth Setup")
+- **Copy response**: Copy button below each AI response with model name and generation duration
+- **Agent info relocated**: Model name and duration moved to bottom of response, agent type stays at top
+- **Markdown table fix**: `fixMarkdownTables()` utility fixes malformed tables from streaming — auto-corrects separator column count, splits crammed rows, injects missing separators
+- **Scroll bounce fix**: Removed gradient overlay mount/unmount loop, added `isAutoScrolling` guard, throttled streaming scroll (80ms)
+
+### Sidebar
+- **Left sidebar toggle**: Click logo icon to collapse/expand with smooth CSS grid animation (0.25s cubic-bezier)
+- **Right sidebar toggle**: Toggle button inside sidebar header + in chat header when closed
+- **Sidebar font size**: Increased project names, session titles, and labels from 12px to 13px for readability
+- **Footer removed**: Cleaner layout, sidebar toggles moved to header
+
+### Settings & Providers
+- **Custom provider fix**: Added "Default Model" input field for Custom/Ollama providers — fixes silent save failure
+- **Error feedback**: Save errors now shown as red inline messages instead of silent `console.error`
+- **Base URL save**: Can now save base URL even when provider doesn't exist yet (auto-creates)
+- **Model dropdown scroll**: Fixed dropdown clipping — added `max-height: 360px` with scrollbar for long model lists
+
+### Header & Navigation
+- **Rename chat**: Click pencil icon → inline edit with Enter/Escape
+- **Three-dot menu**: New Chat + Delete Chat options
+- **Delete confirmation**: Custom dark-themed confirm dialog (replaced broken native `window.confirm`)
+- **Theme toggle**: Dark/light mode switch in header
+- **enowX Flux toggle**: ON/OFF pill button with status tooltip
+
+### Theme & Design
+- **Dark mode warm palette**: Replaced cold pure black (#0a0a0a) with warm Claude-inspired tones (#1a1816 bg, #e8e4de text, #D4845A accent)
+- **Light/dark preview sync**: iframe previews now use class-based theme switching (`.light`/`.dark`) instead of `prefers-color-scheme` media query — syncs with app toggle
+- **Tauri dialog permissions**: Upgraded from `dialog:allow-open` to `dialog:default` for full dialog support
+
+### Bug Fixes
+- **Agent request fix**: Fixed parameter mismatch — frontend now wraps `run_agent` params in `request` object matching Rust `RunAgentRequest` struct
+- **API key pre-flight**: Added validation check before API calls — clear error message instead of cryptic 401
+- **First message missing**: Fixed race condition where `setActiveSessionId` triggered DB reload that wiped optimistic user message
+- **Clippy warnings**: Fixed all Rust clippy warnings — dead code removal, parameter struct refactoring, simplified patterns
+
+### Infrastructure
+- **Tauri capabilities**: Added `dialog:default` permission for ask/confirm/message dialogs
+- **Generate title command**: New `generate_title` Tauri command — LLM generates chat title via non-streaming API call
+
+---
+
+## [0.1.0] — Initial Release
+
+- Tauri desktop app with Rust backend + React/TypeScript frontend
+- Multi-agent system: Orchestrator, Planner, Coder FE/BE, Security, UX, UI, Tester, Reviewer, Researcher, Librarian
+- Provider support: enowX Labs, OpenAI, Anthropic, Gemini, Ollama, Custom (OpenAI-compatible)
+- Chat with streaming responses
+- Tool execution: read_file, write_file, list_dir, search_files, run_command, web_search
+- Settings modal with provider configuration and model management
+- Project and session management
+- Agent configuration per agent type
diff --git a/README.md b/README.md
index 102e366..73e083a 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,310 @@
-# Tauri + React + Typescript
+
-This template should help get you started developing with Tauri, React and Typescript in Vite.
+# 🚀 enowX Coder
-## Recommended IDE Setup
+**Agentic Coding Tools - AI-Powered Development Assistant**
-- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
+[](LICENSE)
+[](https://tauri.app)
+[](https://react.dev)
+[](https://www.rust-lang.org)
+[](https://www.typescriptlang.org)
+
+*A powerful desktop application that combines AI agents with an intuitive interface for enhanced coding workflows*
+
+[Features](#-features) • [Installation](#-installation) • [Usage](#-usage) • [Architecture](#-architecture) • [Contributing](#-contributing)
+
+
+
+---
+
+## ✨ Features
+
+### 🤖 AI-Powered Chat Interface
+- **Multi-Provider Support**: Connect to enowX Labs, OpenAI, Anthropic, Google, or custom providers
+- **Conversation Memory**: Smart context management with sliding window (up to 20 message pairs)
+- **Agent Execution Timeline**: Visual representation of AI thinking process, tool usage, and results
+- **Markdown Rendering**: Full GitHub-flavored markdown with syntax highlighting
+- **Code Block Actions**: Copy, execute, or preview code directly from chat
+
+### 🎨 Excalidraw Canvas Integration
+- **Collaborative Whiteboard**: Full-featured drawing canvas with shapes, arrows, text, and freehand tools
+- **AI-Generated Diagrams**: Describe what you want to draw, AI generates native Excalidraw elements
+- **Context-Aware Editing**: AI understands existing canvas elements for precise modifications
+- **Auto-Save**: Drawings persist per project with 1-second auto-save
+- **Theme Sync**: Dark/light mode automatically syncs with app theme
+
+### 🛠️ Advanced Agent System
+- **Multiple Specialized Agents**: Orchestrator, Planner, Coder (FE/BE), Tester, Reviewer, Security, and more
+- **Tool Execution**: File operations, shell commands, web scraping, and custom tools
+- **Permission System**: User approval required for sensitive operations
+- **Streaming Responses**: Real-time token streaming for immediate feedback
+
+### 🎯 Project Management
+- **Multi-Project Support**: Switch between projects seamlessly
+- **Session History**: Persistent chat sessions per project
+- **SQLite Backend**: Fast, reliable local storage for all data
+
+### 🌓 Beautiful UI/UX
+- **Warm Dark Mode**: Claude-inspired color palette with terracotta accents
+- **Responsive Layout**: Three-panel layout with collapsible sidebars
+- **Smooth Animations**: Polished transitions and micro-interactions
+- **Accessibility**: Keyboard shortcuts and screen reader support
+
+---
+
+## 📸 Screenshots
+
+### enowX Flux - AI Chat Interface
+*Powerful AI-powered conversations with enowX Flux integration and warm Claude-inspired dark theme*
+
+
+
+### Chat Interface - Light Mode
+*Clean and modern light theme for comfortable daytime coding*
+
+
+
+### Excalidraw Canvas
+*Collaborative whiteboard with AI diagram generation*
+
+
+
+### Provider Settings
+*Manage multiple AI providers and models with enable/disable toggles*
+
+
+
+---
+
+## 🚀 Installation
+
+### Prerequisites
+- **Node.js** 18+ or **Bun** 1.0+
+- **Rust** 1.75+ (for Tauri)
+- **Git**
+
+### Quick Start
+
+```bash
+# Clone the repository
+git clone https://github.com/mhmmadazis/enowX-Coder.git
+cd enowX-Coder
+
+# Install dependencies
+npm install
+# or
+bun install
+
+# Run in development mode
+npm run tauri dev
+# or
+bun run tauri dev
+```
+
+### Build for Production
+
+```bash
+# Build the application
+npm run tauri build
+# or
+bun run tauri build
+```
+
+The compiled application will be available in `src-tauri/target/release/bundle/`.
+
+---
+
+## 🎯 Usage
+
+### Getting Started
+
+1. **Launch the Application**
+ - Run `npm run tauri dev` or open the built executable
+
+2. **Configure AI Provider**
+ - Click the settings icon (⚙️) in the top-right corner
+ - Navigate to "Providers" tab
+ - Add your API key for enowX Labs, OpenAI, Anthropic, or custom provider
+ - Enable the provider and select a model
+
+3. **Create a Project**
+ - Click "New Project" in the left sidebar
+ - Enter project name and optional path
+ - Start chatting with AI agents
+
+### Chat Commands
+
+- **Ask Questions**: Type naturally, AI understands context
+- **Request Code**: "Create a React component for user authentication"
+- **Execute Tools**: AI can read/write files, run shell commands (with permission)
+- **Generate Diagrams**: Switch to Canvas tab and describe what to draw
+
+### Canvas Mode
+
+1. **Switch to Canvas**: Click "Canvas" in the header segmented control
+2. **Draw Manually**: Use Excalidraw toolbar for shapes, arrows, text
+3. **AI Generation**: Type prompt in bottom bar (e.g., "Draw a system architecture diagram")
+4. **Edit with AI**: "Change the color of Database to blue" - AI modifies only that element
+
+### Keyboard Shortcuts
+
+- `Ctrl/Cmd + N` - New chat session
+- `Ctrl/Cmd + ,` - Open settings
+- `Ctrl/Cmd + B` - Toggle left sidebar
+- `Ctrl/Cmd + Shift + B` - Toggle right sidebar
+- `Esc` - Close dialogs/modals
+
+---
+
+## 🏗️ Architecture
+
+### Tech Stack
+
+**Frontend**
+- **React 19** - UI framework with concurrent features
+- **TypeScript** - Type-safe development
+- **Tailwind CSS 4** - Utility-first styling
+- **Zustand** - Lightweight state management
+- **Excalidraw** - Canvas whiteboard library
+
+**Backend**
+- **Rust** - High-performance, memory-safe backend
+- **Tauri 2** - Cross-platform desktop framework
+- **SQLite** - Embedded database via rusqlite
+- **Tokio** - Async runtime for concurrent operations
+
+### Project Structure
+
+```
+enowX-Coder/
+├── src/ # React frontend
+│ ├── components/ # UI components
+│ │ ├── chat/ # Chat interface components
+│ │ ├── canvas/ # Excalidraw canvas
+│ │ ├── layout/ # App shell and layout
+│ │ ├── settings/ # Settings panels
+│ │ └── ui/ # Reusable UI primitives
+│ ├── stores/ # Zustand state stores
+│ ├── types/ # TypeScript type definitions
+│ └── lib/ # Utility functions
+│
+├── src-tauri/ # Rust backend
+│ ├── src/
+│ │ ├── agents/ # AI agent implementations
+│ │ ├── commands/ # Tauri commands (IPC)
+│ │ ├── services/ # Business logic layer
+│ │ ├── models/ # Data models
+│ │ ├── tools/ # Agent tool executors
+│ │ └── state.rs # Application state
+│ ├── migrations/ # SQLite schema migrations
+│ └── Cargo.toml # Rust dependencies
+│
+├── public/ # Static assets
+├── screenshots/ # Application screenshots
+└── CHANGELOG.md # Version history
+```
+
+### Key Components
+
+**Agent System** (`src-tauri/src/agents/`)
+- `runner.rs` - Main agent execution loop with ReAct pattern
+- `prompts/` - Specialized agent prompts (Orchestrator, Coder, Tester, etc.)
+
+**Services** (`src-tauri/src/services/`)
+- `chat_service.rs` - Message handling, streaming, conversation memory
+- `provider_service.rs` - AI provider management and API calls
+- `drawing_service.rs` - Canvas persistence and AI diagram generation
+- `agent_service.rs` - Agent configuration and execution
+
+**State Management**
+- Frontend: Zustand stores for UI state, chat, projects, settings
+- Backend: Tauri managed state with Arc> for thread-safe access
+
+---
+
+## 🔧 Configuration
+
+### Environment Variables
+
+Create a `.env` file in the project root (optional):
+
+```env
+# Default AI Provider
+DEFAULT_PROVIDER=enowx-labs
+
+# API Keys (can also be set in UI)
+ENOWX_API_KEY=your_key_here
+OPENAI_API_KEY=your_key_here
+ANTHROPIC_API_KEY=your_key_here
+```
+
+### Custom Providers
+
+Add custom AI providers via Settings UI:
+1. Click "+ Add Provider"
+2. Enter provider name, base URL, and API key
+3. Add models manually or fetch from API
+4. Enable provider and select model
+
+---
+
+## 🤝 Contributing
+
+Contributions are welcome! Please follow these guidelines:
+
+1. **Fork the repository**
+2. **Create a feature branch**: `git checkout -b feature/amazing-feature`
+3. **Commit your changes**: `git commit -m 'feat: add amazing feature'`
+4. **Push to the branch**: `git push origin feature/amazing-feature`
+5. **Open a Pull Request**
+
+### Commit Convention
+
+Follow [Conventional Commits](https://www.conventionalcommits.org/):
+- `feat:` - New feature
+- `fix:` - Bug fix
+- `refactor:` - Code refactoring
+- `docs:` - Documentation changes
+- `chore:` - Maintenance tasks
+- `test:` - Test additions/changes
+
+### Development Guidelines
+
+- Run `cargo clippy` before committing Rust code
+- Run `npm run build` to check TypeScript compilation
+- Test on multiple platforms (Windows, macOS, Linux)
+- Update CHANGELOG.md for notable changes
+
+---
+
+## 📝 License
+
+This project is licensed under the **Apache License 2.0** - see the [LICENSE](LICENSE) file for details.
+
+---
+
+## 🙏 Acknowledgments
+
+- **Tauri Team** - For the amazing cross-platform framework
+- **Excalidraw** - For the collaborative whiteboard library
+- **Anthropic** - For Claude AI inspiration and design patterns
+- **enowX Labs** - For AI infrastructure and model access
+
+---
+
+## 📞 Support
+
+- **Issues**: [GitHub Issues](https://github.com/mhmmadazis/enowX-Coder/issues)
+- **Discussions**: [GitHub Discussions](https://github.com/mhmmadazis/enowX-Coder/discussions)
+- **Email**: support@enowx.dev
+
+---
+
+
+
+**Built with ❤️ by the enowX Community**
+
+⭐ Star this repo if you find it useful!
+
+
diff --git a/package.json b/package.json
index fc7a71c..72d018c 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,7 @@
"tauri": "tauri"
},
"dependencies": {
+ "@excalidraw/excalidraw": "^0.18.1",
"@phosphor-icons/react": "^2.1.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.4",
diff --git a/screenshots/.gitkeep b/screenshots/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/screenshots/Excalidraw.png b/screenshots/Excalidraw.png
new file mode 100644
index 0000000..703fdc2
Binary files /dev/null and b/screenshots/Excalidraw.png differ
diff --git a/screenshots/Light Moded.png b/screenshots/Light Moded.png
new file mode 100644
index 0000000..ab1350a
Binary files /dev/null and b/screenshots/Light Moded.png differ
diff --git a/screenshots/Providers.png b/screenshots/Providers.png
new file mode 100644
index 0000000..a2b83a3
Binary files /dev/null and b/screenshots/Providers.png differ
diff --git a/screenshots/enowX Flux.png b/screenshots/enowX Flux.png
new file mode 100644
index 0000000..3fbba60
Binary files /dev/null and b/screenshots/enowX Flux.png differ
diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock
index f41d07d..859be03 100644
--- a/src-tauri/Cargo.lock
+++ b/src-tauri/Cargo.lock
@@ -982,6 +982,7 @@ dependencies = [
"thiserror 2.0.18",
"tokio",
"tokio-stream",
+ "tokio-util",
"urlencoding",
"uuid",
"walkdir",
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index 33cf77a..d7c3ab4 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -26,6 +26,7 @@ reqwest = { version = "0.12", features = ["json", "stream"] }
uuid = { version = "1", features = ["v4", "serde"] }
chrono = { version = "0.4", features = ["serde"] }
futures-util = "0.3"
+tokio-util = "0.7"
tokio-stream = "0.1"
anyhow = "1"
log = "0.4"
diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json
index 62f67cf..778bfb5 100644
--- a/src-tauri/capabilities/default.json
+++ b/src-tauri/capabilities/default.json
@@ -6,6 +6,6 @@
"permissions": [
"core:default",
"opener:default",
- "dialog:allow-open"
+ "dialog:default"
]
}
diff --git a/src-tauri/migrations/20260422000_provider_enabled.sql b/src-tauri/migrations/20260422000_provider_enabled.sql
new file mode 100644
index 0000000..a66cf32
--- /dev/null
+++ b/src-tauri/migrations/20260422000_provider_enabled.sql
@@ -0,0 +1,2 @@
+-- Add is_enabled flag to providers (default true)
+ALTER TABLE providers ADD COLUMN is_enabled INTEGER NOT NULL DEFAULT 1;
diff --git a/src-tauri/migrations/20260422001_drawings.sql b/src-tauri/migrations/20260422001_drawings.sql
new file mode 100644
index 0000000..11c1a52
--- /dev/null
+++ b/src-tauri/migrations/20260422001_drawings.sql
@@ -0,0 +1,11 @@
+-- Drawings table: one drawing per project, stores Excalidraw JSON
+CREATE TABLE IF NOT EXISTS drawings (
+ id TEXT PRIMARY KEY NOT NULL,
+ project_id TEXT NOT NULL,
+ data TEXT NOT NULL DEFAULT '{}',
+ created_at TEXT NOT NULL,
+ updated_at TEXT NOT NULL,
+ FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
+);
+
+CREATE UNIQUE INDEX IF NOT EXISTS idx_drawings_project ON drawings(project_id);
diff --git a/src-tauri/src/agents/runner.rs b/src-tauri/src/agents/runner.rs
index 9f72c85..ff92ba2 100644
--- a/src-tauri/src/agents/runner.rs
+++ b/src-tauri/src/agents/runner.rs
@@ -1,6 +1,8 @@
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
+use std::time::Duration;
+use futures_util::stream::FuturesUnordered;
use futures_util::StreamExt;
use reqwest::header::{AUTHORIZATION, CONTENT_TYPE};
use serde::Serialize;
@@ -8,23 +10,234 @@ use serde_json::{json, Value};
use sqlx::SqlitePool;
use tauri::ipc::Channel;
use tauri::{AppHandle, Emitter};
-use tokio::task::JoinSet;
+use tokio::sync::oneshot;
+use tokio_util::sync::CancellationToken;
use uuid::Uuid;
use crate::agents::prompts::get_prompt;
use crate::error::{AppError, AppResult};
use crate::services::{now_rfc3339, provider_service};
+use crate::state::PermissionState;
use crate::tools::{ToolCall, ToolExecutor, ToolName};
-const MAX_REACT_ITERATIONS: usize = 20;
+const MAX_REACT_ITERATIONS: usize = 10;
const SYNTHESIS_REACT_ITERATIONS: usize = 8;
const LANGUAGE_GUARD: &str =
"IMPORTANT: Reply using the same language as the user's latest message. If user writes Indonesian, answer in Indonesian. Never switch to another language unless the user explicitly asks you to.";
+const PREVIEW_GUIDE: &str = r#"INTERACTIVE PREVIEW — VISUAL CREATION SYSTEM
+
+When the user asks for a visualization, diagram, chart, interactive demo, UI mockup, or any visual/interactive HTML content, output it directly in your response as a fenced code block with the language tag `html:preview`. Do NOT use write_file — the app renders it as a live interactive preview inline. Only use write_file when the user explicitly asks to save a file on disk.
+
+Format: ```html:preview followed by a COMPLETE standalone HTML document (DOCTYPE, html, head, body).
+
+The preview iframe has a full design system pre-loaded: CSS variables, SVG color ramp classes, pre-styled form elements, and light/dark mode support. Your HTML inherits all of this automatically.
+
+## Core Design Philosophy
+- **Seamless**: The widget should feel like a natural extension of the chat, not an embed from somewhere else.
+- **Flat**: No gradients, mesh backgrounds, noise textures, drop shadows, blur, glow, or neon effects. Clean flat surfaces only.
+- **Transparent background**: Do NOT set background color on `` or outer containers. The host app provides the background. Only set background on inner cards/surfaces using `var(--color-background-secondary)`.
+- **Compact**: Show the essential visual inline. All explanatory text goes in your response text OUTSIDE the code block.
+- **Text goes in your response, visuals go in the widget** — All explanatory text, descriptions, introductions, and summaries must be written as normal response text OUTSIDE the code block. The widget should contain ONLY the visual element. Never put paragraphs of explanation inside the HTML.
+- **Streaming-safe**: `${bridge}`;
+
+ if (/]/i.test(cleaned)) {
+ let result = cleaned.replace(/]*)>/i, ``);
+ if (/]/i.test(result)) {
+ return result.replace(/]*)>/i, `${headContent}`);
+ }
+ return result.replace(/]*)>/i, `${headContent}`);
+ }
+
+ return `${headContent}${cleaned}`;
+}
+
+export const HtmlPreview: React.FC = ({ code, title, onSendPrompt }) => {
+ const iframeRef = useRef(null);
+ const [expanded, setExpanded] = useState(false);
+ const [showSource, setShowSource] = useState(false);
+ const [copied, setCopied] = useState(false);
+ const [iframeHeight, setIframeHeight] = useState(200);
+ const theme = useUIStore((s) => s.theme);
+
+ // Debounced srcdoc — only rebuild after code stops changing for 400ms
+ // This prevents iframe reload on every streaming token
+ const [debouncedCode, setDebouncedCode] = useState(code);
+ const debounceTimer = useRef | null>(null);
+
+ useEffect(() => {
+ if (debounceTimer.current) clearTimeout(debounceTimer.current);
+ debounceTimer.current = setTimeout(() => {
+ setDebouncedCode(code);
+ }, 400);
+ return () => { if (debounceTimer.current) clearTimeout(debounceTimer.current); };
+ }, [code]);
+
+ const srcdoc = React.useMemo(() => buildSrcdoc(debouncedCode, theme), [debouncedCode, theme]);
+
+ // Measure iframe content height directly via contentDocument (same-origin allowed by sandbox)
+ const pollTimers = useRef[]>([]);
+
+ const measureHeight = useCallback(() => {
+ const iframe = iframeRef.current;
+ if (!iframe) return;
+ try {
+ const doc = iframe.contentDocument;
+ if (!doc?.body) return;
+ // Wrap all body content in a measuring div to get exact height
+ const h = doc.body.scrollHeight;
+ if (h > 20) {
+ setIframeHeight(h);
+ }
+ } catch {}
+ }, []);
+
+ // Poll height after load and at intervals to catch async renders (Chart.js etc)
+ const startPolling = useCallback(() => {
+ pollTimers.current.forEach(clearTimeout);
+ pollTimers.current = [];
+ [0, 100, 300, 600, 1000, 2000, 4000].forEach((delay) => {
+ pollTimers.current.push(setTimeout(measureHeight, delay));
+ });
+ }, [measureHeight]);
+
+ // Re-poll when debounced code changes (iframe reloads)
+ useEffect(() => {
+ startPolling();
+ return () => pollTimers.current.forEach(clearTimeout);
+ }, [debouncedCode, startPolling]);
+
+ // Re-measure on window resize so iframe adapts when user resizes the app
+ useEffect(() => {
+ let resizeRaf = 0;
+ const onResize = () => {
+ cancelAnimationFrame(resizeRaf);
+ resizeRaf = requestAnimationFrame(() => {
+ measureHeight();
+ // Poll again after content reflows
+ setTimeout(measureHeight, 200);
+ setTimeout(measureHeight, 600);
+ });
+ };
+ window.addEventListener('resize', onResize);
+ return () => {
+ window.removeEventListener('resize', onResize);
+ cancelAnimationFrame(resizeRaf);
+ };
+ }, [measureHeight]);
+
+ // Listen for sendPrompt/openLink messages from iframe
+ useEffect(() => {
+ const handleMessage = (event: MessageEvent) => {
+ const data = event.data;
+ if (!data || typeof data !== 'object') return;
+ if (data.type === 'sendPrompt' && typeof data.text === 'string') {
+ onSendPrompt?.(data.text);
+ }
+ if (data.type === 'openLink' && typeof data.url === 'string') {
+ window.open(data.url, '_blank', 'noopener');
+ }
+ };
+ window.addEventListener('message', handleMessage);
+ return () => window.removeEventListener('message', handleMessage);
+ }, [onSendPrompt]);
+
+ const handleCopy = useCallback(async () => {
+ await navigator.clipboard.writeText(code);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ }, [code]);
+
+ const displayTitle = React.useMemo(() => {
+ if (title) return title;
+ const match = /(.*?)<\/title>/i.exec(code);
+ if (match?.[1]) return match[1];
+ return 'Preview';
+ }, [title, code]);
+
+ useEffect(() => {
+ if (!expanded) return;
+ const handleKey = (e: KeyboardEvent) => { if (e.key === 'Escape') setExpanded(false); };
+ window.addEventListener('keydown', handleKey);
+ return () => window.removeEventListener('keydown', handleKey);
+ }, [expanded]);
+
+ const btnClass = 'p-1.5 rounded-lg transition-all duration-150';
+ const btnIdle = 'text-[var(--text-subtle)] hover:text-[var(--text-muted)] hover:bg-[var(--hover-bg)]';
+
+ return (
+ <>
+ {expanded && (
+
setExpanded(false)}>
+
e.stopPropagation()}>
+
+ {displayTitle}
+
+
+
+
+
+
+
+ {showSource ? (
+
{code}
+ ) : (
+
+ )}
+
+
+
+ )}
+
+ {/* Seamless inline preview */}
+
+
+
+
+
+
+
+ {showSource ? (
+
{code}
+ ) : (
+
+ )}
+
+ >
+ );
+};
diff --git a/src/components/chat/MarkdownCodeBlock.tsx b/src/components/chat/MarkdownCodeBlock.tsx
new file mode 100644
index 0000000..03107a6
--- /dev/null
+++ b/src/components/chat/MarkdownCodeBlock.tsx
@@ -0,0 +1,96 @@
+import React from 'react';
+import { Copy, Check } from '@phosphor-icons/react';
+import { HtmlPreview } from './HtmlPreview';
+
+interface MarkdownCodeBlockProps {
+ inline?: boolean;
+ className?: string;
+ children?: React.ReactNode;
+}
+
+/**
+ * Detect if a language tag is an HTML preview trigger.
+ * Supports: html:preview, html:artifact, html:viz, html:visual, html:render
+ */
+const isPreviewLang = (lang: string): boolean =>
+ /^html:(preview|artifact|viz|visual|render)$/i.test(lang);
+
+/**
+ * Extract a title hint from the language tag, e.g. "html:preview:My Chart" → "My Chart"
+ */
+const extractPreviewTitle = (lang: string): string | undefined => {
+ const parts = lang.split(':');
+ return parts.length >= 3 ? parts.slice(2).join(':') : undefined;
+};
+
+/**
+ * Detect if HTML code looks like a full interactive page that should be previewed.
+ */
+const looksLikeFullPage = (code: string): boolean => {
+ if (code.length < 100) return false;
+ const hasDoctype = /]/i.test(code);
+ const hasBodyTag = /]/i.test(code);
+ const hasScript = /