diff --git a/CHANGELOG.md b/CHANGELOG.md
index 40d1837..f2f9885 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -32,6 +32,20 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
persistés dans `data/config.json`, rechargement à chaud dans `backend/ai.py`, sélecteurs
« Fournisseur par défaut » / « Modèle par défaut » dans `#cfg-ai`, i18n FR/EN.
+- **#80 Assistant IA — Rendu Markdown, liens fichiers/paths & sessions** — réponses de l'assistant
+ rendues en **Markdown formaté** (titres, listes, tableaux, citations, code, emphase) via un
+ renderer auto-contenu ; les **fichiers et chemins** mentionnés deviennent des liens cliquables
+ (clic fichier → ouverture dans le viewer, clic répertoire → révélation/surlignage dans
+ l'arborescence, outils `open_file`/`reveal_in_tree` côté in-app) ; **gestion des sessions** par
+ contexte (historique consultable, rechargeable, supprimable) avec migration de l'ancienne
+ conversation unique. Tests : `tests/frontend/ai.test.mjs`.
+
+### Modifié
+
+- **Assistant IA (`frontend/js/bookslm.js`)** — l'ancien rendu Markdown minimal est remplacé par un
+ renderer bloc/inline complet ; l'en-tête expose un bouton « Historique des sessions » ; les badges
+ de sources ouvrent désormais réellement le fichier (événement `obsigate:open-file`).
+
### Corrigé
- **Typage backend (mypy) — 33 erreurs corrigées** (`backend/main.py`, `indexer.py`,
diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md
index be4b929..23695c0 100644
--- a/docs/ROADMAP.md
+++ b/docs/ROADMAP.md
@@ -54,6 +54,16 @@
- [ ] **F.** Durcissement (rate limiting, redaction secrets, OpenAPI + guide MCP, E2E)
- **Documentation :** [AI_ARCHITECTURE_GUIDE.md](./AI_ARCHITECTURE_GUIDE.md) (architecture, catalogue d'outils, sécurité, phases).
+### 80. Assistant IA — Rendu Markdown, liens fichiers/paths & sessions
+
+- **Effort :** 1-2 jours | **Impact :** 🟡
+- **Statut :** livré (en attente de vérification utilisateur). Détail complet : [features/ai-assistant-ux.md](./features/ai-assistant-ux.md)
+- **Description :** Réponses de l'assistant rendues en **Markdown formaté** (titres, listes, tableaux,
+ citations, code) ; les **fichiers et chemins** mentionnés deviennent des liens cliquables (ouvrir
+ le fichier / révéler le répertoire dans l'arborescence) ; **gestion des sessions** (historique
+ consultable, rechargeable et supprimable par contexte).
+- **Reste à faire :** validation utilisateur.
+
---
## ⚪ Backlog — Priorité 3 (P3)
@@ -162,6 +172,7 @@
| 76 | BooksLM — Console AI contextuelle par répertoire | 2.2.0 | [features/bookslm.md](./features/bookslm.md) |
| 78 | Éditeur Excalidraw | 2.2.0 | [features/excalidraw.md](./features/excalidraw.md) |
| 77 | Application Desktop native — Tauri | 🔵 en cours | [features/desktop-tauri.md](./features/desktop-tauri.md) |
+| 80 | Assistant IA — Rendu Markdown, liens fichiers/paths & sessions | 🔵 en cours | [features/ai-assistant-ux.md](./features/ai-assistant-ux.md) |
---
@@ -171,7 +182,7 @@
|---|---|---|
| ✅ Complété | #1 → #59, #61, #63–68, #71, #72, #74–76, #78 | ~82 jours réalisés |
| 🔵 P2 restant | #77 Desktop : signature code (optionnel), wizard 1er lancement (optionnel), 6 tests E2E **manuels** | ~1-2 jours |
-| ⚪ P3 restant | #62 Collaboration Yjs (5-7j) · #79 Assistant IA outils + MCP (10-15j, Phase 0 + G livrés) | ~15-22 jours |
+| ⚪ P3 restant | #62 Collaboration Yjs (5-7j) · #79 Assistant IA outils + MCP (10-15j, Phase 0 + G livrés) · #80 Assistant IA UX (livré, en attente vérif) | ~15-22 jours |
| ⚪ P4 restant | #69 Mobile éditeur (2-3j) · #70 Sémantique (4-5j) · #73 Sync (6-8j) | 11-16 jours |
| **Total restant** | **6 items + finitions** | **~27-39 jours** |
diff --git a/docs/features/ai-assistant-ux.md b/docs/features/ai-assistant-ux.md
new file mode 100644
index 0000000..cac5fac
--- /dev/null
+++ b/docs/features/ai-assistant-ux.md
@@ -0,0 +1,55 @@
+# #80 — Assistant IA — Rendu Markdown, liens de fichiers/paths & sessions
+
+> **Statut :** 🔵 Livré (en attente de vérification utilisateur)
+> **Effort :** 1-2 jours | **Impact :** 🟡
+> **Références :** [Roadmap](../ROADMAP.md) · [BooksLM #76](./bookslm.md) · [Outils IA #79](./ai-tools-mcp.md) · [Changelog](../../CHANGELOG.md)
+
+- **Description :** Trois améliorations d'expérience de l'assistant IA (`frontend/js/bookslm.js`) :
+ 1. **Rendu Markdown** des réponses de l'assistant (titres, listes, tableaux, citations, blocs de
+ code, emphase) au lieu d'un rendu texte approximatif.
+ 2. **Liens cliquables** : les noms de fichiers et chemins mentionnés par l'assistant deviennent
+ des liens — clic sur un **fichier** → ouverture dans le viewer ; clic sur un **répertoire** →
+ révélation et surlignage dans l'arborescence (outil `reveal_in_tree`).
+ 3. **Gestion des sessions** : chaque contexte (répertoire / documents / général) conserve un
+ **historique de sessions** consultable, rechargeable et supprimable.
+
+## A. Rendu Markdown — ✅ livré
+ - [x] **A1.** Renderer auto-contenu (aucune dépendance externe) : titres `#`→`######`, listes
+ ordonnées/non ordonnées (imbriquées), listes de tâches, tableaux, citations `>`, règles
+ horizontales, blocs de code clôturés avec langage, code inline, gras/italique/barré, liens.
+ - [x] **A2.** Échappement HTML systématique avant génération de balises (anti-injection) ;
+ seules les balises produites par le renderer sont injectées.
+ - [x] **A3.** Styles dédiés dans `frontend/style.css` (`.bookslm-bubble.assistant ...`).
+
+## B. Liens fichiers / chemins — ✅ livré
+ - [x] **B1.** Détection des mentions : code inline (`` `notes/a.md` ``), liens Markdown
+ `[libellé](chemin)` et chemins nus `dossier/fichier.ext`.
+ - [x] **B2.** Classification `file` (extension connue) vs `dir` (le reste) via `_classifyPath()`.
+ - [x] **B3.** Clic fichier → `Viewer.openFile()` (événement `obsigate:open-file` également émis) puis
+ repositionnement de l'indicateur dans l'arborescence ; clic répertoire → `focusPathInSidebar()`
+ (`obsigate:reveal-in-tree`).
+ - [x] **B4.** Résolution du vault : vault du contexte, sinon premier document ouvert, sinon
+ `state.currentVault`. Lien externe `http(s)` conservé tel quel.
+
+## C. Gestion des sessions — ✅ livré
+ - [x] **C1.** Stockage `bookslm-sessions-{mode}-{contexte}` (localStorage) : liste de sessions
+ `{id, title, createdAt, updatedAt, messages}`.
+ - [x] **C2.** Migration automatique de l'ancienne clé mono-conversation
+ `bookslm-history-{mode}-{contexte}`.
+ - [x] **C3.** Bouton « Historique » dans l'en-tête → menu listant les sessions (titre dérivé du
+ 1er message utilisateur + date), sélection pour recharger, bouton de suppression.
+ - [x] **C4.** `Nouvelle conversation` crée une session ; `_saveHistory()` met à jour la session
+ courante (upsert) et son titre.
+
+## D. Tests & documentation — ✅ livré
+ - [x] **D1.** `tests/frontend/ai.test.mjs` : rendu Markdown, échappement HTML, liens fichier/dossier,
+ clic sur lien, persistance/rechargement/suppression de session, menu d'historique, migration.
+ - [x] **D2.** i18n FR/EN (`bookslm.session_history`, `bookslm.session_delete`, `bookslm.history_empty`,
+ `bookslm.untitled`, `bookslm.no_vault`).
+ - [x] **D3.** CHANGELOG + Roadmap.
+
+## E. Points d'attention
+ - Le renderer est volontairement **autonome** (pas de CDN) pour rester disponible hors-ligne et
+ compatible PWA. Il ne couvre pas les extensions Markdown d'Obsidian (wikilinks, callouts).
+ - Les liens de chemin utilisent `data-kind` pour distinguer fichier/répertoire ; le vault est
+ résolu au moment du clic (pas stocké dans le HTML).
diff --git a/docs/features/ai-tools-mcp.md b/docs/features/ai-tools-mcp.md
index 6ca8cc8..3260bfc 100644
--- a/docs/features/ai-tools-mcp.md
+++ b/docs/features/ai-tools-mcp.md
@@ -1,6 +1,6 @@
# #79 — Assistant IA — Outils (function calling) & serveur MCP
-> **Statut :** 🔵 En cours — Phase 0 + B1/B2/B3/B7 + G livrés (2026-09-11)
+> **Statut :** 🔵 En cours — Phase 0 + B1/B2/B3/B6/B7 + G livrés (2026-09-11)
> **Effort :** 10-15 jours | **Impact :** 🟡
> **Références :** [Roadmap](../ROADMAP.md) · [Guide d'architecture IA](../AI_ARCHITECTURE_GUIDE.md)
@@ -20,7 +20,7 @@
- [x] **B3.** Fallback : retry sans `tools` si le provider rejette les tools (400/404/422) → chat simple ; protocole texte `obsigate-action` conservé côté frontend
- [ ] **B4.** SSE réellement streaming (corriger `bookslm_routes.py` — le message final reste envoyé en un seul événement)
- [ ] **B5.** Confirmations UI : outils `read` auto, outils `write` via carte Apply (`bookslm.js:515`), aperçu diff pour `edit_file`
- - [ ] **B6.** Outils de navigation in-app : `open_file`, `reveal_in_tree` (événement `obsigate:open-file`)
+ - [x] **B6.** Outils de navigation in-app : `open_file`, `reveal_in_tree` (événement `obsigate:open-file`) — livré via les liens cliquables de l'assistant (#80, [ai-assistant-ux.md](./ai-assistant-ux.md))
- [x] **B7.** Tests : agent loop LLM mocké (`tests/test_agent_loop.py`), providers (`tests/test_ai_chat.py`), endpoint (`tests/test_bookslm.py`)
## C. Catalogue d'outils — lecture & recherche (1-2 jours)
diff --git a/docs/features/bookslm.md b/docs/features/bookslm.md
index 6f1441d..264db3c 100644
--- a/docs/features/bookslm.md
+++ b/docs/features/bookslm.md
@@ -4,6 +4,9 @@
> **Effort :** 5-6 jours (réalisé) | **Impact :** 🟡
> **Références :** [Roadmap](../ROADMAP.md) · [Changelog — 2.2.0](../../CHANGELOG.md)
+> **Extension (#80, 2026-09-11) :** rendu Markdown complet des réponses, liens cliquables
+> fichiers/chemins et gestion multi-sessions — voir [ai-assistant-ux.md](./ai-assistant-ux.md).
+>
> **Extension (2026-09-11) :** l'assistant AI est désormais **multi-contexte** — Répertoire
> (mode historique), Documents (documents ouverts, via le bouton flottant) et Général (aucun
> document ouvert : aide sur l'app + création de fichiers/dossiers par cartes d'action). Header
diff --git a/frontend/index.html b/frontend/index.html
index 5fcc0c6..d67b3c3 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -4181,6 +4181,29 @@
Pour changer de provider par défaut :
AI_DEFAULT_PROVIDER=openrouter
+
+
🧠 Panneau Assistant (BooksLM)
+
+ L'assistant latéral (bouton flottant ou menu contextuel d'un
+ dossier) répond en Markdown formaté et contextualise vos
+ répertoires ou documents.
+
+
+
+ Réponses formatées : titres, listes, tableaux, citations et
+ blocs de code.
+
+
+ Les fichiers et chemins cités sont des liens : cliquez sur un
+ fichier pour l'ouvrir, sur un dossier pour le révéler dans
+ l'arborescence.
+
+
+ L'icône historique de l'en-tête liste les sessions passées
+ (recharger ou supprimer) ; « + » démarre une nouvelle
+ conversation.
+
+
diff --git a/frontend/js/bookslm.js b/frontend/js/bookslm.js
index 667c890..1a11b8e 100644
--- a/frontend/js/bookslm.js
+++ b/frontend/js/bookslm.js
@@ -9,6 +9,7 @@ import { buildAIPickerUI } from './ai.js';
import { safeCreateIcons } from './utils.js';
import { api, AuthManager } from './auth.js';
import { showToast } from './ui.js';
+import { state } from './state.js';
export const MODE = Object.freeze({
DIRECTORY: 'directory',
@@ -16,6 +17,13 @@ export const MODE = Object.freeze({
GENERAL: 'general',
});
+// File extensions recognised when turning a mention into a clickable link.
+// Anything else that looks like a path is treated as a directory (revealed
+// in the sidebar tree instead of being opened in the viewer).
+const FILE_EXT_RE = /\.(?:md|markdown|mdx|txt|pdf|excalidraw|canvas|png|jpe?g|gif|svg|webp|bmp|avif|csv|tsv|json|ya?ml|toml|ini|cfg|conf|html?|css|scss|sass|less|jsx?|tsx?|vue|svelte|py|rb|go|rs|java|kt|c|cc|cpp|h|hpp|cs|php|sh|bash|zsh|ps1|bat|sql|xml|log|tex|org|rtf|docx?|xlsx?|pptx?)$/i;
+// A path-like token: optional `dir/` segments followed by a final segment.
+const PATH_WITH_DIR_RE = /(^|[\s(>"'])((?:[\w.-]+\/)+[\w.-]+\.[A-Za-z0-9]{1,8})/g;
+
/**
* Gather the files currently open across the singleton TabManager and every
* split-view pane (window globals avoid a circular import with ui.js).
@@ -63,6 +71,9 @@ class BooksLM {
this._abortCtrl = null;
this._panel = null;
this._isFullscreen = false;
+ this._sessions = [];
+ this._currentSessionId = null;
+ this._pendingNewSession = false;
}
// ── Public API ──────────────────────────────────────────────────────
@@ -122,6 +133,8 @@ class BooksLM {
this._updateStatus();
this._renderMessages();
this._showSuggestions();
+ this._closeHistoryMenu();
+ this._renderHistoryMenu();
requestAnimationFrame(() => {
this._panel.classList.add('open');
@@ -194,17 +207,27 @@ class BooksLM {
}
newConversation() {
- this._messages = [];
- this._saveHistory();
- if (this._panel) {
- this._renderMessages();
- this._showSuggestions();
+ // Called before the panel exists (command palette): defer the fresh
+ // session until the assistant opens for its actual context.
+ if (!this._panel) {
+ this._pendingNewSession = true;
+ this._messages = [];
+ return;
}
+ const session = this._createSession([]);
+ this._sessions.unshift(session);
+ this._currentSessionId = session.id;
+ this._messages = session.messages;
+ this._persistSessions();
+ this._renderMessages();
+ this._showSuggestions();
+ this._renderHistoryMenu();
}
exportConversation() {
if (!this._messages.length) return;
- const label = this._contextTitle() || 'assistant';
+ const current = this._currentSession();
+ const label = (current && current.title) || this._contextTitle() || 'assistant';
let md = `# ${label}\n\n`;
for (const msg of this._messages) {
const role = msg.role === 'user' ? '**You**' : '**AI**';
@@ -219,9 +242,60 @@ class BooksLM {
}
openFile(path) {
- window.dispatchEvent(new CustomEvent('obsigate:open-file', {
- detail: { vault: this._vault, path },
- }));
+ return this._openFileLink(path);
+ }
+
+ /** Resolve the vault used to open/reveal a link mentioned by the assistant. */
+ _resolveVault() {
+ if (this._vault) return this._vault;
+ try {
+ const docs = collectOpenDocuments();
+ if (docs.length) return docs[0].vault;
+ } catch { /* not ready */ }
+ return state.currentVault || null;
+ }
+
+ /** Open a file mentioned in an assistant answer in the main viewer. */
+ async _openFileLink(path) {
+ const vault = this._resolveVault();
+ if (!vault || !path) {
+ showToast(t('bookslm.no_vault'), 'error');
+ return;
+ }
+ window.dispatchEvent(new CustomEvent('obsigate:open-file', { detail: { vault, path } }));
+ try {
+ const m = await import('./viewer.js');
+ if (m && typeof m.openFile === 'function') await m.openFile(vault, path);
+ } catch (e) {
+ console.warn('AI assistant: open file link failed', e);
+ }
+ // Reposition the tree indicator on the opened file (best-effort).
+ try {
+ const s = await import('./sidebar.js');
+ if (s && typeof s.focusPathInSidebar === 'function') {
+ await s.focusPathInSidebar(vault, path, { alignToTop: true });
+ }
+ } catch (e) {
+ console.warn('AI assistant: reveal file in tree failed', e);
+ }
+ }
+
+ /** Reveal a directory (or file) path in the sidebar tree. */
+ async _revealPath(path) {
+ const vault = this._resolveVault();
+ if (!vault || !path) {
+ showToast(t('bookslm.no_vault'), 'error');
+ return;
+ }
+ window.dispatchEvent(new CustomEvent('obsigate:reveal-in-tree', { detail: { vault, path } }));
+ try {
+ const m = await import('./sidebar.js');
+ if (m && typeof m.focusPathInSidebar === 'function') {
+ await m.focusPathInSidebar(vault, path, { alignToTop: true, expandTarget: true });
+ }
+ } catch (e) {
+ console.warn('AI assistant: reveal path failed', e);
+ }
}
// ── Context ─────────────────────────────────────────────────────────
@@ -302,12 +376,14 @@ class BooksLM {
+
+
${t('ai.model_settings')}
@@ -333,8 +409,31 @@ class BooksLM {
panel.querySelector('.bookslm-btn-close').addEventListener('click', () => this.close());
panel.querySelector('.bookslm-btn-new').addEventListener('click', () => this.newConversation());
+ panel.querySelector('.bookslm-btn-history').addEventListener('click', (e) => {
+ e.stopPropagation();
+ this._toggleHistoryMenu();
+ });
panel.querySelector('.bookslm-btn-export').addEventListener('click', () => this.exportConversation());
panel.querySelector('.bookslm-btn-toggle').addEventListener('click', () => this._toggleSidebar());
+
+ // Clicking a file/path link inside an assistant answer either opens the
+ // file in the viewer or reveals the path in the sidebar tree.
+ panel.querySelector('.bookslm-messages').addEventListener('click', (e) => {
+ const link = e.target.closest('a.bookslm-path-link');
+ if (!link) return;
+ e.preventDefault();
+ const path = link.getAttribute('data-path');
+ if (!path) return;
+ if (link.getAttribute('data-kind') === 'dir') this._revealPath(path);
+ else this._openFileLink(path);
+ });
+
+ // Close the session menu when clicking elsewhere in the panel.
+ panel.addEventListener('click', (e) => {
+ if (!e.target.closest('.bookslm-history-menu') && !e.target.closest('.bookslm-btn-history')) {
+ this._closeHistoryMenu();
+ }
+ });
panel.querySelector('.bookslm-btn-fullscreen').addEventListener('click', () => {
this._isFullscreen = !this._isFullscreen;
panel.classList.toggle('fullscreen', this._isFullscreen);
@@ -710,49 +809,491 @@ class BooksLM {
this._saveHistory();
}
- // ── Markdown (lightweight) ──────────────────────────────────────────
+ // ── Markdown (self-contained renderer) ──────────────────────────────
+ //
+ // Renders the assistant answer as formatted HTML (headings, lists, tables,
+ // blockquotes, code, emphasis) without any external dependency. All model
+ // text is HTML-escaped before markup is produced, and file/path mentions
+ // become clickable links handled by the delegated listener in `_render()`.
_renderMarkdown(text) {
if (!text) return '';
- // Escape HTML first so model output cannot inject markup.
- let html = text
+ const lines = String(text).replace(/\r\n?/g, '\n').split('\n');
+ const out = [];
+ const para = [];
+ let i = 0;
+
+ const flushPara = () => {
+ if (!para.length) return;
+ out.push(`