feat(settings): editable model library paths for standalone mode

Standalone users previously had to hand-edit settings.json to configure
primary folder_paths. Add a standalone-only Model Paths section to the
settings modal:

- Backend exposes standalone_mode, folder_paths (with template placeholder
  values filtered out) and a data-driven folder_path_schema derived from
  OTHER_MODEL_FOLDER_SUBTYPES via GET /api/lm/settings
- The new section renders multi-path editors per model type from the
  schema, with inline enable_other_models / sub-type controls so other
  model types are configured without leaving the tab
- Persistent restart-required cues after a save: nav dot, inline notice
  and a global banner (unique id per change so dismissals don't mute
  future reminders)
- The missing-model-paths startup banner and the Other Models no-paths
  empty state now deep-link into the new section instead of pointing at
  settings.json
This commit is contained in:
Will Miao
2026-09-18 19:36:19 +08:00
parent d4b82d98b2
commit 5adfa3be36
27 changed files with 1566 additions and 66 deletions
+25
View File
@@ -54,6 +54,7 @@ from ...utils.constants import (
SUPPORTED_MEDIA_EXTENSIONS,
VALID_LORA_TYPES,
VALID_OTHER_CIVITAI_TYPES,
folder_path_schema,
)
from .model_source_handlers import ModelSourceHandler
from .agent_handlers import AgentHandler
@@ -1580,6 +1581,30 @@ class SettingsHandler:
availability_error,
)
response_data["other_models_paths_available"] = None
standalone_mode = os.environ.get("LORA_MANAGER_STANDALONE", "0") == "1"
response_data["standalone_mode"] = standalone_mode
if standalone_mode:
# Standalone reads its model roots exclusively from
# settings.json, so the Model Paths settings UI needs the
# current values plus the editable-key schema. In plugin mode
# the paths come from the ComfyUI host and stay hidden.
folder_paths = self._settings.get("folder_paths") or {}
# A fresh install is seeded from settings.json.example, whose
# folder_paths are documentation placeholders — hide them so
# the UI starts with empty editors instead of fake paths.
get_placeholders = getattr(
self._settings, "get_template_folder_path_placeholders", None
)
placeholders = get_placeholders() if get_placeholders else set()
if placeholders:
folder_paths = {
key: [p for p in paths if p not in placeholders]
if isinstance(paths, list)
else paths
for key, paths in folder_paths.items()
}
response_data["folder_paths"] = folder_paths
response_data["folder_path_schema"] = folder_path_schema()
settings_file = getattr(self._settings, "settings_file", None)
if settings_file:
response_data["settings_file"] = settings_file
+2 -2
View File
@@ -89,8 +89,8 @@ class OtherRoutes(BaseModelRoutes):
"standalone_mode": standalone_mode,
}
if standalone_mode:
# The settings UI cannot edit primary folder_paths, so the empty
# state must point at the actual file the user has to edit.
# The empty state points at the Model Paths settings section and
# shows the settings.json path as a fallback reference.
context["settings_file"] = getattr(self._settings, "settings_file", "") or ""
return context
+35 -3
View File
@@ -19,6 +19,7 @@ from typing import (
Mapping,
Optional,
Sequence,
Set,
Tuple,
)
@@ -308,6 +309,29 @@ class SettingsManager:
return payload == template
def get_template_folder_path_placeholders(self) -> Set[str]:
"""Placeholder folder_paths values shipped in settings.json.example.
A fresh standalone install is seeded from the template, so its
documentation-only placeholder paths end up in the live settings
file. The Model Paths settings UI hides them; the first real save
overwrites them via ``set("folder_paths")``.
"""
template = self._read_template_payload()
if not template:
return set()
folder_paths = template.get("folder_paths")
if not isinstance(folder_paths, Mapping):
return set()
placeholders: Set[str] = set()
for value in folder_paths.values():
paths = value if isinstance(value, list) else [value]
placeholders.update(p for p in paths if isinstance(p, str) and p)
return placeholders
def _merge_template_with_defaults(
self, defaults: Dict[str, Any], template: Mapping[str, Any]
) -> Dict[str, Any]:
@@ -1219,19 +1243,27 @@ class SettingsManager:
if self._bootstrap_reason == "missing":
message = (
"LoRA Manager created a default settings.json because no configuration was found. "
"Edit settings.json to add your model directories so library scanning can run."
"Open Settings → Model Paths to add your model directories so library scanning can run."
)
else:
message = (
"LoRA Manager could not locate any configured model directories. "
"Edit settings.json to add your model folders so library scanning can run."
"Open Settings → Model Paths to add your model folders so library scanning can run."
)
self._add_startup_message(
code="missing-model-paths",
title="Model folders need setup",
message=message,
severity="warning",
actions=self._default_settings_actions(),
actions=[
{
"action": "open-model-paths-settings",
"label": "Configure model folders",
"type": "primary",
"icon": "fas fa-cog",
},
*self._default_settings_actions(),
],
dismissible=False,
)
+23
View File
@@ -127,6 +127,29 @@ def other_sub_type_folder_keys() -> Dict[str, List[str]]:
# Precomputed inverse of OTHER_MODEL_FOLDER_SUBTYPES, keeping the table order.
OTHER_SUB_TYPE_FOLDER_KEYS: Dict[str, List[str]] = other_sub_type_folder_keys()
# Core folder_paths keys every LoRA Manager installation understands.
CORE_FOLDER_PATH_KEYS: List[str] = ["loras", "checkpoints", "unet", "embeddings"]
def folder_path_schema() -> List[Dict[str, Any]]:
"""Ordered schema describing the editable folder_paths keys.
Drives the standalone-only Model Paths settings UI: the frontend renders
one multi-path editor per entry and resolves labels via the
``settings.modelPaths.folderKeys.<key>`` i18n keys, so adding a new model
category is a constants + locale change only. ``sub_type`` lets the UI
hide editors for other-model categories the user has not enabled.
"""
schema: List[Dict[str, Any]] = [
{"key": key, "category": "core", "sub_type": None}
for key in CORE_FOLDER_PATH_KEYS
]
schema.extend(
{"key": folder_key, "category": "other", "sub_type": sub_type}
for folder_key, sub_type in OTHER_MODEL_FOLDER_SUBTYPES.items()
)
return schema
def normalize_other_sub_types(value: Any) -> List[str]:
"""Normalize a stored/requested enabled-sub_type list.