feat: menu + de l'assistant en hub de contexte — phase 1/8 (v7.51.0)
FlowDeck CI / lint (push) Successful in 2m11s
FlowDeck CI / test (push) Failing after 3h10m48s
FlowDeck CI / docker (push) Skipped

- Bouton + : menu à sections (fichiers/répertoires, compétences-skills,
  connecteurs, Design System – Canevas, Add plugins, Mémoire on/off).
  Sections pas encore livrées affichées « bientôt (phase N) » mais désactivées,
  navigation clavier ↑/↓/Entrée/Échap, focus visible, la frappe referme le menu.
- Parcours « Parcourir… » : un niveau par appel via GET /api/nav/menu
  (contrat : dossier = icon 'folder'), fil d'Ariane cliquable, épingle de dossier
  via la ligne « 📌 Épingler le dossier ».
- Jeton folder:<id> résolu par ContextBuilder._single_folder() : titre du dossier
  + documents directs, budget ~12k caractères (marqueur « tronqué »), enfants
  directs seulement.
- « Rechercher… » conserve l'ancien sélecteur de mentions (@ inline + recherche
  par nom) : régression zéro sur le chemin existant.
- Tests : tests/test_v751_plus_menu.py (7 tests). Suite complète 1273 verts
  (-n auto), ruff check app tests propre, eslint static/js 0 erreur.
