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

+32
View File
@@ -38,15 +38,42 @@ export interface Session {
message_count?: number;
}
export interface ToolCall {
name: string;
args: Record<string, unknown>;
summary?: string;
status?: "ok" | "error" | "running";
}
export interface Message {
id: string;
session_id: string;
role: "user" | "assistant";
content: string;
model?: string;
meta?: { tools?: ToolCall[] } | null;
created_at: number;
}
export interface FileNode {
name: string;
path: string;
type: "dir" | "file";
size?: number;
children?: FileNode[];
}
export async function listFiles(): Promise<FileNode[]> {
const res = await fetch("/api/files");
return (await res.json()).tree;
}
export async function fileContent(path: string): Promise<string> {
const res = await fetch(`/api/files/content?path=${encodeURIComponent(path)}`);
if (!res.ok) return "";
return (await res.json()).content;
}
export async function listSessions(): Promise<Session[]> {
const res = await fetch("/api/sessions");
return (await res.json()).sessions;
@@ -77,6 +104,8 @@ export async function streamChat(
body: { session_id: string; content: string; model?: string },
handlers: {
onToken: (t: string) => void;
onToolCall: (call: ToolCall) => void;
onToolResult: (call: ToolCall) => void;
onDone: (full: string) => void;
onError: (msg: string) => void;
}
@@ -113,6 +142,9 @@ export async function streamChat(
try {
const payload = JSON.parse(data);
if (event === "token") handlers.onToken(payload.content);
else if (event === "tool_call")
handlers.onToolCall({ ...payload, status: "running" });
else if (event === "tool_result") handlers.onToolResult(payload);
else if (event === "done") handlers.onDone(payload.content);
else if (event === "error") handlers.onError(payload.message);
} catch {