From a5201a62e08a32a47066efda0368aaac6365d8f0 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Sat, 12 Sep 2026 16:44:12 -0400 Subject: [PATCH] fix(ai): liens sans vault, navigation clavier des menus et purge caches (BUG-013, BUG-014) BUG-013: _openFileLink/_revealPath utilisent _activeVault() (contexte -> selection -> premier vault). BUG-014: navigation up/down geree au niveau du panneau en phase de capture, independante du focus. Caches: SW_VERSION v6 + migration qui purge tous les caches obsigate-*. --- CHANGELOG.md | 16 +++++++++ docs/ISSUES_TODOLIST.md | 5 ++- docs/features/ai-provider-picker.md | 11 ++++++- frontend/index.html | 6 ++-- frontend/js/bookslm.js | 31 +++++++++++++----- frontend/sw.js | 2 +- tests/frontend/ai.test.mjs | 51 +++++++++++++++++++++++++++++ 7 files changed, 108 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23be7ec..757590c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -235,6 +235,22 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Corrigé +- **BUG-014 — Assistant IA : navigation clavier ↑/↓ des menus inopérante** — la navigation était + liée au `keydown` du champ de saisie : dès que le focus quittait la zone de texte (clic sur le + menu, bouton Envoyer…), les flèches ne faisaient plus défiler les items. La gestion est déplacée + au niveau du **panneau en phase de capture**, ce qui fonctionne quel que soit l'élément focalisé + dans la barre latérale. Tests JSDOM (`tests/frontend/ai.test.mjs` +1). + +- **BUG-013 — Assistant IA : « Aucun vault actif pour ouvrir ce lien »** — les liens + fichiers/répertoires des réponses utilisaient `_resolveVault()` seul, nul en mode Général sans + document ouvert. `_openFileLink()` et `_revealPath()` utilisent désormais `_activeVault()` qui + retombe sur le vault de contexte, le vault sélectionné dans la barre latérale, puis le premier + vault disponible. Tests JSDOM (`tests/frontend/ai.test.mjs` +1). + +- **Assistant IA — caches obsolètes** — `SW_VERSION` porté à **v6** et migration de purge élargie à + tous les caches `obsigate-*` (clé `obsigate-sw-migration` → `v3`), afin que les clients bloqués sur + une ancienne version récupèrent bien le menu `@` et les correctifs. + - **BUG-012 — Assistant IA : la commande `@` n'affichait aucun menu en mode Général** — sans vault résolu, la détection `@` ne listait rien (menu masqué) et le backend ignorait ensuite le contexte. Le menu est désormais **toujours rendu** et `_mentionVault()` retombe sur le vault de diff --git a/docs/ISSUES_TODOLIST.md b/docs/ISSUES_TODOLIST.md index cda4873..bab6656 100644 --- a/docs/ISSUES_TODOLIST.md +++ b/docs/ISSUES_TODOLIST.md @@ -121,7 +121,9 @@ Avant de corriger quoi que ce soit, un agent IA doit : | *BUG-009* | Assistant IA : liste de modèles corrompue (art ASCII) et clés i18n brutes (`ai.model_search`) sous OpenRouter | 🟢 corrigé | P1 | 📱 frontend | IA | `frontend/js/ai.js`, `frontend/js/i18n.js`, `frontend/sw.js`, `frontend/style.css` | Ouvrir le menu modèle sous « openrouter » | Locale non content-hashée en cache HTTP → `cache:'no-store'` + bump `SW_VERSION` v4 ; styles critiques du picker appliqués **en ligne** (popover absolu, liste en colonne, options `display:block`) ; rendu plafonné à 200 modèles + indicateur « … N autres » | OpenRouter expose plusieurs centaines de modèles ; si `style.css` est en cache, la liste s'affichait en bloc inline (art ASCII) et les clés i18n brutes provenaient d'un `fr.json` obsolète. Tests : `tests/frontend/ai.test.mjs` (+1) | | *BUG-010* | Assistant IA : la commande `@` n'ajoute pas le contexte à la requête en mode Général | 🟢 corrigé | P1 | 📱 frontend | IA | `frontend/js/bookslm.js` | Assistant en mode Général, taper `@fichier`, sélectionner, envoyer | Capture du `vault` renvoyé par `/api/tree-search` dans la sélection `@` ; `_contextVault()` propage ce vault aux requêtes `/context` et `/chat` ; recherches `@` suivantes limitées à ce vault | En mode Général, `_vault` est nul : la requête partait sans vault et le backend ignorait `extra_files`. Tests : `tests/frontend/ai.test.mjs` (+1) | | *BUG-011* | Assistant IA : noms de modèles illisibles dans la liste déroulante | 🟢 corrigé | P2 | 📱 frontend | IA | `frontend/js/ai.js`, `frontend/style.css` | Ouvrir le menu modèle dans la barre latérale de l'assistant | Popover aligné à droite du déclencheur (`right:0`), largeur 340 px bornée à `100vw - 24px`, noms sur plusieurs lignes (`overflow-wrap:anywhere`, police 0,78 rem) + `title` | La liste s'ouvrait vers la droite et dépassait le bord de la sidebar ancrée à droite ; noms tronqués. Tests : `tests/frontend/ai.test.mjs` | -| *BUG-012* | Assistant IA : la commande `@` n'affiche aucun menu de sélection en mode Général | 🟢 corrigé | P1 | 📱 frontend | IA | `frontend/js/bookslm.js` | Assistant en mode Général (aucun vault), taper `@` | `_showMentionMenu` rend **toujours** le menu (même vide) et `_mentionVault()` retombe sur `state.selectedContextVault` puis le premier `state.allVaults`, avant `vault=all` pour les requêtes | Sans vault résolu, l'ancien code masquait le menu et le backend ignorait le contexte. Tests : `tests/frontend/ai.test.mjs` (+1) | +| *BUG-012* | Assistant IA : la commande `@` n'affiche aucun menu de sélection en mode Général | 🟢 corrigé | P1 | 📱 frontend | IA | `frontend/js/bookslm.js` | Assistant en mode Général (aucun vault), taper `@` | `_showMentionMenu` rend **toujours** le menu (même vide) et `_activeVault()` retombe sur `state.selectedContextVault` puis le premier `state.allVaults`, avant `vault=all` pour les requêtes | Sans vault résolu, l'ancien code masquait le menu et le backend ignorait le contexte. Tests : `tests/frontend/ai.test.mjs` (+1) | +| *BUG-013* | Assistant IA : les liens fichiers/répertoires des réponses affichent « Aucun vault actif pour ouvrir ce lien » | 🟢 corrigé | P1 | 📱 frontend | IA | `frontend/js/bookslm.js` | En mode Général, cliquer un lien de fichier/dossier dans une réponse | `_openFileLink()` et `_revealPath()` utilisent `_activeVault()` (vault de contexte → vault sélectionné → premier vault) au lieu de `_resolveVault()` seul | `_resolveVault()` est nul sans document ouvert ; les liens échouaient en mode Général. Tests : `tests/frontend/ai.test.mjs` (+1) | +| *BUG-014* | Assistant IA : les flèches ↑/↓ ne naviguent pas dans les menus `/` et `@` | 🟢 corrigé | P1 | 📱 frontend | IA | `frontend/js/bookslm.js` | Ouvrir `/`, puis ↑/↓ (y compris après un clic hors zone de texte) | Navigation gérée au niveau du **panneau en phase de capture** (`panel.addEventListener('keydown', ..., true)`), donc indépendante du focus | Le `keydown` n'était écouté que sur la zone de texte ; dès que le focus changeait, les flèches étaient ignorées. Tests : `tests/frontend/ai.test.mjs` (+1) | ### TODOs techniques (améliorations / nouvelles tâches) @@ -149,6 +151,7 @@ Avant de corriger quoi que ce soit, un agent IA doit : | 2026-09-12 | BUG-009 | Correction | `frontend/js/ai.js`, `frontend/js/i18n.js`, `frontend/sw.js`, `frontend/style.css`, `frontend/locales/{fr,en}.json`, `tests/frontend/ai.test.mjs` | BUG-009 : locale non content-hashée servie depuis le cache HTTP → clés i18n brutes ; passage en `cache:'no-store'` + `SW_VERSION` v4. Liste de modèles corrompue (OpenRouter, styles en cache) → styles critiques en ligne, options `display:block`, plafond 200 + indicateur. Vérifié : tests frontend 42/42. | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-12 | BUG-010, BUG-011 | Correction | `frontend/js/bookslm.js`, `frontend/js/ai.js`, `frontend/style.css`, `tests/frontend/ai.test.mjs` | BUG-010 : capture du `vault` des résultats `@` + `_contextVault()` propagé aux requêtes `/context`/`/chat` (mode Général). BUG-011 : popover modèle aligné à droite, largeur 340 px, noms multi-lignes + `title`. Vérifié en navigateur (Playwright) et tests frontend 43/43. | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-12 | BUG-012, #82 | Correction + UX | `frontend/js/bookslm.js`, `frontend/js/ai.js`, `frontend/style.css`, `frontend/sw.js`, `frontend/locales/{fr,en}.json`, `tests/frontend/ai.test.mjs` | BUG-012 : le menu `@` est toujours rendu et `_mentionVault()` retombe sur le vault de contexte puis le premier vault disponible. #82 : suppression des intitulés « Fournisseur & modèle » / « Fournisseur : », description du contexte Général déplacée en info-bulle, placeholder retiré, bouton Envoyer en emoji ✈️, indicateur d'activité (envoi/réception/outils/confirmation/succès/échec), `SW_VERSION` v5. Vérifié Playwright + tests frontend 45/45. | 🟢 corrigé (en attente vérif utilisateur) | +| 2026-09-12 | BUG-013, BUG-014 | Correction | `frontend/js/bookslm.js`, `frontend/index.html`, `frontend/sw.js`, `tests/frontend/ai.test.mjs` | BUG-013 : liens fichiers/dossiers via `_activeVault()` (vault de contexte → sélection → premier vault). BUG-014 : navigation ↑/↓ gérée au niveau du panneau en phase de capture. Purge des caches élargie (`SW_VERSION` v6, migration `obsigate-*`). Vérifié : tests frontend 47/47, pytest 930. | 🟢 corrigé (en attente vérif utilisateur) | --- diff --git a/docs/features/ai-provider-picker.md b/docs/features/ai-provider-picker.md index 78ac31b..4ac46c6 100644 --- a/docs/features/ai-provider-picker.md +++ b/docs/features/ai-provider-picker.md @@ -63,7 +63,16 @@ - [x] `@` : le menu est toujours rendu et `_mentionVault()` retombe sur le vault de contexte puis le premier vault disponible (BUG-012). -## G. Points d'attention +## G. Robustesse menus & liens (BUG-013, BUG-014) — ✅ livré +- [x] Navigation clavier ↑/↓ des menus `/` et `@` gérée au niveau du **panneau en phase de + capture** : fonctionne quel que soit l'élément focalisé (BUG-014). +- [x] Les liens fichiers/répertoires des réponses utilisent `_activeVault()` (vault de contexte → + vault sélectionné → premier vault), ce qui supprime l'erreur « Aucun vault actif » en mode + Général (BUG-013). +- [x] Purge des caches élargie : `SW_VERSION` v6 + migration qui supprime tous les caches + `obsigate-*` (clé `obsigate-sw-migration` → `v3`). + +## H. Points d'attention - La table de capacités reste **statique** (`backend/model_capabilities.py`) : un modèle inconnu retombe sur le défaut du fournisseur. - Le select masqué est conservé uniquement pour la rétro-compatibilité ; ne pas le supprimer sans diff --git a/frontend/index.html b/frontend/index.html index ccde82f..3dd4b3f 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -5158,12 +5158,12 @@ curl -X POST https://votre-serveur.com/webhook \ if (!('serviceWorker' in navigator) || !('caches' in window)) return; var MIGRATION_KEY = 'obsigate-sw-migration'; try { - if (localStorage.getItem(MIGRATION_KEY) === 'v2') return; - localStorage.setItem(MIGRATION_KEY, 'v2'); + if (localStorage.getItem(MIGRATION_KEY) === 'v3') return; + localStorage.setItem(MIGRATION_KEY, 'v3'); } catch (e) { return; } caches.keys().then(function (names) { return Promise.all(names.map(function (name) { - return /^obsigate-(v1|offline-v1)$/.test(name) ? caches.delete(name) : null; + return /^obsigate-/.test(name) ? caches.delete(name) : null; })); }).then(function () { return navigator.serviceWorker.getRegistration(); diff --git a/frontend/js/bookslm.js b/frontend/js/bookslm.js index c2c98c5..41cdd82 100644 --- a/frontend/js/bookslm.js +++ b/frontend/js/bookslm.js @@ -336,11 +336,11 @@ class BooksLM { } /** - * Concrete vault used to browse files in the `@` mention menu. Falls back to - * the sidebar's selected context vault (then the first vault) so the menu can - * list files even in General mode where no vault is otherwise resolved. + * Concrete vault used by the assistant when none is explicitly resolved: + * falls back to the sidebar's selected context vault, then the first + * available vault. Used to browse `@` files and to open/reveal links. */ - _mentionVault() { + _activeVault() { const resolved = this._contextVault(); if (resolved) return resolved; const selected = state.selectedContextVault; @@ -351,7 +351,7 @@ class BooksLM { /** Open a file mentioned in an assistant answer in the main viewer. */ async _openFileLink(path) { - const vault = this._resolveVault(); + const vault = this._activeVault(); if (!vault || !path) { showToast(t('bookslm.no_vault'), 'error'); return; @@ -376,7 +376,7 @@ class BooksLM { /** Reveal a directory (or file) path in the sidebar tree. */ async _revealPath(path) { - const vault = this._resolveVault(); + const vault = this._activeVault(); if (!vault || !path) { showToast(t('bookslm.no_vault'), 'error'); return; @@ -583,6 +583,21 @@ class BooksLM { this._onComposerInput(); }); textarea.addEventListener('paste', (e) => this._onPaste(e)); + + // Menu navigation is handled at the panel level (capture phase) so the + // arrow keys keep working even if focus is not exactly on the textarea + // (e.g. after interacting with the menu). It runs before the textarea + // handler and stops propagation when it consumes the event. + panel.addEventListener('keydown', (e) => { + if (!this._menuKind) return; + const isComposer = e.target && e.target.tagName === 'TEXTAREA'; + const isNav = e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'Escape'; + const isPick = e.key === 'Enter' && isComposer && !e.ctrlKey && !e.metaKey; + if ((isNav || isPick) && this._handleMenuKey(e)) { + e.stopPropagation(); + } + }, true); + textarea.addEventListener('keydown', (e) => { // Menu navigation (mention/command) takes precedence over submit. if (this._handleMenuKey(e)) return; @@ -841,7 +856,7 @@ class BooksLM { async _showMentionMenu(query) { if (!this._panel) return; const seq = ++this._menuSeq; - const vault = this._mentionVault(); + const vault = this._activeVault(); try { let results = []; if (query) { @@ -880,7 +895,7 @@ class BooksLM { textarea.focus(); } const path = item.id; - const vault = item.vault || this._resolveVault(); + const vault = item.vault || this._activeVault(); if (IMAGE_EXT_RE.test(path)) { this._addImagePath(path, vault); } else if (item.type === 'dir' || this._classifyPath(path) === 'dir') { diff --git a/frontend/sw.js b/frontend/sw.js index afe7c24..5b4ae55 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -11,7 +11,7 @@ * cache or Cloudflare does NOT clear the Service Worker Cache Storage, which is * a separate store. Bumping SW_VERSION invalidates it on every release. */ -const SW_VERSION = 'v5'; +const SW_VERSION = 'v6'; const CODE_CACHE = `obsigate-code-${SW_VERSION}`; const RUNTIME_CACHE = `obsigate-runtime-${SW_VERSION}`; const API_CACHE = `obsigate-api-${SW_VERSION}`; diff --git a/tests/frontend/ai.test.mjs b/tests/frontend/ai.test.mjs index 65c8440..a629a9a 100644 --- a/tests/frontend/ai.test.mjs +++ b/tests/frontend/ai.test.mjs @@ -1070,6 +1070,57 @@ async function main() { localStorage.clear(); }); + await test("active vault falls back to the first available vault", async () => { + const stateMod = await import(pathToFileURL(path.join(JS_DIR, "state.js")).href); + const prevVaults = stateMod.state.allVaults; + const prevSelected = stateMod.state.selectedContextVault; + stateMod.state.allVaults = [{ name: "V1" }, { name: "V2" }]; + stateMod.state.selectedContextVault = "all"; + const b = new BooksLM(); + b._vault = null; + assert.equal(b._activeVault(), "V1", "first vault used for links/@ browsing"); + stateMod.state.selectedContextVault = "V2"; + assert.equal(b._activeVault(), "V2", "selected context vault takes precedence"); + stateMod.state.allVaults = prevVaults; + stateMod.state.selectedContextVault = prevSelected; + }); + + await test("menu navigation works from the panel, not only the textarea", async () => { + localStorage.clear(); + globalThis.fetch = async (url) => { + if (String(url).includes("/api/ai/skills")) { + return { + ok: true, status: 200, + json: async () => ({ + skills: [ + { id: "a", label: "Alpha", type: "skill", description: "d" }, + { id: "b", label: "Beta", type: "skill", description: "d" }, + ], + commands: [], + }), + }; + } + return { ok: true, status: 200, json: async () => ({}) }; + }; + const b = new BooksLM(); + b._panel = b._render(); + document.body.appendChild(b._panel); + const ta = b._panel.querySelector("textarea"); + ta.value = "/"; + ta.selectionStart = ta.selectionEnd = 1; + b._onComposerInput(); + await sleep(20); + const items = () => Array.from(b._panel.querySelectorAll(".bookslm-command-menu .bookslm-menu-item")); + assert.equal(items().length, 2); + + // Dispatch on the panel itself (focus elsewhere) — the capture handler must + // still navigate the menu. + b._panel.dispatchEvent(new w.KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true, cancelable: true })); + assert.ok(items()[1].classList.contains("active"), "ArrowDown from the panel moves the highlight"); + b._panel.remove(); + localStorage.clear(); + }); + // ── Summary ── console.log(`\n${passCount}/${testCount} tests passed`); if (passCount !== testCount) {