feat: Add OpenClaw module with dedicated frontend page, backend API, and documentation.
This commit is contained in:
parent
69642a17ea
commit
2a22990461
@ -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_name>/
|
||||
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/<agent_name>/."""
|
||||
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))
|
||||
|
||||
33
docs/innovations_openclaw.md
Normal file
33
docs/innovations_openclaw.md
Normal file
@ -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.
|
||||
375
frontend/package-lock.json
generated
375
frontend/package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<T>(path: string, options?: RequestInit): Promise<T> {
|
||||
@ -291,15 +328,31 @@ export const api = {
|
||||
|
||||
// OpenClaw
|
||||
openclawStatus: () => request<OpenClawStatus>('/api/openclaw/status'),
|
||||
openclawConfig: () => request<{ config: Record<string, unknown> | null; error?: string }>('/api/openclaw/config'),
|
||||
openclawConfig: () => request<{ config: Record<string, unknown> }>('/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<AgentFilesResult>(`/api/openclaw/agents/${encodeURIComponent(name)}/files`),
|
||||
openclawReadAgentFile: (name: string, filename: string) =>
|
||||
request<AgentFileResult>(`/api/openclaw/agents/${encodeURIComponent(name)}/file?filename=${encodeURIComponent(filename)}`),
|
||||
openclawWriteAgentFile: (name: string, filename: string, content: string) =>
|
||||
request<WriteFileResult>(`/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<OpenClawLogs>(`/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<OpenClawLogs>(`/api/openclaw/logs?${qs.toString()}`);
|
||||
},
|
||||
openclawModels: () => request<OpenClawModels>('/api/openclaw/models'),
|
||||
openclawFilesystem: () => request<{ root: string; tree: FileTreeNode[] }>('/api/openclaw/filesystem'),
|
||||
openclawReadFile: (path: string) =>
|
||||
request<ReadFileResult>(`/api/openclaw/file?path=${encodeURIComponent(path)}`),
|
||||
openclawWriteFile: (path: string, content: string) =>
|
||||
request<WriteFileResult>(`/api/openclaw/file?path=${encodeURIComponent(path)}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ content }),
|
||||
}),
|
||||
};
|
||||
|
||||
@ -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 */}
|
||||
<div className="animate-fade-in">
|
||||
{activeTab === 'status' && <StatusTab status={status} />}
|
||||
{activeTab === 'filesystem' && <FilesystemTab />}
|
||||
{activeTab === 'config' && <ConfigTab />}
|
||||
{activeTab === 'agents' && <AgentsTab />}
|
||||
{activeTab === 'skills' && <SkillsTab />}
|
||||
@ -109,15 +116,6 @@ export default function OpenClawPage() {
|
||||
// ═════════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function StatusTab({ status }: { status: OpenClawStatus | null }) {
|
||||
const [tree, setTree] = useState<FileTreeNode[]>([]);
|
||||
const [treeLoading, setTreeLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
api.openclawFilesystem()
|
||||
.then(d => { setTree(d.tree); setTreeLoading(false); })
|
||||
.catch(() => setTreeLoading(false));
|
||||
}, []);
|
||||
|
||||
if (!status) return <EmptyState message="Impossible de charger le status" />;
|
||||
|
||||
const metrics = [
|
||||
@ -172,24 +170,6 @@ function StatusTab({ status }: { status: OpenClawStatus | null }) {
|
||||
<MetricCard key={m.label} {...m} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Filesystem tree */}
|
||||
<div className="glass-card p-6">
|
||||
<h2 className="text-lg font-bold text-white mb-4 flex items-center gap-2">
|
||||
<span>📂</span> Arborescence FOXY_HOME
|
||||
</h2>
|
||||
{treeLoading ? (
|
||||
<div className="text-gray-500 text-sm animate-pulse">Chargement de l'arborescence…</div>
|
||||
) : tree.length === 0 ? (
|
||||
<p className="text-gray-500 text-sm">Aucun fichier trouvé</p>
|
||||
) : (
|
||||
<div className="font-mono text-xs max-h-80 overflow-y-auto space-y-0.5">
|
||||
{tree.map(node => (
|
||||
<TreeNode key={node.path} node={node} depth={0} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -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 (
|
||||
<div>
|
||||
<div
|
||||
className={`flex items-center gap-2 py-1 px-2 rounded-lg transition-colors ${
|
||||
isDir ? 'hover:bg-surface-700/50 cursor-pointer' : 'text-gray-400'
|
||||
className={`flex items-center gap-2 py-1 px-2 rounded-lg transition-colors cursor-pointer ${
|
||||
isDir ? 'hover:bg-surface-700/50' : 'hover:bg-surface-700/50 text-gray-400'
|
||||
}`}
|
||||
style={{ paddingLeft: `${indent + 8}px` }}
|
||||
onClick={() => isDir && setOpen(!open)}
|
||||
onClick={() => {
|
||||
if (isDir) setOpen(!open);
|
||||
else onSelect?.(node);
|
||||
}}
|
||||
>
|
||||
{isDir ? (
|
||||
<span className="text-yellow-400 w-4 text-center">{open ? '📂' : '📁'}</span>
|
||||
@ -242,7 +225,7 @@ function TreeNode({ node, depth }: { node: FileTreeNode; depth: number }) {
|
||||
)}
|
||||
</div>
|
||||
{isDir && open && node.children?.map(child => (
|
||||
<TreeNode key={child.path} node={child} depth={depth + 1} />
|
||||
<TreeNode key={child.path} node={child} depth={depth + 1} onSelect={onSelect} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
@ -356,22 +339,83 @@ function AgentsTab() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [expandedAgent, setExpandedAgent] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const [deleting, setDeleting] = useState<string | null>(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 <LoadingSpinner />;
|
||||
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 <LoadingSpinner />;
|
||||
if (agents.length === 0) return <EmptyState message="Aucun agent trouvé dans le répertoire agents/" />;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Agent count header */}
|
||||
<div className="glass-card p-4 flex items-center gap-3">
|
||||
<span className="text-lg">🤖</span>
|
||||
<span className="text-white font-semibold">{agents.length} agents</span>
|
||||
<span className="text-gray-500 text-sm">détectés dans le répertoire OpenClaw</span>
|
||||
<div className="glass-card p-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-lg">🤖</span>
|
||||
<span className="text-white font-semibold">{agents.length} agents</span>
|
||||
<span className="text-gray-500 text-sm hidden sm:inline">détectés dans le répertoire OpenClaw</span>
|
||||
</div>
|
||||
<button onClick={loadAgents} className="text-gray-400 hover:text-white transition-colors" title="Actualiser">🔄</button>
|
||||
</div>
|
||||
|
||||
{/* Agent grid */}
|
||||
@ -384,55 +428,122 @@ function AgentsTab() {
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className="p-5 cursor-pointer hover:bg-glass-hover transition-colors"
|
||||
onClick={() => 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); }}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-amber-500/20 to-orange-600/10 border border-amber-500/20 flex items-center justify-center text-xl">
|
||||
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-amber-500/20 to-orange-600/10 border border-amber-500/20 flex items-center justify-center text-xl shrink-0">
|
||||
🤖
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-white font-semibold text-sm">{agent.name}</h3>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<div className="min-w-0">
|
||||
<h3 className="text-white font-semibold text-sm truncate pr-2">{agent.name}</h3>
|
||||
<div className="flex items-center gap-2 mt-1 flex-wrap">
|
||||
<span className="badge bg-surface-600 text-gray-300 text-[10px]">{agent.type}</span>
|
||||
{agent.model && (
|
||||
<span className="badge bg-purple-500/15 text-purple-300 text-[10px]">{agent.model}</span>
|
||||
<span className="badge bg-purple-500/15 text-purple-300 text-[10px] truncate max-w-[150px]">{agent.model}</span>
|
||||
)}
|
||||
{agent.file_count != null && (
|
||||
<span className="text-[10px] text-gray-600">{agent.file_count} fichiers</span>
|
||||
{agent.has_workspace && (
|
||||
<span className="badge bg-green-500/15 text-green-300 text-[10px]" title="A un dossier workspace défini">ws ✓</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-gray-600 text-sm">{expandedAgent === agent.name ? '▲' : '▼'}</span>
|
||||
</div>
|
||||
|
||||
{/* Config files */}
|
||||
{agent.config_files && agent.config_files.length > 0 && (
|
||||
<div className="flex gap-2 mt-3 flex-wrap">
|
||||
{agent.config_files.map(f => (
|
||||
<span key={f} className="px-2 py-0.5 rounded-md bg-surface-700 text-gray-400 text-[10px] font-mono">{f}</span>
|
||||
))}
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<button
|
||||
onClick={(e) => handleDelete(e, agent.name)}
|
||||
className="p-1.5 rounded-lg text-gray-500 hover:text-red-400 hover:bg-red-400/10 transition-colors"
|
||||
title="Supprimer l'agent"
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
<span className="text-gray-600 text-sm ml-2 w-4 text-center">
|
||||
{expandedAgent === agent.name ? '▲' : '▼'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expanded details */}
|
||||
{expandedAgent === agent.name && (
|
||||
<div className="border-t border-glass-border p-5 bg-surface-900/40 space-y-4">
|
||||
{agent.system_prompt && (
|
||||
<div className="border-t border-glass-border p-5 bg-surface-900/40 space-y-6">
|
||||
|
||||
{/* Agent Identity Files Manager */}
|
||||
{agent.has_workspace && agent.identity_files && agent.identity_files.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-xs text-gray-500 uppercase tracking-wider mb-2">System Prompt</h4>
|
||||
<div className="p-3 rounded-xl bg-surface-800 text-xs text-gray-300 font-mono whitespace-pre-wrap max-h-48 overflow-y-auto leading-relaxed">
|
||||
<h4 className="text-xs text-gray-400 font-bold uppercase tracking-wider mb-3 flex items-center gap-2">
|
||||
<span>🧠</span> Fichiers d'identité
|
||||
</h4>
|
||||
<div className="flex gap-2 flex-wrap mb-3">
|
||||
{agent.identity_files.map(file => {
|
||||
const isActive = activeFile?.agent === agent.name && activeFile?.file === file.name;
|
||||
return (
|
||||
<button
|
||||
key={file.name}
|
||||
onClick={() => openFile(agent.name, file.name)}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-mono transition-all ${
|
||||
isActive
|
||||
? 'bg-gradient-to-r from-purple-600 to-indigo-600 text-white shadow-[0_0_10px_rgba(147,51,234,0.3)]'
|
||||
: file.size > 0
|
||||
? 'bg-surface-800 text-gray-300 border border-surface-600 hover:border-purple-500/50 hover:bg-surface-700'
|
||||
: 'bg-surface-800 text-gray-600 border border-surface-700/50 opacity-60 hover:opacity-100 hover:text-gray-400'
|
||||
}`}
|
||||
>
|
||||
{file.name}
|
||||
{file.size > 0 && <span className="ml-1.5 opacity-60 text-[9px]">{formatSize(file.size)}</span>}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Identity File Editor */}
|
||||
{activeFile?.agent === agent.name && (
|
||||
<div className="rounded-xl border border-glass-border bg-[#282c34] overflow-hidden shadow-inner mt-4 animate-fade-in relative">
|
||||
<div className="flex items-center justify-between px-3 py-2 bg-surface-900/80 border-b border-glass-border">
|
||||
<span className="text-xs font-mono text-purple-300">workspace/{agent.name}/{activeFile.file}</span>
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => setActiveFile(null)} className="px-2 py-1 rounded text-[10px] text-gray-400 hover:bg-surface-700 hover:text-white transition-colors">Fermer</button>
|
||||
<button onClick={saveFile} disabled={saving} className="px-3 py-1 rounded bg-purple-600/80 hover:bg-purple-500 text-white text-[10px] font-medium transition-colors">
|
||||
{saving ? '...' : 'Sauvegarder'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{fileLoading ? (
|
||||
<div className="p-8 text-center text-gray-500 text-sm animate-pulse">Chargement…</div>
|
||||
) : (
|
||||
<div className="max-h-80 overflow-y-auto custom-scrollbar [&_.cm-editor]:bg-transparent [&_.cm-scroller]:font-mono [&_.cm-scroller]:text-xs">
|
||||
<CodeMirror
|
||||
value={fileContent}
|
||||
theme={oneDark}
|
||||
extensions={[markdown()]}
|
||||
onChange={(val) => setFileContent(val)}
|
||||
basicSetup={{ lineNumbers: true, foldGutter: false, highlightActiveLine: true, tabSize: 2 }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* System Prompt (Read-only overlay for old config style) */}
|
||||
{agent.system_prompt && !activeFile && (
|
||||
<div>
|
||||
<h4 className="text-xs text-gray-400 font-bold uppercase tracking-wider mb-2">Instructions principales</h4>
|
||||
<div className="px-4 py-3 rounded-xl bg-surface-800/80 text-xs text-gray-300 font-mono whitespace-pre-wrap max-h-48 overflow-y-auto leading-relaxed custom-scrollbar border border-surface-700/50">
|
||||
{agent.system_prompt}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{agent.config && (
|
||||
|
||||
{/* Configuration (Read-only) */}
|
||||
{agent.config && !activeFile && (
|
||||
<div>
|
||||
<h4 className="text-xs text-gray-500 uppercase tracking-wider mb-2">Configuration complète</h4>
|
||||
<pre className="p-3 rounded-xl bg-surface-800 text-xs text-gray-300 font-mono whitespace-pre-wrap max-h-64 overflow-y-auto leading-relaxed">
|
||||
<h4 className="text-xs text-gray-400 font-bold uppercase tracking-wider mb-2 flex justify-between items-center">
|
||||
<span>Configuration JSON/YAML</span>
|
||||
<span className="font-mono lowercase text-[10px] text-gray-500">{agent.config_files?.join(', ')}</span>
|
||||
</h4>
|
||||
<pre className="px-4 py-3 rounded-xl bg-surface-800/80 text-[11px] text-gray-400 font-mono whitespace-pre-wrap max-h-64 overflow-y-auto leading-relaxed custom-scrollbar border border-surface-700/50">
|
||||
{JSON.stringify(agent.config, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
@ -815,3 +926,186 @@ function EmptyState({ message }: { message: string }) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════════════
|
||||
// Tab 7 — Filesystem (Arborescence)
|
||||
// ═════════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function FilesystemTab() {
|
||||
const [tree, setTree] = useState<FileTreeNode[]>([]);
|
||||
const [treeLoading, setTreeLoading] = useState(true);
|
||||
const [selectedFile, setSelectedFile] = useState<ReadFileResult | null>(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 (
|
||||
<div className="flex gap-6 h-[calc(100vh-250px)] min-h-[600px]">
|
||||
{/* Sidebar Tree */}
|
||||
<div className="glass-card w-1/3 p-4 flex flex-col border border-glass-border">
|
||||
<div className="flex items-center justify-between mb-4 px-2">
|
||||
<h2 className="text-lg font-bold text-white flex items-center gap-2">
|
||||
<span>📂</span> Arborescence
|
||||
</h2>
|
||||
<button onClick={loadTree} className="p-1.5 rounded-lg text-gray-400 hover:text-white hover:bg-surface-700 transition-colors" title="Actualiser">🔄</button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto font-mono text-[13px] pr-2 custom-scrollbar">
|
||||
{treeLoading ? (
|
||||
<div className="text-gray-500 animate-pulse px-2">Chargement de l'arborescence…</div>
|
||||
) : tree.length === 0 ? (
|
||||
<p className="text-gray-500 px-2">Aucun fichier trouvé</p>
|
||||
) : (
|
||||
<div className="space-y-0.5">
|
||||
{tree.map(node => (
|
||||
<TreeNode key={node.path} node={node} depth={0} onSelect={handleSelectFile} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Editor Panel */}
|
||||
<div className="glass-card w-2/3 flex flex-col overflow-hidden border border-glass-border shadow-[0_8px_30px_rgb(0,0,0,0.5)]">
|
||||
{fileLoading ? (
|
||||
<div className="flex-1 flex items-center justify-center bg-surface-900/40">
|
||||
<div className="text-gray-400 flex flex-col items-center gap-3">
|
||||
<span className="text-3xl animate-spin-slow">⏳</span>
|
||||
<span className="text-sm tracking-wide">Ouverture du fichier…</span>
|
||||
</div>
|
||||
</div>
|
||||
) : !selectedFile ? (
|
||||
<div className="flex-1 flex items-center justify-center text-gray-500 flex-col gap-4 bg-surface-900/40">
|
||||
<span className="text-5xl opacity-30 grayscale">📄</span>
|
||||
<p className="tracking-wide">Sélectionnez un fichier pour le visualiser</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Editor Header */}
|
||||
<div className="p-4 bg-surface-800 border-b border-glass-border flex items-center justify-between z-10 shrink-0">
|
||||
<div className="min-w-0 pr-4">
|
||||
<h3 className="text-white font-medium flex items-center gap-2 text-base truncate">
|
||||
<span className="text-xl">📄</span> {selectedFile.path.split('/').pop()}
|
||||
</h3>
|
||||
<div className="flex items-center gap-3 mt-1 text-[11px] text-gray-400 font-mono">
|
||||
<span className="truncate max-w-[200px] md:max-w-xs" title={selectedFile.path}>{selectedFile.path}</span>
|
||||
<span className="opacity-50">•</span>
|
||||
<span>{formatSize(selectedFile.size)}</span>
|
||||
<span className="opacity-50">•</span>
|
||||
<span className="uppercase text-purple-300 bg-purple-500/10 px-1.5 py-0.5 rounded">{selectedFile.language}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 shrink-0">
|
||||
{!selectedFile.binary && (
|
||||
<>
|
||||
{editMode ? (
|
||||
<>
|
||||
<button onClick={() => setEditMode(false)} disabled={saving} className="px-3.5 py-1.5 rounded-lg bg-surface-700 text-gray-300 text-sm hover:bg-surface-600 transition-colors">
|
||||
Annuler
|
||||
</button>
|
||||
<button onClick={handleSave} disabled={saving} className={`px-4 py-1.5 rounded-lg bg-gradient-to-r from-emerald-600 to-teal-600 text-white text-sm font-medium hover:from-emerald-500 hover:to-teal-500 transition-all shadow-[0_0_12px_rgba(16,185,129,0.3)] flex items-center gap-2 ${saving ? 'opacity-50 cursor-not-allowed scale-95' : 'hover:scale-105'}`}>
|
||||
{saving ? 'Sauvegarde…' : '💾 Sauvegarder'}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button onClick={() => { setEditMode(true); setEditContent(selectedFile.pretty || selectedFile.content || ''); }} className="px-4 py-1.5 rounded-lg bg-gradient-to-r from-purple-600 to-indigo-600 text-white text-sm font-medium hover:from-purple-500 hover:to-indigo-500 transition-all shadow-[0_0_12px_rgba(147,51,234,0.3)] hover:scale-105 flex items-center gap-2">
|
||||
<span>✏️</span> Modifier
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Editor Body */}
|
||||
<div className="flex-1 overflow-hidden relative bg-[#282c34]">
|
||||
{selectedFile.binary ? (
|
||||
<div className="absolute inset-0 flex items-center justify-center text-gray-500 bg-surface-900/60">
|
||||
<div className="text-center">
|
||||
<div className="text-5xl mb-3 opacity-30">📦</div>
|
||||
<p className="tracking-wide">Fichier binaire. Aperçu non disponible.</p>
|
||||
</div>
|
||||
</div>
|
||||
) : editMode ? (
|
||||
<div className="h-full w-full absolute inset-0 [&_.cm-editor]:h-full [&_.cm-scroller]:h-full [&_.cm-scroller]:font-mono [&_.cm-scroller]:text-[13px]">
|
||||
<CodeMirror
|
||||
value={editContent}
|
||||
height="100%"
|
||||
theme={oneDark}
|
||||
extensions={getExtensions()}
|
||||
onChange={(val) => setEditContent(val)}
|
||||
basicSetup={{
|
||||
lineNumbers: true,
|
||||
foldGutter: true,
|
||||
highlightActiveLine: true,
|
||||
tabSize: 2,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-full overflow-y-auto w-full custom-scrollbar absolute inset-0">
|
||||
<div className="p-4 min-h-full">
|
||||
<pre className="text-[13px] text-[#abb2bf] font-mono whitespace-pre-wrap leading-relaxed">
|
||||
{selectedFile.pretty || selectedFile.content}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user