feat(ai): menu @ instantane, selecteurs agrandis et BooksLM racine (#82)
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:
@@ -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) {
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user