feat: menu + — Compétences-skills « Gérer » + « Parcourir » — phase 2/8 (v7.52.0)
- Sous-menu « Compétences-skills » : 11 skills builtin + skills enregistrés,
libellés affichés « Deep research » (/research) et « Skill-creator »
(/create-new-skill) via FD_SKILL_ALIAS (slugs intacts) ; le clic épingle le
skill au contexte, même chemin que la palette /.
- « Gérer les compétences » : section du menu + formulaire intégré (nom,
description, prompt) — création, édition, suppression (chip épinglé nettoyé),
export JSON téléchargé, import fichier avec overwrite (pas de doublon).
- « Parcourir les compétences » : section galerie filtrable (nom/description/
slug), installation en 1 clic (installGallerySkill réutilisé).
- PATCH /api/agent/skills/{skill_id} créé : édition partielle, whitelist de
colonnes, 400/401/404/409. OpenAPI régénéré : 515 chemins, version 7.52.0.
- En-tête ← + titre de section dans le menu (navigation à 3 niveaux).
- Fix : épingler un skill enregistré n'efface plus le message déjà tapé
(_pinDbSkill ne vide le composer que s'il est vide).
- Tests : tests/test_v752_skills_menu.py (7) + test_v751 adapté. Suite
complète 1280 verts (-n auto), ruff 0, eslint 0 problème.
This commit is contained in:
+179
-9
@@ -38,7 +38,7 @@
|
||||
{key:'files', icon:'📎', label:'Ajouter des fichiers ou répertoires',
|
||||
sub:'Rechercher ou parcourir le workspace', action:'files', chev:true},
|
||||
{key:'skills', icon:'✨', label:'Compétences-skills',
|
||||
sub:'Deep research, Skill-creator… — bientôt (phase 2)', disabled:true},
|
||||
sub:'Deep research, Skill-creator… · Gérer · Parcourir', action:'skills', chev:true},
|
||||
{key:'connectors', icon:'🔌', label:'Connecteurs',
|
||||
sub:'Parcourir / connecteur personnalisé — bientôt (phase 5)', disabled:true},
|
||||
{key:'canvases', icon:'🎨', label:'Design System – Canevas',
|
||||
@@ -54,6 +54,8 @@
|
||||
{key:'f-browse', icon:'📁', label:'Parcourir…',
|
||||
sub:'Dossiers et fichiers du workspace', action:'browse', chev:true}
|
||||
];
|
||||
// Libellés affichés des skills builtin (le slug /commande reste inchangé).
|
||||
var FD_SKILL_ALIAS = {'research': 'Deep research', 'create-new-skill': 'Skill-creator'};
|
||||
|
||||
function agentPanel(){
|
||||
return {
|
||||
@@ -69,6 +71,8 @@
|
||||
mentionFocus: -1, _mentionFrom: null, _mentionTimer: null, _atNode: null,
|
||||
plusOpen: false, plusSection: 'root', plusFocus: -1, plusLoading: false,
|
||||
plusBrowse: [], plusCrumb: [],
|
||||
skillForm: {id: null, name: '', description: '', prompt_template: ''},
|
||||
galleryFilter: '',
|
||||
cmdFocus: -1, slashDismiss: false,
|
||||
toastMsg: '', toastErr: false, _toastTimer: null,
|
||||
_livePh: null,
|
||||
@@ -493,7 +497,8 @@
|
||||
var pin = {key:'skill-'+id, kind:'skill', icon: icon || '✨',
|
||||
label: String(name || '').replace(/^\//, ''), token:'skill:'+id, skillId:id};
|
||||
this.contextChips.push(pin);
|
||||
this.clearComposer();
|
||||
// Ne jamais effacer un message déjà tapé (le token s'ajoute à la fin).
|
||||
if(!this.composerText().trim()) this.clearComposer();
|
||||
this._insertToken({token:pin.token, label:pin.label, icon:pin.icon, kind:'skill'});
|
||||
this.toast('Skill épinglé — décrivez votre demande, il sera appliqué à l\u2019envoi.');
|
||||
},
|
||||
@@ -1113,18 +1118,78 @@
|
||||
this.plusFocus = -1;
|
||||
},
|
||||
get plusTitle(){
|
||||
if(this.plusSection === 'files') return 'Fichiers & répertoires';
|
||||
if(this.plusSection === 'browse') return 'Parcourir le workspace';
|
||||
var s = this.plusSection;
|
||||
if(s === 'files') return 'Fichiers & répertoires';
|
||||
if(s === 'browse') return 'Parcourir le workspace';
|
||||
if(s === 'skills') return 'Compétences-skills';
|
||||
if(s === 'skills-manage') return 'Gérer les compétences';
|
||||
if(s === 'skills-gallery') return 'Parcourir les compétences';
|
||||
if(s === 'skills-edit') return this.skillForm.id ? 'Modifier le skill' : 'Nouveau skill';
|
||||
return 'Ajouter au contexte';
|
||||
},
|
||||
get plusItems(){
|
||||
if(this.plusSection === 'files') return FD_PLUS_FILES;
|
||||
if(this.plusSection === 'browse') return this.plusBrowse;
|
||||
var s = this.plusSection;
|
||||
if(s === 'files') return FD_PLUS_FILES;
|
||||
if(s === 'browse') return this.plusBrowse;
|
||||
if(s === 'skills') return this._skillsItems();
|
||||
if(s === 'skills-manage') return this._manageItems();
|
||||
if(s === 'skills-gallery') return this._galleryItems();
|
||||
if(s === 'skills-edit') return []; // formulaire, pas de liste
|
||||
return FD_PLUS_MENU;
|
||||
},
|
||||
// Sous-menu Compétences : builtin (alias affiché) + installés, puis les 2 actions.
|
||||
_skillsItems(){
|
||||
var out = [];
|
||||
FD_SKILLS.forEach(function(c){
|
||||
var slug = String(c.cmd).replace(/^\//, '');
|
||||
out.push({key:'bi-'+slug, icon:c.icon, label:FD_SKILL_ALIAS[slug] || c.cmd,
|
||||
sub:c.desc, action:'skill',
|
||||
suggestion:{key:'bi-'+slug, cmd:c.cmd, icon:c.icon, template:c.template, _type:'builtin'}});
|
||||
});
|
||||
(this.skills || []).forEach(function(sk){
|
||||
out.push({key:'db-'+sk.id, icon:'✨', label:sk.name,
|
||||
sub:sk.description || 'Skill enregistré', action:'skill',
|
||||
suggestion:{key:'db-'+sk.id, cmd:'/'+sk.name, icon:'✨', _type:'db', skillId:sk.id}});
|
||||
});
|
||||
out.push({key:'sep-actions', _sep:'Gérer / parcourir'});
|
||||
out.push({key:'a-manage', icon:'🛠️', label:'Gérer les compétences',
|
||||
sub:'Créer, modifier, supprimer, exporter / importer', action:'skills-manage', chev:true});
|
||||
out.push({key:'a-browse', icon:'🗂️', label:'Parcourir les compétences',
|
||||
sub:'Galerie de presets installables en 1 clic', action:'skills-gallery', chev:true});
|
||||
return out;
|
||||
},
|
||||
// Sous-section Gérer : liste des skills + création + import.
|
||||
_manageItems(){
|
||||
var out = [];
|
||||
(this.skills || []).forEach(function(sk){
|
||||
out.push({key:'m-'+sk.id, icon:'✨', label:sk.name,
|
||||
sub:sk.description || 'Cliquer pour modifier', action:'skill-edit', chev:true, skillId:sk.id});
|
||||
});
|
||||
if(!out.length) out.push({key:'m-none', _sep:'Aucun skill enregistré'});
|
||||
out.push({key:'m-new', icon:'+', label:'Nouveau skill',
|
||||
sub:'Nom, description, prompt', action:'skill-new', chev:true});
|
||||
out.push({key:'m-sep', _sep:'Import'});
|
||||
out.push({key:'m-import', icon:'📥', label:'Importer un skill (JSON)',
|
||||
sub:'Document exporté depuis une autre instance', action:'skill-import'});
|
||||
return out;
|
||||
},
|
||||
// Sous-section Parcourir : presets de la galerie, filtrables.
|
||||
_galleryItems(){
|
||||
var f = String(this.galleryFilter || '').toLowerCase();
|
||||
var out = [];
|
||||
(this.gallery || []).forEach(function(g){
|
||||
var hay = (String(g.name || '') + ' ' + (g.description || '') + ' ' + (g.slug || '')).toLowerCase();
|
||||
if(f && hay.indexOf(f) < 0) return;
|
||||
out.push({key:'g-'+g.slug, icon:g.icon || '📦', label:g.name,
|
||||
sub:g.description || 'Preset de la galerie', action:'skill-install',
|
||||
slug:g.slug, skillName:g.name});
|
||||
});
|
||||
if(!out.length) out.push({key:'g-none', _sep: f ? 'Aucun résultat' : 'Galerie vide'});
|
||||
return out;
|
||||
},
|
||||
_plusSelectable(){
|
||||
var out = [];
|
||||
this.plusItems.forEach(function(it, i){ if(!it.disabled) out.push(i); });
|
||||
this.plusItems.forEach(function(it, i){ if(!it.disabled && !it._sep) out.push(i); });
|
||||
return out;
|
||||
},
|
||||
movePlusFocus(delta){
|
||||
@@ -1142,12 +1207,41 @@
|
||||
if(el && el.scrollIntoView){ menu.scrollTop = el.offsetTop - menu.clientHeight / 2 + el.clientHeight / 2; }
|
||||
},
|
||||
plusAction(it){
|
||||
if(!it || it.disabled) return;
|
||||
if(!it || it.disabled || it._sep) return;
|
||||
if(it.action === 'files'){ this.plusSection = 'files'; this.plusFocus = 0; return; }
|
||||
if(it.action === 'search'){ this.closePlus(); this.openMentionPicker('+'); return; }
|
||||
if(it.action === 'browse'){
|
||||
this.plusSection = 'browse'; this.plusCrumb = []; this.plusLoadBrowse(null); return;
|
||||
}
|
||||
// ── Compétences (v7.52.0) ──
|
||||
if(it.action === 'skills'){ this.plusSection = 'skills'; this.plusFocus = 0; return; }
|
||||
if(it.action === 'skill'){ this.closePlus(); this.useSuggestion(it.suggestion); return; }
|
||||
if(it.action === 'skills-manage'){
|
||||
this.plusSection = 'skills-manage'; this.plusFocus = 0; return;
|
||||
}
|
||||
if(it.action === 'skills-gallery'){
|
||||
this.galleryFilter = ''; this.fetchSkills();
|
||||
this.plusSection = 'skills-gallery'; this.plusFocus = 0; return;
|
||||
}
|
||||
if(it.action === 'skill-new'){
|
||||
this.skillForm = {id:null, name:'', description:'', prompt_template:''};
|
||||
this.plusSection = 'skills-edit'; this.plusFocus = -1; return;
|
||||
}
|
||||
if(it.action === 'skill-edit'){
|
||||
var sk = null, wanted = it.skillId;
|
||||
(this.skills || []).forEach(function(x){ if(x.id === wanted) sk = x; });
|
||||
if(!sk) return;
|
||||
this.skillForm = {id: sk.id, name: sk.name, description: sk.description || '',
|
||||
prompt_template: sk.prompt_template || ''};
|
||||
this.plusSection = 'skills-edit'; this.plusFocus = -1; return;
|
||||
}
|
||||
if(it.action === 'skill-install'){
|
||||
this.installGallerySkill(it.slug, it.icon, it.skillName); return;
|
||||
}
|
||||
if(it.action === 'skill-import'){
|
||||
if(this.$refs.skillImport) this.$refs.skillImport.click();
|
||||
return;
|
||||
}
|
||||
if(it._folder){ // entrer dans un sous-dossier
|
||||
this.plusCrumb.push({id: it._folder, name: it.label});
|
||||
this.plusLoadBrowse(it._folder);
|
||||
@@ -1157,11 +1251,16 @@
|
||||
this.closePlus(); // sections futures (phases 2-8)
|
||||
},
|
||||
plusBack(){
|
||||
if(this.plusSection === 'browse' && this.plusCrumb.length){
|
||||
var sec = this.plusSection;
|
||||
if(sec === 'browse' && this.plusCrumb.length){
|
||||
this.plusCrumb.pop();
|
||||
this.plusLoadBrowse(this.plusCrumb.length ? this.plusCrumb[this.plusCrumb.length - 1].id : null);
|
||||
return;
|
||||
}
|
||||
if(sec === 'skills-edit'){ this.plusSection = 'skills-manage'; this.plusFocus = 0; return; }
|
||||
if(sec === 'skills-manage' || sec === 'skills-gallery'){
|
||||
this.plusSection = 'skills'; this.plusFocus = 0; return;
|
||||
}
|
||||
this.plusSection = 'root';
|
||||
this.plusFocus = 0;
|
||||
},
|
||||
@@ -1203,6 +1302,77 @@
|
||||
})
|
||||
.catch(function(){ self.plusBrowse = []; self.plusLoading = false; self.plusFocus = -1; });
|
||||
},
|
||||
// ── Gérer les compétences : CRUD / export / import (v7.52.0) ──
|
||||
saveSkill(){
|
||||
var self = this, f = this.skillForm;
|
||||
if(!(f.name || '').trim()){ this.toast('Le nom est requis.', true); return; }
|
||||
fetch(f.id ? '/api/agent/skills/' + f.id : '/api/agent/skills', {
|
||||
method: f.id ? 'PATCH' : 'POST',
|
||||
headers: {'X-CSRF-Token': getCsrf(), 'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({name: f.name, description: f.description || '',
|
||||
prompt_template: f.prompt_template || ''})
|
||||
}).then(function(r){
|
||||
return r.json().then(function(d){ return {ok: r.ok, d: d}; });
|
||||
}).then(function(res){
|
||||
if(!res.ok){ self.toast((res.d && res.d.detail) || 'Enregistrement impossible.', true); return; }
|
||||
self.toast('Skill enregistré.');
|
||||
self.fetchSkills();
|
||||
self.plusSection = 'skills-manage'; self.plusFocus = 0;
|
||||
}).catch(function(){ self.toast('Enregistrement impossible (réseau).', true); });
|
||||
},
|
||||
deleteSkill(){
|
||||
var self = this, f = this.skillForm;
|
||||
if(!f.id) return;
|
||||
if(!window.confirm('Supprimer le skill « ' + f.name + ' » ?')) return;
|
||||
fetch('/api/agent/skills/' + f.id, {method: 'DELETE', headers: {'X-CSRF-Token': getCsrf()}})
|
||||
.then(function(r){ return r.json().then(function(d){ return {ok: r.ok, d: d}; }); })
|
||||
.then(function(res){
|
||||
if(!res.ok){ self.toast((res.d && res.d.detail) || 'Suppression impossible.', true); return; }
|
||||
self.toast('Skill supprimé.');
|
||||
// le chip épinglé ne doit pas survivre au skill
|
||||
self.contextChips = self.contextChips.filter(function(ch){
|
||||
return !(ch.kind === 'skill' && ch.skillId === f.id);
|
||||
});
|
||||
self.skillForm = {id: null, name: '', description: '', prompt_template: ''};
|
||||
self.fetchSkills();
|
||||
self.plusSection = 'skills-manage'; self.plusFocus = 0;
|
||||
}).catch(function(){ self.toast('Suppression impossible (réseau).', true); });
|
||||
},
|
||||
exportSkill(){
|
||||
var self = this, f = this.skillForm;
|
||||
if(!f.id) return;
|
||||
fetch('/api/agent/skills/' + f.id + '/export')
|
||||
.then(function(r){ return r.json(); })
|
||||
.then(function(doc){
|
||||
var blob = new Blob([JSON.stringify(doc, null, 2)], {type: 'application/json'});
|
||||
var a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = String((doc && doc.name) || 'skill').replace(/[^\w.-]+/g, '-') + '.json';
|
||||
document.body.appendChild(a); a.click(); document.body.removeChild(a);
|
||||
setTimeout(function(){ URL.revokeObjectURL(a.href); }, 1000);
|
||||
}).catch(function(){ self.toast('Export impossible.', true); });
|
||||
},
|
||||
importSkillFile(ev){
|
||||
var self = this;
|
||||
var input = ev.target, file = input.files && input.files[0];
|
||||
if(!file) return;
|
||||
file.text().then(function(txt){
|
||||
var payload;
|
||||
try{ payload = JSON.parse(txt); }
|
||||
catch{ self.toast('Fichier JSON invalide.', true); return; }
|
||||
return fetch('/api/agent/skills/import', {
|
||||
method: 'POST', headers: {'X-CSRF-Token': getCsrf(), 'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({payload: payload, overwrite: true})
|
||||
}).then(function(r){ return r.json().then(function(d){ return {ok: r.ok, d: d}; }); })
|
||||
.then(function(res){
|
||||
if(!res.ok){ self.toast((res.d && res.d.detail) || 'Import impossible.', true); return; }
|
||||
self.toast('Skill importé : ' + ((res.d && res.d.name) || '') + ' (pas de doublon).');
|
||||
self.fetchSkills();
|
||||
self.plusSection = 'skills-manage'; self.plusFocus = 0;
|
||||
});
|
||||
}).catch(function(){ self.toast('Lecture du fichier impossible.', true); })
|
||||
.finally(function(){ input.value = ''; });
|
||||
},
|
||||
openMentionPicker(from){
|
||||
this.plusOpen = false;
|
||||
this.mentionOpen = true;
|
||||
|
||||
Reference in New Issue
Block a user