Skip to content

WDG Playground - Technical Architecture

Comprehensive Technical Design DocumentVersion: 1.0 Last Updated: October 2025

Table of Contents

  1. Technology Stack
  2. System Architecture
  3. Embedded Binaries Architecture
  4. MCP Server Implementation
  5. Agent SDK Integration
  6. Process Management
  7. Data Architecture
  8. Security Architecture
  9. Performance Optimization
  10. Cross-Platform Considerations

Technology Stack

Frontend Layer

yaml
Framework: React 18.2+
Language: TypeScript 5.0+
Build Tool: Vite 4.0+
UI Library: shadcn/ui (matching platform dashboard design)
State Management: React Context + Hooks
Styling: Tailwind CSS 3.0+ (using dashboard design tokens)
Icons: Lucide React
Markdown: react-markdown with remark-gfm
Code Highlighting: react-syntax-highlighter
Notifications: react-hot-toast (matching platform)

Backend Layer (Tauri)

yaml
Framework: Tauri 2.0
Language: Rust 1.75+
Purpose: System integration, process management, IPC bridge
Async Runtime: Tokio
HTTP Client: reqwest
Git Library: git2-rs
Database: SQLite (via rusqlite)
Serialization: serde + serde_json

Application Logic Layer (Node.js Sidecar)

yaml
Runtime: Node.js 20+ (bundled as sidecar)
Language: TypeScript 5.0+
MCP Server: Agent SDK createSdkMcpServer()
Vector DB Client: @qdrant/js-client-rest
Embeddings: @xenova/transformers (all-MiniLM-L6-v2)
Process Management: Managed by Rust (Tauri)

Embedded Services

yaml
Web Server: Nginx 1.24+
Application Server: PHP 8.2+ (PHP-FPM)
Database: MySQL 8.0 (embedded server)
Vector Database: Qdrant 1.7+
Embeddings: Transformers.js (all-MiniLM-L6-v2, runs in Node.js)

AI Integration

yaml
SDK: Anthropic Agent SDK (TypeScript)
Models: Claude Sonnet 4.5, Claude Haiku 4
Protocol: Model Context Protocol (MCP)
Embeddings: Local (384-dimensional vectors)
Context: Project-specific CLAUDE.md + skills

System Architecture

Complete System Diagram

%%{init:{'theme':'neutral'}}%%
graph TB
   subgraph "User Interface"
       Browser[Tauri WebView]
       TrayIcon[System Tray]
   end

   subgraph "Tauri Frontend (React + TS)"
       App[App Shell]
       ChatUI[Chat Interface]
       ProjectUI[Project Manager]
       ExplorerUI[Block Explorer]
       SettingsUI[Settings]

       App --> ChatUI
       App --> ProjectUI
       App --> ExplorerUI
       App --> SettingsUI
   end

   subgraph "IPC Layer"
       Commands[Tauri Commands]
       Events[Event System]
   end

   subgraph "Tauri Backend (Rust)"
       AppState[Application State]
       ServiceMgr[Service Manager]
       GitOps[Git Operations]
       ProjectMgr[Project Manager]
       ConfigMgr[Config Manager]

       subgraph "Process Supervisor"
           PHPMgr[PHP Process]
           MySQLMgr[MySQL Process]
           NginxMgr[Nginx Process]
           QdrantMgr[Qdrant Process]
           NodeMgr[Node.js Sidecar]
       end
   end

   subgraph "Node.js Sidecar (TypeScript)"
       AgentSDK[Agent SDK Runtime]
       MCPServer[MCP Server]
       MCPTools[MCP Tools]
       QdrantClient[Qdrant Client]
       Embedder[Transformers.js]
   end

   subgraph "External AI"
       AnthropicAPI[Anthropic API]
   end

   subgraph "Embedded Binaries"
       PHP[PHP 8.2 Binary]
       MySQL[MySQL 8.0 Binary]
       Nginx[Nginx Binary]
       Qdrant[Qdrant Binary]
   end

   subgraph "File System"
       ProjectFiles[(Project Files)]
       ConfigFiles[(Configs)]
       DBFiles[(Databases)]
       VectorDB[(Vector Data)]
       Logs[(Logs)]
   end

   Browser --> App
   TrayIcon --> AppState

   ChatUI --> AgentSDK
   AgentSDK --> AnthropicAPI
   AgentSDK --> MCPServer
   MCPServer --> MCPTools
   MCPTools --> QdrantClient
   MCPTools --> Embedder

   ProjectUI --> Commands
   Commands --> ProjectMgr
   Commands --> ServiceMgr
   Commands --> GitOps

   ServiceMgr --> PHPMgr
   ServiceMgr --> MySQLMgr
   ServiceMgr --> NginxMgr
   ServiceMgr --> QdrantMgr
   ServiceMgr --> NodeMgr

   PHPMgr --> PHP
   MySQLMgr --> MySQL
   NginxMgr --> Nginx
   QdrantMgr --> Qdrant
   NodeMgr --> AgentSDK

   QdrantClient --> Qdrant

   ProjectMgr --> ProjectFiles
   ConfigMgr --> ConfigFiles
   MySQLMgr --> DBFiles
   QdrantMgr --> VectorDB

Data Flow: User Creates Project

