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

+53 -20
View File
@@ -1,17 +1,25 @@
/** Panneau droit : onglets Aperçu / Code / Logs + zone d'aperçu. */
import { useState } from "react";
import { useStore } from "../store/useStore";
type TabId = "preview" | "code";
/** Panneau droit : onglets Aperçu / Code + rendu du fichier sélectionné. */
export function PreviewPanel() {
const { previewPath, previewContent } = useStore();
const [tab, setTab] = useState<TabId>("preview");
const isHtml = previewPath ? /\.html?$/.test(previewPath) : false;
return (
<div className="flex w-[452px] flex-none flex-col border-l border-line-soft bg-panel">
{/* Onglets */}
<div className="flex h-[42px] flex-none items-center gap-1 border-b border-line-soft px-3.5 pr-2">
<div className="flex gap-0.5">
<Tab active>Aperçu</Tab>
<Tab>Code</Tab>
<Tab>
Logs
<span className="ml-1.5 rounded-[5px] bg-line-strong px-1.5 py-px font-mono text-[10px] text-muted">
0
</span>
<Tab active={tab === "preview"} onClick={() => setTab("preview")}>
Aperçu
</Tab>
<Tab active={tab === "code"} onClick={() => setTab("code")}>
Code
</Tab>
</div>
</div>
@@ -19,36 +27,61 @@ export function PreviewPanel() {
{/* URL bar */}
<div className="flex flex-none items-center gap-2 px-3.5 py-[9px]">
<div className="flex h-[30px] flex-1 items-center gap-2 rounded-lg border border-line bg-sunken px-[11px]">
<span className="font-mono text-[11.5px] text-muted-3">
workspace/
<span className="truncate font-mono text-[11.5px] text-muted-3">
{previewPath ? `workspace/${previewPath}` : "workspace/"}
</span>
</div>
</div>
{/* Viewport */}
<div className="scr mx-3.5 mb-3.5 flex-1 overflow-auto rounded-xl border border-line bg-[#faf6ef]">
<div className="flex h-full flex-col items-center justify-center gap-2 p-8 text-center">
<div className="font-mono text-[11px] text-[#a98b63]">
[ aucun aperçu ]
</div>
<div className="max-w-[240px] text-xs text-[#8a7a66]">
L'aperçu HTML s'affichera ici dès que l'agent générera une page.
</div>
</div>
{!previewPath ? (
<Empty>
L'aperçu s'affichera ici dès que l'agent générera un fichier (clique
aussi un fichier à gauche).
</Empty>
) : tab === "code" ? (
<pre className="m-0 whitespace-pre-wrap p-4 font-mono text-[11.5px] leading-relaxed text-[#2a2018]">
{previewContent}
</pre>
) : isHtml ? (
<iframe
title="aperçu"
srcDoc={previewContent}
className="h-full w-full border-0 bg-white"
sandbox="allow-same-origin"
/>
) : (
<pre className="m-0 whitespace-pre-wrap p-4 font-mono text-[11.5px] leading-relaxed text-[#2a2018]">
{previewContent}
</pre>
)}
</div>
</div>
);
}
function Empty({ children }: { children: React.ReactNode }) {
return (
<div className="flex h-full flex-col items-center justify-center gap-2 p-8 text-center">
<div className="font-mono text-[11px] text-[#a98b63]">[ aucun aperçu ]</div>
<div className="max-w-[240px] text-xs text-[#8a7a66]">{children}</div>
</div>
);
}
function Tab({
children,
active,
onClick,
}: {
children: React.ReactNode;
active?: boolean;
onClick?: () => void;
}) {
return (
<div
<button
onClick={onClick}
className={`flex h-7 items-center rounded-lg px-[13px] text-[12.5px] ${
active
? "bg-[#252019] font-semibold text-ink"
@@ -56,6 +89,6 @@ function Tab({
}`}
>
{children}
</div>
</button>
);
}