mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-21 14:08:11 +02:00
Niveau supérieur (inspiré de Jean) : modes Plan/Build/Yolo + panneau Git & Diff
Modes d'exécution (composer) : - routes/chat : champ mode ; _apply_mode adapte la config — plan = outils lecture seule + plan forcé ; yolo = confirm_shell off ; build = comportement normal. - Frontend : ModeSelector dans le composer, mode dans le store + requête chat. Panneau Git & Diff (le workspace est un dépôt git, Aider commite) : - routes/git : /api/git/log (commits), /api/git/diff (commit ou working), /api/git/revert (git revert --no-edit, confiné au workspace). - Frontend : onglet Git dans le PreviewPanel — liste des commits, diff colorisé, bouton Annuler (revert) avec rafraîchissement de l'arbo. Bonus : le panneau Matériel explique que « GPU déclaré 12 Go » vient de GPU_VRAM_MB (valeur manuelle) — à corriger en 16000 pour une 16 Go. Tests : git log/diff/revert, modes plan/yolo/build via la route chat, builds. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SVay7z3y7q2gEe54ByAE6N
This commit is contained in:
9 files changed
+388
-15
No files matched your search
+2
-1
@@ -14,7 +14,7 @@ from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from . import agent_config, coder, db, rag
|
||||
from .config import settings
|
||||
from .routes import benchmark, chat, config, files, models, sessions, shell, system
|
||||
from .routes import benchmark, chat, config, files, git, models, sessions, shell, system
|
||||
|
||||
|
||||
async def _warm_default_model() -> None:
|
||||
@@ -71,6 +71,7 @@ app.include_router(config.router)
|
||||
app.include_router(shell.router)
|
||||
app.include_router(system.router)
|
||||
app.include_router(benchmark.router)
|
||||
app.include_router(git.router)
|
||||
|
||||
|
||||
@app.get("/api/health")
|
||||
|
||||
@@ -31,6 +31,29 @@ class ChatRequest(BaseModel):
|
||||
session_id: str
|
||||
content: str
|
||||
model: str | None = None
|
||||
# Mode d'exécution : "plan" (lecture seule), "build" (normal), "yolo" (auto).
|
||||
mode: str = "build"
|
||||
|
||||
|
||||
# Outils autorisés en mode Plan : lecture/analyse uniquement.
|
||||
_READONLY_TOOLS = {"read_file", "list_dir", "grep_search"}
|
||||
|
||||
|
||||
def _apply_mode(cfg: dict, mode: str) -> dict:
|
||||
"""Adapte la config au mode d'exécution choisi dans le composer."""
|
||||
cfg = dict(cfg)
|
||||
if mode == "plan":
|
||||
# Lecture seule : aucune écriture, aucun shell, aucun moteur code.
|
||||
cfg["tools"] = {
|
||||
name: (on and name in _READONLY_TOOLS)
|
||||
for name, on in cfg["tools"].items()
|
||||
}
|
||||
cfg["plan_mode"] = True
|
||||
elif mode == "yolo":
|
||||
# Autonomie maximale : plus de validation shell.
|
||||
cfg["confirm_shell"] = False
|
||||
# "build" : comportement par défaut (inchangé).
|
||||
return cfg
|
||||
|
||||
|
||||
def _sse(event: str, data: dict) -> str:
|
||||
@@ -153,7 +176,7 @@ async def chat(req: ChatRequest) -> StreamingResponse:
|
||||
raise HTTPException(404, "session introuvable")
|
||||
|
||||
model = req.model or session.get("model") or settings.default_model
|
||||
cfg = agent_config.get_config(model)
|
||||
cfg = _apply_mode(agent_config.get_config(model), req.mode)
|
||||
|
||||
# Premier message : titre la session avec un extrait.
|
||||
if not db.list_messages(req.session_id):
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
"""Routes Git du workspace : historique, diff, retour arrière.
|
||||
|
||||
Le workspace est un dépôt git (Aider commite chaque modification). Ces routes
|
||||
donnent à l'UI un panneau Git : voir les commits, leur diff, et annuler une
|
||||
modification. Toutes les commandes sont exécutées DANS le workspace.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ..coder import ensure_git
|
||||
from ..config import settings
|
||||
|
||||
router = APIRouter(prefix="/api/git", tags=["git"])
|
||||
|
||||
|
||||
def _git(*args: str, timeout: int = 15) -> subprocess.CompletedProcess:
|
||||
"""Exécute une commande git dans le workspace."""
|
||||
ensure_git(settings.workspace_dir)
|
||||
return subprocess.run(
|
||||
["git", *args],
|
||||
cwd=settings.workspace_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/log")
|
||||
async def git_log(limit: int = 40) -> dict:
|
||||
"""Historique des commits (hash court, message, auteur, date, nb fichiers)."""
|
||||
fmt = "%h%x1f%s%x1f%an%x1f%ar%x1f%H"
|
||||
proc = _git("log", f"-{max(1, min(limit, 200))}", f"--pretty=format:{fmt}")
|
||||
if proc.returncode != 0:
|
||||
# Dépôt sans commit encore.
|
||||
return {"commits": []}
|
||||
|
||||
commits = []
|
||||
for line in proc.stdout.splitlines():
|
||||
parts = line.split("\x1f")
|
||||
if len(parts) != 5:
|
||||
continue
|
||||
short, subject, author, when, full = parts
|
||||
# Nombre de fichiers touchés par ce commit.
|
||||
stat = _git("show", "--stat", "--oneline", "--format=", full)
|
||||
files = [l for l in stat.stdout.splitlines() if "|" in l]
|
||||
commits.append({
|
||||
"hash": short,
|
||||
"full_hash": full,
|
||||
"subject": subject,
|
||||
"author": author,
|
||||
"when": when,
|
||||
"files_changed": len(files),
|
||||
})
|
||||
return {"commits": commits}
|
||||
|
||||
|
||||
@router.get("/diff")
|
||||
async def git_diff(hash: str | None = None) -> dict:
|
||||
"""Diff d'un commit (hash) ou des modifications non commitées si absent."""
|
||||
if hash:
|
||||
if not hash.replace("-", "").isalnum():
|
||||
raise HTTPException(400, "hash invalide")
|
||||
proc = _git("show", "--no-color", hash)
|
||||
else:
|
||||
proc = _git("diff", "--no-color", "HEAD")
|
||||
if proc.returncode != 0:
|
||||
raise HTTPException(404, "diff introuvable")
|
||||
# Borne la taille pour ne pas noyer l'UI.
|
||||
return {"diff": proc.stdout[:200_000]}
|
||||
|
||||
|
||||
class RevertRequest(BaseModel):
|
||||
hash: str
|
||||
|
||||
|
||||
@router.post("/revert")
|
||||
async def git_revert(req: RevertRequest) -> dict:
|
||||
"""Annule un commit en créant un commit inverse (git revert)."""
|
||||
h = req.hash.strip()
|
||||
if not h.replace("-", "").isalnum():
|
||||
raise HTTPException(400, "hash invalide")
|
||||
proc = _git("revert", "--no-edit", h, timeout=30)
|
||||
if proc.returncode != 0:
|
||||
# Conflit ou commit introuvable : on nettoie un éventuel revert en cours.
|
||||
_git("revert", "--abort")
|
||||
detail = (proc.stderr or proc.stdout).strip()[:300]
|
||||
raise HTTPException(409, f"revert impossible : {detail}")
|
||||
return {"ok": True, "message": f"commit {h[:7]} annulé"}
|
||||
Reference in new issue
Block a user