%%{init:{'theme':'neutral'}}%%
sequenceDiagram
   actor User
   participant UI as React UI
   participant IPC as Tauri IPC
   participant PM as Project Manager
   participant SM as Service Manager
   participant FS as File System
   participant PHP
   participant MySQL
   participant Nginx

   User->>UI: Click "Create Project"
   UI->>UI: Show project wizard
   User->>UI: Enter name, template
   UI->>IPC: create_project(config)
   IPC->>PM: handle_create_project()

   PM->>FS: Create project directory
   PM->>FS: Copy WordPress core
   PM->>FS: Copy template files
   PM->>PM: Generate wp-config.php
   PM->>PM: Generate nginx.conf

   PM->>SM: Start services
   SM->>MySQL: Start MySQL process
   MySQL-->>SM: Running (port 3306)
   SM->>MySQL: Create database

   SM->>PHP: Start PHP-FPM
   PHP-->>SM: Running (socket)

   SM->>Nginx: Start Nginx
   Nginx-->>SM: Running (port 8443)

   PM->>PHP: Run WP-CLI install
   PHP-->>PM: WordPress installed

   PM->>IPC: Project created
   IPC->>UI: Show success + URL
   UI->>User: "Project ready at https://my-site.localhost:8443"

Data Flow: AI Chat with Tool Use

%%{init:{'theme':'neutral'}}%%
sequenceDiagram
   actor User
   participant UI as Chat UI
   participant Node as Node.js Sidecar
   participant SDK as Agent SDK
   participant API as Anthropic API
   participant MCP as MCP Server
   participant Qdrant as Qdrant Binary

   User->>UI: "What Wikit blocks are available?"
   UI->>Node: Send message via IPC
   Node->>SDK: Pass to Agent SDK
   SDK->>API: POST /messages (streaming)
   API-->>SDK: tool_use: search_wikit_blocks
   SDK->>MCP: Execute tool (createSdkMcpServer)
   MCP->>Qdrant: HTTP: Query vectors (filter: component_type=block)
   Qdrant-->>MCP: 50 blocks found
   MCP-->>SDK: Return block list
   SDK->>API: Continue with tool result
   API-->>SDK: Stream response text
   SDK-->>Node: Stream tokens
   Node-->>UI: Stream via IPC
   UI-->>User: Display formatted response

Embedded Binaries Architecture

Binary Selection Strategy

rust
// src-tauri/src/binaries/mod.rs

pub struct BinaryManager {
   platform: Platform,
   architecture: Architecture,
   binaries_path: PathBuf,
}

#[derive(Debug)]
pub enum Platform {
   MacOS,
   Windows,
   Linux,
}

#[derive(Debug)]
pub enum Architecture {
   X64,
   Arm64,
}

impl BinaryManager {
   /// Get platform-specific PHP binary path
   pub fn php_binary(&self) -> Result<PathBuf> {
       let binary_name = match (&self.platform, &self.architecture) {
           (Platform::MacOS, Architecture::X64) => "php-8.2-macos-x64/bin/php",
           (Platform::MacOS, Architecture::Arm64) => "php-8.2-macos-arm64/bin/php",
           (Platform::Windows, Architecture::X64) => "php-8.2-windows-x64/php.exe",
           (Platform::Linux, Architecture::X64) => "php-8.2-linux-x64/bin/php",
           _ => return Err(anyhow::anyhow!("Unsupported platform")),
       };

       Ok(self.binaries_path.join(binary_name))
   }

   /// Get platform-specific MySQL binary path
   pub fn mysql_binary(&self) -> Result<PathBuf> {
       let binary_name = match (&self.platform, &self.architecture) {
           (Platform::MacOS, _) => "mysql-8.0-macos/bin/mysqld",
           (Platform::Windows, _) => "mysql-8.0-windows/bin/mysqld.exe",
           (Platform::Linux, _) => "mysql-8.0-linux/bin/mysqld",
       };

       Ok(self.binaries_path.join(binary_name))
   }

   /// Get platform-specific Nginx binary path
   pub fn nginx_binary(&self) -> Result<PathBuf> {
       let binary_name = match &self.platform {
           Platform::MacOS => "nginx-macos/nginx",
           Platform::Windows => "nginx-windows/nginx.exe",
           Platform::Linux => "nginx-linux/nginx",
       };

       Ok(self.binaries_path.join(binary_name))
   }
}

Binary Bundle Structure

wdg-playground-app/
src-tauri/
    resources/
        binaries/
           node-20-macos-x64/        # ~40MB
              bin/node
           node-20-macos-arm64/      # ~40MB
           node-20-windows-x64/      # ~45MB
           node-20-linux-x64/        # ~40MB
           php-8.2-macos-x64/        # ~40MB
              bin/php
              bin/php-fpm
              lib/php/extensions/
           php-8.2-macos-arm64/      # ~40MB
           php-8.2-windows-x64/      # ~50MB
           php-8.2-linux-x64/        # ~45MB
           mysql-8.0-macos/          # ~150MB
              bin/mysqld
              bin/mysql
              share/
           mysql-8.0-windows/        # ~170MB
           mysql-8.0-linux/          # ~160MB
           nginx-macos/              # ~2MB
              nginx
              conf/
           nginx-windows/            # ~3MB
           nginx-linux/              # ~2MB
           qdrant-macos/             # ~30MB
           qdrant-windows/           # ~35MB
           qdrant-linux/             # ~30MB
        nodejs-app/                   # TypeScript MCP server
            dist/                     # Compiled TypeScript
               server.js              # Main MCP server
               tools/                 # MCP tool implementations
            node_modules/             # Dependencies (~20MB)
            package.json
        wordpress/
            latest.tar.gz              # ~20MB

Binary Sources

Node.js 20:

  • All platforms: From https://nodejs.org/dist/ (official binaries)
  • Minimal install: Just node binary, no npm needed
  • Size: ~40-45MB per platform

PHP 8.2:

MySQL 8.0:

  • All platforms: MySQL Community Server embedded build
  • Alternative: MariaDB embedded for smaller footprint
  • Configuration: Minimal schema, single-user mode

Nginx:

  • All platforms: Official nginx binaries or static compile
  • Modules: Only core + SSL module needed

Qdrant:

First-Run Extraction

