feat: cache repo list to disk, make search bar a local filter only

- list_all_repos() now saves results to .repo_cache.json on first fetch;
  subsequent startups load from cache instantly with no HF API calls
- GET /modeldownloader/repos?force=1 bypasses cache (used by Refresh button)
- Search bar is now a pure client-side filter (repo name/id matching);
  no API calls on typing — use Search Files or Search HuggingFace buttons
  for API-backed searches

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
BISAM20
2026-03-20 13:46:09 +01:00
co-authored by Claude Sonnet 4.6
parent e01919e59f
commit 229f136dc4
4 changed files with 38 additions and 35 deletions
+5 -2
View File
@@ -29,9 +29,12 @@ from .model_manager import (
async def handle_list_repos(request: web.Request) -> web.Response:
"""GET /modeldownloader/repos — sorted by lastModified desc."""
"""GET /modeldownloader/repos — sorted by lastModified desc.
Pass ?force=1 to bypass the disk cache and re-fetch from HuggingFace.
"""
force = request.rel_url.query.get("force", "0") == "1"
loop = asyncio.get_event_loop()
repos = await loop.run_in_executor(None, list_all_repos)
repos = await loop.run_in_executor(None, list_all_repos, force)
return web.json_response(repos)
+18 -1
View File
@@ -2,6 +2,7 @@
Model Manager: HuggingFace + GitHub repo scanning, local model scanning,
file classification, downloading.
"""
import json
import os
import re
import threading
@@ -11,6 +12,8 @@ import requests
from pathlib import Path
from typing import Optional
_REPO_CACHE_FILE = Path(__file__).parent / ".repo_cache.json"
try:
from huggingface_hub import HfApi
HF_AVAILABLE = True
@@ -539,7 +542,14 @@ def get_specific_repo_info(repo_id: str) -> dict:
}
def list_all_repos() -> list[dict]:
def list_all_repos(force: bool = False) -> list[dict]:
# Return cached result unless a forced refresh is requested
if not force and _REPO_CACHE_FILE.exists():
try:
return json.loads(_REPO_CACHE_FILE.read_text(encoding="utf-8"))
except Exception:
pass
all_repos: list[dict] = []
seen: set[str] = set()
for author in TRACKED_AUTHORS:
@@ -553,6 +563,13 @@ def list_all_repos() -> list[dict]:
seen.add(repo_id)
# Sort by lastModified descending (blank strings sort last)
all_repos.sort(key=lambda r: r.get("lastModified") or "", reverse=True)
# Persist to disk cache
try:
_REPO_CACHE_FILE.write_text(json.dumps(all_repos, ensure_ascii=False), encoding="utf-8")
except Exception:
pass
return all_repos
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "comfyui-advanced-model-manager"
version = "1.0.6"
version = "1.0.7"
description = "Advanced Model Manager for ComfyUI — browse, search and download models from HuggingFace and GitHub workflows directly from the ComfyUI interface."
license = { file = "LICENSE" }
dependencies = [
+14 -31
View File
@@ -206,7 +206,7 @@ class ModelDownloaderDialog {
// Search box
this.searchInput = el("input",{
type:"text",placeholder:"🔍 Search repos or files…",
type:"text",placeholder:"🔍 Filter repos…",
style:{flex:"1",minWidth:"160px",padding:"6px 12px",
background:"#0d1117",border:"1px solid #30363d",
borderRadius:"8px",color:"#e0e0e0",fontSize:"13px"},
@@ -327,7 +327,7 @@ class ModelDownloaderDialog {
this.leftList.innerHTML="";
try {
const [rr,lr] = await Promise.all([
fetch("/modeldownloader/repos"),
fetch(`/modeldownloader/repos${force ? "?force=1" : ""}`),
fetch("/modeldownloader/local_models"),
]);
S.repos = await rr.json();
@@ -437,37 +437,20 @@ class ModelDownloaderDialog {
// ── Search ─────────────────────────────────────────────────────────────────
_onSearch() {
// Search bar is always a local filter only.
// API searches are triggered exclusively via the dedicated buttons.
S.hfSearchResults = null;
const q = S.searchQuery.trim();
S.fileSearchResults = null;
this._renderLeft();
// LOCAL mode: just filter in place
if (S.browseMode === MODE_LOCAL) {
S.fileSearchResults = null;
this._renderLeft();
if (S.localCategory) this._renderLocalFiles(S.localCategory);
return;
}
// WORKFLOW mode: just filter in place
if (S.browseMode === MODE_WORKFLOW) {
S.fileSearchResults = null;
this._renderLeft();
if (S.githubWorkflowFiles) this._renderGithubWorkflows(S.githubWorkflowFiles);
return;
}
// Empty query → clear search, go back to normal view
if (!q) {
S.fileSearchResults = null;
this._renderLeft();
if (S.selectedRepo && S.repoFiles[S.selectedRepo])
this._renderFileList(S.repoFiles[S.selectedRepo]);
else this._showPlaceholder(true);
return;
}
// Otherwise auto-search across all repos
this._triggerFileSearch();
if (S.browseMode === MODE_LOCAL && S.localCategory)
this._renderLocalFiles(S.localCategory);
else if (S.browseMode === MODE_WORKFLOW && S.githubWorkflowFiles)
this._renderGithubWorkflows(S.githubWorkflowFiles);
else if (S.selectedRepo && S.repoFiles[S.selectedRepo])
this._renderFileList(S.repoFiles[S.selectedRepo]);
else if (!S.searchQuery.trim())
this._showPlaceholder(true);
}
async _triggerFileSearch() {