Les fichiers du chat vivaient dans un pot commun : on changeait de discussion
et on revoyait les mêmes pièces jointes, et supprimer une discussion laissait
derrière elle tout ce qu'on y avait déposé ou fait écrire à l'agent (seules ses
captures, déjà rangées par identifiant, partaient avec).
Chaque discussion a désormais son dossier, <workspace>/discussions/<id>/ :
- les dépôts (uploads/), les captures (captures/) et ce que l'agent écrit y
atterrissent ; le shell et les chemins relatifs du modèle y sont résolus ;
- le panneau Fichiers s'ouvre sur ce dossier et n'en sort pas, et se redessine
quand la discussion change (bascule ou vidage, signalés par le flux SSE) ;
- supprimer une discussion — ou la vider — emporte ses fichiers. Les deux gestes
le disent maintenant avant de demander confirmation ; « clear chat » en
demandait aucune.
La racine du dossier de travail reste la borne de sécurité : les liens des
anciens messages (uploads/x.pdf, captures/<id>/y.jpg) continuent d'ouvrir leur
fichier par un chemin de repli. Au démarrage, une migration range les captures
dans le dossier de leur discussion et rend chaque dépôt à la discussion qui le
mentionne dans son journal ; ce que personne ne réclame reste à la racine,
atteignable par le bouton « hors discussion » du panneau, qui disparaît une fois
le ménage fait.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LPyFxVHNAN9u5pVzSYMwjd
L'agent produit des rapports, des scripts, des exports et des captures dans son
dossier de travail. On ne pouvait en récupérer un que si le modèle avait pensé à
en mettre le lien dans sa réponse : tout ce qu'il écrivait sans le dire restait
invisible, et faire le ménage demandait un shell.
Un panneau de droite, ouvert et fermé par le bouton dossier du pied de carte,
liste ce dossier avec navigation dans les sous-dossiers, téléchargement et
suppression. Il pousse la conversation au lieu de la recouvrir — la largeur de
lecture étant pilotée par --measure, le fil se resserre et se recentre tout
seul. Sur téléphone c'est un tiroir, comme les réglages à gauche.
Le socle existait : /api/chat/file téléchargeait déjà, workspaceRel bornait déjà
les chemins, downloadWorkspaceFile gérait déjà le blob. Manquaient les deux
verbes qui comptent, /api/chat/files pour lister et /api/chat/file/delete pour
supprimer.
Trois choix qui méritent d'être dits :
- Un dossier affiche la taille de TOUT son contenu, pas celle de son inode :
c'est ce qu'on libère en le supprimant, donc c'est le chiffre qui aide à
décider. Le parcours est borné à 20 000 entrées pour que la liste reste
instantanée si l'agent y dézippe quelque chose d'énorme.
- La racine du dossier de travail est refusée à la suppression : l'agent y perd
le dossier dans lequel il écrit, et « tout effacer » ne doit pas être à un
clic. La suppression d'un dossier annonce le nombre de fichiers et le poids
emportés avant de demander confirmation.
- Le panneau ne se remplit qu'à l'ouverture. Un ReadDir récursif à chaque
chargement de page pour un panneau fermé serait payé par tout le monde.
Ouvrir le panneau rétrécit la conversation SANS déclencher de `resize` : la
gouttière de barre de défilement mesurée dans --sbw resterait celle de l'ancienne
largeur et la carte de saisie serait décalée du fil. On resynchronise donc
explicitement ; la hauteur, elle, est déjà suivie par un ResizeObserver.
Vérifié. Six tests Go sur le bornage, qui est la partie sensible — ces routes
suppriment sur disque à partir d'un chemin venu du navigateur : « .. », chemins
absolus, racine, et un lien symbolique posé dans le dossier de travail sont tous
refusés, et le fichier voisin survit. Puis au navigateur, en clair et en sombre :
listing trié dossiers d'abord, taille récursive, descente et fil d'Ariane,
téléchargement réel (l'événement download porte bien rapport.md), suppression
confirmée puis disparition de la ligne, persistance de l'état ouvert, tiroir
mobile avec voile, et la saisie reste alignée au pixel sur le fil (0 px des deux
côtés) une fois le panneau ouvert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VueWA9xcYadaYq65tBisix
Le retrait de l'accès distant s'était arrêté à mi-chemin. La section « Accès
distant » de la barre latérale et son module 13-remote.js ont bien disparu au
commit 3740b88, mais l'autre moitié du même sujet est restée : le bouton
« Postes distants » du composeur, l'icône écran/wifi sous « send ».
Ce sont deux fonctionnalités distinctes dans le code (relay_* pour le tunnel
ajean.link, node_* pour les postes appairés), mais à l'usage c'est la même
chose : faire agir Loki ailleurs que sur le serveur qui l'héberge. Sur une
installation en conteneur, ni l'une ni l'autre n'a d'objet, et ce bouton
occupait une place dans une ligne d'outils déjà dense pour ouvrir une modale
d'appairage qui ne servira jamais.
Partent avec lui : les deux modales (celle d'appairage servait aussi d'écran
d'édition d'un poste, via un remplacement de pied), le module 15-node.js
entier, et les règles CSS #node-btn et .node-*.
Le code serveur ne bouge pas — mêmes raisons qu'en 3740b88 : les routes
/api/node/* et le serveur WebSocket d'appairage restent en place mais
deviennent inatteignables, plus rien ne pouvant générer de code d'appairage.
Les retirer créerait un conflit à chaque reprise de l'amont pour un gain nul.
Un piège mérite d'être signalé, et il est maintenant commenté dans le code :
loadAll() enchaîne ses chargements dans un Promise.allSettled, qui AVALE une
ReferenceError. Oublier de retirer loadNode() de cette ligne n'aurait rien
cassé visiblement — la page se serait simplement chargée à moitié. Le test
navigateur écoute donc pageerror et échoue au moindre message.
Vérifié en 1400×900, thèmes clair et sombre : aucune erreur JS au chargement,
bouton et modales absents du DOM, openNodeHub indéfini, et le pied de carte
garde son décompte de contexte, « compacter » et le trombone, tous atteignables
au clic. Le composeur perd 26 px de large ; comme sa hauteur est mesurée depuis
le correctif précédent, la réserve sous le fil suit toute seule — overlap.js
repasse au vert de 380 à 2000 px.
Deux échecs relevés au premier passage venaient du banc d'essai, pas du code :
/api/backends/devices répond 400 parce que le BIN du fixture n'est pas un vrai
llama-server, et « compacter » est masqué tant que le contexte est sous 50 %.
Les assertions ont été corrigées, pas le code.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VueWA9xcYadaYq65tBisix
Installer un modèle demandait d'aller sur huggingface.co, de naviguer dans
l'arborescence d'un dépôt et de coller un lien à la main. Rien ne disait si le
fichier tiendrait en mémoire, et surtout rien ne reliait un modèle à SON
projecteur vision : c'est ainsi qu'un mmproj-Qwen3VL-8B s'est retrouvé
configuré pour un Qwen3.8-27B — deux modèles sans rapport, moteur qui démarre
et ne voit rien.
La recherche Hugging Face arrive dans l'éditeur de preset. Elle ne remonte que
les dépôts GGUF ; déplier un dépôt montre ses quantifications avec leur taille
et un verdict mémoire, et propose le projecteur vision DU MÊME DÉPÔT — le seul
qui corresponde. Quand le dépôt n'en publie pas, Loki le dit au lieu d'aller en
chercher un ailleurs.
Le nouveau code ne télécharge rien : il produit des URL que le chemin existant
consomme tel quel (normalizeHFURL, shardURLSet, sonde d'espace disque, reprise
et annulation). Une file d'attente enchaîne modèle puis projecteur — le serveur
ne mène qu'un transfert à la fois, et le champ Vision ne se remplit que si le
modèle est déjà sélectionné.
Trois familles de .gguf cohabitent dans un dépôt et ne veulent pas dire la même
chose : mmproj-* (projecteur), mtp-* (poids de décodage spéculatif) et le
modèle. Les deux premières ressemblent à un modèle ; les proposer en vrac, ce
serait offrir de lancer llama-server sur un encodeur d'images. Les tranches
d'une famille sont repliées en une entrée de taille TOTALE : annoncer 15 Go
pour un modèle qui en occupe 45 promet une place qui n'existe pas.
Le verdict mémoire s'appuie enfin sur le GPU. detectHardware() ne renvoyait que
la RAM système alors que detectGPUs() existait déjà : sur un serveur à carte
NVIDIA, le verdict se prononçait sur la mauvaise grandeur. Le coût du cache KV
reste une estimation assumée — l'exact demanderait de parser l'en-tête GGUF —
et l'interface l'annonce comme telle plutôt que d'afficher un chiffre
faussement sûr.
Le catalogue ajean.link disparaît. Sa route n'avait aucun consommateur (l'écran
d'accueil qu'elle attendait n'a jamais existé), son repli embarqué proposait du
Qwen2.5 de 2024, et un fork qui laisse le serveur de l'amont décider de ce
qu'il propose n'est pas vraiment un fork.
Une piste écartée en cours de route : marquer les dépôts « vision » d'après les
tags Hugging Face. Ils mentent — des deux dépôts GGUF de Qwen3.8-27B qui
publient tous deux un mmproj, seul ggml-org est taggé image-text-to-text. Une
pastille sur l'un et pas sur l'autre aurait été pire que rien. La vision est
donc déduite de la seule source qui ne se trompe pas : la présence d'un
mmproj-*.gguf dans l'arborescence.
Vérifié : 8 tests unitaires sur les arborescences réelles des deux dépôts, puis
au navigateur contre le vrai Hugging Face — 25 dépôts trouvés, 3 quants listés
sans aucun mtp ni mmproj, projecteur Q8_0 proposé et coché, verdicts affichés
avec leur explication, modale dans l'écran. L'ordre de la file (modèle puis
projecteur) est testé en interceptant les appels, sans transfert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VueWA9xcYadaYq65tBisix
Trois gains de place, mesurés au navigateur en 1500×900.
- Raisonnement, outils et mémoire se posent CÔTE À CÔTE dans une rangée qui
déborde à la ligne : un tour d'agent en enchaîne parfois dix, et empilées
elles repoussaient la réponse hors de l'écran alors qu'elles ne portent
qu'une étiquette tant qu'elles sont repliées. Mesuré : 8 bulles occupent
2 lignes au lieu de 8. Dépliée, une bulle reprend toute la largeur — son
contenu ne doit pas être comprimé dans une colonne étroite.
- Les sections de la barre latérale sont réunies en DEUX cartes (réglages
d'IA / réglages d'application) au lieu d'une carte chacune : onze arrondis
et autant de gouttières faisaient déborder la colonne. Mesuré : 876 px de
contenu pour 876 px visibles — plus d'ascenseur.
- Section « Accès distant » supprimée, ainsi que son module JS (13-remote.js)
et son chargement : elle pilotait le tunnel ajean.link, sans objet ici.
Le code serveur du relais reste en place mais inerte (aucun jeton) : le
retirer créerait des conflits à chaque reprise de l'amont.
Reprise de la maquette fournie (LOKI — direction Terre) : bruns chauds, vert
sauge, argile, et des surfaces en CARTES arrondies séparées par une gouttière
au lieu d'une colonne à filets.
- Palette transposée sur les jetons EXISTANTS (--bg, --panel, --accent…) et
non par réécriture des 1200 lignes : les 300 usages de variables suivent
d'eux-mêmes. Rôles clarifiés — sauge = sélection et action, argile =
chiffres et avertissements, terre cuite = arrêt et danger.
- Polices Bricolage Grotesque + IBM Plex Mono EMBARQUÉES dans le binaire et
servies par Loki (/fonts/), pas par Google Fonts : une instance locale ou
coupée d'internet doit s'afficher correctement, sans qu'un tiers apprenne
qui consulte l'interface. Sous-ensemble latin, 106 Ko au total.
- Structure en cartes : barre latérale en pile de cartes, colonne de chat en
panneau unique, carte de saisie posée un ton au-dessus. Liseré sauge sur la
discussion active, bulle de question au coin bas-droit rentré.
- Bouton d'envoi en sauge : il reprenait --text, donc un brun indistinct du
reste ; l'arrêt reste en terre cuite.
Bornage au bureau (min-width:721px) : sur téléphone la barre latérale est un
tiroir plein écran et le fil occupe toute la largeur — des cartes n'y
grignoteraient que des pixels utiles.
Vérifié au navigateur, thèmes clair ET sombre, plus un rendu 390 px :
polices réellement chargées (200 sur les trois woff2), aucun débordement
horizontal, tiroir mobile inchangé.
Le champ « Ton prénom » n'annonçait rien sur sa nature : les gestionnaires le
prenaient pour un identifiant de connexion et recouvraient la saisie d'une
liste de comptes enregistrés (192.168.1.16 / admin…), rendant le champ
pénible à remplir.
autocomplete="off" ne suffit pas seul — la plupart des gestionnaires
l'ignorent délibérément sur ce qu'ils croient être un formulaire de
connexion. On ajoute donc les marqueurs qu'ils respectent réellement :
data-1p-ignore (1Password), data-lpignore (LastPass), data-form-type="other"
(Dashlane), et un name explicite qui n'évoque ni user ni login.
- Identité : prénom et avatars (emoji) affichés en tête des bulles, à la
place des libellés « user » / « assistant ». Stockés dans les préférences
SERVEUR, donc partagés entre appareils comme le thème. La liste d'emojis
est servie par /api/prefs : pas de seconde copie côté client.
Validation stricte — prénom nettoyé de ses caractères de contrôle et borné
à 24 runes, avatar refusé s'il n'est pas dans la liste fermée.
- Barre latérale : séparateur « Paramètres » qui regroupe Identité,
Apparence, Accès OpenAI, Accès distant et Actions, sous les réglages d'IA
(Machine, Presets, Mode agent, System prompt, Moteur). Un séparateur plutôt
qu'un second niveau de repli, illisible dans une colonne aussi étroite.
- Logo : 10 → 17 px, il était minuscule sur un écran dense.
Le prénom vient de l'utilisateur : les libellés sont écrits en textContent,
jamais en innerHTML.
Loki passait TOUJOURS -ngl, avec 999 comme repli quand le champ est vide :
vider « Couches sur GPU » ne changeait donc rien. Or llama.cpp sait ajuster
lui-même le nombre de couches à la mémoire libre (common_fit_params), mais y
renonce dès que la valeur est imposée — « n_gpu_layers already set by user to
999, abort ». D'où, sur une carte trop juste pour le contexte demandé, des
cudaMalloc en échec, un repli partiel sur le CPU, et un débit effondré alors
que le GPU tourne à 100 %.
NGL=auto n'envoie plus -ngl du tout. Le défaut reste 999 : omettre le drapeau
sur un llama.cpp antérieur à cet ajustement le ferait tourner 100 % CPU.
Le champ de l'UI passe en texte pour accepter « auto ».
Vérifié : NGL=auto → aucun -ngl dans la ligne de commande ; NGL=999 → -ngl 999.
L'amont ne connaît qu'un fil unique (bkChat/conversation) que « clear chat »
effaçait définitivement. On garde toute la machinerie (un seul conv en
mémoire, mêmes flux SSE, même compactage) mais rangée par discussion :
bkChat/index liste des discussions (métadonnées seules)
bkChat/active discussion ouverte, partagée par tous les appareils
bkChat/conv:<id> état complet d'une discussion
Basculer réutilise le mécanisme d'epoch du reset : les abonnés SSE reçoivent
{reset:true} et rejouent le nouveau fil — aucun code de rendu à toucher. Le
fil unique existant est repris comme première discussion au premier
démarrage, et sa clé d'origine est laissée intacte.
- routes /api/conversations (liste, new, switch, rename, delete)
- barre latérale : liste (titre déduit du 1er message, date, nb d'échanges),
bouton +, renommer, supprimer ; lignes construites en DOM et non en
innerHTML, les titres venant de messages utilisateur
- suppression de la dernière discussion : convCreate et non convNew, qui
aurait réenregistré celle qu'on vient d'effacer
Le bouton « Vérifier les mises à jour » répondait « GitHub a répondu 404 » :
il interrogeait les releases du dépôt du fork, qui n'en publie aucune. En
conteneur, remplacer le binaire n'a de toute façon pas de sens — l'UI, l'API
et la CLI renvoient désormais « docker compose pull ».
Vérifié en conteneur : création, bascule, renommage (titre accentué avec
< > &), suppression de l'active puis de la dernière ; /api/update et
loki update renvoient la note Docker.
Le sélecteur de l'éditeur de modèle proposait encore « Précompilé » et
« Compilé » — deux installations que Loki ne fait pas en conteneur : cliquer
répondait « installez d'abord llama.cpp… ». Pire, le moteur de l'image ne
correspondant à aucun des deux, il était classé « Personnalisé ».
En conteneur, ces deux options laissent place à « Image » (sélectionnée par
défaut, y compris pour un preset sans BIN) ; « Personnalisé » reste pour un
binaire déposé dans /data/backends.
Le moteur de l'image est désormais déclaré par l'image elle-même via
LOKI_ENGINE_BIN (Dockerfile + entrypoint) au lieu d'être deviné d'après le
chemin : /api/llamacpp expose provided et provided_bin.
Attribution : sous le nom Loki, « fork de AJEAN » avec lien vers le dépôt
d'origine.
Vérifié dans l'image : provided=true, provided_bin=/app/llama-server,
BIN semé identique ; build, vet et tests verts.
Le panneau « Moteur » proposait d'installer llama.cpp alors que l'image le
fournit déjà (/app/llama-server) : cliquer lançait une compilation qui
échouait sur « outils manquants : cmake », l'image n'embarquant ni cmake ni
compilateur.
- engineProvided() : vrai en conteneur quand BIN désigne un exécutable
existant HORS de LOKI_HOME (donc ni dépôt cloné, ni précompilé, ni backend
custom, qui sont gérés par Loki) ; exposé en clé 'provided' de /api/llamacpp
- les routes install / install-custom / update refusent en 409 avec un
message explicite au lieu de partir en build
- l'UI affiche l'état réel (chemin du binaire) et masque les trois modes
Le journal du moteur n'était repliable que par un second clic sur la pastille
d'état — sans chevron ni titre cliquable, contrairement à toutes les autres
sections de la barre latérale. Il devient un <details><summary> comme les
autres ; la pastille reste un raccourci et le chargement suit l'événement
toggle, quel que soit le moyen d'ouverture.
Vérifié dans l'image : provided=true, config_bin=/app/llama-server,
POST /api/llamacpp/install → 409 explicite, <details id="svc-log-box"> servi.
- NOTICE.md : Loki est un fork d'AJEAN (nathaninline, MIT), liste des
modifications ; LICENSE amont conservée à l'identique.
- README réécrit : bandeau fork, architecture conteneur, démarrage Docker,
procédure Unraid (plugin Nvidia Driver), différences avec l'amont.
- UI : le logo pixel-art épelle désormais LOKI (il épelait encore AJEAN),
infobulle d'attribution sur la marque ; index.html régénéré.
- Workflow GHCR : libération d'espace disque du runner (l'étape CUDA devel
ne tient pas dans les ~14 Go libres), build-args CUDA_ARCHS/LOKI_VERSION,
cache GHA en mode min (plafond 10 Go).
- module github.com/R0m1k3/Loki, cmd/loki, internal/loki (package loki)
- LOKI_HOME, LOKI_MODEL_DIRS, LOKI_SERVICE, LOKI_DL_CONNS ; /etc/loki ;
units loki-engine / loki-ui ; binaire et aide CLI
- updateRepo pointe sur R0m1k3/Loki (l'auto-update ne tirera plus les
binaires AJEAN amont)
Conservé à l'identique : le domaine ajean.link (service de tunnel amont),
les littéraux de migration 0.7.x (migrate_07.go), RELEASE_NOTES.md et
LICENSE (historique et licence de l'amont).
go build/vet/test : verts.