Files
ComfyUI-Lora-Manager/AGENTS.md
T
Will Miao a750ca88ac docs(agents): pin the locale file layout and the stale-translation rule
Two things agents kept getting wrong while editing UI copy, both learned the
expensive way:

* `locales/*.json` come in two layouts: `en.json` (the reference, hand-maintained)
  is 2-space indented, the nine translated locales are 4-space (what
  `sync_translation_keys.py` writes). Loading one with `json.load` and dumping it
  back with the other indent reformats the whole file: changing two strings in the
  translated locales produced a 26k-line diff that had to be reverted and redone in
  place. The section now names both layouts, says to edit values in place, and adds
  the cheap guard that catches the whole class of mistake: after writing to an
  existing structured file, `git diff --stat` must match the change you meant to
  make.
* Changing an existing English string makes every other locale's value stale.
  Those values have to go back to `[TODO: Translate] <new English>` rather than
  keep describing the old behaviour until someone notices.
2026-10-12 10:55:49 +08:00

14 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).

locales/*.json keep a hand-maintained layout and the two groups differ: en.json (the reference) uses 2-space indent, the nine translated locales use 4-space indent (what sync_translation_keys.py writes). Both keep ensure_ascii=False and exactly one trailing newline. Edit values in place (line/anchored text edit) and add keys with the sync script. Never json.load + json.dumps a locale file just to change a string: the serializer rewrites the whole layout, so a two-value edit lands as a several-thousand-line diff that has to be reverted and redone (tests/i18n/test_i18n.py::test_locale_files_keep_their_maintained_layout now fails on that). After writing to any existing structured file, check git diff --stat: the changed-line count must match the change you meant to make.

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).

Changing an existing English string is a translation event too: the other locales' value for that key is now stale, so put it back in the queue ([TODO: Translate] <new English>) instead of leaving a translation that describes the old behaviour.

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).