fix(ai): liens sans vault, navigation clavier des menus et purge caches (BUG-013, BUG-014)
CI / lint (push) Successful in 1m9s
CI / security (push) Successful in 45s
CI / test (push) Successful in 2m27s
CI / build (push) Successful in 40s
CI / e2e (push) Successful in 10m47s

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-*.
This commit is contained in:
2026-09-12 16:44:12 -04:00
parent ee4c273d73
commit a5201a62e0
7 changed files with 108 additions and 14 deletions
+16
View File
@@ -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
+4 -1
View File
@@ -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) |
---
+10 -1
View File
@@ -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
+3 -3
View File
@@ -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();
+23 -8
View File
@@ -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') {
+1 -1
View File
@@ -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}`;
+51
View File
@@ -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) {