diff --git a/README.md b/README.md index cecbede..e2c89b1 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# Jean +# AJEAN -![Interface web de Jean](docs/ui.png) +![Interface web d'AJEAN](docs/ui.png) **Un gestionnaire mono-binaire pour serveurs [llama.cpp](https://github.com/ggml-org/llama.cpp) auto-hébergés — avec une interface web intégrée, un chat terminal, et un compilateur de backend qui détecte automatiquement le matériel.** -Dépose un seul binaire sur une machine, lance `jean llamacpp install`, et Jean clone, configure et compile llama.cpp pour le matériel de *cette* machine (CUDA / ROCm / Metal / Vulkan / CPU) — aucun flag à retenir. Ensuite `jean start` et tu as un endpoint compatible OpenAI avec un chat web par-dessus. +Dépose un seul binaire sur une machine, lance `jean llamacpp install`, et AJEAN clone, configure et compile llama.cpp pour le matériel de *cette* machine (CUDA / ROCm / Metal / Vulkan / CPU) — aucun flag à retenir. Ensuite `jean start` et tu as un endpoint compatible OpenAI avec un chat web par-dessus. ``` télécharge le binaire → jean llamacpp install → jean edit → jean start → c'est parti @@ -14,7 +14,7 @@ télécharge le binaire → jean llamacpp install → jean edit → jean s ## Pourquoi -Faire tourner llama.cpp comme un vrai service, ça veut dire d'habitude : trouver les bons flags CMake pour ton GPU, écrire une unité systemd, gérer une clé API, changer de modèle, garder le build à jour… Jean transforme tout ça en quelques sous-commandes derrière un binaire statique unique, **sans dépendance à l'exécution** (à part llama.cpp lui-même, que Jean peut compiler pour toi). +Faire tourner llama.cpp comme un vrai service, ça veut dire d'habitude : trouver les bons flags CMake pour ton GPU, écrire une unité systemd, gérer une clé API, changer de modèle, garder le build à jour… AJEAN transforme tout ça en quelques sous-commandes derrière un binaire statique unique, **sans dépendance à l'exécution** (à part llama.cpp lui-même, que AJEAN peut compiler pour toi). ## Fonctionnalités @@ -59,7 +59,7 @@ sudo jean install jean llamacpp install ``` -Jean détecte ton accélérateur, compile `llama-server`, et pointe la config sur le nouveau binaire. Nécessite `git` et `cmake` (plus le toolkit correspondant, ex. CUDA, si tu veux l'accélération GPU). +AJEAN détecte ton accélérateur, compile `llama-server`, et pointe la config sur le nouveau binaire. Nécessite `git` et `cmake` (plus le toolkit correspondant, ex. CUDA, si tu veux l'accélération GPU). ### 4. Pointe-le sur un modèle et démarre @@ -77,7 +77,7 @@ jean web # http://:8090 ## Windows -Jean fonctionne aussi sur Windows. Les différences avec Linux : +AJEAN fonctionne aussi sur Windows. Les différences avec Linux : - **Pas de systemd.** `jean start` lance `jean serve` en arrière-plan (processus détaché), suivi via un fichier PID. `stop` / `restart` / `status` / `logs` agissent dessus — aucun droit administrateur requis. `enable` / `disable` (démarrage au boot) ne sont pas gérés : utilise une tâche planifiée ou `sc.exe` si tu en as besoin. - **`JEAN_HOME`** vaut par défaut `%ProgramData%\jean` (repli `%LOCALAPPDATA%\jean`). Surcharge avec la variable d'environnement `JEAN_HOME`. @@ -98,7 +98,7 @@ jean logs # suit %ProgramData%\jean\jean.log ## macOS -Comme sous Windows, tu peux lancer Jean **d'un simple clic** : la page [Releases](../../releases) publie `Jean-macos-arm64.zip` / `Jean-macos-amd64.zip`, un bundle **`Jean.app`**. Dézippe, glisse-le dans *Applications*, ouvre-le : Jean démarre son UI web sur `http://localhost:8090`, l'ouvre dans le navigateur et pose son **icône dans la barre de menus** (en haut à droite) — pendant exact du system tray Windows, avec « Ouvrir Jean » / « Quitter ». Pas de fenêtre de Terminal, pas d'icône dans le Dock. +Comme sous Windows, tu peux lancer AJEAN **d'un simple clic** : la page [Releases](../../releases) publie `Jean-macos-arm64.zip` / `Jean-macos-amd64.zip`, un bundle **`Jean.app`**. Dézippe, glisse-le dans *Applications*, ouvre-le : AJEAN démarre son UI web sur `http://localhost:8090`, l'ouvre dans le navigateur et pose son **icône dans la barre de menus** (en haut à droite) — pendant exact du system tray Windows, avec « Ouvrir AJEAN » / « Quitter ». Pas de fenêtre de Terminal, pas d'icône dans le Dock. L'app n'est **pas signée** par un certificat Apple (seulement ad-hoc) : au premier lancement, fais **clic droit → Ouvrir** (ou *Réglages Système → Confidentialité et sécurité → Ouvrir quand même*). @@ -188,7 +188,7 @@ La clé API (quand elle est définie avec `jean set-api-key`) est stockée dans ### API de pilotage à distance -`jean web` expose une API HTTP pour piloter Jean à distance : status, VRAM, liste/sélection de presets (switch de modèle), démarrage/arrêt/redémarrage du service, chat. Pour l'exposer sur internet en sécurité, protège-la par une clé : +`jean web` expose une API HTTP pour piloter AJEAN à distance : status, VRAM, liste/sélection de presets (switch de modèle), démarrage/arrêt/redémarrage du service, chat. Pour l'exposer sur internet en sécurité, protège-la par une clé : ``` jean set-web-key # génère une clé aléatoire @@ -215,7 +215,7 @@ Endpoints utiles pour un client : La clé est stockée dans `$JEAN_HOME/.web_key`, distincte de `.api_key` (pilotage ≠ accès complétions), et relue à chaud à chaque requête. Le pilotage du service est cross-platform (systemd sous Linux, supervision par PID-file sous Windows). -> ⚠️ La clé voyage en clair en HTTP. Pour une exposition publique, place Jean derrière un reverse-proxy HTTPS (Caddy, nginx) ou un tunnel (Tailscale, Cloudflare Tunnel). +> ⚠️ La clé voyage en clair en HTTP. Pour une exposition publique, place AJEAN derrière un reverse-proxy HTTPS (Caddy, nginx) ou un tunnel (Tailscale, Cloudflare Tunnel). ### Accès internet de l'IA (Crawl4AI) @@ -256,13 +256,13 @@ Une fois lié, tu retrouves depuis le portail : - la gestion de **plusieurs serveurs** et d'**agents** depuis un tableau de bord ; - en option, un **endpoint compatible OpenAI** (voir ci-dessous). -C'est un service optionnel et payant ; tout le reste de Jean est et restera open source et gratuit. +C'est un service optionnel et payant ; tout le reste d'AJEAN est et restera open source et gratuit. ### Sécurité — boîte noire `jean link` est conçu pour que le relais ajean.link soit un **pur tube aveugle** : il transporte tes données mais ne peut pas les lire. -- **Chat chiffré de bout en bout.** Le chat entre ton navigateur et ton serveur Jean est chiffré (X25519 + AES-GCM) : le relais ne voit ni tes prompts ni les réponses, seulement de l'opaque. La clé est dérivée de ton mot de passe (protocole **OPAQUE**) et ne quitte jamais ton navigateur. +- **Chat chiffré de bout en bout.** Le chat entre ton navigateur et ton serveur AJEAN est chiffré (X25519 + AES-GCM) : le relais ne voit ni tes prompts ni les réponses, seulement de l'opaque. La clé est dérivée de ton mot de passe (protocole **OPAQUE**) et ne quitte jamais ton navigateur. - **Empreinte vérifiée.** `jean link` affiche une empreinte de la clé de la machine, à confirmer une fois dans le portail — ça défait toute tentative d'interception par le relais. - **Appairage authentifié.** En plus de l'empreinte, tu autorises ton navigateur avec un **code d'appairage** (`jean link code`) : à usage unique et valable 10 min, il garantit que seul *ton* navigateur peut piloter le serveur — même un relais compromis ne peut pas forger de commande. - **Aucun chat en clair par le relais.** L'ancien chemin en clair est refusé à travers le tunnel ; seul le chemin chiffré transporte du contenu. @@ -272,7 +272,7 @@ C'est un service optionnel et payant ; tout le reste de Jean est et restera open #### Endpoint OpenAI (`.oai.ajean.link`) — opt-in -Pour brancher des outils tiers (OpenCode, etc.), Jean peut exposer un endpoint compatible OpenAI **par machine** : `https://.oai.ajean.link/v1`. L'authentification est la clé API de ton `llama-server` (`jean set-api-key`), présentée en `Authorization: Bearer `. C'est **désactivé par défaut**. +Pour brancher des outils tiers (OpenCode, etc.), AJEAN peut exposer un endpoint compatible OpenAI **par machine** : `https://.oai.ajean.link/v1`. L'authentification est la clé API de ton `llama-server` (`jean set-api-key`), présentée en `Authorization: Bearer `. C'est **désactivé par défaut**. Tu l'actives par machine depuis l'**interface web** (panneau « Accès OpenAI ») : le drapeau est relu **en direct** par le tunnel, sans redémarrage. *(Ancien mécanisme : la variable d'environnement `JEAN_LINK_ALLOW_OAI=1` sur le service — encore acceptée pour rétro-compatibilité, mais le toggle de l'UI est désormais la voie recommandée.)* @@ -288,7 +288,7 @@ Contrairement aux premières versions, ce flux **n'est pas en clair chez le rela ## Compiler depuis les sources -Nécessite Go 1.25+. Jean est un binaire 100 % Go (l'UI web est embarquée via `go:embed`) : +Nécessite Go 1.25+. AJEAN est un binaire 100 % Go (l'UI web est embarquée via `go:embed`) : ```bash git clone https://github.com/nathaninline/jean.git @@ -299,7 +299,7 @@ CGO_ENABLED=0 go build -o jean ./cmd/jean GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o jean-linux-amd64 ./cmd/jean ``` -> Compiler **Jean** ne nécessite que Go. Compiler le **backend llama.cpp** (`jean llamacpp install`) nécessite `git`, `cmake`, et le toolkit de ton accélérateur (CUDA, ROCm, etc.). +> Compiler **AJEAN** ne nécessite que Go. Compiler le **backend llama.cpp** (`jean llamacpp install`) nécessite `git`, `cmake`, et le toolkit de ton accélérateur (CUDA, ROCm, etc.). ## Comment ça marche diff --git a/cmd/jean/resource_windows_amd64.syso b/cmd/jean/resource_windows_amd64.syso index a750ba6..3f0ba29 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 61e9918..05efd88 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 584977e..a3cc7ed 100644 --- a/cmd/jean/versioninfo.json +++ b/cmd/jean/versioninfo.json @@ -3,13 +3,13 @@ "FileVersion": { "Major": 0, "Minor": 5, - "Patch": 9, + "Patch": 10, "Build": 0 }, "ProductVersion": { "Major": 0, "Minor": 5, - "Patch": 9, + "Patch": 10, "Build": 0 }, "FileFlagsMask": "3f", @@ -19,13 +19,13 @@ "FileSubType": "00" }, "StringFileInfo": { - "CompanyName": "Jean contributors", - "FileDescription": "Jean — gestionnaire mono-binaire de serveurs llama.cpp auto-hébergés", + "CompanyName": "AJEAN contributors", + "FileDescription": "AJEAN — gestionnaire mono-binaire de serveurs llama.cpp auto-hébergés", "InternalName": "jean", - "LegalCopyright": "Copyright (c) 2026 Jean contributors. MIT License.", + "LegalCopyright": "Copyright (c) 2026 AJEAN contributors. MIT License.", "OriginalFilename": "jean.exe", - "ProductName": "Jean", - "ProductVersion": "0.5.9", + "ProductName": "AJEAN", + "ProductVersion": "0.5.10", "Comments": "https://github.com/nathaninline/jean — projet open source (MIT)" }, "VarFileInfo": { diff --git a/internal/jean/chat_conversation.go b/internal/jean/chat_conversation.go index 65982a0..bbf4b39 100644 --- a/internal/jean/chat_conversation.go +++ b/internal/jean/chat_conversation.go @@ -324,10 +324,16 @@ func (c *Conversation) generate(ctx context.Context, caps Caps, temperature floa case ev.Err != nil: c.appendDelta(epoch, map[string]any{"error": ev.Err.Error()}) case ev.ToolUsed != nil: - c.appendDelta(epoch, map[string]any{"tool_used": map[string]any{ + tu := map[string]any{ "name": ev.ToolUsed.Name, "label": ev.ToolUsed.Label, "result": ev.ToolUsed.Result, "done": ev.ToolUsed.Done, "typing": ev.ToolUsed.Typing, - }}) + } + // Lignes +/- d'une écriture (edit / mémoire) : persistées avec le reste + // pour que le diff soit encore là après un rafraîchissement. + if len(ev.ToolUsed.Diff) > 0 { + tu["diff"] = ev.ToolUsed.Diff + } + c.appendDelta(epoch, map[string]any{"tool_used": tu}) case ev.Stats != nil: // Taille réelle du contexte (usage.prompt_tokens + généré) pour le compteur // et la décision de compactage au tour suivant. diff --git a/internal/jean/chat_diff.go b/internal/jean/chat_diff.go new file mode 100644 index 0000000..11e852c --- /dev/null +++ b/internal/jean/chat_diff.go @@ -0,0 +1,121 @@ +package jean + +// chat_diff.go — petit diff ligne à ligne pour l'interface : quand l'IA modifie +// un fichier (outil `edit`) ou une page de mémoire (`mem_edit`, `mem_add`), on +// envoie à l'UI le détail des lignes ajoutées et retirées, qu'elle affiche en +// vert (+) et rouge (-). Aucune dépendance : LCS classique sur les lignes, avec +// des garde-fous pour ne jamais transformer un gros remplacement en pavé. + +import "strings" + +const ( + diffMaxLines = 400 // au-delà, on compare sans détail (trop gros / trop lent) + diffMaxShown = 120 // lignes envoyées à l'UI (le reste est résumé) +) + +// DiffLine est une ligne de diff : Op vaut " " (contexte), "-" ou "+". +type DiffLine struct { + Op string `json:"op"` + Text string `json:"text"` +} + +// lineDiff compare deux blocs de texte ligne à ligne. Le résultat garde les +// lignes communes comme contexte : c'est ce qui rend le changement lisible +// quand l'IA ne touche qu'un mot au milieu d'un paragraphe. +func lineDiff(oldText, newText string) []DiffLine { + a := splitLines(oldText) + b := splitLines(newText) + // Blocs énormes : on ne calcule pas la LCS (coût quadratique), on montre + // simplement l'ancien en retrait et le nouveau en ajout. + if len(a) > diffMaxLines || len(b) > diffMaxLines { + out := make([]DiffLine, 0, len(a)+len(b)) + for _, l := range a { + out = append(out, DiffLine{Op: "-", Text: l}) + } + for _, l := range b { + out = append(out, DiffLine{Op: "+", Text: l}) + } + return capLines(out) + } + + // LCS : table des longueurs, puis remontée. + n, m := len(a), len(b) + lcs := make([][]int, n+1) + for i := range lcs { + lcs[i] = make([]int, m+1) + } + for i := n - 1; i >= 0; i-- { + for j := m - 1; j >= 0; j-- { + if a[i] == b[j] { + lcs[i][j] = lcs[i+1][j+1] + 1 + } else if lcs[i+1][j] >= lcs[i][j+1] { + lcs[i][j] = lcs[i+1][j] + } else { + lcs[i][j] = lcs[i][j+1] + } + } + } + var out []DiffLine + i, j := 0, 0 + for i < n && j < m { + switch { + case a[i] == b[j]: + out = append(out, DiffLine{Op: " ", Text: a[i]}) + i++ + j++ + case lcs[i+1][j] >= lcs[i][j+1]: + out = append(out, DiffLine{Op: "-", Text: a[i]}) + i++ + default: + out = append(out, DiffLine{Op: "+", Text: b[j]}) + j++ + } + } + for ; i < n; i++ { + out = append(out, DiffLine{Op: "-", Text: a[i]}) + } + for ; j < m; j++ { + out = append(out, DiffLine{Op: "+", Text: b[j]}) + } + return capLines(out) +} + +// addedDiff présente un contenu entièrement nouveau (création d'une page). +func addedDiff(text string) []DiffLine { + lines := splitLines(text) + out := make([]DiffLine, 0, len(lines)) + for _, l := range lines { + out = append(out, DiffLine{Op: "+", Text: l}) + } + return capLines(out) +} + +func splitLines(s string) []string { + s = strings.ReplaceAll(s, "\r\n", "\n") + if s == "" { + return nil + } + return strings.Split(s, "\n") +} + +// capLines borne la taille envoyée à l'UI et signale ce qui a été coupé. +func capLines(d []DiffLine) []DiffLine { + if len(d) <= diffMaxShown { + return d + } + cut := len(d) - diffMaxShown + out := append([]DiffLine{}, d[:diffMaxShown]...) + return append(out, DiffLine{Op: " ", Text: "…(" + itoa(cut) + " lignes de plus)"}) +} + +func itoa(n int) string { + if n == 0 { + return "0" + } + var b []byte + for n > 0 { + b = append([]byte{byte('0' + n%10)}, b...) + n /= 10 + } + return string(b) +} diff --git a/internal/jean/chat_memory.go b/internal/jean/chat_memory.go index dcb40f0..c85824e 100644 --- a/internal/jean/chat_memory.go +++ b/internal/jean/chat_memory.go @@ -1,6 +1,7 @@ package jean import ( + "errors" "fmt" "os" "path/filepath" @@ -215,6 +216,10 @@ func MemAdd(name, content string) error { // MemEdit remplace oldText par newText dans une page. oldText doit apparaître // EXACTEMENT une fois (sinon erreur), pour une édition sans ambiguïté. +// errAlreadyApplied signale une édition dont le résultat est DÉJÀ en place : +// ce n'est pas un échec, la page est dans l'état demandé. +var errAlreadyApplied = errors.New("déjà à jour — la page contient déjà cette modification") + func MemEdit(name, oldText, newText string) error { p, err := safeMemPath(name) if err != nil { @@ -230,6 +235,11 @@ func MemEdit(name, oldText, newText string) error { return fmt.Errorf("old vide") } if n == 0 { + // Déjà remplacé (le modèle rejoue souvent la même édition) : ce n'est pas + // une erreur, la page est dans l'état demandé. + if newText != "" && strings.Contains(content, newText) { + return errAlreadyApplied + } return fmt.Errorf("old introuvable dans la page") } if n > 1 { diff --git a/internal/jean/chat_tools.go b/internal/jean/chat_tools.go index ba59256..3554097 100644 --- a/internal/jean/chat_tools.go +++ b/internal/jean/chat_tools.go @@ -179,6 +179,11 @@ func fileEdit(path, oldText, newText string) string { content := string(b) n := strings.Count(content, oldText) if n == 0 { + // Modification déjà en place : on le dit clairement plutôt que de renvoyer + // une erreur, sinon le modèle croit avoir échoué et recommence. + if newText != "" && strings.Contains(content, newText) { + return "[ok] déjà à jour — le fichier contient déjà cette modification" + } return "[erreur] old introuvable dans le fichier" } if n > 1 { diff --git a/internal/jean/cli_app.go b/internal/jean/cli_app.go index 32bf63b..aa36c04 100644 --- a/internal/jean/cli_app.go +++ b/internal/jean/cli_app.go @@ -35,7 +35,7 @@ func cmdApp(args []string) error { if err != nil { // Jean tourne déjà sur ce port : on ouvre juste l'UI sur l'instance // existante plutôt que d'échouer. - fmt.Printf("Jean est déjà lancé — ouverture de %s\n", url) + fmt.Printf("AJEAN est déjà lancé — ouverture de %s\n", url) return openBrowser(url) } @@ -59,7 +59,7 @@ func cmdApp(args []string) error { startAppLink(appWebMux) }() - sp := showSplash("Lancement de Jean en cours…") + sp := showSplash("Lancement d'AJEAN en cours…") waitServerReady(url) _ = openBrowser(url) time.Sleep(900 * time.Millisecond) // laisse le navigateur s'afficher par-dessus le splash diff --git a/internal/jean/llm_client.go b/internal/jean/llm_client.go index 9ed4837..b219ccf 100644 --- a/internal/jean/llm_client.go +++ b/internal/jean/llm_client.go @@ -5,6 +5,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -264,6 +265,9 @@ type ToolUsedEvent struct { Result string // tool output (stdout/stderr/exit for run_shell, skill body for read_skill) Done bool // false = call announced (command only); true = result is ready Typing bool // true = command still being written (partial), no spinner yet + // Diff : lignes ajoutées/retirées quand l'outil a MODIFIÉ quelque chose + // (edit, mem_add, mem_edit). L'UI les affiche en vert (+) et rouge (-). + Diff []DiffLine } // previewArg pulls the (possibly incomplete) string value of key out of a @@ -414,6 +418,9 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps // several times, it's stuck — break instead of spinning to the iteration cap. lastSig := "" repeatSig := 0 + // Appels d'outil déjà exécutés (clé = nom + arguments bruts) : sert à ne pas + // rejouer deux fois exactement la même écriture dans un même échange. + doneCalls := map[string]string{} // Garde-fou « pensé sans agir » : certains modèles à reasoning planifient un // appel d'outil dans leur puis émettent le token de fin SANS l'émettre // (ni réponse, ni tool_call). On relance alors UNE fois le tour avec un nudge @@ -747,6 +754,22 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps cb(StreamEvent{ToolUsed: &ToolUsedEvent{Name: tc.Function.Name, Label: label}}) result := "" + // diff : rempli par les outils d'écriture (edit / mémoire) pour que + // l'UI montre les lignes ajoutées et retirées. + var diff []DiffLine + // Appel rigoureusement identique déjà exécuté dans ce tour : on ne le + // rejoue pas. Les petits modèles réémettent volontiers deux fois la + // même écriture ; la rejouer produisait une fausse erreur (« old + // introuvable », puisque le remplacement est déjà fait). + callKey := tc.Function.Name + "\x00" + tc.Function.Arguments + if prev, seen := doneCalls[callKey]; seen { + result = prev + "\n[note] appel identique déjà exécuté dans ce tour — non rejoué" + cb(StreamEvent{ToolUsed: &ToolUsedEvent{Name: tc.Function.Name, Label: label, Result: result, Done: true}}) + toolMsg := Message{Role: "tool", ToolCallID: tc.ID, Content: result} + messages = append(messages, toolMsg) + extra = append(extra, toolMsg) + continue + } switch tc.Function.Name { case "mem_search": lim := 0 @@ -782,19 +805,27 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps result = "[erreur] " + werr.Error() } else { result = fmt.Sprintf("[ok] page '%s' créée", label) + diff = addedDiff(content) } case "mem_edit": oldText, _ := args["old"].(string) newText, _ := args["new"].(string) - if werr := MemEdit(label, oldText, newText); werr != nil { + if werr := MemEdit(label, oldText, newText); errors.Is(werr, errAlreadyApplied) { + result = fmt.Sprintf("[ok] page '%s' %s", label, werr.Error()) + } else if werr != nil { result = "[erreur] " + werr.Error() } else { result = fmt.Sprintf("[ok] page '%s' modifiée", label) + diff = lineDiff(oldText, newText) } case "edit": oldText, _ := args["old"].(string) newText, _ := args["new"].(string) result = fileEdit(label, oldText, newText) + // Diff seulement si l'édition a réussi (sinon le fichier n'a pas bougé). + if !strings.HasPrefix(result, "[erreur]") { + diff = lineDiff(oldText, newText) + } case "bash": to := 0 switch v := args["timeout"].(type) { @@ -819,11 +850,14 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps result = "[erreur] outil inconnu: " + tc.Function.Name } } + if !strings.HasPrefix(result, "[erreur]") { + doneCalls[callKey] = result + } shown := result if r := []rune(shown); len(r) > 4000 { shown = string(r[:4000]) + "\n…[tronqué]" } - cb(StreamEvent{ToolUsed: &ToolUsedEvent{Name: tc.Function.Name, Label: label, Result: shown, Done: true}}) + cb(StreamEvent{ToolUsed: &ToolUsedEvent{Name: tc.Function.Name, Label: label, Result: shown, Done: true, Diff: diff}}) toolMsg := Message{Role: "tool", ToolCallID: tc.ID, Content: result} messages = append(messages, toolMsg) extra = append(extra, toolMsg) diff --git a/internal/jean/relay_link.go b/internal/jean/relay_link.go index 183750e..a62ab6e 100644 --- a/internal/jean/relay_link.go +++ b/internal/jean/relay_link.go @@ -225,7 +225,7 @@ func runLinkForeground() error { return fmt.Errorf("aucun token. Usage: jean link ") } fmt.Printf("%s connexion au relais %s …\n", cyan("[link]"), relayURL()) - fmt.Printf(" (UI Jean + endpoint OpenAI servis dans le tunnel — pas besoin de 'jean web')\n") + fmt.Printf(" (UI AJEAN + endpoint OpenAI servis dans le tunnel — pas besoin de 'jean web')\n") if fp := e2eFingerprint(); fp != "" { fmt.Printf("%s empreinte E2E : %s (code d'appairage : jean link code)\n", green("[e2e]"), bold(fp)) } diff --git a/internal/jean/run.go b/internal/jean/run.go index 979752f..e431cdb 100644 --- a/internal/jean/run.go +++ b/internal/jean/run.go @@ -10,7 +10,7 @@ import ( "strings" ) -const Version = "0.5.9" +const Version = "0.5.10" // Main est le vrai main() du binaire (cmd/jean ne fait que l'appeler). func Main() { diff --git a/internal/jean/sys_console_darwin.go b/internal/jean/sys_console_darwin.go index 2dcc927..40b4409 100644 --- a/internal/jean/sys_console_darwin.go +++ b/internal/jean/sys_console_darwin.go @@ -53,7 +53,7 @@ func appWarning() string { if !strings.Contains(exe, "/AppTranslocation/") { return "" } - return "Jean tourne depuis une copie temporaire en lecture seule (App Translocation de macOS). " + + return "AJEAN tourne depuis une copie temporaire en lecture seule (App Translocation de macOS). " + "Fermez l'app, déplacez Jean.app dans le dossier Applications, puis rouvrez-la — " + "sinon chaque lancement crée une instance séparée et les mises à jour du moteur échouent." } diff --git a/internal/jean/sys_splash_windows.go b/internal/jean/sys_splash_windows.go index a4eee7a..2abd239 100644 --- a/internal/jean/sys_splash_windows.go +++ b/internal/jean/sys_splash_windows.go @@ -152,7 +152,7 @@ func createSplashWindow() uintptr { cy, _, _ := pGetSystemMetrics.Call(smCY) x := (int(cx) - splashW) / 2 y := (int(cy) - splashH) / 2 - title, _ := syscall.UTF16PtrFromString("Jean") + title, _ := syscall.UTF16PtrFromString("AJEAN") hwnd, _, _ := pCreateWindowExW.Call( uintptr(exTopmost|exToolwindow), @@ -240,7 +240,7 @@ func paintSplash(hdc uintptr) { pSetTextColor.Call(hdc, colWhite) textX := ox + logo + 26 - drawText(hdc, "Jean", -30, 700, rect{textX, 40, splashW - 20, 82}) + drawText(hdc, "AJEAN", -30, 700, rect{textX, 40, splashW - 20, 82}) drawText(hdc, "Lancement en cours…", -17, 400, rect{textX, 84, splashW - 20, 118}) } diff --git a/internal/jean/sys_tray_darwin.go b/internal/jean/sys_tray_darwin.go index a77e3df..9f98f87 100644 --- a/internal/jean/sys_tray_darwin.go +++ b/internal/jean/sys_tray_darwin.go @@ -23,10 +23,10 @@ import ( func runTray(url string) { systray.Run(func() { systray.SetIcon(trayIconPNG()) - systray.SetTooltip("Jean — votre IA locale") - mOpen := systray.AddMenuItem("Ouvrir Jean", "Ouvrir l'interface") + systray.SetTooltip("AJEAN — votre IA locale") + mOpen := systray.AddMenuItem("Ouvrir AJEAN", "Ouvrir l'interface") systray.AddSeparator() - mQuit := systray.AddMenuItem("Quitter", "Arrêter Jean") + mQuit := systray.AddMenuItem("Quitter", "Arrêter AJEAN") go func() { for { diff --git a/internal/jean/sys_tray_windows.go b/internal/jean/sys_tray_windows.go index 89b09ca..13b89a3 100644 --- a/internal/jean/sys_tray_windows.go +++ b/internal/jean/sys_tray_windows.go @@ -24,11 +24,11 @@ import ( func runTray(url string) { systray.Run(func() { systray.SetIcon(trayIcon()) - systray.SetTitle("Jean") - systray.SetTooltip("Jean — votre IA locale") - mOpen := systray.AddMenuItem("Ouvrir Jean", "Ouvrir l'interface") + systray.SetTitle("AJEAN") + systray.SetTooltip("AJEAN — votre IA locale") + mOpen := systray.AddMenuItem("Ouvrir AJEAN", "Ouvrir l'interface") systray.AddSeparator() - mQuit := systray.AddMenuItem("Quitter", "Arrêter Jean") + mQuit := systray.AddMenuItem("Quitter", "Arrêter AJEAN") go func() { for { diff --git a/internal/jean/sys_update.go b/internal/jean/sys_update.go index 0b68c31..f45a655 100644 --- a/internal/jean/sys_update.go +++ b/internal/jean/sys_update.go @@ -368,7 +368,7 @@ func restartAfterUpdate() (bool, string) { func restartHintText() string { if runtime.GOOS == "windows" { - return "Redémarre Jean (quitte puis relance) pour appliquer la mise à jour." + return "Redémarre AJEAN (quitte puis relance) pour appliquer la mise à jour." } return "Redémarre les services pour appliquer : sudo systemctl restart jean jean-link" } diff --git a/internal/jean/ui/index.html b/internal/jean/ui/index.html index fd871ff..3005b0d 100644 --- a/internal/jean/ui/index.html +++ b/internal/jean/ui/index.html @@ -7,8 +7,8 @@ - -Jean + +AJEAN @@ -24,7 +24,7 @@ reste (sélecteur, persistance, application) est automatique. */ /* --font : police par défaut (mono). Un thème peut la surcharger (voir "soft"). Les blocs plus spécifiques qui ne redéfinissent pas --font héritent de :root. */ -:root{color-scheme:dark;--font:ui-monospace,SFMono-Regular,Menlo,monospace;--bubble:#1f6feb;--bg:#0d1117;--panel:#161b22;--border:#30363d;--text:#e6edf3;--dim:#7d8590;--accent:#58a6ff;--accent-bg:rgba(88,166,255,.12);--ok:#58a6ff;--warn:#d29922;--err:#f85149;--mag:#bc8cff;--code-bg:#0d1117;--danger-bg:#3d1f23;--warn-bg:#3d2f1f} +:root{color-scheme:dark;--font:ui-monospace,SFMono-Regular,Menlo,monospace;--bubble:#1f6feb;--bg:#0d1117;--panel:#161b22;--border:#30363d;--text:#e6edf3;--dim:#7d8590;--accent:#58a6ff;--accent-bg:rgba(88,166,255,.12);--ok:#58a6ff;--warn:#d29922;--err:#f85149;--mag:#bc8cff;--code-bg:#0d1117;--danger-bg:#3d1f23;--warn-bg:#3d2f1f;--add:#3fb950;--add-bg:rgba(63,185,80,.12);--del:#f85149;--del-bg:rgba(248,81,73,.12)} [data-theme="dark"]{color-scheme:dark;--bg:#0d1117;--panel:#161b22;--border:#30363d;--text:#e6edf3;--dim:#7d8590;--accent:#58a6ff;--accent-bg:rgba(88,166,255,.12);--ok:#58a6ff;--warn:#d29922;--err:#f85149;--mag:#bc8cff;--code-bg:#0d1117;--danger-bg:#3d1f23;--warn-bg:#3d2f1f} [data-theme="light"]{color-scheme:light;--bg:#f0f1f4;--panel:#f8f9fb;--border:#d7dbe1;--text:#24292f;--dim:#636b74;--accent:#0969da;--accent-bg:rgba(9,105,218,.10);--ok:#0969da;--warn:#9a6700;--err:#cf222e;--mag:#8250df;--code-bg:#e6e9ee;--danger-bg:#fbe6e6;--warn-bg:#fbefda} /* "doux" — thème clair chaud + police Lexend (lecture agréable). */ @@ -39,7 +39,10 @@ /* "AJEAN" — clair, bleu, esprit rétro-machine : bordures nettes de 2 px, aucun arrondi, ombres en blocs décalés, fond quadrillé (même charte que ajean.app). Mise en page complète plus bas (section "Thème pixel"). */ -[data-theme="ajean"]{color-scheme:light;--font:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;--bubble:#1f6feb;--bg:#f5f8ff;--panel:#ffffff;--border:#cfdcf5;--text:#0f1f3d;--dim:#5b6f96;--accent:#1f6feb;--accent-bg:rgba(31,111,235,.10);--ok:#1f6feb;--warn:#9a6700;--err:#cf222e;--mag:#6f52d8;--code-bg:#eef3fe;--danger-bg:#fbe6e6;--warn-bg:#fbefda;--grid:#e4ecfa;--line2:#b6cbf0} +[data-theme="ajean"]{color-scheme:light;--font:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;--bubble:#1f6feb;--bg:#f5f8ff;--panel:#ffffff;--border:#cfdcf5;--text:#0f1f3d;--dim:#5b6f96;--accent:#1f6feb;--accent-bg:rgba(31,111,235,.10);--ok:#1f6feb;--warn:#9a6700;--err:#cf222e;--mag:#6f52d8;--code-bg:#eef3fe;--danger-bg:#fbe6e6;--warn-bg:#fbefda;--grid:#e4ecfa;--line2:#b6cbf0;--soft:#e8f0ff;--sh:#cddffb;--knob:#ffffff} +/* « AJEAN sombre » — même habillage pixel, palette nuit : bleu vif sur fond + presque noir, quadrillage discret, ombres bleutées. */ +[data-theme="ajean-dark"]{color-scheme:dark;--font:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;--bubble:#3b82f6;--bg:#050810;--panel:#0a1020;--border:#16233f;--text:#dce6ff;--dim:#63739c;--accent:#3b82f6;--accent-bg:rgba(59,130,246,.16);--ok:#3b82f6;--warn:#d29922;--err:#f85149;--mag:#9b8cff;--code-bg:#03060d;--danger-bg:#3d1f23;--warn-bg:#3d2f1f;--grid:#0a1120;--line2:#1e3054;--soft:#101b33;--sh:#132342;--knob:#dce6ff} *{box-sizing:border-box} body{margin:0;font-family:var(--font);background:var(--bg);color:var(--text);height:100vh;height:100dvh;display:flex;font-size:14px} /* PWA plein écran (viewport-fit=cover) : on décale le contenu des bords pour ne @@ -215,6 +218,16 @@ button:disabled{opacity:.5;cursor:not-allowed} .msg.reasoning{align-self:flex-start;background:transparent;border:1px dashed var(--border);color:var(--mag);font-size:12px;max-width:90%;overflow-wrap:anywhere;word-break:break-word} .msg.tool{align-self:flex-start;background:transparent;border:1px dashed var(--border);max-width:90%;white-space:normal;padding:8px 12px;font-size:12px;overflow-wrap:anywhere;word-break:break-word} .msg.tool .tool-head{font-size:12px;color:var(--accent);font-weight:600;margin-bottom:6px} +/* ===== Diff des écritures (outil edit / mémoire) ================== + Lignes ajoutées en vert, retirées en rouge, contexte neutre. Les couleurs + viennent de variables : chaque thème peut les ajuster (clair vs sombre). */ +.msg.tool pre.diff{display:block;padding:0;overflow-x:auto;background:var(--code-bg)} +.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)} +.diff-count{margin-left:8px;letter-spacing:0;text-transform:none} +.diff-count .a{color:var(--add)} +.diff-count .d{color:var(--del)} .msg.tool .tool-sub{font-size:10px;color:var(--dim);text-transform:uppercase;letter-spacing:.1em;margin:8px 0 4px} .msg.tool pre{position:relative;background:var(--code-bg);border:1px solid var(--border);border-radius:6px;padding:8px 10px;overflow:auto;margin:0;max-height:280px} .msg.tool pre code{background:transparent;padding:0;font-size:.82em;line-height:1.4;white-space:pre} @@ -265,7 +278,10 @@ button:disabled{opacity:.5;cursor:not-allowed} #ctx-text{font-size:10px;letter-spacing:.03em;white-space:nowrap} #ctx-compact{display:none;margin:0;padding:2px 10px;font-size:10px;background:var(--warn-bg);border-color:var(--warn,#c93)} #inputbar{padding:8px 12px;display:flex;gap:8px} -#input{flex:1;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:10px;font:inherit;resize:none;min-height:44px;max-height:200px} +/* Astuce clavier sous la saisie (style repris du HTML : un thème doit pouvoir + changer ses marges, ce qu'un style en ligne empêchait). */ +#sendhint{padding:0 12px 6px;font-size:10px;text-align:center;opacity:.7} +#input{flex:1;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:10px;font:inherit;resize:none;min-height:56px;max-height:200px;overflow-y:hidden} #input:focus{outline:none;border-color:var(--accent)} #send{padding:0 20px} .muted{color:var(--dim);font-size:12px} @@ -363,10 +379,14 @@ button:disabled{opacity:.5;cursor:not-allowed} .pe-row.stack.plain .pe-inp:focus{box-shadow:none} /* Champ + bouton accolé (téléchargement) — même trait que les autres champs, sans pastille colorée qui ressort. */ -.pe-inpbtn{display:flex} -.pe-inpbtn .pe-inp{border-top-right-radius:0;border-bottom-right-radius:0} -.pe-inpbtn button{flex:none;margin:0;border:1px solid var(--border);border-left:0;border-radius:0 8px 8px 0;background:var(--panel);color:var(--dim);padding:0 15px;cursor:pointer;font-size:15px;transition:color .12s,border-color .12s} -.pe-inpbtn button:hover{color:var(--accent);border-color:var(--accent)} +.pe-inpbtn{display:flex;align-items:stretch} +.pe-inpbtn .pe-inp{border-top-right-radius:0;border-bottom-right-radius:0;border-right:0} +/* Le bouton reprend EXACTEMENT le cadre du champ (mêmes coins, même bordure) : + sans ça, un thème qui arrondit tous les boutons (ChatGPT) le détachait du + champ, avec un coin rond au milieu de la paire. */ +.pe-inpbtn button{flex:none;margin:0;border:1px solid var(--border);border-radius:0 8px 8px 0;background:var(--panel);color:var(--dim);padding:0 15px;cursor:pointer;font-size:15px;transition:color .12s,border-color .12s,background .12s} +.pe-inpbtn button:hover:not(:disabled){color:#fff;background:var(--accent);border-color:var(--accent)} +.pe-inpbtn:focus-within .pe-inp,.pe-inpbtn:focus-within button{border-color:var(--accent)} .pe-inpbtn button:disabled{opacity:.5;cursor:default} /* Contrôle segmenté (moteur) façon iOS. */ .pe-seg{display:flex;gap:3px;background:color-mix(in srgb,var(--dim) 16%,transparent);border-radius:9px;padding:3px;width:100%} @@ -539,7 +559,7 @@ button:disabled{opacity:.5;cursor:not-allowed} [data-theme="gpt"] #scrollbtn{bottom:180px;border-radius:999px} [data-theme="gpt"] #compact-banner{order:0;margin:0 var(--col) 8px;border-radius:14px} [data-theme="gpt"] #inputbar{order:1;margin:0 var(--col);padding:8px 8px 8px 16px;gap:8px;align-items:flex-end;background:var(--panel);border:1px solid var(--border);border-bottom:none;border-radius:26px 26px 0 0} -[data-theme="gpt"] #input{background:transparent;border:none;border-radius:0;padding:9px 0;min-height:26px} +[data-theme="gpt"] #input{background:transparent;border:none;border-radius:0;padding:9px 0;min-height:36px;line-height:18px;max-height:160px} /* Pas de cadre qui s'assombrit au focus : la carte garde exactement la même bordure, seule la saisie change (curseur). */ [data-theme="gpt"] #input:focus{border-color:transparent} @@ -559,9 +579,14 @@ button:disabled{opacity:.5;cursor:not-allowed} l'arrondi du pied de carte. */ [data-theme="gpt"] #ctxtrack{top:auto;bottom:0;height:7px;background:var(--border)} [data-theme="gpt"] #ctx-fill{background:var(--text);opacity:.75} -[data-theme="gpt"] #sendhint{order:4;padding:8px var(--col) 0} +[data-theme="gpt"] #sendhint{order:4;padding:12px var(--col) 2px} /* Reste de l'UI : coins arrondis et contrastes doux, dans le même esprit. */ [data-theme="gpt"] button,[data-theme="gpt"] .addbtn,[data-theme="gpt"] .mem-search,[data-theme="gpt"] #theme-select{border-radius:10px} +/* Champ + bouton accolés : on ne garde l'arrondi que sur les coins extérieurs. */ +[data-theme="gpt"] .pe-inpbtn .pe-inp{border-radius:10px 0 0 10px} +[data-theme="gpt"] .pe-inpbtn button{border-radius:0 10px 10px 0} +[data-theme="gpt"] .input-btn .ctl{border-radius:10px 0 0 10px} +[data-theme="gpt"] .input-btn .btn-in{border-radius:0 10px 10px 0} [data-theme="gpt"] .preset,[data-theme="gpt"] .gated-block,[data-theme="gpt"] .lc-mode{border-radius:12px} [data-theme="gpt"] .preset{background:transparent;border-color:transparent} [data-theme="gpt"] .preset:hover{background:var(--accent-bg);border-color:transparent} @@ -576,52 +601,122 @@ button:disabled{opacity:.5;cursor:not-allowed} Rétro-machine : plus aucun arrondi, bordures de 2 px, ombres portées en blocs pleins (pas de flou), fond quadrillé comme du papier millimétré, boutons en capitales qui « s'enfoncent » au survol. Tout est préfixé - [data-theme="ajean"] : aucun autre thème n'est touché. */ + [data-theme^="ajean"] : aucun autre thème n'est touché. */ /* Un seul balayage pour supprimer TOUS les arrondis (bulles, boutons, pastilles, interrupteurs, points animés…) — c'est ce qui fait l'identité du thème. */ -[data-theme="ajean"] *,[data-theme="ajean"] *::before,[data-theme="ajean"] *::after{border-radius:0 !important} +[data-theme^="ajean"] *,[data-theme^="ajean"] *::before,[data-theme^="ajean"] *::after{border-radius:0 !important} /* Fond quadrillé : le papier millimétré derrière toute l'interface. */ -[data-theme="ajean"] body{background-image:linear-gradient(var(--grid) 1px,transparent 1px),linear-gradient(90deg,var(--grid) 1px,transparent 1px);background-size:32px 32px} -[data-theme="ajean"] .side{background:var(--panel);border-right:2px solid var(--border)} -[data-theme="ajean"] #composer,[data-theme="ajean"] #chat{background:transparent} +[data-theme^="ajean"] body{background-image:linear-gradient(var(--grid) 1px,transparent 1px),linear-gradient(90deg,var(--grid) 1px,transparent 1px);background-size:32px 32px} +[data-theme^="ajean"] .side{background:var(--panel);border-right:2px solid var(--border)} +[data-theme^="ajean"] #composer,[data-theme^="ajean"] #chat{background:transparent} /* Boutons : capitales, bordure épaisse, ombre en bloc qui disparaît au survol pendant que le bouton se décale — impression de touche enfoncée. */ -[data-theme="ajean"] button,[data-theme="ajean"] .addbtn,[data-theme="ajean"] .preset-add{border:2px solid var(--line2);background:var(--panel);color:var(--text);text-transform:uppercase;letter-spacing:.06em;font-weight:700;box-shadow:3px 3px 0 0 var(--grid);transition:.1s} -[data-theme="ajean"] button:hover:not(:disabled),[data-theme="ajean"] .addbtn:hover,[data-theme="ajean"] .preset-add:hover{background:var(--accent);border-color:var(--accent);color:#fff;box-shadow:none;transform:translate(3px,3px)} -[data-theme="ajean"] .preset-edit,[data-theme="ajean"] .copybtn{border:none;box-shadow:none;text-transform:none} -[data-theme="ajean"] .copybtn{border:2px solid var(--border)} -[data-theme="ajean"] #send{background:var(--accent);border-color:var(--accent);color:#fff} -[data-theme="ajean"] #menubtn{border:2px solid var(--line2);box-shadow:3px 3px 0 0 var(--grid)} +[data-theme^="ajean"] button,[data-theme^="ajean"] .addbtn,[data-theme^="ajean"] .preset-add{border:2px solid var(--line2);background:var(--panel);color:var(--text);text-transform:uppercase;letter-spacing:.06em;font-weight:700;box-shadow:3px 3px 0 0 var(--grid);transition:.1s} +[data-theme^="ajean"] button:hover:not(:disabled),[data-theme^="ajean"] .addbtn:hover,[data-theme^="ajean"] .preset-add:hover{background:var(--accent);border-color:var(--accent);color:#fff;box-shadow:none;transform:translate(3px,3px)} +[data-theme^="ajean"] .preset-edit,[data-theme^="ajean"] .copybtn{border:none;box-shadow:none;text-transform:none} +[data-theme^="ajean"] .copybtn{border:2px solid var(--border)} +[data-theme^="ajean"] #send{background:var(--accent);border-color:var(--accent);color:#fff} +[data-theme^="ajean"] #menubtn{border:2px solid var(--line2);box-shadow:3px 3px 0 0 var(--grid)} /* Messages : la question en carré bleu plein, la réponse en fiche blanche posée sur son ombre. Raisonnement et outils restent en pointillés. */ -[data-theme="ajean"] #chat .msg.user{background:var(--accent);color:#fff;box-shadow:4px 4px 0 0 #cddffb} -[data-theme="ajean"] #chat .msg.assistant{background:var(--panel);border:2px solid var(--border);box-shadow:4px 4px 0 0 var(--grid)} -[data-theme="ajean"] #chat .msg.reasoning,[data-theme="ajean"] #chat .msg.tool{border:2px dashed var(--line2);background:var(--panel)} -[data-theme="ajean"] #chat .msg .label{color:var(--dim);letter-spacing:.14em} -[data-theme="ajean"] #chat .msg.user .label{color:rgba(255,255,255,.85)} -[data-theme="ajean"] .msg.assistant .body pre,[data-theme="ajean"] .msg.reasoning .body pre,[data-theme="ajean"] .msg.tool pre{border:2px solid var(--border);background:var(--code-bg)} -[data-theme="ajean"] .msg.typing span{background:var(--accent)} +[data-theme^="ajean"] #chat .msg.user{background:var(--accent);color:#fff;box-shadow:4px 4px 0 0 var(--sh)} +[data-theme^="ajean"] #chat .msg.assistant{background:var(--panel);border:2px solid var(--border);box-shadow:4px 4px 0 0 var(--grid)} +[data-theme^="ajean"] #chat .msg.reasoning,[data-theme^="ajean"] #chat .msg.tool{border:2px dashed var(--line2);background:var(--panel)} +[data-theme^="ajean"] #chat .msg .label{color:var(--dim);letter-spacing:.14em} +[data-theme^="ajean"] #chat .msg.user .label{color:rgba(255,255,255,.85)} +[data-theme^="ajean"] .msg.assistant .body pre,[data-theme^="ajean"] .msg.reasoning .body pre,[data-theme^="ajean"] .msg.tool pre{border:2px solid var(--border);background:var(--code-bg)} +[data-theme^="ajean"] .msg.typing span{background:var(--accent)} /* Panneaux, encadrés et champs : mêmes bordures épaisses, mêmes ombres. */ -[data-theme="ajean"] .preset,[data-theme="ajean"] .gated-block,[data-theme="ajean"] .tag,[data-theme="ajean"] .qtag,[data-theme="ajean"] .rtag,[data-theme="ajean"] .btag,[data-theme="ajean"] .statuspill,[data-theme="ajean"] .lc-mode{border-width:2px} -[data-theme="ajean"] .preset.active{border-color:var(--accent);color:var(--accent);box-shadow:3px 3px 0 0 #cddffb} -[data-theme="ajean"] #input,[data-theme="ajean"] .mem-search,[data-theme="ajean"] .ask-input,[data-theme="ajean"] #modal input,[data-theme="ajean"] #modal textarea,[data-theme="ajean"] #modal select,[data-theme="ajean"] #theme-select{border:2px solid var(--line2);background:var(--panel)} -[data-theme="ajean"] #input:focus,[data-theme="ajean"] .mem-search:focus,[data-theme="ajean"] .ask-input:focus{border-color:var(--accent)} -[data-theme="ajean"] #composer{border-top:2px solid var(--border);background:var(--panel)} +[data-theme^="ajean"] .preset,[data-theme^="ajean"] .gated-block,[data-theme^="ajean"] .tag,[data-theme^="ajean"] .qtag,[data-theme^="ajean"] .rtag,[data-theme^="ajean"] .btag,[data-theme^="ajean"] .statuspill,[data-theme^="ajean"] .lc-mode{border-width:2px} +[data-theme^="ajean"] .preset.active{border-color:var(--accent);color:var(--accent);box-shadow:3px 3px 0 0 var(--sh)} +[data-theme^="ajean"] #input,[data-theme^="ajean"] .mem-search,[data-theme^="ajean"] .ask-input,[data-theme^="ajean"] #theme-select,[data-theme^="ajean"] .modal-card .ctl{border:2px solid var(--line2);background:var(--panel)} +[data-theme^="ajean"] #input:focus,[data-theme^="ajean"] .mem-search:focus,[data-theme^="ajean"] .ask-input:focus{border-color:var(--accent)} +[data-theme^="ajean"] #composer{border-top:2px solid var(--border);background:var(--panel)} /* Jauge de contexte : un vrai bandeau plein, pas un filet dégradé. */ -[data-theme="ajean"] #ctxtrack{height:5px;background:var(--grid)} -[data-theme="ajean"] #ctx-fill{background:var(--accent)} +[data-theme^="ajean"] #ctxtrack{height:5px;background:var(--grid)} +[data-theme^="ajean"] #ctx-fill{background:var(--accent)} /* Fenêtres modales / info-bulles / toast : cadre net + ombre en bloc bleutée. */ -[data-theme="ajean"] .ask-box,[data-theme="ajean"] .help>.tip,[data-theme="ajean"] #toast,[data-theme="ajean"] .modal-box,[data-theme="ajean"] #modal>div{border:2px solid var(--line2);box-shadow:7px 7px 0 0 #cddffb} -[data-theme="ajean"] #scrollbtn{border:2px solid var(--line2);box-shadow:3px 3px 0 0 var(--grid)} +[data-theme^="ajean"] .ask-box,[data-theme^="ajean"] .help>.tip,[data-theme^="ajean"] #toast,[data-theme^="ajean"] .modal-box,[data-theme^="ajean"] #modal>div{border:2px solid var(--line2);box-shadow:7px 7px 0 0 var(--sh)} +[data-theme^="ajean"] #scrollbtn{border:2px solid var(--line2);box-shadow:3px 3px 0 0 var(--grid)} /* Interrupteurs : carrés, bleus quand ils sont actifs. */ -[data-theme="ajean"] .switch .slider{background:var(--line2)} -[data-theme="ajean"] .switch input:checked + .slider{background:var(--accent)} +[data-theme^="ajean"] .switch .slider{background:var(--line2)} +[data-theme^="ajean"] .switch input:checked + .slider{background:var(--accent)} /* Pastille carrée : box-sizing explicite (un ::before n'hérite pas de la règle globale *{box-sizing:border-box}) — sans ça la bordure débordait du rail. */ -[data-theme="ajean"] .switch .slider::before{box-sizing:border-box;width:14px;height:14px;background:#fff;border:2px solid var(--line2);box-shadow:none} -[data-theme="ajean"] .switch input:checked + .slider::before{border-color:#fff;background:var(--panel)} +[data-theme^="ajean"] .switch .slider::before{box-sizing:border-box;width:14px;height:14px;background:var(--knob);border:2px solid var(--line2);box-shadow:none} +[data-theme^="ajean"] .switch input:checked + .slider::before{border-color:var(--knob);background:var(--knob)} /* Étiquettes de section : capitales espacées, comme des en-têtes de terminal. */ -[data-theme="ajean"] .subhead,[data-theme="ajean"] .skillshead>span{letter-spacing:.16em} +[data-theme^="ajean"] .subhead,[data-theme^="ajean"] .skillshead>span{letter-spacing:.16em} + +/* ----- Thème AJEAN : modales (éditeur de preset, mémoire, MCP) ------------- + Les règles génériques du thème (bouton en capitales + ombre en bloc + décalage + au survol) sont faites pour la barre latérale ; dans un formulaire dense elles + rendaient l'éditeur de preset illisible : boutons énormes, ombres qui se + chevauchent, champs collés au fond de la carte. On rejoue donc la modale à + plat — hiérarchie nette, un seul niveau de relief (la carte). */ +[data-theme^="ajean"] .modal-ov{background:rgba(15,31,61,.45)} +[data-theme^="ajean"] .modal-card{background:var(--panel);border:2px solid var(--line2);box-shadow:8px 8px 0 0 var(--sh)} +[data-theme^="ajean"] .modal-head{border-bottom:2px solid var(--border);background:var(--soft,#e8f0ff)} +[data-theme^="ajean"] .modal-head strong{text-transform:uppercase;letter-spacing:.14em;font-size:12px;color:var(--accent)} +[data-theme^="ajean"] .modal-foot{border-top:2px solid var(--border)} +/* Champs simples (mémoire, MCP…) : fond teinté pour se détacher de la carte. */ +[data-theme^="ajean"] .ctl{border:2px solid var(--line2);background:var(--bg);background-image:none} +[data-theme^="ajean"] .ctl:focus{border-color:var(--accent)} +[data-theme^="ajean"] .fld-l{color:var(--text);letter-spacing:.1em} +[data-theme^="ajean"] .fld-hint,[data-theme^="ajean"] .seg .be-note{color:var(--dim)} +[data-theme^="ajean"] .modal-body .card{border:2px solid var(--border);box-shadow:none;background:var(--panel)} +[data-theme^="ajean"] .card-h{letter-spacing:.14em} + +/* --- Éditeur de preset (listes façon Réglages) --- */ +[data-theme^="ajean"] .pe-body{background:var(--bg)} +/* En-tête de groupe : petite étiquette bleue en capitales. */ +[data-theme^="ajean"] .pe-gh{color:var(--accent);text-transform:uppercase;letter-spacing:.14em;font-size:11px;font-weight:700} +/* La LISTE porte le cadre ; les champs à l'intérieur n'en ont pas (sinon double + encadrement). Séparateurs de rangées à la même épaisseur. */ +[data-theme^="ajean"] .pe-list,[data-theme^="ajean"] .pe-adv{border:2px solid var(--border);background:var(--panel)} +[data-theme^="ajean"] .pe-row:not(:last-child){border-bottom:2px solid var(--border)} +[data-theme^="ajean"] .pe-adv[open]>summary{border-bottom:2px solid var(--border)} +[data-theme^="ajean"] .pe-val,[data-theme^="ajean"] .pe-selc select{border:none;background:transparent} +/* Champs pleine largeur (URL, chemin…) : eux gardent un cadre, sur fond teinté. */ +[data-theme^="ajean"] .pe-inp{border:2px solid var(--line2);background:var(--bg)} +[data-theme^="ajean"] .pe-inp:focus{border-color:var(--accent);box-shadow:none} +[data-theme^="ajean"] .pe-row.stack.plain .pe-inp{border:none;background:transparent} +[data-theme^="ajean"] .pe-inpbtn button{border:2px solid var(--line2);box-shadow:none;background:var(--panel);text-transform:none;letter-spacing:0} +[data-theme^="ajean"] .pe-inpbtn .pe-inp{border-right:0} +[data-theme^="ajean"] .pe-inpbtn button:hover:not(:disabled){transform:none;background:var(--accent);border-color:var(--accent);color:#fff} +/* Sélecteur segmenté : cases carrées côte à côte, bleu plein pour l'active. */ +[data-theme^="ajean"] .pe-seg{background:transparent;padding:0;gap:8px} +[data-theme^="ajean"] .pe-seg>label{border:2px solid var(--border);background:var(--panel);font-weight:700;text-transform:uppercase;letter-spacing:.06em;font-size:11px;padding:9px 4px} +[data-theme^="ajean"] .pe-seg>label:has(input:checked){background:var(--accent);border-color:var(--accent);color:#fff;box-shadow:none} +/* Interrupteurs de la modale : carrés, comme ceux de la barre latérale. */ +[data-theme^="ajean"] .pe-sl{background:var(--line2)} +[data-theme^="ajean"] .pe-sl::before{box-sizing:border-box;background:var(--knob);border:2px solid var(--line2);box-shadow:none} +[data-theme^="ajean"] .pe-switch input:checked+.pe-sl{background:var(--accent)} +[data-theme^="ajean"] .pe-switch input:checked+.pe-sl::before{border-color:var(--knob);background:var(--knob)} +/* Boutons DANS la modale : mêmes capitales, mais posés à plat — pas d'ombre ni + de décalage au survol, sinon les rangées de boutons dansent et se recouvrent. */ +[data-theme^="ajean"] .modal-card button{box-shadow:none;font-size:11.5px} +[data-theme^="ajean"] .modal-card button:hover:not(:disabled){transform:none} +[data-theme^="ajean"] .modal-card .btn-save{background:var(--accent);border-color:var(--accent);color:#fff} +[data-theme^="ajean"] .modal-card .btn-danger{background:var(--danger-bg);border-color:var(--err);color:var(--err)} +[data-theme^="ajean"] .modal-card .btn-danger:hover{background:var(--err);border-color:var(--err);color:#fff} +[data-theme^="ajean"] .modal-x{border:2px solid transparent;background:transparent;color:var(--dim)} +[data-theme^="ajean"] .modal-x:hover{background:transparent;border-color:var(--line2);color:var(--text)} +/* Champ + bouton accolés : les bordures se chevauchent au lieu de doubler. */ +[data-theme^="ajean"] .input-btn .btn-in{margin-left:-2px} +/* Sélecteur segmenté : bleu plein quand c'est coché (le liseré seul se voyait mal). */ +[data-theme^="ajean"] .seg>label{border:2px solid var(--border);background:var(--bg)} +[data-theme^="ajean"] .seg>label:has(input:checked){background:var(--accent);border-color:var(--accent);color:#fff} +[data-theme^="ajean"] .seg>label:has(input:checked) .be-note{color:rgba(255,255,255,.85)} +/* Listes (MCP, outils) : mêmes bordures épaisses, survol teinté. */ +[data-theme^="ajean"] .mcp-row,[data-theme^="ajean"] .mcp-tools-list,[data-theme^="ajean"] #lc-log{border:2px solid var(--border)} +[data-theme^="ajean"] .mcp-tool-row:hover{background:var(--soft,#e8f0ff)} + +/* Diff sur fond clair : mêmes rôles, teintes plus profondes pour rester lisibles. */ +[data-theme="light"],[data-theme="soft"],[data-theme="gpt"],[data-theme="ajean"]{ + --add:#1a7f37;--add-bg:rgba(26,127,55,.10);--del:#cf222e;--del-bg:rgba(207,34,46,.10)} +[data-theme="soft-dark"],[data-theme="ajean-dark"]{ + --add:#4ac26b;--add-bg:rgba(74,194,107,.13);--del:#f85149;--del-bg:rgba(248,81,73,.13)}
@@ -655,7 +750,7 @@ button:disabled{opacity:.5;cursor:not-allowed}
Mode agent - +
Mémoire?
@@ -686,7 +781,7 @@ button:disabled{opacity:.5;cursor:not-allowed}
-
Serveurs MCP?Connecte des serveurs MCP (Model Context Protocol) pour donner de nouveaux outils à l'IA : accès fichiers, bases de données, APIs métier… stdio = programme lancé localement (npx/uvx/binaire) · http = serveur distant. ⚠️ un serveur stdio exécute du code sur cette machine, au même titre que le shell.
+
Serveurs MCP?Connecte des serveurs MCP (Model Context Protocol) pour donner de nouveaux outils à l'IA : accès fichiers, bases de données, APIs métier… stdio = programme lancé localement (npx/uvx/binaire) · http = serveur distant. ⚠ un serveur stdio exécute du code sur cette machine, au même titre que le shell.
@@ -770,17 +865,17 @@ button:disabled{opacity:.5;cursor:not-allowed}
…
-
⚡ Version rapide conseillé
+
Version rapide conseillé
prête à l'emploi, installée en ~2 min
-
🔧 Version optimisée
+
Version optimisée
compilée pour votre machine, plus longue
-
🧩 Backend personnalisé
+
Backend personnalisé
forks llama.cpp pour modèles spéciaux (ternaire…)
@@ -804,7 +899,7 @@ button:disabled{opacity:.5;cursor:not-allowed}
- +
@@ -822,18 +917,18 @@ button:disabled{opacity:.5;cursor:not-allowed}
- +
-
Entrée pour envoyer · Maj+Entrée = nouvelle ligne
+
Entrée pour envoyer · Maj+Entrée = nouvelle ligne