Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions sidecar/attune_gui/home_summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")

Expand Down Expand Up @@ -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,
Expand Down
16 changes: 14 additions & 2 deletions sidecar/attune_gui/routes/cowork_health.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from __future__ import annotations

import importlib.metadata as ilm
import sys
from typing import Any

from fastapi import APIRouter
Expand All @@ -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")
Expand Down
7 changes: 7 additions & 0 deletions sidecar/attune_gui/templates/home.html
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,13 @@ <h2 class="section-title">Workspace</h2>

<section class="section">
<h2 class="section-title">Family snapshot</h2>
{% if summary.family_interpreter %}
<p class="meta-line">
Probed under
<code title="Dashboard process interpreter. A 'Not installed' result usually means this Python differs from the venv where the package lives.">{{ summary.family_interpreter }}</code>
{% if summary.family_python_version %}(Python {{ summary.family_python_version }}){% endif %}
</p>
{% endif %}
<div class="grid grid-4">
{% for v in summary.family %}
<article class="card layer-card {% if v.importable %}ok{% else %}danger{% endif %}">
Expand Down
12 changes: 12 additions & 0 deletions sidecar/tests/test_cowork_health.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
10 changes: 9 additions & 1 deletion sidecar/tests/test_home_summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}
)
Expand Down Expand Up @@ -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
Expand Down
Loading