Presets : modifier le preset en service l'applique aussitôt

CTX passé à 100000 dans l'éditeur, « enregistré »… et la carte du chat
qui affiche toujours 32768. Seul le fichier du preset changeait :
config.env gardait l'ancienne valeur, le moteur tournait avec, et plus
aucun preset n'était détecté actif (empreinte différente). Il fallait
penser à rebasculer dessus à la main.

SavePresetApplying décide AVANT l'écriture si le preset édité est celui
en service (empreinte de l'ancien fichier == configuration courante) ;
si oui, la nouvelle version est installée comme une bascule
(applyPresetFile : réglages machine et moteur préservés) et le service
redémarre en arrière-plan. Un preset inactif ou nouveau reste un simple
fichier. L'UI le dit et rafraîchit l'état — la jauge de contexte suit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFhcmBMUXdK6UesdGUgFPH
This commit is contained in:
MichaelandClaude Fable 5 committed 2026-08-30 17:13:19 +02:00
1 parent 9d2ff33943
commit 2d4f5378c0
5 files changed
+130 -5

No files matched your search

+35
View File
@@ -366,6 +366,41 @@ func SavePreset(id, name, content string) (string, error) {
return id, os.WriteFile(p, []byte(content), 0o644)
}
// SavePresetApplying enregistre un preset et, si c'était celui EN SERVICE,
// installe aussitôt la nouvelle version comme configuration active (mêmes
// règles qu'une bascule : applyPresetFile). Renvoie applied=true dans ce cas —
// l'appelant redémarre alors le moteur.
//
// Vécu : CTX passé à 100000 dans l'éditeur, « enregistré »… et la carte du
// chat qui affiche toujours 32768. Seul le fichier du preset avait changé ;
// config.env gardait l'ancienne valeur, le moteur tournait avec, et plus aucun
// preset n'était détecté actif (empreinte différente). Éditer le preset en
// service, c'est vouloir ces réglages-là tout de suite. Un preset inactif ou
// nouveau, lui, n'est qu'un fichier : rien ne bouge.
//
// « Actif » se décide AVANT l'écriture : c'est l'ancienne version du fichier qui
// porte l'empreinte de la configuration courante.
func SavePresetApplying(id, name, content string) (newID string, applied bool, err error) {
wasActive := false
if id != "" {
if old, rerr := ReadPreset(id); rerr == nil {
wasActive = presetFingerprint([]byte(old)) == configFingerprint(ReadConfig())
}
}
newID, err = SavePreset(id, name, content)
if err != nil || !wasActive {
return newID, false, err
}
p, err := safePresetPath(newID)
if err != nil {
return newID, false, err
}
if err := applyPresetFile(p); err != nil {
return newID, false, err
}
return newID, true, nil
}
// DeletePreset removes a preset by id; refuses if it is the active config.
func DeletePreset(id string) error {
p, err := safePresetPath(id)
+51
View File
@@ -160,3 +160,54 @@ func TestSwitchToPresetLaisseLePresetImposerSesGPU(t *testing.T) {
t.Errorf("WEB_ENGINE = %q, attendu go (réglage machine, jamais dicté par un preset)", cfg["WEB_ENGINE"])
}
}
// Modifier le preset ACTIF doit changer la configuration courante. Vécu : CTX
// passé à 100000 dans l'éditeur, « enregistré » — et la carte du chat qui
// affiche toujours 32768, parce que seul le fichier du preset avait bougé ;
// config.env, lui, gardait l'ancienne valeur et plus aucun preset n'était
// détecté actif. Un preset qui n'est pas celui en service, ou un preset
// nouveau, ne touche pas à la configuration.
func TestModifierLePresetActifLApplique(t *testing.T) {
testHome(t)
dir := presetsDir()
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
write := func(name, body string) {
if err := os.WriteFile(filepath.Join(dir, name+".env"), []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
write("actif", "# NAME=Actif\nMODEL=foo.gguf\nCTX=32768\n")
write("autre", "# NAME=Autre\nMODEL=bar.gguf\nCTX=4096\n")
setConfig(t, "MODEL=foo.gguf\nCTX=32768\nMEM_MODE=always\n")
// Le preset en service : la config suit, les réglages machine restent.
id, applied, err := SavePresetApplying("actif", "Actif", "# NAME=Actif\nMODEL=foo.gguf\nCTX=100000\n")
if err != nil {
t.Fatal(err)
}
if id != "actif" || !applied {
t.Fatalf("id=%q applied=%v, attendu actif/true", id, applied)
}
cfg := ReadConfig()
if cfg["CTX"] != "100000" || cfg["MEM_MODE"] != "always" {
t.Fatalf("configuration non suivie : %v", cfg)
}
// Un autre preset : le fichier change, la config non.
if _, applied, err = SavePresetApplying("autre", "Autre", "# NAME=Autre\nMODEL=bar.gguf\nCTX=8192\n"); err != nil {
t.Fatal(err)
}
if applied || ReadConfig()["CTX"] != "100000" {
t.Fatalf("un preset inactif a touché la configuration (applied=%v, CTX=%q)", applied, ReadConfig()["CTX"])
}
// Un preset nouveau : jamais appliqué à la volée.
if _, applied, err = SavePresetApplying("", "Neuf", "MODEL=baz.gguf\n"); err != nil {
t.Fatal(err)
}
if applied || ReadConfig()["MODEL"] != "foo.gguf" {
t.Fatalf("un preset nouveau a touché la configuration (applied=%v)", applied)
}
}
+26 -3
View File
@@ -2345,7 +2345,8 @@ html[data-files="1"] #files-btn{color:var(--accent)}
<!-- Fichiers : ce qui appartient à la DISCUSSION ouverte — pièces jointes
déposées, captures, ce que l'agent y a écrit. 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
carte. Il ne se remplit qu'à l'ouverture (loadFiles), puis se redessine seul
à chaque résultat d'outil, fin de tour ou dépôt (filesOnActivity) — 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>
@@ -5303,6 +5304,10 @@ async function saveItem(){
: {name, old: editingKey, content};
const r = await jpost(K.saveUrl, payload);
if(!r.ok){ toast('erreur : ' + (r.error||'')); return; }
// Preset EN SERVICE modifié : le serveur l'a réappliqué et relance le moteur
// (SavePresetApplying). On le dit, et on rafraîchit l'état global — la carte
// du chat doit montrer le nouveau contexte, pas l'ancien.
if(r.applied){ toast('enregistré et appliqué — redémarrage…'); closeModal(); K.reload(); if(typeof loadAll==='function') loadAll(); return; }
toast('enregistré'); closeModal(); K.reload();
}
async function delItem(){
@@ -6670,7 +6675,8 @@ function handleDelta(d){
if(T.serverStats) renderStats(T.contentEl||T.reasonEl, T.serverStats);
else if(T.contentEl) paintStats(T.contentEl); // sans mesures serveur, la durée reste
paintTurnClock(); // fige le total au-dessus de la carte
setBusy(false); if(!REPLAYING && typeof loadConversations==='function') loadConversations(); return; }
setBusy(false); if(!REPLAYING && typeof loadConversations==='function') loadConversations();
if(typeof filesOnActivity==='function') filesOnActivity(); return; }
if(d.error){ settleBlocks(); removeTyping(); T.contentEl=null; T.reasonEl=null; const eb=addMsg('assistant',''); eb.classList.add('errmsg'); renderBody(eb, d.error); return; }
if(d.compacting!==undefined){ setCompacting(d.compacting); return; }
if(d.compacted){ setCompacting(false); addCompactMark(); return; }
@@ -6708,7 +6714,8 @@ function handleDelta(d){
// Carte à image : retirée de la liste du tour, sinon le repli de fin de
// tour (collapseAll) la fermerait quand même.
if(tu.image && T.pendingToolEl){ const i=T.turnCollapsibles.indexOf(T.pendingToolEl); if(i>=0) T.turnCollapsibles.splice(i,1); }
T.pendingToolEl=null; if(tu.name==='mem_add'||tu.name==='mem_edit') loadMem(); }
T.pendingToolEl=null; if(tu.name==='mem_add'||tu.name==='mem_edit') loadMem();
if(typeof filesOnActivity==='function') filesOnActivity(); }
return; }
if(d.drop_reasoning){
smoothReset(); cancelRender(); // le bloc raisonnement disparaît : rien à rendre
@@ -7925,6 +7932,7 @@ window.addEventListener('drop', e=>{
// rouge, elle dit déjà ce qui s'est passé.
async function attachPaths(){
await Promise.all(ATTACH.filter(a=>a.state==='queued').map(uploadAttach));
if(typeof filesOnActivity==='function') filesOnActivity(); // les dépôts apparaissent dans le panneau Fichiers
return ATTACH.filter(a=>a.state==='ok'&&a.path).map(a=>a.path);
}
renderAttach();
@@ -8193,6 +8201,21 @@ function filesOnConvChange(){
if(FILES_OPEN) loadFiles('');
}
// Quelque chose a pu changer sur disque : un outil vient de rendre son résultat
// (l'agent écrit un rapport, une capture, un script), le tour est fini, une
// pièce jointe vient d'être déposée. Le panneau se redessine tout seul —
// jusqu'ici il fallait cliquer « rafraîchir » pour voir apparaître ce que
// l'agent venait d'écrire. Regroupé : une rafale d'appels d'outils ne
// déclenche qu'une lecture. Panneau fermé ou rejeu du journal : rien à faire,
// l'ouverture recharge de toute façon.
let filesRefreshT = 0;
function filesOnActivity(){
if(!FILES_OPEN) return;
if(typeof REPLAYING !== 'undefined' && REPLAYING) return;
clearTimeout(filesRefreshT);
filesRefreshT = setTimeout(()=>{ filesRefreshT = 0; loadFiles(); }, 400);
}
// 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);
+4
View File
@@ -851,6 +851,10 @@ async function saveItem(){
: {name, old: editingKey, content};
const r = await jpost(K.saveUrl, payload);
if(!r.ok){ toast('erreur : ' + (r.error||'')); return; }
// Preset EN SERVICE modifié : le serveur l'a réappliqué et relance le moteur
// (SavePresetApplying). On le dit, et on rafraîchit l'état global — la carte
// du chat doit montrer le nouveau contexte, pas l'ancien.
if(r.applied){ toast('enregistré et appliqué — redémarrage…'); closeModal(); K.reload(); if(typeof loadAll==='function') loadAll(); return; }
toast('enregistré'); closeModal(); K.reload();
}
async function delItem(){
+14 -2
View File
@@ -423,12 +423,24 @@ func handlePresetSave(w http.ResponseWriter, r *http.Request) {
sendJSON(w, 400, map[string]any{"ok": false, "error": err.Error()})
return
}
newID, err := SavePreset(req.ID, req.Name, req.Content)
// Le preset en service modifié est réappliqué sur-le-champ, moteur relancé
// en arrière-plan comme pour une bascule (handleSwitch) : la réponse ne
// doit pas attendre l'arrêt de llama-server.
newID, applied, err := SavePresetApplying(req.ID, req.Name, req.Content)
if err != nil {
sendJSON(w, 400, map[string]any{"ok": false, "error": err.Error()})
return
}
sendJSON(w, 200, map[string]any{"ok": true, "id": newID, "name": req.Name})
if applied {
fmt.Printf("%s config.env <- %s (preset actif modifié)\n", green("[ok]"), newID)
fmt.Println(dim("[info] redémarrage du service..."))
go func() {
if err := serviceAction("restart"); err != nil {
fmt.Printf("%s redémarrage après modification du preset: %v\n", red("[ERREUR]"), err)
}
}()
}
sendJSON(w, 200, map[string]any{"ok": true, "id": newID, "name": req.Name, "applied": applied})
}
func handlePresetDelete(w http.ResponseWriter, r *http.Request) {