feat(ai): menu @ instantane, selecteurs agrandis et BooksLM racine (#82)
CI / lint (push) Successful in 1m11s
CI / security (push) Successful in 47s
CI / test (push) Successful in 2m12s
CI / build (push) Successful in 45s
CI / e2e (push) Successful in 10m21s

Nouvel endpoint GET /api/vault/{vault}/paths + prechargement et filtrage client du menu @ (affichage instantane des fichiers/repertoires). Selecteurs Fournisseur/Modele agrandis (0,8rem / 34px) pour s'aligner sur le reste du site. Ajout de BooksLM au menu contextuel de la racine des vaults.
This commit is contained in:
2026-09-12 20:09:08 -04:00
parent d6cdd670b1
commit 9274dbd49f
10 changed files with 218 additions and 26 deletions
+41
View File
@@ -1121,6 +1121,47 @@ async function main() {
localStorage.clear();
});
// ── 42c. Fast `@` menu: client-side filtering of the prefetched path index ──
await test("mention menu filters the prefetched path index instantly", async () => {
localStorage.clear();
const calls = [];
globalThis.fetch = async (url) => {
const u = String(url);
calls.push(u);
if (u.includes("/paths")) {
return {
ok: true, status: 200,
json: async () => ({ vault: "V", count: 3, results: [
{ vault: "V", path: "notes/a.md", name: "a.md", type: "file" },
{ vault: "V", path: "notes/b.md", name: "b.md", type: "file" },
{ vault: "V", path: "projects", name: "projects", type: "directory" },
] }),
};
}
return { ok: true, status: 200, json: async () => ({}) };
};
const b = new BooksLM();
b._panel = b._render();
document.body.appendChild(b._panel);
b._vault = "V";
await b._showMentionMenu("notes");
const menu = b._panel.querySelector(".bookslm-mention-menu");
assert.equal(menu.querySelectorAll(".bookslm-menu-item").length, 2, "client-side filter keeps notes/*");
assert.ok(!calls.some((u) => u.includes("/api/tree-search")), "no server search once the path cache is warm");
b._panel.remove();
localStorage.clear();
});
await test("vault root context menu exposes BooksLM", async () => {
const { readFileSync } = await import("node:fs");
const src = readFileSync(path.join(JS_DIR, "ui.js"), "utf-8");
const vaultBranch = src.slice(
src.indexOf("if (type === 'vault')"),
src.indexOf("} else if (type === 'directory')")
);
assert.ok(vaultBranch.includes("BooksLM"), "BooksLM entry present in the vault root context menu");
});
// ── Summary ──
console.log(`\n${passCount}/${testCount} tests passed`);
if (passCount !== testCount) {
+16
View File
@@ -312,6 +312,22 @@ class TestTreeSearch:
data = resp.json()
assert len(data["results"]) == 0
def test_vault_paths_returns_indexed_paths(self, client):
"""Flat path index backs the AI assistant `@` mention menu."""
resp = client.get("/api/vault/TestVault/paths")
assert resp.status_code == 200
data = resp.json()
assert data["vault"] == "TestVault"
paths = [p["path"] for p in data["results"]]
assert "note1.md" in paths
assert all("type" in p for p in data["results"])
def test_vault_paths_rejects_unknown_vault(self, client):
resp = client.get("/api/vault/DoesNotExist/paths")
assert resp.status_code in (200, 403, 404)
if resp.status_code == 200:
assert resp.json()["results"] == []
class TestSearchReplace:
def test_search_replace_invalid(self, client):