From 43489769f41943826e8daafc0dc657b9e6a00474 Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Thu, 20 Aug 2026 17:16:55 +0200 Subject: [PATCH] =?UTF-8?q?Cartes=20raisonnement=20born=C3=A9es=20et=20flu?= =?UTF-8?q?ides=20;=20version=200.11.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Un long raisonnement (des milliers de tokens) faisait grandir la page de plusieurs écrans et finissait par figer l'affichage : le bloc entier était re-parsé en Markdown à chaque tick, en O(n²). Deux mesures : - Les cartes raisonnement/outils ont une hauteur bornée (280 px) avec défilement interne, collé en bas pendant la génération (un défilement manuel vers le haut est respecté). - En direct, un bloc de raisonnement géant n'est re-parsé que sur sa fin (REASON_TAIL) ; le texte complet est posé au rendu de fin de bloc. La finalité voyage avec le rendu en attente (renderPending.final) : le timer déjà armé passait final=false et consommait le rendu final en ne posant que la queue — le début du raisonnement n'était jamais rendu. Version 0.11.0 (const, versioninfo, .syso régénérés) ; README et RELEASE_NOTES à jour sur les nouveautés du fork. Co-Authored-By: Claude Fable 5 --- README.md | 14 +++++++-- RELEASE_NOTES.md | 45 +++++++++++++++++++++------ cmd/loki/resource_windows_amd64.syso | Bin 7544 -> 7768 bytes cmd/loki/resource_windows_arm64.syso | Bin 7544 -> 7768 bytes cmd/loki/versioninfo.json | 10 +++--- internal/loki/run.go | 2 +- internal/loki/ui/index.html | 40 ++++++++++++++++++++---- internal/loki/ui/src/js/09-stream.js | 34 ++++++++++++++++---- internal/loki/ui/src/styles.css | 6 ++++ 9 files changed, 122 insertions(+), 29 deletions(-) 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 f07048d3014ce345ac7100683dc8e531214f6e54..d125b84320d346a9c01394ec76d0a625ed78bc3c 100644 GIT binary patch literal 7768 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 S1o9nE>5KZ;Vv6>TMDkD03(Nli literal 7544 zcmeGhX;c(vvU(0V2k@c5qH!jkm=&Bvq97=P0*Z;C(G?;{7!Uyi<8a6kVQ3=?dEPf3 zQH+~tOfV7+pgT5o}Ub%?^(n4MX&|w0~AvP{Hl;!FkJ4OSzB6=AiY36v#*o#nojSqZ8?Eq*qgas7P z76=V_KqKHZQ{W+$g>ppx3_=0Ia)d&JbqI$c>_ljQ&;s=sBD6zjgiwai7@^S`HnpYvTG&QQXYnb zVV*BCn()Bn5~>66_Vn>MJtz2MLAaFM_}`s>H*%0S}FUVk+|7;li7rr%xIpe=c3<(j$j!_PGj68ClaOgM31*!bU8 zAMM!fY=3%~uPCVLe4BKs(VcDYpEXO5I2S09S+&&xAiIj+Hd_XqU_k zaayAqTS8f9pC=s-vN-CX$<;tQL7N)ZYO=^pj!A>&!zxTfVmz3GO738Xzw+WXsrjo{EgYA(WtZo`)(= z{Y`4&aY)H9`{Ry9WLN(+caC$xv`3y1DOt{^e-vo*S5#R&DVcM=aL4CK_1E&vYWIBC zJ^itjaHRwD@M`W)R=cVSZzQ>A7PmEwk$4X8X?J;M8BE24tw}cjsz1C&dj*v~cCCw9 zuI*0rpIjB)bFCk$RzPtla`}e672R_lmez z5Y5M7r9d3`29Fp<8m{}J-zmAm2kxP(KEL-QwJs-zL4{eRkz;oUH1b2 z1~5$BUds)Q>VwS~9Bv2)h#!p-G#fwlY&k9joO6$h4GsW0pj@t!?N*!p+RZ`9705{u~{(!Qu#ccjOBlURLsMa!h1Yn}I!{g6L06tMLZ7<+ORfZ*fa`l(srQ@z^8YjNP4%w+BS>ye(-=}u28tk-?> zHnd(j+g29X+VxcRu+kh{4tlwj`W+XSY2UWl0Zk3;(o0oiX>YFJ^ zHK#|_v}KH%9TmPmr}^nzk@)%9##L!iUFt33g#5;`yi=dtRX<(UlDOHVv{Sz2dUe1k zxSx@`ELyn`wuo!hmjY5AxTN1zYd>0D6~K%Hr=8*ax+-yP^^#Gk6QRuf(V3*L-r)y# zD;+jZFC0%8?aIO+-W<(w=xoo3( zQtFSq&Qw&Im-@G*97z*7FRW^qS1?-V0Xp%2c%^Pr(SJVpD7k&yl<6cI!IJ-NG!}}* z;DWz5|x@NbrNeD!1wcN~S4>0$$y6h2m!{Rjm7wa8sFhX}bdqr^!nAo+s1nA`*wk zC=wfm5*}GJ+RJ{V+rQ{~_#G+}it_2wp3~)VbViC<@TdFpiXQS>*8*9Jf!dKW zLGagq8Fl;KYqVjh%~$#P9ZAL3bWe0%bq-@(XG^C>U&x(h@WhsG`sLf}<7Ve>grNK< zUfW%7ZG?R;D-VtP{;UagJF#KnypnU4e47GwB~{=#qTt*7pKqt(`-owp8l5_hi7FGh zuP%ZQw0{`dSCWVg?JIx(|Aq|9WoTbHw67%F$Dw`Y(7tkLUpcg|#L`RWxR6a=YJdX5{Q&a)SWIBqRp$dVfbrd;4B?p{3ik#1q z^L$_L=^sr=W;fB zO0+CNT<51f#S8PYl_WxJS$6lisD4; zWj+A5;3x{-MY(WRiez62$!I+iIVAJIRz!au$xbL{Mgni4#kXKOV%&(4niwbp4k)L~ zsswFDvYA%k9K+Ca@(s@zeevzJt&3%pK38NGq$&YnUp+Y>oy78Vb6$jF*T71QWhi>? zqaVSo4-!ujpZ>DHkLt^q;(VJyxK{%JrB=k2UDAPCeH5HH;7BJD1J_?$=a` I^@%w9F9{NQ0RR91 diff --git a/cmd/loki/resource_windows_arm64.syso b/cmd/loki/resource_windows_arm64.syso index c92084b559ae0eb500a450bc6fabbdecce8f957e..7364aa2ceaf7898aa1e29c57583a3bc3c4a88aff 100644 GIT binary patch literal 7768 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 T0P-DA>5KZ;VhUMII+A|^4G_!} literal 7544 zcmeGhZBP_fvS)TzJ{Pc1;Ly06_=P#aH6aS3vM8XK2x>ebf`kPXFt9Ek@)2PfBN0-5 zYoe$`A)bjLK_wbMfk18zN1Ynwh;b7HQBgqysDObiIM;6$)*(V&-BqgY$MJS%`s4NM z?$_OKc)VGiM*(S}F97H|Ys7aPWL^^aX+0cIn55g}=oc+0tqQ2k8+0z@4E7z>2O z6fjl@jcH&I@R~W$NM)fMkw1k{h_DKw2w?-l5eT~w8X>eoJ;n$f5Sk#ABQ!-AfY1z~ z3ZXf|OoU>DIS3K(8U-e3z=aqc(DeL;L+}HJ;71I>Pa1-sIRrmz2>$UQ_{Br;tA^lf zhu}9o$EUfxYf0SF03Hqzzpm%T>EmZik~pAIoIdGudDqL^p}Yg$sk|}Ak>2IVE`1%10lCS?1(tQ0{SkQd^S}a_?emm=d`n9$I zEBlQhbg>&qXCd9ei^~T~d>4pK?Mc~+eHMByLr9i}l`$!FNjs}R;OXP(u{u)gO#f(=%-p=fJ~N%(k>`}7WfZ$UJdjf_wUyBr z4wikg#AMKm3w(kNc7-i4``~?QvO?Zlz{WJ#(iW zVFY6vz^F>_>57;QE$SDMZlu&7&I;Me{QUA4o90qp=UW=P!jARa!ZpjX|J*6|`Z8#8 z0owmJ{>o57#`)_{cV{5+d7r=9Z%sGL&C_o#Zqk)Mwwax|`kiCiJBj*{!7gr7LLoPi0iF70fazIh(1 z&ik9p%JYzlW%tMBM~wP2oW@vHZ|B2uzkPkblT6|JhVc~Cy@Y{{-;NsT`gS=4{{ zb1Xlv!1;#tL;{NQ0 zn3cMo#DFO^(Y@EI{20*{Dq<>QArIpVR??)67-iLZFTM;&FNGbdCrD;bgxrc`|1smEVqbC`t@?#)yiM< z(?Zrw3*0C?KgXz{?ne8Wvu0M485ioj<6$=1NuCY2gnHUU>I*S+ru6_;Yu;reyB}}I z^!jP`->FF}WY+|dv4hGEGW@?}i944Tbne{HEBQ~2U1&)tc{<^D&N|5cHT>arn6IY3$I3c)uy{Vx@^1g zp(GrbhSfx|%?CG`Q>z7c|sJ>TBN|lR611E$^L5`r>s# zNRP^C>x_~KgwdfY38v?1k3v^RMw~7|b3;_ob<@68`IA2wm)OFQrz*#pGh1lpR3P=R z+45S)<-%29^&}Bee$#y2TM%Ol>8WDIFD;_0Yy;vZklt&`v`bd1mGvS}(EZn5Tjt6g znvqd|d&=Q7vFqZRrUk`g^&X%T|A$xV4mI=VgZGj<#!sC=q7fqf-$rAx zL?UT#%7980CC`FC^3v{PS$FA`SI>gTwc_Qm0K=o zFlFA+h23_-k{%8I5LDw{n?T96Wl&5vtW+w0#!|()9}U-3i5GS{!AORzgl}lMb`O!r z9;Zxf7D;KcXml6-Nq0c$)$kisCX^O^Hsk(8X?lLQ=aHEy79k(+FD$)FKimLhDF*3A z%Y`9d{$3*{Eih&RlVI zBLCGz@WJ*E!~04SvEhB?@BiPBW4R3PD~I=$WcxV0uN>Z24(}_6_m%wZ^YFfMcwgCf zT1a-56pR2X+)Gj*RaQ;0L})`{2_X zanj0!ZBNN)@?C+u9oFI&opN7&3 zj;lb7SdQYJYfxSR>rflvOJS5$ikf0^4B^EiXFbxymK61`N1d^#aSc)quo!(_4pX=Z zE(B|XZz@h#GDb?l`AGykU=c>r19?);BjP)WKqzXAN4>G!OaRVcJSamN@ETqAK5@iUqmvmPAL7Mb_6p4D7`96n9*G>1dEhEyAdh4x3^OBzSJ2`sFat4u#7IpHmH{V} z(`QwRHY2%AD{+o%&~x$)&lG*}>$9zoWt1UTWEP|<0bzeVIU$|I@$_?Ef@9aiYK&zV zdhTl&!LJVzPZFPjvcHe&vzX$y541_)N>!lt=T{TJ1YC!Jl^Tl@WHuzb{gr|pU@kmG z1PtH!Xjq8v9@1zCFkle|EXjan8n7$_cHDp!8?Y(^rZr$q2CU0~^?wbcfqduEd%*vi KN+C.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