v0.2.10 : mode agent unifie (un seul toggle = shell + skills) + jeu de mot 'ajean' + refonte du header (pastille d'etat, preset, espacements)

This commit is contained in:
nathaninline committed 2026-06-26 12:09:43 +02:00
1 parent 175bbd34d9
commit d3a2f1c428
11 files changed
+199 -207

No files matched your search

+3 -4
View File
@@ -21,7 +21,7 @@ Faire tourner llama.cpp comme un vrai service, ça veut dire d'habitude : trouve
- **ROCm/HIP** (AMD), **Metal** (macOS / Apple Silicon), **Vulkan**, ou repli **CPU**
- `update` récupère le dernier commit, arrête le service le temps de recompiler, puis le redémarre
- **Intégration systemd** — `jean install` écrit l'unité, une règle sudoers `systemctl` sans mot de passe, et les dossiers de données
- **Interface web** (`jean web`) — chat, changement de modèle/preset, activation des skills & tools
- **Interface web** (`jean web`) — chat, changement de modèle/preset, activation du mode agent
- **Chat terminal** (`jean chat`) — réponses en streaming
- **Accès distant chiffré** (`jean link`) — connecte ce serveur à [ajean.link](https://ajean.link) par une connexion **sortante** (aucun port à ouvrir, marche même en CGNAT). Le chat est **chiffré de bout en bout** : le relais ne voit jamais tes conversations (voir [Sécurité — boîte noire](#sécurité--boîte-noire))
- **Presets** (`jean switch`) — garde plusieurs profils `config.env` et bascule entre eux
@@ -119,9 +119,8 @@ Accès distant (ajean.link) :
link code génère un code d'appairage (valable 10 min, usage unique) pour le portail
link status | logout état du lien / oublier le token
Outils côté LLM :
skills [on|off|list] laisse le modèle lire SKILLS/<nom>/SKILL.md
machine [on|off|status] active l'accès machine (le modèle dispose d'un shell complet sur le serveur)
Mode agent :
agent [on|off|status] active TOUS les outils du modèle (shell complet + skills) — un seul interrupteur
Backend (llama.cpp) :
llamacpp install clone + compile llama.cpp (détecte CUDA/ROCm/Metal/CPU), règle BIN
+83
View File
@@ -0,0 +1,83 @@
package main
import (
"fmt"
"os"
)
// Le « mode agent » est l'unique interrupteur qui donne à l'IA l'accès à ses
// outils. Un skill est un outil comme un autre : quand le mode agent est actif,
// l'IA dispose à la fois du shell (run_shell) et de la gestion des skills.
// Plus de drapeaux machine/skills séparés — un seul fichier .agent_enabled.
func agentEnabled() bool {
if _, err := os.Stat(agentFlag()); err == nil {
return true
}
// Migration : si l'un des anciens drapeaux séparés traîne encore, on
// considère le mode agent comme actif (et on le matérialise au prochain set).
if _, err := os.Stat(legacyToolsFlag()); err == nil {
return true
}
if _, err := os.Stat(legacySkillsFlag()); err == nil {
return true
}
return false
}
func setAgentEnabled(on bool) error {
_ = os.MkdirAll(JeanHome(), 0o755)
// On nettoie systématiquement les anciens drapeaux pour ne pas garder un
// état fantôme « à moitié activé » hérité de l'ancien modèle à deux toggles.
_ = os.Remove(legacyToolsFlag())
_ = os.Remove(legacySkillsFlag())
if on {
f, err := os.Create(agentFlag())
if err != nil {
return err
}
return f.Close()
}
if err := os.Remove(agentFlag()); err != nil && !os.IsNotExist(err) {
return err
}
return nil
}
func cmdAgent(args []string) error {
sub := ""
if len(args) > 0 {
sub = args[0]
}
switch sub {
case "on":
if err := setAgentEnabled(true); err != nil {
return err
}
fmt.Println(green("[ok]") + " mode agent activé — l'IA dispose du shell complet et de ses skills")
case "off":
if err := setAgentEnabled(false); err != nil {
return err
}
fmt.Println(green("[ok]") + " mode agent désactivé")
case "", "status", "list":
state := dim("off")
if agentEnabled() {
state = green("on")
}
fmt.Printf("%s état: %s\n", cyan("Mode agent"), state)
fmt.Printf(" outils : run_shell (timeout %ds, max %ds) + skill (read/write/append/delete)\n", toolDefaultTimeout, toolMaxTimeout)
sk := ListSkills()
if len(sk) == 0 {
fmt.Printf(" skills : aucun — crée %s/<nom>/SKILL.md\n", skillsDir())
return nil
}
fmt.Printf(" skills (%s) :\n", skillsDir())
for _, s := range sk {
fmt.Printf(" %s %s\n", bold(s.Name), s.Desc)
}
default:
return fmt.Errorf("usage: jean agent [on|off|status]")
}
return nil
}
+6 -8
View File
@@ -87,14 +87,15 @@ func runShellTool() Tool {
// what the model can do for this conversation, instead of always inheriting the
// machine's global config. Use globalCaps() to fall back to the global config.
type Caps struct {
Tools bool
Skills bool
// Agent = mode agent actif : un seul interrupteur qui débloque TOUS les
// outils de l'IA (shell + skills). Un skill est un outil comme un autre.
Agent bool
}
// globalCaps reads the machine-wide config — the default when a request doesn't
// specify its own capabilities.
func globalCaps() Caps {
return Caps{Tools: toolsEnabled(), Skills: skillsEnabled()}
return Caps{Agent: agentEnabled()}
}
// InjectSkills prepends context system messages to msgs: the decisive-agent
@@ -130,11 +131,8 @@ func InjectSkills(msgs []Message, caps Caps) []Message {
// EnabledTools returns the tools to advertise on the next inference call.
func EnabledTools(caps Caps) []Tool {
tools := []Tool{}
if caps.Skills {
tools = append(tools, skillTool())
}
if caps.Tools {
tools = append(tools, runShellTool())
if caps.Agent {
tools = append(tools, skillTool(), runShellTool())
}
return tools
}
+13 -11
View File
@@ -15,7 +15,7 @@ import (
"strings"
)
const Version = "0.2.9"
const Version = "0.2.10"
func main() {
args := os.Args[1:]
@@ -47,10 +47,11 @@ func main() {
mustExit(cmdWeb(args))
case "link":
mustExit(cmdLink(args))
case "skills":
mustExit(cmdSkills(args))
case "machine", "tools":
mustExit(cmdTools(args))
case "agent", "skills", "machine", "tools":
// « mode agent » unifie l'ancien couple machine + skills : un seul
// interrupteur active TOUS les outils de l'IA (shell + skills). Les
// anciens noms restent acceptés comme alias rétro-compatibles.
mustExit(cmdAgent(args))
case "serve":
mustExit(cmdServe(args))
case "test":
@@ -96,7 +97,7 @@ Presets:
Interaction:
chat [system-prompt] chat terminal streamé
web [PORT] UI web (défaut :8090) — chat + presets + skills + tools
web [PORT] UI web (défaut :8090) — chat + presets + mode agent
Accès distant (ajean.link) :
link <token> enregistre le token et démarre le lien au relais (token = 1re fois / pour le changer)
@@ -106,9 +107,8 @@ Accès distant (ajean.link) :
link (sans argument) affiche l'aide des sous-commandes link
link serve exécute le worker au premier plan (utilisé par jean-link.service ; pendant de 'jean serve')
LLM-side outils:
skills [on|off|list] active la lecture de SKILLS/<nom>/SKILL.md par l'IA
machine [on|off|status] active l'accès machine (l'IA dispose d'un shell complet sur le serveur)
Mode agent:
agent [on|off|status] active TOUS les outils de l'IA (shell complet + skills) — un seul interrupteur
Backend llama.cpp :
llamacpp install clone + compile llama.cpp (détecte CUDA/ROCm/Metal/CPU), pointe BIN dessus
@@ -176,8 +176,10 @@ func readEtcDefault() string {
func confPath() string { return filepath.Join(JeanHome(), "config.env") }
func presetsDir() string { return filepath.Join(JeanHome(), "configs") }
func skillsDir() string { return filepath.Join(JeanHome(), "SKILLS") }
func skillsFlag() string { return filepath.Join(skillsDir(), ".enabled") }
func toolsFlag() string { return filepath.Join(JeanHome(), ".tools_enabled") }
func agentFlag() string { return filepath.Join(JeanHome(), ".agent_enabled") }
// Anciens drapeaux séparés, conservés pour la migration vers le mode agent unifié.
func legacySkillsFlag() string { return filepath.Join(skillsDir(), ".enabled") }
func legacyToolsFlag() string { return filepath.Join(JeanHome(), ".tools_enabled") }
func apiKeyPath() string { return filepath.Join(JeanHome(), ".api_key") }
func serviceName() string {
if n := os.Getenv("JEAN_SERVICE"); n != "" {
Binary file not shown.
Binary file not shown.
+1 -57
View File
@@ -15,26 +15,6 @@ type Skill struct {
Content string
}
func skillsEnabled() bool {
_, err := os.Stat(skillsFlag())
return err == nil
}
func setSkillsEnabled(on bool) error {
_ = os.MkdirAll(skillsDir(), 0o755)
if on {
f, err := os.Create(skillsFlag())
if err != nil {
return err
}
return f.Close()
}
if err := os.Remove(skillsFlag()); err != nil && !os.IsNotExist(err) {
return err
}
return nil
}
// ListSkills walks SKILLS/<name>/SKILL.md, returning name+desc+full content.
func ListSkills() []Skill {
entries, err := os.ReadDir(skillsDir())
@@ -157,7 +137,7 @@ func DeleteSkill(name string) error {
// skillsSystemPrompt returns the lightweight skills directory message to
// prepend to the conversation when skills are enabled.
func skillsSystemPrompt(caps Caps) string {
if !caps.Skills {
if !caps.Agent {
return ""
}
list := ListSkills()
@@ -181,39 +161,3 @@ func skillsSystemPrompt(caps Caps) string {
}
return strings.TrimRight(b.String(), "\n")
}
func cmdSkills(args []string) error {
sub := ""
if len(args) > 0 {
sub = args[0]
}
switch sub {
case "on":
if err := setSkillsEnabled(true); err != nil {
return err
}
fmt.Println(green("[ok]") + " skills activés")
case "off":
if err := setSkillsEnabled(false); err != nil {
return err
}
fmt.Println(green("[ok]") + " skills désactivés")
case "", "list":
state := dim("off")
if skillsEnabled() {
state = green("on")
}
fmt.Printf("%s (%s) état: %s\n", cyan("Skills"), skillsDir(), state)
sk := ListSkills()
if len(sk) == 0 {
fmt.Printf(" (aucun — crée %s/<nom>/SKILL.md)\n", skillsDir())
return nil
}
for _, s := range sk {
fmt.Printf(" %s %s\n", bold(s.Name), s.Desc)
}
default:
return fmt.Errorf("usage: jean skills [on|off|list]")
}
return nil
}
+2 -51
View File
@@ -17,26 +17,6 @@ const (
toolMaxOutput = 8000 // characters of stdout/stderr returned to the model
)
func toolsEnabled() bool {
_, err := os.Stat(toolsFlag())
return err == nil
}
func setToolsEnabled(on bool) error {
_ = os.MkdirAll(JeanHome(), 0o755)
if on {
f, err := os.Create(toolsFlag())
if err != nil {
return err
}
return f.Close()
}
if err := os.Remove(toolsFlag()); err != nil && !os.IsNotExist(err) {
return err
}
return nil
}
// baseSystemPrompt is the always-on system preamble. Structured like pi's
// proven prompt (identity → method → guidelines → concision → date): a concrete,
// procedural prompt gives the model rails so it stops deliberating forever
@@ -47,7 +27,7 @@ func baseSystemPrompt(caps Caps) string {
// No tool access → no agentic preamble. A plain chat model told to "call
// tools immediately" hallucinates textual tool calls (e.g. default_api:bash)
// that leak into the answer. Let the user's own system prompt stand alone.
if !caps.Tools && !caps.Skills {
if !caps.Agent {
return ""
}
var b strings.Builder
@@ -72,7 +52,7 @@ func baseSystemPrompt(caps Caps) string {
// run_shell acts upon (and doesn't claim it has no access to "your PC").
// Returns "" when machine access is off.
func machineSystemPrompt(caps Caps) string {
if !caps.Tools {
if !caps.Agent {
return ""
}
host, _ := os.Hostname()
@@ -156,32 +136,3 @@ func tailRunes(s string, n int) string {
}
return string(r[len(r)-n:])
}
func cmdTools(args []string) error {
sub := ""
if len(args) > 0 {
sub = args[0]
}
switch sub {
case "on":
if err := setToolsEnabled(true); err != nil {
return err
}
fmt.Println(green("[ok]") + " accès machine activé — l'IA dispose d'un shell complet sur le serveur")
case "off":
if err := setToolsEnabled(false); err != nil {
return err
}
fmt.Println(green("[ok]") + " accès machine désactivé")
case "", "status":
state := dim("off")
if toolsEnabled() {
state = green("on")
}
fmt.Printf("%s état: %s\n", cyan("Accès machine"), state)
fmt.Printf(" timeout défaut: %ds, max: %ds\n", toolDefaultTimeout, toolMaxTimeout)
default:
return fmt.Errorf("usage: jean machine [on|off|status]")
}
return nil
}
+62 -40
View File
@@ -30,10 +30,34 @@ body{margin:0;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;background
.preset-meta .qtag,.preset-meta .rtag,.preset-meta .btag{font-size:11px;padding:2px 9px;border-radius:9px}
.preset-meta .qtag svg,.preset-meta .rtag svg,.preset-meta .btag svg{width:11px;height:11px}
}
h1{margin:0 0 4px;font-size:18px;color:var(--accent)}
h1{margin:0;font-size:18px;color:var(--accent)}
#brand{display:inline-flex;align-items:baseline}
.sidehead{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:18px}
/* pastille d'état service, en haut à droite de la barre latérale */
.statuspill{display:inline-flex;align-items:center;gap:6px;font-size:11px;padding:3px 10px;border-radius:999px;border:1px solid var(--border);background:var(--panel);color:var(--dim);font-weight:600;white-space:nowrap}
.statuspill.ok{color:var(--ok);border-color:rgba(63,185,80,.45);background:rgba(63,185,80,.10)}
.statuspill.err{color:var(--err);border-color:rgba(248,81,73,.45);background:rgba(248,81,73,.10)}
.statuspill .dot{width:7px;height:7px;border-radius:50%;background:currentColor}
.statuspill .port{opacity:.6;font-weight:400}
/* en-tête + bouton d'ajout de skill (toujours visible) */
.skillshead{display:flex;align-items:center;justify-content:space-between;margin:12px 0 5px}
.skillshead>span{font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.08em;font-weight:600}
.addbtn{font-size:11px;padding:3px 11px 4px;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:6px;cursor:pointer;line-height:1.2}
.addbtn:hover{border-color:var(--accent);color:var(--accent)}
#brand-a{display:inline-block;line-height:1;max-width:0;opacity:0;overflow:hidden;color:#fff;transition:max-width .35s ease,opacity .35s ease}
#brand.agent #brand-a{max-width:1ch;opacity:1;margin-right:.04em}
/* interrupteur (switch) */
.switchrow{display:flex;align-items:center;gap:9px;font-size:12px;padding:6px 0;cursor:pointer;user-select:none}
.switch{position:relative;display:inline-block;width:36px;height:20px;flex:none}
.switch input{position:absolute;opacity:0;width:0;height:0}
.switch .slider{position:absolute;inset:0;background:var(--border);border-radius:999px;transition:background .25s ease}
.switch .slider::before{content:"";position:absolute;width:14px;height:14px;left:3px;top:3px;background:#fff;border-radius:50%;transition:transform .25s ease;box-shadow:0 1px 2px rgba(0,0,0,.4)}
.switch input:checked + .slider{background:var(--accent)}
.switch input:checked + .slider::before{transform:translateX(16px)}
.switch input:focus-visible + .slider{box-shadow:0 0 0 2px var(--bg),0 0 0 4px var(--accent)}
h2{margin:20px 0 8px;font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.1em;font-weight:600}
details{margin:8px 0;border-top:1px solid var(--border);padding-top:6px}
details>summary{font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.1em;font-weight:600;cursor:pointer;padding:4px 0;list-style:none;display:flex;justify-content:space-between;align-items:center;user-select:none}
details{margin:7px 0;border-top:1px solid var(--border);padding-top:4px}
details>summary{font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.1em;font-weight:600;cursor:pointer;padding:3px 0;list-style:none;display:flex;justify-content:space-between;align-items:center;user-select:none}
details>summary::-webkit-details-marker{display:none}
details>summary::after{content:"▸";transition:transform .15s;color:var(--dim);font-size:10px}
details[open]>summary::after{transform:rotate(90deg)}
@@ -72,6 +96,7 @@ button:disabled{opacity:.5;cursor:not-allowed}
.msg.assistant,.msg.reasoning{white-space:normal}
.msg .body{overflow-wrap:anywhere;word-break:break-word}
.msg.user{align-self:flex-end;background:#1f6feb;color:#fff}
.msg.user .label{color:rgba(255,255,255,.8)}
.msg.assistant{align-self:flex-start;background:var(--panel);border:1px solid var(--border)}
.msg .body>*{margin:1em 0}
.msg .body>*:first-child{margin-top:0}
@@ -145,8 +170,10 @@ button:disabled{opacity:.5;cursor:not-allowed}
<button id="menubtn" onclick="toggleSide()">☰</button>
<div id="backdrop" onclick="toggleSide()"></div>
<div class="side" id="side">
<h1>jean</h1>
<div id="statusline" class="muted"><span id="status-svc">…</span> <span id="status-preset"></span></div>
<div class="sidehead">
<h1 id="brand"><span id="brand-a">a</span>jean</h1>
<span id="status-svc" class="statuspill">…</span>
</div>
<details open><summary>GPU</summary>
<div id="vram" class="muted">…</div>
</details>
@@ -156,12 +183,10 @@ button:disabled{opacity:.5;cursor:not-allowed}
<details><summary>Presets<button class="preset-add" onclick="event.preventDefault();event.stopPropagation();openPreset('')" title="ajouter">+</button></summary>
<div id="presets"></div>
</details>
<details><summary>Accès machine <span id="tools-badge" style="margin-left:auto;font-size:10px"></span></summary>
<label style="display:flex;align-items:center;gap:6px;font-size:12px;padding:4px 0"><input type="checkbox" id="tools-toggle" onchange="toggleTools()"> activer l'accès machine</label>
<div class="muted" style="font-size:11px">⚠️ donne à l'IA un accès shell complet au serveur — elle peut inspecter, lancer des scripts et tout faire via le terminal (timeout 30s, max 300s)</div>
</details>
<details><summary>Skills <span id="skills-badge" style="margin-left:auto;font-size:10px"></span><button class="preset-add" onclick="event.preventDefault();event.stopPropagation();openSkill('')" title="ajouter">+</button></summary>
<label style="display:flex;align-items:center;gap:6px;font-size:12px;padding:4px 0"><input type="checkbox" id="skills-toggle" onchange="toggleSkills()"> activer</label>
<details><summary>Mode agent <span id="agent-badge" style="margin-left:auto;font-size:10px"></span></summary>
<label class="switchrow"><span class="switch"><input type="checkbox" id="agent-toggle" onchange="toggleAgent()"><span class="slider"></span></span> activer le mode agent</label>
<div class="muted" style="font-size:11px">⚠️ active l'<b>accès machine</b> (shell complet sur le serveur) <b>et</b> les <b>skills</b> — l'IA peut inspecter, lancer des scripts et exécuter ses skills (timeout 30s, max 300s)</div>
<div class="skillshead"><span>Skills</span><button class="addbtn" onclick="event.preventDefault();event.stopPropagation();openSkill('')" title="ajouter un skill">+ ajouter</button></div>
<div id="skills-list" class="muted" style="font-size:12px"></div>
</details>
<details><summary>System prompt</summary>
@@ -188,8 +213,11 @@ button:disabled{opacity:.5;cursor:not-allowed}
<div id="composer">
<div id="ctxtrack"><div id="ctx-fill"></div></div>
<div id="ctxmeta">
<span id="ctx-text" class="muted">contexte 0 / 0</span>
<button id="ctx-compact" onclick="compactContext()">compacter</button>
<span id="status-preset" class="muted" style="font-size:10px;letter-spacing:.03em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis"></span>
<span style="display:flex;align-items:center;gap:10px;margin-left:auto">
<span id="ctx-text" class="muted">contexte 0 / 0</span>
<button id="ctx-compact" onclick="compactContext()">compacter</button>
</span>
</div>
<div id="inputbar">
<textarea id="input" placeholder="message…" onkeydown="onKey(event)"></textarea>
@@ -298,8 +326,9 @@ async function jget(u){ const r=await jfetch(u); return r.json(); }
async function jpost(u,b){ const r=await jfetch(u,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(b||{})}); return r.json(); }
async function loadStatus(){
const s=await jget('/api/status');
const t=s.active?'<span class="tag ok">● active</span>':'<span class="tag err">○ '+s.state+'</span>';
document.getElementById('status-svc').innerHTML = t + ' <span class="muted">:'+s.port+'</span>';
const el=document.getElementById('status-svc');
el.className='statuspill '+(s.active?'ok':'err');
el.innerHTML='<span class="dot"></span>'+(s.active?'active':s.state)+' <span class="port">:'+s.port+'</span>';
if(s.ctx){ CTX_MAX=s.ctx; updateCtxMeter(); }
}
// Compteur de contexte : CTX_USED estimé via les stats serveur (prefill+decode
@@ -338,12 +367,8 @@ async function loadPresets(){
// Build via DOM (not string concat) so preset names can contain anything —
// spaces, accents, quotes, < > & — without breaking markup or handlers.
const sp = document.getElementById('status-preset');
sp.innerHTML='';
if(act){
const tag=document.createElement('span');
tag.className='tag'; tag.style.borderColor='var(--accent)'; tag.style.color='var(--accent)';
tag.textContent=act.name; sp.appendChild(tag);
}
sp.textContent = act ? act.name : '';
sp.title = act ? 'preset actif' : '';
const cont=document.getElementById('presets');
cont.innerHTML='';
if(!p.length){ cont.innerHTML='<span class="muted">(aucun)</span>'; return; }
@@ -383,31 +408,28 @@ async function loadPresets(){
cont.appendChild(row);
});
}
async function loadSkills(){
const s=await jget('/api/skills');
document.getElementById('skills-toggle').checked = s.enabled;
document.getElementById('skills-badge').innerHTML = s.enabled
// « Mode agent » = accès machine + skills réunis en un seul interrupteur.
// Quand il est actif, un « a » blanc apparaît en fondu devant « jean » → « ajean ».
async function loadAgent(){
const s=await jget('/api/agent');
const on = s.enabled;
document.getElementById('agent-toggle').checked = on;
document.getElementById('agent-badge').innerHTML = on
? '<span class="tag ok">on</span>' : '<span class="tag" style="opacity:.6">off</span>';
document.getElementById('brand').classList.toggle('agent', on);
document.getElementById('skills-list').innerHTML = (s.skills||[]).length
? s.skills.map(x=>'<div class="preset" style="font-size:12px" onclick="openSkill(\''+x.name+'\')"><span><b style="color:var(--text)">'+x.name+'</b> <span class="muted">— '+x.desc+'</span></span><button onclick="event.stopPropagation();openSkill(\''+x.name+'\')" style="margin:0;padding:2px 8px;font-size:11px">edit</button></div>').join('')
: '<div class="muted">(aucun skill)</div>';
}
async function toggleSkills(){
const on=document.getElementById('skills-toggle').checked;
await jpost('/api/skills/toggle',{on}); loadSkills();
// alias : plusieurs appelants rafraîchissent juste la liste des skills
const loadSkills = loadAgent;
async function toggleAgent(){
const on=document.getElementById('agent-toggle').checked;
if(on && !confirm("Activer le mode agent ? L'IA aura un accès shell complet au serveur et pourra exécuter ses skills.")){ document.getElementById('agent-toggle').checked=false; return; }
await jpost('/api/agent/toggle',{on});
loadAgent();
}
async function loadTools(){
const t=await jget('/api/tools');
document.getElementById('tools-toggle').checked = t.enabled;
document.getElementById('tools-badge').innerHTML = t.enabled
? '<span class="tag ok">on</span>' : '<span class="tag" style="opacity:.6">off</span>';
}
async function toggleTools(){
const on=document.getElementById('tools-toggle').checked;
if(on && !confirm("Activer l'accès machine ? L'IA aura un accès shell complet au serveur.")){ document.getElementById('tools-toggle').checked=false; return; }
await jpost('/api/tools/toggle',{on}); loadTools();
}
async function loadAll(){ await Promise.all([loadStatus(),loadVram(),loadCfg(),loadPresets(),loadSkills(),loadTools()]); }
async function loadAll(){ await Promise.all([loadStatus(),loadVram(),loadCfg(),loadPresets(),loadAgent()]); }
async function act(a){ toast(a+'…'); await jpost('/api/'+a); setTimeout(loadAll,1500); }
function openBenchModal(){ document.getElementById('bench-modal').style.display = 'flex'; }
function closeBenchModal(){ document.getElementById('bench-modal').style.display = 'none'; }
+3 -3
View File
@@ -3,13 +3,13 @@
"FileVersion": {
"Major": 0,
"Minor": 2,
"Patch": 9,
"Patch": 10,
"Build": 0
},
"ProductVersion": {
"Major": 0,
"Minor": 2,
"Patch": 9,
"Patch": 10,
"Build": 0
},
"FileFlagsMask": "3f",
@@ -25,7 +25,7 @@
"LegalCopyright": "Copyright (c) 2026 Jean contributors. MIT License.",
"OriginalFilename": "jean.exe",
"ProductName": "Jean",
"ProductVersion": "0.2.9",
"ProductVersion": "0.2.10",
"Comments": "https://github.com/nathaninline/jean — projet open source (MIT)"
},
"VarFileInfo": {
+26 -33
View File
@@ -83,13 +83,18 @@ func newWebMux() *http.ServeMux {
api("/api/preset", handlePreset)
api("/api/preset/save", handlePresetSave)
api("/api/preset/delete", handlePresetDelete)
api("/api/skills", handleSkills)
api("/api/skills/toggle", handleSkillsToggle)
api("/api/agent", handleAgent)
api("/api/agent/toggle", handleAgentToggle)
// Alias rétro-compat : l'ancien portail ajean.link (dépôt jean-relay) pilote
// encore l'agent via /api/tools* et /api/skills/toggle à travers le tunnel E2E.
// On les mappe sur le mode agent unifié le temps que le portail soit mis à jour.
api("/api/tools", handleAgent)
api("/api/tools/toggle", handleAgentToggle)
api("/api/skills", handleAgent)
api("/api/skills/toggle", handleAgentToggle)
api("/api/skill", handleSkill)
api("/api/skill/save", handleSkillSave)
api("/api/skill/delete", handleSkillDelete)
api("/api/tools", handleTools)
api("/api/tools/toggle", handleToolsToggle)
api("/api/switch", handleSwitch)
api("/api/start", svcHandler("start"))
api("/api/stop", svcHandler("stop"))
@@ -419,25 +424,27 @@ func handlePresetDelete(w http.ResponseWriter, r *http.Request) {
sendJSON(w, 200, map[string]any{"ok": true, "modelDeleted": modelDeleted, "modelError": modelErr})
}
func handleSkills(w http.ResponseWriter, r *http.Request) {
// handleAgent renvoie l'état du mode agent ET la liste des skills (qui sont des
// outils gérés sous ce même interrupteur) — un seul aller-retour pour l'UI.
func handleAgent(w http.ResponseWriter, r *http.Request) {
sk := ListSkills()
out := []map[string]any{}
for _, s := range sk {
out = append(out, map[string]any{"name": s.Name, "desc": s.Desc})
}
sendJSON(w, 200, map[string]any{"enabled": skillsEnabled(), "skills": out})
sendJSON(w, 200, map[string]any{"enabled": agentEnabled(), "skills": out})
}
func handleSkillsToggle(w http.ResponseWriter, r *http.Request) {
func handleAgentToggle(w http.ResponseWriter, r *http.Request) {
var req struct {
On bool `json:"on"`
}
_ = json.NewDecoder(r.Body).Decode(&req)
if err := setSkillsEnabled(req.On); err != nil {
if err := setAgentEnabled(req.On); err != nil {
sendJSON(w, 500, map[string]any{"ok": false, "error": err.Error()})
return
}
sendJSON(w, 200, map[string]any{"ok": true, "enabled": skillsEnabled()})
sendJSON(w, 200, map[string]any{"ok": true, "enabled": agentEnabled()})
}
func handleSkill(w http.ResponseWriter, r *http.Request) {
@@ -480,22 +487,6 @@ func handleSkillDelete(w http.ResponseWriter, r *http.Request) {
sendJSON(w, 200, map[string]any{"ok": true})
}
func handleTools(w http.ResponseWriter, r *http.Request) {
sendJSON(w, 200, map[string]any{"enabled": toolsEnabled()})
}
func handleToolsToggle(w http.ResponseWriter, r *http.Request) {
var req struct {
On bool `json:"on"`
}
_ = json.NewDecoder(r.Body).Decode(&req)
if err := setToolsEnabled(req.On); err != nil {
sendJSON(w, 500, map[string]any{"ok": false, "error": err.Error()})
return
}
sendJSON(w, 200, map[string]any{"ok": true, "enabled": toolsEnabled()})
}
func handleSwitch(w http.ResponseWriter, r *http.Request) {
var req struct {
N int `json:"n"`
@@ -576,9 +567,11 @@ func handleBenchLast(w http.ResponseWriter, r *http.Request) {
type chatReq struct {
Messages []Message `json:"messages"`
Temperature float64 `json:"temperature"`
// Optional per-request capability overrides (used by ajean.link agents,
// which carry their own tools/skills toggles). nil = inherit the
// machine's global config.
// Optional per-request override of the agent mode (used by ajean.link
// agents, which carry their own toggle). nil = inherit the machine's
// global config. Tools/Skills sont conservés pour la rétro-compat des
// anciens clients relais : l'un OU l'autre à true active le mode agent.
Agent *bool `json:"agent"`
Tools *bool `json:"tools"`
Skills *bool `json:"skills"`
}
@@ -623,11 +616,11 @@ func runChatStream(ctx context.Context, body chatReq, emit func(map[string]any)
body.Temperature = 0.7
}
caps := globalCaps()
if body.Tools != nil {
caps.Tools = *body.Tools
}
if body.Skills != nil {
caps.Skills = *body.Skills
if body.Agent != nil {
caps.Agent = *body.Agent
} else if body.Tools != nil || body.Skills != nil {
// rétro-compat : anciens clients qui envoyaient deux drapeaux séparés
caps.Agent = (body.Tools != nil && *body.Tools) || (body.Skills != nil && *body.Skills)
}
msgs := InjectSkills(body.Messages, caps)
extra, _ := runChat(ctx, msgs, body.Temperature, caps, func(ev StreamEvent) bool {