diff --git a/README.md b/README.md index 8c8edfa..91b4952 100644 --- a/README.md +++ b/README.md @@ -320,8 +320,18 @@ Ajoutées par ce fork : `AGENT_BUDGET=off` le désactive complètement. - **Identité** : ton prénom et un avatar emoji pour toi et pour Loki, affichés dans le fil. -- **Paramètres** : les réglages d'application (identité, apparence, accès - OpenAI, actions) sont regroupés à part des réglages d'IA. +- **Réglages en modale** : tous les réglages vivent dans une fenêtre à deux + volets — la nav des sections à gauche (IA, moteur, application), le panneau + choisi à droite. La barre latérale ne garde que les discussions (les plus + récentes en tête) et le moniteur machine. +- **Nom du modèle sur chaque réponse** : une pastille à côté de « Loki » dit + quel modèle a produit la réponse. Elle est journalisée avec le tour : elle + survit au rechargement, et un vieux tour garde le modèle de l'époque. +- **Cartes raisonnement/outils à hauteur bornée** : un long raisonnement ne + fait plus grandir la page de plusieurs écrans — la carte reste à taille fixe + et défile toute seule pendant la génération. Sur un raisonnement géant, seul + le bas du bloc est re-rendu en direct (le texte complet est posé à la fin) : + l'affichage ne se fige plus. Retirées par ce fork : diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 0faa042..9aa152c 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,19 +1,46 @@ -La grande nouveauté de cette version : AJEAN sait enfin voir les images. +# Loki 0.11.0 -## La vision, configurable par preset +Première version numérotée du fork depuis la synchronisation avec l'amont +AJEAN (v0.10.7). L'essentiel de cette version : une interface qui respire. -Jusqu'ici rien dans l'interface ne permettait de donner des yeux à un modèle. Il fallait glisser l'option `--mmproj` à la main dans la configuration brute, et son chemin n'était même pas résolu comme celui du modèle. L'éditeur de preset gagne un champ **Vision** : tu y choisis le projecteur multimodal (fichier `mmproj`) qui accompagne le modèle, et c'est tout. Au démarrage du moteur, AJEAN le charge tout seul. +## Réglages en modale -Le champ ne liste que les projecteurs (les fichiers dont le nom contient `mmproj`), pas les modèles de plusieurs Go, pour que le choix reste lisible. Et s'il te manque un projecteur, le champ « télécharger un modèle » accepte aussi un lien vers un `mmproj` : une fois récupéré, il se place directement dans le champ Vision. +Tous les réglages quittent la barre latérale pour une fenêtre à deux volets : +la nav des sections à gauche (IA, moteur, application), le panneau choisi à +droite — chaque section a enfin toute la largeur. La barre latérale ne garde +que les discussions et le moniteur machine. Les chemins historiques (pastille +d'état du moteur, pastille d'installation llama.cpp) rouvrent la modale au bon +endroit. -## Les images arrivent vraiment au modèle +## Discussions : la plus récente en tête -Avant, une image collée dans le chat était simplement déposée comme un fichier dans le dossier de travail, à charge pour le modèle de l'ouvrir avec ses outils (ce qui ne donnait qu'un tas d'octets illisibles). Désormais, quand un projecteur est configuré, l'image part au modèle en contenu multimodal : il la voit, et peut en parler. +La liste se lit en ordre d'activité, la discussion du moment en haut — y +compris une discussion toute neuve, qui apparaissait sous celle qu'on venait de +quitter. + +## Nom du modèle sur chaque réponse + +Une pastille à côté de « Loki » dit quel modèle a produit la réponse. Le nom +est journalisé avec le tour : il survit au rechargement de page, et un vieux +tour garde le modèle de l'époque, pas celui chargé aujourd'hui. + +## Cartes de raisonnement à hauteur bornée + +Un long raisonnement (plusieurs milliers de tokens) faisait grandir la page de +plusieurs écrans et finissait par figer l'affichage (re-parse Markdown du bloc +entier à chaque token, en O(n²)). La carte reste maintenant à taille fixe et +suit la génération en défilant toute seule ; en direct, seul le bas d'un bloc +géant est re-rendu, et le texte complet est posé en fin de bloc. + +## Divers + +- Jeton Hugging Face réglable dans l'interface (dépôts verrouillés : le + message dit lesquels, et pourquoi le 401). +- Tâches planifiées reprises de l'amont : l'IA travaille toute seule sur une + fréquence réglable, isolée des discussions. ## Mise à jour ``` -ajean update +docker compose pull && docker compose up -d ``` - -Non vérifié sur cette version : le résultat final dépend d'un modèle vision et de son projecteur compatibles (Qwen2.5-VL, Gemma 3, etc.). À noter, l'image reste dans l'historique de la conversation et repart au moteur à chaque tour tant que la conversation dure. diff --git a/cmd/loki/resource_windows_amd64.syso b/cmd/loki/resource_windows_amd64.syso index f07048d..d125b84 100644 Binary files a/cmd/loki/resource_windows_amd64.syso and b/cmd/loki/resource_windows_amd64.syso differ diff --git a/cmd/loki/resource_windows_arm64.syso b/cmd/loki/resource_windows_arm64.syso index c92084b..7364aa2 100644 Binary files a/cmd/loki/resource_windows_arm64.syso and b/cmd/loki/resource_windows_arm64.syso differ diff --git a/cmd/loki/versioninfo.json b/cmd/loki/versioninfo.json index 9129eed..3152dd1 100644 --- a/cmd/loki/versioninfo.json +++ b/cmd/loki/versioninfo.json @@ -2,14 +2,14 @@ "FixedFileInfo": { "FileVersion": { "Major": 0, - "Minor": 9, - "Patch": 4, + "Minor": 11, + "Patch": 0, "Build": 0 }, "ProductVersion": { "Major": 0, - "Minor": 9, - "Patch": 4, + "Minor": 11, + "Patch": 0, "Build": 0 }, "FileFlagsMask": "3f", @@ -25,7 +25,7 @@ "LegalCopyright": "Copyright (c) 2026 Loki contributors. MIT License.", "OriginalFilename": "loki.exe", "ProductName": "Loki", - "ProductVersion": "0.9.4", + "ProductVersion": "0.11.0", "Comments": "https://github.com/R0m1k3/Loki — projet open source (MIT)" }, "VarFileInfo": { diff --git a/internal/loki/run.go b/internal/loki/run.go index 48141ce..56d8798 100644 --- a/internal/loki/run.go +++ b/internal/loki/run.go @@ -10,7 +10,7 @@ import ( "strings" ) -const Version = "0.9.4" +const Version = "0.11.0" // Main est le vrai main() du binaire (cmd/loki ne fait que l'appeler). func Main() { diff --git a/internal/loki/ui/index.html b/internal/loki/ui/index.html index e9a4aee..eb577bb 100644 --- a/internal/loki/ui/index.html +++ b/internal/loki/ui/index.html @@ -687,6 +687,12 @@ textarea.sctl{min-height:76px;resize:vertical;line-height:1.5} /* height ET width sont animées (en px, pilotées en JS via scrollHeight/scrollWidth) → repli/dépli fluide en diagonale. overflow:hidden clippe pendant l'animation. */ .msg.collapsible .bodywrap{overflow:hidden;transition:height .42s cubic-bezier(.4,0,.2,1),width .42s cubic-bezier(.4,0,.2,1),opacity .3s ease,margin-top .42s cubic-bezier(.4,0,.2,1);opacity:1;margin-top:4px} +/* Raisonnement / outils : hauteur BORNÉE avec défilement interne. Un long + raisonnement (des milliers de tokens) faisait grandir la page de plusieurs + écrans ; la carte reste maintenant à taille fixe et suit la génération en + défilant toute seule (voir flushRender, 09-stream.js). L'animation de repli + garde son overflow:hidden via la hauteur inline — les deux cohabitent. */ +.msg.reasoning .bodywrap,.msg.tool .bodywrap{max-height:280px;overflow-y:auto;overscroll-behavior:contain;scrollbar-width:thin} .msg.collapsible .bodywrap>.body{min-width:0} .msg.collapsible.collapsed .bodywrap{opacity:0;margin-top:0} /* Composer = un seul bloc cohérent : barre de contexte intégrée en tête (fine @@ -6379,11 +6385,15 @@ function syncSendBtn(){ // garanti par flushRender(), appelé à chaque frontière de bloc (outil, fin de // tour, erreur, caught_up). let renderTimer=null, renderPending=null, lastRenderMs=0; // {el, text} -function scheduleRender(el, text){ +function scheduleRender(el, text, final){ // Changement de bloc en cours de route : on rend d'abord l'ancien à sa dernière // valeur, sinon son ultime bout de texte serait perdu. if(renderPending && renderPending.el!==el) flushRender(); - renderPending={el, text}; + // `final` voyage AVEC le rendu en attente : le timer déjà armé (qui passe + // final=false) peut être celui qui consommera ce rendu — sans le drapeau sur + // le pending, le rendu de FIN de bloc repassait par le chemin « queue seule » + // et le début du raisonnement n'était jamais posé. + renderPending={el, text, final:!!final}; if(renderTimer) return; // Cadence ADAPTATIVE : on vise à ne pas passer plus d'~1/6 du temps à re-parser // le Markdown. Tant que le bloc est petit, un rendu coûte 1-2 ms → plancher @@ -6391,15 +6401,33 @@ function scheduleRender(el, text){ // devient énorme, le rendu coûte cher et on espace tout seul jusqu'à 500 ms : // le O(n²) est cassé sans jamais figer l'interface. const delay=Math.min(500, Math.max(16, lastRenderMs*6)); - renderTimer=setTimeout(flushRender, delay); + renderTimer=setTimeout(()=>flushRender(false), delay); } -function flushRender(){ +// Au-delà de cette taille, un raisonnement EN COURS n'est re-parsé que sur sa +// fin : re-parser le bloc entier à chaque tick est en O(n²) et finissait par +// figer l'affichage (le texte semblait s'arrêter alors que le moteur débitait). +// La carte étant bornée en hauteur et collée en bas, on ne perd rien à l'écran ; +// le texte COMPLET est posé au rendu final (frontière de bloc → final=true). +const REASON_TAIL=6000; +function flushRender(final){ if(renderTimer){ clearTimeout(renderTimer); renderTimer=null; } const p=renderPending; renderPending=null; if(!p) return; + let text=p.text; + if(final===false && !p.final && text.length>REASON_TAIL+800 && p.el.closest('.msg.reasoning')){ + let cut=text.length-REASON_TAIL; + const nl=text.indexOf('\n', cut); if(nl>-1 && nl=bw.scrollHeight-60; const t0=performance.now(); - renderBody(p.el, p.text); + renderBody(p.el, text); lastRenderMs=performance.now()-t0; + if(scrollable && stick) bw.scrollTop=bw.scrollHeight; } // Annule le rendu en attente SANS le poser : le bloc visé disparaît (fil vidé, // raisonnement jeté), le rendre ensuite écrirait dans un élément détaché. @@ -6418,7 +6446,7 @@ function smoothReset(){ if(smooth&&smooth.raf) cancelAnimationFrame(smooth.raf); // Solde le bloc courant : on affiche TOUT immédiatement. Appelé à chaque // frontière (fin de tour, outil, changement de bloc) — sinon l'ultime bout de // texte resterait en retard derrière le curseur de révélation. -function smoothSnap(){ if(!smooth) return; const el=smooth.el, target=smooth.target; smoothReset(); scheduleRender(el, target); } +function smoothSnap(){ if(!smooth) return; const el=smooth.el, target=smooth.target; smoothReset(); scheduleRender(el, target, true); } // Débit BASÉ SUR LE TEMPS (indépendant du taux de rafraîchissement). La vitesse // de révélation est proportionnelle au RETARD accumulé, avec une constante de // temps TAU : le curseur traîne volontairement ~TAU derrière l'arrivée, ce qui diff --git a/internal/loki/ui/src/js/09-stream.js b/internal/loki/ui/src/js/09-stream.js index 7366772..485fc21 100644 --- a/internal/loki/ui/src/js/09-stream.js +++ b/internal/loki/ui/src/js/09-stream.js @@ -278,11 +278,15 @@ function syncSendBtn(){ // garanti par flushRender(), appelé à chaque frontière de bloc (outil, fin de // tour, erreur, caught_up). let renderTimer=null, renderPending=null, lastRenderMs=0; // {el, text} -function scheduleRender(el, text){ +function scheduleRender(el, text, final){ // Changement de bloc en cours de route : on rend d'abord l'ancien à sa dernière // valeur, sinon son ultime bout de texte serait perdu. if(renderPending && renderPending.el!==el) flushRender(); - renderPending={el, text}; + // `final` voyage AVEC le rendu en attente : le timer déjà armé (qui passe + // final=false) peut être celui qui consommera ce rendu — sans le drapeau sur + // le pending, le rendu de FIN de bloc repassait par le chemin « queue seule » + // et le début du raisonnement n'était jamais posé. + renderPending={el, text, final:!!final}; if(renderTimer) return; // Cadence ADAPTATIVE : on vise à ne pas passer plus d'~1/6 du temps à re-parser // le Markdown. Tant que le bloc est petit, un rendu coûte 1-2 ms → plancher @@ -290,15 +294,33 @@ function scheduleRender(el, text){ // devient énorme, le rendu coûte cher et on espace tout seul jusqu'à 500 ms : // le O(n²) est cassé sans jamais figer l'interface. const delay=Math.min(500, Math.max(16, lastRenderMs*6)); - renderTimer=setTimeout(flushRender, delay); + renderTimer=setTimeout(()=>flushRender(false), delay); } -function flushRender(){ +// Au-delà de cette taille, un raisonnement EN COURS n'est re-parsé que sur sa +// fin : re-parser le bloc entier à chaque tick est en O(n²) et finissait par +// figer l'affichage (le texte semblait s'arrêter alors que le moteur débitait). +// La carte étant bornée en hauteur et collée en bas, on ne perd rien à l'écran ; +// le texte COMPLET est posé au rendu final (frontière de bloc → final=true). +const REASON_TAIL=6000; +function flushRender(final){ if(renderTimer){ clearTimeout(renderTimer); renderTimer=null; } const p=renderPending; renderPending=null; if(!p) return; + let text=p.text; + if(final===false && !p.final && text.length>REASON_TAIL+800 && p.el.closest('.msg.reasoning')){ + let cut=text.length-REASON_TAIL; + const nl=text.indexOf('\n', cut); if(nl>-1 && nl=bw.scrollHeight-60; const t0=performance.now(); - renderBody(p.el, p.text); + renderBody(p.el, text); lastRenderMs=performance.now()-t0; + if(scrollable && stick) bw.scrollTop=bw.scrollHeight; } // Annule le rendu en attente SANS le poser : le bloc visé disparaît (fil vidé, // raisonnement jeté), le rendre ensuite écrirait dans un élément détaché. @@ -317,7 +339,7 @@ function smoothReset(){ if(smooth&&smooth.raf) cancelAnimationFrame(smooth.raf); // Solde le bloc courant : on affiche TOUT immédiatement. Appelé à chaque // frontière (fin de tour, outil, changement de bloc) — sinon l'ultime bout de // texte resterait en retard derrière le curseur de révélation. -function smoothSnap(){ if(!smooth) return; const el=smooth.el, target=smooth.target; smoothReset(); scheduleRender(el, target); } +function smoothSnap(){ if(!smooth) return; const el=smooth.el, target=smooth.target; smoothReset(); scheduleRender(el, target, true); } // Débit BASÉ SUR LE TEMPS (indépendant du taux de rafraîchissement). La vitesse // de révélation est proportionnelle au RETARD accumulé, avec une constante de // temps TAU : le curseur traîne volontairement ~TAU derrière l'arrivée, ce qui diff --git a/internal/loki/ui/src/styles.css b/internal/loki/ui/src/styles.css index 6594a3e..f30142c 100644 --- a/internal/loki/ui/src/styles.css +++ b/internal/loki/ui/src/styles.css @@ -656,6 +656,12 @@ textarea.sctl{min-height:76px;resize:vertical;line-height:1.5} /* height ET width sont animées (en px, pilotées en JS via scrollHeight/scrollWidth) → repli/dépli fluide en diagonale. overflow:hidden clippe pendant l'animation. */ .msg.collapsible .bodywrap{overflow:hidden;transition:height .42s cubic-bezier(.4,0,.2,1),width .42s cubic-bezier(.4,0,.2,1),opacity .3s ease,margin-top .42s cubic-bezier(.4,0,.2,1);opacity:1;margin-top:4px} +/* Raisonnement / outils : hauteur BORNÉE avec défilement interne. Un long + raisonnement (des milliers de tokens) faisait grandir la page de plusieurs + écrans ; la carte reste maintenant à taille fixe et suit la génération en + défilant toute seule (voir flushRender, 09-stream.js). L'animation de repli + garde son overflow:hidden via la hauteur inline — les deux cohabitent. */ +.msg.reasoning .bodywrap,.msg.tool .bodywrap{max-height:280px;overflow-y:auto;overscroll-behavior:contain;scrollbar-width:thin} .msg.collapsible .bodywrap>.body{min-width:0} .msg.collapsible.collapsed .bodywrap{opacity:0;margin-top:0} /* Composer = un seul bloc cohérent : barre de contexte intégrée en tête (fine