The example now only carries use_portable_settings, civitai_api_key and the four core folder_paths keys (loras/checkpoints/unet/embeddings). Optional keys such as the other-model folders, default_*_root and auto_organize_exclusions are removed; their defaults live in DEFAULT_SETTINGS and reach the user's settings.json on demand. AGENTS.md now forbids adding optional/default keys to the example unless the user explicitly asks for it.
12 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 annotationsfor forward references - Group imports: standard library, third-party, local (blank line separated)
- Use
TYPE_CHECKINGguard 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 inpy/services/errors.py async deffor I/O,@pytest.mark.asynciofor async tests- Singleton with
asyncio.Lock: seeModelScanner.get_instance() - Return
aiohttp.web.json_responseorweb.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.jsorkebab-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 invue-widgets/) - ComfyUI:
app.registerExtension(),node.addDOMWidget(name, type, element, options) - Event handlers via
addEventListeneror 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_pathsfor model discovery - Standalone mode:
standalone.pymocks ComfyUI dependencies, reads paths fromsettings.json - Detection:
os.environ.get("LORA_MANAGER_STANDALONE", "0") == "1"
Backend Entry Points
__init__.py— ComfyUI plugin entry: registers nodes viaNODE_CLASS_MAPPINGS, setsWEB_DIRECTORY, callsLoraManager.add_routes()standalone.py— Standalone server: mocksfolder_pathsand node modules, starts aiohttp serverpy/lora_manager.py— MainLoraManagerclass that registers all HTTP routes
Service Layer
ServiceRegistrysingleton for DI, services useget_instance()classmethodBaseModelServiceabstract base →LoraService,CheckpointService,EmbeddingServiceModelScannerbase →LoraScanner,CheckpointScanner,EmbeddingScannerfor file discovery with hash-based deduplicationPersistentModelCache(SQLite) for metadata persistenceMetadataSyncService— background sync from CivitAI/CivArchive APIsSettingsManager— settings with schema migration supportWebSocketManager— real-time progress broadcastingModelServiceFactory— 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_responseorweb.Response - Endpoints consumed by the companion browser extension (lm-civitai-extension)
MUST also accept
GETwith query-string params: the extension is GET-only by convention (see its AGENTS.md), even for state-changing operations such asGET /api/lm/recipe/{recipe_id}/reimport
Recipe System
- Base:
py/recipes/base.py, Enrichment:RecipeEnrichmentServiceinpy/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
NAMEclass attribute used as key inNODE_CLASS_MAPPINGS - Standard ComfyUI node pattern:
INPUT_TYPES()classmethod,RETURN_TYPES,FUNCTION
Configuration
py/config.pymanages folder paths for models and handles symlink mappings- Auto-saves paths to
settings.jsonin ComfyUI mode settings.json.exampleis intentionally minimal (see Important Notes); all other defaults live inDEFAULT_SETTINGS(py/services/settings_manager.py)
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.pyin 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 viapy/vue_widget_builder.py, typecheck viavue-tsc - Widget registration:
app.registerExtension()andgetCustomWidgetshooks;node.addDOMWidget(...)embeds HTML in LiteGraph nodes - See
docs/dom_widget_dev_guide.mdfor the DOMWidget development guide
Testing
Backend (pytest)
- Config in
pytest.ini:--import-mode=importlib, testpaths=tests - Fixtures in
tests/conftest.pymock ComfyUI dependencies; usetmp_path_factoryfor isolation - Markers:
@pytest.mark.asyncio,@pytest.mark.no_settings_dir_isolation(tests needing real settings paths)
Frontend (vitest)
- Vanilla JS tests:
tests/frontend/**/*.test.jswith jsdom; setup intests/frontend/setup.js - Vue widget tests:
vue-widgets/tests/**/*.test.tswith 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>/settingsand sandboxedfolder_pathsunder/tmp— the repo folder is the real plugin folder and asettings.jsonthere is read by the live instance. Never touch real config or real model libraries. - Never kill a process you did not start;
start_server.pytracks its own PIDs via pidfile and refuses to touch unrelated processes on the port. - Abort after ~30 minutes or 3 consecutive tool failures; report
BLOCKEDwith 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)
settings.json.examplemust stay minimal: onlyuse_portable_settings,civitai_api_key, and the four corefolder_pathskeys (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 inDEFAULT_SETTINGSinpy/services/settings_manager.py.- Run
python scripts/sync_translation_keys.pyafter adding UI strings tolocales/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.realpathis only for scanner dedup and the symlink cache. Any path passed toos.remove/os.rename/shutil.moveor validated by a containment check MUST use the business path (i.e.os.path.abspath, notrealpath).