diff --git a/cmd/jean/resource_windows_amd64.syso b/cmd/jean/resource_windows_amd64.syso index 49a3bc4..a770c1b 100644 Binary files a/cmd/jean/resource_windows_amd64.syso and b/cmd/jean/resource_windows_amd64.syso differ diff --git a/cmd/jean/resource_windows_arm64.syso b/cmd/jean/resource_windows_arm64.syso index 45c3f44..cbcda36 100644 Binary files a/cmd/jean/resource_windows_arm64.syso and b/cmd/jean/resource_windows_arm64.syso differ diff --git a/cmd/jean/versioninfo.json b/cmd/jean/versioninfo.json index 64ff05d..c258b9e 100644 --- a/cmd/jean/versioninfo.json +++ b/cmd/jean/versioninfo.json @@ -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": { diff --git a/internal/jean/backend_presets.go b/internal/jean/backend_presets.go index 4415ce8..4a9794e 100644 --- a/internal/jean/backend_presets.go +++ b/internal/jean/backend_presets.go @@ -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 diff --git a/internal/jean/backend_presets_test.go b/internal/jean/backend_presets_test.go new file mode 100644 index 0000000..d6d3dc0 --- /dev/null +++ b/internal/jean/backend_presets_test.go @@ -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") + } +} diff --git a/internal/jean/run.go b/internal/jean/run.go index c8712a8..bd81213 100644 --- a/internal/jean/run.go +++ b/internal/jean/run.go @@ -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() { diff --git a/internal/jean/ui/index.html b/internal/jean/ui/index.html index 45c91c6..845fa9d 100644 --- a/internal/jean/ui/index.html +++ b/internal/jean/ui/index.html @@ -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}
diff --git a/internal/jean/ui/src/styles.css b/internal/jean/ui/src/styles.css index 87d6148..873ee4a 100644 --- a/internal/jean/ui/src/styles.css +++ b/internal/jean/ui/src/styles.css @@ -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}