feat(sidecars): surface storage location and cover excluded models in migration

After migrating to centralized sidecar storage users had no indication
where their files went, and portable-mode installs silently placed the
sidecar root inside the plugin folder where a reinstall or git clean
would delete it.

- Migration now also covers models excluded from the library view and
  returns the resolved sidecar root in its result payload
- get_settings exposes the resolved sidecar root, whether it is the
  default, and whether it lives inside the installation folder
- New POST /api/lm/sidecars/open-location endpoint opens (or copies)
  the sidecar storage folder
- Settings UI always shows the effective storage path with an
  open-folder button, and warns when the root is inside the
  installation folder (portable-mode hazard)
- Migration confirmation shows the destination; on completion a result
  dialog summarizes moved/skipped/conflict counts with the storage
  location and an open-folder action
- Ignore /sidecars/ at the repository root so portable-mode sidecars
  are never committed

Refs #1045
This commit is contained in:
Will Miao
2026-09-26 23:08:29 +08:00
parent 62c144d80a
commit 7aee964448
25 changed files with 817 additions and 34 deletions
@@ -31,6 +31,9 @@
'language': 'en',
'llm_api_key_set': False,
'other_models_paths_available': False,
'sidecar_storage_root': '/sidecars',
'sidecar_storage_root_in_repo': False,
'sidecar_storage_root_is_default': True,
'standalone_mode': False,
'theme': 'dark',
}),
+13 -1
View File
@@ -117,8 +117,20 @@ class TestSettingsHandlerSnapshots:
"""Snapshot tests for SettingsHandler responses."""
@pytest.mark.asyncio
async def test_get_settings_response_format(self, snapshot: SnapshotAssertion):
async def test_get_settings_response_format(
self, snapshot: SnapshotAssertion, monkeypatch: pytest.MonkeyPatch
):
"""Verify get_settings response format matches snapshot."""
# Pin the resolved sidecar root: it derives from the machine-specific
# settings directory, which would make the snapshot non-deterministic.
monkeypatch.setattr(
"py.routes.handlers.misc_handlers.describe_sidecar_root",
lambda: {
"root": "/sidecars",
"is_default": True,
"inside_repo": False,
},
)
settings_service = DummySettings({
"civitai_api_key": "test-key",
"language": "en",
+52
View File
@@ -533,6 +533,58 @@ async def test_open_backup_location_uses_settings_directory(tmp_path, monkeypatc
assert calls == [["xdg-open", str(backup_dir)]]
@pytest.mark.asyncio
async def test_open_sidecar_location_opens_configured_root(tmp_path, monkeypatch):
from py.services.settings_manager import get_settings_manager
root = tmp_path / "sidecars"
get_settings_manager().set("sidecar_storage_path", str(root))
handler = FileSystemHandler(settings_service=SimpleNamespace())
calls = []
def fake_popen(args):
calls.append(args)
return MagicMock()
monkeypatch.setattr(subprocess, "Popen", fake_popen)
monkeypatch.setattr("py.routes.handlers.misc_handlers._is_docker", lambda: False)
monkeypatch.setattr("py.routes.handlers.misc_handlers._is_wsl", lambda: False)
response = await handler.open_sidecar_location(FakeRequest()) # pyright: ignore[reportArgumentType]
payload = _json_payload(response)
assert response.status == 200
assert payload["success"] is True
assert payload["path"] == str(root)
# Created on demand so the button works before any migration ran.
assert root.is_dir()
assert calls == [["xdg-open", str(root)]]
@pytest.mark.asyncio
async def test_get_settings_includes_resolved_sidecar_root(tmp_path):
from py.services.settings_manager import get_settings_manager
root = tmp_path / "sidecars-custom"
get_settings_manager().set("sidecar_storage_path", str(root))
handler = SettingsHandler(
settings_service=DummySettings(),
metadata_provider_updater=noop_async,
downloader_factory=dummy_downloader_factory,
)
response = await handler.get_settings(FakeRequest()) # pyright: ignore[reportArgumentType]
payload = _json_payload(response)
assert payload["success"] is True
assert payload["settings"]["sidecar_storage_root"] == str(root)
assert payload["settings"]["sidecar_storage_root_is_default"] is False
assert payload["settings"]["sidecar_storage_root_in_repo"] is False
@pytest.mark.asyncio
async def test_open_settings_location_headless_returns_clipboard_mode(tmp_path, monkeypatch):
"""Without a GUI session xdg-open cannot work; the handler must hand the