rust
// src-tauri/src/setup.rs

pub async fn first_run_setup(app_handle: &AppHandle) -> Result<()> {
   let data_dir = app_handle.path_resolver()
       .app_data_dir()
       .ok_or_else(|| anyhow::anyhow!("Failed to get app data dir"))?;

   // Check if already extracted
   let marker_file = data_dir.join(".extracted");
   if marker_file.exists() {
       return Ok(());
   }

   log::info!("First run detected, extracting binaries...");

   // Extract binaries to app data directory
   extract_binaries(&app_handle, &data_dir).await?;

   // Initialize MySQL data directory
   init_mysql_datadir(&data_dir).await?;

   // Generate default configs
   generate_default_configs(&data_dir)?;

   // Create marker file
   std::fs::write(marker_file, "")?;

   log::info!("First run setup complete");
   Ok(())
}

async fn extract_binaries(app: &AppHandle, dest: &Path) -> Result<()> {
   let resource_path = app.path_resolver()
       .resolve_resource("resources/binaries")
       .ok_or_else(|| anyhow::anyhow!("Resource path not found"))?;

   // Copy all binaries to app data directory
   copy_dir_all(&resource_path, &dest.join("binaries"))?;

   // Make binaries executable (Unix)
   #[cfg(unix)]
   {
       use std::os::unix::fs::PermissionsExt;
       set_executable_recursive(&dest.join("binaries"))?;
   }

   Ok(())
}

Process Management

Service Lifecycle Manager

rust
// src-tauri/src/services/manager.rs

use std::process::{Child, Command};
use std::collections::HashMap;
use tokio::sync::RwLock;

pub struct ServiceManager {
   services: RwLock<HashMap<String, ServiceHandle>>,
   binary_manager: BinaryManager,
}

pub struct ServiceHandle {
   name: String,
   process: Child,
   port: Option<u16>,
   socket: Option<PathBuf>,
   status: ServiceStatus,
}

#[derive(Debug, Clone)]
pub enum ServiceStatus {
   Starting,
   Running,
   Stopping,
   Stopped,
   Failed(String),
}

impl ServiceManager {
   pub async fn start_mysql(&self, project: &Project) -> Result<()> {
       let mysql_bin = self.binary_manager.mysql_binary()?;
       let data_dir = project.data_dir().join("mysql");
       let socket = project.socket_dir().join("mysql.sock");

       // Ensure data directory exists
       std::fs::create_dir_all(&data_dir)?;

       // Start MySQL process
       let process = Command::new(mysql_bin)
           .arg("--datadir").arg(&data_dir)
           .arg("--socket").arg(&socket)
           .arg("--port").arg("3306")
           .arg("--skip-networking")  // Use socket only
           .spawn()?;

       let handle = ServiceHandle {
           name: "mysql".to_string(),
           process,
           port: None,
           socket: Some(socket.clone()),
           status: ServiceStatus::Starting,
       };

       self.services.write().await.insert("mysql".to_string(), handle);

       // Wait for socket to be created
       self.wait_for_socket(&socket, Duration::from_secs(30)).await?;

       // Update status
       if let Some(handle) = self.services.write().await.get_mut("mysql") {
           handle.status = ServiceStatus::Running;
       }

       log::info!("MySQL started at socket: {}", socket.display());
       Ok(())
   }

   pub async fn start_php_fpm(&self, project: &Project) -> Result<()> {
       let php_bin = self.binary_manager.php_binary()?;
       let php_fpm = php_bin.parent().unwrap().join("php-fpm");
       let socket = project.socket_dir().join("php-fpm.sock");
       let config = project.config_dir().join("php-fpm.conf");

       // Generate php-fpm.conf
       self.generate_php_fpm_config(project, &config, &socket)?;

       // Start PHP-FPM
       let process = Command::new(php_fpm)
           .arg("--fpm-config").arg(&config)
           .arg("--nodaemonize")  // Run in foreground
           .spawn()?;

       let handle = ServiceHandle {
           name: "php-fpm".to_string(),
           process,
           port: None,
           socket: Some(socket.clone()),
           status: ServiceStatus::Starting,
       };

       self.services.write().await.insert("php-fpm".to_string(), handle);

       // Wait for socket
       self.wait_for_socket(&socket, Duration::from_secs(10)).await?;

       // Update status
       if let Some(handle) = self.services.write().await.get_mut("php-fpm") {
           handle.status = ServiceStatus::Running;
       }

       log::info!("PHP-FPM started at socket: {}", socket.display());
       Ok(())
   }

   pub async fn start_nginx(&self, project: &Project) -> Result<()> {
       let nginx_bin = self.binary_manager.nginx_binary()?;
       let config = project.config_dir().join("nginx.conf");

       // Generate nginx.conf
       self.generate_nginx_config(project, &config)?;

       // Start Nginx
       let process = Command::new(nginx_bin)
           .arg("-c").arg(&config)
           .arg("-g").arg("daemon off;")  // Run in foreground
           .spawn()?;

       let handle = ServiceHandle {
           name: "nginx".to_string(),
           process,
           port: Some(8443),
           socket: None,
           status: ServiceStatus::Starting,
       };

       self.services.write().await.insert("nginx".to_string(), handle);

       // Wait for port
       self.wait_for_port(8443, Duration::from_secs(10)).await?;

       // Update status
       if let Some(handle) = self.services.write().await.get_mut("nginx") {
           handle.status = ServiceStatus::Running;
       }

       log::info!("Nginx started on https://localhost:8443");
       Ok(())
   }