- Docs : CHANGELOG, ROADMAP (phase 1 cochée), docs/V74_Agent_Plus_Menu.md statut.
This commit is contained in:
2026-10-06 20:01:47 -04:00
parent f006880394
commit f271ac9b7a
9 changed files with 396 additions and 22 deletions
+146 -3
View File
@@ -32,6 +32,28 @@
{cmd:'/livrable', icon:'📨', desc:'email / compte-rendu / message Slack',
template:'Rédige un livrable professionnel (email, compte-rendu ou message Slack) sur [sujet / destinataire / contexte].'}
];
// ── Menu + (v7.51.0) : sections à deux niveaux. `soon` = section pas encore
// livrée → affichée mais non cliquable (le menu ne promet rien que le code ne fait).
var FD_PLUS_MENU = [
{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},
{key:'connectors', icon:'🔌', label:'Connecteurs',
sub:'Parcourir / connecteur personnalisé — bientôt (phase 5)', disabled:true},
{key:'canvases', icon:'🎨', label:'Design System – Canevas',
sub:'Galerie de canevas — bientôt (phase 3)', disabled:true},
{key:'plugins', icon:'🧩', label:'Add plugins',
sub:'Modules on/off — bientôt (phase 8)', disabled:true},
{key:'memory', icon:'🧠', label:'Mémoire (on/off)',
sub:'Mémoire persistante — bientôt (phase 4)', disabled:true}
];
var FD_PLUS_FILES = [
{key:'f-search', icon:'🔍', label:'Rechercher…',
sub:'Documents, bases, pages par nom', action:'search', chev:true},
{key:'f-browse', icon:'📁', label:'Parcourir…',
sub:'Dossiers et fichiers du workspace', action:'browse', chev:true}
];
function agentPanel(){
return {
@@ -45,6 +67,8 @@
contextChips: [], skills: [], gallery: [],
mentionOpen: false, mentionItems: [], mentionQuery: '', mentionLoad: false,
mentionFocus: -1, _mentionFrom: null, _mentionTimer: null, _atNode: null,
plusOpen: false, plusSection: 'root', plusFocus: -1, plusLoading: false,
plusBrowse: [], plusCrumb: [],
cmdFocus: -1, slashDismiss: false,
toastMsg: '', toastErr: false, _toastTimer: null,
_livePh: null,
@@ -1070,12 +1094,117 @@
this._toastTimer = setTimeout(function(){ self.toastMsg = ''; self.toastErr = false; }, 3200);
},
// ── Mentions & contexte épinglé (@ / +) ──
// ── Menu + (sections) & mentions (@ / recherche) ──
toggleAddPicker(){
if(this.mentionOpen && this._mentionFrom === '+'){ this.mentionOpen = false; this.mentionQuery = ''; return; }
this.openMentionPicker('+');
if(this.mentionOpen){
this.mentionOpen = false; this.mentionQuery = '';
this._mentionFrom = null; this._atNode = null;
}
if(this.plusOpen){ this.closePlus(); return; }
this.plusOpen = true;
this.plusSection = 'root';
this.plusFocus = 0;
},
closePlus(){
this.plusOpen = false;
this.plusSection = 'root';
this.plusCrumb = [];
this.plusBrowse = [];
this.plusFocus = -1;
},
get plusTitle(){
if(this.plusSection === 'files') return 'Fichiers & répertoires';
if(this.plusSection === 'browse') return 'Parcourir le workspace';
return 'Ajouter au contexte';
},
get plusItems(){
if(this.plusSection === 'files') return FD_PLUS_FILES;
if(this.plusSection === 'browse') return this.plusBrowse;
return FD_PLUS_MENU;
},
_plusSelectable(){
var out = [];
this.plusItems.forEach(function(it, i){ if(!it.disabled) out.push(i); });
return out;
},
movePlusFocus(delta){
var sel = this._plusSelectable();
if(!sel.length) return;
var idx = sel.indexOf(this.plusFocus);
if(idx < 0) idx = delta > 0 ? -1 : 0;
this.plusFocus = sel[(idx + delta + sel.length) % sel.length];
this._scrollPlusItem();
},
_scrollPlusItem(){
var menu = this.$refs && this.$refs.plusMenu;
if(!menu) return;
var el = menu.querySelector('.fd-mention-item.focus');
if(el && el.scrollIntoView){ menu.scrollTop = el.offsetTop - menu.clientHeight / 2 + el.clientHeight / 2; }
},
plusAction(it){
if(!it || it.disabled) 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;
}
if(it._folder){ // entrer dans un sous-dossier
this.plusCrumb.push({id: it._folder, name: it.label});
this.plusLoadBrowse(it._folder);
return;
}
if(it.token){ this._mentionFrom = null; this._atNode = null; this.closePlus(); this.pickMention(it); return; }
this.closePlus(); // sections futures (phases 2-8)
},
plusBack(){
if(this.plusSection === 'browse' && this.plusCrumb.length){
this.plusCrumb.pop();
this.plusLoadBrowse(this.plusCrumb.length ? this.plusCrumb[this.plusCrumb.length - 1].id : null);
return;
}
this.plusSection = 'root';
this.plusFocus = 0;
},
plusGoto(i){ // fil d'Ariane : -1 = racine
this.plusCrumb = i < 0 ? [] : this.plusCrumb.slice(0, i + 1);
this.plusLoadBrowse(i < 0 ? null : this.plusCrumb[this.plusCrumb.length - 1].id);
},
plusLoadBrowse(parentId){
// Un niveau par appel : /api/nav/menu (dossier = icon 'folder').
var self = this;
this.plusLoading = true;
fetch('/api/nav/menu' + (parentId ? ('?parent_id=' + parentId) : ''))
.then(function(r){ return r.json(); })
.then(function(d){
var items = [];
// icon de /api/nav/menu = nom SVG ('folder'|'edit'|'image'|'file') → emoji
var ICONS = {folder:'📁', edit:'📄', image:'🖼️', file:'📎'};
(d.items || []).forEach(function(x){
var isFolder = x.icon === 'folder';
items.push({
key: 'br-' + x.id, icon: ICONS[x.icon] || '📄',
label: x.name, sub: isFolder ? 'Dossier — ouvrir' : 'Document — épingler',
token: (isFolder ? 'folder:' : 'document:') + x.id,
type: isFolder ? 'folder' : 'document', id: x.id,
_folder: isFolder ? x.id : null
});
});
if(self.plusCrumb.length){
var cur = self.plusCrumb[self.plusCrumb.length - 1];
items.unshift({key: 'pin', icon: '📌',
label: 'Épingler le dossier « ' + cur.name + ' »',
sub: 'Ajoute le dossier entier au contexte',
token: 'folder:' + cur.id, type: 'folder', id: cur.id});
}
self.plusBrowse = items;
self.plusLoading = false;
self.plusFocus = items.length ? 0 : -1;
self.$nextTick(function(){ self._scrollPlusItem(); });
})
.catch(function(){ self.plusBrowse = []; self.plusLoading = false; self.plusFocus = -1; });
},
openMentionPicker(from){
this.plusOpen = false;
this.mentionOpen = true;
this._mentionFrom = from || '+';
this.mentionQuery = '';
@@ -1177,6 +1306,7 @@
onInput(){
this.resizeInput();
this.slashDismiss = false;
if(this.plusOpen) this.closePlus(); // tape = on écrit le message, le menu se referme
var text = this.composerText();
this.input = text;
var cmdNow = /^\s*\//.test(text);
@@ -1249,16 +1379,29 @@
}catch { /* volontaire */ }
}
if(key === 'Escape'){
if(this.plusOpen){ e.preventDefault(); e.stopPropagation(); this.closePlus(); return; }
if(this.mentionOpen){ e.preventDefault(); e.stopPropagation(); this.mentionOpen = false; this._mentionFrom = null; this._atNode = null; return; }
if(this.commandOpen){ e.preventDefault(); e.stopPropagation(); this.slashDismiss = true; this.cmdFocus = -1; return; }
return; // laisse fermer le panneau
}
if(key === 'ArrowDown' || key === 'ArrowUp'){
var delta = key === 'ArrowDown' ? 1 : -1;
if(this.plusOpen){ e.preventDefault(); this.movePlusFocus(delta); return; }
if(this.mentionOpen && this.mentionItems.length){ e.preventDefault(); this.moveMentionFocus(delta); return; }
if(this.commandOpen && !this.slashDismiss && this._slashItems().length){ e.preventDefault(); this.moveSlashFocus(delta); return; }
return;
}
// Entrée / Tab avec le menu + ouvert
if(this.plusOpen){
if(key === 'Enter' || key === 'Tab'){
e.preventDefault();
var psel = this._plusSelectable();
if(!psel.length) return;
var pidx = psel.indexOf(this.plusFocus) >= 0 ? this.plusFocus : psel[0];
this.plusAction(this.plusItems[pidx]);
return;
}
}
// Entrée / Tab avec un menu ouvert (skills / mentions)
if(this.mentionOpen){
var mSel = this._selectableMentionItems();