# Changelog — ObsiGate Toutes les modifications notables d'ObsiGate sont documentées dans ce fichier. Format basé sur [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/), et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). > **En cours de développement** : les changements à venir sont listés dans la section > [Unreleased](#unreleased). La dernière version livrée est **2.10.1**. --- ## [Unreleased] --- ## [2.10.1] — 2026-09-17 ### Corrigé - **BUG-059 — Assistant IA : un clic dans la conversation faisait sauter le texte au bas de la fenêtre** : dans une conversation ouverte (post ancré en haut), tout clic — lien de fichier, étapes, sélection de texte — dépinait l'ancre via le gestionnaire `mousedown` et retirait le padding d'ancre, bornant le scroll à la nouvelle hauteur max (saut au bas). Le dépintage est désormais réservé aux vrais gestes de scroll : molette, tactile et poignée de scroll uniquement (`isScrollbarPress`). Fichiers : `frontend/js/bookslm.js`, `tests/frontend/ai.test.mjs`. - **#92 — `create_pdf` de l'Assistant : tableaux mal formatés** : l'outil `create_pdf` (génération PDF via l'assistant) utilisait un rendu reportlab simplifié sans support des tableaux. Il passe désormais par le même pipeline que le bouton « Télécharger PDF » de la page document (mistune + plugin `table` + WeasyPrint), avec repli automatique sur le rendu simple si WeasyPrint n'est pas disponible (GTK absent). Fichiers : `backend/tools/documents.py`, `tests/test_document_tools.py`. --- ## [2.10.0] — 2026-09-17 ### Ajouté - **#92 — Assistant IA : écosystème d'outils phase 2 (web étendu, sources connectées, documents)** : - **Recherche web à clé** : fournisseurs optionnels essayés avant SearXNG — Tavily, Brave Search, SerpAPI (Google) et Exa (`OBSIGATE_TAVILY_API_KEY`, `OBSIGATE_BRAVE_API_KEY`, `OBSIGATE_SERPAPI_API_KEY`, `OBSIGATE_EXA_API_KEY`), avec ordre personnalisable via `OBSIGATE_WEB_PROVIDERS`. - **Transverse** : réessais réseau avec backoff maison (`OBSIGATE_WEB_RETRY`) et cache SQLite des résultats web avec TTL (`OBSIGATE_WEB_CACHE_TTL`, 900 s par défaut, `OBSIGATE_WEB_CACHE_PATH`). - **Rendu dynamique** : `fetch_url(render=True)` délègue les pages SPA à un worker Playwright isolé (dépendance optionnelle, dégradation propre si non installée). - **Crawl de site** : `crawl_site` (WRITE, confirmation) capture jusqu'à 20 pages d'un même hôte et enregistre un condensé Markdown dans un vault. - **Sources connectées** : Gitea (`OBSIGATE_GITEA_URL`/`OBSIGATE_GITEA_TOKEN`) et GitHub (`OBSIGATE_GITHUB_TOKEN`) — `git_list_repos`, `git_search_issues`, `git_get_file` (READ, rate-limités, audités). Les drives cloud (Google Drive / OneDrive) restent orientés serveur MCP externe (#79), conformément à la feuille de route. - **Production de documents** : `create_xlsx` (openpyxl), `create_docx` (python-docx), `create_csv`, `create_pdf` (reportlab) — outils WRITE avec confirmation et sauvegarde dans le vault (backup avant écrasement). - Chaque outil : libellé `labels.py` + clés i18n `ai.step.*` FR/EN + tests unitaires mockés (httpx). Dépendances ajoutées : `openpyxl`, `python-docx`, `reportlab`. Fichiers : `backend/tools/{webcache,webrender,connected,crawler,documents,web,schemas,labels}.py`, `backend/services/mutations.py`, `tests/test_{web_cache,web_search_providers,webrender,connected_tools,document_tools,crawler}.py`. --- ## [2.9.1] — 2026-09-17 ### Corrigé - **BUG-058 — Éditeur « Editer » : la barre de numérotation de ligne ne suit pas le thème** : CodeMirror peint son gutter (colonne des numéros de ligne) avec des valeurs claires codées en dur (`#f5f5f5`, bordure `#ddd`), si bien qu'en thème sombre la barre restait gris clair alors que le fond de l'éditeur suivait le thème. Le gutter est désormais dérivé des variables CSS du thème ObsiGate (`color-mix(in srgb, var(--text-primary) …)` pour un fond subtil, `--text-secondary` pour les numéros, `--border` pour la séparation), ce qui le fait suivre tous les thèmes et modes (sombre, clair, contraste élevé, sépia). Fichiers : `frontend/style.css`. Tests : `tests/frontend/editor-inline.test.mjs`. --- ## [2.9.0] — 2026-09-17 ### Ajouté - **#102 — Assistant IA : bouton « Ajouter la section » par bloc de code** : chaque bloc de code d'une réponse de l'assistant (par ex. une section ```markdown```) affiche un bouton discret « Ajouter la section » qui insère **uniquement ce bloc** dans le document ouvert (sans les délimiteurs de code), au lieu de la réponse complète. Fichiers : `frontend/js/bookslm.js`, `frontend/style.css`, `frontend/locales/{fr,en}.json`. Tests : `tests/frontend/ai.test.mjs`. ### Corrigé - **BUG-057 — Assistant IA : bouton « Ajouter » inopérant dans l'éditeur Forge** : le bouton ne ciblait que `state.editorView` (CodeMirror de « Editer ») et affichait « Aucun document ouvert dans l'éditeur » en Forge. `_insertIntoEditor()` prend désormais en charge les trois surfaces : CodeMirror, l'iframe Forge (délégation par `postMessage({ type: 'parent-insert' })` → `insertAtCursor` dans `editor-poc.html`) et le textarea de repli. Fichiers : `frontend/js/bookslm.js`, `frontend/editor-poc.html`. Tests : `tests/frontend/ai.test.mjs`, `tests/frontend/editor-inline.test.mjs`. --- ## [2.8.4] — 2026-09-17 --- ## [2.8.3] — 2026-09-17 ### Corrigé - **BUG-056 - Éditeur Forge en plein écran : l'Assistant IA s'ouvre en arrière-plan** : le panneau de l'assistant est monté dans le document parent, alors que le plein écran Forge porte sur l'iframe — l'API Fullscreen n'affichant que l'élément plein écran et ses descendants, le panneau restait invisible. Le plein écran est désormais quitté avant d'ouvrir le panneau : côté **parent** (`sync.js`, sur `forge-open-ai` — le plein écran peut appartenir au document parent et non à l'iframe) **et** côté iframe (`editor-poc.html`, `openAssistant`), la demande d'ouverture étant émise une fois la sortie effective. Fichiers : `frontend/editor-poc.html`, `frontend/js/sync.js`. Tests : `tests/frontend/forge-completion.test.mjs` (+1), `tests/frontend/editor-inline.test.mjs` (+1). --- ## [2.8.2] — 2026-09-17 ### Corrigé - **BUG-055 (complément) - Éditeur Forge : une complétion acceptée au `Tab` était supprimée 1–2 s plus tard** : l'auto-sauvegarde (2 s) déclenche un événement SSE `index_updated` sur le fichier en cours, et le parent rechargeait alors le tampon de Forge **depuis le disque** — écrasant une complétion (ou toute frappe) faite après la sauvegarde. Le rechargement SSE est désormais ignoré tant que le tampon local est modifié (`isDirty`) ; seul un écrit externe (assistant IA) force le rechargement. L'auto-sauvegarde ne repasse plus l'état « enregistré » si des modifications sont arrivées pendant la requête (Forge **et** éditeur CodeMirror), et l'acceptation du ghost annule la requête de prédiction en attente. Fichiers : `frontend/editor-poc.html`, `frontend/js/utils.js`. Tests : `tests/frontend/forge-completion.test.mjs` (+3), `tests/frontend/editor-inline.test.mjs` (+1). --- ## [2.8.1] — 2026-09-17 ### Corrigé - **BUG-055 - Éditeur Forge : autocomplétion Tab naturelle et fiable** : la touche `Tab` déclenchait simultanément trois actions indépendantes (indentation, complétion d'un mot du document, acceptation de la prédiction IA), ce qui insérait un ou deux espaces avant le mot complété — et le retour arrière effaçait alors l'ajout. La gestion de `Tab` est désormais **unifiée** avec une priorité claire (liste de suggestions ouverte → prédiction IA → complétion de mot du document → indentation), une seule action par appui. Les helpers de complétion sont extraits en fonctions pures partagées avec CodeMirror (`getWordFragment`, `findWordCompletions`, `normalizeGhost`, `chooseTabAction`) ; plusieurs candidats affichent une liste positionnée au curseur ; la **complétion fantôme** n'affiche plus un miroir transparent de tout le document (source du décalage visuel et des espaces fantômes) mais uniquement la prédiction, positionnée exactement au curseur et nettoyée dès que le curseur bouge ou que la vue défile ; une complétion de mot ne peut plus introduire d'espace. Le prompt `/api/ai/inline-complete` est simplifié et borné à 128 tokens pour des suggestions plus courtes et plus rapides. Fichiers : `frontend/editor-poc.html`, `frontend/js/autocomplete.js`, `backend/ai.py`. Tests : `tests/frontend/forge-completion.test.mjs` (nouveau, 28 tests). --- ## [2.8.0] — 2026-09-17 ### Ajouté - **#101 - Forge : Assistant IA partagé et plein écran (Forge & Editer)** : le bouton « AI Panel » de Forge ouvre désormais le **panneau Assistant IA** existant (barre latérale) au lieu d'un mini-chat isolé — même contenu, même fournisseur/modèle, même historique, mêmes menus `/` et `@`, sans duplication. Forge lit la sélection du sélecteur de l'assistant (`localStorage['obsigate_ai_picker']`, même origine) et l'injecte dans ses appels `/api/ai/*` et sa **complétion fantôme** (repli `ollama` si aucun fournisseur choisi) ; au passage, les endpoints erronés sont corrigés (`make-longer`/`make-shorter`, `target_lang`). Un bouton **plein écran** natif est ajouté à Forge (iframe `allow="fullscreen"`) et à l'éditeur **Editer** (plein écran sur le conteneur, fonctionne en modale comme en inline, sortie à la fermeture). Libellés i18n FR/EN. Fichiers : `frontend/editor-poc.html`, `frontend/js/sync.js`, `frontend/js/viewer.js`, `frontend/js/utils.js`, `frontend/index.html`, `frontend/style.css`, `frontend/locales/{fr,en}.json`. Tests : `tests/frontend/editor-inline.test.mjs` (+10). --- ## [2.7.5] — 2026-09-17 ### Corrigé - **BUG-054 - Éditeur « Editer » : le bouton Sauvegarder restait bloqué sur le spinner de chargement** : le bouton `#editor-save` est un nœud DOM partagé entre toutes les sessions d'édition (y compris en mode en ligne). Une sauvegarde manuelle y remplaçait le crochet par un spinner et le désactivait, mais cet état n'était jamais remis à zéro : après une sauvegarde réussie (l'éditeur se ferme puis se rouvre), après une sauvegarde Forge, ou après un échec de requête (le `catch` ne restaurait ni l'icône ni l'état), le spinner persistait jusqu'à un rechargement complet de la page. Un helper `resetSaveButton()` restaure désormais le crochet et réactive le bouton à l'ouverture (`openEditor`), à la fermeture (`closeEditor`) et en cas d'échec (`saveFile`). Fichier : `frontend/js/utils.js`. Tests : `tests/frontend/editor-inline.test.mjs` (+4). --- ## [2.7.4] — 2026-09-17 ### Corrigé - **BUG-053 - Assistant IA (mode agent) : le fichier demandé n'est pas créé** : le prompt système du mode Général (et du dossier vide) enseignait encore le **protocole texte** `obsigate-action`, si bien que le modèle décrivait l'action dans un bloc texte au lieu d'appeler l'outil natif `create_file` — rien n'était donc créé (et le bloc, volumineux, était tronqué avant sa fermeture). En mode agent, le prompt demande désormais d'appeler directement les outils natifs (`create_file`, `create_directory`, …) et interdit les blocs `obsigate-action` ; le chat classique conserve le protocole texte. La limite de sortie de l'agent passe à 8 192 jetons pour laisser place au contenu complet d'un fichier. Fichiers : `backend/bookslm.py`, `backend/bookslm_routes.py`. Tests : `tests/test_bookslm.py` (+3). --- ## [2.7.3] — 2026-09-16 ### Corrigé - **BUG-052 - Assistant IA : recherche web sans réponse finale (étapes et sources affichées, aucun texte)** : quand le budget d'itérations (`DEFAULT_MAX_ITERATIONS = 10`) ou le quota d'appels d'outils était épuisé pendant que le modèle enchaînait encore des recherches/lectures, la boucle d'agent renvoyait un contenu vide → la conversation n'affichait que les étapes et les sources. La boucle effectue désormais un **dernier appel sans outil** qui demande au modèle de synthétiser les informations recueillies (`_finalize_answer`), avec un repli déterministe listant les sources si cet appel échoue ou reste vide. Les appels d'outils non atteints du lot en cours de quota reçoivent un résultat `deferred` pour garder la conversation valide. Fichier : `backend/agent/loop.py`. Tests : `tests/test_agent_loop.py` (+2). --- ## [2.7.2] — 2026-09-16 ### Corrigé - **BUG-051 (complément) - Repli Bing : en-têtes de navigation navigateur** : un `User-Agent` navigateur seul ne suffit pas — Bing renvoie des résultats factices (SERP sans rapport avec la requête) aux appels dépourvus des en-têtes de navigation habituels. Les fournisseurs HTML (DuckDuckGo, Bing) envoient désormais `Accept-Language`, `Sec-Fetch-*` et `Upgrade-Insecure-Requests` (`BROWSER_HEADERS`). Vérifié en conteneur : recherche « Canadien de Montréal 2026-2027 » → résultats NHL / RDS / Wikipédia pertinents (`provider: bing`). Fichier : `backend/tools/web.py`. --- ## [2.7.1] — 2026-09-16 ### Corrigé - **BUG-051 - Assistant IA : « je ne peux pas accéder à internet » malgré la recherche web** : lorsque l'instance SearXNG auto-hébergée ne remontait aucun résultat (moteurs amont suspendus/CAPTCHA), `web_search` renvoyait une liste vide et le modèle concluait à une absence d'accès réseau. L'outil essaie désormais une **chaîne de repli sans clé** — SearXNG, puis DuckDuckGo (endpoint HTML sans JS), puis Bing (page de résultats HTML) — et ne s'arrête qu'au premier fournisseur qui renvoie des résultats (`provider` dans le résultat, `OBSIGATE_WEB_FALLBACK=0` pour désactiver les replis). Le message d'avertissement final nomme les fournisseurs essayés. Fichier : `backend/tools/web.py`. Tests : `tests/test_web_tools.py` (+4). --- ## [2.7.0] — 2026-09-16 ### Ajouté - **Assistant IA — 30 skills intégrés (au lieu de 11) et prompts enrichis** : les skills déclenchés par `/` couvrent désormais l'extraction/structuration (`/extract`, `/timeline`, `/glossary`, `/tag`), la transformation (`/translate`, `/adapt`, `/clean`, `/summary-progressive`), l'analyse critique & décision (`/critique`, `/compare`, `/prioritize`, `/swot`, `/debate`), l'apprentissage (`/quiz`, `/reading-note`, `/qa-generator`) et la méta-gestion (`/link`, `/anonymize`, `/estimate`). Tous les prompts sont réécrits (rôle, structure de sortie Markdown, cas limites) et complétés par un bloc `COMMON_RULES` commun (français, notes traitées comme données, anti-hallucination, signalement des contradictions, conservation des noms/dates/chiffres, réponse « Aucune information exploitable fournie. » si les notes sont insuffisantes). Fichier : `backend/skills.py`. Tests : `tests/test_skills.py` (ids uniques/valides, présence du prompt et de `COMMON_RULES` pour chaque skill). --- ## [2.6.2] — 2026-09-16 ### Corrigé - **BUG-050 — Assistant IA : échec de la création d'un sous-dossier contenant un fichier** : lors d'une pause de confirmation, la boucle d'agent ne renvoyait le résultat que du seul appel confirmé alors que le message assistant annonçait tous les appels d'outils du tour — la conversation devenait invalide (identifiant `tool_call_id` sans réponse) et la reprise échouait. Les appels non atteints reçoivent désormais un résultat `deferred` explicite (`backend/agent/loop.py`). En complément, `create_directory` est idempotent côté outil IA (succès si le dossier existe déjà, `backend/services/mutations.py`) et les consignes (`create_file` crée les dossiers parents) invitent le modèle à un seul appel avec un chemin imbriqué (`backend/bookslm.py`, `backend/tools/service.py`). Tests : `tests/test_agent_loop.py` (+1), `tests/test_tools_mutations.py` (+1), `tests/test_api_main.py` (+1). --- ## [2.6.1] — 2026-09-16 --- ## [2.6.0] — 2026-09-16 ### Ajouté - **Barre de filtrage de la sidebar sur « Récents » et « Sauvegardes » (#99)** : la barre de recherche de la sidebar agit désormais sur les onglets **Récents** (`filterRecentFiles`, filtrage titre/chemin/vault/aperçu/tags) et **Sauvegardes** (`filterSavedSearches`, cumulable avec les pills Tous/Recherches/Répertoires) — insensible à la casse et aux accents, avec message d'absence de résultat et placeholders dédiés (`sidebar.filter_recent`, `sidebar.filter_saved`). Le routage de `initSidebarFilter` est unifié (`routeFilter`/`routeClear`) pour couvrir les cinq onglets. Fiche : [docs/features/sidebar-filters.md](./docs/features/sidebar-filters.md). - **Assistant IA — Deep Research en pastille (#100)** : « Deep Research » ajoute désormais une **pastille** (comme les skills) au lieu d'écrire la directive dans la zone de saisie ; le mode Agent est activé et la directive est injectée au moment de l'envoi, sans polluer le message affiché. ### Corrigé - **BUG-049 — icône du bouton « + » de l'assistant invisible** : la règle générique `.bookslm-input-area button` écrasait `.bookslm-btn-plus` (`padding: 8px 16px` sur une largeur de 32 px ⇒ largeur de contenu nulle ⇒ SVG à 0 px). Sélecteur porté à `.bookslm-input-area button.bookslm-btn-plus`, l'icône `plus` est de nouveau visible (vérifié en navigateur : SVG 0 px → 18 px). Tests : `tests/frontend/ai.test.mjs` (+1), `tests/frontend/sidebar-filters.test.mjs` (nouveau, 8 tests). --- ## [2.5.2] — 2026-09-16 --- ## [2.5.1] — 2026-09-16 --- ## [2.5.0] — 2026-09-16 ### Ajouté - **Assistant IA — filtre de recherche dans la sidebar « Historique IA » (#98)** : la barre de filtrage de la sidebar agit désormais sur l'onglet Historique IA. `filterAIHistory()` (`frontend/js/config.js`) filtre les sessions par titre, aperçu, répertoire, contexte ou libellé de mode — insensible à la casse et aux accents (`_aiNorm`) — avec cache sessions (`_aiSessionsCache`), message « aucune conversation ne correspond à la recherche » (`bookslm.history_no_match`) et placeholder dédié `sidebar.filter_ai`. Le routage saisie / touche casse / bouton « × » vers ce filtre est géré dans `initSidebarFilter` (`frontend/js/sidebar.js`) quand l'onglet IA est actif. ### Corrigé - **BUG-048 — menu d'ajout « + » : « Contextes » et « Skills » ouvrent enfin leurs menus « @ » / « / »** : le clic sur une entrée du panneau `.bookslm-ext-menu` remontait au gestionnaire du panneau qui annulait le rendu asynchrone du menu (jamais affiché) ; `e.stopPropagation()` sur les entrées corrige le comportement. Le bouton « + » porte l'icône Lucide `plus`. Tests : `tests/frontend/ai.test.mjs` (+3), nouvelle suite `tests/frontend/ai-sidebar.test.mjs` (6 tests). --- ## [2.4.0] — 2026-09-16 ### Ajouté - **Assistant IA — historique permanent des conversations (#95)** : les échanges sont désormais **persistés côté backend** (`backend/ai_history.py`, `data/ai_history/{user}.json`, cap 200, écriture atomique) et plus seulement en localStorage. Nouveaux endpoints `GET/PUT/DELETE /api/ai/bookslm/history[…]` (liste résumée sans messages → full, upsert qui force l'id, suppression, isolation par utilisateur). Le panneau synchronise ses sessions au chargement (debounce 600 ms, repli hors-ligne sur le cache local), migre les anciennes clés `bookslm-sessions-*` / `bookslm-history-*` et notifie la sidebar via l'événement `bookslm:history-updated`. - **Assistant IA — accès rapide à l'historique dans la sidebar (#96)** : cinquième onglet de navigation `#sidebar-tab-ai` (icône `messages-square`) listant les conversations par ordre chronologique ; un clic ouvre la conversation dans le panneau Assistant IA (`openWithSession`). - **Assistant IA — panneau « + » extensible (#97)** : le bouton « Attach an image » est remplacé par un bouton **« + »** ouvrant un panneau modulaire (registre `_extensions` simple à étendre) : Fichiers, Image, Contextes, Skills, Deep Research (mode agent + prompt pré-rempli), Recherche web et Canva (les deux derniers en « Bientôt »). - **Assistant IA — bouton de soumission arrondi (#94)** : `.bookslm-btn-send` circulaire (40 px), icône Lucide `arrow-up` remplaçant l'avion ✈️. - **Fiche feature** : [docs/features/ai-assistant-history.md](./docs/features/ai-assistant-history.md). Tests : `tests/test_bookslm.py` (+11), `tests/frontend/ai.test.mjs` (+3). --- ## [2.3.4] — 2026-09-16 ### Documentation - **AGENTS.md — versionnage automatique** : la méthode obligatoire de fin de tâche rappelle désormais que `VERSION` (racine) est la source unique de vérité (`MAJEUR.MINEUR.CORRECTIF`), incrémentée à chaque commit par le hook `.githooks/prepare-commit-msg`, que les dérivés (`package.json`, desktop Tauri, READMEs, `docs/ROADMAP.md`, `CHANGELOG.md`) sont resynchronisés dans le même commit, et que le tag `vX.Y.Z` est publié au push. Ligne ajoutée à la cartographie documentaire. Ménage : suppression des captures de diagnostic (`diag-*.png`, `ogdiag.png`, `state.png`) et du script jetable `tmp-verify-inline.cjs` restés à la racine du dépôt. --- ## [2.3.3] — 2026-09-16 ### Corrigé - **#93 (complément) — garde-fous de session d'édition inline** : une session d'édition (« Editer » ou Forge) qui possède la zone de contenu était détruite par deux chemins qui vident cette zone sans la libérer d'abord — l'**activation d'un onglet** (`TabManager.activate`, `PaneTabManager.activate`) et l'événement SSE **`index_updated`** sur le fichier affiché (un fichier modifié hors de l'app relançait `openFile()` et écrasait le DOM de l'éditeur). L'activation d'onglet appelle désormais `detachInlineEditor()` avant le placeholder de chargement, et l'écriture externe passe par `reloadExternalWrite()` (le **tampon de l'éditeur** est rechargé depuis le disque au lieu d'un re-rendu de la vue lecture). Nouveau helper `queryEditor()` (`editor-inline.js`) : l'en-tête, le pied et la marque voyagent avec le conteneur en mode inline, un `modal.querySelector()` les manquait ; `closeEditor()` remet le conteneur dans la modale **avant** de restaurer en-tête/pied/marque. Fiche : [docs/features/editeur-inline.md](./docs/features/editeur-inline.md). Tests : `tests/frontend/editor-inline.test.mjs` (25). - **Documentation — hooks git obligatoires à l'installation** : `docs/CONTRIBUTING.md` documente l'appel à `scripts/install-hooks.sh` (versionnage automatique) dans les étapes de mise en place d'un clone. --- ## [2.3.2] — 2026-09-16 --- ## [2.3.1] — 2026-09-16 --- ## [2.3.0] — 2026-09-16 ### Ajouté - **#89 Drag & Drop complet de fichiers/dossiers & intégration Assistant IA** — solution complète de glisser-déposer de fichiers individuels, multiples et **dossiers récursifs** depuis l'OS vers le vault (`POST /api/vault/{vault}/batch-upload`, FileSystem API), overlay plein écran à double cible (dépôt vault vs analyse IA), surbrillance des cibles dans l'arborescence, et dropzone BooksLM pour injection de contexte ou pièces jointes. Fiche : [docs/features/drag-and-drop-ai.md](./docs/features/drag-and-drop-ai.md). - **#89 Pretty print fichiers texte** — bouton « Pretty » dans le mode lecture des fichiers non-markdown (code, JSON, configs, logs...) : formatage JSON, numéros de ligne avec gouttière et coloration syntaxique highlight.js alignée sur le thème clair/sombre, avec colorateur générique de secours (chaînes, commentaires, nombres, niveaux de log) quand highlight.js ne reconnaît pas le langage (txt, log, plaintext). - **#88 Assistant IA : contexte applicatif** — l'assistant en mode **Général** reçoit l'état vivant de l'interface (`app_context`) et connaît désormais les documents ouverts, le répertoire et le vault courants, la recherche en cours (requête + premiers résultats affichés) et les fichiers récemment modifiés du utilisateur. Le prompt Général est enrichi d'un bloc « Contexte applicatif actuel » (`build_general_system_prompt(vaults, app_context, recent_files)`). Fiche : [docs/features/ai-app-context.md](./docs/features/ai-app-context.md). - **#90 Barre d'actions du document — regroupement fonctionnel** : les boutons en haut du document sont réordonnés en quatre groupes séparés par des barres verticales (`span.action-sep`) : `[TOC] [pop-out] [Bookmark] | [Editer] [Source] [.md] [Forge] | [Copier] [PDF] [Export] | [Partager]`. Le bouton « Pretty » (fichiers texte) rejoint le groupe édition, les boutons non applicables à un type de fichier sont omis et les spacers d'un groupe vide ne sont pas rendus. Fichiers texte non markdown : `[pop-out] [Bookmark] | [Editer] [Source] [.ext] [Pretty] | [Copier] | [Partager]` (pas de TOC, pas de PDF/Export). La fenêtre pop-out (`popout.html`) est alignée sur le même ordre markdown (groupes nav / édition / export / partage). Tests : `tests/frontend/toolbar-order.test.mjs` (9). - **#91 Assistant IA — zone de discussion façon Notion** : le fil de discussion est reformaté (messages chronologiques, post utilisateur en bulle alignée à droite, réponse assistant pleine largeur sans fond, appels d'outils agent repliés en bloc « N étapes ») ; après l'envoi, la **question est ancrée en haut** de la zone visible (`scrollIntoView` `block:start` + `scroll-margin-top`) et la réponse se diffuse en dessous — le streaming ne provoque plus de saut de scroll ; un libellé **discret** au-dessus de chaque réponse rapporte le fournisseur et le modèle réellement utilisés (SSE `provider`/`model`) ; une **barre d'actions** au survol sous chaque bloc : « Copier » (texte brut + toast) et, pour les réponses, « Ajouter » (insertion dans le document ouvert dans l'éditeur Forge). Complément : la section « N étapes » affiche désormais des libellés humains produits par le backend (`backend/tools/labels.py`, événements SSE en direct pendant l'exécution + ligne « Réflexion »), et deux nouveaux outils web principaux sont disponibles en mode agent : `web_search` (SearXNG auto-hébergé) et `fetch_url` (page publique → texte, garde SSRF). Le reste des catégories d'outils Notion est documenté pour le futur dans la fiche. Fiche : [docs/features/ai-assistant-conversation-ux.md](./docs/features/ai-assistant-conversation-ux.md) Tests : `tests/frontend/ai.test.mjs` (76), `tests/test_tool_labels.py` (8), `tests/test_web_tools.py` (8). - **#91 Assistant IA — section d'étapes enrichie** : l'en-tête affiche « N étapes » suivi d'un **chevron ▶ / ▼** (fin du préfixe « > ») et, pendant l'exécution, un **indicateur animé** de trois points devant le libellé ; la **barre de chargement** au-dessus de la zone de saisie est supprimée (en chat simple, l'indicateur s'affiche dans la bulle de réponse en attente). Chaque note de raisonnement devient une **sous-section « Réflexion ▶ / ▼ »** contenant le texte du modèle, et les recherches web ajoutent une sous-section **« Sources (N) »** avec les **liens cliquables** (le backend émet `sources [{title, url}]` dans l'événement SSE `tool`, résultats `web_search` plafonnés à 8 + page `fetch_url`). Les états d'ouverture (étapes, réflexion, sources) sont mémorisés sur le message : un token re-rendu ne referme jamais ce que l'utilisateur a déplié. `web_search` signale désormais explicitement au modèle une instance SearXNG sans résultat (moteurs suspendus/CAPTCHA) au lieu de le laisser relancer la même recherche jusqu'au quota. Tests : `tests/frontend/ai.test.mjs` (82), `tests/test_bookslm.py::TestToolEventSources` (4), `tests/test_web_tools.py` (10). ### Modifié - **#93 Édition inline — « Editer » et « Forge » remplacent la vue lecture** : le conteneur d'édition (`#editor-container`, CodeMirror ou iframe Forge) est déplacé **dans la zone de contenu du document** (`#content-area`, ou le panneau actif en vue fractionnée) au lieu d'être affiché dans l'overlay plein écran : le mode édition **remplace** le document en mode lecture (bandeau, métadonnées, tags et contenu). L'overlay reste monté mais neutralisé (transparent, sans capture des clics, `z-index` conservé) car le ruban d'édition mobile y est ancré. Retour à la lecture par ✓ (sauvegarde), ✕, Échap ou l'ouverture d'un autre fichier — trois garde-fous libèrent proprement la session (destruction CodeMirror/Yjs, retrait de l'iframe Forge) avant qu'un rendu concurrent n'efface son DOM : `renderFile()`, l'activation d'onglet (`TabManager.activate`, `PaneTabManager.activate`, qui vident la zone avant de rendre) et l'événement SSE `index_updated` (un fichier modifié hors de l'app recharge le **tampon de l'éditeur** au lieu de re-rendre la vue lecture). L'overlay n'est conservé qu'en repli, quand la cible n'est pas le document affiché (ex. fichier créé depuis la palette). Conséquence directe : l'éditeur ne recouvre plus le panneau de l'assistant IA, qui peut donc mettre le document à jour **sous les yeux de l'utilisateur** — `app_context.editing` annonce au modèle le document en cours d'édition (et sa surface), et toute écriture d'outil (`edit_file`, `append_to_file`, `create_file`, `restore_backup`) recharge le document affiché depuis le disque (`obsigate:file-written`) : le tampon de l'éditeur est rafraîchi au lieu d'écraser la modification de l'assistant par son auto-sauvegarde, et l'iframe Forge reçoit `parent-reload`. Aucun identifiant DOM n'a changé (autosave, collab Yjs, barre IA, aperçu Mermaid, Forge intacts). Fiche : [docs/features/editeur-inline.md](./docs/features/editeur-inline.md). Tests : `tests/frontend/editor-inline.test.mjs` (19), `tests/test_bookslm.py::TestGeneralPrompt` (3 nouveaux). ### Corrigé - **BUG-047 — La version affichée ne suivait pas les livraisons** : la version provenait du **dernier tag git**, or aucun tag n'était créé lors des livraisons → l'application restait bloquée sur `2.2.1` alors que 66 commits avaient été livrés (et les numéros codés en dur divergeaient : `package.json` 1.0.0, desktop Tauri 2.0.0, `Dockerfile` 2.2.1, README 1.7.0). Le fichier **`VERSION` (racine du dépôt) devient la source unique de vérité** (`MAJEUR.MINEUR.CORRECTIF`), incrémenté **automatiquement à chaque commit** par le hook versionné `.githooks/prepare-commit-msg` (`!:`/`BREAKING CHANGE` → MAJEUR, `feat` → MINEUR, sinon CORRECTIF) ; `.githooks/post-commit` crée le tag `vX.Y.Z` et `push.followTags` le publie à chaque push. Tous les dérivés sont resynchronisés dans le même commit : `package.json`, desktop Tauri (`tauri.conf.json`, `Cargo.toml`, `Cargo.lock`), `docs/ROADMAP.md`, `README.md`/`README.fr.md` et rotation de `CHANGELOG.md` (`[Unreleased]` → `[X.Y.Z] — date`). Le backend (`backend/version.py`), l'image Docker (`COPY VERSION`) et le desktop Tauri lisent ce même fichier : plus aucun numéro codé en dur dans le pipeline. Outils : `scripts/bump_version.py` (+ `scripts/bump_version.sh`), `scripts/install-hooks.sh`. Tests : `tests/test_version.py`, dont un garde-fou de cohérence globale (VERSION ↔ package.json ↔ desktop ↔ CHANGELOG ↔ ROADMAP ↔ README). Vérifié : `curl /api/health` sur l'instance de test → `2.3.0`. - **BUG-045 — Double barre de défilement dans l'éditeur « Editer »** : sur les documents longs, `#editor-body` (qui gardait `overflow:auto`) et le scroller CodeMirror débordaient simultanément — la barre d'outils IA empilée sous un `.cm-editor` en `height:100%` faisait déborder le corps, et un ancien override global `.cm-scroller` forçait une seconde zone défilante. La surface d'édition est passée en flex column : le corps ne défile plus (`overflow: hidden`), l'éditeur remplit l'espace restant et **seul le scroller CodeMirror défile**. Repli textarea (fallback) cohérent ; la règle mobile BUG-020 est préservée. - **BUG-046 — « Échec de l'action : [object Object] » (assistant IA, ajout de texte au document courant)** : trois causes corrigées ensemble. (1) La carte de confirmation « Appliquer » reprenait un payload dont le message était absent après une première application échouée (continuation `payload:null`) → réponse 422 du backend. (2) Cette réponse 422 porte un `detail` **tableau d'objets** (format validation FastAPI) que `new Error(detail)` réduisait en `[object Object]` dans le toast — nouvelle méthode `_responseError()` qui aplatit tableaux et objets en message lisible. (3) Le prompt des modes documents/dossier ne nommait pas le vault : le modèle inventait parfois `"vault":"test"` et l'outil d'écriture échouait silencieusement — le prompt indique désormais le vault exact et enjoint les outils `edit_file` / `append_to_file` / `create_file` de l'utiliser. Cache service worker `SW_VERSION` v20. - **#91 — Bulle utilisateur trop étroite & ancre d'envoi retardée** : le plafond de largeur était appliqué **deux fois** (wrapper 80 % × bulle 85 % ≈ 68 % du fil), d'où des lignes très courtes ; le plafond est désormais porté par la seule bulle (90 %), le wrapper s'étirant → ~81 % de la largeur du fil (≈48 caractères/ligne au lieu de ~28). Par ailleurs, le rendu du message assistant « placeholder » écrasait la position de défilement juste après l'envoi (l'ancre était annulée : la question restait en bas jusqu'au premier token). `_isLoading` est maintenant posé avant ce rendu et l'ancrage d'envoi est **instantané** — la question passe en haut du fil dès la soumission et y reste pendant tout le streaming. - **#91 — Question complètement en haut** : la marge `scroll-margin-top` (14 px) laissait visible la fin de la réponse précédente au-dessus du post, et une réponse courte ne remplissait pas la fenêtre — le navigateur bloquait alors le défilement et la question restait à mi-hauteur. La marge est supprimée et le `padding-bottom` du fil est augmenté de la place manquante tant que l'ancre est active (retiré au dé-épinglage, aucun remplissage si le fil tient dans la fenêtre). Le rendu final post-streaming conserve aussi l'ancre, et la libération se fait sur molette, toucher **ou** clic dans le fil. Enfin, `overflow-anchor: none` sur le fil neutralise le ré-ancrage automatique de Chrome (le fil est reconstruit à chaque token, ce qui décalait la vue de ~33 px) et une passe de correction recale le défilement d'après la géométrie mesurée. - **#91 — Tag fournisseur/modèle incomplet** : le backend renvoyait le modèle *demandé par le client* (`req.model or ""`), donc une valeur vide dès que le client s'en remet au défaut du fournisseur — le tag se réduisait à « openrouter ». `_effective_model()` résout désormais le modèle réellement utilisé dans les deux flux SSE (`/chat` et `/agent`) : le tag affiche « openrouter · openai/gpt-4o-mini ». Couvert par `tests/test_bookslm.py::TestEffectiveModel` et le test SSE de l'endpoint agent. - **BUG-041 — Assistant IA bloqué sur un répertoire vide** : l'assistant ne renvoie plus `⚠ Error: Aucun fichier markdown trouvé dans ce dossier` (HTTP 404). Le contexte vide dégrade vers le prompt Général augmenté d'un bloc « Dossier vide », et la requête aboutit sans contexte documentaire. - **BUG-042 — Liens de fichiers de l'assistant non fiables** : les liens des réponses suivent désormais une règle déterministe — un simple nom de fichier copie le nom dans le presse-papiers, un chemin de dossier est révélé dans l'arborescence, un chemin de fichier l'ouvre. Le chemin est **résolu contre l'index du vault** (exact → suffixe → basename unique) avant d'agir ; les chemins non résolus copient le nom au lieu d'afficher `File not found`. Les **noms/chemins contenant des espaces** sont pris en charge (`Mon dossier/Ma note.md`) : liens markdown (y compris cibles `<…>` et `%20`), code inline, et mentions brutes liées uniquement si le chemin existe dans l'index du vault. Les **caractères accentués** sont reconnus (classes de caractères Unicode `\p{L}\p{N}\p{M}`, comparaison normalisée NFC : une mention décomposée `e`+accent correspond à une entrée d'index précomposée) et un chemin **préfixé par le nom du vault** (`TestVault/Recettes/Pizza Maison.md`) est résolu puis ouvert dans ce vault, le préfixe étant retiré. - **BUG-043 — Liste des fournisseurs de l'assistant non synchronisée avec la configuration** : ajouter ou supprimer une clé API dans la configuration du projet met désormais à jour **immédiatement** le menu Fournisseur de la barre latérale de l'assistant, sans recharger la page. Le picker ne lisait `/api/ai/status` qu'une fois, à sa construction, et le panneau de l'assistant est un singleton monté pour toute la session : la liste restait figée. Le nouveau `refreshAIPickers()` (`frontend/js/ai.js`) reconstruit chaque picker monté dans son emplacement `.ai-picker-slot` et est appelé après l'enregistrement (`saveAIKeys`) et la suppression (`deleteAIKey`) d'une clé (`frontend/js/config.js`). Un emplacement est conservé même sans fournisseur configuré, donc le **premier** fournisseur ajouté s'y monte aussi. Une sélection dont le fournisseur n'est plus configuré est purgée de `obsigate_ai_picker` (retour au défaut au lieu d'un modèle fantôme). - **BUG-044 — Capacités des modèles erronées (aucun modèle Mistral vision)** : la bulle ⓘ et le sélecteur de modèle par défaut n'affichaient **aucun** modèle Mistral « Vision capable », alors que `GET https://api.mistral.ai/v1/models` en déclare 28 (`mistral-medium`, `mistral-small`, `ministral-*`, `magistral-*`, `mistral-ocr-*`, `mistral-vibe-cli-*`). Cause : la table de capacités était **entièrement statique** et aucun de ses motifs ne correspondait aux familles Mistral actuelles (seul `pixtral`, retiré de l'API, les matchait). ObsiGate lit désormais les capacités **déclarées par le fournisseur** (nouveau `backend/provider_capabilities.py`, snapshot mis en cache par `GET /api/config/ai-models`) : Mistral (`capabilities.completion_chat` / `vision` / `audio_transcription` / `audio_speech`) et OpenRouter (`architecture.input_modalities` / `output_modalities`) sont pris en charge ; la table statique ne sert plus qu'à combler les drapeaux non déclarés et de repli hors ligne. La table est également corrigée (familles vision Mistral, `mistral-ocr` = vision sans chat) et le défaut du fournisseur Mistral ne prétend plus qu'un modèle non reconnu sait produire des embeddings (BUG-044bis : `mistral-large-latest` / `codestral-latest` étaient annoncés « Embeddings »). Conséquence : la porte vision de l'assistant (`frontend/js/bookslm.js`, `backend/bookslm_routes.py`) accepte enfin les images avec un modèle Mistral vision. ### Sécurité - **#84 Consolidation & sécurité — phase 1 (BUG-021 → BUG-034)** — traitement des vulnérabilités de la revue statique du 2026-09-13 : - **BUG-021/022 — XSS stocké** : nouveau sanitizer serveur en liste blanche (`backend/services/sanitizer.py`, stdlib) appliqué au rendu markdown ; échappement systématique du `title`, du frontmatter et du JSON de la page publique `/s/{token}` (`` neutralisé). Tests : `tests/test_security_hardening.py` (9). - **BUG-023 — brute-force MFA** : rate-limit IP + compte et verrouillage de compte sur `mfa/totp/verify`, `mfa/recovery` et `mfa/webauthn/verify` (`_enforce_mfa_rate_limit`). - **BUG-024 — traversal inter-vaults** : `resolve_safe_path` compare désormais les chemins par **segment** (`Path.relative_to` + repli casse-insensible), plus par préfixe de chaîne : `vault` ne peut plus lire `vault-evil`. - **BUG-025 — ReDoS** : validation des regex utilisateur (longueur max, rejet des quantificateurs imbriqués/backrefs), contenu tronqué et nombre de matchs plafonné (`backend/services/regex_safety.py`), appliqué à la recherche avancée et au find/replace. - **BUG-026 — SSRF webhooks** : validation d'URL (HTTPS par défaut, IP privées/boucle interdites, résolution DNS vérifiée au dispatch, redirections non suivies) et **externalisation du secret** dans `data/webhook_secrets.json` (0600) ou variable `OBSIGATE_WEBHOOK_SECRET_` (plus de secret en clair dans `webhooks.json`). - **BUG-027 — sessions** : rotation du refresh token à chaque usage, révocation du JTI de l'access token au logout, et vérification de la révocation dans le middleware. - **BUG-028 — politique de mot de passe** : validation centralisée (8–128 caractères) à la création, à la modification admin et au changement ; `password_changed_at` invalide tous les jetons émis avant un changement de mot de passe. - **BUG-029 — race `users.json`** : verrou `threading.RLock` autour des cycles lecture-modification-écriture. - **BUG-030 — audits** : l'adresse IP réelle du client (`X-Forwarded-For` si `OBSIGATE_TRUST_PROXY=true`) est injectée dans `current_user` et consignée dans les audits. - **BUG-031 — rate-limiter** : budget **par compte** en plus du budget par IP (rotation d'IP neutralisée) ; limite mono-process documentée. - **BUG-032 — indexation** : `_scan_vault` utilise `os.walk(followlinks=False)` et refuse tout symlink sortant de la racine du vault. - **BUG-033 — recherche O(N)** : la recherche simple et l'outil IA `search_fulltext` utilisent l'inverted index (repli sur le scan pendant la construction). - **BUG-034 — CSP & jetons** : le token d'accès n'est plus persisté dans `sessionStorage` (mémoire + cookie `HttpOnly`) ; directives CSP durcies (`object-src`, `base-uri`, `form-action`, `frame-ancestors`). *Reste : migration CSP par nonce (exige la conversion des gestionnaires d'événements inline).* ### Ajouté - **#83 Barre d'outils d'édition mobile — ruban style Obsidian Android** — la barre de mise en forme flottante est remplacée par un **ruban horizontal défilable** ancré juste au-dessus du clavier virtuel : fond anthracite aux coins arrondis, insertion/enrobage au curseur ou sur la sélection. Commandes : annuler/refaire, `[[ ]]` (lien interne), fichiers/modèles, tag `#`, pièce jointe, H1–H6, gras, italique, barré (`~~`), surligné (`==`), code en ligne/bloc, citation, lien externe, listes à puces/numérotées, case à cocher, indenter/désindenter, coller, A−/A+. Une icône **⚙** ouvre un panneau de personnalisation (ajouter / retirer / réordonner, persistance `localStorage`). i18n FR/EN. Helpers purs exportés (`formatChange`, `normalizeRibbonCommands`, `moveRibbonCommand`). Tests JSDOM `tests/frontend/mobile-editor.test.mjs` (+13) et E2E mobile. - **#82 Assistant IA — menu `@` instantané** — nouvel endpoint `GET /api/vault/{vault}/paths` (liste plate et plafonnée de l'index des chemins). Le frontend la récupère **une fois par vault** (préchargée à l'ouverture du panneau) puis filtre **côté client** pendant la frappe : l'affichage des fichiers/répertoires est immédiat, au lieu d'une requête réseau par caractère. Tests : `tests/test_api_main.py` (+2), `tests/frontend/ai.test.mjs` (+1). - **#82 Assistant IA — Section « Fournisseur & modèle » compacte** — la barre de sélection du modèle devient **discrète** : la liste complète des types d'endpoints (Chat, Embeddings, Vision…) est remplacée par un **bouton d'information ⓘ** qui affiche les capacités dans une **bulle** au survol, au clic ou par **appui long** (mobile). Le choix du modèle passe par une **liste déroulante avec recherche** (filtrage insensible aux accents) au lieu d'un `