diff --git a/sidecar/attune_gui/routes/cowork_pages.py b/sidecar/attune_gui/routes/cowork_pages.py index 4501365..bd4265e 100644 --- a/sidecar/attune_gui/routes/cowork_pages.py +++ b/sidecar/attune_gui/routes/cowork_pages.py @@ -185,14 +185,45 @@ async def page_templates(request: Request, filter: str = "all") -> HTMLResponse: @router.get("/dashboard/specs", response_class=HTMLResponse, include_in_schema=False) async def page_specs(request: Request) -> HTMLResponse: - """Render the Specs page — feature specs grouped by phase + status.""" + """Render the Specs page — feature specs grouped by project.""" from attune_gui.routes import cowork_specs + from attune_gui.workspace import get_workspace + + data = await _safe_call(cowork_specs.list_specs()) or { + "specs": [], + "specs_root": None, + "specs_roots": [], + } + specs = data["specs"] + + # Project order = first-seen order in specs (preserves backend root priority). + projects_in_order: list[str] = [] + for s in specs: + p = s.get("project") or "(unknown)" + if p not in projects_in_order: + projects_in_order.append(p) + + ws = get_workspace() + ws_name = ws.name if ws is not None else None + if ws_name and ws_name in projects_in_order: + active_project = ws_name + elif projects_in_order: + active_project = projects_in_order[0] + else: + active_project = None + + roots_count = len(data.get("specs_roots") or []) or (1 if data.get("specs_root") else 0) - data = await _safe_call(cowork_specs.list_specs()) or {"specs": [], "specs_root": None} return templates.TemplateResponse( request, "specs.html", - _ctx(request, "specs", specs=data["specs"], specs_root=data["specs_root"]), + _ctx( + request, + "specs", + specs=specs, + active_project=active_project, + roots_count=roots_count, + ), ) diff --git a/sidecar/attune_gui/static_cw/style.css b/sidecar/attune_gui/static_cw/style.css index 57565dd..e2efd37 100644 --- a/sidecar/attune_gui/static_cw/style.css +++ b/sidecar/attune_gui/static_cw/style.css @@ -154,6 +154,9 @@ code, .mono { margin: -8px 0 16px; } +.spec-project-group { margin: 0 0 18px; } +.spec-project-group > summary { cursor: pointer; padding: 0.5rem 0; } + .empty { color: var(--ink-mute); padding: 32px 0; diff --git a/sidecar/attune_gui/templates/specs.html b/sidecar/attune_gui/templates/specs.html index 07747a2..d4f7237 100644 --- a/sidecar/attune_gui/templates/specs.html +++ b/sidecar/attune_gui/templates/specs.html @@ -7,9 +7,9 @@ {% endblock %} {% block content %} - {% if specs_root %} + {% if roots_count and roots_count > 0 %}
{% endif %} @@ -29,62 +29,67 @@ {% if specs %} -| Feature | -Phase | -Status | -Files | -- |
|---|---|---|---|---|
| - {% if s.phase %} - {{ s.feature }} - {% else %} - {{ s.feature }} - {% endif %} - | -- {% if s.phase_label %}{{ s.phase_label }} - {% else %}—{% endif %} - | -- {% if s.status %} - {% set st = s.status|lower %} - {% if st in ('approved','complete','completed','done') %} - {{ s.status }} - {% elif st in ('in-review','in_review','review') %} - {{ s.status }} - {% elif st == 'draft' %} - {{ s.status }} - {% else %} - {{ s.status }} - {% endif %} - {% else %}—{% endif %} - | -- {% for f in s.files %} - {{ f }} - {% endfor %} - | -- {% if s.phase == 'requirements.md' %} - - {% elif s.phase == 'design.md' %} - - {% endif %} - | -
| Feature | +Phase | +Status | +Files | ++ |
|---|---|---|---|---|
| + {% if s.phase %} + {{ s.feature }} + {% else %} + {{ s.feature }} + {% endif %} + | ++ {% if s.phase_label %}{{ s.phase_label }} + {% else %}—{% endif %} + | ++ {% if s.status %} + {% set st = s.status|lower %} + {% if st in ('approved','complete','completed','done') %} + {{ s.status }} + {% elif st in ('in-review','in_review','review') %} + {{ s.status }} + {% elif st == 'draft' %} + {{ s.status }} + {% else %} + {{ s.status }} + {% endif %} + {% else %}—{% endif %} + | ++ {% for f in s.files %} + {{ f }} + {% endfor %} + | ++ {% if s.phase == 'requirements.md' %} + + {% elif s.phase == 'design.md' %} + + {% endif %} + | +
No specs found. Click + New spec to start one.
{% endif %} diff --git a/sidecar/tests/test_cowork_pages.py b/sidecar/tests/test_cowork_pages.py index 6c0caf6..704a2f8 100644 --- a/sidecar/tests/test_cowork_pages.py +++ b/sidecar/tests/test_cowork_pages.py @@ -114,6 +114,31 @@ def test_specs_page_lists_seeded_features( assert "approved" in r.text +def test_specs_page_groups_by_project( + client: TestClient, monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """Federated specs from two projects render as two