   pub async fn start_qdrant(&self, project: &Project) -> Result<()> {
       let qdrant_bin = self.binary_manager.qdrant_binary()?;
       let storage = project.data_dir().join("qdrant");

       std::fs::create_dir_all(&storage)?;

       // Start Qdrant
       let process = Command::new(qdrant_bin)
           .arg("--storage-path").arg(&storage)
           .arg("--http-port").arg("6333")
           .env("QDRANT__SERVICE__GRPC_PORT", "6334")
           .spawn()?;

       let handle = ServiceHandle {
           name: "qdrant".to_string(),
           process,
           port: Some(6333),
           socket: None,
           status: ServiceStatus::Starting,
       };

       self.services.write().await.insert("qdrant".to_string(), handle);

       // Wait for port
       self.wait_for_port(6333, Duration::from_secs(20)).await?;

       // Update status
       if let Some(handle) = self.services.write().await.get_mut("qdrant") {
           handle.status = ServiceStatus::Running;
       }

       log::info!("Qdrant started on http://localhost:6333");
       Ok(())
   }

   pub async fn stop_all(&self) -> Result<()> {
       let mut services = self.services.write().await;

       for (name, handle) in services.iter_mut() {
           log::info!("Stopping service: {}", name);
           handle.status = ServiceStatus::Stopping;

           // Try graceful shutdown first
           #[cfg(unix)]
           {
               use nix::sys::signal::{kill, Signal};
               use nix::unistd::Pid;
               let _ = kill(Pid::from_raw(handle.process.id() as i32), Signal::SIGTERM);
           }

           #[cfg(windows)]
           {
               let _ = handle.process.kill();
           }

           // Wait up to 5 seconds
           match tokio::time::timeout(
               Duration::from_secs(5),
               wait_for_process_exit(&mut handle.process)
           ).await {
               Ok(_) => log::info!("Service {} stopped gracefully", name),
               Err(_) => {
                   log::warn!("Service {} did not stop, killing", name);
                   let _ = handle.process.kill();
               }
           }

           handle.status = ServiceStatus::Stopped;
       }

       services.clear();
       Ok(())
   }

   /// Health check for all services
   pub async fn health_check(&self) -> HashMap<String, ServiceStatus> {
       let services = self.services.read().await;
       services.iter()
           .map(|(name, handle)| (name.clone(), handle.status.clone()))
           .collect()
   }

   /// Restart a specific service
   pub async fn restart_service(&self, name: &str, project: &Project) -> Result<()> {
       // Stop service
       if let Some(mut handle) = self.services.write().await.remove(name) {
           let _ = handle.process.kill();
       }

       // Start service again
       match name {
           "mysql" => self.start_mysql(project).await,
           "php-fpm" => self.start_php_fpm(project).await,
           "nginx" => self.start_nginx(project).await,
           "qdrant" => self.start_qdrant(project).await,
           _ => Err(anyhow::anyhow!("Unknown service: {}", name)),
       }
   }
}

Process Supervision & Auto-Restart

rust
// src-tauri/src/services/supervisor.rs

pub struct ServiceSupervisor {
   manager: Arc<ServiceManager>,
   project: Arc<Project>,
   running: AtomicBool,
}

impl ServiceSupervisor {
   /// Start supervision loop
   pub async fn supervise(&self) {
       self.running.store(true, Ordering::SeqCst);

       while self.running.load(Ordering::SeqCst) {
           tokio::time::sleep(Duration::from_secs(5)).await;

           let health = self.manager.health_check().await;

           for (name, status) in health {
               match status {
                   ServiceStatus::Failed(err) => {
                       log::error!("Service {} failed: {}", name, err);
                       log::info!("Attempting to restart {}...", name);

                       if let Err(e) = self.manager.restart_service(&name, &self.project).await {
                           log::error!("Failed to restart {}: {}", name, e);
                       } else {
                           log::info!("Service {} restarted successfully", name);
                       }
                   }
                   ServiceStatus::Stopped => {
                       log::warn!("Service {} unexpectedly stopped, restarting", name);
                       let _ = self.manager.restart_service(&name, &self.project).await;
                   }
                   _ => {}
               }
           }
       }
   }

   pub fn stop(&self) {
       self.running.store(false, Ordering::SeqCst);
   }
}

MCP Server Implementation

TypeScript MCP Server (Node.js Sidecar)

typescript
// nodejs-app/src/server.ts

import { tool, createSdkMcpServer } from '@anthropic-ai/claude-agent-sdk';
import { QdrantClient } from '@qdrant/js-client-rest';
import { pipeline } from '@xenova/transformers';
import { z } from 'zod';

// Initialize Qdrant client
const qdrant = new QdrantClient({ url: 'http://localhost:6333' });

// Initialize embedding model (runs in Node.js, no Python needed!)
const embedder = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');

/**
 * Generate embedding for text using Transformers.js
 */
async function generateEmbedding(text: string): Promise<number[]> {
  const output = await embedder(text, { pooling: 'mean', normalize: true });
  return Array.from(output.data);
}

/**
 * Search codebase using vector similarity
 */
const searchCodebase = tool(
  'search_codebase',
  'Search the codebase using semantic search',
  z.object({
    query: z.string().describe('Search query'),
    project: z.string().optional().describe('Project name to filter by'),
    file_types: z.array(z.string()).optional().describe('File extensions to filter (php, js, css)'),
    component_type: z.string().optional().describe('Component type (function, class, block)'),
    limit: z.number().optional().default(10).describe('Maximum results'),
  }),
  async (args) => {
    // Generate embedding for query
    const embedding = await generateEmbedding(args.query);

    // Build filter conditions
    const filter: any = { must: [] };

    if (args.project) {
      filter.must.push({
        key: 'project',
        match: { value: args.project }
      });
    }

    if (args.component_type) {
      filter.must.push({
        key: 'component_type',
        match: { value: args.component_type }
      });
    }

    if (args.file_types && args.file_types.length > 0) {
      filter.should = args.file_types.map(ft => ({
        key: 'file_extension',
        match: { value: ft }
      }));
    }

    // Search Qdrant
    const results = await qdrant.search('wdg_framework', {
      vector: embedding,
      filter: Object.keys(filter.must).length > 0 ? filter : undefined,
      limit: args.limit,
      with_payload: true,
    });

    // Format results
    const formatted = results.map(result => ({
      file_path: result.payload?.file_path,
      component_name: result.payload?.name,
      component_type: result.payload?.component_type,
      content: result.payload?.content,
      score: result.score,
    }));

    return {
      content: [{
        type: 'text',
        text: JSON.stringify(formatted, null, 2)
      }]
    };
  }
);

