mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-22 06:28:11 +02:00
Un dossier de fichiers par discussion
Les fichiers du chat vivaient dans un pot commun : on changeait de discussion et on revoyait les mêmes pièces jointes, et supprimer une discussion laissait derrière elle tout ce qu'on y avait déposé ou fait écrire à l'agent (seules ses captures, déjà rangées par identifiant, partaient avec). Chaque discussion a désormais son dossier, <workspace>/discussions/<id>/ : - les dépôts (uploads/), les captures (captures/) et ce que l'agent écrit y atterrissent ; le shell et les chemins relatifs du modèle y sont résolus ; - le panneau Fichiers s'ouvre sur ce dossier et n'en sort pas, et se redessine quand la discussion change (bascule ou vidage, signalés par le flux SSE) ; - supprimer une discussion — ou la vider — emporte ses fichiers. Les deux gestes le disent maintenant avant de demander confirmation ; « clear chat » en demandait aucune. La racine du dossier de travail reste la borne de sécurité : les liens des anciens messages (uploads/x.pdf, captures/<id>/y.jpg) continuent d'ouvrir leur fichier par un chemin de repli. Au démarrage, une migration range les captures dans le dossier de leur discussion et rend chaque dépôt à la discussion qui le mentionne dans son journal ; ce que personne ne réclame reste à la racine, atteignable par le bouton « hors discussion » du panneau, qui disparaît une fois le ménage fait. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LPyFxVHNAN9u5pVzSYMwjd
This commit is contained in:
21 files changed
+914
-168
No files matched your search
@@ -327,9 +327,18 @@ function addCopyButtons(root){
|
||||
pre.appendChild(btn);
|
||||
});
|
||||
}
|
||||
// Nouvelle conversation POUR TOUS LES APPAREILS : le serveur vide le fil et
|
||||
// Vider la discussion POUR TOUS LES APPAREILS : le serveur vide le fil et
|
||||
// diffuse un {reset} ; le flux d'abonnement nettoie alors l'affichage.
|
||||
function resetChat(){ jfetch('/api/chat/reset',{method:'POST'}).catch(()=>{}); toast('nouvelle conversation'); }
|
||||
//
|
||||
// Confirmé, car le geste emporte aussi les FICHIERS de la discussion (dépôts,
|
||||
// captures, ce que l'agent y a écrit) : ils n'appartiennent qu'à elle, et plus
|
||||
// un message ne les mentionnerait.
|
||||
async function resetChat(){
|
||||
if(!await askConfirm('Le fil de cette discussion et ses fichiers (pièces jointes, captures, fichiers écrits par l\'agent) seront effacés.\n\nC\'est définitif.',
|
||||
{title:'Vider la discussion', okText:'Vider', danger:true})) return;
|
||||
jfetch('/api/chat/reset',{method:'POST'}).catch(()=>{});
|
||||
toast('discussion vidée');
|
||||
}
|
||||
// Compaction : on demande à l'IA un résumé de la conversation destiné à la
|
||||
// reprendre dans une session neuve, puis on repart d'un contexte propre seedé
|
||||
// avec ce résumé. Réduit drastiquement les tokens tout en gardant le fil.
|
||||
|
||||
@@ -154,8 +154,9 @@ function handleDelta(d){
|
||||
return; }
|
||||
// `reset` = fil vidé OU bascule de discussion (même mécanisme d'epoch côté
|
||||
// serveur) : on nettoie l'écran et on resynchronise la liste, car la bascule
|
||||
// a pu être déclenchée depuis un autre appareil.
|
||||
if(d.reset!==undefined){ PENDING=null; document.getElementById('chat').innerHTML=''; newTurn(); setCtxUsed(0); lastSeq=0; setBusy(false); if(typeof loadConversations==='function') loadConversations(); return; }
|
||||
// a pu être déclenchée depuis un autre appareil. Les fichiers suivent : ils
|
||||
// appartiennent à la discussion, le panneau doit changer avec elle.
|
||||
if(d.reset!==undefined){ PENDING=null; document.getElementById('chat').innerHTML=''; newTurn(); setCtxUsed(0); lastSeq=0; setBusy(false); if(typeof loadConversations==='function') loadConversations(); if(typeof filesOnConvChange==='function') filesOnConvChange(); return; }
|
||||
if(d.user!==undefined){
|
||||
newTurn();
|
||||
let el=PENDING;
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
// ─── Fichiers du dossier de travail ──────────────────────────────────────────
|
||||
// ─── Fichiers de la discussion ───────────────────────────────────────────────
|
||||
// L'agent écrit des rapports, des scripts, des exports, des captures. Jusqu'ici
|
||||
// on ne pouvait récupérer un fichier que si le modèle avait pensé à en mettre le
|
||||
// lien dans sa réponse — tout le reste restait invisible, et faire le ménage
|
||||
// demandait un shell.
|
||||
//
|
||||
// Le panneau montre les fichiers de la discussion OUVERTE, et rien d'autre :
|
||||
// chaque discussion a son dossier côté serveur (chat_convfiles.go), changer de
|
||||
// discussion change donc la liste, et supprimer une discussion emporte ses
|
||||
// fichiers. `FILES_SCOPE = 'legacy'` ouvre le détour « hors discussion » : les
|
||||
// fichiers d'avant ce rangement, que la migration n'a pas su rattacher.
|
||||
//
|
||||
// Le panneau est construit EN DOM, jamais en innerHTML : les noms de fichiers
|
||||
// viennent du modèle et du système de fichiers, donc d'ailleurs. Même règle que
|
||||
// pour les titres de discussions (16-conversations.js).
|
||||
@@ -11,7 +17,8 @@
|
||||
// Le téléchargement réutilise downloadWorkspaceFile (14-attach.js), qui sait
|
||||
// déjà gérer le blob et les cas particuliers du navigateur.
|
||||
|
||||
let FILES_DIR = ''; // dossier affiché, relatif au dossier de travail
|
||||
let FILES_DIR = ''; // dossier affiché, relatif à la racine du panneau
|
||||
let FILES_SCOPE = ''; // '' = discussion ouverte, 'legacy' = fichiers hors discussion
|
||||
let FILES_OPEN = false;
|
||||
|
||||
// Ouvre/ferme le panneau. L'état est persisté : sur un grand écran on garde
|
||||
@@ -44,7 +51,7 @@ async function loadFiles(dir){
|
||||
const foot = document.getElementById('files-foot');
|
||||
if(!list) return;
|
||||
let r;
|
||||
try{ r = await jget('/api/chat/files?dir='+encodeURIComponent(FILES_DIR)); }
|
||||
try{ r = await jget('/api/chat/files?dir='+encodeURIComponent(FILES_DIR)+'&scope='+encodeURIComponent(FILES_SCOPE)); }
|
||||
catch(_){ filesMsg('lecture impossible'); return; }
|
||||
if(!r.ok){
|
||||
// Un dossier supprimé entre deux affichages (par l'agent, ou depuis un autre
|
||||
@@ -58,13 +65,40 @@ async function loadFiles(dir){
|
||||
if(!r.entries.length){
|
||||
const d = document.createElement('div');
|
||||
d.className = 'files-empty muted';
|
||||
d.textContent = FILES_DIR ? '(dossier vide)' : "L'agent n'a encore rien écrit ici.";
|
||||
d.textContent = FILES_DIR ? '(dossier vide)'
|
||||
: (FILES_SCOPE === 'legacy' ? '(plus rien hors discussion)'
|
||||
: "Rien dans cette discussion — les fichiers déposés et ceux que l'agent écrit apparaissent ici.");
|
||||
list.appendChild(d);
|
||||
}
|
||||
for(const e of r.entries) list.appendChild(fileRow(e));
|
||||
|
||||
foot.textContent = r.count + (r.count > 1 ? ' fichiers · ' : ' fichier · ') + fmtSize(r.total);
|
||||
renderFoot(foot, r);
|
||||
}
|
||||
|
||||
// Pied : occupation disque de ce qui est affiché — la discussion ouverte, ou le
|
||||
// hors-discussion — et le va-et-vient entre les deux. Le bouton n'apparaît que
|
||||
// s'il reste vraiment quelque chose hors discussion (compté par le serveur), et
|
||||
// disparaît de lui-même une fois le ménage fait.
|
||||
function renderFoot(foot, r){
|
||||
if(!foot) return;
|
||||
foot.textContent = '';
|
||||
const n = document.createElement('span');
|
||||
n.textContent = r.count + (r.count > 1 ? ' fichiers · ' : ' fichier · ') + fmtSize(r.total);
|
||||
foot.appendChild(n);
|
||||
foot.title = r.root || '';
|
||||
if(FILES_SCOPE !== 'legacy' && !r.legacy) return;
|
||||
const b = document.createElement('button');
|
||||
b.className = 'files-scope';
|
||||
if(FILES_SCOPE === 'legacy'){
|
||||
b.textContent = '← cette discussion';
|
||||
b.title = 'revenir aux fichiers de la discussion ouverte';
|
||||
b.onclick = ()=>{ FILES_SCOPE = ''; loadFiles(''); };
|
||||
} else {
|
||||
b.textContent = 'hors discussion (' + r.legacy + ')';
|
||||
b.title = "fichiers d'avant le rangement par discussion";
|
||||
b.onclick = ()=>{ FILES_SCOPE = 'legacy'; loadFiles(''); };
|
||||
}
|
||||
foot.appendChild(b);
|
||||
}
|
||||
|
||||
function filesMsg(txt){
|
||||
@@ -76,7 +110,9 @@ function filesMsg(txt){
|
||||
list.appendChild(d);
|
||||
}
|
||||
|
||||
// Fil d'Ariane : « travail / captures / <id> », chaque segment cliquable.
|
||||
// Fil d'Ariane : « discussion / captures », chaque segment cliquable. La racine
|
||||
// est le dossier de la discussion ouverte — on n'en sort pas, sauf par le
|
||||
// détour « hors discussion », qui a sa propre racine.
|
||||
function renderCrumb(r){
|
||||
const c = document.getElementById('files-crumb');
|
||||
c.textContent = '';
|
||||
@@ -89,7 +125,7 @@ function renderCrumb(r){
|
||||
c.appendChild(b);
|
||||
};
|
||||
const parts = r.dir ? r.dir.split('/') : [];
|
||||
seg('travail', '', parts.length === 0);
|
||||
seg(FILES_SCOPE === 'legacy' ? 'hors discussion' : 'discussion', '', parts.length === 0);
|
||||
let acc = '';
|
||||
parts.forEach((p, i)=>{
|
||||
acc = acc ? acc + '/' + p : p;
|
||||
@@ -155,12 +191,21 @@ async function removeFile(e){
|
||||
: '« '+e.name+' » ('+fmtSize(e.size)+') sera supprimé.';
|
||||
if(!await askConfirm(quoi + '\n\nC\'est définitif — le fichier ne part pas à la corbeille.',
|
||||
{title:'Supprimer ?', okText:'Supprimer', danger:true})) return;
|
||||
const r = await jpost('/api/chat/file/delete', {path: e.path});
|
||||
const r = await jpost('/api/chat/file/delete', {path: e.path, scope: FILES_SCOPE});
|
||||
if(!r.ok){ toast('suppression impossible : '+(r.error||'')); return; }
|
||||
toast('supprimé — '+fmtSize(r.freed||0)+' libérés');
|
||||
loadFiles();
|
||||
}
|
||||
|
||||
// Changement de discussion (bascule, « vider »), signalé par le flux SSE :
|
||||
// le panneau montre les fichiers de la discussion OUVERTE, il doit donc être
|
||||
// redessiné. On revient aussi à sa racine — le sous-dossier affiché était celui
|
||||
// de l'autre discussion, il n'existe pas forcément ici.
|
||||
function filesOnConvChange(){
|
||||
FILES_DIR = ''; FILES_SCOPE = '';
|
||||
if(FILES_OPEN) loadFiles('');
|
||||
}
|
||||
|
||||
// Date courte : l'heure pour aujourd'hui, la date sinon. Ce qu'on veut savoir,
|
||||
// c'est « est-ce que ça vient du tour que je viens de lancer ? ».
|
||||
document.addEventListener('DOMContentLoaded', initFiles);
|
||||
|
||||
@@ -85,7 +85,9 @@ async function convRename(id, current){
|
||||
}
|
||||
|
||||
async function convDelete(id, title){
|
||||
if(!await askConfirm('Supprimer « ' + title + ' » ? Cette discussion et son historique seront perdus.',
|
||||
// Le geste emporte aussi les fichiers de la discussion : ils lui appartiennent
|
||||
// (un dossier par discussion côté serveur), autant le dire avant.
|
||||
if(!await askConfirm('Supprimer « ' + title + ' » ? Son historique ET ses fichiers (pièces jointes, captures, fichiers écrits par l\'agent) seront perdus.',
|
||||
{title:'Supprimer la discussion', okText:'Supprimer', danger:true})) return;
|
||||
try{
|
||||
const r = await jpost('/api/conversations/delete', {id});
|
||||
|
||||
Reference in new issue
Block a user