Files
Atomizer/docs/plans/DASHBOARD_CHAT_ARCHITECTURE.md
Anto01 73a7b9d9f1 feat: Add dashboard chat integration and MCP server
Major changes:
- Dashboard: WebSocket-based chat with session management
- Dashboard: New chat components (ChatPane, ChatInput, ModeToggle)
- Dashboard: Enhanced UI with parallel coordinates chart
- MCP Server: New atomizer-tools server for Claude integration
- Extractors: Enhanced Zernike OPD extractor
- Reports: Improved report generator

New studies (configs and scripts only):
- M1 Mirror: Cost reduction campaign studies
- Simple Beam, Simple Bracket, UAV Arm studies

Note: Large iteration data (2_iterations/, best_design_archive/)
excluded via .gitignore - kept on local Gitea only.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 15:53:55 -05:00

75 KiB

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 <config.json>                                  │  │
│  │                                                                     │  │
│  │  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:

{
  "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:

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:

// 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

// 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

// 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

"""
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

"""
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

"""
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)

"""
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)

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<string, any>;
  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<ChatState>({
    messages: [],
    isThinking: false,
    error: null,
    suggestions: [],
    mode: initialMode,
    sessionId: null,
    isConnected: false,
  });

  const wsRef = useRef<WebSocket | null>(null);
  const reconnectTimeoutRef = useRef<number | null>(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)

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<ModeToggleProps> = ({
  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 (
    <div className="relative">
      <div className="flex rounded-lg bg-dark-800 p-1 gap-1">
        <button
          onClick={() => onModeChange('user')}
          disabled={disabled}
          className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm transition-all ${
            mode === 'user'
              ? 'bg-primary-500/20 text-primary-400'
              : 'text-dark-400 hover:text-dark-200'
          } ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
        >
          <User className="w-4 h-4" />
          <span>User</span>
        </button>
        <button
          onClick={handlePowerClick}
          disabled={disabled}
          className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm transition-all ${
            mode === 'power'
              ? 'bg-amber-500/20 text-amber-400'
              : 'text-dark-400 hover:text-dark-200'
          } ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
        >
          <Zap className="w-4 h-4" />
          <span>Power</span>
        </button>
      </div>

      {/* Power Mode Confirmation Modal */}
      {showConfirm && (
        <div className="absolute top-full right-0 mt-2 w-72 p-4 bg-dark-800 rounded-lg border border-dark-600 shadow-xl z-50">
          <div className="flex items-start gap-3 mb-3">
            <AlertTriangle className="w-5 h-5 text-amber-400 flex-shrink-0 mt-0.5" />
            <div>
              <h4 className="text-sm font-medium text-dark-100 mb-1">
                Enable Power Mode?
              </h4>
              <p className="text-xs text-dark-400">
                Power Mode allows full system access including file editing,
                code modification, and shell commands.
              </p>
            </div>
          </div>
          <div className="flex gap-2 justify-end">
            <button
              onClick={() => setShowConfirm(false)}
              className="px-3 py-1.5 text-sm text-dark-300 hover:text-dark-100"
            >
              Cancel
            </button>
            <button
              onClick={confirmPowerMode}
              className="px-3 py-1.5 text-sm bg-amber-500/20 text-amber-400 rounded-md hover:bg-amber-500/30"
            >
              Enable
            </button>
          </div>
        </div>
      )}
    </div>
  );
};

3.3 Create Tool Call Card Component

File: frontend/src/components/chat/ToolCallCard.tsx (NEW)

import React, { useState } from 'react';
import { ChevronDown, ChevronRight, CheckCircle, XCircle, Loader } from 'lucide-react';

interface ToolCall {
  name: string;
  arguments: Record<string, any>;
  result?: {
    type: string;
    content: any;
    isError?: boolean;
  };
}

interface ToolCallCardProps {
  toolCall: ToolCall;
}

// Map tool names to friendly labels
const TOOL_LABELS: Record<string, string> = {
  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<ToolCallCardProps> = ({ 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 (
    <div className="my-2 rounded-lg border border-dark-600 bg-dark-800/50 overflow-hidden">
      <button
        onClick={() => setIsExpanded(!isExpanded)}
        className="w-full flex items-center gap-2 px-3 py-2 text-left hover:bg-dark-700/50"
      >
        {isExpanded ? (
          <ChevronDown className="w-4 h-4 text-dark-400" />
        ) : (
          <ChevronRight className="w-4 h-4 text-dark-400" />
        )}

        <span className="flex-1 text-sm text-dark-200">{label}</span>

        {isLoading && (
          <Loader className="w-4 h-4 text-primary-400 animate-spin" />
        )}
        {hasResult && !isError && (
          <CheckCircle className="w-4 h-4 text-green-400" />
        )}
        {hasResult && isError && (
          <XCircle className="w-4 h-4 text-red-400" />
        )}
      </button>

      {isExpanded && (
        <div className="px-3 pb-3 border-t border-dark-700">
          {/* Arguments */}
          <div className="mt-2">
            <span className="text-xs text-dark-500 uppercase">Arguments</span>
            <pre className="mt-1 p-2 rounded bg-dark-900 text-xs text-dark-300 overflow-x-auto">
              {JSON.stringify(toolCall.arguments, null, 2)}
            </pre>
          </div>

          {/* Result */}
          {hasResult && (
            <div className="mt-2">
              <span className="text-xs text-dark-500 uppercase">Result</span>
              <pre className={`mt-1 p-2 rounded text-xs overflow-x-auto ${
                isError ? 'bg-red-900/20 text-red-400' : 'bg-dark-900 text-dark-300'
              }`}>
                {typeof toolCall.result?.content === 'string'
                  ? toolCall.result.content
                  : JSON.stringify(toolCall.result?.content, null, 2)}
              </pre>
            </div>
          )}
        </div>
      )}
    </div>
  );
};

3.4 Update ChatMessage Component

File: frontend/src/components/chat/ChatMessage.tsx (UPDATE)

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<string, any>;
    result?: any;
  }>;
}

interface ChatMessageProps {
  message: Message;
}

export const ChatMessage: React.FC<ChatMessageProps> = ({ message }) => {
  const isUser = message.role === 'user';

  return (
    <div className={`flex gap-3 ${isUser ? 'flex-row-reverse' : ''}`}>
      {/* Avatar */}
      <div
        className={`w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 ${
          isUser
            ? 'bg-primary-500/20 text-primary-400'
            : 'bg-dark-700 text-dark-300'
        }`}
      >
        {isUser ? <User className="w-4 h-4" /> : <Bot className="w-4 h-4" />}
      </div>

      {/* Message Content */}
      <div className={`flex-1 max-w-[85%] ${isUser ? 'text-right' : ''}`}>
        <div
          className={`inline-block rounded-lg px-4 py-2 ${
            isUser
              ? 'bg-primary-500/20 text-dark-100'
              : 'bg-dark-700/50 text-dark-200'
          }`}
        >
          {isUser ? (
            <p className="text-sm whitespace-pre-wrap">{message.content}</p>
          ) : (
            <div className="text-sm prose prose-invert prose-sm max-w-none">
              <ReactMarkdown>{message.content}</ReactMarkdown>

              {/* Tool Calls */}
              {message.toolCalls && message.toolCalls.length > 0 && (
                <div className="mt-3 space-y-2">
                  {message.toolCalls.map((tool, idx) => (
                    <ToolCallCard key={idx} toolCall={tool} />
                  ))}
                </div>
              )}

              {/* Streaming cursor */}
              {message.isStreaming && (
                <span className="inline-block w-2 h-4 bg-primary-400 animate-pulse ml-1" />
              )}
            </div>
          )}
        </div>

        {/* Timestamp */}
        <div className={`text-xs text-dark-500 mt-1 ${isUser ? 'text-right' : ''}`}>
          {message.timestamp.toLocaleTimeString([], {
            hour: '2-digit',
            minute: '2-digit'
          })}
        </div>
      </div>
    </div>
  );
};

3.5 Update ChatPane Component

File: frontend/src/components/chat/ChatPane.tsx (UPDATE)

Add mode toggle and update header:

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<ChatPaneProps> = ({
  isOpen,
  onToggle,
  className = '',
}) => {
  const { selectedStudy } = useStudy();
  const messagesEndRef = useRef<HTMLDivElement>(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 (
      <button
        onClick={onToggle}
        className={`fixed right-0 top-1/2 -translate-y-1/2 z-40 bg-dark-800 border border-dark-700 border-r-0 rounded-l-lg p-3 hover:bg-dark-700 transition-colors group ${className}`}
        title="Open Assistant"
      >
        <div className="flex items-center gap-2">
          <ChevronLeft className="w-4 h-4 text-dark-400 group-hover:text-primary-400" />
          <MessageSquare className="w-5 h-5 text-primary-400" />
        </div>
      </button>
    );
  }

  const panelWidth = isExpanded ? 'w-[600px]' : 'w-[380px]';

  return (
    <div className={`fixed right-0 top-0 h-full ${panelWidth} bg-dark-850 border-l border-dark-700 flex flex-col z-40 shadow-2xl transition-all duration-200 ${className}`}>
      {/* Header */}
      <div className="flex items-center justify-between px-4 py-3 border-b border-dark-700 bg-dark-800/50">
        <div className="flex items-center gap-3">
          <div className="w-8 h-8 rounded-lg bg-primary-500/20 flex items-center justify-center">
            <MessageSquare className="w-4 h-4 text-primary-400" />
          </div>
          <div>
            <div className="flex items-center gap-2">
              <h3 className="font-medium text-dark-100 text-sm">Atomizer Assistant</h3>
              {isConnected ? (
                <Wifi className="w-3 h-3 text-green-400" />
              ) : (
                <WifiOff className="w-3 h-3 text-red-400" />
              )}
            </div>
            <p className="text-xs text-dark-400">
              {selectedStudy ? selectedStudy.name || selectedStudy.id : 'No study'}
            </p>
          </div>
        </div>
        <div className="flex items-center gap-1">
          <ModeToggle mode={mode} onModeChange={switchMode} disabled={isThinking} />
          {messages.length > 0 && (
            <button
              onClick={clearMessages}
              className="p-2 rounded-lg text-dark-400 hover:text-red-400 hover:bg-dark-700"
              title="Clear conversation"
            >
              <Trash2 className="w-4 h-4" />
            </button>
          )}
          <button
            onClick={() => setIsExpanded(!isExpanded)}
            className="p-2 rounded-lg text-dark-400 hover:text-primary-400 hover:bg-dark-700"
            title={isExpanded ? 'Collapse' : 'Expand'}
          >
            {isExpanded ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
          </button>
          <button
            onClick={onToggle}
            className="p-2 rounded-lg text-dark-400 hover:text-dark-200 hover:bg-dark-700"
            title="Close"
          >
            <ChevronRight className="w-4 h-4" />
          </button>
        </div>
      </div>

      {/* Mode indicator bar */}
      {mode === 'power' && (
        <div className="px-4 py-2 bg-amber-500/10 border-b border-amber-500/20">
          <p className="text-xs text-amber-400">
             Power Mode - Full system access enabled
          </p>
        </div>
      )}

      {/* Messages Area */}
      <div className="flex-1 overflow-y-auto p-4 space-y-4">
        {messages.length === 0 && !isThinking && (
          <div className="text-center py-8">
            <div className="w-12 h-12 rounded-xl bg-primary-500/10 flex items-center justify-center mx-auto mb-4">
              <MessageSquare className="w-6 h-6 text-primary-400" />
            </div>
            <p className="text-dark-300 text-sm max-w-xs mx-auto mb-6">
              {selectedStudy
                ? `Ready to help with **${selectedStudy.name || selectedStudy.id}**`
                : 'Select a study or ask me to create one'}
            </p>
            {suggestions.length > 0 && (
              <div className="space-y-2">
                <p className="text-xs text-dark-500 uppercase">Try asking</p>
                {suggestions.slice(0, 3).map((suggestion, idx) => (
                  <button
                    key={idx}
                    onClick={() => sendMessage(suggestion)}
                    className="block w-full text-left text-sm px-4 py-2 rounded-lg bg-dark-700/50 text-dark-300 hover:bg-dark-700 hover:text-white"
                  >
                    {suggestion}
                  </button>
                ))}
              </div>
            )}
          </div>
        )}

        {messages.map((message) => (
          <ChatMessage key={message.id} message={message} />
        ))}

        {isThinking && <ThinkingIndicator />}

        {error && (
          <div className="bg-red-500/10 border border-red-500/20 rounded-lg px-4 py-3 text-sm text-red-400">
            {error}
          </div>
        )}

        <div ref={messagesEndRef} />
      </div>

      {/* Input */}
      <ChatInput
        onSend={sendMessage}
        disabled={isThinking || !isConnected}
        suggestions={suggestions}
        placeholder={
          !isConnected
            ? 'Connecting...'
            : selectedStudy
            ? `Ask about ${selectedStudy.name || 'this study'}...`
            : 'Ask a question...'
        }
      />
    </div>
  );
};

Phase 4: Integration & Testing

4.1 Update Main Entry Point

File: backend/main.py (UPDATE)

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

## 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