/**
 * Search for specific functions
 */
const searchFunctions = tool(
  'search_functions',
  'Search for function definitions',
  z.object({
    function_name: z.string().describe('Function name to search for'),
    project: z.string().optional().describe('Project name to filter by'),
    language: z.string().optional().describe('Programming language (php, javascript)'),
  }),
  async (args) => {
    const filter: any = {
      must: [
        { key: 'component_type', match: { value: 'function' } },
        { key: 'name', match: { value: args.function_name } },
      ]
    };

    if (args.project) {
      filter.must.push({ key: 'project', match: { value: args.project } });
    }

    if (args.language) {
      filter.must.push({ key: 'language', match: { value: args.language } });
    }

    const results = await qdrant.scroll('wdg_framework', {
      filter,
      limit: 20,
      with_payload: true,
    });

    return {
      content: [{
        type: 'text',
        text: JSON.stringify(results.points, null, 2)
      }]
    };
  }
);

/**
 * Search for Wikit blocks
 */
const searchWikitBlocks = tool(
  'search_wikit_blocks',
  'Find available Wikit Gutenberg blocks',
  z.object({
    block_name: z.string().optional().describe('Block name to search for'),
  }),
  async (args) => {
    const filter: any = {
      must: [
        { key: 'component_type', match: { value: 'block' } },
        { key: 'framework', match: { value: 'wikit' } },
      ]
    };

    if (args.block_name) {
      filter.must.push({ key: 'name', match: { value: args.block_name } });
    }

    const results = await qdrant.scroll('wdg_framework', {
      filter,
      limit: 50,
      with_payload: true,
    });

    return {
      content: [{
        type: 'text',
        text: JSON.stringify(results.points, null, 2)
      }]
    };
  }
);

/**
 * Search for classes
 */
const searchClasses = tool(
  'search_classes',
  'Search for class definitions',
  z.object({
    class_name: z.string().describe('Class name to search for'),
    project: z.string().optional().describe('Project name to filter by'),
  }),
  async (args) => {
    const filter: any = {
      must: [
        { key: 'component_type', match: { value: 'class' } },
        { key: 'name', match: { value: args.class_name } },
      ]
    };

    if (args.project) {
      filter.must.push({ key: 'project', match: { value: args.project } });
    }

    const results = await qdrant.scroll('wdg_framework', {
      filter,
      limit: 20,
      with_payload: true,
    });

    return {
      content: [{
        type: 'text',
        text: JSON.stringify(results.points, null, 2)
      }]
    };
  }
);

// Create MCP server with all tools
export const wdgMcpServer = createSdkMcpServer({
  name: 'wdg-local',
  version: '1.0.0',
  tools: [
    searchCodebase,
    searchFunctions,
    searchWikitBlocks,
    searchClasses,
  ],
});

// Start server
console.log('WDG MCP Server started');

Node.js Sidecar Startup (Rust)

rust
// src-tauri/src/services/nodejs.rs

use std::process::{Child, Command};
use std::path::PathBuf;

pub struct NodeJsManager {
    process: Option<Child>,
    node_bin: PathBuf,
    app_path: PathBuf,
}

impl NodeJsManager {
    pub fn new(binary_manager: &BinaryManager, app_handle: &AppHandle) -> Result<Self> {
        let node_bin = binary_manager.node_binary()?;
        let app_path = app_handle
            .path_resolver()
            .resolve_resource("resources/nodejs-app/dist/server.js")
            .ok_or_else(|| anyhow::anyhow!("Node.js app not found"))?;

        Ok(Self {
            process: None,
            node_bin,
            app_path,
        })
    }

    pub fn start(&mut self) -> Result<()> {
        log::info!("Starting Node.js MCP server...");

        let process = Command::new(&self.node_bin)
            .arg(&self.app_path)
            .env("QDRANT_URL", "http://localhost:6333")
            .spawn()?;

        self.process = Some(process);
        log::info!("Node.js MCP server started (PID: {})", self.process.as_ref().unwrap().id());

        Ok(())
    }

    pub fn stop(&mut self) -> Result<()> {
        if let Some(mut process) = self.process.take() {
            log::info!("Stopping Node.js MCP server...");
            process.kill()?;
        }
        Ok(())
    }

    pub fn is_running(&self) -> bool {
        self.process.is_some()
    }
}

Agent SDK Integration

Agent SDK with MCP Server

typescript
// nodejs-app/src/agent.ts

import { Anthropic } from '@anthropic-ai/sdk';
import { query } from '@anthropic-ai/claude-agent-sdk';
import { wdgMcpServer } from './server.js';

export class WDGAgent {
  private apiKey: string;
  private project: string;

  constructor(apiKey: string, project: string) {
    this.apiKey = apiKey;
    this.project = project;
  }

