Files
flowdeck/app/templates/agent_panel.html
T
bruno f8acb906e5
FlowDeck CI / lint (push) Successful in 2m14s
FlowDeck CI / test (push) Failing after 17m51s
FlowDeck CI / docker (push) Skipped
feat: connecteurs Discord/Telegram/MCP + outils MCP dynamiques au registre (v7.57.0)
- presets Discord / Telegram : colonnes kind + auth (bearer/bot/none) sur
  agent_connectors (migration 34) ; Discord envoie le jeton préfixé dans
  l'en-tête d'autorisation, Telegram le place dans l'URL via le substitut
  {secret} remplacé à l'appel — jamais stocké en clair
- Teams : scope ChannelMessage.Read.All ajouté aux Graph scopes M365
- app/services/mcp_client.py : handshake initialize → notifications/initialized
  → tools/list → tools/call (JSON-RPC 2.0, réponse JSON ou premier data: d'un
  text/event-stream), garde SSRF, outils cachés en base (tools_json) —
  le bouton « Tester » rejoue le handshake
- ToolRegistry._all() merge le cache MCP à chaque run : outils exposés au LLM
  sous mcp_<serveur>_<outil> avec leur inputSchema, dispatch tools/call ;
  serveur désactivé → outil absent du schéma, échec réseau → ToolResult(error)
- menu + : champ Type dans le formulaire connecteur (preset URL + auth),
  badge « · N outil(s) » sur la fiche d'un serveur MCP
- tests : tests/test_v757_mcp_discord.py (12 tests, 0 appel réseau réel) ;
  suite complète 1331 verts, ruff 0, eslint 0
- livraison : VERSION + app/main = 7.57.0, OpenAPI 523 chemins, CHANGELOG,
  ROADMAP phase 7 cochée, avenant phase 7 dans docs/V74_Agent_Plus_Menu.md
2026-10-07 09:09:19 -04:00

