- 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.
12 KiB
V74 — Menu + de l'assistant : hub de contexte (design)
Statut : design — phase 1 livrée en v7.51.0 (2026-10-06) ; phases 2-8 restent à livrer (le parcours utilise
GET /api/nav/menu?parent_id=un niveau par appel, pas le tree récursif — contrat : dossier =icon === 'folder'). Plan phasé :ROADMAP.md→ section « v7.51.0 → v7.58.0 — Menu + de l'assistant ». Date : 2026-10-06 · Version cible de départ : v7.51.0 (phase 1).
1. Objectif
Le bouton + du panneau agent (app/templates/agent_panel.html:409) ouvre
aujourd'hui une liste plate de fichiers/pages/bases à épingler en contexte. On le
transforme en menu à sections à deux niveaux :
+ ┌──────────────────────────────────────────────────────┐
│ 📎 Ajouter des fichiers ou répertoires › │ → picker actuel + « Parcourir… »
│ ✨ Compétences-skills › │ → Deep research, Skill-creator, …
│ │ ────────────────
│ │ Gérer les compétences
│ │ Parcourir les compétences
│ 🔌 Connecteurs › │ → Parcourir les connecteurs
│ │ Ajouter un connecteur personnalisé
│ 🎨 Design System – Canevas › │ → liste des canevas
│ 🧩 Add plugins │ → catalogue on/off (modale)
│ 🧠 Mémoire [◉/○] │ → toggle persisté
└──────────────────────────────────────────────────────┘
Règle d'or : chaque entrée vit dans la même phase qui la rend réelle. Tant qu'une section n'est pas livrée, elle s'affiche grisée « Bientôt (phase N) » — le menu ne promet rien que le code ne fait.
2. État des lieux (lu dans le code, 2026-10-06)
| Section | Existe | Manque |
|---|---|---|
| Fichiers / répertoires | GET /api/agent/mentions (app/routers/agent.py:596) → documents, collections, pages ; jetons document:id, collection:id, page:id résolus par ContextBuilder._mentions_context (app/services/context_builder.py:120) |
Sous-menu, mode parcours d'arborescence, jeton folder:<id> |
| Compétences | agent_skills CRUD (/api/agent/skills : GET/POST/DELETE, apply, import/export), galerie de 17 presets (app/services/skill_gallery.py), 11 skills builtin (FD_SKILLS, static/js/agent_panel_2.js:11) |
UI « Gérer » (CRUD complet : PATCH /skills/{id} à créer), UI « Parcourir » (la galerie n'est accessible que via la palette /), libellés affichés type « Deep research » |
| Canevas | GET /api/page-templates (built-ins app/services/block_templates.py + personnels), POST /api/page-templates/{id}/use (app/routers/board/page_api.py:75,135) |
Menu dans le panneau agent, action « insérer dans le document ouvert », canevas « design system » |
| Connecteurs | Rien (0 hit connector dans app/). Natifs déjà branchés ailleurs : Gitea (gitea_client.py), GitHub (github_adapter.py), Web (web_search.py + tool fetch_url v7.46) |
Table, service d'adapters, catalogue UI, tools agent, OAuth Google/M365, Discord/Telegram, MCP |
| Plugins | Rien | Registre, catalogue UI, câblage réel (désactivation effective) |
| Mémoire | Rien (l'historique de la conversation est déjà envoyé, ce n'est pas de la mémoire) | Table, service d'extraction, injection conditionnelle, toggle persisté |
3. Découpage front (Alpine)
Tout vit dans static/js/agent_panel_2.js (composant agentPanel()) + un bloc de
markup dans app/templates/agent_panel.html, sans nouveau fichier JS.
État ajouté
plusOpen: false, // menu ouvert
plusSection: 'root', // 'root' | 'files' | 'skills' | 'connectors' | 'canvases'
plusFocus: -1, // index de l'item focalisé
plusItems: [], // items de la section courante (fetch si besoin)
memoryOn: true, // état du toggle (phase 4)
Items = données, pas du markup en dur : un tableau FD_PLUS_MENU
{key, icon, label, sub, action, phase} → le rendu et la navigation clavier
(↑/↓/Entrée/Échap) sont une seule boucle. Les sections phase > livrée sont
disabled: true.
Réemploi : le conteneur et les styles .fd-ap-mention-menu /
.fd-mention-item (agent_panel.html:171-181) servent déjà exactement ce
comportement — on duplique le CSS minimal (.fd-plus-*) et on garde la logique de
focus/scroll (moveMentionFocus, _scrollMentionItem) comme modèle.
Entrées existantes préservées : la touche @ dans le composer et le mode
recherche du + continuent de fonctionner à l'identique (régression zéro) ; le +
devient seulement l'entrée en menu au lieu d'ouvrir directement la liste.
4. Backend par phase
| Phase | Existant réutilisé | À créer |
|---|---|---|
| 1 — menu + fichiers/répertoires | /api/agent/mentions, /api/local-workspace/tree?folder=, enfants de page (parent_id, local_workspace.py:243) |
Route(s) de parcours adaptée(s) au panneau (shape items compatible mention), jeton folder:<id> dans ContextBuilder |
| 2 — compétences | /api/agent/skills (GET/POST/DELETE/apply/import/export), /api/agent/skills/gallery + install |
PATCH /api/agent/skills/{id} (édition), 2 modales |
| 3 — canevas | /api/page-templates (GET/POST/use) |
Insersion dans l'éditeur ouvert (API d'insertion de blocs de l'éditeur), presets « design system » dans block_templates.py |
| 4 — mémoire | ContextBuilder, PATCH /conversations/{id} (pattern du toggle) |
Table agent_memory, app/services/agent_memory.py, colonne conversations.memory_enabled, config AGENT_MEMORY_DEFAULT |
| 5 — connecteurs (socle) | _is_public_host (SSRF), encrypt_secret (sso_provisioning.py:51), http_client.shared_client, tool_registry |
Table agent_connectors, app/services/connectors/, 3 adapters natifs (gitea/github/web), catalogue UI |
| 6 — Google + M365 | auth/oauth.py (client OAuth2), encrypt_secret, shared_client |
Flows PKCE Google + Microsoft Graph, table de tokens (motif calendar_sync.py:38), écran connecteur |
| 7 — Discord / Telegram / Teams / MCP | Graph (phase 6), pattern de tools dynamiques du registre | 3 adapters + client MCP (initialize, tools/list, tools/call) |
| 8 — plugins | — | Table plugins, catalogue UI, désactivation réelle (routes + UI) |
5. Modèle de données
-- Phase 4 — mémoire
ALTER TABLE conversations ADD COLUMN memory_enabled INTEGER NOT NULL DEFAULT 1;
CREATE TABLE agent_memory (
id INTEGER PRIMARY KEY,
workspace_id INTEGER NOT NULL,
conversation_id INTEGER, -- NULL = mémoire d'espace (partagée)
kind TEXT NOT NULL DEFAULT 'summary', -- summary | fact
content TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE INDEX idx_agent_memory_ws ON agent_memory(workspace_id, conversation_id);
-- Phase 5 — connecteurs
CREATE TABLE agent_connectors (
id INTEGER PRIMARY KEY,
workspace_id INTEGER,
kind TEXT NOT NULL, -- gitea | github | web | google | ms365 | discord | telegram | mcp | custom
name TEXT NOT NULL,
config_json TEXT NOT NULL DEFAULT '{}', -- URL, scopes, channels…
secret_encrypted TEXT, -- Fernet (encrypt_secret, sso_provisioning.py:51)
enabled INTEGER NOT NULL DEFAULT 1,
status TEXT NOT NULL DEFAULT 'unknown', -- ok | error | unknown
last_probe_at TEXT,
created_by INTEGER
);
-- Phase 8 — plugins
CREATE TABLE plugins (
slug TEXT PRIMARY KEY, -- web-tools | web-clipper | automations | …
enabled INTEGER NOT NULL DEFAULT 1,
updated_at TEXT NOT NULL
);
Migrations ajoutées dans app/migrations.py (motif _migration_* existant,
transaction par migration — A31).
6. Sécurité (non négociable)
- SSRF : toute URL de connecteur passée par
_is_public_host(app/services/importers/url_fetch.py), re-vérifiée à chaque saut de redirection. - Secrets : jamais en clair, jamais renvoyés par l'API (champ retiré des
sérialiseurs) —
encrypt_secret()existant, dérivé deapp_secret_key. - Auth + CSRF : session requise sur tout CUD ;
/api/agentest sur la liste des préfixes CSRF cookie → headerX-CSRF-Tokensur les nouveauxfetch(helpergetCsrf()). - Borne de tokens : la mémoire et les sources de connecteurs injectées dans le contexte sont tronquées (budget explicite) — sinon la phase 4 fait exploser le coût de chaque run.
- Plugins OFF = effet réel : un simple drapeau affiché serait un menu qui ment (règle du projet).
7. Décisions techniques
- D1 — Un seul menu, deux niveaux (pas de modale racine) : ouverture instantanée,
clavier, et le
+reste un geste unique. Les modales ne servent qu'aux écrans lourds (Gérer / Parcourir / Connecteurs / Plugins). - D2 — Pas de nouveau fichier JS : tout dans
agent_panel_2.js+ markup du template, conforme à l'extraction A27 (un fichier par bloc). - D3 — États « Bientôt » visibles plutôt que sections masquées : le menu sert de spec vivante et évite d'implémenter6 features d'un coup.
- D4 — La mémoire est un résumé structuré, pas l'historique brut : réinvoquer
tout l'historique à chaque run coûterait plus cher que la réponse ; un résumé borné
(~1–2 k tokens) + les faits saillants suffisent, avec repli déterministe hors-ligne
(motif
ai_writing.py). - D5 — Connecteurs = registry + adapters (interface
probe/list_sources/fetch), pas un if par provider : la phase 7 (MCP) ajoute des adapters sans toucher au socle. - D6 — Canevas : « créer » par défaut, « insérer » seulement si un document est ouvert — l'insertion dans l'éditeur est le point le plus fragile (blocs + sélection), donc livrée en second dans la phase 3.
- D7 — Pas de dépendance ajoutée : OAuth via
httpx(shared_client), chiffrement viacryptographydéjà présent, MCP en HTTP direct.
8. Pièges du repo à respecter
ROADMAP.md/CHANGELOG.mdsont en CRLF :patchexige des lignes complètes.- Routes statiques avant les wildcards (
/skills/galleryavant/skills/{id}), sinon 422 sur un id non numérique. - Events
AgentEngine.run()plats ({"type": …, "content": …}), pas enveloppés dansdata. - Bumper
VERSION+app/main.pyà chaque phase, régénérerdocs/openapi-v2.json(CRLF, sans newline final) si de nouvelles routes sortent. - Le panneau agent est rendu sur toutes les pages (
base.html:2555) : le JS doit rester défensif (window.appStateabsent, page non authentifiée).
9. Critères d'acceptation par phase
- Phase 1 : le
+ouvre le menu, « Parcourir… » montre l'arborescence, un dossier épinglé apparaît en chip et son contenu est bien dans le contexte du run. - Phase 2 : créer/éditer/supprimer un skill depuis la modale, l'installer depuis
la galerie, il apparaît dans la palette
/et le sous-menu. - Phase 3 : choisir un canevas crée la page (et l'insertion dans le document ouvert fonctionne quand un doc est édité).
- Phase 4 : ON → la mémoire est injectée (test qui le prouve) ; OFF → absente ; l'état survit au rechargement et au changement de conversation.
- Phase 5 : connecteur créé/testé/supprimé, URL privée refusée (SSRF), tool exposé au LLM avec le bon schéma.
- Phase 6 : connexion Google et Microsoft, refresh, déconnexion, aucun appel réseau réel dans les tests.
- Phase 7 : Discord/Telegram branchés, MCP
tools/list→ outils visibles dans le registre. - Phase 8 : plugin OFF = route refusée + UI disparue (test de non-réintroduction).
Chaque phase se clôt par : ruff + suite verte, bump de version, CHANGELOG, ROADMAP
à jour, tag.
10. Hors périmètre
- Écriture dans les sources distantes (Google Docs, Slack…) — lecture seule au départ.
- Synchronisation d'arborescence locale ↔ connecteur.
- Marketplace de plugins tiers (le registre est interne à l'instance).
- Refonte du composer / de la palette
/(compatibilité maintenue à l'identique).