Files
flowdeck/docs/V74_Agent_Plus_Menu.md
T
bruno f271ac9b7a
FlowDeck CI / lint (push) Successful in 2m11s
FlowDeck CI / test (push) Failing after 3h10m48s
FlowDeck CI / docker (push) Skipped
feat: menu + de l'assistant en hub de contexte — phase 1/8 (v7.51.0)
- 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.
2026-10-06 20:01:47 -04:00

12 KiB
Raw Blame History

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é 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).