536 lines
41 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{# ════════════ FlowDeck Agent panel v4.13.0 — Notion-AI-style sidebar ════════════ #}
<style>
#fd-agent-fab{position:fixed;right:22px;bottom:22px;width:54px;height:54px;border-radius:50%;background:var(--text-primary,#37352F);color:var(--bg-primary,#fff);border:none;cursor:pointer;box-shadow:0 8px 26px rgba(0,0,0,.35);display:flex;align-items:center;justify-content:center;z-index:1700;transition:transform .15s,box-shadow .15s}
#fd-agent-fab:hover{transform:scale(1.06);box-shadow:0 12px 34px rgba(0,0,0,.45)}
#fd-agent-panel{position:fixed;top:0;right:0;bottom:0;width:var(--fd-agent-width,470px);max-width:100vw;background:var(--bg-primary,#191919);border-left:1px solid var(--border,rgba(255,255,255,.1));z-index:1850;display:flex;flex-direction:column;box-shadow:-16px 0 48px rgba(0,0,0,.55);transform:translateX(102%);transition:transform .24s cubic-bezier(.16,1,.3,1)}
#fd-agent-panel.open{transform:translateX(0)}
#fd-agent-panel ::-webkit-scrollbar{width:8px;height:8px}
#fd-agent-panel ::-webkit-scrollbar-thumb{background:rgba(128,128,128,.35);border-radius:4px}
#fd-agent-panel ::-webkit-scrollbar-track{background:transparent}
/* Resize handle (left edge) */
.fd-ap-resize{position:absolute;top:0;bottom:0;left:0;width:6px;cursor:col-resize;z-index:5}
.fd-ap-resize::before{content:"";position:absolute;top:0;bottom:0;left:2px;width:2px;background:transparent;border-radius:2px;transition:background .12s ease}
.fd-ap-resize:hover::before,body.fd-ap-resizing .fd-ap-resize::before{background:var(--accent,#2383E2)}
body.fd-ap-resizing{cursor:col-resize!important;user-select:none}
body.fd-ap-resizing *{cursor:col-resize!important}
/* Header */
.fd-ap-head{display:flex;align-items:center;gap:12px;padding:16px 18px 12px}
.fd-ap-logo{width:36px;height:36px;border-radius:50%;background:var(--text-primary,#37352F);color:var(--bg-primary,#fff);display:flex;align-items:center;justify-content:center;flex-shrink:0;box-shadow:0 2px 8px rgba(0,0,0,.25)}
.fd-ap-head-main{flex:1;min-width:0}
.fd-ap-title{font-size:15px;font-weight:600;color:var(--text,#f0f0f0);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.fd-ap-sub{font-size:11.5px;color:var(--text-dim,#9aa);margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.fd-ap-headbtn{background:none;border:none;color:var(--text-dim,#999);width:30px;height:30px;border-radius:8px;cursor:pointer;font-size:18px;display:flex;align-items:center;justify-content:center;flex-shrink:0}
.fd-ap-headbtn:hover{background:var(--bg-hover,#2b2b2b);color:var(--text,#fff)}
/* Context bar */
.fd-ap-ctx{display:flex;align-items:center;gap:8px;margin:0 16px 8px;padding:7px 11px;border-radius:9px;background:rgba(35,131,226,.09);border:1px solid rgba(35,131,226,.16);font-size:12px;color:var(--text-secondary,#ddd);min-height:18px}
.fd-ap-ctx .fd-ctx-ico{font-size:13px}
.fd-ap-ctx .fd-ctx-lbl{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* No-config banner */
.fd-ap-noconfig{padding:8px 14px;font-size:12px;color:#e6b4b4;background:rgba(227,98,98,.1);border:1px solid rgba(227,98,98,.18);margin:0 16px 10px;border-radius:9px;display:flex;gap:6px;flex-wrap:wrap;align-items:center}
.fd-ap-noconfig a{color:#ff9d9d;font-weight:600;text-decoration:none}
.fd-ap-noconfig .fd-k-sub{width:100%;color:var(--text-dim,#aaa)}
/* Tabs */
.fd-ap-tabs{display:flex;gap:2px;padding:0 16px 8px;border-bottom:1px solid var(--border,rgba(255,255,255,.08))}
.fd-ap-tab{flex:1;text-align:center;padding:7px;background:none;border:none;border-radius:8px;color:var(--text-dim,#999);font-size:12.5px;cursor:pointer}
.fd-ap-tab.active{background:var(--bg-tertiary,#242424);color:var(--text,#fff);font-weight:600}
/* Body */
.fd-agent-body{flex:1;overflow-y:auto;padding:12px 16px}
.fd-ap-hist-item{padding:8px 11px;border-radius:9px;cursor:pointer;color:var(--text,#e0e0e0);font-size:13px;border:1px solid transparent;margin-bottom:2px}
.fd-ap-hist-item:hover,.fd-ap-hist-item.active{background:var(--bg-hover,#262626);border-color:var(--border,rgba(255,255,255,.08))}
/* History hover / long-press preview */
.fd-ap-preview{position:fixed;width:290px;max-width:calc(100vw - 24px);background:var(--bg-modal,#1c1c1c);border:1px solid var(--border,rgba(255,255,255,.16));border-radius:12px;box-shadow:0 16px 44px rgba(0,0,0,.6);z-index:1990;padding:11px 13px;font-size:12px;color:var(--text,#e6e6e6);display:none;pointer-events:auto}
.fd-ap-preview.show{display:block}
.fd-ap-preview-title{font-weight:600;font-size:12.5px;margin-bottom:2px;color:var(--text,#fff);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.fd-ap-preview-meta{font-size:10px;color:var(--text-dim,#999);margin-bottom:8px}
.fd-ap-preview-empty{font-size:11px;color:var(--text-dim,#aaa);padding:4px 0}
.fd-ap-preview-label{font-size:9.5px;text-transform:uppercase;letter-spacing:.5px;color:var(--text-dim,#999);margin:7px 0 3px}
.fd-ap-preview-txt{white-space:pre-wrap;word-break:break-word;line-height:1.55;max-height:72px;overflow:hidden;color:var(--text-secondary,#d0d0d0)}
/* Empty / welcome state */
.fd-ap-empty{padding:6px 2px 4px}
.fd-ap-empty-title{font-size:14px;font-weight:600;color:var(--text,#eee);margin-bottom:2px}
.fd-ap-empty-desc{font-size:12px;color:var(--text-dim,#999);margin-bottom:12px}
.fd-ap-qa{display:flex;flex-direction:column;gap:6px}
.fd-ap-qa-item{display:flex;align-items:center;gap:10px;text-align:left;background:var(--bg-tertiary,#202020);border:1px solid var(--border,rgba(255,255,255,.07));border-radius:10px;padding:9px 12px;color:var(--text,#e6e6e6);font-size:13px;cursor:pointer;transition:background .12s,border-color .12s}
.fd-ap-qa-item:hover{background:var(--bg-hover,#2a2a2a);border-color:rgba(35,131,226,.4)}
.fd-ap-qa-item .fd-qa-ico{font-size:16px;flex-shrink:0}
.fd-ap-qa-item .fd-qa-new{margin-left:auto;font-size:9px;font-weight:700;color:#fff;background:var(--accent);border-radius:99px;padding:2px 8px;letter-spacing:.3px}
/* Messages */
.fd-agent-msg{display:flex;gap:9px;margin-bottom:14px}
.fd-agent-msg-avatar{width:28px;height:28px;border-radius:50%;background:var(--bg-tertiary,#2b2b2b);display:flex;align-items:center;justify-content:center;font-size:15px;flex-shrink:0}
.fd-agent-msg-assistant .fd-agent-msg-avatar{background:rgba(35,131,226,.18)}
.fd-agent-msg-body{flex:1;min-width:0}
.fd-agent-msg-meta{font-size:10px;color:var(--text-dim,#888);margin-bottom:2px;text-transform:uppercase;letter-spacing:.5px}
.fd-agent-msg-content{font-size:13.5px;color:var(--text,#ececec);white-space:pre-wrap;word-break:break-word;line-height:1.55}
.fd-agent-msg-content a{color:var(--accent)}
.fd-agent-msg-proposal .fd-agent-msg-content{background:rgba(35,131,226,.07);border:1px solid rgba(35,131,226,.22);border-radius:10px;padding:10px 12px}
.fd-proposal-bar{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:8px}
.fd-proposal-btn{background:var(--bg-tertiary,#242424);border:1px solid var(--border,rgba(255,255,255,.12));color:var(--text,#e6e6e6);border-radius:8px;padding:6px 12px;font-size:12.5px;cursor:pointer}
.fd-proposal-btn:hover{background:var(--bg-hover,#2e2e2e)}
.fd-proposal-btn.primary{background:var(--accent,#2383E2);border-color:transparent;color:#fff;font-weight:600}
.fd-proposal-btn:disabled{opacity:.5;cursor:not-allowed}
.fd-proposal-note{font-size:11px;color:var(--text-dim,#999)}
.fd-agent-thinking{font-size:12.5px;color:var(--text-dim,#999);padding:10px 12px;border-radius:9px;background:rgba(255,255,255,.04);margin:4px 0 8px;font-style:italic}
.fd-ap-err{color:#ef8a8a;font-size:12px;padding:4px 2px}
.fd-ap-notice{color:var(--text-secondary,#ccc);font-size:12px;padding:7px 10px;margin:2px 0 8px;border-radius:9px;background:rgba(128,128,128,.12);border:1px solid var(--border,rgba(255,255,255,.12))}
/* Steps accordion (Notion-style reasoning + actions audit trail) */
.fd-steps{margin:0 0 8px;border:1px solid var(--border,rgba(255,255,255,.11));border-radius:10px;background:rgba(255,255,255,.03);overflow:hidden}
.fd-steps-head{display:flex;align-items:center;gap:7px;width:100%;padding:7px 11px;background:transparent;border:none;color:var(--text-secondary,#ccc);font-size:12px;cursor:pointer;text-align:left}
.fd-steps-head:hover{background:rgba(255,255,255,.05)}
.fd-steps-chev{display:inline-block;font-size:9px;color:var(--text-dim,#999);transition:transform .15s;width:12px;flex-shrink:0;text-align:center}
.fd-steps-chev.down{transform:rotate(90deg)}
.fd-steps-count{font-weight:600;color:var(--text,#eee)}
.fd-steps-elem{font-weight:400;color:var(--text-dim,#999);margin-left:6px}
.fd-steps-body{border-top:1px solid var(--border,rgba(255,255,255,.08));padding:6px 8px 8px;max-height:340px;overflow-y:auto}
.fd-step-thought{margin:3px 0}
.fd-step-thought-head{display:flex;align-items:center;gap:6px;width:100%;border:none;background:transparent;color:var(--text-dim,#b9b9b9);font-size:12px;text-align:left;cursor:pointer;padding:4px 5px;border-radius:7px}
.fd-step-thought-head:hover{background:rgba(128,128,128,.1);color:var(--text,#fff)}
.fd-step-thought-title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.fd-step-thought-title .fd-t-ico{opacity:.85}
.fd-step-thought-body{margin:2px 0 4px 27px;padding:7px 10px;font-size:12px;color:var(--text-secondary,#cfcfcf);background:rgba(128,128,128,.1);border-left:2px solid rgba(35,131,226,.45);border-radius:0 8px 8px 0;white-space:pre-wrap;word-break:break-word;line-height:1.55}
.fd-step-action{display:flex;align-items:flex-start;gap:7px;padding:4px 7px;font-size:12px;color:var(--text-secondary,#c9c9c9)}
.fd-step-action .fd-step-ico{flex-shrink:0;width:14px;text-align:center;font-size:11px;margin-top:1px}
.fd-step-action.success{color:var(--text,#d9d9d9)}
.fd-step-action.success .fd-step-ico{color:#58c47a}
.fd-step-action.error{color:#e78c8c}
.fd-step-action.error .fd-step-ico{color:#ef6a6a}
.fd-step-action-detail{font-size:11px;color:#e0a3a3;display:block;margin-top:1px}
/* Composer */
.fd-ap-composer{padding:10px 14px 14px;border-top:1px solid var(--border,rgba(255,255,255,.09));position:relative}
.fd-ap-box{background:var(--bg-tertiary,#1f1f1f);border:1px solid var(--border,rgba(255,255,255,.13));border-radius:16px;padding:10px 12px 8px;transition:border-color .12s,box-shadow .12s}
.fd-ap-box:focus-within{border-color:rgba(35,131,226,.65);box-shadow:0 0 0 3px rgba(35,131,226,.12)}
.fd-ap-input{width:100%;background:transparent;border:none;outline:none;color:var(--text,#fff);font-size:14px;resize:none;font-family:inherit;line-height:1.6;max-height:220px;box-sizing:border-box;overflow-y:auto;white-space:pre-wrap;word-break:break-word;min-height:26px;position:relative}
.fd-ap-input.empty::before{content:attr(data-placeholder);color:var(--text-dim,#8a8a8a);position:absolute;left:0;top:0;pointer-events:none;white-space:pre-wrap;max-width:100%}
.ment-token{display:inline-flex;align-items:center;gap:4px;vertical-align:middle;background:rgba(35,131,226,.14);border:1px solid rgba(35,131,226,.35);color:var(--text,#eaf2ff);border-radius:6px;padding:0 3px 0 4px;font-size:13px;line-height:1.5;margin:0 1px;max-width:200px;overflow:hidden;white-space:nowrap}
.ment-token .tk-lbl{overflow:hidden;text-overflow:ellipsis}
.ment-token.skill{background:rgba(168,85,247,.16);border-color:rgba(168,85,247,.5);color:#f3e8ff}
.ment-token .tk-ico{flex-shrink:0;opacity:.9}
.fd-ap-toolbar{display:flex;align-items:center;gap:8px;margin-top:6px;flex-wrap:wrap}
.fd-ap-tip{font-size:11px;color:var(--text-dim,#8f8f8f);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.fd-ap-tools{display:flex;align-items:center;gap:6px;margin-left:auto}
.fd-ap-model{display:flex;align-items:center;gap:6px;background:rgba(255,255,255,.05);border:1px solid var(--border);border-radius:99px;padding:4px 10px;font-size:11.5px;color:var(--text-secondary,#ddd);cursor:pointer;white-space:nowrap;max-width:220px;overflow:hidden;text-overflow:ellipsis}
.fd-ap-model:hover{background:var(--bg-hover,#2c2c2c)}
.fd-ap-model.off{opacity:.75}
.fd-ap-send{width:34px;height:34px;border-radius:50%;border:none;background:var(--accent,#2383E2);color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:15px;transition:background .12s,transform .12s;flex-shrink:0}
.fd-ap-send:hover{background:#2c8ceb}
.fd-ap-send:disabled{opacity:.4;cursor:not-allowed;transform:none}
.fd-ap-send:not(:disabled):active{transform:scale(.94)}
/* Model picker popover */
.fd-ap-pop{position:absolute;right:14px;bottom:calc(100% + 6px);width:270px;background:var(--bg-modal,#1c1c1c);border:1px solid var(--border,rgba(255,255,255,.12));border-radius:12px;box-shadow:0 12px 40px rgba(0,0,0,.5);padding:12px;z-index:60}
.fd-ap-pop label{display:block;font-size:10.5px;text-transform:uppercase;letter-spacing:.5px;color:var(--text-dim,#999);margin:8px 0 4px}
.fd-ap-pop select{width:100%;background:var(--bg-tertiary,#232323);border:1px solid var(--border);border-radius:8px;color:var(--text,#fff);font-size:12.5px;padding:7px 8px;outline:none}
.fd-ap-pop a{display:inline-block;margin-top:10px;font-size:12px;color:var(--accent);text-decoration:none}
/* Slash command menu */
.fd-agent-cmd-menu{position:absolute;bottom:calc(100% + 4px);left:0;right:0;background:var(--bg-modal,#1e1e1e);border:1px solid var(--border,rgba(255,255,255,.12));border-radius:11px;max-height:190px;overflow-y:auto;box-shadow:0 -8px 26px rgba(0,0,0,.4);z-index:70;display:none}
.fd-agent-cmd-menu.show{display:block}
.fd-agent-cmd-item{padding:8px 12px;font-size:12px;color:var(--text,#eee);cursor:pointer;border-radius:7px;margin:3px;display:flex;gap:8px;align-items:baseline}
.fd-agent-cmd-item:hover{background:var(--bg-hover,#2b2b2b)}
.fd-agent-cmd-item.focus{background:var(--bg-hover,#2b2b2b);box-shadow:inset 2px 0 0 var(--accent,#2383E2);color:var(--text,#fff)}
.fd-agent-cmd-item b{color:var(--accent,#2383E2);font-family:monospace;white-space:nowrap}
.fd-agent-cmd-item b.k-cmd{color:var(--accent,#2383E2)}
.fd-agent-cmd-item b.k-skill{color:#c084fc}
.fd-agent-cmd-item span{color:var(--text-dim,#999);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.fd-agent-cmd-sep{font-size:10px;text-transform:uppercase;letter-spacing:.6px;color:var(--text-dim,#888);padding:8px 14px 2px}
/* Action bar under each assistant post */
.fd-agent-msg-actions{display:flex;gap:5px;flex-wrap:wrap;margin-top:7px;align-items:center}
.fd-act-btn{display:inline-flex;align-items:center;justify-content:center;gap:0;background:transparent;border:1px solid var(--border,rgba(255,255,255,.09));color:var(--text-dim,#9aa);border-radius:7px;width:30px;height:26px;font-size:13px;cursor:pointer;transition:background .12s,color .12s,border-color .12s;line-height:1}
.fd-act-btn:hover{background:var(--bg-hover,#262626);color:var(--text,#fff);border-color:rgba(255,255,255,.25)}
.fd-act-btn.on{background:rgba(35,131,226,.18);border-color:rgba(35,131,226,.55);color:#fff}
.fd-act-btn:disabled,.fd-act-btn.disabled{opacity:.4;cursor:not-allowed}
/* Chips (contexte épinglé @ / + / skills) */
.fd-ap-chips{display:flex;flex-direction:column;gap:5px;padding:8px 4px 4px;max-height:92px;overflow-y:auto}
.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}
.fd-chip-x{background:none;border:none;color:var(--text-dim,#aaa);cursor:pointer;font-size:13px;line-height:1;padding:0 4px;border-radius:50%;flex-shrink:0}
.fd-chip-x:hover{color:#fff;background:rgba(255,255,255,.14)}
/* Add button (+ contexte) */
.fd-ap-add{width:30px;height:30px;border-radius:50%;background:var(--bg-tertiary,#262626);border:1px solid var(--border,rgba(255,255,255,.14));color:var(--text,#ddd);cursor:pointer;font-size:17px;display:flex;align-items:center;justify-content:center;flex-shrink:0;line-height:1}
.fd-ap-add:hover{background:var(--bg-hover,#303030);color:#fff}
/* Mention menu (@ / +) */
.fd-ap-mention-menu{position:absolute;bottom:calc(100% + 4px);left:0;right:0;background:var(--bg-modal,#1e1e1e);border:1px solid var(--border,rgba(255,255,255,.12));border-radius:11px;max-height:220px;overflow-y:auto;box-shadow:0 -8px 26px rgba(0,0,0,.4);z-index:70;display:none}
.fd-ap-mention-menu.show{display:block}
.fd-mention-item{display:flex;align-items:center;gap:9px;padding:7px 12px;font-size:12.5px;color:var(--text,#eee);cursor:pointer;border-radius:7px;margin:2px 4px}
.fd-mention-item:hover,.fd-mention-item.focus{background:var(--bg-hover,#2b2b2b)}
.fd-mention-ico{width:24px;text-align:center;flex-shrink:0;font-size:14px}
.fd-mention-main{flex:1;min-width:0;display:flex;flex-direction:column}
.fd-mention-lbl{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.fd-mention-sub{font-size:10.5px;color:var(--text-dim,#999);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.fd-mention-sep{font-size:10px;text-transform:uppercase;letter-spacing:.6px;color:var(--text-dim,#888);padding:8px 14px 2px}
.fd-mention-empty,.fd-mention-load{padding:11px 12px;font-size:12px;color:var(--text-dim,#aaa);text-align:center}
/* Menu + (sections) — réutilise le skin du menu de mentions */
.fd-ap-plus-menu{max-height:min(340px,60vh)}
.fd-ap-plus-menu .fd-mention-item.disabled{opacity:.45;cursor:default;background:none}
.fd-ap-plus-menu .fd-mention-item.disabled:hover{background:none}
.fd-plus-head{display:flex;align-items:center;gap:8px;padding:7px 11px 6px;border-bottom:1px solid var(--border,rgba(255,255,255,.08));margin-bottom:3px}
.fd-plus-back{background:none;border:1px solid var(--border,rgba(255,255,255,.14));color:var(--text,#ddd);border-radius:7px;width:26px;height:22px;cursor:pointer;line-height:1;flex-shrink:0}
.fd-plus-back:hover{background:var(--bg-hover,#2b2b2b)}
.fd-plus-title{font-size:11px;font-weight:600;color:var(--text-secondary,#ddd);text-transform:uppercase;letter-spacing:.4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.fd-plus-filter{padding:7px 11px 2px}
.fd-plus-filter input{width:100%;background:var(--bg-tertiary,#232323);border:1px solid var(--border,rgba(255,255,255,.14));border-radius:8px;color:var(--text,#fff);font-size:12.5px;padding:6px 8px;outline:none;box-sizing:border-box}
.fd-plus-form{padding:4px 11px 12px}
.fd-plus-form label{display:block;font-size:10.5px;text-transform:uppercase;letter-spacing:.5px;color:var(--text-dim,#999);margin:9px 0 3px}
.fd-plus-form input,.fd-plus-form textarea{width:100%;background:var(--bg-tertiary,#232323);border:1px solid var(--border,rgba(255,255,255,.14));border-radius:8px;color:var(--text,#fff);font-size:12.5px;padding:7px 8px;outline:none;font-family:inherit;box-sizing:border-box}
.fd-plus-form textarea{min-height:110px;resize:vertical;line-height:1.5}
.fd-plus-file{display:none}
.fd-plus-chev{margin-left:auto;color:var(--text-dim,#999);opacity:.75;font-size:15px;flex-shrink:0}
.fd-plus-crumb{display:flex;gap:5px;flex-wrap:wrap;padding:7px 11px 3px;font-size:11px;border-bottom:1px solid var(--border,rgba(255,255,255,.08));margin-bottom:3px}
.fd-plus-crumb-it{color:var(--accent,#2383E2);cursor:pointer;white-space:nowrap}
.fd-plus-crumb-it:hover{text-decoration:underline}
/* Toast */
.fd-ap-toast{position:absolute;left:14px;right:14px;bottom:calc(100% + 10px);background:var(--bg-modal,#1c1c1c);border:1px solid rgba(35,131,226,.5);color:var(--text,#fff);font-size:12px;border-radius:9px;padding:8px 12px;box-shadow:0 10px 30px rgba(0,0,0,.45);z-index:90;display:flex;gap:8px;align-items:center;justify-content:space-between}
.fd-ap-toast.err{border-color:rgba(227,98,98,.55)}
.fd-ap-toast button{background:none;border:none;color:var(--text-dim);cursor:pointer;font-size:13px;padding:0}
[x-cloak]{display:none!important}
.fd-agent-msg-markdown{line-height:1.6}
.fd-agent-msg-markdown p{margin:0 0 8px}
.fd-agent-msg-markdown h1,.fd-agent-msg-markdown h2,.fd-agent-msg-markdown h3,.fd-agent-msg-markdown h4{margin:12px 0 6px;font-weight:700;line-height:1.25}
.fd-agent-msg-markdown h1{font-size:1.4em}
.fd-agent-msg-markdown h2{font-size:1.25em}
.fd-agent-msg-markdown h3{font-size:1.1em}
.fd-agent-msg-markdown h4{font-size:1em}
.fd-agent-msg-markdown ul,.fd-agent-msg-markdown ol{margin:0 0 8px;padding-left:20px}
.fd-agent-msg-markdown li{margin:2px 0}
.fd-agent-msg-markdown code{background:rgba(127,127,127,.18);border-radius:4px;padding:1px 5px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.9em}
.fd-agent-msg-markdown pre{background:var(--bg-code,var(--bg-tertiary,#1f1f1f));border:1px solid var(--border,rgba(255,255,255,.12));border-radius:8px;padding:12px;overflow-x:auto;margin:8px 0;color:var(--text,#ececec)}
.fd-agent-msg-markdown pre code{background:none;padding:0;font-size:12px;line-height:1.5}
.fd-md-table-wrap{overflow-x:auto;margin:8px 0}
.fd-md-table{border-collapse:collapse;width:100%;font-size:13px}
.fd-md-table th,.fd-md-table td{border:1px solid var(--border,#333);padding:6px 10px;text-align:left}
.fd-md-table th{background:rgba(127,127,127,.14);font-weight:600}
.fd-md-table tr:nth-child(even) td{background:rgba(127,127,127,.06)}
.fd-md-quote{border-left:3px solid var(--accent,#2383E2);margin:8px 0;padding:2px 12px;color:var(--text-dim)}
.fd-md-quote p{margin:4px 0}
.fd-md-hr{border:none;border-top:1px solid var(--border,#333);margin:12px 0}
.fd-agent-msg-markdown a{color:var(--accent,#2383E2);text-decoration:none}
.fd-agent-msg-markdown a:hover{text-decoration:underline}
</style>
{# ── Global Agent API (available before Alpine for the FAB / shortcut) ── #}
<script data-cfasync="false" nonce="{{ csp_nonce() }}" src="/static/js/agent_panel_1.js?v={{ asset_version }}"></script>
{# ── Floating button ── #}
<button id="fd-agent-fab" title="FlowDeck Agent (Ctrl+J)" onclick="window.fdAgent&&window.fdAgent.toggle()"><svg viewBox="0 0 24 24" width="26" height="26" fill="currentColor" aria-hidden="true"><path d="M12 3l1.9 5.8a2 2 0 0 0 1.3 1.3L21 12l-5.8 1.9a2 2 0 0 0-1.3 1.3L12 21l-1.9-5.8a2 2 0 0 0-1.3-1.3L3 12l5.8-1.9a2 2 0 0 0 1.3-1.3L12 3Z"/></svg></button>
{# ── Panel ── #}
<div id="fd-agent-panel" class="fd-ap" :class="{open: open}" x-data="agentPanel()" @keydown.escape.window="close()" role="dialog" aria-label="Agent FlowDeck">
<div class="fd-ap-resize" x-ref="resizeHandle" role="separator" aria-orientation="vertical" aria-label="Redimensionner le panneau" tabindex="0" title="Glisser pour redimensionner — double-clic pour réinitialiser"></div>
<div class="fd-ap-head">
<span class="fd-ap-logo"><svg viewBox="0 0 24 24" width="19" height="19" fill="currentColor" aria-hidden="true"><path d="M12 3l1.9 5.8a2 2 0 0 0 1.3 1.3L21 12l-5.8 1.9a2 2 0 0 0-1.3 1.3L12 21l-1.9-5.8a2 2 0 0 0-1.3-1.3L3 12l5.8-1.9a2 2 0 0 0 1.3-1.3L12 3Z"/></svg></span>
<div class="fd-ap-head-main">
<div class="fd-ap-title" x-text="currentConv && currentConv.title ? currentConv.title : 'How can I help you today?'"></div>
<div class="fd-ap-sub" x-text="providerModelLabel"></div>
</div>
<button class="fd-ap-headbtn" title="Nouvelle conversation" @click="newConversation()">+</button>
<button class="fd-ap-headbtn" title="Fermer (Ctrl+J)" @click="close()">✕</button>
</div>
<div class="fd-ap-noconfig" x-show="!hasActiveProvider">
<span>⚠️ Aucun fournisseur activé et fonctionnel.</span>
<a href="/settings" target="_blank" @click.stop>Configurer → Agent &amp; IA</a>
<template x-if="unverifiedCount">
<span class="fd-k-sub">🔑 <span x-text="unverifiedCount"></span> clé(s) à tester : Paramètres puis « Tester ».</span>
</template>
</div>
<div class="fd-ap-tabs">
<button class="fd-ap-tab" :class="{active: tab==='chat'}" @click="tab='chat'">Conversation</button>
<button class="fd-ap-tab" :class="{active: tab==='history'}" @click="showHistory()">Historique</button>
</div>
<div class="fd-agent-body">
<template x-if="tab==='history'">
<div>
<template x-for="c in conversations" :key="c.id">
<div class="fd-ap-hist-item" :class="{active: currentConv && currentConv.id===c.id}"
@click="historyClick(c.id)"
@mouseenter="previewConversation(c, $event)" @mouseleave="schedulePreviewHide()"
@touchstart="startLongPress(c, $event)" @touchend="onTouchEnd()"
@touchmove="onTouchCancel()" @touchcancel="onTouchCancel()">
<div x-text="c.title || 'New conversation'"></div>
<div style="font-size:11px;color:var(--text-dim)" x-text="c.updated_at"></div>
</div>
</template>
<div x-show="!conversations.length" style="font-size:13px;color:var(--text-dim);padding:10px">Aucune conversation. Démarrez-en une !</div>
</div>
</template>
<template x-if="tab==='chat'">
<div>
<!-- Welcome / quick actions -->
<div class="fd-ap-empty" x-show="!messages.length && !streaming" x-cloak>
<div class="fd-ap-empty-desc" x-text="quickIntro"></div>
<div class="fd-ap-qa">
<template x-for="q in quickActions" :key="q.label">
<button class="fd-ap-qa-item" type="button" @click="runQuick(q)">
<span class="fd-qa-ico" x-text="q.icon"></span>
<span x-text="q.label"></span>
<span class="fd-qa-new" x-show="q.new" x-cloak>New</span>
</button>
</template>
</div>
</div>
<template x-for="m in messages" :key="m.id">
<div class="fd-agent-msg" :class="'fd-agent-msg-'+(m.role||'user') + (m.proposal ? ' fd-agent-msg-proposal' : '')">
<div class="fd-agent-msg-avatar" x-text="m.role==='assistant'?'🤖':'👤'"></div>
<div class="fd-agent-msg-body">
<div class="fd-agent-msg-meta" x-text="(m.role==='assistant'?'Agent':'Vous')+(m.model?' · '+m.model:'')"></div>
<!-- Steps accordion (reasoning thoughts + tool actions) -->
<div class="fd-steps" x-show="m.steps && m.steps.length" x-cloak>
<button class="fd-steps-head" type="button" @click="toggleSteps(m)">
<span class="fd-steps-chev" :class="{down: m.stepsOpen}">▶</span>
<span class="fd-steps-count" x-text="!m.steps ? '' : (m.steps.length === 1 ? '1 étape' : m.steps.length + ' étapes')"></span>
<span class="fd-steps-elem" x-text="m.generating ? 'en cours…' : (m.stepsOpen ? 'réduire' : 'voir le déroulé')"></span>
</button>
<div class="fd-steps-body" x-show="m.stepsOpen" x-cloak>
<template x-for="(s, si) in m.steps" :key="si">
<div>
<template x-if="s.type==='thought'">
<div class="fd-step-thought">
<button class="fd-step-thought-head" type="button" @click="toggleStep(s)">
<span class="fd-steps-chev" :class="{down: s.open}">▶</span>
<span class="fd-step-thought-title"><span class="fd-t-ico">💭</span> <span x-text="s.title || 'Réflexion'"></span></span>
</button>
<div class="fd-step-thought-body" x-show="s.open" x-text="s.content"></div>
</div>
</template>
<template x-if="s.type==='action'">
<div class="fd-step-action" :class="s.status || 'success'" :title="(s.detail && s.status==='error') ? s.detail : s.tool || ''">
<span class="fd-step-ico" x-text="(s.status==='success') ? '✓' : '✗'"></span>
<div>
<div x-text="s.label"></div>
<div class="fd-step-action-detail" x-show="s.status==='error' && s.detail" x-text="s.detail" x-cloak></div>
</div>
</div>
</template>
</div>
</template>
</div>
</div>
<div class="fd-agent-msg-content" x-show="m.generating" x-cloak>Génération…</div>
<div class="fd-agent-msg-content fd-agent-msg-markdown" x-show="!m.generating && m.role==='assistant' && m.content" x-init="bindMarkdown($el, m)"></div>
<div class="fd-agent-msg-content" x-show="!m.generating && m.role!=='assistant' && m.content" x-text="m.content"></div>
<template x-if="m.proposal">
<div class="fd-proposal-bar">
<button class="fd-proposal-btn primary" type="button" @click="approveProposal(m)" :disabled="m.applied || m.offline">
<span x-text="m.applied ? (m.rejected ? 'Rejetée' : '✓ Appliquée') : '✓ Appliquer au document'"></span>
</button>
<button class="fd-proposal-btn" type="button" @click="rejectProposal(m)" :disabled="m.applied">✕ Rejeter</button>
<span class="fd-proposal-note" x-show="m.offline" x-cloak>Hors-ligne — configurez une clé API pour appliquer la modification.</span>
</div>
</template>
<template x-if="m.role==='assistant' && !m.generating && m.content && !m.act">
<div class="fd-agent-msg-actions">
<button class="fd-act-btn" type="button" @click="copyResponse(m)" title="Copier la réponse">⧉</button>
<button class="fd-act-btn" type="button" @click="insertIntoPage(m)" :disabled="!canInsert" title="Insérer dans la page ouverte">➕</button>
<button class="fd-act-btn" :class="{on: m.fb==='up'}" type="button" @click="feedback(m,'up')" :disabled="!!m.fb && m.fb!=='up'" title="Feedback positif">👍</button>
<button class="fd-act-btn" :class="{on: m.fb==='down'}" type="button" @click="feedback(m,'down')" :disabled="!!m.fb && m.fb!=='down'" title="Feedback négatif">👎</button>
</div>
</template>
</div>
</div>
</template>
<div class="fd-agent-thinking" x-show="streaming && !(_livePh && _livePh.steps && _livePh.steps.length)">L'agent réfléchit et agit… <span x-text="liveReasoning"></span></div>
<div class="fd-ap-notice" x-show="notice" x-cloak x-text="notice"></div>
<div class="fd-ap-err" x-show="errorMsg" x-text="errorMsg"></div>
</div>
</template>
</div>
<div class="fd-ap-composer">
<div style="position:relative;">
<div class="fd-ap-toast" x-show="toastMsg" x-cloak :class="{err: toastErr}">
<span x-text="toastMsg"></span>
<button type="button" @click="toastMsg=''">✕</button>
</div>
<div class="fd-agent-cmd-menu" x-ref="cmdMenu" :class="{show: commandOpen && commandSuggestions.length && !slashDismiss}">
<template x-for="c in commandSuggestions" :key="c.key">
<div>
<div class="fd-agent-cmd-sep" x-show="c._sep" x-text="c._sep"></div>
<div class="fd-agent-cmd-item" x-show="!c._sep" :class="{focus: c._i===cmdFocus}" @click="useSuggestion(c)" @mouseenter="cmdFocus=c._i">
<b :class="c._admin ? 'k-cmd' : 'k-skill'" x-text="(c._admin ? c.cmd : c.cmd)"></b>
<span x-text="c.desc"></span>
</div>
</div>
</template>
</div>
<div class="fd-ap-mention-menu" :class="{show: mentionOpen}" x-ref="mentionMenu">
<div class="fd-mention-load" x-show="mentionLoad" x-cloak>Recherche…</div>
<template x-if="!mentionLoad && !hasMentionItems">
<div class="fd-mention-empty">Aucun élément. Tapez un nom de document, page ou base.</div>
</template>
<template x-if="!mentionLoad">
<template x-for="it in mentionItems" :key="it.token || it.key">
<div>
<div class="fd-mention-sep" x-show="it._sep" x-text="it._sep"></div>
<div class="fd-mention-item" x-show="!it._sep" :class="{focus: it._i===mentionFocus}"
@click="pickMention(it)" @mouseenter="setMentionFocus(it._i)">
<span class="fd-mention-ico" x-text="it.icon"></span>
<div class="fd-mention-main">
<div class="fd-mention-lbl" x-text="it.label"></div>
<div class="fd-mention-sub" x-text="it.sub"></div>
</div>
</div>
</div>
</template>
</template>
</div>
<div class="fd-ap-mention-menu fd-ap-plus-menu" :class="{show: plusOpen}" x-ref="plusMenu">
<div class="fd-plus-head" x-show="plusSection!=='root'" x-cloak>
<button class="fd-plus-back" type="button" @click="plusBack()" title="Retour" aria-label="Retour">←</button>
<span class="fd-plus-title" x-text="plusTitle"></span>
</div>
<div class="fd-plus-filter" x-show="plusSection==='skills-gallery'" x-cloak>
<input type="search" placeholder="Filtrer les compétences…" aria-label="Filtrer les compétences" x-model="galleryFilter">
</div>
<div class="fd-plus-crumb" x-show="plusSection==='browse'">
<span class="fd-plus-crumb-it" @click="plusGoto(-1)">Racine</span>
<template x-for="(c, ci) in plusCrumb" :key="c.id">
<span class="fd-plus-crumb-it" @click="plusGoto(ci)" x-text="'› ' + c.name"></span>
</template>
</div>
<div class="fd-mention-load" x-show="plusSection==='browse' && plusLoading" x-cloak>Chargement…</div>
<div class="fd-mention-empty" x-show="plusSection==='browse' && !plusLoading && !plusItems.length" x-cloak>Dossier vide</div>
<template x-for="(it, i) in plusItems" :key="it.key">
<div>
<div class="fd-mention-sep" x-show="it._sep" x-text="it._sep"></div>
<div class="fd-mention-item" x-show="!it._sep" :class="{focus: i===plusFocus, disabled: !!it.disabled}"
@click="plusAction(it)" @mouseenter="it.disabled ? null : plusFocus = i">
<span class="fd-mention-ico" x-text="it.icon"></span>
<div class="fd-mention-main">
<div class="fd-mention-lbl" x-text="it.label"></div>
<div class="fd-mention-sub" x-text="it.sub"></div>
</div>
<span class="fd-plus-chev" x-show="it.chev || it._folder">›</span>
</div>
</div>
</template>
<div class="fd-plus-form" x-show="plusSection==='skills-edit'" x-cloak>
<label for="fd-plus-skill-name">Nom</label>
<input id="fd-plus-skill-name" type="text" x-model="skillForm.name" placeholder="ex. synthese-reunion" @keydown.enter.prevent="saveSkill()">
<label for="fd-plus-skill-desc">Description</label>
<input id="fd-plus-skill-desc" type="text" x-model="skillForm.description" placeholder="Une phrase">
<label for="fd-plus-skill-prompt">Prompt (consigne appliquée à la demande)</label>
<textarea id="fd-plus-skill-prompt" x-model="skillForm.prompt_template" placeholder="Fais…"></textarea>
<div class="fd-proposal-bar">
<button class="fd-proposal-btn primary" type="button" @click="saveSkill()">✓ Enregistrer</button>
<button class="fd-proposal-btn" type="button" x-show="skillForm.id" @click="deleteSkill()" x-cloak>✕ Supprimer</button>
<button class="fd-proposal-btn" type="button" x-show="skillForm.id" @click="exportSkill()" x-cloak>⤓ Export</button>
<button class="fd-proposal-btn" type="button" @click="plusBack()">← Retour</button>
</div>
</div>
<div class="fd-plus-form" x-show="plusSection==='connector-new'" x-cloak>
<label for="fd-plus-cn-kind">Type</label>
<select id="fd-plus-cn-kind" x-model="connectorForm.kind" @change="connectorPreset()">
<option value="custom">Connecteur personnalisé (HTTP + clé)</option>
<option value="discord">Discord (bot)</option>
<option value="telegram">Telegram (bot)</option>
<option value="mcp">Serveur MCP (streamable HTTP)</option>
</select>
<label for="fd-plus-cn-name">Nom</label>
<input id="fd-plus-cn-name" type="text" x-model="connectorForm.name" placeholder="ex. Base de connaissances" @keydown.enter.prevent="connectorCreate()">
<label for="fd-plus-cn-url" x-text="connectorForm.hint || 'URL publique (hôtes privés refusés)'"></label>
<input id="fd-plus-cn-url" type="text" x-model="connectorForm.url" placeholder="https://api.exemple.com">
<label for="fd-plus-cn-secret">Clé / jeton — chiffrée, jamais renvoyée</label>
<input id="fd-plus-cn-secret" type="password" x-model="connectorForm.secret" placeholder="sk-…" autocomplete="off">
<div class="fd-proposal-bar">
<button class="fd-proposal-btn primary" type="button" @click="connectorCreate()">✓ Ajouter</button>
<button class="fd-proposal-btn" type="button" @click="plusBack()">← Retour</button>
</div>
</div>
<input type="file" accept="application/json,.json" x-ref="skillImport" class="fd-plus-file" aria-label="Importer un skill JSON" @change="importSkillFile($event)">
</div>
</div>
<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 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.stop="removeChip(chip.token)" title="Retirer du contexte">✕</button>
</span>
</template>
</div>
<div class="fd-chip-row" x-show="skillChips.length">
<template x-for="chip in skillChips" :key="chip.key">
<span class="fd-chip skill">
<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 le skill">✕</button>
</span>
</template>
</div>
</div>
<div class="fd-ap-box">
<div class="fd-ap-input empty" x-ref="cmdInput" contenteditable="true" spellcheck="false" role="textbox" aria-multiline="true"
data-placeholder="Demandez à l'agent… Tapez @ pour référencer une page/base, / pour un skill. Entrée = envoyer, Maj+Entrée = ligne"
@input="onInput()" @keydown="onComposerKeydown($event)" @paste="onPaste($event)"></div>
<div class="fd-ap-toolbar">
<button class="fd-ap-add" type="button" title="Ajouter au contexte : fichiers, répertoires, compétences…" aria-label="Menu d'ajout au contexte" aria-haspopup="menu" :aria-expanded="plusOpen" @click="toggleAddPicker()">+</button>
<span class="fd-ap-tip" x-text="composerHint"></span>
<div class="fd-ap-tools">
<button class="fd-ap-model" :class="{off: !hasActiveProvider}" type="button" @click="modelOpen=!modelOpen" title="Choisir fournisseur et modèle">
<span x-text="autoLabel"></span> <span style="opacity:.6">▾</span>
</button>
<button class="fd-ap-send" @click="send()" :disabled="streaming || !canSend" title="Envoyer">▲</button>
</div>
</div>
</div>
<div class="fd-ap-pop" x-show="modelOpen" @click.outside="modelOpen=false" x-cloak>
<label>Fournisseur</label>
<select x-model="llmProvider" @change="onProviderChange()">
<template x-for="p in providers" :key="p.id">
<option :value="p.id" x-text="p.id==='offline' ? 'Hors-ligne (mock)' : p.name"></option>
</template>
</select>
<label>Modèle</label>
<select x-model="llmModel" @change="patchConversation()" x-show="llmProvider!=='offline'">
<template x-for="m in providerModels" :key="m">
<option :value="m" x-text="m"></option>
</template>
</select>
<a href="/settings" target="_blank" @click="modelOpen=false">⚙️ Configurer (clés API) →</a>
</div>
</div>
{# Historique : aperçu (hover / long-press) #}
<div class="fd-ap-preview" x-ref="convPreview" :class="{show: previewVisible}"
@mouseenter="cancelPreviewHide()" @mouseleave="schedulePreviewHide()" x-cloak>
<div class="fd-ap-preview-title" x-text="previewTitle"></div>
<div class="fd-ap-preview-meta" x-text="previewMeta"></div>
<div class="fd-ap-preview-empty" x-show="!previewStart && !previewEnd" x-text="previewEmptyHint"></div>
<div class="fd-ap-preview-label" x-show="previewStart" x-cloak>Début</div>
<div class="fd-ap-preview-txt" x-show="previewStart" x-text="previewStart"></div>
<div class="fd-ap-preview-label" x-show="previewEnd" x-cloak>Réponse</div>
<div class="fd-ap-preview-txt" x-show="previewEnd" x-text="previewEnd"></div>
</div>
</div>
<script data-cfasync="false" nonce="{{ csp_nonce() }}" src="/static/js/agent_panel_2.js?v={{ asset_version }}"></script>