feat(projets): client API + store re-racinés par projet

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 4.8 committed 2026-07-19 10:57:34 +02:00
1 parent 0291831479
commit 5e7461096b
5 files changed
+134 -32

No files matched your search

+78 -19
View File
@@ -51,27 +51,39 @@ export interface GitCommit {
files_changed: number;
}
export async function getGitLog(): Promise<GitCommit[]> {
const projQuery = (project?: string | null) =>
project ? `&project=${encodeURIComponent(project)}` : "";
export async function getGitLog(project?: string | null): Promise<GitCommit[]> {
try {
const res = await fetch("/api/git/log");
const res = await fetch(`/api/git/log?limit=40${projQuery(project)}`);
return (await res.json()).commits;
} catch {
return [];
}
}
export async function getGitDiff(hash?: string): Promise<string> {
const url = hash ? `/api/git/diff?hash=${encodeURIComponent(hash)}` : "/api/git/diff";
const res = await fetch(url);
export async function getGitDiff(
hash?: string,
project?: string | null
): Promise<string> {
const params = new URLSearchParams();
if (hash) params.set("hash", hash);
if (project) params.set("project", project);
const qs = params.toString();
const res = await fetch(`/api/git/diff${qs ? `?${qs}` : ""}`);
if (!res.ok) return "";
return (await res.json()).diff;
}
export async function revertCommit(hash: string): Promise<{ message: string }> {
export async function revertCommit(
hash: string,
project?: string | null
): Promise<{ message: string }> {
const res = await fetch("/api/git/revert", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ hash }),
body: JSON.stringify({ hash, project: project ?? null }),
});
if (!res.ok) throw await apiError(res, "revert impossible");
return res.json();
@@ -214,6 +226,7 @@ export interface Session {
id: string;
title: string;
model?: string;
project?: string | null;
created_at: number;
updated_at: number;
message_count?: number;
@@ -400,29 +413,39 @@ export interface FileNode {
children?: FileNode[];
}
export async function listFiles(): Promise<FileNode[]> {
const res = await fetch("/api/files");
export async function listFiles(project?: string | null): Promise<FileNode[]> {
const query = project ? `?project=${encodeURIComponent(project)}` : "";
const res = await fetch(`/api/files${query}`);
return (await res.json()).tree;
}
export async function fileContent(path: string): Promise<string> {
const res = await fetch(`/api/files/content?path=${encodeURIComponent(path)}`);
export async function fileContent(
path: string,
project?: string | null
): Promise<string> {
const res = await fetch(
`/api/files/content?path=${encodeURIComponent(path)}${projQuery(project)}`
);
if (!res.ok) return "";
return (await res.json()).content;
}
export async function deleteFile(path: string): Promise<void> {
const res = await fetch(`/api/files?path=${encodeURIComponent(path)}`, {
method: "DELETE",
});
export async function deleteFile(
path: string,
project?: string | null
): Promise<void> {
const res = await fetch(
`/api/files?path=${encodeURIComponent(path)}${projQuery(project)}`,
{ method: "DELETE" }
);
if (!res.ok) {
throw await apiError(res, `suppression impossible (${res.status})`);
}
}
export function downloadFile(path: string): void {
export function downloadFile(path: string, project?: string | null): void {
const link = document.createElement("a");
link.href = `/api/files/download?path=${encodeURIComponent(path)}`;
link.href = `/api/files/download?path=${encodeURIComponent(path)}${projQuery(project)}`;
link.download = path.split(/[\\/]/).pop() ?? "fichier";
document.body.appendChild(link);
link.click();
@@ -473,15 +496,51 @@ export async function listSessions(): Promise<Session[]> {
return (await res.json()).sessions;
}
export async function createSession(model?: string): Promise<Session> {
export async function createSession(
model?: string,
project?: string | null
): Promise<Session> {
const res = await fetch("/api/sessions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: "Nouvelle session", model }),
body: JSON.stringify({
title: "Nouvelle session",
model,
project: project ?? null,
}),
});
return res.json();
}
export async function listProjects(): Promise<{
projects: { name: string; files: number }[];
root_files: number;
}> {
const res = await fetch("/api/projects");
return res.json();
}
export async function createProject(name: string): Promise<void> {
const res = await fetch("/api/projects", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name }),
});
if (!res.ok) throw await apiError(res, "création du projet impossible");
}
export async function setSessionProject(
id: string,
project: string | null
): Promise<void> {
const res = await fetch(`/api/sessions/${id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ project: project ?? "" }),
});
if (!res.ok) throw await apiError(res, "changement de projet impossible");
}
export async function getSession(
id: string
): Promise<{ session: Session; messages: Message[] }> {
+2 -2
View File
@@ -61,7 +61,7 @@ export function FilesView() {
}
function WorkspaceTree({ nodes, depth }: { nodes: FileNode[]; depth: number }) {
const { openPreview, previewPath, removeFile } = useStore();
const { openPreview, previewPath, removeFile, currentProject } = useStore();
const confirmDelete = (node: FileNode) => {
const msg =
node.type === "dir"
@@ -93,7 +93,7 @@ function WorkspaceTree({ nodes, depth }: { nodes: FileNode[]; depth: number }) {
</button>
{node.type === "file" && (
<button
onClick={() => downloadFile(node.path)}
onClick={() => downloadFile(node.path, currentProject())}
className="flex items-center gap-1 border-2 border-line bg-base px-2 py-1 text-[11px] text-accent"
title={`Télécharger ${node.name}`}
>
+2 -2
View File
@@ -117,7 +117,7 @@ export function LeftPanel() {
}
function FileTree({ nodes, depth }: { nodes: FileNode[]; depth: number }) {
const { openPreview, previewPath, removeFile } = useStore();
const { openPreview, previewPath, removeFile, currentProject } = useStore();
const confirmDelete = (n: FileNode) => {
const msg =
n.type === "dir"
@@ -156,7 +156,7 @@ function FileTree({ nodes, depth }: { nodes: FileNode[]; depth: number }) {
<button
onClick={(event) => {
event.stopPropagation();
downloadFile(n.path);
downloadFile(n.path, currentProject());
}}
className="hidden flex-none text-muted-2 hover:text-accent group-hover:block"
title={`Télécharger ${n.name}`}
+8 -5
View File
@@ -98,7 +98,7 @@ export function PreviewPanel() {
{previewPath && (
<>
<button
onClick={() => downloadFile(previewPath)}
onClick={() => downloadFile(previewPath, useStore.getState().currentProject())}
className="flex h-8 items-center gap-1.5 border-[3px] border-line bg-card px-2.5 text-[12px] text-accent"
title={`Télécharger ${previewPath}`}
>
@@ -202,21 +202,24 @@ export function PreviewPanel() {
/** Onglet Git : historique des commits, diff colorisé, retour arrière. */
function GitPanel() {
const refreshFiles = useStore((s) => s.refreshFiles);
const currentProject = useStore((s) => s.currentProject);
const project = currentProject();
const [commits, setCommits] = useState<GitCommit[]>([]);
const [selected, setSelected] = useState<string | null>(null);
const [diff, setDiff] = useState("");
const [busy, setBusy] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const load = () => getGitLog().then(setCommits).catch(() => {});
const load = () => getGitLog(project).then(setCommits).catch(() => {});
useEffect(() => {
load();
}, []);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [project]);
const openDiff = async (hash: string) => {
setSelected(hash);
setDiff("Chargement…");
setDiff((await getGitDiff(hash)) || "(diff vide)");
setDiff((await getGitDiff(hash, project)) || "(diff vide)");
};
const doRevert = async (hash: string) => {
@@ -225,7 +228,7 @@ function GitPanel() {
setBusy(hash);
setError(null);
try {
await revertCommit(hash);
await revertCommit(hash, project);
await load();
await refreshFiles();
if (selected === hash) setSelected(null);
+44 -4
View File
@@ -4,6 +4,8 @@ import {
deleteFile,
deleteSession,
getConfig,
listProjects,
setSessionProject,
getModels,
getSession,
getStatus,
@@ -77,6 +79,11 @@ interface LokiState {
removeSession: (id: string) => Promise<void>;
renameSession: (id: string, title: string) => Promise<void>;
removeFile: (path: string) => Promise<void>;
projects: { name: string; files: number }[];
refreshProjects: () => Promise<void>;
currentProject: () => string | null;
setProject: (name: string | null) => Promise<void>;
sendMessage: (content: string) => Promise<void>;
stopStreaming: () => void;
@@ -110,6 +117,7 @@ export const useStore = create<LokiState>((set, get) => ({
fileTree: [],
previewPath: null,
previewContent: "",
projects: [],
config: null,
availableTools: [],
pendingShell: null,
@@ -156,7 +164,7 @@ export const useStore = create<LokiState>((set, get) => ({
},
openPreview: async (path) => {
const content = await fileContent(path);
const content = await fileContent(path, get().currentProject());
set({ previewPath: path, previewContent: content });
},
@@ -197,12 +205,39 @@ export const useStore = create<LokiState>((set, get) => ({
refreshFiles: async () => {
try {
set({ fileTree: await listFiles() });
set({ fileTree: await listFiles(get().currentProject()) });
} catch {
/* workspace indisponible */
}
},
refreshProjects: async () => {
try {
const { projects } = await listProjects();
set({ projects });
} catch {
/* backend indisponible */
}
},
currentProject: () => {
const s = get().sessions.find((x) => x.id === get().currentSessionId);
return s?.project ?? null;
},
setProject: async (name) => {
const sid = get().currentSessionId;
if (!sid) {
const s = await createSession(get().selectedModel || undefined, name);
set({ currentSessionId: s.id, messages: [] });
} else {
await setSessionProject(sid, name);
}
await get().refreshSessions();
set({ previewPath: null, previewContent: "" });
await get().refreshFiles();
},
refreshStatus: async () => {
try {
const status = await getStatus();
@@ -250,7 +285,10 @@ export const useStore = create<LokiState>((set, get) => ({
},
newSession: async () => {
const s = await createSession(get().selectedModel || undefined);
// La nouvelle session hérite du projet de la session courante.
const s = await createSession(
get().selectedModel || undefined, get().currentProject()
);
const streaming = get().streaming;
set({
currentSessionId: s.id,
@@ -272,6 +310,8 @@ export const useStore = create<LokiState>((set, get) => ({
? {}
: { streamContent: "", streamStatus: "", streamNotice: null }),
});
// L'arborescence suit le projet de la session ouverte.
await get().refreshFiles();
},
removeSession: async (id) => {
@@ -290,7 +330,7 @@ export const useStore = create<LokiState>((set, get) => ({
},
removeFile: async (path) => {
await deleteFile(path);
await deleteFile(path, get().currentProject());
// Ferme l'aperçu si le fichier supprimé (ou son dossier) y est affiché.
const preview = get().previewPath;
if (preview && (preview === path || preview.startsWith(path + "/"))) {