feat(agent): v4.15.2 - skills multi-epingles en bulles + chips de contexte cliquables
FlowDeck CI / test (push) Failing after 11s
FlowDeck CI / docker (push) Skipped

- Les skills (menu /, integres et enregistres) s'epinglent comme des bulles dans le composer, au meme titre que les mentions @; plusieurs peuvent coexister sur un meme post
- L'API /run accepte skill_ids (tableau) et l'engine injecte tous leurs prompts dans le system prompt (skill_id conserve pour retro-compat)
- Clic sur une bulle de contexte (@ document/page/base) ouvre l'element comme document courant (/pages/<id> ou /db/<id>); le bouton x retire sans ouvrir
- Envoi possible avec des skills seuls sans texte
- test_engine_multiple_skills_applied
This commit is contained in:
2026-09-06 13:27:38 -04:00
parent 113f880863
commit 917a04d543
7 changed files with 119 additions and 19 deletions
+47 -11
View File
@@ -115,6 +115,8 @@
.fd-chip-row{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
.fd-chip{display:inline-flex;align-items:center;gap:6px;background:rgba(35,131,226,.10);border:1px solid rgba(35,131,226,.30);color:var(--text,#e8e8e8);border-radius:99px;padding:3px 5px 3px 9px;font-size:11.5px;max-width:220px}
.fd-chip.skill{background:rgba(168,85,247,.12);border-color:rgba(168,85,247,.38)}
.fd-chip.chip-open{cursor:pointer}
.fd-chip.chip-open:hover{background:rgba(35,131,226,.20);border-color:rgba(35,131,226,.55)}
.fd-chip-ico{flex-shrink:0;font-size:12px}
.fd-chip-lbl{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.fd-chip-spacer{flex-basis:100%;height:0}
@@ -289,10 +291,10 @@
<div class="fd-ap-chips" x-show="contextChips.length" x-cloak>
<div class="fd-chip-row" x-show="mentionChips.length">
<template x-for="chip in mentionChips" :key="chip.key">
<span class="fd-chip">
<span class="fd-chip chip-open" :title="chipAction(chip)" @click="openChip(chip)">
<span class="fd-chip-ico" x-text="chip.icon"></span>
<span class="fd-chip-lbl" x-text="chip.label"></span>
<button class="fd-chip-x" type="button" @click="removeChip(chip.token)" title="Retirer du contexte">✕</button>
<button class="fd-chip-x" type="button" @click.stop="removeChip(chip.token)" title="Retirer du contexte">✕</button>
</span>
</template>
</div>
@@ -523,7 +525,7 @@
get composerHint(){
var chips = this.contextChips.length;
var base = 'Demandez, tapez / pour un skill, @ pour référencer un élément';
var extra = chips ? ' · ' + chips + ' réf. épinglée(s)' : '';
var extra = chips ? ' · ' + chips + ' réf./skill épinglé(s)' : '';
return base + extra + ' · Entrée ↵ = envoyer · Maj+Entrée = ligne';
},
get canInsert(){ return !!(this.activeContext && this.activeContext.kind === 'page'); },
@@ -1075,16 +1077,24 @@
var exists = this.contextChips.some(function(ch){ return ch.kind === 'skill' && ch.skillId === c.skillId; });
if(exists){ this.toast('Ce skill est déjà épinglé au contexte.'); }
else {
var hasSkill = this.contextChips.some(function(ch){ return ch.kind === 'skill'; });
this.contextChips.push({key:'skill-'+c.skillId, kind:'skill', icon:c.icon||'✨',
label:String(c.cmd).replace(/^\//, ''), token:'skill:'+c.skillId, skillId:c.skillId});
var pin = {key:'skill-'+c.skillId, kind:'skill', icon:c.icon||'✨',
label:String(c.cmd).replace(/^\//, ''), token:'skill:'+c.skillId, skillId:c.skillId};
this.contextChips.push(pin);
this.clearComposer();
this._insertToken({token:'skill:'+c.skillId, label:String(c.cmd).replace(/^\//, ''), icon:c.icon||'✨', kind:'skill'});
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.');
}
} else {
this.setComposerText(c.template || '');
this.toast('Complétez la demande puis envoyez (Entrée).');
var slug = String(c.cmd).replace(/^\/+/, '').toLowerCase().replace(/\s+/g, '-');
var builtinExists = this.contextChips.some(function(ch){ return ch.kind === 'skill' && ch.token === 'builtin:' + slug; });
if(builtinExists){ this.toast('Ce skill est déjà épinglé au contexte.'); }
else {
this.contextChips.push({key:'bi-'+slug, kind:'skill', icon:c.icon||'✨',
label:slug, token:'builtin:'+slug, skillId:null, builtin:true});
var cur = this.composerText();
this.setComposerText(cur ? (cur + '\n\n' + (c.template || '')) : (c.template || ''));
this.toast('Skill épinglé — complétez la demande puis envoyez.');
}
}
this.cmdFocus = -1; this.slashDismiss = false;
this._focusComposer();
@@ -1218,6 +1228,27 @@
this._removeTokenSpan(chip.token);
this._emptyCheck();
},
chipHref(chip){
if(!chip || chip.kind !== 'mention') return null;
var id;
if(chip.refId != null) id = Number(chip.refId);
else {
var tk = String(chip.token || '').split(':');
if(tk.length === 2 && /^\d+$/.test(tk[1])) id = Number(tk[1]);
}
if(id == null || isNaN(id)) return null;
if(chip.type === 'collection') return '/db/' + id;
return '/pages/' + id; // document & pages de collection s'ouvrent dans l'éditeur
},
chipAction(chip){
if(!chip || chip.kind !== 'mention') return '';
return chip.type === 'collection' ? 'Ouvrir la base de données' : 'Ouvrir le document';
},
openChip(chip){
var href = this.chipHref(chip);
if(!href){ this.toast('Impossible d\u2019ouvrir cet élément.', true); return; }
window.location.assign(href);
},
onInput(){
this.resizeInput();
this.slashDismiss = false;
@@ -1399,7 +1430,9 @@
if(/^\/+$/.test(text)){ self.clearComposer(); return; }
var ment = self.composerMentions();
if(!text){
var hasSkills = (self.contextChips || []).some(function(ch){ return ch.kind === 'skill'; });
if(ment.length){ text = 'Analyse le contexte référencé.'; }
else if(hasSkills){ text = 'Applique le(s) skill(s) sélectionné(s) à ma demande.'; }
else { return; }
}
try { await self.ensureConversation(); } catch(e){ return; }
@@ -1427,8 +1460,11 @@
var ctxText = self.contextPayload();
if(ctxText) payload.context = ctxText;
if(ment && ment.length) payload.mentions = ment;
var skillChip = (self.contextChips || []).find(function(ch){ return ch.kind === 'skill'; });
if(skillChip && skillChip.skillId) payload.skill_id = skillChip.skillId;
var skillIds = [];
(self.contextChips || []).forEach(function(ch){
if(ch.kind === 'skill' && ch.skillId) skillIds.push(ch.skillId);
});
if(skillIds.length) payload.skill_ids = skillIds;
return fetch('/api/agent/conversations/'+self.currentConv.id+'/run', {
method:'POST',