# Dashboard Chat Architecture - Full Implementation Plan **Status**: Planning **Created**: 2025-01-08 **Target**: YouTube Launch Demo --- ## Executive Summary Transform the Atomizer dashboard chat from a stateless CLI wrapper into a full-featured, MCP-powered Claude Code experience with two modes: - **User Mode**: Safe, product-like experience for running optimizations - **Power Mode**: Full Claude Code capabilities for extending Atomizer --- ## Architecture Overview ``` ┌──────────────────────────────────────────────────────────────────────────┐ │ FRONTEND (React) │ │ ┌────────────────────────────────────────────────────────────────────┐ │ │ │ ChatPane │ │ │ │ ┌─────────────┐ ┌─────────────┐ │ │ │ │ │ 👤 User │ │ ⚡ Power │ Mode Toggle │ │ │ │ └─────────────┘ └─────────────┘ │ │ │ │ │ │ │ │ Messages Area (streaming) │ │ │ │ Tool Call Visualizations │ │ │ │ Input with suggestions │ │ │ └────────────────────────────────────────────────────────────────────┘ │ │ │ │ │ │ WebSocket │ │ ▼ │ ├──────────────────────────────────────────────────────────────────────────┤ │ BACKEND (FastAPI) │ │ ┌────────────────────────────────────────────────────────────────────┐ │ │ │ SessionManager │ │ │ │ - Session lifecycle (create/resume/destroy) │ │ │ │ - Mode enforcement (user/power) │ │ │ │ - Conversation persistence (SQLite) │ │ │ │ - Context injection (study, global) │ │ │ └────────────────────────────────────────────────────────────────────┘ │ │ │ │ │ │ Subprocess │ │ ▼ │ ├──────────────────────────────────────────────────────────────────────────┤ │ CLAUDE CODE (subprocess) │ │ ┌────────────────────────────────────────────────────────────────────┐ │ │ │ claude --mcp-config │ │ │ │ │ │ │ │ Configured with: │ │ │ │ - Atomizer MCP Server (tools) │ │ │ │ - System prompt (context) │ │ │ │ - Allowed directories │ │ │ └────────────────────────────────────────────────────────────────────┘ │ │ │ │ │ │ MCP Protocol │ │ ▼ │ ├──────────────────────────────────────────────────────────────────────────┤ │ ATOMIZER MCP SERVER (Node.js) │ │ ┌────────────────────────────────────────────────────────────────────┐ │ │ │ USER MODE TOOLS │ POWER MODE TOOLS (additional) │ │ │ │ ─────────────────────────────┼────────────────────────────────── │ │ │ │ create_study │ edit_file │ │ │ │ list_studies │ create_extractor │ │ │ │ get_study_status │ modify_protocol │ │ │ │ run_optimization │ run_shell_command │ │ │ │ stop_optimization │ git_commit │ │ │ │ get_trial_data │ git_push │ │ │ │ analyze_convergence │ create_file │ │ │ │ compare_trials │ delete_file │ │ │ │ generate_report │ search_codebase │ │ │ │ get_best_design │ modify_dashboard │ │ │ │ explain_physics │ │ │ │ │ recommend_method │ │ │ │ │ query_database │ │ │ │ └────────────────────────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────────────────────┘ ``` --- ## Directory Structure ``` atomizer-dashboard/ ├── backend/ │ ├── api/ │ │ ├── services/ │ │ │ ├── session_manager.py # NEW: Manages Claude sessions │ │ │ ├── conversation_store.py # NEW: SQLite persistence │ │ │ ├── context_builder.py # NEW: Builds prompts/context │ │ │ └── claude_cli_agent.py # DEPRECATED: Old stateless approach │ │ └── routes/ │ │ └── claude.py # UPDATE: Use SessionManager │ └── sessions.db # NEW: Conversation storage │ ├── frontend/ │ └── src/ │ ├── components/ │ │ └── chat/ │ │ ├── ChatPane.tsx # UPDATE: Mode toggle, tool viz │ │ ├── ChatMessage.tsx # UPDATE: Tool call rendering │ │ ├── ModeToggle.tsx # NEW: User/Power mode switch │ │ └── ToolCallCard.tsx # NEW: Visualize tool calls │ └── hooks/ │ └── useChat.ts # UPDATE: WebSocket, sessions │ └── mcp-server/ # EXISTING (siemens-docs) └── atomizer-tools/ # NEW: Atomizer MCP server ├── package.json ├── tsconfig.json ├── src/ │ ├── index.ts # MCP server entry │ ├── tools/ │ │ ├── study.ts # Study management tools │ │ ├── optimization.ts # Run/stop/monitor tools │ │ ├── analysis.ts # Results analysis tools │ │ ├── reporting.ts # Report generation tools │ │ ├── physics.ts # FEA explanation tools │ │ └── admin.ts # Power mode tools │ └── utils/ │ ├── database.ts # SQLite access │ ├── python.ts # Call Python scripts │ └── validation.ts # Input validation └── README.md ``` --- ## Phase 1: MCP Server Foundation ### 1.1 Create MCP Server Scaffold **Files to create:** - `mcp-server/atomizer-tools/package.json` - `mcp-server/atomizer-tools/tsconfig.json` - `mcp-server/atomizer-tools/src/index.ts` **Package.json:** ```json { "name": "atomizer-mcp-server", "version": "1.0.0", "type": "module", "main": "dist/index.js", "scripts": { "build": "tsc", "start": "node dist/index.js", "dev": "tsx src/index.ts" }, "dependencies": { "@anthropic-ai/sdk": "^0.30.0", "@modelcontextprotocol/sdk": "^1.0.0", "better-sqlite3": "^11.0.0", "zod": "^3.23.0" }, "devDependencies": { "@types/better-sqlite3": "^7.6.0", "@types/node": "^20.0.0", "tsx": "^4.0.0", "typescript": "^5.0.0" } } ``` **Index.ts structure:** ```typescript import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; // Tool imports import { studyTools } from "./tools/study.js"; import { optimizationTools } from "./tools/optimization.js"; import { analysisTools } from "./tools/analysis.js"; import { reportingTools } from "./tools/reporting.js"; import { physicsTools } from "./tools/physics.js"; import { adminTools } from "./tools/admin.js"; const MODE = process.env.ATOMIZER_MODE || "user"; // "user" or "power" const server = new Server({ name: "atomizer-tools", version: "1.0.0", }); // Register tools based on mode const userTools = [ ...studyTools, ...optimizationTools, ...analysisTools, ...reportingTools, ...physicsTools, ]; const powerTools = [ ...userTools, ...adminTools, ]; const tools = MODE === "power" ? powerTools : userTools; // Register all tools server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: tools.map(t => t.definition), })); server.setRequestHandler(CallToolRequestSchema, async (request) => { const tool = tools.find(t => t.definition.name === request.params.name); if (!tool) throw new Error(`Unknown tool: ${request.params.name}`); return tool.handler(request.params.arguments); }); // Start server const transport = new StdioServerTransport(); await server.connect(transport); ``` ### 1.2 Implement Core User Mode Tools **Priority order:** 1. `list_studies` - List available studies 2. `get_study_status` - Get current study state 3. `create_study` - Create new study from description 4. `run_optimization` - Start optimization 5. `get_trial_data` - Query trial results 6. `analyze_convergence` - Convergence analysis **Tool Definition Template:** ```typescript // tools/study.ts import { z } from "zod"; import { execSync } from "child_process"; import Database from "better-sqlite3"; const STUDIES_DIR = "C:/Users/antoi/Atomizer/studies"; const PYTHON_PATH = "C:/Users/antoi/anaconda3/envs/atomizer/python.exe"; export const studyTools = [ { definition: { name: "list_studies", description: "List all available optimization studies", inputSchema: { type: "object", properties: { filter: { type: "string", description: "Optional filter pattern (e.g., 'bracket', 'mirror')", }, }, }, }, handler: async (args: { filter?: string }) => { // Implementation: Read studies directory, return list const fs = await import("fs/promises"); const studies = await fs.readdir(STUDIES_DIR); let filtered = studies.filter(s => !s.startsWith("_")); if (args.filter) { filtered = filtered.filter(s => s.toLowerCase().includes(args.filter!.toLowerCase()) ); } // Get status for each study const results = await Promise.all(filtered.map(async (name) => { const configPath = `${STUDIES_DIR}/${name}/optimization_config.json`; const dbPath = `${STUDIES_DIR}/${name}/3_results/study.db`; let status = "unknown"; let trials = 0; try { const db = new Database(dbPath, { readonly: true }); const row = db.prepare("SELECT COUNT(*) as count FROM trials").get(); trials = row.count; status = trials > 0 ? "has_results" : "configured"; db.close(); } catch { status = "no_results"; } return { name, status, trials }; })); return { content: [{ type: "text", text: JSON.stringify(results, null, 2), }], }; }, }, { definition: { name: "create_study", description: "Create a new optimization study from a natural language description", inputSchema: { type: "object", properties: { name: { type: "string", description: "Study name (snake_case, e.g., 'bracket_mass_v1')", }, description: { type: "string", description: "Natural language description of what to optimize", }, template: { type: "string", enum: ["bracket", "beam", "mirror", "custom"], description: "Base template to use", }, nx_model_path: { type: "string", description: "Path to the NX model file (.prt)", }, }, required: ["name", "description"], }, }, handler: async (args) => { // Call Python study creator const script = ` from optimization_engine.study.creator import StudyCreator creator = StudyCreator() result = creator.create_from_description( name="${args.name}", description="""${args.description}""", template="${args.template || 'custom'}", nx_model_path=${args.nx_model_path ? `"${args.nx_model_path}"` : 'None'} ) print(result) `; try { const output = execSync(`${PYTHON_PATH} -c "${script}"`, { encoding: "utf-8", cwd: STUDIES_DIR, }); return { content: [{ type: "text", text: `Study created successfully!\n\n${output}`, }], }; } catch (error) { return { content: [{ type: "text", text: `Error creating study: ${error.message}`, }], isError: true, }; } }, }, // ... more tools ]; ``` ### 1.3 Implement Analysis Tools ```typescript // tools/analysis.ts export const analysisTools = [ { definition: { name: "get_trial_data", description: "Query trial data from a study's database", inputSchema: { type: "object", properties: { study_name: { type: "string", description: "Study name" }, query: { type: "string", enum: ["all", "best", "pareto", "recent", "failed"], description: "Type of query", }, limit: { type: "number", description: "Max results to return" }, objective: { type: "string", description: "Objective to sort by" }, }, required: ["study_name", "query"], }, }, handler: async (args) => { const dbPath = `${STUDIES_DIR}/${args.study_name}/3_results/study.db`; const db = new Database(dbPath, { readonly: true }); let sql: string; switch (args.query) { case "best": sql = ` SELECT t.number, tv.value as objective_value, GROUP_CONCAT(tp.key || '=' || tp.value) as params FROM trials t JOIN trial_values tv ON t.trial_id = tv.trial_id JOIN trial_params tp ON t.trial_id = tp.trial_id WHERE t.state = 'COMPLETE' GROUP BY t.trial_id ORDER BY tv.value ASC LIMIT ${args.limit || 10} `; break; // ... other query types } const results = db.prepare(sql).all(); db.close(); return { content: [{ type: "text", text: JSON.stringify(results, null, 2), }], }; }, }, { definition: { name: "analyze_convergence", description: "Analyze optimization convergence and provide insights", inputSchema: { type: "object", properties: { study_name: { type: "string" }, window_size: { type: "number", description: "Rolling window for analysis" }, }, required: ["study_name"], }, }, handler: async (args) => { // Call Python analysis script const script = ` from optimization_engine.reporting.convergence_analyzer import analyze_convergence result = analyze_convergence("${args.study_name}", window=${args.window_size || 10}) import json print(json.dumps(result, indent=2)) `; const output = execSync(`${PYTHON_PATH} -c '${script}'`, { encoding: "utf-8" }); return { content: [{ type: "text", text: output }] }; }, }, ]; ``` ### 1.4 Implement Power Mode Tools ```typescript // tools/admin.ts export const adminTools = [ { definition: { name: "edit_file", description: "Edit a file in the Atomizer codebase", inputSchema: { type: "object", properties: { file_path: { type: "string", description: "Relative path from Atomizer root" }, old_content: { type: "string", description: "Content to replace" }, new_content: { type: "string", description: "New content" }, }, required: ["file_path", "old_content", "new_content"], }, }, handler: async (args) => { const fs = await import("fs/promises"); const fullPath = `C:/Users/antoi/Atomizer/${args.file_path}`; const content = await fs.readFile(fullPath, "utf-8"); if (!content.includes(args.old_content)) { return { content: [{ type: "text", text: "Error: old_content not found in file" }], isError: true, }; } const newContent = content.replace(args.old_content, args.new_content); await fs.writeFile(fullPath, newContent); return { content: [{ type: "text", text: `File updated: ${args.file_path}` }], }; }, }, { definition: { name: "run_shell_command", description: "Run a shell command (Power Mode only)", inputSchema: { type: "object", properties: { command: { type: "string" }, cwd: { type: "string", description: "Working directory" }, }, required: ["command"], }, }, handler: async (args) => { const output = execSync(args.command, { encoding: "utf-8", cwd: args.cwd || "C:/Users/antoi/Atomizer", }); return { content: [{ type: "text", text: output }] }; }, }, { definition: { name: "create_extractor", description: "Create a new physics extractor following Atomizer conventions", inputSchema: { type: "object", properties: { name: { type: "string", description: "Extractor name (e.g., 'thermal_gradient')" }, physics_type: { type: "string", enum: ["displacement", "stress", "frequency", "thermal", "custom"], }, description: { type: "string" }, }, required: ["name", "physics_type", "description"], }, }, handler: async (args) => { // Generate extractor from template const template = `\"\"\" ${args.name} Extractor Auto-generated extractor for ${args.physics_type} physics. ${args.description} \"\"\" from pathlib import Path from typing import Dict, Any, Optional import numpy as np def extract_${args.name}( op2_path: Path, config: Optional[Dict[str, Any]] = None ) -> Dict[str, Any]: \"\"\" Extract ${args.name} from OP2 results. Args: op2_path: Path to the OP2 file config: Optional configuration dict Returns: Dict with extracted values \"\"\" from pyNastran.op2.op2 import OP2 op2 = OP2() op2.read_op2(str(op2_path)) # TODO: Implement extraction logic result = { "value": 0.0, "units": "TODO", "metadata": {} } return result `; const fs = await import("fs/promises"); const path = `C:/Users/antoi/Atomizer/optimization_engine/extractors/extract_${args.name}.py`; await fs.writeFile(path, template); // Update __init__.py const initPath = "C:/Users/antoi/Atomizer/optimization_engine/extractors/__init__.py"; const initContent = await fs.readFile(initPath, "utf-8"); const newImport = `from .extract_${args.name} import extract_${args.name}\n`; await fs.writeFile(initPath, newImport + initContent); return { content: [{ type: "text", text: `Created extractor: ${path}\nUpdated __init__.py\n\nNext steps:\n1. Implement the extraction logic\n2. Add to SYS_12_EXTRACTOR_LIBRARY.md\n3. Test with a sample OP2 file`, }], }; }, }, ]; ``` --- ## Phase 2: Backend Session Manager ### 2.1 Create Session Manager **File:** `backend/api/services/session_manager.py` ```python """ Claude Code Session Manager Manages persistent Claude Code sessions with MCP integration. """ import asyncio import json import os import uuid from dataclasses import dataclass, field from datetime import datetime from pathlib import Path from typing import AsyncGenerator, Dict, List, Literal, Optional from .conversation_store import ConversationStore from .context_builder import ContextBuilder ATOMIZER_ROOT = Path(__file__).parent.parent.parent.parent.parent MCP_SERVER_PATH = ATOMIZER_ROOT / "mcp-server" / "atomizer-tools" @dataclass class ClaudeSession: """Represents an active Claude Code session""" session_id: str mode: Literal["user", "power"] study_id: Optional[str] process: Optional[asyncio.subprocess.Process] = None created_at: datetime = field(default_factory=datetime.now) last_active: datetime = field(default_factory=datetime.now) def is_alive(self) -> bool: return self.process is not None and self.process.returncode is None class SessionManager: """Manages Claude Code sessions with MCP tools""" def __init__(self): self.sessions: Dict[str, ClaudeSession] = {} self.store = ConversationStore() self.context_builder = ContextBuilder() self._cleanup_task: Optional[asyncio.Task] = None async def start(self): """Start the session manager""" # Start periodic cleanup of stale sessions self._cleanup_task = asyncio.create_task(self._cleanup_loop()) async def stop(self): """Stop the session manager""" if self._cleanup_task: self._cleanup_task.cancel() # Terminate all sessions for session in self.sessions.values(): await self._terminate_session(session) async def create_session( self, mode: Literal["user", "power"] = "user", study_id: Optional[str] = None, resume_session_id: Optional[str] = None, ) -> ClaudeSession: """ Create or resume a Claude Code session. Args: mode: "user" for safe mode, "power" for full access study_id: Optional study context resume_session_id: Optional session ID to resume Returns: ClaudeSession object """ # Resume existing session if requested if resume_session_id and resume_session_id in self.sessions: session = self.sessions[resume_session_id] if session.is_alive(): session.last_active = datetime.now() return session session_id = resume_session_id or str(uuid.uuid4())[:8] # Build MCP config for this session mcp_config = self._build_mcp_config(mode) mcp_config_path = ATOMIZER_ROOT / f".claude-mcp-{session_id}.json" with open(mcp_config_path, "w") as f: json.dump(mcp_config, f) # Build system prompt with context system_prompt = self.context_builder.build( mode=mode, study_id=study_id, conversation_history=self.store.get_history(session_id) if resume_session_id else [], ) # Write system prompt to temp file prompt_path = ATOMIZER_ROOT / f".claude-prompt-{session_id}.md" with open(prompt_path, "w") as f: f.write(system_prompt) # Start Claude Code subprocess # Using --mcp-config to load our MCP server # Using --system-prompt to set context process = await asyncio.create_subprocess_exec( "claude", "--mcp-config", str(mcp_config_path), "--system-prompt", str(prompt_path), "--output-format", "stream-json", # For structured streaming stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, cwd=str(ATOMIZER_ROOT), env={ **os.environ, "ATOMIZER_MODE": mode, "ATOMIZER_STUDY": study_id or "", }, ) session = ClaudeSession( session_id=session_id, mode=mode, study_id=study_id, process=process, ) self.sessions[session_id] = session return session async def send_message( self, session_id: str, message: str, ) -> AsyncGenerator[Dict, None]: """ Send a message to a session and stream the response. Args: session_id: Session ID message: User message Yields: Response chunks (text, tool_calls, etc.) """ session = self.sessions.get(session_id) if not session or not session.is_alive(): yield {"type": "error", "message": "Session not found or expired"} return session.last_active = datetime.now() # Store user message self.store.add_message(session_id, "user", message) # Send message to Claude session.process.stdin.write(f"{message}\n".encode()) await session.process.stdin.drain() # Stream response full_response = "" tool_calls = [] while True: line = await session.process.stdout.readline() if not line: break try: data = json.loads(line.decode()) if data.get("type") == "text": full_response += data.get("content", "") yield {"type": "text", "content": data.get("content", "")} elif data.get("type") == "tool_use": tool_calls.append(data) yield {"type": "tool_call", "tool": data} elif data.get("type") == "tool_result": yield {"type": "tool_result", "result": data} elif data.get("type") == "done": break except json.JSONDecodeError: # Raw text output text = line.decode() full_response += text yield {"type": "text", "content": text} # Store assistant response self.store.add_message( session_id, "assistant", full_response, tool_calls=tool_calls, ) yield {"type": "done", "tool_calls": tool_calls} async def switch_mode( self, session_id: str, new_mode: Literal["user", "power"], ) -> ClaudeSession: """Switch a session's mode (requires restart)""" session = self.sessions.get(session_id) if not session: raise ValueError(f"Session {session_id} not found") # Terminate existing session await self._terminate_session(session) # Create new session with same ID but different mode return await self.create_session( mode=new_mode, study_id=session.study_id, resume_session_id=session_id, ) async def set_study_context( self, session_id: str, study_id: str, ): """Update the study context for a session""" session = self.sessions.get(session_id) if session: session.study_id = study_id # Send context update command to Claude if session.is_alive(): context_update = self.context_builder.build_study_context(study_id) session.process.stdin.write( f"[CONTEXT UPDATE] Study changed to: {study_id}\n{context_update}\n".encode() ) await session.process.stdin.drain() def get_session(self, session_id: str) -> Optional[ClaudeSession]: """Get session by ID""" return self.sessions.get(session_id) def _build_mcp_config(self, mode: Literal["user", "power"]) -> dict: """Build MCP configuration for Claude""" return { "mcpServers": { "atomizer": { "command": "node", "args": [str(MCP_SERVER_PATH / "dist" / "index.js")], "env": { "ATOMIZER_MODE": mode, "ATOMIZER_ROOT": str(ATOMIZER_ROOT), }, }, # Keep existing siemens-docs server "siemens-docs": { "command": "node", "args": [str(ATOMIZER_ROOT / "mcp-server" / "dist" / "index.js")], }, }, } async def _terminate_session(self, session: ClaudeSession): """Terminate a Claude session""" if session.process and session.is_alive(): session.process.terminate() try: await asyncio.wait_for(session.process.wait(), timeout=5.0) except asyncio.TimeoutError: session.process.kill() # Clean up temp files for pattern in [f".claude-mcp-{session.session_id}.json", f".claude-prompt-{session.session_id}.md"]: path = ATOMIZER_ROOT / pattern if path.exists(): path.unlink() async def _cleanup_loop(self): """Periodically clean up stale sessions""" while True: await asyncio.sleep(300) # Every 5 minutes now = datetime.now() stale = [ sid for sid, session in self.sessions.items() if (now - session.last_active).total_seconds() > 3600 # 1 hour ] for sid in stale: session = self.sessions.pop(sid, None) if session: await self._terminate_session(session) ``` ### 2.2 Create Conversation Store **File:** `backend/api/services/conversation_store.py` ```python """ Conversation persistence using SQLite. """ import json import sqlite3 from datetime import datetime from pathlib import Path from typing import Any, Dict, List, Optional DB_PATH = Path(__file__).parent.parent.parent / "sessions.db" class ConversationStore: """SQLite-backed conversation storage""" def __init__(self, db_path: Path = DB_PATH): self.db_path = db_path self._init_db() def _init_db(self): """Initialize database schema""" conn = sqlite3.connect(self.db_path) conn.execute(""" CREATE TABLE IF NOT EXISTS sessions ( session_id TEXT PRIMARY KEY, mode TEXT NOT NULL, study_id TEXT, created_at TEXT NOT NULL, last_active TEXT NOT NULL ) """) conn.execute(""" CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, role TEXT NOT NULL, content TEXT NOT NULL, tool_calls TEXT, timestamp TEXT NOT NULL, FOREIGN KEY (session_id) REFERENCES sessions(session_id) ) """) conn.execute(""" CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id) """) conn.commit() conn.close() def create_session( self, session_id: str, mode: str, study_id: Optional[str] = None, ): """Create a new session record""" conn = sqlite3.connect(self.db_path) now = datetime.now().isoformat() conn.execute( "INSERT INTO sessions VALUES (?, ?, ?, ?, ?)", (session_id, mode, study_id, now, now), ) conn.commit() conn.close() def add_message( self, session_id: str, role: str, content: str, tool_calls: Optional[List[Dict]] = None, ): """Add a message to a session""" conn = sqlite3.connect(self.db_path) conn.execute( "INSERT INTO messages (session_id, role, content, tool_calls, timestamp) VALUES (?, ?, ?, ?, ?)", ( session_id, role, content, json.dumps(tool_calls) if tool_calls else None, datetime.now().isoformat(), ), ) conn.execute( "UPDATE sessions SET last_active = ? WHERE session_id = ?", (datetime.now().isoformat(), session_id), ) conn.commit() conn.close() def get_history( self, session_id: str, limit: int = 50, ) -> List[Dict[str, Any]]: """Get conversation history for a session""" conn = sqlite3.connect(self.db_path) conn.row_factory = sqlite3.Row cursor = conn.execute( """ SELECT role, content, tool_calls, timestamp FROM messages WHERE session_id = ? ORDER BY id DESC LIMIT ? """, (session_id, limit), ) rows = cursor.fetchall() conn.close() messages = [] for row in reversed(rows): msg = { "role": row["role"], "content": row["content"], "timestamp": row["timestamp"], } if row["tool_calls"]: msg["tool_calls"] = json.loads(row["tool_calls"]) messages.append(msg) return messages def get_session_info(self, session_id: str) -> Optional[Dict]: """Get session metadata""" conn = sqlite3.connect(self.db_path) conn.row_factory = sqlite3.Row cursor = conn.execute( "SELECT * FROM sessions WHERE session_id = ?", (session_id,), ) row = cursor.fetchone() conn.close() if row: return dict(row) return None ``` ### 2.3 Create Context Builder **File:** `backend/api/services/context_builder.py` ```python """ Context builder for Claude sessions. Builds rich context from Atomizer knowledge for Claude prompts. """ import json from pathlib import Path from typing import List, Literal, Optional ATOMIZER_ROOT = Path(__file__).parent.parent.parent.parent.parent class ContextBuilder: """Builds context prompts for Claude sessions""" def build( self, mode: Literal["user", "power"], study_id: Optional[str] = None, conversation_history: Optional[List[dict]] = None, ) -> str: """Build full system prompt with context""" parts = [self._base_context(mode)] if study_id: parts.append(self._study_context(study_id)) else: parts.append(self._global_context()) if conversation_history: parts.append(self._conversation_context(conversation_history)) parts.append(self._mode_instructions(mode)) return "\n\n---\n\n".join(parts) def _base_context(self, mode: str) -> str: """Base identity and capabilities""" return f"""# Atomizer Assistant You are the Atomizer Assistant - an expert system for structural optimization using FEA. **Current Mode**: {mode.upper()} Your role: - Help engineers with FEA optimization workflows - Create, configure, and run optimization studies - Analyze results and provide insights - Explain FEA concepts and methodology Important: - Be concise and professional - Use technical language appropriate for engineers - You are "Atomizer Assistant", not an AI or Claude - Use the available tools to perform actions """ def _study_context(self, study_id: str) -> str: """Context for a specific study""" study_dir = ATOMIZER_ROOT / "studies" / study_id context = f"# Current Study: {study_id}\n\n" # Load config config_path = study_dir / "1_setup" / "optimization_config.json" if not config_path.exists(): config_path = study_dir / "optimization_config.json" if config_path.exists(): with open(config_path) as f: config = json.load(f) context += "## Configuration\n\n" dvs = config.get("design_variables", []) if dvs: context += "**Design Variables:**\n" for dv in dvs[:10]: context += f"- {dv['name']}: [{dv.get('lower', '?')}, {dv.get('upper', '?')}]\n" if len(dvs) > 10: context += f"- ... and {len(dvs) - 10} more\n" objs = config.get("objectives", []) if objs: context += "\n**Objectives:**\n" for obj in objs: direction = "minimize" if obj.get("direction") == "minimize" else "maximize" context += f"- {obj['name']} ({direction})\n" constraints = config.get("constraints", []) if constraints: context += "\n**Constraints:**\n" for c in constraints: context += f"- {c['name']}: {c.get('type', 'unknown')}\n" # Check for results db_path = study_dir / "3_results" / "study.db" if db_path.exists(): import sqlite3 conn = sqlite3.connect(db_path) try: count = conn.execute("SELECT COUNT(*) FROM trials").fetchone()[0] best = conn.execute(""" SELECT MIN(tv.value) FROM trial_values tv JOIN trials t ON tv.trial_id = t.trial_id WHERE t.state = 'COMPLETE' """).fetchone()[0] context += f"\n## Status\n\n" context += f"- **Trials completed**: {count}\n" if best is not None: context += f"- **Best objective**: {best:.4f}\n" except: pass finally: conn.close() return context def _global_context(self) -> str: """Context when no study is selected""" studies_dir = ATOMIZER_ROOT / "studies" context = "# Available Studies\n\n" if studies_dir.exists(): studies = [d.name for d in studies_dir.iterdir() if d.is_dir() and not d.name.startswith("_")] if studies: context += "The following studies are available:\n\n" for name in sorted(studies)[:20]: context += f"- {name}\n" if len(studies) > 20: context += f"\n... and {len(studies) - 20} more\n" else: context += "No studies found. Use `create_study` to create one.\n" context += "\n## Quick Actions\n\n" context += "- Create a new study: Describe what you want to optimize\n" context += "- List studies: Ask to see available studies\n" context += "- Open a study: Say the study name to get its details\n" return context def _conversation_context(self, history: List[dict]) -> str: """Recent conversation for continuity""" if not history: return "" context = "# Recent Conversation\n\n" for msg in history[-10:]: role = "User" if msg["role"] == "user" else "Assistant" content = msg["content"][:500] # Truncate long messages if len(msg["content"]) > 500: content += "..." context += f"**{role}**: {content}\n\n" return context def _mode_instructions(self, mode: str) -> str: """Mode-specific instructions""" if mode == "power": return """# Power Mode Instructions You have full access to Atomizer's codebase. You can: - Edit any file - Create new extractors and protocols - Run shell commands - Modify the dashboard code - Commit and push changes Use these powers responsibly. Always explain what you're doing. """ else: return """# User Mode Instructions You can help with optimization workflows: - Create and configure studies - Run optimizations - Analyze results - Generate reports For code modifications, suggest switching to Power Mode. """ def build_study_context(self, study_id: str) -> str: """Build just the study context (for updates)""" return self._study_context(study_id) ``` ### 2.4 Update API Routes **File:** `backend/api/routes/claude.py` (UPDATE) ```python """ Claude Chat API Routes - Updated for Session Manager """ from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect from pydantic import BaseModel from typing import Optional, List, Dict, Any, Literal import json router = APIRouter() # Lazy import to avoid circular dependencies _session_manager = None def get_session_manager(): global _session_manager if _session_manager is None: from api.services.session_manager import SessionManager _session_manager = SessionManager() return _session_manager class CreateSessionRequest(BaseModel): mode: Literal["user", "power"] = "user" study_id: Optional[str] = None resume_session_id: Optional[str] = None class SendMessageRequest(BaseModel): session_id: str message: str class SwitchModeRequest(BaseModel): session_id: str mode: Literal["user", "power"] @router.post("/sessions") async def create_session(request: CreateSessionRequest): """Create or resume a Claude session""" try: manager = get_session_manager() session = await manager.create_session( mode=request.mode, study_id=request.study_id, resume_session_id=request.resume_session_id, ) return { "session_id": session.session_id, "mode": session.mode, "study_id": session.study_id, } except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.post("/sessions/{session_id}/mode") async def switch_mode(session_id: str, request: SwitchModeRequest): """Switch session mode""" try: manager = get_session_manager() session = await manager.switch_mode(session_id, request.mode) return { "session_id": session.session_id, "mode": session.mode, } except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.websocket("/sessions/{session_id}/ws") async def session_websocket(websocket: WebSocket, session_id: str): """WebSocket for real-time chat with a session""" await websocket.accept() manager = get_session_manager() session = manager.get_session(session_id) if not session: await websocket.send_json({"type": "error", "message": "Session not found"}) await websocket.close() return try: while True: data = await websocket.receive_json() if data.get("type") == "message": message = data.get("content", "") async for chunk in manager.send_message(session_id, message): await websocket.send_json(chunk) elif data.get("type") == "set_study": study_id = data.get("study_id") await manager.set_study_context(session_id, study_id) await websocket.send_json({ "type": "context_updated", "study_id": study_id, }) elif data.get("type") == "ping": await websocket.send_json({"type": "pong"}) except WebSocketDisconnect: pass except Exception as e: await websocket.send_json({"type": "error", "message": str(e)}) # Keep legacy endpoints for backwards compatibility @router.get("/status") async def get_claude_status(): """Check if Claude CLI is available""" import shutil claude_available = shutil.which("claude") is not None return { "available": claude_available, "message": "Claude CLI available" if claude_available else "Claude CLI not found", "mode": "session", } @router.get("/suggestions") async def get_suggestions(study_id: Optional[str] = None): """Get contextual suggestions""" if study_id: return { "suggestions": [ f"What's the status of {study_id}?", "Show me the best design found", "Analyze convergence", "Generate a report", "Compare the top 5 trials", ] } return { "suggestions": [ "List available studies", "Create a new optimization study", "Help me understand Atomizer", "What can you help me with?", ] } ``` --- ## Phase 3: Frontend Updates ### 3.1 Update useChat Hook **File:** `frontend/src/hooks/useChat.ts` (UPDATE) ```typescript import { useState, useCallback, useRef, useEffect } from 'react'; import { Message } from '../components/chat/ChatMessage'; interface UseChatOptions { studyId?: string | null; initialMode?: 'user' | 'power'; onError?: (error: string) => void; } interface ToolCall { name: string; arguments: Record; result?: any; } interface ChatState { messages: Message[]; isThinking: boolean; error: string | null; suggestions: string[]; mode: 'user' | 'power'; sessionId: string | null; isConnected: boolean; } export function useChat({ studyId, initialMode = 'user', onError }: UseChatOptions = {}) { const [state, setState] = useState({ messages: [], isThinking: false, error: null, suggestions: [], mode: initialMode, sessionId: null, isConnected: false, }); const wsRef = useRef(null); const reconnectTimeoutRef = useRef(null); // Create session on mount useEffect(() => { createSession(); return () => { if (wsRef.current) { wsRef.current.close(); } if (reconnectTimeoutRef.current) { clearTimeout(reconnectTimeoutRef.current); } }; }, []); // Update study context when study changes useEffect(() => { if (state.sessionId && wsRef.current?.readyState === WebSocket.OPEN && studyId) { wsRef.current.send(JSON.stringify({ type: 'set_study', study_id: studyId, })); } }, [studyId, state.sessionId]); const createSession = async () => { try { const response = await fetch('/api/claude/sessions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ mode: state.mode, study_id: studyId || undefined, }), }); if (!response.ok) throw new Error('Failed to create session'); const data = await response.json(); setState(prev => ({ ...prev, sessionId: data.session_id })); connectWebSocket(data.session_id); loadSuggestions(); } catch (error: any) { setState(prev => ({ ...prev, error: error.message })); onError?.(error.message); } }; const connectWebSocket = (sessionId: string) => { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const ws = new WebSocket(`${protocol}//${window.location.host}/api/claude/sessions/${sessionId}/ws`); ws.onopen = () => { setState(prev => ({ ...prev, isConnected: true, error: null })); }; ws.onmessage = (event) => { const data = JSON.parse(event.data); handleWebSocketMessage(data); }; ws.onerror = () => { setState(prev => ({ ...prev, error: 'Connection error' })); }; ws.onclose = () => { setState(prev => ({ ...prev, isConnected: false })); // Attempt reconnection reconnectTimeoutRef.current = window.setTimeout(() => { if (state.sessionId) { connectWebSocket(state.sessionId); } }, 3000); }; wsRef.current = ws; }; const handleWebSocketMessage = (data: any) => { switch (data.type) { case 'text': setState(prev => { const messages = [...prev.messages]; const lastMsg = messages[messages.length - 1]; if (lastMsg && lastMsg.role === 'assistant' && lastMsg.isStreaming) { lastMsg.content += data.content; } return { ...prev, messages }; }); break; case 'tool_call': setState(prev => { const messages = [...prev.messages]; const lastMsg = messages[messages.length - 1]; if (lastMsg && lastMsg.role === 'assistant') { lastMsg.toolCalls = [...(lastMsg.toolCalls || []), data.tool]; } return { ...prev, messages }; }); break; case 'tool_result': setState(prev => { const messages = [...prev.messages]; const lastMsg = messages[messages.length - 1]; if (lastMsg && lastMsg.toolCalls) { const tool = lastMsg.toolCalls.find(t => t.name === data.result.name); if (tool) { tool.result = data.result.content; } } return { ...prev, messages }; }); break; case 'done': setState(prev => { const messages = [...prev.messages]; const lastMsg = messages[messages.length - 1]; if (lastMsg) { lastMsg.isStreaming = false; } return { ...prev, messages, isThinking: false }; }); break; case 'error': setState(prev => ({ ...prev, error: data.message, isThinking: false })); onError?.(data.message); break; } }; const loadSuggestions = async () => { try { const url = studyId ? `/api/claude/suggestions?study_id=${encodeURIComponent(studyId)}` : '/api/claude/suggestions'; const response = await fetch(url); if (response.ok) { const data = await response.json(); setState(prev => ({ ...prev, suggestions: data.suggestions || [] })); } } catch { // Silently fail } }; const sendMessage = useCallback(async (content: string) => { if (!content.trim() || state.isThinking || !wsRef.current) return; const userMessage: Message = { id: `msg_${Date.now()}`, role: 'user', content: content.trim(), timestamp: new Date(), }; const assistantMessage: Message = { id: `msg_${Date.now()}_response`, role: 'assistant', content: '', timestamp: new Date(), isStreaming: true, toolCalls: [], }; setState(prev => ({ ...prev, messages: [...prev.messages, userMessage, assistantMessage], isThinking: true, error: null, })); wsRef.current.send(JSON.stringify({ type: 'message', content: content.trim(), })); }, [state.isThinking]); const switchMode = useCallback(async (newMode: 'user' | 'power') => { if (newMode === state.mode) return; try { const response = await fetch(`/api/claude/sessions/${state.sessionId}/mode`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ mode: newMode }), }); if (!response.ok) throw new Error('Failed to switch mode'); setState(prev => ({ ...prev, mode: newMode })); // Reconnect WebSocket if (wsRef.current) { wsRef.current.close(); } if (state.sessionId) { connectWebSocket(state.sessionId); } } catch (error: any) { onError?.(error.message); } }, [state.sessionId, state.mode]); const clearMessages = useCallback(() => { setState(prev => ({ ...prev, messages: [], error: null, })); }, []); return { messages: state.messages, isThinking: state.isThinking, error: state.error, suggestions: state.suggestions, mode: state.mode, isConnected: state.isConnected, sendMessage, switchMode, clearMessages, }; } ``` ### 3.2 Create Mode Toggle Component **File:** `frontend/src/components/chat/ModeToggle.tsx` (NEW) ```tsx import React from 'react'; import { User, Zap, AlertTriangle } from 'lucide-react'; interface ModeToggleProps { mode: 'user' | 'power'; onModeChange: (mode: 'user' | 'power') => void; disabled?: boolean; } export const ModeToggle: React.FC = ({ mode, onModeChange, disabled = false, }) => { const [showConfirm, setShowConfirm] = React.useState(false); const handlePowerClick = () => { if (mode === 'power') return; setShowConfirm(true); }; const confirmPowerMode = () => { setShowConfirm(false); onModeChange('power'); }; return (
{/* Power Mode Confirmation Modal */} {showConfirm && (

Enable Power Mode?

Power Mode allows full system access including file editing, code modification, and shell commands.

)}
); }; ``` ### 3.3 Create Tool Call Card Component **File:** `frontend/src/components/chat/ToolCallCard.tsx` (NEW) ```tsx import React, { useState } from 'react'; import { ChevronDown, ChevronRight, CheckCircle, XCircle, Loader } from 'lucide-react'; interface ToolCall { name: string; arguments: Record; result?: { type: string; content: any; isError?: boolean; }; } interface ToolCallCardProps { toolCall: ToolCall; } // Map tool names to friendly labels const TOOL_LABELS: Record = { list_studies: 'Listing Studies', create_study: 'Creating Study', get_study_status: 'Getting Status', run_optimization: 'Starting Optimization', get_trial_data: 'Querying Trials', analyze_convergence: 'Analyzing Convergence', generate_report: 'Generating Report', edit_file: 'Editing File', run_shell_command: 'Running Command', }; export const ToolCallCard: React.FC = ({ toolCall }) => { const [isExpanded, setIsExpanded] = useState(false); const label = TOOL_LABELS[toolCall.name] || toolCall.name; const hasResult = toolCall.result !== undefined; const isError = toolCall.result?.isError; const isLoading = !hasResult; return (
{isExpanded && (
{/* Arguments */}
Arguments
              {JSON.stringify(toolCall.arguments, null, 2)}
            
{/* Result */} {hasResult && (
Result
                {typeof toolCall.result?.content === 'string'
                  ? toolCall.result.content
                  : JSON.stringify(toolCall.result?.content, null, 2)}
              
)}
)}
); }; ``` ### 3.4 Update ChatMessage Component **File:** `frontend/src/components/chat/ChatMessage.tsx` (UPDATE) ```tsx import React from 'react'; import ReactMarkdown from 'react-markdown'; import { User, Bot } from 'lucide-react'; import { ToolCallCard } from './ToolCallCard'; export interface Message { id: string; role: 'user' | 'assistant'; content: string; timestamp: Date; isStreaming?: boolean; toolCalls?: Array<{ name: string; arguments: Record; result?: any; }>; } interface ChatMessageProps { message: Message; } export const ChatMessage: React.FC = ({ message }) => { const isUser = message.role === 'user'; return (
{/* Avatar */}
{isUser ? : }
{/* Message Content */}
{isUser ? (

{message.content}

) : (
{message.content} {/* Tool Calls */} {message.toolCalls && message.toolCalls.length > 0 && (
{message.toolCalls.map((tool, idx) => ( ))}
)} {/* Streaming cursor */} {message.isStreaming && ( )}
)}
{/* Timestamp */}
{message.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
); }; ``` ### 3.5 Update ChatPane Component **File:** `frontend/src/components/chat/ChatPane.tsx` (UPDATE) Add mode toggle and update header: ```tsx import React, { useRef, useEffect, useState } from 'react'; import { MessageSquare, ChevronRight, ChevronLeft, Trash2, Minimize2, Maximize2, Wifi, WifiOff, } from 'lucide-react'; import { ChatMessage } from './ChatMessage'; import { ChatInput } from './ChatInput'; import { ThinkingIndicator } from './ThinkingIndicator'; import { ModeToggle } from './ModeToggle'; import { useChat } from '../../hooks/useChat'; import { useStudy } from '../../context/StudyContext'; interface ChatPaneProps { isOpen: boolean; onToggle: () => void; className?: string; } export const ChatPane: React.FC = ({ isOpen, onToggle, className = '', }) => { const { selectedStudy } = useStudy(); const messagesEndRef = useRef(null); const [isExpanded, setIsExpanded] = useState(false); const { messages, isThinking, error, suggestions, mode, isConnected, sendMessage, switchMode, clearMessages, } = useChat({ studyId: selectedStudy?.id, onError: (err) => console.error('Chat error:', err), }); useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages, isThinking]); if (!isOpen) { return ( ); } const panelWidth = isExpanded ? 'w-[600px]' : 'w-[380px]'; return (
{/* Header */}

Atomizer Assistant

{isConnected ? ( ) : ( )}

{selectedStudy ? selectedStudy.name || selectedStudy.id : 'No study'}

{messages.length > 0 && ( )}
{/* Mode indicator bar */} {mode === 'power' && (

⚡ Power Mode - Full system access enabled

)} {/* Messages Area */}
{messages.length === 0 && !isThinking && (

{selectedStudy ? `Ready to help with **${selectedStudy.name || selectedStudy.id}**` : 'Select a study or ask me to create one'}

{suggestions.length > 0 && (

Try asking

{suggestions.slice(0, 3).map((suggestion, idx) => ( ))}
)}
)} {messages.map((message) => ( ))} {isThinking && } {error && (
{error}
)}
{/* Input */}
); }; ``` --- ## Phase 4: Integration & Testing ### 4.1 Update Main Entry Point **File:** `backend/main.py` (UPDATE) ```python from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from contextlib import asynccontextmanager # Import routers from api.routes import studies, trials, optimization, insights, claude # Session manager lifecycle @asynccontextmanager async def lifespan(app: FastAPI): # Startup from api.routes.claude import get_session_manager manager = get_session_manager() await manager.start() yield # Shutdown await manager.stop() app = FastAPI( title="Atomizer Dashboard API", version="2.0.0", lifespan=lifespan, ) # CORS app.add_middleware( CORSMiddleware, allow_origins=["http://localhost:3004", "http://localhost:5173"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Routes app.include_router(studies.router, prefix="/api/studies", tags=["studies"]) app.include_router(trials.router, prefix="/api/trials", tags=["trials"]) app.include_router(optimization.router, prefix="/api/optimization", tags=["optimization"]) app.include_router(insights.router, prefix="/api/insights", tags=["insights"]) app.include_router(claude.router, prefix="/api/claude", tags=["claude"]) @app.get("/health") async def health(): return {"status": "healthy", "version": "2.0.0"} ``` ### 4.2 Test Plan ```markdown ## Test Cases ### Phase 1: MCP Server - [ ] Server starts without errors - [ ] list_studies returns study list - [ ] get_study_status returns correct info - [ ] create_study creates valid study structure - [ ] run_optimization starts optimization - [ ] Power mode tools only available in power mode ### Phase 2: Session Manager - [ ] Session creation works - [ ] WebSocket connects - [ ] Messages stream correctly - [ ] Tool calls are executed - [ ] Mode switching works - [ ] Session cleanup on disconnect - [ ] Conversation persistence ### Phase 3: Frontend - [ ] Mode toggle displays correctly - [ ] Mode switch confirmation works - [ ] Tool calls render properly - [ ] Messages stream smoothly - [ ] Connection status shows correctly - [ ] Error handling works ### Phase 4: End-to-End - [ ] Create study via chat - [ ] Run optimization via chat - [ ] Analyze results via chat - [ ] Power mode: Edit file - [ ] Power mode: Create extractor - [ ] Session resume after page refresh ``` --- ## Task List (for Ralph Loop) ### PHASE 1: MCP SERVER (Priority: High) ``` [ ] P1.1 Create MCP server scaffold - Create mcp-server/atomizer-tools/package.json - Create mcp-server/atomizer-tools/tsconfig.json - Create mcp-server/atomizer-tools/src/index.ts [ ] P1.2 Implement study tools - Create src/tools/study.ts - Implement list_studies - Implement get_study_status - Implement create_study [ ] P1.3 Implement optimization tools - Create src/tools/optimization.ts - Implement run_optimization - Implement stop_optimization - Implement get_trial_data [ ] P1.4 Implement analysis tools - Create src/tools/analysis.ts - Implement analyze_convergence - Implement compare_trials - Implement get_best_design [ ] P1.5 Implement reporting tools - Create src/tools/reporting.ts - Implement generate_report [ ] P1.6 Implement physics tools - Create src/tools/physics.ts - Implement explain_physics - Implement recommend_method [ ] P1.7 Implement admin tools (power mode) - Create src/tools/admin.ts - Implement edit_file - Implement create_extractor - Implement run_shell_command [ ] P1.8 Build and test MCP server - npm install - npm run build - Test with claude --mcp-config ``` ### PHASE 2: BACKEND SESSION MANAGER (Priority: High) ``` [ ] P2.1 Create conversation store - Create backend/api/services/conversation_store.py - Implement SQLite schema - Implement CRUD operations [ ] P2.2 Create context builder - Create backend/api/services/context_builder.py - Implement base context - Implement study context - Implement mode instructions [ ] P2.3 Create session manager - Create backend/api/services/session_manager.py - Implement session lifecycle - Implement WebSocket communication - Implement mode switching [ ] P2.4 Update API routes - Update backend/api/routes/claude.py - Add session endpoints - Add WebSocket endpoint - Keep legacy endpoints [ ] P2.5 Update main.py - Add lifespan handler - Initialize session manager ``` ### PHASE 3: FRONTEND UPDATES (Priority: High) ``` [ ] P3.1 Create new components - Create ModeToggle.tsx - Create ToolCallCard.tsx [ ] P3.2 Update useChat hook - Add WebSocket support - Add session management - Add mode switching - Add tool call handling [ ] P3.3 Update ChatMessage - Add tool call rendering - Update styling [ ] P3.4 Update ChatPane - Add mode toggle - Add connection status - Update styling [ ] P3.5 Export new components - Update components/chat/index.ts ``` ### PHASE 4: TESTING & POLISH (Priority: Medium) ``` [ ] P4.1 Backend tests - Test session creation - Test message handling - Test mode switching [ ] P4.2 Frontend tests - Test mode toggle - Test tool rendering - Test connection handling [ ] P4.3 End-to-end tests - Test full conversation flow - Test study creation - Test power mode operations [ ] P4.4 Documentation - Update README - Add API documentation - Add user guide ``` --- ## Dependencies ``` Backend: - FastAPI (existing) - SQLite3 (built-in) - asyncio (built-in) MCP Server (new): - @modelcontextprotocol/sdk - better-sqlite3 - zod - typescript Frontend (existing): - React - react-markdown - lucide-react ``` --- ## Risk Mitigation | Risk | Mitigation | |------|------------| | Claude CLI doesn't support --mcp-config | Fall back to enhanced context approach | | WebSocket connection instability | Implement reconnection logic | | Session memory issues | Implement cleanup and limits | | MCP server crashes | Add error boundaries and restart logic | --- ## Success Criteria 1. **User Mode**: User can create study, run optimization, get results via chat 2. **Power Mode**: User can edit files, create extractors via chat 3. **Persistence**: Conversation survives page refresh 4. **Context**: Chat knows about current study when inside one 5. **Demo-ready**: Smooth enough for YouTube demo --- *Document Version: 1.0* *Last Updated: 2025-01-08*