Phase 4 : boucle agentique, outils fichiers et aperçu live

Backend :
- tools.py : read_file / write_file / list_dir confinés au workspace (anti-traversal)
- agent.py : boucle de tool-calling itérative au-dessus d'Ollama, génère des
  événements (token, tool_call, tool_result, final)
- routes/chat.py : pilote run_agent, relaie les événements en SSE, persiste la
  réponse finale + le récap des outils (meta)
- routes/files.py : arborescence et contenu du workspace (confiné)
- db.py : colonne meta sur messages (+ migration douce), list_messages_for_model

Frontend :
- ToolCard : carte d'appel d'outil dans le fil (statut en cours/terminé/échec)
- streamChat : événements tool_call / tool_result
- store : streamTools, fileTree, preview (ouverture auto d'un HTML écrit)
- LeftPanel : arborescence réelle du workspace, clic -> aperçu
- PreviewPanel : rendu HTML en iframe + onglet Code
- ChatPanel : rendu des outils dans les bulles agent (streaming + persisté)

Tests manuels : outils + confinement, boucle agent (mock Ollama), chat HTTP de
bout en bout (SSE, persistance meta, fichiers écrits, confinement route).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVay7z3y7q2gEe54ByAE6N
This commit is contained in:
MichaelandClaude Opus 4.8 committed 2026-06-29 18:55:16 +00:00
1 parent 48cbcaafaf
commit b6bf4edf97
13 files changed
+677 -80

No files matched your search

+84
View File
@@ -0,0 +1,84 @@
import type { ToolCall } from "../api/client";
/** Carte d'appel d'outil rendue dans le fil (fidèle à la maquette). */
export function ToolCard({ call }: { call: ToolCall }) {
const running = call.status === "running";
const error = call.status === "error";
// Argument principal affiché entre parenthèses (path le plus souvent).
const argPreview =
typeof call.args?.path === "string"
? `("${call.args.path}")`
: Object.keys(call.args ?? {}).length
? "(…)"
: "()";
return (
<div className="mb-[9px] overflow-hidden rounded-[11px] border border-line bg-card-deep">
<div className="flex items-center gap-2.5 px-[13px] py-2.5">
<span
className="flex h-6 w-6 items-center justify-center rounded-[7px] text-accent"
style={{ background: "rgba(240,161,92,.13)" }}
>
<ToolGlyph name={call.name} />
</span>
<span className="font-mono text-[12.5px] font-semibold text-[#e7ddcd]">
{call.name}
</span>
<span className="font-mono text-xs text-muted-2">{argPreview}</span>
<div className="flex-1" />
{running ? (
<span className="flex items-center gap-1.5 font-mono text-[11px] text-muted">
<span className="h-2.5 w-2.5 animate-spin rounded-full border-2 border-muted-3 border-t-accent" />
en cours
</span>
) : (
<span
className={`flex items-center gap-1.5 font-mono text-[11px] ${
error ? "text-warn" : "text-ok"
}`}
>
{error ? "✕ échec" : "✓ terminé"}
</span>
)}
</div>
{call.summary && !running && (
<div className="border-t border-line-soft px-[13px] py-2.5 font-mono text-[11.5px] text-muted-3">
→ {call.summary}
</div>
)}
</div>
);
}
function ToolGlyph({ name }: { name: string }) {
const common = {
width: 13,
height: 13,
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
strokeWidth: 1.9,
strokeLinecap: "round" as const,
strokeLinejoin: "round" as const,
};
if (name === "write_file")
return (
<svg {...common}>
<path d="M5 19h14M7 14l9-9 3 3-9 9-4 1z" />
</svg>
);
if (name === "list_dir")
return (
<svg {...common}>
<path d="M4 7h6l2 2h8v9H4z" />
</svg>
);
// read_file (défaut)
return (
<svg {...common}>
<path d="M7 4h7l4 4v12H7z" />
<path d="M14 4v4h4" />
</svg>
);
}