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

+54 -6
View File
@@ -1,6 +1,7 @@
import { useEffect } from "react";
import { useStore } from "../store/useStore";
import { PlusIcon } from "../components/Icon";
import type { FileNode } from "../api/client";
/** Panneau gauche : historique des sessions + arborescence de fichiers. */
export function LeftPanel() {
@@ -8,6 +9,8 @@ export function LeftPanel() {
sessions,
currentSessionId,
refreshSessions,
refreshFiles,
fileTree,
newSession,
openSession,
removeSession,
@@ -15,7 +18,8 @@ export function LeftPanel() {
useEffect(() => {
refreshSessions();
}, [refreshSessions]);
refreshFiles();
}, [refreshSessions, refreshFiles]);
return (
<div className="flex w-[268px] flex-none flex-col border-r border-line-soft bg-panel">
@@ -91,16 +95,60 @@ export function LeftPanel() {
</div>
<div className="scr flex-1 overflow-auto px-2.5 pb-3.5 font-mono text-xs">
<div className="px-2 py-4 text-center text-muted-3">
Aucun fichier pour l'instant.
<br />
L'agent créera des fichiers ici.
</div>
{fileTree.length === 0 ? (
<div className="px-2 py-4 text-center text-muted-3">
Aucun fichier pour l'instant.
<br />
L'agent créera des fichiers ici.
</div>
) : (
<FileTree nodes={fileTree} depth={0} />
)}
</div>
</div>
);
}
function FileTree({ nodes, depth }: { nodes: FileNode[]; depth: number }) {
const openPreview = useStore((s) => s.openPreview);
return (
<>
{nodes.map((n) => (
<div key={n.path}>
<div
onClick={() => n.type === "file" && openPreview(n.path)}
className={`flex items-center gap-2 rounded-[7px] px-2 py-[5px] ${
n.type === "file"
? "cursor-pointer text-ink-2 hover:bg-[#221e18]"
: "text-muted"
}`}
style={{ paddingLeft: 8 + depth * 14 }}
>
{n.type === "dir" ? (
<svg
width="11"
height="11"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.4"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="m6 9 6 6 6-6" />
</svg>
) : (
<span className="h-1.5 w-1.5 rounded-[1.5px] bg-accent" />
)}
<span className="truncate">{n.name}</span>
</div>
{n.children && <FileTree nodes={n.children} depth={depth + 1} />}
</div>
))}
</>
);
}
function relTime(ts: number): string {
const diff = Date.now() / 1000 - ts;
if (diff < 60) return "à l'instant";