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-*.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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}`;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user