diff --git a/backend/app/routers/openclaw.py b/backend/app/routers/openclaw.py index 7f37aec..84ec1a4 100644 --- a/backend/app/routers/openclaw.py +++ b/backend/app/routers/openclaw.py @@ -10,12 +10,13 @@ import json import logging import os import re +import shutil import socket from datetime import datetime, timezone from pathlib import Path from typing import Any, Optional -from fastapi import APIRouter, Query +from fastapi import APIRouter, Body, HTTPException, Query from app.config import settings @@ -27,12 +28,53 @@ _SECRET_PATTERNS = re.compile( r"(token|key|secret|password|apiKey|api_key)", re.IGNORECASE ) +# Agent identity files expected in workspace// +AGENT_IDENTITY_FILES = [ + "AGENTS.md", "BOOTSTRAP.md", "HEARTBEAT.md", "IDENTITY.md", + "identity.md", "SOUL.md", "TOOLS.md", "USER.md", +] + +# File extensions we consider text-editable +_TEXT_EXTENSIONS = { + ".json", ".yaml", ".yml", ".md", ".txt", ".sh", ".py", ".js", ".ts", + ".tsx", ".jsx", ".css", ".html", ".xml", ".toml", ".ini", ".cfg", + ".conf", ".log", ".env", ".gitignore", ".dockerfile", +} + +_LANG_MAP = { + ".json": "json", + ".yaml": "yaml", ".yml": "yaml", + ".md": "markdown", + ".py": "python", + ".js": "javascript", ".jsx": "javascript", + ".ts": "typescript", ".tsx": "typescript", + ".sh": "shell", + ".css": "css", + ".html": "html", + ".xml": "xml", + ".toml": "toml", +} + def _home() -> Path: """Resolve the OpenClaw home directory.""" return Path(settings.FOXY_HOME) +def _workspace() -> Path: + """Resolve the OpenClaw workspace directory.""" + return Path(settings.FOXY_WORKSPACE) + + +def _safe_path(relative: str) -> Path: + """Resolve a relative path under FOXY_HOME, preventing path traversal.""" + home = _home() + resolved = (home / relative).resolve() + if not str(resolved).startswith(str(home.resolve())): + raise HTTPException(status_code=403, detail="Path traversal denied") + return resolved + + def _mask_secrets(obj: Any, depth: int = 0) -> Any: """Recursively mask values whose keys look like secrets.""" if depth > 20: @@ -67,7 +109,7 @@ def _read_json(path: Path) -> Optional[dict]: return None -def _dir_tree(root: Path, max_depth: int = 3, _depth: int = 0) -> list[dict]: +def _dir_tree(root: Path, max_depth: int = 5, _depth: int = 0) -> list[dict]: """Build a lightweight directory tree.""" entries: list[dict] = [] if not root.is_dir() or _depth > max_depth: @@ -179,7 +221,11 @@ async def list_openclaw_agents(): if entry.is_dir(): # Directory-based agent — look for config files agent["type"] = "directory" - config_files = list(entry.glob("*.json")) + list(entry.glob("*.yaml")) + list(entry.glob("*.yml")) + config_files = ( + list(entry.glob("*.json")) + + list(entry.glob("*.yaml")) + + list(entry.glob("*.yml")) + ) agent["config_files"] = [f.name for f in config_files] # Try to read agent config @@ -188,9 +234,14 @@ async def list_openclaw_agents(): if cfg: agent["config"] = _mask_secrets(cfg) agent["model"] = cfg.get("model", cfg.get("modelId", None)) - agent["system_prompt"] = cfg.get("systemPrompt", cfg.get("system_prompt", cfg.get("instructions", None))) + agent["system_prompt"] = cfg.get( + "systemPrompt", + cfg.get("system_prompt", cfg.get("instructions", None)), + ) if agent["system_prompt"] and len(agent["system_prompt"]) > 300: - agent["system_prompt_preview"] = agent["system_prompt"][:300] + "…" + agent["system_prompt_preview"] = ( + agent["system_prompt"][:300] + "…" + ) break # Count files @@ -208,11 +259,152 @@ async def list_openclaw_agents(): agent["config"] = _mask_secrets(cfg) agent["model"] = cfg.get("model", cfg.get("modelId", None)) + # Check workspace for identity files + ws_dir = _workspace() / entry.name + if ws_dir.is_dir(): + agent["has_workspace"] = True + identity_files = [] + for fname in AGENT_IDENTITY_FILES: + fpath = ws_dir / fname + if fpath.is_file(): + try: + identity_files.append( + {"name": fname, "size": fpath.stat().st_size} + ) + except OSError: + identity_files.append({"name": fname, "size": 0}) + agent["identity_files"] = identity_files + else: + agent["has_workspace"] = False + agent["identity_files"] = [] + agents.append(agent) return {"agents": agents, "count": len(agents)} +@router.delete("/agents/{agent_name}") +async def delete_agent(agent_name: str): + """Delete an agent directory from agents/.""" + agent_dir = _home() / "agents" / agent_name + if not agent_dir.exists(): + raise HTTPException(status_code=404, detail=f"Agent '{agent_name}' not found") + + try: + if agent_dir.is_dir(): + shutil.rmtree(str(agent_dir)) + else: + agent_dir.unlink() + log.info(f"Agent deleted: {agent_name}") + return {"message": f"Agent '{agent_name}' deleted"} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/agents/{agent_name}/files") +async def list_agent_identity_files(agent_name: str): + """List identity files for an agent from workspace//.""" + ws_dir = _workspace() / agent_name + if not ws_dir.is_dir(): + return {"files": [], "workspace": str(ws_dir), "exists": False} + + files = [] + for fname in AGENT_IDENTITY_FILES: + fpath = ws_dir / fname + if fpath.is_file(): + try: + stat = fpath.stat() + files.append({ + "name": fname, + "size": stat.st_size, + "modified": datetime.fromtimestamp( + stat.st_mtime, tz=timezone.utc + ).isoformat(), + }) + except OSError: + files.append({"name": fname, "size": 0, "modified": None}) + + # Also list other non-identity files in the workspace + other_files = [] + try: + for child in sorted(ws_dir.iterdir()): + if child.is_file() and child.name not in AGENT_IDENTITY_FILES: + try: + other_files.append({ + "name": child.name, + "size": child.stat().st_size, + }) + except OSError: + other_files.append({"name": child.name, "size": 0}) + except PermissionError: + pass + + return { + "files": files, + "other_files": other_files, + "workspace": str(ws_dir), + "exists": True, + } + + +@router.get("/agents/{agent_name}/file") +async def read_agent_file( + agent_name: str, + filename: str = Query(..., description="Filename to read"), +): + """Read an agent identity file content.""" + ws_dir = _workspace() / agent_name + file_path = ws_dir / filename + + # Security: ensure the file is within the workspace directory + if not str(file_path.resolve()).startswith(str(ws_dir.resolve())): + raise HTTPException(status_code=403, detail="Path traversal denied") + + if not file_path.is_file(): + raise HTTPException(status_code=404, detail=f"File '{filename}' not found") + + try: + content = await asyncio.to_thread( + file_path.read_text, encoding="utf-8", errors="replace" + ) + return { + "content": content, + "filename": filename, + "size": file_path.stat().st_size, + "language": _LANG_MAP.get(file_path.suffix.lower(), "text"), + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@router.put("/agents/{agent_name}/file") +async def write_agent_file( + agent_name: str, + filename: str = Query(..., description="Filename to write"), + content: str = Body(..., embed=True), +): + """Write/update an agent identity file.""" + ws_dir = _workspace() / agent_name + file_path = ws_dir / filename + + # Security: ensure the file is within the workspace directory + if not str(file_path.resolve()).startswith(str(ws_dir.resolve())): + raise HTTPException(status_code=403, detail="Path traversal denied") + + try: + ws_dir.mkdir(parents=True, exist_ok=True) + await asyncio.to_thread( + file_path.write_text, content, encoding="utf-8" + ) + log.info(f"Agent file written: {agent_name}/{filename}") + return { + "message": f"File '{filename}' saved", + "size": file_path.stat().st_size, + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + # ═════════════════════════════════════════════════════════════════════════════════ # 4. Skills # ═════════════════════════════════════════════════════════════════════════════════ @@ -221,7 +413,6 @@ async def list_openclaw_agents(): @router.get("/skills") async def list_openclaw_skills(): """List available skills from the skills/ directory.""" - # Skills can be in multiple locations possible_dirs = [ _home() / "skills", _home() / "workspace" / "skills", @@ -239,32 +430,34 @@ async def list_openclaw_skills(): if entry.is_dir(): skill["type"] = "directory" - # Look for SKILL.md or README.md for doc_name in ("SKILL.md", "README.md", "skill.md", "readme.md"): doc = entry / doc_name if doc.is_file(): try: content = doc.read_text(encoding="utf-8", errors="replace") - # Extract description from YAML frontmatter or first lines skill["description"] = content[:500] - # Try extracting YAML description if content.startswith("---"): end = content.find("---", 3) if end > 0: frontmatter = content[3:end].strip() for line in frontmatter.split("\n"): if line.startswith("description:"): - skill["description_short"] = line.split(":", 1)[1].strip().strip("'\"") + skill["description_short"] = ( + line.split(":", 1)[1] + .strip() + .strip("'\"") + ) break except Exception: pass break - # Count files try: all_files = list(entry.rglob("*")) skill["file_count"] = len([f for f in all_files if f.is_file()]) - skill["subdirs"] = [d.name for d in entry.iterdir() if d.is_dir()] + skill["subdirs"] = [ + d.name for d in entry.iterdir() if d.is_dir() + ] except Exception: skill["file_count"] = 0 @@ -288,28 +481,30 @@ async def list_openclaw_skills(): @router.get("/logs") async def get_gateway_logs( lines: int = Query(200, ge=10, le=2000), - level: Optional[str] = Query(None, description="Filter by log level: INFO, WARNING, ERROR"), + level: Optional[str] = Query( + None, description="Filter by log level: INFO, WARNING, ERROR" + ), ): """Read the last N lines of the gateway log.""" log_file = _home() / "logs" / "gateway.log" if not log_file.is_file(): - # Try alternate locations alt = _home() / "gateway.log" if alt.is_file(): log_file = alt else: - return {"lines": [], "error": "Gateway log file not found", "log_path": str(log_file)} + return { + "lines": [], + "error": "Gateway log file not found", + "log_path": str(log_file), + } try: content = await asyncio.to_thread( log_file.read_text, encoding="utf-8", errors="replace" ) all_lines = content.strip().split("\n") - - # Take last N lines result_lines = all_lines[-lines:] - # Filter by level if requested if level: level_upper = level.upper() result_lines = [l for l in result_lines if level_upper in l.upper()] @@ -337,22 +532,30 @@ async def list_models(): if data is None: return {"models": [], "providers": [], "error": "openclaw.json not found"} - # Extract providers providers = [] - providers_cfg = data.get("mcpServers", data.get("providers", data.get("llm", {}))) + providers_cfg = data.get( + "mcpServers", data.get("providers", data.get("llm", {})) + ) if isinstance(providers_cfg, dict): for name, cfg in providers_cfg.items(): provider: dict[str, Any] = { "name": name, - "type": cfg.get("type", cfg.get("provider", "unknown")) if isinstance(cfg, dict) else "unknown", + "type": ( + cfg.get("type", cfg.get("provider", "unknown")) + if isinstance(cfg, dict) + else "unknown" + ), } if isinstance(cfg, dict): provider["enabled"] = cfg.get("enabled", True) - provider["model"] = cfg.get("model", cfg.get("modelId", None)) - provider["endpoint"] = cfg.get("endpoint", cfg.get("baseUrl", cfg.get("url", None))) + provider["model"] = cfg.get( + "model", cfg.get("modelId", None) + ) + provider["endpoint"] = cfg.get( + "endpoint", cfg.get("baseUrl", cfg.get("url", None)) + ) providers.append(provider) - # Extract models list if present models = [] models_cfg = data.get("models", []) if isinstance(models_cfg, list): @@ -389,3 +592,81 @@ async def get_filesystem(): tree = await asyncio.to_thread(_dir_tree, home) return {"root": str(home), "tree": tree} + + +# ═════════════════════════════════════════════════════════════════════════════════ +# 8. File Read / Write (for Arborescence editor) +# ═════════════════════════════════════════════════════════════════════════════════ + + +@router.get("/file") +async def read_file( + path: str = Query(..., description="Relative path under FOXY_HOME"), +): + """Read a file from the OpenClaw home directory.""" + file_path = _safe_path(path) + + if not file_path.is_file(): + raise HTTPException(status_code=404, detail=f"File not found: {path}") + + suffix = file_path.suffix.lower() + language = _LANG_MAP.get(suffix, "text") + is_text = suffix in _TEXT_EXTENSIONS or suffix == "" + + if not is_text: + # For binary files, just return metadata + return { + "content": None, + "binary": True, + "path": path, + "size": file_path.stat().st_size, + "language": language, + } + + try: + content = await asyncio.to_thread( + file_path.read_text, encoding="utf-8", errors="replace" + ) + + # Pretty-print JSON if applicable + pretty = None + if suffix == ".json": + try: + parsed = json.loads(content) + pretty = json.dumps(parsed, indent=2, ensure_ascii=False) + except json.JSONDecodeError: + pass + + return { + "content": content, + "pretty": pretty, + "binary": False, + "path": path, + "size": file_path.stat().st_size, + "language": language, + "modified": datetime.fromtimestamp( + file_path.stat().st_mtime, tz=timezone.utc + ).isoformat(), + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@router.put("/file") +async def write_file( + path: str = Query(..., description="Relative path under FOXY_HOME"), + content: str = Body(..., embed=True), +): + """Write/update a file in the OpenClaw home directory.""" + file_path = _safe_path(path) + + try: + file_path.parent.mkdir(parents=True, exist_ok=True) + await asyncio.to_thread(file_path.write_text, content, encoding="utf-8") + log.info(f"File written: {path}") + return { + "message": f"File '{path}' saved", + "size": file_path.stat().st_size, + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) diff --git a/docs/innovations_openclaw.md b/docs/innovations_openclaw.md new file mode 100644 index 0000000..36a7aff --- /dev/null +++ b/docs/innovations_openclaw.md @@ -0,0 +1,33 @@ +# 🚀 Propositions d'Innovations pour OpenClaw Dashboard + +Voici 6 propositions de fonctionnalités innovantes pour amener la section OpenClaw du dashboard Foxy Dev Team au niveau supérieur en termes de professionnalisme et de puissance d'intégration multi-agents. + +## 1. Visualisateur de Workflows Multi-Agents (Map) +**Concept** : Un canevas interactif (drag & drop) permettant de visualiser comment les agents interagissent entre eux. +- **Utilité** : D'un simple coup d'œil, voir quel agent appelle quel skill, ou quel agent délègue une tâche à un autre. +- **Technologie** : Intégration de `React Flow` pour dessiner des graphes relationnels générés automatiquement depuis la configuration des agents. + +## 2. Terminal Logs "Real-Time" (WebSockets) +**Concept** : Remplacer le rafraîchissement manuel (polling API) par un flux continu en temps réel de logs. +- **Utilité** : Le terminal ressemble à un vrai terminal Linux, réactif à la milliseconde sans surcharger le serveur avec des requêtes HTTP. +- **Technologie** : Implémenter des WebSockets dans l'API FastAPI (`app.routers.openclaw`) et utiliser un vrai composant terminal web comme `xterm.js`. + +## 3. Playground "Bac à Sable" d'Agent +**Concept** : Un espace intégré pour discuter directement avec un agent OpenClaw en isolement. +- **Utilité** : Permet de tester un `system_prompt` ou un [skill](file:///c:/dev/git/openclaw/FoxyDevTeam/foxy-dev-team/backend/app/routers/openclaw.py#413-474) que l'on vient de modifier via l'éditeur CodeMirror, sans avoir à lancer l'intégralité du workflow global. Voir en temps réel les `Tool Calls` JSON passer. +- **Technologie** : Une interface de chat (similaire à ChatGPT) branchée directement sur l'agent ciblé via l'API OpenClaw. + +## 4. "Marketplace" de Skills / Registry +**Concept** : Un onglet permettant de découvrir, installer et mettre à jour des Skills OpenClaw depuis des dépôts distants (GitHub, registre interne). +- **Utilité** : Démocratiser l'usage et la réutilisabilité des Skills. Au lieu de copier-coller du code Python, un développeur clique sur "Installer" et le Skill est injecté dans `FOXY_HOME/skills/`. +- **Technologie** : API backend capable de `git clone` ou télécharger des sources, couplé à une belle UI de "Store". + +## 5. Débogage "Time-Travel" (Voyage dans le temps) +**Concept** : Garder un historique de l'état mental (mémoire) des agents à chaque étape d'une mission. +- **Utilité** : Lorsqu'un agent fait une erreur ou une hallucination, le développeur peut "rembobiner" l'état de l'agent pour voir exactement ce qu'il a perçu à l'étape -3 et pourquoi il a pris sa décision. +- **Technologie** : Affichage côté frontend des checkpoints d'exécution (`HEARTBEAT.md`, traces JSON). + +## 6. Analyseur IA des Erreurs Gateway +**Concept** : Utiliser un LLM (via un des providers configurés d'OpenClaw) pour lire les logs Gateway quand une erreur survient, et proposer une solution (Auto-healing). +- **Utilité** : Au lieu de lire une stack-trace ardue dans l'onglet Logs, l'UI affiche un composant magique "Explication IA : L'erreur vient du port 18789 qui est déjà utilisé. Cliquez ici pour libérer le port." +- **Technologie** : Prompt engineering injectant les 50 dernières lignes du log d'erreur à un modèle LLM interne au dashboard. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 19b2e25..186b360 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,13 @@ "name": "foxy-dev-team-dashboard", "version": "2.0.0", "dependencies": { + "@codemirror/lang-json": "^6.0.2", + "@codemirror/lang-markdown": "^6.5.0", + "@codemirror/lang-yaml": "^6.1.2", + "@codemirror/state": "^6.6.0", + "@codemirror/theme-one-dark": "^6.1.3", + "@codemirror/view": "^6.40.0", + "@uiw/react-codemirror": "^4.25.8", "react": "^19.0.0", "react-dom": "^19.0.0", "react-router-dom": "^7.1.0" @@ -256,6 +263,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -304,6 +320,184 @@ "node": ">=6.9.0" } }, + "node_modules/@codemirror/autocomplete": { + "version": "6.20.1", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.1.tgz", + "integrity": "sha512-1cvg3Vz1dSSToCNlJfRA2WSI4ht3K+WplO0UMOgmUYPivCyy2oueZY6Lx7M9wThm7SDUBViRmuT+OG/i8+ON9A==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.3.tgz", + "integrity": "sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.6.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-css": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz", + "integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.2", + "@lezer/css": "^1.1.7" + } + }, + "node_modules/@codemirror/lang-html": { + "version": "6.4.11", + "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz", + "integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/lang-css": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/language": "^6.4.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/css": "^1.1.0", + "@lezer/html": "^1.3.12" + } + }, + "node_modules/@codemirror/lang-javascript": { + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.5.tgz", + "integrity": "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.6.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/javascript": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-json": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz", + "integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/json": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-markdown": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.5.0.tgz", + "integrity": "sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.7.1", + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.3.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.2.1", + "@lezer/markdown": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-yaml": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-yaml/-/lang-yaml-6.1.2.tgz", + "integrity": "sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.2.0", + "@lezer/lr": "^1.0.0", + "@lezer/yaml": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.12.2", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.2.tgz", + "integrity": "sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.9.5", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.5.tgz", + "integrity": "sha512-GElsbU9G7QT9xXhpUg1zWGmftA/7jamh+7+ydKRuT0ORpWS3wOSP0yT1FOlIZa7mIJjpVPipErsyvVqB9cfTFA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.35.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.6.0.tgz", + "integrity": "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.37.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.6.0.tgz", + "integrity": "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/theme-one-dark": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz", + "integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.40.0", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.40.0.tgz", + "integrity": "sha512-WA0zdU7xfF10+5I3HhUUq3kqOx3KjqmtQ9lqZjfK7jtYk4G72YW9rezcSywpaUMCWOMlq+6E0pO1IWg1TNIhtg==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.6.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -796,6 +990,101 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lezer/common": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.1.tgz", + "integrity": "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==", + "license": "MIT" + }, + "node_modules/@lezer/css": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.1.tgz", + "integrity": "sha512-PYAKeUVBo3HFThruRyp/iK91SwiZJnzXh8QzkQlwijB5y+N5iB28+iLk78o2zmKqqV0uolNhCwFqB8LA7b0Svg==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/highlight": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.3.0" + } + }, + "node_modules/@lezer/html": { + "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.13.tgz", + "integrity": "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/javascript": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz", + "integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.1.3", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/json": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz", + "integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.8.tgz", + "integrity": "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/markdown": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.6.3.tgz", + "integrity": "sha512-jpGm5Ps+XErS+xA4urw7ogEGkeZOahVQF21Z6oECF0sj+2liwZopd2+I8uH5I/vZsRuuze3OxBREIANLf6KKUw==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@lezer/yaml": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@lezer/yaml/-/yaml-1.0.4.tgz", + "integrity": "sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.4.0" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "license": "MIT" + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -1497,6 +1786,59 @@ "@types/react": "^19.2.0" } }, + "node_modules/@uiw/codemirror-extensions-basic-setup": { + "version": "4.25.8", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.25.8.tgz", + "integrity": "sha512-9Rr+liiBmK4xzZHszL+twNRJApthqmITBwDP3emNTtTrkBFN4gHlqfp+nodKmoVt1+bUH1qQCtyqt+7dbDTHiw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@codemirror/autocomplete": ">=6.0.0", + "@codemirror/commands": ">=6.0.0", + "@codemirror/language": ">=6.0.0", + "@codemirror/lint": ">=6.0.0", + "@codemirror/search": ">=6.0.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/view": ">=6.0.0" + } + }, + "node_modules/@uiw/react-codemirror": { + "version": "4.25.8", + "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.25.8.tgz", + "integrity": "sha512-A0aLOuJZm2yJ+U9GlMFwxwFciztjd5LhcAG4SMqFxdD58wH+sCQXuY4UU5J2hqgS390qAlShtUgREvJPUonbuQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.6", + "@codemirror/commands": "^6.1.0", + "@codemirror/state": "^6.1.1", + "@codemirror/theme-one-dark": "^6.0.0", + "@uiw/codemirror-extensions-basic-setup": "4.25.8", + "codemirror": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.11.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/theme-one-dark": ">=6.0.0", + "@codemirror/view": ">=6.0.0", + "codemirror": ">=6.0.0", + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -1586,6 +1928,21 @@ ], "license": "CC-BY-4.0" }, + "node_modules/codemirror": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", + "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -1606,6 +1963,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -2316,6 +2679,12 @@ "node": ">=0.10.0" } }, + "node_modules/style-mod": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", + "license": "MIT" + }, "node_modules/tailwindcss": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", @@ -2474,6 +2843,12 @@ } } }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 31dba31..4ea497f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,17 +9,24 @@ "preview": "vite preview" }, "dependencies": { + "@codemirror/lang-json": "^6.0.2", + "@codemirror/lang-markdown": "^6.5.0", + "@codemirror/lang-yaml": "^6.1.2", + "@codemirror/state": "^6.6.0", + "@codemirror/theme-one-dark": "^6.1.3", + "@codemirror/view": "^6.40.0", + "@uiw/react-codemirror": "^4.25.8", "react": "^19.0.0", "react-dom": "^19.0.0", "react-router-dom": "^7.1.0" }, "devDependencies": { + "@tailwindcss/vite": "^4.0.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@vitejs/plugin-react": "^4.3.4", + "tailwindcss": "^4.0.0", "typescript": "~5.7.0", - "vite": "^6.0.0", - "@tailwindcss/vite": "^4.0.0", - "tailwindcss": "^4.0.0" + "vite": "^6.0.0" } } diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 75ecf64..ece8e4e 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -173,6 +173,8 @@ export interface OpenClawAgent { system_prompt?: string; system_prompt_preview?: string; file_count?: number; + has_workspace?: boolean; + identity_files?: { name: string; size: number }[]; } export interface OpenClawSkill { @@ -209,6 +211,41 @@ export interface FileTreeNode { children?: FileTreeNode[]; } +export interface ReadFileResult { + content: string | null; + pretty?: string | null; + binary: boolean; + path: string; + size: number; + language: string; + modified?: string; +} + +export interface WriteFileResult { + message: string; + size: number; +} + +export interface IdentityFile { + name: string; + size: number; + modified?: string | null; +} + +export interface AgentFilesResult { + files: IdentityFile[]; + other_files: IdentityFile[]; + workspace: string; + exists: boolean; +} + +export interface AgentFileResult { + content: string; + filename: string; + size: number; + language: string; +} + // ─── HTTP Helpers ─────────────────────────────────────────────────────────── async function request(path: string, options?: RequestInit): Promise { @@ -291,15 +328,31 @@ export const api = { // OpenClaw openclawStatus: () => request('/api/openclaw/status'), - openclawConfig: () => request<{ config: Record | null; error?: string }>('/api/openclaw/config'), + openclawConfig: () => request<{ config: Record }>('/api/openclaw/config'), openclawAgents: () => request<{ agents: OpenClawAgent[]; count: number }>('/api/openclaw/agents'), + openclawDeleteAgent: (name: string) => request<{ message: string }>(`/api/openclaw/agents/${encodeURIComponent(name)}`, { method: 'DELETE' }), + openclawAgentFiles: (name: string) => request(`/api/openclaw/agents/${encodeURIComponent(name)}/files`), + openclawReadAgentFile: (name: string, filename: string) => + request(`/api/openclaw/agents/${encodeURIComponent(name)}/file?filename=${encodeURIComponent(filename)}`), + openclawWriteAgentFile: (name: string, filename: string, content: string) => + request(`/api/openclaw/agents/${encodeURIComponent(name)}/file?filename=${encodeURIComponent(filename)}`, { + method: 'PUT', + body: JSON.stringify({ content }), + }), openclawSkills: () => request<{ skills: OpenClawSkill[]; count: number }>('/api/openclaw/skills'), - openclawLogs: (params?: { lines?: number; level?: string }) => { - const qs = params ? '?' + new URLSearchParams( - Object.fromEntries(Object.entries(params).filter(([, v]) => v != null).map(([k, v]) => [k, String(v)])) - ).toString() : ''; - return request(`/api/openclaw/logs${qs}`); + openclawLogs: (params: { lines?: number; level?: string }) => { + const qs = new URLSearchParams(); + if (params.lines) qs.set('lines', params.lines.toString()); + if (params.level) qs.set('level', params.level); + return request(`/api/openclaw/logs?${qs.toString()}`); }, openclawModels: () => request('/api/openclaw/models'), openclawFilesystem: () => request<{ root: string; tree: FileTreeNode[] }>('/api/openclaw/filesystem'), + openclawReadFile: (path: string) => + request(`/api/openclaw/file?path=${encodeURIComponent(path)}`), + openclawWriteFile: (path: string, content: string) => + request(`/api/openclaw/file?path=${encodeURIComponent(path)}`, { + method: 'PUT', + body: JSON.stringify({ content }), + }), }; diff --git a/frontend/src/pages/OpenClaw.tsx b/frontend/src/pages/OpenClaw.tsx index 1680c47..a83a102 100644 --- a/frontend/src/pages/OpenClaw.tsx +++ b/frontend/src/pages/OpenClaw.tsx @@ -1,7 +1,12 @@ import { useEffect, useState, useCallback, useRef } from 'react'; +import CodeMirror from '@uiw/react-codemirror'; +import { json } from '@codemirror/lang-json'; +import { markdown } from '@codemirror/lang-markdown'; +import { yaml } from '@codemirror/lang-yaml'; +import { oneDark } from '@codemirror/theme-one-dark'; import type { OpenClawStatus, OpenClawAgent, OpenClawSkill, OpenClawLogs, - OpenClawModels, FileTreeNode, + OpenClawModels, FileTreeNode, ReadFileResult } from '../api/client'; import { api } from '../api/client'; @@ -11,6 +16,7 @@ import { api } from '../api/client'; const TABS = [ { id: 'status', label: 'Status', icon: '🔋' }, + { id: 'filesystem', label: 'Arborescence', icon: '📂' }, { id: 'config', label: 'Configuration', icon: '⚙️' }, { id: 'agents', label: 'Agents', icon: '🤖' }, { id: 'skills', label: 'Skills', icon: '🧩' }, @@ -93,6 +99,7 @@ export default function OpenClawPage() { {/* Tab Content */}
{activeTab === 'status' && } + {activeTab === 'filesystem' && } {activeTab === 'config' && } {activeTab === 'agents' && } {activeTab === 'skills' && } @@ -109,15 +116,6 @@ export default function OpenClawPage() { // ═════════════════════════════════════════════════════════════════════════════════ function StatusTab({ status }: { status: OpenClawStatus | null }) { - const [tree, setTree] = useState([]); - const [treeLoading, setTreeLoading] = useState(true); - - useEffect(() => { - api.openclawFilesystem() - .then(d => { setTree(d.tree); setTreeLoading(false); }) - .catch(() => setTreeLoading(false)); - }, []); - if (!status) return ; const metrics = [ @@ -172,24 +170,6 @@ function StatusTab({ status }: { status: OpenClawStatus | null }) { ))}
- - {/* Filesystem tree */} -
-

- 📂 Arborescence FOXY_HOME -

- {treeLoading ? ( -
Chargement de l'arborescence…
- ) : tree.length === 0 ? ( -

Aucun fichier trouvé

- ) : ( -
- {tree.map(node => ( - - ))} -
- )} -
); } @@ -217,7 +197,7 @@ function MetricCard({ label, value, accent, icon }: { label: string; value: stri } -function TreeNode({ node, depth }: { node: FileTreeNode; depth: number }) { +function TreeNode({ node, depth, onSelect }: { node: FileTreeNode; depth: number, onSelect?: (n: FileTreeNode) => void }) { const [open, setOpen] = useState(depth < 1); const isDir = node.type === 'directory'; const indent = depth * 20; @@ -225,11 +205,14 @@ function TreeNode({ node, depth }: { node: FileTreeNode; depth: number }) { return (
isDir && setOpen(!open)} + onClick={() => { + if (isDir) setOpen(!open); + else onSelect?.(node); + }} > {isDir ? ( {open ? '📂' : '📁'} @@ -242,7 +225,7 @@ function TreeNode({ node, depth }: { node: FileTreeNode; depth: number }) { )}
{isDir && open && node.children?.map(child => ( - + ))}
); @@ -356,22 +339,83 @@ function AgentsTab() { const [loading, setLoading] = useState(true); const [expandedAgent, setExpandedAgent] = useState(null); - useEffect(() => { + const [deleting, setDeleting] = useState(null); + const [activeFile, setActiveFile] = useState<{agent: string, file: string} | null>(null); + const [fileContent, setFileContent] = useState(''); + const [fileLoading, setFileLoading] = useState(false); + const [saving, setSaving] = useState(false); + + const loadAgents = () => { + setLoading(true); api.openclawAgents() .then(d => { setAgents(d.agents); setLoading(false); }) .catch(() => setLoading(false)); - }, []); + }; - if (loading) return ; + useEffect(() => { loadAgents(); }, []); + + const handleDelete = async (e: React.MouseEvent, name: string) => { + e.stopPropagation(); + if (!confirm(`Voulez-vous vraiment supprimer l'agent "${name}" ?\n\n⚠️ Cette action supprimera définitivement son dossier de configuration.`)) return; + setDeleting(name); + try { + await api.openclawDeleteAgent(name); + if (expandedAgent === name) setExpandedAgent(null); + loadAgents(); + } catch(err) { + console.error(err); + alert('Erreur lors de la suppression: ' + String(err)); + } finally { + setDeleting(null); + } + }; + + const openFile = async (agentName: string, filename: string) => { + if (activeFile?.agent === agentName && activeFile?.file === filename) { + setActiveFile(null); // toggle close + return; + } + setActiveFile({ agent: agentName, file: filename }); + setFileLoading(true); + try { + const data = await api.openclawReadAgentFile(agentName, filename); + setFileContent(data.content || ''); + } catch(err) { + console.error(err); + setFileContent('Erreur de chargement du fichier.'); + } finally { + setFileLoading(false); + } + }; + + const saveFile = async () => { + if (!activeFile) return; + setSaving(true); + try { + await api.openclawWriteAgentFile(activeFile.agent, activeFile.file, fileContent); + loadAgents(); // refresh file sizes + setActiveFile(null); // close the editor after saving + } catch(err) { + console.error(err); + alert('Erreur lors de la sauvegarde: ' + String(err)); + } finally { + setSaving(false); + } + }; + + if (loading && agents.length === 0) return ; if (agents.length === 0) return ; return (
{/* Agent count header */} -
- 🤖 - {agents.length} agents - détectés dans le répertoire OpenClaw +
+
+ 🤖 + {agents.length} agents + détectés dans le répertoire OpenClaw +
+
{/* Agent grid */} @@ -384,55 +428,122 @@ function AgentsTab() { }`} >
setExpandedAgent(expandedAgent === agent.name ? null : agent.name)} + className={`p-5 cursor-pointer hover:bg-glass-hover transition-colors ${deleting === agent.name ? 'opacity-50 pointer-events-none' : ''}`} + onClick={() => { setExpandedAgent(expandedAgent === agent.name ? null : agent.name); setActiveFile(null); }} >
-
+
🤖
-
-

{agent.name}

-
+
+

{agent.name}

+
{agent.type} {agent.model && ( - {agent.model} + {agent.model} )} - {agent.file_count != null && ( - {agent.file_count} fichiers + {agent.has_workspace && ( + ws ✓ )}
- {expandedAgent === agent.name ? '▲' : '▼'} -
- - {/* Config files */} - {agent.config_files && agent.config_files.length > 0 && ( -
- {agent.config_files.map(f => ( - {f} - ))} +
+ + + {expandedAgent === agent.name ? '▲' : '▼'} +
- )} +
{/* Expanded details */} {expandedAgent === agent.name && ( -
- {agent.system_prompt && ( +
+ + {/* Agent Identity Files Manager */} + {agent.has_workspace && agent.identity_files && agent.identity_files.length > 0 && (
-

System Prompt

-
+

+ 🧠 Fichiers d'identité +

+
+ {agent.identity_files.map(file => { + const isActive = activeFile?.agent === agent.name && activeFile?.file === file.name; + return ( + + ); + })} +
+ + {/* Identity File Editor */} + {activeFile?.agent === agent.name && ( +
+
+ workspace/{agent.name}/{activeFile.file} +
+ + +
+
+ {fileLoading ? ( +
Chargement…
+ ) : ( +
+ setFileContent(val)} + basicSetup={{ lineNumbers: true, foldGutter: false, highlightActiveLine: true, tabSize: 2 }} + /> +
+ )} +
+ )} +
+ )} + + {/* System Prompt (Read-only overlay for old config style) */} + {agent.system_prompt && !activeFile && ( +
+

Instructions principales

+
{agent.system_prompt}
)} - {agent.config && ( + + {/* Configuration (Read-only) */} + {agent.config && !activeFile && (
-

Configuration complète

-
+                    

+ Configuration JSON/YAML + {agent.config_files?.join(', ')} +

+
                       {JSON.stringify(agent.config, null, 2)}
                     
@@ -815,3 +926,186 @@ function EmptyState({ message }: { message: string }) {
); } + + +// ═════════════════════════════════════════════════════════════════════════════════ +// Tab 7 — Filesystem (Arborescence) +// ═════════════════════════════════════════════════════════════════════════════════ + +function FilesystemTab() { + const [tree, setTree] = useState([]); + const [treeLoading, setTreeLoading] = useState(true); + const [selectedFile, setSelectedFile] = useState(null); + const [fileLoading, setFileLoading] = useState(false); + const [editMode, setEditMode] = useState(false); + const [editContent, setEditContent] = useState(''); + const [saving, setSaving] = useState(false); + + useEffect(() => { + loadTree(); + }, []); + + const loadTree = () => { + setTreeLoading(true); + api.openclawFilesystem() + .then(d => { setTree(d.tree); setTreeLoading(false); }) + .catch(() => setTreeLoading(false)); + }; + + const handleSelectFile = async (node: FileTreeNode) => { + if (node.type !== 'file') return; + setFileLoading(true); + setEditMode(false); + try { + const data = await api.openclawReadFile(node.path); + setSelectedFile(data); + setEditContent(data.pretty || data.content || ''); + } catch (err) { + console.error(err); + } finally { + setFileLoading(false); + } + }; + + const handleSave = async () => { + if (!selectedFile) return; + setSaving(true); + try { + await api.openclawWriteFile(selectedFile.path, editContent); + const data = await api.openclawReadFile(selectedFile.path); + setSelectedFile(data); + setEditMode(false); + } catch (err) { + console.error(err); + } finally { + setSaving(false); + } + }; + + const getExtensions = () => { + if (!selectedFile) return []; + const lang = selectedFile.language; + if (lang === 'json') return [json()]; + if (lang === 'markdown') return [markdown()]; + if (lang === 'yaml') return [yaml()]; + return []; + }; + + return ( +
+ {/* Sidebar Tree */} +
+
+

+ 📂 Arborescence +

+ +
+
+ {treeLoading ? ( +
Chargement de l'arborescence…
+ ) : tree.length === 0 ? ( +

Aucun fichier trouvé

+ ) : ( +
+ {tree.map(node => ( + + ))} +
+ )} +
+
+ + {/* Editor Panel */} +
+ {fileLoading ? ( +
+
+ + Ouverture du fichier… +
+
+ ) : !selectedFile ? ( +
+ 📄 +

Sélectionnez un fichier pour le visualiser

+
+ ) : ( +
+ {/* Editor Header */} +
+
+

+ 📄 {selectedFile.path.split('/').pop()} +

+
+ {selectedFile.path} + + {formatSize(selectedFile.size)} + + {selectedFile.language} +
+
+
+ {!selectedFile.binary && ( + <> + {editMode ? ( + <> + + + + ) : ( + + )} + + )} +
+
+ + {/* Editor Body */} +
+ {selectedFile.binary ? ( +
+
+
📦
+

Fichier binaire. Aperçu non disponible.

+
+
+ ) : editMode ? ( +
+ setEditContent(val)} + basicSetup={{ + lineNumbers: true, + foldGutter: true, + highlightActiveLine: true, + tabSize: 2, + }} + /> +
+ ) : ( +
+
+
+                      {selectedFile.pretty || selectedFile.content}
+                    
+
+
+ )} +
+
+ )} +
+
+ ); +}