mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
Mode Code : agent de code avec critères, vérification et LSP
Sélecteur Chat|Code par discussion dans le pied du composeur ; en mode chat, une détection serveur suggère la bascule (puce ignorable, jamais automatique). Conception reprise d'OpenFox (MIT), réécrite en Go — voir NOTICE.md. - Outils fichiers : read (lignes numérotées, borné), grep, glob, et le tracker « lu avant d'écrire » qui refuse write/edit sur un fichier non lu ou modifié depuis la lecture ; edit préserve les fins de ligne CRLF. - Politique d'exécution : commandes catastrophiques refusées (rm -rf /, mkfs, reboot…), chemins bornés au dossier de la discussion en mode code, mutex par fichier. - Critères d'acceptation : contrat posé via l'outil criteria (éditable dans l'UI), passe de vérification indépendante sur contexte isolé — seule habilitée à marquer « passed » — puis corrections plafonnées. - Rôles embarqués (agents/*.md) : builder, planner, verifier, explorer, code-reviewer ; badge de rôle dans le fil. - LSP : gopls / typescript-language-server / pyright (inclus dans l'image), diagnostics injectés dans le retour de write/edit. - Git natif : git_status, git_diff, git_clone (borné à la discussion). - Jobs d'arrière-plan bash_bg/bash_tail (serveur de dev, build long). - Auto-retry : un appel d'outil écrit en texte (default_api:…, <tool_call>…) relance le tour une fois avec consigne corrective. - Outil ask : question à choix rendue en carte à boutons. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c6cc7d300b
commit
901d518f70
39 files changed
+3202
-8
No files matched your search
+199
-1
@@ -1993,6 +1993,39 @@ html[data-files="1"] #files-btn{color:var(--accent)}
|
||||
dernière marge. */
|
||||
.perf-row{margin:0 0 12px}
|
||||
.perf-sub{padding:0 0 10px}
|
||||
|
||||
/* --- Mode Chat/Code (20-mode.js) ------------------------------------------
|
||||
Sélecteur segmenté dans le pied de la carte du composeur, puce de
|
||||
suggestion, panneau des critères, badge de rôle, carte de question. */
|
||||
#mode-seg{display:inline-flex;border:1px solid var(--border);border-radius:3px;overflow:hidden;flex:none}
|
||||
#mode-seg button{border:0;background:transparent;color:var(--dim);font-size:10px;letter-spacing:.04em;text-transform:uppercase;padding:2px 8px;cursor:pointer;font-family:inherit}
|
||||
#mode-seg button.on{background:var(--accent-bg);color:var(--accent)}
|
||||
#mode-seg button:not(.on):hover{color:var(--text)}
|
||||
|
||||
#code-hint{display:flex;align-items:center;gap:8px;margin:0 auto 6px;max-width:820px;padding:6px 10px;border:1px solid var(--border);border-left:2px solid var(--accent);border-radius:3px;background:var(--panel);color:var(--dim);font-size:12px}
|
||||
#code-hint button{border:1px solid var(--accent);background:transparent;color:var(--accent);border-radius:3px;padding:2px 8px;font-size:12px;cursor:pointer;font-family:inherit}
|
||||
#code-hint button:hover{background:var(--accent-bg)}
|
||||
#code-hint #code-hint-no{border-color:var(--border);color:var(--dim);margin-left:auto}
|
||||
|
||||
#criteria-box{margin:0 auto 6px;max-width:820px;border:1px solid var(--border);border-radius:3px;background:var(--panel);font-size:12px;overflow:hidden}
|
||||
#criteria-box.allpass{border-left:2px solid var(--accent)}
|
||||
#criteria-head{padding:5px 10px;color:var(--dim);cursor:pointer;user-select:none;letter-spacing:.03em}
|
||||
#criteria-box.allpass #criteria-head{color:var(--accent)}
|
||||
#criteria-box.folded #criteria-list{display:none}
|
||||
#criteria-list{list-style:none;margin:0;padding:0 10px 6px}
|
||||
#criteria-list li{padding:2px 0;color:var(--text)}
|
||||
#criteria-list li.crit-passed{color:var(--accent)}
|
||||
#criteria-list li.crit-failed{color:var(--err,#b3564d)}
|
||||
#criteria-list li.crit-pending{color:var(--dim)}
|
||||
|
||||
.rolebadge{display:inline-block;margin-left:6px;padding:0 6px;border:1px solid var(--accent);border-radius:3px;color:var(--accent);font-size:9px;letter-spacing:.05em;text-transform:uppercase;vertical-align:middle}
|
||||
.msg[data-agent-role="verifier"]{border-left:2px solid var(--accent)}
|
||||
|
||||
.askcard .body{display:flex;flex-direction:column;gap:8px}
|
||||
.askopts{display:flex;flex-wrap:wrap;gap:6px}
|
||||
.askopts button{border:1px solid var(--accent);background:transparent;color:var(--accent);border-radius:3px;padding:4px 10px;font-size:13px;cursor:pointer;font-family:inherit}
|
||||
.askopts button:hover:not(:disabled){background:var(--accent-bg)}
|
||||
.askopts button:disabled{opacity:.5;cursor:default}
|
||||
</style></head><body>
|
||||
<button id="menubtn" onclick="toggleSide()" title="menu" aria-label="ouvrir le menu"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16"/><path d="M4 12h16"/><path d="M4 17h16"/></svg></button>
|
||||
<!-- Installation d'un moteur en cours : le détail vit dans le panneau, qui est
|
||||
@@ -2358,6 +2391,14 @@ html[data-files="1"] #files-btn{color:var(--accent)}
|
||||
<div id="dropveil" aria-hidden="true"><div>Déposer pour joindre au message</div></div>
|
||||
<div id="composer">
|
||||
<div id="ctxmeta">
|
||||
<!-- Mode de la discussion : Chat (assistant généraliste) ou Code (agent
|
||||
de code : critères, vérification, LSP). Dans le pied de carte, pas
|
||||
dans la barre de saisie déjà chargée — ça se règle par discussion,
|
||||
pas par message. -->
|
||||
<div id="mode-seg" role="group" aria-label="Mode de la discussion">
|
||||
<button type="button" data-mode="chat" class="on" onclick="setMode('chat')">Chat</button>
|
||||
<button type="button" data-mode="code" onclick="setMode('code')">Code</button>
|
||||
</div>
|
||||
<div id="ctxtrack"><div id="ctx-fill"></div></div>
|
||||
<span id="status-preset" class="muted" style="font-size:10px;letter-spacing:.03em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis"></span>
|
||||
<!-- Pied de carte, à droite : le décompte de contexte, compacter, puis le
|
||||
@@ -5134,9 +5175,13 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
});
|
||||
});
|
||||
|
||||
// Badge de rôle du mode code (builder / verifier) : posé par 09-stream.js à
|
||||
// réception d'un delta {role}, repris par addMsg sur les bulles suivantes.
|
||||
let ROLE_BADGE='';
|
||||
function addMsg(role, text){
|
||||
const el=document.createElement('div');
|
||||
el.className='msg '+role;
|
||||
if(ROLE_BADGE && (role==='assistant'||role==='reasoning'||role==='tool')) el.dataset.agentRole=ROLE_BADGE;
|
||||
const collapsible = (role==='reasoning' || role==='tool');
|
||||
// .body must be a real block so <p>/<pre>/<ul> margins behave properly.
|
||||
if(collapsible){
|
||||
@@ -5150,6 +5195,11 @@ function addMsg(role, text){
|
||||
if((role==='user'||role==='assistant') && typeof paintLabel==='function'){
|
||||
paintLabel(el.querySelector('.label'), role);
|
||||
}
|
||||
// Badge visible sur la bulle assistant pendant une passe de rôle.
|
||||
if(ROLE_BADGE && role==='assistant'){
|
||||
const b=document.createElement('span'); b.className='rolebadge'; b.textContent=ROLE_BADGE==='verifier'?'vérification':ROLE_BADGE;
|
||||
el.querySelector('.label').appendChild(b);
|
||||
}
|
||||
}
|
||||
el.querySelector('.body').textContent=text;
|
||||
// Bulles techniques (raisonnement, outils, mémoire) : elles se posent CÔTE À
|
||||
@@ -5804,7 +5854,14 @@ function handleDelta(d){
|
||||
// serveur) : on nettoie l'écran et on resynchronise la liste, car la bascule
|
||||
// a pu être déclenchée depuis un autre appareil. Les fichiers suivent : ils
|
||||
// appartiennent à la discussion, le panneau doit changer avec elle.
|
||||
if(d.reset!==undefined){ stopWorkTimer(); PENDING=null; document.getElementById('chat').innerHTML=''; newTurn(); setCtxUsed(0); lastSeq=0; setBusy(false); if(typeof loadConversations==='function') loadConversations(); if(typeof filesOnConvChange==='function') filesOnConvChange(); return; }
|
||||
if(d.reset!==undefined){ stopWorkTimer(); PENDING=null; document.getElementById('chat').innerHTML=''; newTurn(); setCtxUsed(0); lastSeq=0; setBusy(false); if(typeof loadConversations==='function') loadConversations(); if(typeof filesOnConvChange==='function') filesOnConvChange(); if(typeof modeOnConvChange==='function') modeOnConvChange(); return; }
|
||||
// --- Mode code (20-mode.js) -----------------------------------------------
|
||||
if(d.mode!==undefined){ if(typeof applyModeDelta==='function') applyModeDelta(d.mode); return; }
|
||||
if(d.code_hint){ if(typeof showCodeHint==='function') showCodeHint(); return; }
|
||||
if(d.criteria!==undefined){ if(typeof renderCriteria==='function') renderCriteria(d.criteria); return; }
|
||||
if(d.role!==undefined){ ROLE_BADGE = d.role || ''; T.contentEl=null; T.reasonEl=null; return; }
|
||||
if(d.ask){ if(typeof renderAskCard==='function') renderAskCard(d.ask); return; }
|
||||
if(d.verify_done){ if(typeof onVerifyDone==='function') onVerifyDone(d.verify_done); return; }
|
||||
if(d.user!==undefined){
|
||||
newTurn();
|
||||
let el=PENDING;
|
||||
@@ -7740,4 +7797,145 @@ function bindZoom(img, name){
|
||||
img.dataset.zoom='1';
|
||||
img.addEventListener('click', ()=>openLightbox(img.currentSrc||img.src, name||img.alt||''));
|
||||
}
|
||||
// 20-mode.js — le mode Chat/Code : sélecteur du composeur, puce de suggestion,
|
||||
// panneau des critères d'acceptation, carte de question (outil ask).
|
||||
// La bascule est PAR DISCUSSION et vit côté serveur (/api/chat/mode) : tous
|
||||
// les appareils voient le même mode, comme le fil lui-même.
|
||||
|
||||
let CURRENT_MODE = 'chat';
|
||||
|
||||
function modeSeg(){ return document.getElementById('mode-seg'); }
|
||||
|
||||
function paintMode(){
|
||||
const seg = modeSeg();
|
||||
if(!seg) return;
|
||||
seg.querySelector('[data-mode="chat"]').classList.toggle('on', CURRENT_MODE!=='code');
|
||||
seg.querySelector('[data-mode="code"]').classList.toggle('on', CURRENT_MODE==='code');
|
||||
// Le panneau des critères n'a de sens qu'en mode code.
|
||||
const box = document.getElementById('criteria-box');
|
||||
if(box && CURRENT_MODE!=='code') box.style.display='none';
|
||||
}
|
||||
|
||||
// Bascule demandée depuis CE navigateur.
|
||||
async function setMode(mode){
|
||||
if(mode===CURRENT_MODE) return;
|
||||
try{
|
||||
const r = await jfetch('/api/chat/mode', {method:'POST', body: JSON.stringify({mode})});
|
||||
const j = await r.json();
|
||||
if(j.ok){ CURRENT_MODE = j.mode; paintMode(); hideCodeHint(); }
|
||||
}catch(e){ toast('bascule impossible : '+e); }
|
||||
}
|
||||
|
||||
// Bascule reçue du serveur (autre appareil, ou écho de la nôtre).
|
||||
function applyModeDelta(mode){
|
||||
CURRENT_MODE = mode==='code' ? 'code' : 'chat';
|
||||
paintMode();
|
||||
if(CURRENT_MODE==='code') hideCodeHint();
|
||||
}
|
||||
|
||||
// Recharge le mode + critères au chargement et à chaque bascule de discussion.
|
||||
async function modeOnConvChange(){
|
||||
try{
|
||||
const r = await jfetch('/api/chat/mode');
|
||||
const j = await r.json();
|
||||
if(!j.ok) return;
|
||||
CURRENT_MODE = j.mode==='code' ? 'code' : 'chat';
|
||||
paintMode();
|
||||
renderCriteria(j.criteria||[]);
|
||||
hideCodeHint();
|
||||
}catch(e){}
|
||||
}
|
||||
|
||||
// --- Puce « passer en mode Code ? » -----------------------------------------
|
||||
// Émise par le serveur (delta code_hint) quand un message de mode chat
|
||||
// ressemble à une tâche de code. Ignorable, jamais bloquante.
|
||||
|
||||
function showCodeHint(){
|
||||
if(CURRENT_MODE==='code' || document.getElementById('code-hint')) return;
|
||||
const composer = document.getElementById('composer');
|
||||
if(!composer) return;
|
||||
const chip = document.createElement('div');
|
||||
chip.id = 'code-hint';
|
||||
chip.innerHTML = 'ça ressemble à une tâche de code — <button type="button" id="code-hint-yes">passer en mode Code</button><button type="button" id="code-hint-no" title="ignorer" aria-label="ignorer">✕</button>';
|
||||
composer.parentNode.insertBefore(chip, composer);
|
||||
chip.querySelector('#code-hint-yes').onclick = ()=>{ setMode('code'); };
|
||||
chip.querySelector('#code-hint-no').onclick = hideCodeHint;
|
||||
}
|
||||
|
||||
function hideCodeHint(){
|
||||
const chip = document.getElementById('code-hint');
|
||||
if(chip) chip.remove();
|
||||
}
|
||||
|
||||
// --- Panneau des critères ----------------------------------------------------
|
||||
// Rendu par les deltas {criteria:[...]} (outil criteria, passe de vérif, ou
|
||||
// édition manuelle). Repliable ; en-tête = compteur passés / total.
|
||||
|
||||
function renderCriteria(list){
|
||||
let box = document.getElementById('criteria-box');
|
||||
if(!list || !list.length){ if(box) box.remove(); return; }
|
||||
if(CURRENT_MODE!=='code'){ if(box) box.remove(); return; }
|
||||
if(!box){
|
||||
const composer = document.getElementById('composer');
|
||||
if(!composer) return;
|
||||
box = document.createElement('div');
|
||||
box.id = 'criteria-box';
|
||||
box.innerHTML = '<div id="criteria-head"></div><ul id="criteria-list"></ul>';
|
||||
composer.parentNode.insertBefore(box, composer);
|
||||
box.querySelector('#criteria-head').onclick = ()=> box.classList.toggle('folded');
|
||||
}
|
||||
box.style.display='';
|
||||
const passed = list.filter(c=>c.status==='passed').length;
|
||||
box.querySelector('#criteria-head').textContent =
|
||||
'critères ' + passed + '/' + list.length + (passed===list.length ? ' ✓' : '');
|
||||
box.classList.toggle('allpass', passed===list.length);
|
||||
const ul = box.querySelector('#criteria-list');
|
||||
ul.innerHTML='';
|
||||
for(const c of list){
|
||||
const li = document.createElement('li');
|
||||
li.className = 'crit-'+(c.status||'pending');
|
||||
const mark = c.status==='passed' ? '✓' : (c.status==='failed' ? '✗' : '○');
|
||||
li.textContent = mark+' '+c.text + (c.note ? ' — '+c.note : '');
|
||||
ul.appendChild(li);
|
||||
}
|
||||
}
|
||||
|
||||
function onVerifyDone(status){
|
||||
if(status==='passed') toast('vérification : tous les critères passent ✓');
|
||||
}
|
||||
|
||||
// --- Carte de question (outil ask) ------------------------------------------
|
||||
// Le modèle a posé une question structurée et clos son tour : la carte propose
|
||||
// ses options en boutons ; cliquer envoie la réponse comme un message normal.
|
||||
|
||||
function renderAskCard(ask){
|
||||
if(!ask || !ask.question) return;
|
||||
removeTyping();
|
||||
const el = addMsg('assistant', '');
|
||||
el.classList.add('askcard');
|
||||
const body = el.querySelector('.body');
|
||||
body.textContent = ask.question;
|
||||
if(ask.options && ask.options.length){
|
||||
const row = document.createElement('div');
|
||||
row.className = 'askopts';
|
||||
for(const opt of ask.options){
|
||||
const b = document.createElement('button');
|
||||
b.type = 'button';
|
||||
b.textContent = opt;
|
||||
b.onclick = ()=>{
|
||||
// La réponse part comme un message utilisateur ordinaire.
|
||||
const input = document.getElementById('input');
|
||||
input.value = opt;
|
||||
send();
|
||||
row.querySelectorAll('button').forEach(x=>x.disabled=true);
|
||||
};
|
||||
row.appendChild(b);
|
||||
}
|
||||
body.appendChild(row);
|
||||
}
|
||||
scrollMaybe();
|
||||
}
|
||||
|
||||
// Init : état du mode au chargement de la page.
|
||||
window.addEventListener('DOMContentLoaded', modeOnConvChange);
|
||||
</script></body></html>
|
||||
Reference in new issue
Block a user