WDG Playground - Technical Architecture
Comprehensive Technical Design DocumentVersion: 1.0 Last Updated: October 2025
Table of Contents
- Technology Stack
- System Architecture
- Embedded Binaries Architecture
- MCP Server Implementation
- Agent SDK Integration
- Process Management
- Data Architecture
- Security Architecture
- Performance Optimization
- 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_jsonApplication 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 + skillsSystem 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 # ~20MBBinary 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:
- macOS: Static build from https://php.net/distributions/ or compile with
--enable-static - Windows: Official PHP for Windows (https://windows.php.net/download/)
- Linux: Static build or AppImage-compatible binary
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:
- All platforms: From https://github.com/qdrant/qdrant/releases
- Configuration: Embedded mode, no clustering
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 markerSQLite 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
- Lazy Service Start - Only start services when project opened
- Parallel Initialization - Start PHP, MySQL, Nginx concurrently
- Binary Caching - Keep binaries in memory-mapped files
- Config Templates - Pre-generated configs, minimal substitution
Memory Optimization
- Process Limits - Cap MySQL/PHP memory usage
- Vector Quantization - Qdrant scalar quantization enabled
- Chat History Pruning - Keep last 50 messages, archive rest
- Resource Cleanup - Aggressively free resources on project close
Bundle Size Optimization
- Binary Stripping - Remove debug symbols from binaries
- Compression - UPX for binaries (where compatible)
- Asset Optimization - Compress images, minify JavaScript
- 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 tableTailwind 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" \
--waitWindows:
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.exeTesting 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
- Version Bump - Update version in Cargo.toml, package.json
- Build All Platforms - macOS, Windows, Linux
- Sign Binaries - Code signing certificates
- Generate Update Manifests - JSON with version info
- Upload to CDN - S3/CloudFlare
- Create GitHub Release - Tag and release notes
- Notify Users - In-app update notification
Document Status: Complete Technical Architecture Next Review: Before Phase 1 Implementation Related Docs: Implementation Roadmap, User Experience