v0.6.9 : l'IA écrit enfin les fichiers directement

Nouvel outil write : créer ou réécrire un fichier passe par le disque, plus
par le shell. Sous Windows le shell est cmd.exe et son quoting cassait tout
script contenant des guillemets imbriqués (echo, python -c, base64) ; l'IA
bouclait sans jamais y arriver. Signalé par Arnaud Hocquelet.

L'outil bash annonce désormais le vrai shell de la plateforme (cmd.exe sous
Windows) au lieu de dire bash partout, pour que le modèle arrête de générer
la syntaxe d'un autre shell.

Interface : le contenu d'une écriture se remplit ligne à ligne en direct dans
la bulle (diffusé à la ligne et pas au token, pour ne pas saturer le rendu
mobile), et les compteurs +N -N passent sur l'étiquette pour rester visibles
une fois la bulle repliée.
This commit is contained in:
nathaninline committed 2026-08-03 15:12:18 +02:00
1 parent 73a772520c
commit 7b0a5dbf75
14 files changed
+286 -30

No files matched your search

+1 -1
View File
@@ -60,7 +60,7 @@
<div id="presets"></div>
</details>
<details><summary><span class="stitle">Mode agent</span><span id="agent-badge" class="sbadge"></span></summary>
<div class="slist"><label class="switchrow"><span class="switch"><input type="checkbox" id="agent-toggle" onchange="toggleAgent()"><span class="slider"></span></span> activer le mode agent<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">⚠ active l'<b>accès machine</b> (shell complet via <b>bash</b> sur le serveur) — l'IA peut inspecter, lancer des scripts, lire/éditer des fichiers (timeout 30s, max 300s). Mémoire et accès internet ci-dessous ne fonctionnent que si le mode agent est activé.</span></span></label></div>
<div class="slist"><label class="switchrow"><span class="switch"><input type="checkbox" id="agent-toggle" onchange="toggleAgent()"><span class="slider"></span></span> activer le mode agent<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">⚠ active l'<b>accès machine</b> (shell complet via <b>bash</b> sur le serveur) — l'IA peut inspecter, lancer des scripts, écrire/lire/éditer des fichiers (timeout 30s, max 300s). Mémoire et accès internet ci-dessous ne fonctionnent que si le mode agent est activé.</span></span></label></div>
<!-- Mémoire / Accès internet / Paramètres : sous-réglages du mode agent, en cartes. Grisés/inertes quand l'agent est off (voir setAgentGate). -->
<div id="mem-block" class="gated-block">
<div class="subhead">Mémoire<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span id="mem-mode-desc" class="tip"></span></span></div>
+45 -6
View File
@@ -104,6 +104,17 @@ function collapseInstant(el){
requestAnimationFrame(()=>{ bw.style.transition=''; });
}
function setLabel(el, text){ el.querySelector('.label').textContent = text; }
// Ajoute « +N -N » colorés à l'étiquette d'une bulle. L'étiquette reste visible
// une fois la bulle repliée : c'est le seul endroit où le volume d'une écriture
// survit au repli, donc on le met là plutôt que dans le corps seul.
function setLabelCounts(el, add, del){
const lab=el.querySelector('.label');
const cnt=document.createElement('span'); cnt.className='diff-count';
if(add) cnt.appendChild(Object.assign(document.createElement('span'),{className:'a',textContent:'+'+add}));
if(add && del) cnt.appendChild(document.createTextNode(' '));
if(del) cnt.appendChild(Object.assign(document.createElement('span'),{className:'d',textContent:'-'+del}));
lab.appendChild(cnt);
}
// Ligne de mesures sous une réponse (prefill / decode). Les étiquettes VOUS/JEAN
// sont masquées dans cette mise en page, donc les chiffres qu'on y écrivait
// avaient disparu : ils ont leur propre ligne, discrète, sous le texte. Masquée
@@ -132,6 +143,7 @@ function renderToolMsg(el, tu){
// (web_search/open/read/grep) ont leur propre libellé, pas le fallback mémoire.
const META = {
bash: {lbl:'terminal', head:'commande'},
write: {lbl:'fichier', head:'écriture'},
edit: {lbl:'édition', head:'édition'},
web_search: {lbl:'recherche', head:'recherche web'},
web_open: {lbl:'page web', head:'ouverture'},
@@ -152,6 +164,12 @@ function renderToolMsg(el, tu){
// Indication du volume de la réponse de l'outil (~tokens, estimation 1 tok ≈ 4 car).
if(tu.result){ lbl += ' · ~' + Math.max(1, Math.round(tu.result.length/4)) + ' tok'; }
setLabel(el, lbl);
// Volume de l'écriture (final si le diff est là, provisoire pendant la frappe)
// reporté sur l'étiquette, pour rester lisible bulle repliée.
let add=0, del=0;
if(tu.diff && tu.diff.length){ tu.diff.forEach(l=>{ if(l.op==='+') add++; else if(l.op==='-') del++; }); }
else if(tu.body){ add=tu.body.split('\n').length; }
if(add||del) setLabelCounts(el, add, del);
const body=bodyOf(el); body.innerHTML='';
const head=document.createElement('div'); head.className='tool-head';
head.textContent = meta.head;
@@ -162,16 +180,37 @@ function renderToolMsg(el, tu){
if(tu.typing){ const car=document.createElement('span'); car.className='tool-caret'; car.textContent='▋'; code.appendChild(car); }
pre.appendChild(code); body.appendChild(pre);
}
// Écriture EN COURS : le modèle tape encore le contenu. On l'affiche ligne à
// ligne, dans la même forme que le diff final, pour que la bulle se remplisse
// sous les yeux au lieu de rester vide puis de s'ouvrir d'un coup. Seule la
// dernière ligne est « fraîche » (fondu) : réanimer tout à chaque événement
// ferait clignoter le bloc entier.
if(tu.body && !(tu.diff && tu.diff.length)){
const lines=tu.body.split('\n');
const sub=document.createElement('div'); sub.className='tool-sub';
sub.textContent='écriture en cours'; // le +N vit sur l'étiquette (visible repliée)
body.appendChild(sub);
const pre=document.createElement('pre'); pre.className='diff live';
lines.forEach((t,i)=>{
const ln=document.createElement('span');
ln.className='dl add'+(i===lines.length-1?' fresh':'');
ln.textContent='+ '+t;
if(i===lines.length-1 && tu.typing){
const car=document.createElement('span'); car.className='tool-caret'; car.textContent='▋';
ln.appendChild(car);
}
pre.appendChild(ln);
});
body.appendChild(pre);
// Le bloc est re-créé à chaque événement : on le recale en bas pour suivre
// la ligne en cours (max-height côté CSS l'empêche de pousser le fil).
pre.scrollTop = pre.scrollHeight;
}
// Diff d'une écriture (fichier ou page de mémoire) : lignes ajoutées en vert,
// retirées en rouge, contexte en gris — comme un diff de terminal.
if(tu.diff && tu.diff.length){
const sub=document.createElement('div'); sub.className='tool-sub';
let add=0, del=0;
tu.diff.forEach(l=>{ if(l.op==='+') add++; else if(l.op==='-') del++; });
sub.textContent='modifications';
const cnt=document.createElement('span'); cnt.className='diff-count';
cnt.innerHTML='<span class="a">+'+add+'</span> <span class="d">-'+del+'</span>';
sub.appendChild(cnt);
sub.textContent='modifications'; // le +N -N vit sur l'étiquette (visible repliée)
body.appendChild(sub);
const pre=document.createElement('pre'); pre.className='diff';
tu.diff.forEach(l=>{
+7
View File
@@ -247,6 +247,13 @@ button:disabled{opacity:.5;cursor:not-allowed}
.msg.tool pre.diff .dl{display:block;padding:1px 10px;white-space:pre-wrap;word-break:break-word;color:var(--dim)}
.msg.tool pre.diff .dl.add{color:var(--add);background:var(--add-bg)}
.msg.tool pre.diff .dl.del{color:var(--del);background:var(--del-bg)}
/* Écriture en direct : le bloc se remplit ligne à ligne pendant que le modèle
tape. Seule la dernière ligne est en fondu (.fresh) — animer tout le bloc à
chaque nouvelle ligne le ferait clignoter. Hauteur bornée + scroll auto en
bas (voir 08-chat-render.js) pour qu'un long script ne pousse pas le fil. */
.msg.tool pre.diff.live .dl.fresh{animation:difffresh .18s ease-out}
@keyframes difffresh{from{opacity:0;transform:translateY(2px)}to{opacity:1;transform:none}}
@media (prefers-reduced-motion:reduce){.msg.tool pre.diff.live .dl.fresh{animation:none}}
.diff-count{margin-left:8px;letter-spacing:0;text-transform:none}
.diff-count .a{color:var(--add)}
.diff-count .d{color:var(--del)}