mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
v0.6.11 : le .exe telecharge met a jour et demarre, emplacements dans le journal
- Windows : quand AJEAN est deja installe, lancer le fichier telecharge remplace le binaire installe (s'il est plus recent) puis demarre l'application, sans message. La 0.6.10 annoncait « vous lancez une copie » puis demarrait quand meme : une information inactionnable suivie d'un comportement non annonce. Le refus d'installation est desormais memorise (.install_declined). - la version du binaire installe est lue dans les ressources du fichier (VS_VERSIONINFO). L'implementation evidente (executer `jean version` et lire la sortie) renvoie du VIDE : le binaire est en sous-systeme GUI et n'ecrit pas dans un tuyau, donc la mise a jour ne se serait jamais faite, en silence. Piege documente dans le code + deux tests. - UI : les emplacements passent sous le journal du moteur (clic sur la pastille d'etat) au lieu d'un bouton isole dans les actions.
This commit is contained in:
1 parent
f541d997d2
commit
0d9284e95c
10 files changed
+209
-70
No files matched your search
+7
-21
@@ -1,28 +1,14 @@
|
||||
## Corrections
|
||||
|
||||
**L'IA déposait ses fichiers sur votre Bureau.** En mode agent, quand elle créait un fichier (une recherche web mise de côté, un script, des notes), celui-ci atterrissait dans le dossier depuis lequel AJEAN avait été lancé. Le Bureau si vous aviez double-cliqué le fichier téléchargé, `C:\ProgramData\jean\bin` si vous passiez par l'installation. Personne ne s'attend à ce qu'une conversation laisse des fichiers derrière elle.
|
||||
|
||||
L'IA travaille désormais dans un dossier à elle, `workspace`, rangé avec vos réglages. Les fichiers déjà créés ne bougent pas, et le bouton « Où sont mes fichiers ? » vous dit où regarder. Si vous lui demandez explicitement d'écrire à un endroit précis, elle le fait toujours : seuls les fichiers sans destination indiquée sont rangés.
|
||||
|
||||
Merci à Sébastien pour le signalement, et pour avoir tout de suite pointé le lien avec le dossier de lancement.
|
||||
|
||||
**Le bouton de mise à jour échouait sur un message incompréhensible.** Sous Linux, lancé sans `sudo`, il répondait `open /usr/local/bin/.jean-update.tmp: permission denied`. AJEAN prévoyait bien un message expliquant qu'il fallait des privilèges, mais il arrivait trop tard dans le déroulé : l'échec se produisait avant, au moment d'écrire le fichier temporaire.
|
||||
|
||||
Les droits sont maintenant vérifiés avant de télécharger quoi que ce soit, et le message donne la commande exacte à lancer. Le téléchargement dispose par ailleurs de dix minutes au lieu de trente secondes : sur une connexion lente, une mise à jour qui réussissait pouvait être annoncée comme un échec.
|
||||
|
||||
Merci à Emmanuel pour le signalement et pour avoir isolé le rôle de `sudo`.
|
||||
|
||||
## Lancement sous Windows
|
||||
|
||||
**On sait enfin ce que fait le fichier téléchargé.** Le double-clic sur `jean-windows-amd64.exe` déclenchait une installation silencieuse : copie du programme, ajout au PATH, sans le moindre message. Impossible de savoir si l'on venait d'installer AJEAN ou seulement de l'ouvrir. Deux copies du programme coexistaient alors, celle que vous lanciez et celle installée, et la mise à jour ne touchait que la première.
|
||||
**Le fichier téléchargé met AJEAN à jour et démarre.** Si AJEAN est déjà installé sur la machine, lancer `jean-windows-amd64.exe` remplace le programme installé par cette version, puis ouvre l'application. Sans question, sans message : c'est la seule chose qu'on puisse attendre d'un fichier qu'on vient de télécharger.
|
||||
|
||||
AJEAN vous pose maintenant la question, une seule fois. Il annonce où il s'installe, où vont vos réglages, ajoute un raccourci au menu Démarrer et au Bureau, puis démarre depuis la copie installée. Un seul programme, à un endroit connu, que la mise à jour retrouve. Répondre non le lance sans rien installer. La désinstallation retire les raccourcis qu'elle a posés.
|
||||
La 0.6.10 se contentait de signaler qu'AJEAN était déjà installé ailleurs, puis démarrait quand même la copie téléchargée. Une information dont on ne pouvait rien faire, suivie d'un comportement qu'elle n'annonçait pas.
|
||||
|
||||
À la toute première installation, en revanche, AJEAN demande toujours : c'est le seul moment où savoir ce qui est posé sur la machine, et à quel endroit, change quelque chose. Un refus est désormais mémorisé au lieu d'être reposé à chaque lancement.
|
||||
|
||||
## Interface
|
||||
|
||||
**« Où sont mes fichiers ? »** Un bouton dans les actions affiche l'emplacement de vos données, de la configuration, de la mémoire et du dossier de travail de l'IA. Il vous prévient aussi si le programme que vous exécutez n'est pas celui installé. En ligne de commande : `jean where`.
|
||||
|
||||
**Nouvelle icône.** L'icône de l'application, celle de la zone de notification Windows et celle de la barre de menus macOS reprennent le logo noir du site. Elles sont dessinées à partir d'une source unique et ne peuvent plus se retrouver décalées les unes des autres. Sur macOS, l'icône s'adapte au thème clair ou sombre de la barre de menus.
|
||||
**Les emplacements ont rejoint le journal du moteur.** Le dossier de vos données, la configuration, la mémoire et le dossier de travail de l'IA s'affichent sous le journal, en cliquant sur la pastille d'état. Ils avaient leur propre bouton dans les actions en 0.6.10, loin de l'endroit où l'on va quand on cherche à comprendre son installation.
|
||||
|
||||
## Mise à jour
|
||||
|
||||
@@ -30,6 +16,6 @@ AJEAN vous pose maintenant la question, une seule fois. Il annonce où il s'inst
|
||||
jean update
|
||||
```
|
||||
|
||||
Si vous lancez AJEAN sans privilèges administrateur et que le programme appartient à root, cette version-ci doit encore être installée en ligne de commande avec `sudo jean update`. Les suivantes vous le diront clairement depuis l'interface.
|
||||
Sous Windows, télécharger le fichier depuis la page des releases et le lancer fait désormais le même travail.
|
||||
|
||||
L'icône de la barre de menus macOS n'a pas pu être vérifiée sur une vraie machine.
|
||||
L'icône de la barre de menus macOS, introduite en 0.6.10, n'a toujours pas pu être vérifiée sur une vraie machine.
|
||||
Binary file not shown.
Binary file not shown.
@@ -3,13 +3,13 @@
|
||||
"FileVersion": {
|
||||
"Major": 0,
|
||||
"Minor": 6,
|
||||
"Patch": 10,
|
||||
"Patch": 11,
|
||||
"Build": 0
|
||||
},
|
||||
"ProductVersion": {
|
||||
"Major": 0,
|
||||
"Minor": 6,
|
||||
"Patch": 10,
|
||||
"Patch": 11,
|
||||
"Build": 0
|
||||
},
|
||||
"FileFlagsMask": "3f",
|
||||
@@ -25,7 +25,7 @@
|
||||
"LegalCopyright": "Copyright (c) 2026 AJEAN contributors. MIT License.",
|
||||
"OriginalFilename": "jean.exe",
|
||||
"ProductName": "AJEAN",
|
||||
"ProductVersion": "0.6.10",
|
||||
"ProductVersion": "0.6.11",
|
||||
"Comments": "https://github.com/nathaninline/jean — projet open source (MIT)"
|
||||
},
|
||||
"VarFileInfo": {
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const Version = "0.6.10"
|
||||
const Version = "0.6.11"
|
||||
|
||||
// Main est le vrai main() du binaire (cmd/jean ne fait que l'appeler).
|
||||
func Main() {
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/mod/semver"
|
||||
)
|
||||
|
||||
// Premier lancement sous Windows.
|
||||
@@ -22,12 +24,21 @@ import (
|
||||
// qui tourne, et celle installée) : « mettre à jour » ne touchait que celle lancée,
|
||||
// tandis que le raccourci et le PATH pointaient sur l'autre, restée en arrière.
|
||||
//
|
||||
// Maintenant : on DEMANDE, une bonne fois. Si l'utilisateur accepte, on installe
|
||||
// pour de bon (copie + PATH + raccourcis menu Démarrer/Bureau) puis on relance
|
||||
// depuis la copie installée et on quitte — il n'existe alors qu'UN seul binaire qui
|
||||
// compte, à un emplacement connu, et le bouton de mise à jour agit dessus. S'il
|
||||
// refuse, on lance l'application telle quelle, sans rien écrire ailleurs que dans
|
||||
// le dossier de données.
|
||||
// Maintenant, deux cas seulement :
|
||||
//
|
||||
// 1. AJEAN n'est pas installé. On DEMANDE, une bonne fois : c'est le seul moment
|
||||
// où l'utilisateur gagne à savoir ce qu'on pose sur sa machine et où. Puis
|
||||
// copie + PATH + raccourcis, et on démarre depuis la copie installée. Un refus
|
||||
// est mémorisé, on ne repose pas la question.
|
||||
//
|
||||
// 2. AJEAN est déjà installé. Le fichier téléchargé se comporte alors comme un
|
||||
// installeur de mise à jour : s'il est plus récent, il remplace le binaire
|
||||
// installé, puis l'application démarre. AUCUN message. Annoncer « vous lancez
|
||||
// une copie, ça ne sert à rien » puis démarrer quand même n'apprenait rien et
|
||||
// ne laissait rien à décider.
|
||||
//
|
||||
// Dans les deux cas il n'existe au final qu'UN binaire qui compte, à un
|
||||
// emplacement connu, et c'est celui que le bouton de mise à jour modifie.
|
||||
|
||||
const (
|
||||
mbYesNo = 0x00000004
|
||||
@@ -37,7 +48,14 @@ const (
|
||||
idYes = 6
|
||||
)
|
||||
|
||||
var pMessageBoxW = u32s.NewProc("MessageBoxW")
|
||||
var (
|
||||
pMessageBoxW = u32s.NewProc("MessageBoxW")
|
||||
|
||||
verDLL = syscall.NewLazyDLL("version.dll")
|
||||
pGetFileVersionInfoSizeW = verDLL.NewProc("GetFileVersionInfoSizeW")
|
||||
pGetFileVersionInfoW = verDLL.NewProc("GetFileVersionInfoW")
|
||||
pVerQueryValueW = verDLL.NewProc("VerQueryValueW")
|
||||
)
|
||||
|
||||
func messageBox(text, title string, flags uintptr) int {
|
||||
t, _ := syscall.UTF16PtrFromString(text)
|
||||
@@ -66,21 +84,21 @@ func appFirstRun() bool {
|
||||
return false // on EST déjà l'application installée
|
||||
}
|
||||
if _, err := os.Stat(target); err == nil {
|
||||
// Une copie est déjà installée — typiquement par l'installation silencieuse
|
||||
// des versions ≤ 0.6.9. On le signale UNE SEULE FOIS : répéter la boîte à
|
||||
// chaque double-clic du fichier téléchargé serait un harcèlement, pas une
|
||||
// information.
|
||||
notice := filepath.Join(JeanHome(), ".install_notice")
|
||||
if _, seen := os.Stat(notice); seen != nil {
|
||||
messageBox(
|
||||
"AJEAN est déjà installé sur cet ordinateur :\n\n"+target+
|
||||
"\n\nCe fichier-ci n'est qu'une copie téléchargée : vous pouvez la supprimer et lancer AJEAN depuis le menu Démarrer.\n\nCette copie va démarrer normalement.",
|
||||
"AJEAN", mbIconInfo)
|
||||
_ = os.WriteFile(notice, []byte(target+"\n"), 0o644)
|
||||
}
|
||||
return false
|
||||
// AJEAN est déjà installé. Lancer le fichier téléchargé doit alors se
|
||||
// comporter comme un installeur de mise à jour : si cette copie est plus
|
||||
// récente, elle remplace celle installée, et l'application démarre. Sans
|
||||
// message : annoncer « vous lancez une copie » puis démarrer quand même
|
||||
// n'apprend rien à l'utilisateur et ne lui laisse rien à décider.
|
||||
upgradeInstalled(target)
|
||||
return launch(target)
|
||||
}
|
||||
|
||||
// Première installation : là, une question. C'est le seul moment où
|
||||
// l'utilisateur a intérêt à savoir ce qui va être posé sur sa machine, et
|
||||
// c'est l'absence de cette question qui rendait le double-clic illisible.
|
||||
if declinedInstall() {
|
||||
return false
|
||||
}
|
||||
if messageBox(
|
||||
"Installer AJEAN sur cet ordinateur ?\n\n"+
|
||||
"• le programme sera copié dans :\n "+target+"\n"+
|
||||
@@ -89,6 +107,9 @@ func appFirstRun() bool {
|
||||
"Vous pourrez ensuite supprimer le fichier que vous venez de télécharger.\n\n"+
|
||||
"Répondre Non lance AJEAN sans rien installer.",
|
||||
"Installation d'AJEAN", mbYesNo|mbIconQuestion) != idYes {
|
||||
// Refus mémorisé : reposer la question à chaque double-clic serait
|
||||
// ignorer la réponse déjà donnée.
|
||||
_ = os.WriteFile(declineFlag(), []byte("non\n"), 0o644)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -103,14 +124,103 @@ func appFirstRun() bool {
|
||||
"\n\nL'application va démarrer. Vous pouvez supprimer le fichier téléchargé.",
|
||||
"AJEAN", mbIconInfo)
|
||||
|
||||
// Relance depuis la copie installée : à partir de maintenant, une seule et
|
||||
// même image du programme se met à jour, se relance et apparaît dans le tray.
|
||||
return launch(target)
|
||||
}
|
||||
|
||||
func declineFlag() string { return filepath.Join(JeanHome(), ".install_declined") }
|
||||
|
||||
func declinedInstall() bool {
|
||||
_, err := os.Stat(declineFlag())
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// launch démarre la copie installée et demande à l'appelant de rendre la main.
|
||||
// Renvoie false si le lancement échoue, auquel cas l'exécutable courant prend le
|
||||
// relais : mieux vaut démarrer depuis le mauvais dossier que pas du tout.
|
||||
func launch(target string) bool {
|
||||
cmd := exec.Command(target)
|
||||
cmd.Dir = filepath.Dir(target)
|
||||
if err := cmd.Start(); err != nil {
|
||||
return false // échec du relancement : on continue avec la copie courante
|
||||
return cmd.Start() == nil
|
||||
}
|
||||
|
||||
// upgradeInstalled remplace le binaire installé par celui qu'on exécute, quand
|
||||
// ce dernier est plus récent : lancer le fichier téléchargé met ainsi AJEAN à
|
||||
// jour, ce qui est la seule chose qu'on puisse raisonnablement en attendre.
|
||||
// Silencieux et best-effort : si la copie échoue (application en cours
|
||||
// d'exécution, donc fichier verrouillé), on démarre simplement la version en
|
||||
// place, qui remontera l'onglet existant.
|
||||
func upgradeInstalled(target string) {
|
||||
installed := binaryVersion(target)
|
||||
if installed == "" || semver.Compare(ensureV(Version), ensureV(installed)) <= 0 {
|
||||
return // même version, plus ancienne, ou version illisible : on ne touche à rien
|
||||
}
|
||||
return true
|
||||
// L'exe installé peut être en cours d'exécution : on l'écarte d'abord, comme
|
||||
// pour une mise à jour classique (voir replaceBinary).
|
||||
old := target + ".old"
|
||||
_ = os.Remove(old)
|
||||
if err := os.Rename(target, old); err != nil {
|
||||
return
|
||||
}
|
||||
if _, err := installSelf(filepath.Dir(target)); err != nil {
|
||||
_ = os.Rename(old, target) // rollback
|
||||
return
|
||||
}
|
||||
_ = os.Remove(old) // échoue si l'ancien tourne encore ; nettoyé au prochain lancement
|
||||
}
|
||||
|
||||
// binaryVersion lit la version dans les ressources du fichier (VS_VERSIONINFO,
|
||||
// posées par goversioninfo, cf cmd/jean/versioninfo.json), comme le fait
|
||||
// l'onglet « Détails » des propriétés Windows.
|
||||
//
|
||||
// ⚠️ NE PAS remplacer par un `jean version` exécuté : le binaire est compilé en
|
||||
// sous-système GUI, il s'attache à la console du parent et n'écrit RIEN dans un
|
||||
// tuyau. La sortie capturée est vide, donc la comparaison de versions échouerait
|
||||
// toujours en silence et la mise à jour ne se ferait jamais (vérifié).
|
||||
//
|
||||
// Renvoie "" si la ressource est absente ou illisible : on s'abstient alors de
|
||||
// remplacer quoi que ce soit.
|
||||
func binaryVersion(path string) string {
|
||||
p, err := syscall.UTF16PtrFromString(path)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
size, _, _ := pGetFileVersionInfoSizeW.Call(uintptr(unsafe.Pointer(p)), 0)
|
||||
if size == 0 {
|
||||
return ""
|
||||
}
|
||||
buf := make([]byte, size)
|
||||
if r, _, _ := pGetFileVersionInfoW.Call(
|
||||
uintptr(unsafe.Pointer(p)), 0, size, uintptr(unsafe.Pointer(&buf[0]))); r == 0 {
|
||||
return ""
|
||||
}
|
||||
sub, _ := syscall.UTF16PtrFromString(`\`)
|
||||
var info *vsFixedFileInfo
|
||||
var length uint32
|
||||
if r, _, _ := pVerQueryValueW.Call(
|
||||
uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(sub)),
|
||||
uintptr(unsafe.Pointer(&info)), uintptr(unsafe.Pointer(&length))); r == 0 || info == nil {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%d.%d.%d",
|
||||
info.FileVersionMS>>16, info.FileVersionMS&0xffff, info.FileVersionLS>>16)
|
||||
}
|
||||
|
||||
// vsFixedFileInfo : bloc racine de VS_VERSIONINFO. Seuls les champs de version
|
||||
// nous intéressent, mais la structure doit être complète pour l'alignement.
|
||||
type vsFixedFileInfo struct {
|
||||
Signature uint32
|
||||
StrucVersion uint32
|
||||
FileVersionMS uint32
|
||||
FileVersionLS uint32
|
||||
ProductVersionMS uint32
|
||||
ProductVersionLS uint32
|
||||
FileFlagsMask uint32
|
||||
FileFlags uint32
|
||||
FileOS uint32
|
||||
FileType uint32
|
||||
FileSubtype uint32
|
||||
FileDateMS uint32
|
||||
FileDateLS uint32
|
||||
}
|
||||
|
||||
// provisionDataDir crée le dossier de données et un config.env de départ.
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
//go:build windows
|
||||
|
||||
package jean
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// binaryVersion pilote la mise à jour automatique du binaire installé quand on
|
||||
// lance le fichier téléchargé : si elle renvoie du vide, plus aucune mise à jour
|
||||
// n'a lieu, et en silence. C'est exactement ce qui arrivait avec la première
|
||||
// implémentation (exécuter `jean version`, dont la sortie est vide pour un
|
||||
// binaire en sous-système GUI). D'où ces tests sur la plomberie syscall.
|
||||
func TestBinaryVersionReadsResource(t *testing.T) {
|
||||
// kernel32.dll porte toujours une ressource de version.
|
||||
dll := filepath.Join(os.Getenv("SystemRoot"), "System32", "kernel32.dll")
|
||||
if _, err := os.Stat(dll); err != nil {
|
||||
t.Skip("kernel32.dll introuvable")
|
||||
}
|
||||
v := binaryVersion(dll)
|
||||
if !regexp.MustCompile(`^\d+\.\d+\.\d+$`).MatchString(v) {
|
||||
t.Fatalf("binaryVersion(kernel32.dll) = %q, attendu une version x.y.z", v)
|
||||
}
|
||||
}
|
||||
|
||||
// Un fichier sans ressource de version doit renvoyer "" : upgradeInstalled s'en
|
||||
// sert pour s'abstenir plutôt que de remplacer un binaire au hasard.
|
||||
func TestBinaryVersionWithoutResource(t *testing.T) {
|
||||
p := filepath.Join(t.TempDir(), "vide.exe")
|
||||
if err := os.WriteFile(p, []byte("pas un exe"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if v := binaryVersion(p); v != "" {
|
||||
t.Fatalf("binaryVersion sur un fichier sans ressource = %q, attendu \"\"", v)
|
||||
}
|
||||
}
|
||||
+12
-10
@@ -872,6 +872,12 @@ button:hover{border-color:var(--dim);color:var(--text)}
|
||||
<button onclick="loadSvcLog()" title="rafraîchir">↻</button>
|
||||
</div>
|
||||
<pre id="svc-log" class="muted" style="font-size:10px;line-height:1.45;max-height:220px;overflow:auto;white-space:pre-wrap;word-break:break-word;margin:6px 0 0"></pre>
|
||||
<!-- Emplacements : au même endroit que le journal, c'est le panneau où l'on
|
||||
va quand on cherche à comprendre l'état de son installation. -->
|
||||
<div class="row" style="justify-content:space-between;align-items:center;margin-top:12px">
|
||||
<span class="io-cap">Emplacements</span>
|
||||
</div>
|
||||
<div id="paths-msg" class="muted" style="font-size:10px;line-height:1.5"></div>
|
||||
</div>
|
||||
<!-- GPU et RAM décrivent la même chose (les ressources de la machine) :
|
||||
une seule section, deux blocs de statistique dans la même carte. -->
|
||||
@@ -1059,8 +1065,6 @@ button:hover{border-color:var(--dim);color:var(--text)}
|
||||
<div class="slist pad">
|
||||
<div class="row"><button id="upd-check" onclick="checkUpdate()">↥ Vérifier les mises à jour</button></div>
|
||||
<div id="upd-msg" class="muted"></div>
|
||||
<div class="row"><button onclick="showPaths()">📁 Où sont mes fichiers ?</button></div>
|
||||
<div id="paths-msg" class="muted"></div>
|
||||
<div class="row">
|
||||
<button onclick="loadAll()">↻ refresh</button>
|
||||
<button onclick="resetChat()">⌫ clear chat</button>
|
||||
@@ -1635,7 +1639,7 @@ function toggleSvcLog(){
|
||||
if(!box) return;
|
||||
const show = box.style.display==='none';
|
||||
box.style.display = show ? '' : 'none';
|
||||
if(show) loadSvcLog();
|
||||
if(show){ loadSvcLog(); showPaths(); }
|
||||
}
|
||||
async function loadSvcLog(){
|
||||
const el=document.getElementById('svc-log');
|
||||
@@ -1661,18 +1665,16 @@ async function checkUpdate(){
|
||||
}catch(e){ msg.textContent='Erreur réseau'; }
|
||||
b.disabled=false;
|
||||
}
|
||||
// Emplacements : la question « où sont mes fichiers / est-ce que ça a été
|
||||
// installé ? » revient assez souvent pour mériter une réponse dans l'UI.
|
||||
// Emplacements — affichés avec le journal du moteur : c'est le panneau qu'on
|
||||
// ouvre quand on cherche à comprendre l'état de son installation.
|
||||
async function showPaths(){
|
||||
const el=document.getElementById('paths-msg');
|
||||
if(!el) return;
|
||||
el.textContent='…';
|
||||
try{
|
||||
const p=await jget('/api/paths');
|
||||
const rows=[['Données',p.home],['Configuration',p.config],['Fichiers créés par l\'agent',p.workspace],['Mémoire',p.memory],['Programme en cours',p.exe]];
|
||||
el.innerHTML=rows.map(r=>'<div><b>'+r[0]+'</b><br><code>'+escHtml(r[1]||'')+'</code></div>').join('');
|
||||
if(p.exe && p.installed && p.exe!==p.installed){
|
||||
el.innerHTML+='<div style="color:var(--warn);margin-top:8px">Vous exécutez une copie du programme, pas celle installée (<code>'+escHtml(p.installed)+'</code>). La mise à jour ne modifiera que la copie lancée.</div>';
|
||||
}
|
||||
const rows=[['Données',p.home],['Configuration',p.config],['Fichiers créés par l\'IA',p.workspace],['Mémoire',p.memory],['Programme',p.exe]];
|
||||
el.innerHTML=rows.map(r=>'<div style="margin-bottom:4px">'+r[0]+'<br><code style="word-break:break-all">'+escHtml(r[1]||'')+'</code></div>').join('');
|
||||
}catch(e){ el.textContent='Erreur'; }
|
||||
}
|
||||
async function applyUpdate(){
|
||||
|
||||
@@ -44,6 +44,12 @@
|
||||
<button onclick="loadSvcLog()" title="rafraîchir">↻</button>
|
||||
</div>
|
||||
<pre id="svc-log" class="muted" style="font-size:10px;line-height:1.45;max-height:220px;overflow:auto;white-space:pre-wrap;word-break:break-word;margin:6px 0 0"></pre>
|
||||
<!-- Emplacements : au même endroit que le journal, c'est le panneau où l'on
|
||||
va quand on cherche à comprendre l'état de son installation. -->
|
||||
<div class="row" style="justify-content:space-between;align-items:center;margin-top:12px">
|
||||
<span class="io-cap">Emplacements</span>
|
||||
</div>
|
||||
<div id="paths-msg" class="muted" style="font-size:10px;line-height:1.5"></div>
|
||||
</div>
|
||||
<!-- GPU et RAM décrivent la même chose (les ressources de la machine) :
|
||||
une seule section, deux blocs de statistique dans la même carte. -->
|
||||
@@ -231,8 +237,6 @@
|
||||
<div class="slist pad">
|
||||
<div class="row"><button id="upd-check" onclick="checkUpdate()">↥ Vérifier les mises à jour</button></div>
|
||||
<div id="upd-msg" class="muted"></div>
|
||||
<div class="row"><button onclick="showPaths()">📁 Où sont mes fichiers ?</button></div>
|
||||
<div id="paths-msg" class="muted"></div>
|
||||
<div class="row">
|
||||
<button onclick="loadAll()">↻ refresh</button>
|
||||
<button onclick="resetChat()">⌫ clear chat</button>
|
||||
|
||||
@@ -35,7 +35,7 @@ function toggleSvcLog(){
|
||||
if(!box) return;
|
||||
const show = box.style.display==='none';
|
||||
box.style.display = show ? '' : 'none';
|
||||
if(show) loadSvcLog();
|
||||
if(show){ loadSvcLog(); showPaths(); }
|
||||
}
|
||||
async function loadSvcLog(){
|
||||
const el=document.getElementById('svc-log');
|
||||
@@ -61,18 +61,16 @@ async function checkUpdate(){
|
||||
}catch(e){ msg.textContent='Erreur réseau'; }
|
||||
b.disabled=false;
|
||||
}
|
||||
// Emplacements : la question « où sont mes fichiers / est-ce que ça a été
|
||||
// installé ? » revient assez souvent pour mériter une réponse dans l'UI.
|
||||
// Emplacements — affichés avec le journal du moteur : c'est le panneau qu'on
|
||||
// ouvre quand on cherche à comprendre l'état de son installation.
|
||||
async function showPaths(){
|
||||
const el=document.getElementById('paths-msg');
|
||||
if(!el) return;
|
||||
el.textContent='…';
|
||||
try{
|
||||
const p=await jget('/api/paths');
|
||||
const rows=[['Données',p.home],['Configuration',p.config],['Fichiers créés par l\'agent',p.workspace],['Mémoire',p.memory],['Programme en cours',p.exe]];
|
||||
el.innerHTML=rows.map(r=>'<div><b>'+r[0]+'</b><br><code>'+escHtml(r[1]||'')+'</code></div>').join('');
|
||||
if(p.exe && p.installed && p.exe!==p.installed){
|
||||
el.innerHTML+='<div style="color:var(--warn);margin-top:8px">Vous exécutez une copie du programme, pas celle installée (<code>'+escHtml(p.installed)+'</code>). La mise à jour ne modifiera que la copie lancée.</div>';
|
||||
}
|
||||
const rows=[['Données',p.home],['Configuration',p.config],['Fichiers créés par l\'IA',p.workspace],['Mémoire',p.memory],['Programme',p.exe]];
|
||||
el.innerHTML=rows.map(r=>'<div style="margin-bottom:4px">'+r[0]+'<br><code style="word-break:break-all">'+escHtml(r[1]||'')+'</code></div>').join('');
|
||||
}catch(e){ el.textContent='Erreur'; }
|
||||
}
|
||||
async function applyUpdate(){
|
||||
|
||||
Reference in new issue
Block a user