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

+17 -9
View File
@@ -1,7 +1,8 @@
import { useEffect, useRef, useState } from "react";
import { useStore } from "../store/useStore";
import { ClipIcon, LokiMark, SendIcon } from "../components/Icon";
import type { Message } from "../api/client";
import { ToolCard } from "../components/ToolCard";
import type { Message, ToolCall } from "../api/client";
/** Panneau central : barre de contexte, fil de conversation, composer. */
export function ChatPanel() {
@@ -10,6 +11,7 @@ export function ChatPanel() {
messages,
streaming,
streamContent,
streamTools,
sendMessage,
currentSessionId,
} = useStore();
@@ -20,7 +22,7 @@ export function ChatPanel() {
// Auto-scroll vers le bas à chaque token / message.
useEffect(() => {
scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight });
}, [messages, streamContent]);
}, [messages, streamContent, streamTools]);
const submit = () => {
if (!draft.trim() || streaming) return;
@@ -43,7 +45,7 @@ export function ChatPanel() {
<div className="flex h-[42px] flex-none items-center gap-2 border-b border-line-soft px-[18px]">
<Chip>Invite système</Chip>
<Chip>
<span className="h-1.5 w-1.5 rounded-full bg-ok" />0 outil actif
<span className="h-1.5 w-1.5 rounded-full bg-ok" />3 outils actifs
</Chip>
<Chip>
Température <b className="font-semibold text-[#e7ddcd]">0.7</b>
@@ -82,6 +84,7 @@ export function ChatPanel() {
role: "assistant",
content: streamContent,
model: selectedModel,
meta: { tools: streamTools },
created_at: Date.now() / 1000,
}}
pending
@@ -172,12 +175,17 @@ function Bubble({ msg, pending }: { msg: Message; pending?: boolean }) {
{time}
</span>
</div>
<div className="text-sm leading-[1.65] text-ink-2 whitespace-pre-wrap">
{msg.content}
{pending && (
<span className="ml-0.5 inline-block h-3.5 w-[7px] animate-pulse bg-accent align-middle" />
)}
</div>
{(msg.meta?.tools ?? []).map((t: ToolCall, i: number) => (
<ToolCard key={i} call={t} />
))}
{(msg.content || pending) && (
<div className="text-sm leading-[1.65] text-ink-2 whitespace-pre-wrap">
{msg.content}
{pending && (
<span className="ml-0.5 inline-block h-3.5 w-[7px] animate-pulse bg-accent align-middle" />
)}
</div>
)}
</div>
</div>
);