diff --git a/sidecar/attune_gui/home_summary.py b/sidecar/attune_gui/home_summary.py index cedd93c..2eed64f 100644 --- a/sidecar/attune_gui/home_summary.py +++ b/sidecar/attune_gui/home_summary.py @@ -84,6 +84,8 @@ class HomeSummary: sparkline_points: str = "" # SVG polyline points string ("" = no data) recent_jobs: list[RecentJob] = field(default_factory=list) family: list[FamilyVersion] = field(default_factory=list) + family_interpreter: str | None = None # sys.executable of dashboard process + family_python_version: str | None = None # e.g. "3.11.7" workspace_path: str | None = None manifest_path: str | None = None feature_count: int = 0 @@ -236,9 +238,13 @@ async def build_home_summary() -> HomeSummary: logger.debug("home: cowork_templates.list_templates failed; using empty list") layers: dict[str, dict[str, Any]] = {} + interpreter: str | None = None + python_version: str | None = None try: layer_data = await cowork_health.layer_health() layers = layer_data.get("layers", {}) + interpreter = layer_data.get("interpreter") + python_version = layer_data.get("python_version") except Exception: # noqa: BLE001 logger.debug("home: cowork_health.layer_health failed; using empty list") @@ -277,6 +283,8 @@ async def build_home_summary() -> HomeSummary: sparkline_points=points, recent_jobs=recent, family=family, + family_interpreter=interpreter, + family_python_version=python_version, workspace_path=workspace_path, manifest_path=manifest_path, feature_count=feature_count, diff --git a/sidecar/attune_gui/routes/cowork_health.py b/sidecar/attune_gui/routes/cowork_health.py index 42161db..6ddbb7c 100644 --- a/sidecar/attune_gui/routes/cowork_health.py +++ b/sidecar/attune_gui/routes/cowork_health.py @@ -10,6 +10,7 @@ from __future__ import annotations import importlib.metadata as ilm +import sys from typing import Any from fastapi import APIRouter @@ -35,8 +36,19 @@ def _probe(pkg: str) -> dict[str, Any]: @router.get("/layers") async def layer_health() -> dict[str, Any]: - """Return version + importability for each attune layer.""" - return {"layers": {key: _probe(pkg) for key, pkg in _PACKAGES}} + """Return version + importability for each attune layer. + + Also surfaces the interpreter probing for metadata — a "not installed" + result is usually an env-mismatch (dashboard running under a different + Python than the venv that has the package), so the interpreter path + makes the situation self-diagnosing. + """ + vi = sys.version_info + return { + "layers": {key: _probe(pkg) for key, pkg in _PACKAGES}, + "interpreter": sys.executable, + "python_version": f"{vi.major}.{vi.minor}.{vi.micro}", + } @router.get("/corpus") diff --git a/sidecar/attune_gui/templates/home.html b/sidecar/attune_gui/templates/home.html index 57b2677..e7009ce 100644 --- a/sidecar/attune_gui/templates/home.html +++ b/sidecar/attune_gui/templates/home.html @@ -123,6 +123,13 @@

Workspace

Family snapshot

+ {% if summary.family_interpreter %} +

+ Probed under + {{ summary.family_interpreter }} + {% if summary.family_python_version %}(Python {{ summary.family_python_version }}){% endif %} +

+ {% endif %}
{% for v in summary.family %}
diff --git a/sidecar/tests/test_cowork_health.py b/sidecar/tests/test_cowork_health.py index 2ccc4d3..5bd03b9 100644 --- a/sidecar/tests/test_cowork_health.py +++ b/sidecar/tests/test_cowork_health.py @@ -26,6 +26,18 @@ def test_layers_returns_all_known_packages(client: TestClient) -> None: assert "version" in info +def test_layers_includes_interpreter_diagnostic(client: TestClient) -> None: + """A 'not installed' result is usually env-mismatch; the response surfaces + the interpreter so the dashboard is self-diagnosing.""" + r = client.get("/api/cowork/layers", headers={"Origin": "http://localhost:5173"}) + assert r.status_code == 200 + body = r.json() + assert isinstance(body["interpreter"], str) and body["interpreter"] + assert isinstance(body["python_version"], str) + # "3.11.7" style + assert body["python_version"].count(".") == 2 + + def test_layers_handles_missing_package( client: TestClient, monkeypatch: pytest.MonkeyPatch ) -> None: diff --git a/sidecar/tests/test_home_summary.py b/sidecar/tests/test_home_summary.py index 2f6cc3f..9d55aba 100644 --- a/sidecar/tests/test_home_summary.py +++ b/sidecar/tests/test_home_summary.py @@ -209,7 +209,13 @@ async def test_build_home_summary_composes_all_sources(): "templates_root": "/fake/help", } ) - fake_layers = AsyncMock(return_value={"layers": {"ai": {"importable": True, "version": "9"}}}) + fake_layers = AsyncMock( + return_value={ + "layers": {"ai": {"importable": True, "version": "9"}}, + "interpreter": "/fake/.venv/bin/python", + "python_version": "3.11.7", + } + ) fake_corpus = AsyncMock( return_value={"manifest_path": "/fake/help/features.yaml", "feature_count": 4} ) @@ -241,6 +247,8 @@ async def test_build_home_summary_composes_all_sources(): assert summary.manifest_path == "/fake/help/features.yaml" assert len(summary.family) == 1 assert summary.family[0].package == "attune-ai" + assert summary.family_interpreter == "/fake/.venv/bin/python" + assert summary.family_python_version == "3.11.7" assert len(summary.recent_jobs) == 1 assert summary.recent_jobs[0].name == "demo.run" assert len(summary.sparkline) == 7