Files
ComfyUI-Lora-Manager/AGENTS.md
T
Will Miao f5e983eaaa feat: optional centralized storage for sidecar metadata and previews (#1045)
Add an opt-in 'centralized' sidecar storage mode alongside the default
'alongside' layout. In centralized mode, .metadata.json sidecars and
preview assets live under a configurable root (sidecar_storage_path,
default <settings_dir>/sidecars), mirroring the library-relative
directory structure: <root>/<library>/<root_basename>/<rel_dir>/.

Backend:
- settings: sidecar_storage_mode / sidecar_storage_path with validation;
  changing either refreshes the preview allowlist
- config: centralized root added to preview-serving allowlist
- lifecycle: delete / move / rename / folder-rename / folder-delete and
  undoable-delete staging all operate on the mirror tree in centralized
  mode (model files themselves never move); EXDEV-tolerant cross-
  filesystem moves
- scanners: pending-hash filesystem scan walks the mirror tree in
  centralized mode; preview discovery reads from the sidecar dir;
  .civitai.info stays co-located in both modes
- migration: SidecarMigrationUseCase moves sidecars+previews between
  layouts both directions (keep-newer conflict resolution, preview_url
  rewriting, WebSocket progress), exposed as POST+GET
  /api/lm/sidecars/migrate with a mode guard (force=true for the
  settings-first flow)

Frontend:
- settings modal: sidecar storage section (mode select + path input with
  browse/validation), mode-change confirmation offering immediate
  migration (force=true), and a 'Migrate Sidecars Now' action
- i18n keys synced to all locales ([TODO: Translate] placeholders)

Docs: metadata-json-schema.md gains a storage-location section;
AGENTS.md records the sidecar_paths helper convention.
2026-09-26 10:00:58 +08:00

13 KiB

AGENTS.md

This file provides guidance for agentic coding assistants working in this repository.

Overview

ComfyUI LoRA Manager is a comprehensive LoRA management system for ComfyUI that combines a Python backend with browser-based widgets. It provides model organization, downloading from CivitAI/CivArchive, recipe management, and one-click workflow integration.

Development Commands

Backend Development

# Install dependencies
pip install -r requirements.txt
pip install -r requirements-dev.txt

# Run standalone server (port 8188 by default)
python standalone.py --port 8188

# Run all backend tests
pytest

# Run specific test file
pytest tests/test_recipes.py

# Run specific test function
pytest tests/test_recipes.py::test_function_name

# Run backend tests with coverage
COVERAGE_FILE=coverage/backend/.coverage pytest \
  --cov=py --cov=standalone \
  --cov-report=term-missing \
  --cov-report=html:coverage/backend/html \
  --cov-report=xml:coverage/backend/coverage.xml \
  --cov-report=json:coverage/backend/coverage.json

Frontend Development (LoRA Manager Web UI)

# Install dependencies (root and Vue widgets)
npm install
cd vue-widgets && npm install && cd ..

npm test                    # Run all tests (JS + Vue)
npm run test:js             # Run JS tests only
npm run test:vue            # Run Vue widget tests only
npm run test:watch          # Watch mode (JS tests only)
npm run test:coverage       # Generate coverage report

Vue Widget Development

cd vue-widgets
npm install
npm run dev                 # Build in watch mode
npm run build               # Build production bundle
npm run typecheck           # Run TypeScript type checking
npm test                    # Run Vue widget tests
npm run test:watch          # Watch mode
npm run test:coverage       # Generate coverage report

Localization

# Sync translation keys after UI string updates
python scripts/sync_translation_keys.py

Locale files are in locales/ (en, zh-CN, zh-TW, ja, ko, fr, de, es, ru, he).

After adding keys to en.json and syncing, stop: the [TODO: Translate] placeholders in the other locales are the expected end state during feature development. Do NOT translate proactively — translate only when the feature owner explicitly asks (see docs/i18n-translation-guidelines.md §7).

Before translating anything, read docs/i18n-translation-guidelines.md — it defines the term conventions (e.g. "Recipe" stays untranslated in French, 配方 in Chinese; model-type and brand names are never translated), per-locale preferred renderings, placeholder rules, and the known confusion hot-spots.

Code Style

Python

Imports & Formatting

  • Use from __future__ import annotations for forward references
  • Group imports: standard library, third-party, local (blank line separated)
  • Use TYPE_CHECKING guard for type-checking-only imports
  • Absolute imports within py/: from ..services import X
  • PEP 8 with 4-space indentation, type hints required

Naming Conventions

  • Files: snake_case.py, Classes: PascalCase, Functions/vars: snake_case
  • Constants: UPPER_SNAKE_CASE, Private: _protected, __mangled

Error Handling & Async

  • Use logging.getLogger(__name__), define custom exceptions in py/services/errors.py
  • async def for I/O, @pytest.mark.asyncio for async tests
  • Singleton with asyncio.Lock: see ModelScanner.get_instance()
  • Return aiohttp.web.json_response or web.Response

JavaScript/TypeScript

Imports & Modules

  • ES modules: import { app } from "../../scripts/app.js" for ComfyUI
  • Vue: import { ref, computed } from 'vue', type imports: import type { Foo }
  • Export named functions: export function foo() {}

Naming & Formatting

  • camelCase for functions/vars/props, PascalCase for classes
  • Constants: UPPER_SNAKE_CASE, Files: snake_case.js or kebab-case.js
  • 2-space indentation preferred (follow existing file conventions)
  • Vue Single File Components: <script setup lang="ts"> preferred

Widget Development

  • Prefer vanilla JS for web/comfyui/ widgets; avoid framework dependencies (except the Vue widgets in vue-widgets/)
  • ComfyUI: app.registerExtension(), node.addDOMWidget(name, type, element, options)
  • Event handlers via addEventListener or widget callbacks
  • Shared utilities: web/comfyui/utils.js
  • Dual-mode rendering patterns (canvas vs Vue): see docs/comfyui-dual-mode-widgets.md

Vue Composables Pattern

  • Use composition API: useXxxState(widget), return reactive refs and methods
  • Guard restoration loops with flag: let isRestoring = false
  • Build config from state: const buildConfig = (): Config => { ... }

Architecture

Dual Mode Operation

The system runs in two modes:

  • ComfyUI plugin mode: Integrates with ComfyUI's PromptServer, uses folder_paths for model discovery
  • Standalone mode: standalone.py mocks ComfyUI dependencies, reads paths from settings.json
  • Detection: os.environ.get("LORA_MANAGER_STANDALONE", "0") == "1"

Backend Entry Points

  • __init__.py — ComfyUI plugin entry: registers nodes via NODE_CLASS_MAPPINGS, sets WEB_DIRECTORY, calls LoraManager.add_routes()
  • standalone.py — Standalone server: mocks folder_paths and node modules, starts aiohttp server
  • py/lora_manager.py — Main LoraManager class that registers all HTTP routes

Service Layer

  • ServiceRegistry singleton for DI, services use get_instance() classmethod
  • BaseModelService abstract base → LoraService, CheckpointService, EmbeddingService
  • ModelScanner base → LoraScanner, CheckpointScanner, EmbeddingScanner for file discovery with hash-based deduplication
  • PersistentModelCache (SQLite) for metadata persistence
  • MetadataSyncService — background sync from CivitAI/CivArchive APIs
  • SettingsManager — settings with schema migration support
  • WebSocketManager — real-time progress broadcasting
  • ModelServiceFactory — creates the right service for each model type
  • Use cases in py/services/use_cases/ orchestrate complex business logic (auto-organize, bulk refresh, downloads)
  • Separate scanners (discovery) from services (business logic)
  • Handlers in py/routes/handlers/ are pure functions with deps as params

Model Types & Routes

  • API endpoints follow /loras/*, /checkpoints/*, /embeddings/*, /other/* patterns
  • Route registrars organize endpoints by domain: ModelRouteRegistrar, RecipeRouteRegistrar, etc.
  • Request handlers in py/routes/handlers/ implement route logic
  • All routes use aiohttp, return web.json_response or web.Response
  • Endpoints consumed by the companion browser extension (lm-civitai-extension) MUST also accept GET with query-string params: the extension is GET-only by convention (see its AGENTS.md), even for state-changing operations such as GET /api/lm/recipe/{recipe_id}/reimport

Recipe System

  • Base: py/recipes/base.py, Enrichment: RecipeEnrichmentService in py/recipes/enrichment.py
  • Parsers: py/recipes/parsers/ for PNG metadata, JSON, and workflow formats

Custom Nodes

  • Location: py/nodes/, all nodes registered in __init__.py
  • Each node class has a NAME class attribute used as key in NODE_CLASS_MAPPINGS
  • Standard ComfyUI node pattern: INPUT_TYPES() classmethod, RETURN_TYPES, FUNCTION

Configuration

  • py/config.py manages folder paths for models and handles symlink mappings
  • Auto-saves paths to settings.json in ComfyUI mode
  • settings.json.example is intentionally minimal (see Important Notes); all other defaults live in DEFAULT_SETTINGS (py/services/settings_manager.py)
  • folder_paths vs extra_folder_paths — different purposes, do not conflate:
    • folder_paths (primary model roots): in ComfyUI plugin mode these come from the ComfyUI host; in standalone mode they are the ONLY source of model library paths and are currently edited by hand in settings.json.
    • extra_folder_paths is a ComfyUI-plugin-mode feature: paths visible ONLY to LoRA Manager, not to ComfyUI. Its motivation is that a very large model library slows ComfyUI itself down, while LoRA Manager handles large libraries without performance issues — so users keep ComfyUI's library small and add the bulk via extra_folder_paths.

Frontend UI Architecture

1. LoRA Manager Web UI

  • Location: ./static/ (JS/CSS) and ./templates/ (HTML)
  • Tech: Vanilla JS + CSS, served by the hosting server (ComfyUI app in plugin mode, standalone.py in standalone mode)
  • Tests: tests/frontend/**/*.test.js (vitest + jsdom)

2. ComfyUI Custom Node Widgets

  • Location: ./web/comfyui/ (Vanilla JS) + ./vue-widgets/ (Vue)
  • Primary styles: ./web/comfyui/lm_styles.css (NOT ./static/css/)
  • Vue widgets: Vue 3 + TypeScript + PrimeVue + vue-i18n, e.g. LoraPoolWidget, LoraRandomizerWidget, LoraCyclerWidget, AutocompleteTextWidget
  • Vue builds to ./web/comfyui/vue-widgets/; auto-built on ComfyUI startup via py/vue_widget_builder.py, typecheck via vue-tsc
  • Widget registration: app.registerExtension() and getCustomWidgets hooks; node.addDOMWidget(...) embeds HTML in LiteGraph nodes
  • See docs/dom_widget_dev_guide.md for the DOMWidget development guide

Testing

Backend (pytest)

  • Config in pytest.ini: --import-mode=importlib, testpaths=tests
  • Fixtures in tests/conftest.py mock ComfyUI dependencies; use tmp_path_factory for isolation
  • Markers: @pytest.mark.asyncio, @pytest.mark.no_settings_dir_isolation (tests needing real settings paths)

Frontend (vitest)

  • Vanilla JS tests: tests/frontend/**/*.test.js with jsdom; setup in tests/frontend/setup.js
  • Vue widget tests: vue-widgets/tests/**/*.test.ts with jsdom + @vue/test-utils

UI Verification (manual default)

UI/layout changes are verified by the user by eye — do NOT spin up a sandbox, standalone server, or browser automation to "prove" a visual fix. Ask the user to look instead. The full browser E2E ceremony (server + Chrome DevTools MCP + screenshots) is slow, token-heavy, and fragile; reserve it for genuine server+browser integration bugs, and only when the user explicitly agrees.

If a cross-layer issue ever needs a live server, the sandboxed helpers live in scripts/e2e/ (start_server.py, wait_for_server.py). Non-negotiable rules:

  • Always launch with --settings-path <sandbox>/settings and sandboxed folder_paths under /tmp — the repo folder is the real plugin folder and a settings.json there is read by the live instance. Never touch real config or real model libraries.
  • Never kill a process you did not start; start_server.py tracks its own PIDs via pidfile and refuses to touch unrelated processes on the port.
  • Abort after ~30 minutes or 3 consecutive tool failures; report BLOCKED with observed state instead of retrying blindly. Clean up sandbox and server after.

Key Integration Points

  • Settings: Stored in the user config directory (via platformdirs) or portable mode ("use_portable_settings": true)
  • CivitAI/CivArchive: API clients for metadata sync and model downloads; CivitAI API key stored in settings
  • Symlinks: Config scans symlinks to map virtual→physical paths; fingerprinting prevents redundant rescans
  • WebSocket: Broadcasts real-time progress for downloads, scans, and metadata sync
  • Model scanning flow: Walk folders → compute hashes → deduplicate → extract safetensors metadata → cache in SQLite → background CivitAI sync → WebSocket broadcast

Important Notes

  • ALWAYS use English for comments (per copilot-instructions.md)
  • .civitai.info files are NOT LoRA Manager sidecars. They are written by third-party apps; LoRA Manager treats them as read-only and only consumes them during migration/import. Never write, modify, or delete them, and never propose doing so as a fix — LoRA Manager's own metadata lives in the .metadata.json sidecar it owns.
  • Sidecar/preview path derivation must go through py/utils/sidecar_paths.py helpers (never inline splitext + ".metadata.json"): the centralized storage mode (sidecar_storage_mode / sidecar_storage_path settings) relocates .metadata.json files and preview images under a mirror tree, so any hand-built path is wrong in that mode. .civitai.info stays co-located with the model file in both modes. The new settings keys live only in DEFAULT_SETTINGS — settings.json.example stays minimal (see below).
  • settings.json.example must stay minimal: only use_portable_settings, civitai_api_key, and the four core folder_paths keys (loras, checkpoints, unet, embeddings). Do NOT add optional/default keys (model-category folders, default_*_root, auto_organize_exclusions, etc.) to this file unless the user explicitly asks for it. Defaults belong in DEFAULT_SETTINGS in py/services/settings_manager.py.
  • Run python scripts/sync_translation_keys.py after adding UI strings to locales/en.json
  • Symlinks require normalized paths. Business paths vs real paths: All stored paths and operation routing use the original paths as they appear under configured model roots — symlinks are NOT resolved. os.path.realpath is only for scanner dedup and the symlink cache. Any path passed to os.remove/os.rename/shutil.move or validated by a containment check MUST use the business path (i.e. os.path.abspath, not realpath).