  /**
   * Send message to Claude with MCP tool access
   */
  public async *sendMessage(message: string) {
    const stream = query({
      prompt: message,
      options: {
        apiKey: this.apiKey,
        model: 'claude-sonnet-4-20250514',
        mcpServers: {
          'wdg-local': {
            type: 'sdk',
            name: 'wdg-local',
            instance: wdgMcpServer,
          },
        },
        systemPrompt: `You are assisting with WordPress development using the Wikit framework.

Current project: ${this.project}

You have access to MCP tools to search the codebase, find Wikit blocks, and explore WordPress functions.`,
      },
    });

    // Stream responses
    for await (const message of stream) {
      yield message;
    }
  }
}

IPC Bridge (Rust → Node.js)

rust
// src-tauri/src/commands/agent.rs

use serde::{Deserialize, Serialize};
use std::process::Command;

#[derive(Serialize, Deserialize)]
pub struct AgentMessage {
    message: String,
    project: String,
}

#[derive(Serialize, Deserialize)]
pub struct AgentResponse {
    content: String,
    tool_uses: Vec<String>,
}

#[tauri::command]
pub async fn send_agent_message(
    message: String,
    project: String,
    state: tauri::State<'_, AppState>,
) -> Result<AgentResponse, String> {
    // Forward to Node.js sidecar via IPC
    // Node.js handles Agent SDK and MCP tools

    let nodejs_manager = state.nodejs_manager.lock().await;

    // Send message to Node.js process via stdin/stdout
    // Or use HTTP endpoint on localhost

    // For now, simplified version:
    let output = Command::new("curl")
        .arg("-X")
        .arg("POST")
        .arg("http://localhost:3001/chat")
        .arg("-H")
        .arg("Content-Type: application/json")
        .arg("-d")
        .arg(serde_json::to_string(&AgentMessage { message, project }).unwrap())
        .output()
        .map_err(|e| e.to_string())?;

    let response: AgentResponse = serde_json::from_slice(&output.stdout)
        .map_err(|e| e.to_string())?;

    Ok(response)
}

React Chat Component (shadcn/ui)

typescript
// src/components/AgentChat.tsx

