diff --git a/py/routes/handlers/misc_handlers.py b/py/routes/handlers/misc_handlers.py
index d24473c3..4dc7d300 100644
--- a/py/routes/handlers/misc_handlers.py
+++ b/py/routes/handlers/misc_handlers.py
@@ -853,6 +853,9 @@ class MetadataArchiveHandler:
class FileSystemHandler:
+ def __init__(self, settings_service=None) -> None:
+ self._settings = settings_service or get_settings_manager()
+
async def open_file_location(self, request: web.Request) -> web.Response:
try:
data = await request.json()
@@ -877,6 +880,30 @@ class FileSystemHandler:
logger.error("Failed to open file location: %s", exc, exc_info=True)
return web.json_response({"success": False, "error": str(exc)}, status=500)
+ async def open_settings_location(self, request: web.Request) -> web.Response:
+ try:
+ settings_file = getattr(self._settings, "settings_file", None)
+ if not settings_file:
+ return web.json_response({"success": False, "error": "Settings file not found"}, status=404)
+
+ settings_file = os.path.abspath(settings_file)
+ if not os.path.isfile(settings_file):
+ return web.json_response({"success": False, "error": "Settings file does not exist"}, status=404)
+
+ if os.name == "nt":
+ subprocess.Popen(["explorer", "/select,", settings_file])
+ elif os.name == "posix":
+ if sys.platform == "darwin":
+ subprocess.Popen(["open", "-R", settings_file])
+ else:
+ folder = os.path.dirname(settings_file)
+ subprocess.Popen(["xdg-open", folder])
+
+ return web.json_response({"success": True, "message": f"Opened settings folder: {settings_file}"})
+ except Exception as exc: # pragma: no cover - defensive logging
+ logger.error("Failed to open settings location: %s", exc, exc_info=True)
+ return web.json_response({"success": False, "error": str(exc)}, status=500)
+
class NodeRegistryHandler:
def __init__(
@@ -1103,6 +1130,7 @@ class MiscHandlerSet:
"get_metadata_archive_status": self.metadata_archive.get_metadata_archive_status,
"get_model_versions_status": self.model_library.get_model_versions_status,
"open_file_location": self.filesystem.open_file_location,
+ "open_settings_location": self.filesystem.open_settings_location,
}
diff --git a/py/routes/misc_route_registrar.py b/py/routes/misc_route_registrar.py
index a68aa8eb..1a21f57b 100644
--- a/py/routes/misc_route_registrar.py
+++ b/py/routes/misc_route_registrar.py
@@ -41,6 +41,7 @@ MISC_ROUTE_DEFINITIONS: tuple[RouteDefinition, ...] = (
RouteDefinition("POST", "/api/lm/remove-metadata-archive", "remove_metadata_archive"),
RouteDefinition("GET", "/api/lm/metadata-archive-status", "get_metadata_archive_status"),
RouteDefinition("GET", "/api/lm/model-versions-status", "get_model_versions_status"),
+ RouteDefinition("POST", "/api/lm/settings/open-location", "open_settings_location"),
)
diff --git a/py/routes/misc_routes.py b/py/routes/misc_routes.py
index 685821b0..89e1e2b9 100644
--- a/py/routes/misc_routes.py
+++ b/py/routes/misc_routes.py
@@ -107,7 +107,7 @@ class MiscRoutes:
settings_service=self._settings,
metadata_provider_updater=self._metadata_provider_updater,
)
- filesystem = FileSystemHandler()
+ filesystem = FileSystemHandler(settings_service=self._settings)
node_registry_handler = NodeRegistryHandler(
node_registry=self._node_registry,
prompt_server=self._prompt_server,
diff --git a/py/services/settings_manager.py b/py/services/settings_manager.py
index 908013db..1ad53389 100644
--- a/py/services/settings_manager.py
+++ b/py/services/settings_manager.py
@@ -883,6 +883,7 @@ class SettingsManager:
if os.path.abspath(previous_path) != os.path.abspath(target_path):
self._copy_model_cache_directory(previous_dir, target_dir)
+ logger.info("Switching settings file to: %s", target_path)
self._pending_portable_switch = {"other_path": other_path}
self.settings_file = target_path
@@ -929,7 +930,12 @@ class SettingsManager:
and os.path.abspath(source_cache_dir) != os.path.abspath(target_cache_dir)
):
try:
- shutil.copytree(source_cache_dir, target_cache_dir, dirs_exist_ok=True)
+ shutil.copytree(
+ source_cache_dir,
+ target_cache_dir,
+ dirs_exist_ok=True,
+ ignore=shutil.ignore_patterns("*.sqlite-shm", "*.sqlite-wal"),
+ )
except Exception as exc:
logger.warning(
"Failed to copy model_cache directory from %s to %s: %s",
diff --git a/static/js/managers/SettingsManager.js b/static/js/managers/SettingsManager.js
index 2a7aa6b1..63d2b112 100644
--- a/static/js/managers/SettingsManager.js
+++ b/static/js/managers/SettingsManager.js
@@ -17,9 +17,8 @@ export class SettingsManager {
this.initializationPromise = null;
this.availableLibraries = {};
this.activeLibrary = '';
- this.settingsFilePath = null;
this.registeredStartupBannerIds = new Set();
-
+
// Add initialization to sync with modal state
this.currentPage = document.body.dataset.page || 'loras';
@@ -56,7 +55,6 @@ export class SettingsManager {
const data = await response.json();
if (data.success && data.settings) {
state.global.settings = this.mergeSettingsWithDefaults(data.settings);
- this.settingsFilePath = data.settings.settings_file || this.settingsFilePath;
this.registerStartupMessages(data.messages);
console.log('Settings synced from backend');
} else {
@@ -177,10 +175,6 @@ export class SettingsManager {
return;
}
- if (!this.settingsFilePath && typeof message.settings_file === 'string') {
- this.settingsFilePath = message.settings_file;
- }
-
const bannerId = `startup-${message.code || index}`;
if (this.registeredStartupBannerIds.has(bannerId)) {
return;
@@ -289,7 +283,7 @@ export class SettingsManager {
initialize() {
if (this.initialized) return;
-
+
// Add event listener to sync state when modal is closed via other means (like Escape key)
const settingsModal = document.getElementById('settingsModal');
if (settingsModal) {
@@ -297,7 +291,7 @@ export class SettingsManager {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
this.isOpen = settingsModal.style.display === 'block';
-
+
// When modal is opened, update checkbox state from current settings
if (this.isOpen) {
this.loadSettingsToUI();
@@ -305,10 +299,10 @@ export class SettingsManager {
}
});
});
-
+
observer.observe(settingsModal, { attributes: true });
}
-
+
// Add event listeners for all toggle-visibility buttons
document.querySelectorAll('.toggle-visibility').forEach(button => {
button.addEventListener('click', () => this.toggleInputVisibility(button));
@@ -316,12 +310,8 @@ export class SettingsManager {
const openSettingsLocationButton = document.querySelector('.settings-open-location-trigger');
if (openSettingsLocationButton) {
- if (openSettingsLocationButton.dataset.settingsPath) {
- this.settingsFilePath = openSettingsLocationButton.dataset.settingsPath;
- }
openSettingsLocationButton.addEventListener('click', () => {
- const filePath = openSettingsLocationButton.dataset.settingsPath;
- this.openSettingsFileLocation(filePath);
+ this.openSettingsFileLocation();
});
}
@@ -364,29 +354,16 @@ export class SettingsManager {
this.initialized = true;
}
- async openSettingsFileLocation(filePath) {
- const targetPath = filePath || this.settingsFilePath || document.querySelector('.settings-open-location-trigger')?.dataset.settingsPath;
-
- if (!targetPath) {
- showToast('settings.openSettingsFileLocation.failed', {}, 'error');
- return;
- }
-
+ async openSettingsFileLocation() {
try {
- const response = await fetch('/api/lm/open-file-location', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({ file_path: targetPath }),
+ const response = await fetch('/api/lm/settings/open-location', {
+ method: 'POST'
});
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`);
}
- this.settingsFilePath = targetPath;
-
showToast('settings.openSettingsFileLocation.success', {}, 'success');
} catch (error) {
console.error('Failed to open settings file location:', error);
@@ -497,7 +474,7 @@ export class SettingsManager {
// Load default lora root
await this.loadLoraRoots();
-
+
// Load default checkpoint root
await this.loadCheckpointRoots();
@@ -658,7 +635,7 @@ export class SettingsManager {
const proxyEnabledCheckbox = document.getElementById('proxyEnabled');
if (proxyEnabledCheckbox) {
proxyEnabledCheckbox.checked = state.global.settings.proxy_enabled || false;
-
+
// Add event listener for toggling proxy settings group visibility
proxyEnabledCheckbox.addEventListener('change', () => {
const proxySettingsGroup = document.getElementById('proxySettingsGroup');
@@ -666,7 +643,7 @@ export class SettingsManager {
proxySettingsGroup.style.display = proxyEnabledCheckbox.checked ? 'block' : 'none';
}
});
-
+
// Set initial visibility
const proxySettingsGroup = document.getElementById('proxySettingsGroup');
if (proxySettingsGroup) {
@@ -854,23 +831,23 @@ export class SettingsManager {
try {
const defaultLoraRootSelect = document.getElementById('defaultLoraRoot');
if (!defaultLoraRootSelect) return;
-
+
// Fetch lora roots
const response = await fetch('/api/lm/loras/roots');
if (!response.ok) {
throw new Error('Failed to fetch LoRA roots');
}
-
+
const data = await response.json();
if (!data.roots || data.roots.length === 0) {
throw new Error('No LoRA roots found');
}
-
+
// Clear existing options except the first one (No Default)
const noDefaultOption = defaultLoraRootSelect.querySelector('option[value=""]');
defaultLoraRootSelect.innerHTML = '';
defaultLoraRootSelect.appendChild(noDefaultOption);
-
+
// Add options for each root
data.roots.forEach(root => {
const option = document.createElement('option');
@@ -878,11 +855,11 @@ export class SettingsManager {
option.textContent = root;
defaultLoraRootSelect.appendChild(option);
});
-
+
// Set selected value from settings
const defaultRoot = state.global.settings.default_lora_root || '';
defaultLoraRootSelect.value = defaultRoot;
-
+
} catch (error) {
console.error('Error loading LoRA roots:', error);
showToast('toast.settings.loraRootsFailed', { message: error.message }, 'error');
@@ -893,23 +870,23 @@ export class SettingsManager {
try {
const defaultCheckpointRootSelect = document.getElementById('defaultCheckpointRoot');
if (!defaultCheckpointRootSelect) return;
-
+
// Fetch checkpoint roots
const response = await fetch('/api/lm/checkpoints/roots');
if (!response.ok) {
throw new Error('Failed to fetch checkpoint roots');
}
-
+
const data = await response.json();
if (!data.roots || data.roots.length === 0) {
throw new Error('No checkpoint roots found');
}
-
+
// Clear existing options except the first one (No Default)
const noDefaultOption = defaultCheckpointRootSelect.querySelector('option[value=""]');
defaultCheckpointRootSelect.innerHTML = '';
defaultCheckpointRootSelect.appendChild(noDefaultOption);
-
+
// Add options for each root
data.roots.forEach(root => {
const option = document.createElement('option');
@@ -917,11 +894,11 @@ export class SettingsManager {
option.textContent = root;
defaultCheckpointRootSelect.appendChild(option);
});
-
+
// Set selected value from settings
const defaultRoot = state.global.settings.default_checkpoint_root || '';
defaultCheckpointRootSelect.value = defaultRoot;
-
+
} catch (error) {
console.error('Error loading checkpoint roots:', error);
showToast('toast.settings.checkpointRootsFailed', { message: error.message }, 'error');
@@ -972,7 +949,7 @@ export class SettingsManager {
if (!mappingsContainer) return;
const mappings = state.global.settings.base_model_path_mappings || {};
-
+
// Clear existing mappings
mappingsContainer.innerHTML = '';
@@ -993,7 +970,7 @@ export class SettingsManager {
const row = document.createElement('div');
row.className = 'mapping-row';
-
+
const availableModels = MAPPABLE_BASE_MODELS.filter(model => {
const existingMappings = state.global.settings.base_model_path_mappings || {};
return !existingMappings.hasOwnProperty(model) || model === baseModel;
@@ -1003,9 +980,9 @@ export class SettingsManager {