feat: menu + — Compétences-skills « Gérer » + « Parcourir » — phase 2/8 (v7.52.0)
FlowDeck CI / lint (push) Successful in 2m11s
FlowDeck CI / test (push) Failing after 13m54s
FlowDeck CI / docker (push) Skipped

- 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:
2026-10-06 21:39:24 -04:00
parent f271ac9b7a
commit 18a2d45d46
11 changed files with 535 additions and 79 deletions
+179 -9
View File
@@ -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;