import { invoke } from '@tauri-apps/api';
import { useState } from 'react';
import { Send, Loader2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { ScrollArea } from '@/components/ui/scroll-area';
import { cn } from '@/lib/utils';
import { MarkdownMessage } from '@/components/ui/markdown-message';

export function AgentChat({ project }: { project: string }) {
  const [messages, setMessages] = useState<Message[]>([]);
  const [input, setInput] = useState('');
  const [streaming, setStreaming] = useState(false);

  const sendMessage = async () => {
    if (!input.trim()) return;

    const userMessage: Message = {
      role: 'user',
      content: input,
      timestamp: new Date(),
    };

    setMessages(prev => [...prev, userMessage]);
    setInput('');
    setStreaming(true);

    try {
      // Call Tauri command, which forwards to Node.js sidecar
      const response = await invoke<AgentResponse>('send_agent_message', {
        message: input,
        project,
      });

      const assistantMessage: Message = {
        role: 'assistant',
        content: response.content,
        timestamp: new Date(),
        toolUses: response.tool_uses,
      };

      setMessages(prev => [...prev, assistantMessage]);
    } catch (error) {
      console.error('Agent error:', error);
      toast.error('Failed to send message');
    } finally {
      setStreaming(false);
    }
  };

  return (
    <Card className="flex flex-col h-full">
      <CardHeader>
        <CardTitle>AI Assistant - {project}</CardTitle>
      </CardHeader>

      <CardContent className="flex-1 p-0">
        <ScrollArea className="h-full px-4">
          {messages.map((msg, i) => (
            <div
              key={i}
              className={cn(
                "mb-4 p-3 rounded-lg",
                msg.role === 'user'
                  ? "bg-primary-10 dark:bg-white-10 ml-8"
                  : "bg-wdg-gray dark:bg-gray-800 mr-8"
              )}
            >
              <MarkdownMessage content={msg.content} />
              <span className="text-xs text-muted-foreground mt-2 block">
                {msg.timestamp.toLocaleTimeString()}
              </span>
            </div>
          ))}
        </ScrollArea>
      </CardContent>

      <CardFooter className="border-t p-4">
        <div className="flex w-full gap-2">
          <Input
            value={input}
            onChange={(e) => setInput(e.target.value)}
            onKeyPress={(e) => e.key === 'Enter' && !e.shiftKey && sendMessage()}
            disabled={streaming}
            placeholder="Ask about your WordPress project..."
            className="flex-1"
          />
          <Button
            onClick={sendMessage}
            disabled={streaming || !input.trim()}
            size="icon"
          >
            {streaming ? (
              <Loader2 className="h-4 w-4 animate-spin" />
            ) : (
              <Send className="h-4 w-4" />
            )}
          </Button>
        </div>
      </CardFooter>
    </Card>
  );
}

Data Architecture

File System Structure

~/WDG-Playground/                     # App data directory
binaries/                          # Extracted binaries (first run)
   php-8.2/
   mysql-8.0/
   nginx/
   qdrant/
projects/                          # User projects
   my-site/
       config/
          nginx.conf
          php-fpm.conf
          wp-config.php
       data/
          mysql/                 # MySQL data directory
          qdrant/                # Vector storage
       logs/
          nginx.log
          php-fpm.log
          mysql.log
       sockets/
          mysql.sock
          php-fpm.sock
       wp-content/                # WordPress content (git repo)
          themes/
          plugins/
          mu-plugins/
          uploads/
       backups/
       project.json               # Project metadata
       .git/                      # Project git repository
settings.json                      # App settings
sessions.db                        # Chat sessions (SQLite)
.extracted                         # First-run marker

SQLite Schema for Chat Sessions

sql
-- sessions.db

CREATE TABLE sessions (
   session_id TEXT PRIMARY KEY,
   project_name TEXT NOT NULL,
   created_at INTEGER NOT NULL,
   updated_at INTEGER NOT NULL,
   model TEXT NOT NULL,
   permission_mode TEXT NOT NULL
);

CREATE TABLE messages (
   id INTEGER PRIMARY KEY AUTOINCREMENT,
   session_id TEXT NOT NULL,
   role TEXT NOT NULL,  -- 'user' | 'assistant' | 'system'
   content TEXT NOT NULL,
   timestamp INTEGER NOT NULL,
   FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
);

CREATE TABLE tool_uses (
   id INTEGER PRIMARY KEY AUTOINCREMENT,
   session_id TEXT NOT NULL,
   message_id INTEGER NOT NULL,
   tool_name TEXT NOT NULL,
   input JSON NOT NULL,
   output JSON NOT NULL,
   timestamp INTEGER NOT NULL,
   FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE,
   FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE
);

CREATE INDEX idx_sessions_project ON sessions(project_name);
CREATE INDEX idx_messages_session ON messages(session_id);
CREATE INDEX idx_tool_uses_session ON tool_uses(session_id);

Security Architecture

Sandboxing & Permissions

Tauri Security:

  • IPC allowlist (only specific commands callable from frontend)
  • Content Security Policy (CSP) enforced
  • No eval() in frontend
  • Restricted file system access

Service Isolation:

  • MySQL: Socket-only, no network listener
  • PHP-FPM: Socket-only communication
  • Nginx: Binds to localhost only
  • Qdrant: Localhost binding only

API Key Storage

rust
// Secure storage using system keyring

use keyring::Entry;

pub struct SecureStorage;

impl SecureStorage {
   pub fn store_api_key(service: &str, key: &str) -> Result<()> {
       let entry = Entry::new("wdg-playground", service)?;
       entry.set_password(key)?;
       Ok(())
   }

   pub fn get_api_key(service: &str) -> Result<String> {
       let entry = Entry::new("wdg-playground", service)?;
       let password = entry.get_password()?;
       Ok(password)
   }
}

Code Signing

macOS:

  • Developer ID certificate required
  • Notarization via Apple
  • Hardened runtime enabled

Windows:

  • Code signing certificate (EV preferred)
  • SmartScreen reputation building

Performance Optimization

Startup Optimization

  1. Lazy Service Start - Only start services when project opened
  2. Parallel Initialization - Start PHP, MySQL, Nginx concurrently
  3. Binary Caching - Keep binaries in memory-mapped files
  4. Config Templates - Pre-generated configs, minimal substitution

Memory Optimization

  1. Process Limits - Cap MySQL/PHP memory usage
  2. Vector Quantization - Qdrant scalar quantization enabled
  3. Chat History Pruning - Keep last 50 messages, archive rest
  4. Resource Cleanup - Aggressively free resources on project close

Bundle Size Optimization

  1. Binary Stripping - Remove debug symbols from binaries
  2. Compression - UPX for binaries (where compatible)
  3. Asset Optimization - Compress images, minify JavaScript
  4. Tree Shaking - Remove unused code from bundles

Cross-Platform Considerations

Platform-Specific Code

rust
// Platform-specific implementations

#[cfg(target_os = "macos")]
mod macos {
   pub fn set_app_icon(window: &Window) {
       // macOS-specific icon handling
   }
}

#[cfg(target_os = "windows")]
mod windows {
   pub fn set_app_icon(window: &Window) {
       // Windows-specific icon handling
   }
}

#[cfg(target_os = "linux")]
mod linux {
   pub fn set_app_icon(window: &Window) {
       // Linux-specific icon handling
   }
}

Path Handling

rust
// Always use PathBuf for cross-platform paths

use std::path::PathBuf;

pub fn get_project_path(name: &str) -> PathBuf {
   let app_data = get_app_data_dir();
   app_data.join("projects").join(name)
}

Process Management Differences

Unix (macOS/Linux):

  • Signals: SIGTERM for graceful, SIGKILL for force
  • Fork/exec model
  • Socket files for IPC

Windows:

  • TerminateProcess for shutdown
  • Named pipes for IPC
  • Different service management

Build & Distribution

Build Configuration

toml
# src-tauri/Cargo.toml

[package]
name = "wdg-playground"
version = "1.0.0"
edition = "2021"

[dependencies]
tauri = { version = "2.0", features = ["shell-open", "dialog-all", "notification-all"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1", features = ["full"] }
anyhow = "1.0"
log = "0.4"
env_logger = "0.11"
git2 = "0.18"
rusqlite = { version = "0.30", features = ["bundled"] }
reqwest = { version = "0.11", features = ["json"] }
keyring = "2.0"

# Note: No Qdrant client needed - handled by Node.js sidecar

[build-dependencies]
tauri-build = { version = "2.0", features = [] }

Frontend Dependencies (package.json)

json
{
  "name": "wdg-playground",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview",
    "tauri": "tauri"
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "@tauri-apps/api": "^2.0.0",

    "lucide-react": "^0.298.0",
    "class-variance-authority": "^0.7.0",
    "clsx": "^2.0.0",
    "tailwind-merge": "^2.2.0",

    "react-markdown": "^9.0.0",
    "remark-gfm": "^4.0.0",
    "react-syntax-highlighter": "^15.5.0",
    "react-hot-toast": "^2.4.1"
  },
  "devDependencies": {
    "@types/react": "^18.2.0",
    "@types/react-dom": "^18.2.0",
    "@vitejs/plugin-react": "^4.2.0",
    "typescript": "^5.3.0",
    "vite": "^5.0.0",
    "tailwindcss": "^3.4.0",
    "autoprefixer": "^10.4.16",
    "postcss": "^8.4.32",
    "@tauri-apps/cli": "^2.0.0"
  }
}

shadcn/ui Setup

Installation:

bash
# Initialize shadcn/ui
npx shadcn-ui@latest init

# Add components matching dashboard
npx shadcn-ui@latest add button
npx shadcn-ui@latest add card
npx shadcn-ui@latest add input
npx shadcn-ui@latest add scroll-area
npx shadcn-ui@latest add dialog
npx shadcn-ui@latest add dropdown-menu
npx shadcn-ui@latest add tabs
npx shadcn-ui@latest add badge
npx shadcn-ui@latest add alert
npx shadcn-ui@latest add toast
npx shadcn-ui@latest add select
npx shadcn-ui@latest add table

Tailwind Configuration (matching dashboard):

javascript
// tailwind.config.js

/** @type {import('tailwindcss').Config} */
module.exports = {
  darkMode: ["class"],
  content: [
    './pages/**/*.{ts,tsx}',
    './components/**/*.{ts,tsx}',
    './app/**/*.{ts,tsx}',
    './src/**/*.{ts,tsx}',
  ],
  theme: {
    extend: {
      colors: {
        'tonal-black': '#1A1A1A',
        'wdg-gray': '#E5E7EB',
        'primary-5': 'rgba(26, 26, 26, 0.05)',
        'primary-10': 'rgba(26, 26, 26, 0.10)',
        'primary-20': 'rgba(26, 26, 26, 0.20)',
        'primary-30': 'rgba(26, 26, 26, 0.30)',
        'white-5': 'rgba(255, 255, 255, 0.05)',
        'white-10': 'rgba(255, 255, 255, 0.10)',
        'white-20': 'rgba(255, 255, 255, 0.20)',
        accent: {
          DEFAULT: 'hsl(var(--accent))',
          foreground: 'hsl(var(--accent-foreground))',
        },
      },
      fontFamily: {
        sans: ['Inter', 'system-ui', 'sans-serif'],
        mono: ['JetBrains Mono', 'monospace'],
        button: ['Inter', 'system-ui', 'sans-serif'],
      },
      borderRadius: {
        lg: 'var(--radius)',
        md: 'calc(var(--radius) - 2px)',
        sm: 'calc(var(--radius) - 4px)',
      },
    },
  },
  plugins: [require("tailwindcss-animate")],
}

CSS Variables (globals.css):

css
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  :root {
    --background: 0 0% 100%;
    --foreground: 0 0% 10%;
    --color-border-hover: #d1d5db;

    --card: 0 0% 100%;
    --card-foreground: 0 0% 10%;

    --primary: 0 0% 10%;
    --primary-foreground: 0 0% 100%;

    --accent: 210 100% 50%;
    --accent-foreground: 0 0% 100%;

    --muted: 0 0% 96.1%;
    --muted-foreground: 0 0% 45.1%;

    --radius: 0.5rem;
  }

  .dark {
    --background: 0 0% 10%;
    --foreground: 0 0% 90%;
    --color-border-hover: #4b5563;

    --card: 0 0% 10%;
    --card-foreground: 0 0% 90%;

    --primary: 0 0% 90%;
    --primary-foreground: 0 0% 10%;

    --muted: 0 0% 14.9%;
    --muted-foreground: 0 0% 63.9%;
  }
}

Build Scripts

macOS:

bash
#!/bin/bash
# Build for macOS (both architectures)

npm run tauri build -- --target universal-apple-darwin

# Sign and notarize
codesign --deep --force --verify --verbose --sign "Developer ID Application" \
 target/universal-apple-darwin/release/bundle/macos/WDG\ Playground.app

xcrun notarytool submit target/universal-apple-darwin/release/bundle/dmg/WDG\ Playground_1.0.0_universal.dmg \
 --apple-id "your-email@example.com" \
 --team-id "YOUR_TEAM_ID" \
 --password "app-specific-password" \
 --wait

Windows:

powershell
# Build for Windows

npm run tauri build

# Sign with certificate
signtool sign /f certificate.pfx /p password /t http://timestamp.digicert.com `
 target\release\bundle\nsis\WDG-Playground_1.0.0_x64-setup.exe

Testing Strategy

Unit Tests (Rust)

rust
#[cfg(test)]
mod tests {
   use super::*;

   #[tokio::test]
   async fn test_service_start_stop() {
       let manager = ServiceManager::new();
       let project = Project::mock();

       manager.start_mysql(&project).await.unwrap();
       assert!(manager.is_running("mysql").await);

       manager.stop_all().await.unwrap();
       assert!(!manager.is_running("mysql").await);
   }
}

Integration Tests

rust
#[tokio::test]
async fn test_full_project_lifecycle() {
   let app = TestApp::new();

   // Create project
   let project = app.create_project("test-site").await.unwrap();

   // Services should be running
   assert!(app.check_port(8443).await);
   assert!(app.check_socket("mysql.sock").await);

   // WordPress should respond
   let response = reqwest::get("https://localhost:8443").await.unwrap();
   assert_eq!(response.status(), 200);

   // Cleanup
   app.delete_project("test-site").await.unwrap();
}

Deployment & Updates

Auto-Update Configuration

json
{
 "tauri": {
   "updater": {
     "active": true,
     "endpoints": [
       "https://releases.wdg.dev/playground/{{target}}/{{current_version}}"
     ],
     "dialog": true,
     "pubkey": "PUBLIC_KEY_HERE"
   }
 }
}

Release Process

  1. Version Bump - Update version in Cargo.toml, package.json
  2. Build All Platforms - macOS, Windows, Linux
  3. Sign Binaries - Code signing certificates
  4. Generate Update Manifests - JSON with version info
  5. Upload to CDN - S3/CloudFlare
  6. Create GitHub Release - Tag and release notes
  7. Notify Users - In-app update notification

Document Status: Complete Technical Architecture Next Review: Before Phase 1 Implementation Related Docs: Implementation Roadmap, User Experience

Released under the MIT License.