From 00b9cc2aee85db6c337c75a6121d513e0330fbd9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 16 Aug 2026 12:25:57 +0000 Subject: [PATCH] =?UTF-8?q?Panneau=20Fichiers=20:=20acc=C3=A9der=20=C3=A0?= =?UTF-8?q?=20ce=20que=20l'agent=20=C3=A9crit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit L'agent produit des rapports, des scripts, des exports et des captures dans son dossier de travail. On ne pouvait en récupérer un que si le modèle avait pensé à en mettre le lien dans sa réponse : tout ce qu'il écrivait sans le dire restait invisible, et faire le ménage demandait un shell. Un panneau de droite, ouvert et fermé par le bouton dossier du pied de carte, liste ce dossier avec navigation dans les sous-dossiers, téléchargement et suppression. Il pousse la conversation au lieu de la recouvrir — la largeur de lecture étant pilotée par --measure, le fil se resserre et se recentre tout seul. Sur téléphone c'est un tiroir, comme les réglages à gauche. Le socle existait : /api/chat/file téléchargeait déjà, workspaceRel bornait déjà les chemins, downloadWorkspaceFile gérait déjà le blob. Manquaient les deux verbes qui comptent, /api/chat/files pour lister et /api/chat/file/delete pour supprimer. Trois choix qui méritent d'être dits : - Un dossier affiche la taille de TOUT son contenu, pas celle de son inode : c'est ce qu'on libère en le supprimant, donc c'est le chiffre qui aide à décider. Le parcours est borné à 20 000 entrées pour que la liste reste instantanée si l'agent y dézippe quelque chose d'énorme. - La racine du dossier de travail est refusée à la suppression : l'agent y perd le dossier dans lequel il écrit, et « tout effacer » ne doit pas être à un clic. La suppression d'un dossier annonce le nombre de fichiers et le poids emportés avant de demander confirmation. - Le panneau ne se remplit qu'à l'ouverture. Un ReadDir récursif à chaque chargement de page pour un panneau fermé serait payé par tout le monde. Ouvrir le panneau rétrécit la conversation SANS déclencher de `resize` : la gouttière de barre de défilement mesurée dans --sbw resterait celle de l'ancienne largeur et la carte de saisie serait décalée du fil. On resynchronise donc explicitement ; la hauteur, elle, est déjà suivie par un ResizeObserver. Vérifié. Six tests Go sur le bornage, qui est la partie sensible — ces routes suppriment sur disque à partir d'un chemin venu du navigateur : « .. », chemins absolus, racine, et un lien symbolique posé dans le dossier de travail sont tous refusés, et le fichier voisin survit. Puis au navigateur, en clair et en sombre : listing trié dossiers d'abord, taille récursive, descente et fil d'Ariane, téléchargement réel (l'événement download porte bien rapport.md), suppression confirmée puis disparition de la ligne, persistance de l'état ouvert, tiroir mobile avec voile, et la saisie reste alignée au pixel sur le fil (0 px des deux côtés) une fois le panneau ouvert. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01VueWA9xcYadaYq65tBisix --- README.md | 6 + internal/loki/ui/index.html | 268 +++++++++++++++++++++++++++ internal/loki/ui/src/index.tmpl.html | 24 +++ internal/loki/ui/src/js/15-files.js | 175 +++++++++++++++++ internal/loki/ui/src/styles.css | 69 +++++++ internal/loki/web_files.go | 219 ++++++++++++++++++++++ internal/loki/web_files_test.go | 232 +++++++++++++++++++++++ internal/loki/web_server.go | 16 +- 8 files changed, 1002 insertions(+), 7 deletions(-) create mode 100644 internal/loki/ui/src/js/15-files.js create mode 100644 internal/loki/web_files.go create mode 100644 internal/loki/web_files_test.go diff --git a/README.md b/README.md index 3b195b6..615aa84 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,12 @@ Ajoutées par ce fork : lui promettre des yeux qu'il n'a pas — il peut toujours prendre la capture et la montrer, sans prétendre la décrire. L'image relayée au moteur reste éphémère : la persister gonflait le contexte jusqu'à le faire déborder. +- **Panneau Fichiers** (bouton dossier du pied de carte) : ce que l'agent a + écrit dans son dossier de travail, avec navigation dans les sous-dossiers, + téléchargement et suppression. Un dossier affiche la taille de **tout** son + contenu — c'est ce qu'on libère en le supprimant. Le pied donne l'occupation + disque totale de l'agent. Les chemins sont bornés au dossier de travail, liens + symboliques résolus des deux côtés : rien du reste du disque n'est atteignable. - **Identité** : ton prénom et un avatar emoji pour toi et pour Loki, affichés dans le fil. - **Paramètres** : les réglages d'application (identité, apparence, accès diff --git a/internal/loki/ui/index.html b/internal/loki/ui/index.html index ef47c27..e11a22f 100644 --- a/internal/loki/ui/index.html +++ b/internal/loki/ui/index.html @@ -1391,6 +1391,75 @@ button:hover{border-color:var(--dim);color:var(--text)} .hf-mm{display:flex;align-items:center;gap:6px;margin-top:8px;font-size:11.5px; color:var(--dim);cursor:pointer} .hf-mm.muted{cursor:default} + +/* --- Panneau « Fichiers » (droite) ------------------------------------ + Symétrique des réglages à gauche : une colonne fixe qui pousse la + conversation au lieu de la recouvrir. C'est possible ici parce que la largeur + de lecture est pilotée par --measure et que #chat se recentre tout seul dans + la place qui reste — le fil se resserre, il ne passe pas dessous. + Le panneau est retiré du flux quand il est fermé (et non simplement décalé) : + sinon il conserverait sa gouttière et le fil resterait décentré. */ +#files{display:none;width:320px;flex-shrink:0;border-left:1px solid var(--border); + background:var(--panel);padding:16px;padding-bottom:calc(16px + env(safe-area-inset-bottom)); + overflow-y:auto;flex-direction:column;gap:10px} +html[data-files="1"] #files{display:flex} +#files-backdrop{display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:14} +.files-head{display:flex;align-items:center;gap:8px} +.files-head .stitle{flex:1} +#files-refresh,#files-close{margin:0;padding:2px 8px;min-height:0;font-size:12px; + background:transparent;border:1px solid transparent;color:var(--dim);border-radius:8px} +#files-refresh:hover,#files-close:hover{border-color:var(--border);color:var(--text)} +/* Fil d'Ariane : il doit pouvoir revenir à la ligne — un identifiant de + discussion dans le chemin dépasse largement 320 px. */ +.files-crumb{display:flex;flex-wrap:wrap;align-items:center;gap:2px;font-size:11px} +.files-crumb-b{margin:0;padding:2px 6px;min-height:0;font-size:11px;border:0;border-radius:7px; + background:transparent;color:var(--dim);max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.files-crumb-b:hover:not(:disabled){background:var(--row-bg-on);color:var(--text)} +.files-crumb-b.on{color:var(--text);cursor:default} +.files-crumb-s{color:var(--dim);opacity:.5} +.files-list{display:flex;flex-direction:column;flex:1;min-height:0} +.files-empty{font-size:12px;line-height:1.5;padding:10px 2px} +/* Une ligne = pastille, nom + méta, actions. Les actions n'apparaissent qu'au + survol : au repos on veut lire des noms de fichiers, pas une grille de + boutons. Sur écran tactile (pas de survol) elles restent visibles. */ +.files-row{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:12px;font-size:12px} +.files-row:hover{background:var(--row-bg-on)} +.files-ic{flex:none;width:12px;text-align:center;color:var(--dim)} +.files-row.isdir .files-ic{color:var(--accent)} +.files-mid{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px} +.files-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text)} +.files-meta{font-family:var(--mono);font-size:10px;color:var(--dim);white-space:nowrap} +.files-acts{flex:none;display:flex;gap:2px;opacity:0;transition:opacity .12s} +.files-row:hover .files-acts,.files-row:focus-within .files-acts{opacity:1} +@media (hover:none){.files-acts{opacity:1}} +.files-acts .iconbtn{font-size:11px;line-height:1;padding:4px 6px;background:transparent; + border:1px solid transparent;border-radius:6px;color:var(--dim);cursor:pointer;margin:0;min-height:0} +.files-acts .iconbtn:hover{border-color:var(--border);color:var(--text)} +.files-foot{font-family:var(--mono);font-size:10px;padding-top:2px;border-top:1px solid var(--border)} +/* Téléchargement en cours : downloadWorkspaceFile pose .busy sur la ligne. */ +.files-row.busy{opacity:.55;pointer-events:none} +/* Bouton dossier du composeur : même gabarit que le trombone, et teinté quand + le panneau est ouvert — l'état se lit sans regarder à droite. */ +#files-btn{width:18px;height:18px;min-height:18px;flex:none;margin:0;padding:0; + display:flex;align-items:center;justify-content:center;border-radius:999px; + background:transparent;border:1px solid transparent;color:var(--dim)} +#files-btn:hover{background:var(--row-bg-on);border-color:transparent;color:var(--text)} +#files-btn:active{transform:scale(.9)} +#files-btn svg{display:block} +html[data-files="1"] #files-btn{color:var(--accent)} +/* Petit écran : tiroir par la droite, comme les réglages par la gauche. Pousser + une colonne de 320 px sur un téléphone ne laisserait rien à la conversation. */ +@media (max-width:720px){ + #files{position:fixed;inset:0 0 0 auto;width:85%;max-width:340px;z-index:15; + background:var(--bg);padding-top:calc(16px + env(safe-area-inset-top)); + padding-right:calc(16px + env(safe-area-inset-right))} + html[data-files="1"] #files-backdrop{display:block} +} +@media (min-width:721px){ + /* Dans la charte « Terre », le panneau est une carte comme les autres. */ + #files{background:var(--panel);border-left-color:transparent;border-radius:20px; + box-shadow:var(--shadow);margin:0} +} + + + + +
+
+
+