fix(ai): la liste des fournisseurs de l'assistant suit la configuration (BUG-043)
CI / lint (push) Successful in 1m17s
CI / security (push) Successful in 56s
CI / test (push) Successful in 2m58s
CI / build (push) Successful in 47s
CI / e2e (push) Successful in 10m38s

Le picker lit /api/ai/status une seule fois, à sa construction, et le panneau
de l'assistant est un singleton monté pour toute la session : ajouter ou
supprimer une clé API dans la configuration du projet laissait la liste des
fournisseurs figée jusqu'à un rechargement de page.

- ai.js : nouveau refreshAIPickers() qui reconstruit chaque picker monté dans
  son emplacement .ai-picker-slot (constante PICKER_SLOT_CLASS) ; le slot est
  conservé même sans fournisseur configuré, donc le premier fournisseur ajouté
  s'y monte aussi ; une sélection persistée dont le fournisseur n'est plus
  configuré est purgée de obsigate_ai_picker (retour au défaut, plus de modèle
  fantôme dans le déclencheur).
- bookslm.js : l'emplacement .bookslm-picker-host porte la classe
  ai-picker-slot et reste dans la barre (replaceChildren au lieu de replaceWith).
- config.js : refreshAIPickers() après saveAIKeys() et deleteAIKey().
- tests : +4 tests JSDOM (ajout/retrait dans la barre, premier montage dans un
  slot vide, purge de la sélection orpheline, câblage save/delete).

Vérifié : tests frontend 66/66 (IA) + 9 suites JSDOM, validate-imports 36
modules, pytest 963 passed / 6 skipped, ruff 0, et contrôle navigateur
(Playwright) sur l'instance de test — ajout de nvidia visible sans rechargement,
retrait effectif + sélection réinitialisée.
This commit is contained in:
2026-09-14 13:59:08 -04:00
parent c3e6841293
commit ce2f0b6a6c
8 changed files with 195 additions and 6 deletions
+46 -2
View File
@@ -434,8 +434,16 @@ async function _buildPickerUI() {
_updateCaps();
});
// Load models for the initial provider selection
if (pickerState.provider && providerNames.includes(pickerState.provider)) {
// Load models for the initial provider selection. A provider that is no
// longer configured (its API key was removed) is dropped from the stored
// selection so the toolbar never advertises a stale provider/model.
if (pickerState.provider && !providerNames.includes(pickerState.provider)) {
pickerState.provider = null;
pickerState.model = null;
delete pickerState.capabilities;
_writePicker(pickerState);
}
if (pickerState.provider) {
_loadModels(pickerState.provider);
} else {
_syncTrigger();
@@ -450,6 +458,40 @@ async function _buildPickerUI() {
return wrap;
}
// Mount point that survives even when no picker can be built (no configured
// provider). Keeping it lets a later refresh mount the picker at the same spot.
const PICKER_SLOT_CLASS = 'ai-picker-slot';
/**
* Rebuild every mounted provider/model picker so the provider list matches the
* current AI configuration of the project.
*
* A picker reads `/api/ai/status` once, when it is built — and the assistant
* panel is a singleton mounted for the whole session. Without this refresh,
* adding or removing a provider key in the project config left a stale
* provider list in the assistant sidebar until a full page reload.
*/
async function refreshAIPickers() {
const mounts = Array.from(document.querySelectorAll(`.${PICKER_SLOT_CLASS}, .ai-picker`));
let mounted = 0;
for (const mount of mounts) {
// A picker living inside a slot is detached once the slot is refilled.
if (!mount.isConnected) continue;
const fresh = await _buildPickerUI();
if (fresh) {
if (mount.classList.contains(PICKER_SLOT_CLASS)) mount.replaceChildren(fresh);
else mount.replaceWith(fresh);
mounted += 1;
} else if (!mount.classList.contains(PICKER_SLOT_CLASS)) {
// Every provider was removed: keep an inert mount point in place.
const slot = document.createElement('span');
slot.className = PICKER_SLOT_CLASS;
mount.replaceWith(slot);
}
}
return mounted;
}
// ── Get selected text from CodeMirror ──
function getSelection(editorView) {
if (!editorView) return '';
@@ -978,5 +1020,7 @@ export {
getModelCapabilities,
renderCapabilityList,
_buildPickerUI as buildAIPickerUI,
refreshAIPickers,
PICKER_SLOT_CLASS,
_promptRewriteInstruction,
};
+4 -2
View File
@@ -827,7 +827,7 @@ class BooksLM {
</div>
<div class="bookslm-history-menu hidden"></div>
<div class="bookslm-toolbar">
<span class="bookslm-picker-host"></span>
<span class="bookslm-picker-host ai-picker-slot"></span>
</div>
<div class="bookslm-status"></div>
<div class="bookslm-suggestions"></div>
@@ -854,7 +854,9 @@ class BooksLM {
const pickerHost = panel.querySelector('.bookslm-picker-host');
buildAIPickerUI().then((picker) => {
if (!picker || !pickerHost) return;
pickerHost.replaceWith(picker);
// Mount inside the host (kept as an `ai-picker-slot`) so a provider
// change in the config can rebuild the picker in place.
pickerHost.replaceChildren(picker);
if (typeof safeCreateIcons === 'function') safeCreateIcons();
}).catch(() => { /* ignore */ });
+6 -1
View File
@@ -6,7 +6,7 @@ import { syncVaultSelectors, setSelectedVaultContext, refreshSidebarForContext,
import { escapeHtml, safeCreateIcons } from './utils.js';
import { showToast, closeHeaderMenu, closeMobileSidebar } from './ui.js';
import { t, setLocale, getLocale } from './i18n.js';
import { getModelCapabilities, renderCapabilityList } from './ai.js';
import { getModelCapabilities, renderCapabilityList, refreshAIPickers } from './ai.js';
let _recentTimestampTimer = null;
let _recentFilesCache = [];
@@ -1511,6 +1511,9 @@ async function saveAIKeys() {
Object.keys(AI_KEY_MAP).forEach(id => { const el = document.getElementById(id); if (el) el.value = ""; });
loadAIKeys();
loadAIDefaults();
// The assistant sidebar lists the providers it found when the picker was
// built: rebuild it so a newly configured provider shows up immediately.
refreshAIPickers();
} catch(e) { showToast("Erreur: " + e.message, "error"); }
}
@@ -1523,6 +1526,8 @@ async function deleteAIKey(inputId) {
await api("/api/config/ai-keys/" + envName, { method: "DELETE" });
showToast("Cl\u00e9 " + provider + " supprim\u00e9e", "success");
loadAIKeys();
// Drop the removed provider from the assistant sidebar's provider list.
refreshAIPickers();
} catch(e) { showToast("Erreur: " + e.message, "error"); }
}