# V74 — Menu + de l'assistant : hub de contexte (design) > **Statut** : design — **phases 1 à 6 livrées (v7.51.0 → v7.56.0, 2026-10-06)** ; > phases 7-8 restent à livrer. La **mémoire** a été simplifiée par rapport au > modèle ici : une seule ligne résumé **par conversation** (pas de lignes > `workspace_id NULL` — rien ne les écrivait), extraction déterministe sans LLM. Adaptations faites en cours de route : le parcours > utilise `GET /api/nav/menu?parent_id=` (un niveau par appel, contrat > dossier = `icon === 'folder'`), la gestion des skills est une **section du menu > avec formulaire intégré** (pas une modale — `PATCH /api/agent/skills/{id}` > ajouté), et les canevas vivent sous **`/board/api/page-templates`** (prefix > `/board`) avec la nouvelle route `GET …/{id}/blocks` pour l'insertion. > **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:` | | 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é** ```js 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:` 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 ```sql -- 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é de `app_secret_key`. - **Auth + CSRF** : session requise sur tout CUD ; `/api/agent` est sur la liste des préfixes CSRF cookie → header `X-CSRF-Token` sur les nouveaux `fetch` (helper `getCsrf()`). - **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 via `cryptography` déjà présent, MCP en HTTP direct. ## 8. Pièges du repo à respecter - `ROADMAP.md` / `CHANGELOG.md` sont en **CRLF** : `patch` exige des lignes complètes. - **Routes statiques avant les wildcards** (`/skills/gallery` avant `/skills/{id}`), sinon 422 sur un id non numérique. - **Events `AgentEngine.run()` plats** (`{"type": …, "content": …}`), pas enveloppés dans `data`. - Bumper **`VERSION` + `app/main.py`** à chaque phase, régénérer `docs/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.appState` absent, page non authentifiée). ## 9. Critères d'acceptation par phase 1. **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. 2. **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. 3. **Phase 3** : choisir un canevas crée la page (et l'insertion dans le document ouvert fonctionne quand un doc est édité). 4. **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. 5. **Phase 5** : connecteur créé/testé/supprimé, URL privée refusée (SSRF), tool exposé au LLM avec le bon schéma. 6. **Phase 6** : connexion Google et Microsoft, refresh, déconnexion, aucun appel réseau réel dans les tests. 7. **Phase 7** : Discord/Telegram branchés, MCP `tools/list` → outils visibles dans le registre. 8. **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).