v0.5.3 : détection du preset actif robuste + retouches UI

- Presets : la détection du preset actif compare désormais l'ENSEMBLE des
  KEY=VALUE effectifs (ordre, commentaires, lignes vides et clés appareil
  ignorés) au lieu d'un hash ligne à ligne. Fini le preset qui n'apparaît plus
  en surbrillance après un reformatage de config.env (toggle mémoire/internet…).
  Couvert par un test unitaire.
- UI : toggles « Paramètres » du mode agent dans des cartes bordées ; champs de
  la modale MCP correctement espacés ; section MOTEUR sans contours colorés ;
  liens « mettre à jour » / « vérifier la version » sans soulignement.
This commit is contained in:
nathaninline committed 2026-07-24 17:29:50 +02:00
1 parent 142dd87299
commit 2f1f40c604
8 files changed
+76 -24

No files matched your search

Binary file not shown.
Binary file not shown.
+3 -3
View File
@@ -3,13 +3,13 @@
"FileVersion": {
"Major": 0,
"Minor": 5,
"Patch": 2,
"Patch": 3,
"Build": 0
},
"ProductVersion": {
"Major": 0,
"Minor": 5,
"Patch": 2,
"Patch": 3,
"Build": 0
},
"FileFlagsMask": "3f",
@@ -25,7 +25,7 @@
"LegalCopyright": "Copyright (c) 2026 Jean contributors. MIT License.",
"OriginalFilename": "jean.exe",
"ProductName": "Jean",
"ProductVersion": "0.5.2",
"ProductVersion": "0.5.3",
"Comments": "https://github.com/nathaninline/jean — projet open source (MIT)"
},
"VarFileInfo": {
+25 -14
View File
@@ -59,27 +59,38 @@ func withDisplayName(content, name string) string {
// assignment lines symmetrically from both sides before hashing so a plain
// content match still identifies the active preset.
func presetFingerprint(content []byte) string {
var b strings.Builder
// On réduit le fichier à l'ENSEMBLE de ses affectations KEY=VALUE effectives,
// puis on hache cet ensemble TRIÉ. On ignore : commentaires (dont `# NAME=`),
// lignes vides, l'ordre des lignes, et les clés « appareil » (preservedKeys,
// réappliquées par SwitchToPreset). Ainsi un simple reformatage de config.env
// — réécriture par un toggle mémoire/internet, réordonnancement, commentaires —
// ne « perd » plus le preset actif : seule la config effective est comparée.
// (Avant : hachage ligne à ligne, cassé par le moindre changement cosmétique →
// aucun preset détecté comme actif, souvent après un changement de réglage.)
pairs := []string{}
for _, line := range strings.Split(string(content), "\n") {
if isPreservedAssignment(line) {
t := strings.TrimSpace(line)
if t == "" || strings.HasPrefix(t, "#") {
continue
}
b.WriteString(line)
b.WriteByte('\n')
t = strings.TrimPrefix(t, "export ")
eq := strings.IndexByte(t, '=')
if eq < 0 {
continue
}
key := strings.TrimSpace(t[:eq])
if isPreservedKey(key) {
continue
}
pairs = append(pairs, key+"="+strings.TrimSpace(t[eq+1:]))
}
h := sha1.Sum([]byte(b.String()))
sort.Strings(pairs)
h := sha1.Sum([]byte(strings.Join(pairs, "\n")))
return hex.EncodeToString(h[:])
}
// isPreservedAssignment reports whether a line assigns one of the preservedKeys
// (commented or not), e.g. `MEM_MODE=off` or `# CRAWL4AI_URL=...`.
func isPreservedAssignment(line string) bool {
t := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(line), "#"))
eq := strings.IndexByte(t, '=')
if eq < 0 {
return false
}
key := strings.TrimSpace(t[:eq])
// isPreservedKey reports whether key is one of the device-level preservedKeys.
func isPreservedKey(key string) bool {
for _, k := range preservedKeys {
if key == k {
return true
+23
View File
@@ -0,0 +1,23 @@
package jean
import "testing"
// TestPresetFingerprintIgnoresCosmetics vérifie que la détection du preset actif
// ne dépend QUE de la config effective (ensemble KEY=VALUE), pas de la mise en
// forme : ordre des lignes, commentaires (dont `# NAME=`), lignes vides, `export`,
// espaces, ni des clés « appareil » (preservedKeys) réappliquées par SwitchToPreset.
// C'est la régression « aucun preset en surbrillance après un reformatage de
// config.env » (toggle mémoire/internet, réordonnancement…).
func TestPresetFingerprintIgnoresCosmetics(t *testing.T) {
base := []byte("# NAME=Mon preset\nMODEL=foo.gguf\nCTX=4096\nNGL=999\n")
// Même config effective, présentée autrement.
variant := []byte("export NGL=999\nMEM_MODE=off\n\nCTX=4096\n# un commentaire\nMODEL=foo.gguf\n")
if presetFingerprint(base) != presetFingerprint(variant) {
t.Fatal("empreintes différentes alors que la config effective est identique")
}
// Un vrai changement de valeur doit, lui, produire une empreinte différente.
changed := []byte("MODEL=foo.gguf\nCTX=8192\nNGL=999\n")
if presetFingerprint(base) == presetFingerprint(changed) {
t.Fatal("empreintes identiques alors que CTX diffère")
}
}
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"strings"
)
const Version = "0.5.2"
const Version = "0.5.3"
// Main est le vrai main() du binaire (cmd/jean ne fait que l'appeler).
func Main() {
+12 -3
View File
@@ -297,15 +297,14 @@ button:disabled{opacity:.5;cursor:not-allowed}
.lc-modes{display:flex;flex-direction:column;gap:8px}
.lc-modes.busy{opacity:.5;pointer-events:none}
.lc-mode{border:1px solid var(--border);border-radius:8px;padding:10px 12px;cursor:pointer;transition:border-color .12s,background .12s}
.lc-mode:hover{border-color:var(--accent)}
.lc-mode.installed{border-color:var(--accent)}
.lc-mode:hover{border-color:var(--dim)}
.lc-mode-t{font-size:13px;font-weight:600;color:var(--text);display:flex;align-items:center;gap:6px}
.lc-reco{font-size:9px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--accent);border:1px solid var(--accent);border-radius:8px;padding:1px 6px}
.lc-mode-d{font-size:11px;color:var(--dim);margin-top:3px}
.lc-mode-state{font-size:11px;margin-top:7px;display:flex;align-items:center;gap:12px;flex-wrap:wrap}
.lc-mode-go{color:var(--accent)}
.lc-mode-active-tag{color:var(--accent);font-weight:600}
.lc-update-link{color:var(--dim);text-decoration:underline;cursor:pointer}
.lc-update-link{color:var(--dim);text-decoration:none;cursor:pointer}
.lc-update-link:hover{color:var(--accent)}
/* Choix du moteur dans l'éditeur de modèle (3 options : rapide/optimisé/perso) */
.be-opts{display:flex;flex-direction:column;gap:7px;margin:6px 0 8px}
@@ -384,6 +383,16 @@ button:disabled{opacity:.5;cursor:not-allowed}
.io-cap{color:var(--dim);font-size:11px;margin:0 0 3px}
.io-input{flex:1;min-width:0;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:6px;font:inherit;font-size:12px}
.io-group{margin-top:12px}
/* Toggles de la sous-section « Paramètres » du mode agent : chacun dans sa
propre carte bordée et arrondie (comme les serveurs MCP), plus lisible. */
#param-block .switchrow{border:1px solid var(--border);border-radius:8px;padding:9px 11px;background:var(--bg)}
#param-block .switchrow+.switchrow{margin-top:8px}
/* Modale MCP (ajout/édition) : les champs des groupes stdio/http n'avaient aucun
espacement vertical (le conteneur n'est pas un flex à gap) → ils se touchaient.
On aligne leur rythme sur celui du reste du formulaire (.mform). */
#mcp-stdio-fields,#mcp-http-fields{display:flex;flex-direction:column;gap:14px}
</style></head><body>
<button id="menubtn" onclick="toggleSide()">☰</button>
<div id="backdrop" onclick="toggleSide()"></div>
+12 -3
View File
@@ -278,15 +278,14 @@ button:disabled{opacity:.5;cursor:not-allowed}
.lc-modes{display:flex;flex-direction:column;gap:8px}
.lc-modes.busy{opacity:.5;pointer-events:none}
.lc-mode{border:1px solid var(--border);border-radius:8px;padding:10px 12px;cursor:pointer;transition:border-color .12s,background .12s}
.lc-mode:hover{border-color:var(--accent)}
.lc-mode.installed{border-color:var(--accent)}
.lc-mode:hover{border-color:var(--dim)}
.lc-mode-t{font-size:13px;font-weight:600;color:var(--text);display:flex;align-items:center;gap:6px}
.lc-reco{font-size:9px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--accent);border:1px solid var(--accent);border-radius:8px;padding:1px 6px}
.lc-mode-d{font-size:11px;color:var(--dim);margin-top:3px}
.lc-mode-state{font-size:11px;margin-top:7px;display:flex;align-items:center;gap:12px;flex-wrap:wrap}
.lc-mode-go{color:var(--accent)}
.lc-mode-active-tag{color:var(--accent);font-weight:600}
.lc-update-link{color:var(--dim);text-decoration:underline;cursor:pointer}
.lc-update-link{color:var(--dim);text-decoration:none;cursor:pointer}
.lc-update-link:hover{color:var(--accent)}
/* Choix du moteur dans l'éditeur de modèle (3 options : rapide/optimisé/perso) */
.be-opts{display:flex;flex-direction:column;gap:7px;margin:6px 0 8px}
@@ -365,3 +364,13 @@ button:disabled{opacity:.5;cursor:not-allowed}
.io-cap{color:var(--dim);font-size:11px;margin:0 0 3px}
.io-input{flex:1;min-width:0;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:6px;font:inherit;font-size:12px}
.io-group{margin-top:12px}
/* Toggles de la sous-section « Paramètres » du mode agent : chacun dans sa
propre carte bordée et arrondie (comme les serveurs MCP), plus lisible. */
#param-block .switchrow{border:1px solid var(--border);border-radius:8px;padding:9px 11px;background:var(--bg)}
#param-block .switchrow+.switchrow{margin-top:8px}
/* Modale MCP (ajout/édition) : les champs des groupes stdio/http n'avaient aucun
espacement vertical (le conteneur n'est pas un flex à gap) → ils se touchaient.
On aligne leur rythme sur celui du reste du formulaire (.mform). */
#mcp-stdio-fields,#mcp-http-fields{display:flex;flex-direction:column;gap:14px}