Panneau Fichiers : accéder à ce que l'agent écrit

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VueWA9xcYadaYq65tBisix
This commit is contained in:
Claude committed 2026-08-16 12:25:57 +00:00
1 parent 133b30421a
commit 00b9cc2aee
8 files changed
+1002 -7

No files matched your search

+24
View File
@@ -361,6 +361,12 @@
<span style="display:flex;align-items:center;gap:8px;margin-left:auto">
<span id="ctx-text" class="muted">0 / 0</span>
<button id="ctx-compact" onclick="compactContext()">compacter</button>
<!-- Fichiers produits par l'agent : ouvre le panneau de droite. Même
gabarit que le trombone — ce sont les deux faces du même sujet,
ce qui entre et ce qui sort. -->
<button id="files-btn" onclick="toggleFiles()" title="Fichiers du dossier de travail" aria-label="Fichiers du dossier de travail" aria-expanded="false">
<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>
</button>
<input type="file" id="attach-input" multiple style="display:none" onchange="onAttachPick(event)">
<button id="attach" onclick="document.getElementById('attach-input').click()" title="joindre un fichier" aria-label="joindre un fichier">
<!-- Trombone dessiné en SVG : pas de dépendance à une police d'emoji,
@@ -381,6 +387,24 @@
<div id="sendhint" class="muted">Entrée pour envoyer · Maj+Entrée = nouvelle ligne</div>
</div>
</div>
<!-- Fichiers : ce que l'agent a écrit dans son dossier de travail. Panneau de
DROITE, symétrique des réglages à gauche, ouvert et fermé par le bouton
dossier du pied de carte. Il ne se remplit qu'à l'ouverture (loadFiles) —
un ReadDir à chaque chargement de page pour un panneau fermé serait payé
par tout le monde et utile à personne. -->
<div id="files-backdrop" onclick="toggleFiles()"></div>
<aside id="files" aria-label="Fichiers du dossier de travail">
<div class="files-head">
<span class="stitle">Fichiers</span>
<button id="files-refresh" onclick="loadFiles()" title="rafraîchir">↻</button>
<button id="files-close" onclick="toggleFiles()" title="fermer" aria-label="Fermer">×</button>
</div>
<!-- Fil d'Ariane : le dossier de travail contient des sous-dossiers (captures
par discussion), il faut pouvoir y descendre et en remonter. -->
<div id="files-crumb" class="files-crumb"></div>
<div id="files-list" class="files-list"></div>
<div id="files-foot" class="files-foot muted"></div>
</aside>
<div id="toast"></div>
<div id="bench-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:30;align-items:center;justify-content:center;padding:20px" onclick="if(event.target===this)closeBenchModal()">
<div style="background:var(--bg);border:1px solid var(--border);border-radius:8px;width:100%;max-width:480px;display:flex;flex-direction:column">