diff --git a/backend/tests/integration/test_api_v5_lineage.py b/backend/tests/integration/test_api_v5_lineage.py index 98a9ef2..2bf1aa3 100644 --- a/backend/tests/integration/test_api_v5_lineage.py +++ b/backend/tests/integration/test_api_v5_lineage.py @@ -65,6 +65,16 @@ def _fork(client, space_id, checkpoint_id, branch_name="", provider="", model="" return r +def _add_note(client, checkpoint_id, text, author="founder", kind="note"): + r = client.post(f"/api/v5/checkpoint/{checkpoint_id}/notes", json={ + "text": text, + "author": author, + "kind": kind, + }) + assert r.status_code == 201, r.text + return r.json() + + # ── Fork tests ──────────────────────────────────────────────────────────────── def test_fork_creates_new_session(client): @@ -357,6 +367,29 @@ def test_lineage_checkpoint_includes_author_agent(client): ) +def test_lineage_checkpoint_includes_note_summary(client): + """note_count and note_kinds round-trip through the lineage endpoint.""" + repo_id = _create_repo(client, "Lineage Note Summary") + session_id = _create_session(client, repo_id) + + mixed = _commit(client, repo_id, session_id, message="Checkpoint with mixed notes") + plain = _commit(client, repo_id, session_id, message="Checkpoint without notes") + + _add_note(client, mixed["id"], "Milestone note", kind="milestone") + _add_note(client, mixed["id"], "Noise note", kind="noise") + _add_note(client, mixed["id"], "Plain note", kind="note") + + r = client.get(f"/api/v5/lineage/spaces/{repo_id}") + assert r.status_code == 200, r.text + checkpoints = r.json()["checkpoints"] + by_id = {c["id"]: c for c in checkpoints} + + assert by_id[mixed["id"]]["note_count"] == 3 + assert by_id[mixed["id"]]["note_kinds"] == ["milestone", "noise", "note"] + assert by_id[plain["id"]]["note_count"] == 0 + assert by_id[plain["id"]]["note_kinds"] == [] + + # ── Compare tests ───────────────────────────────────────────────────────────── def test_compare_same_checkpoint(client): diff --git a/frontend/src/pages/LineagePage.tsx b/frontend/src/pages/LineagePage.tsx index cb3c37f..1ab0159 100644 --- a/frontend/src/pages/LineagePage.tsx +++ b/frontend/src/pages/LineagePage.tsx @@ -38,6 +38,42 @@ function fmt(d: string) { }); } +function getCheckpointNoteBadge(checkpoint: CheckpointNode) { + const kinds = checkpoint.note_kinds ?? []; + const plural = checkpoint.note_count !== 1 ? 's' : ''; + const kindSummary = kinds.length > 0 ? kinds.join(' + ') : 'note'; + + if (kinds.length > 1) { + return { + label: 'mix', + title: `${checkpoint.note_count} note${plural} · ${kindSummary}`, + className: 'text-sky-300 bg-sky-950/30 border border-sky-500/30', + }; + } + + if (kinds.includes('milestone')) { + return { + label: '★', + title: `${checkpoint.note_count} note${plural} · milestone`, + className: 'text-amber-400 bg-amber-900/20 border border-amber-500/30', + }; + } + + if (kinds.includes('noise')) { + return { + label: '◌', + title: `${checkpoint.note_count} note${plural} · noise`, + className: 'text-gray-500 bg-gray-800/30 border border-gray-700', + }; + } + + return { + label: '●', + title: `${checkpoint.note_count} note${plural} · note`, + className: 'text-gray-400 bg-gray-800/30 border border-gray-700', + }; +} + // ── Compare Panel ───────────────────────────────────────────────────────────── function ComparePanel({ @@ -296,6 +332,7 @@ function CheckpointCard({ onFork: (c: CheckpointNode) => void; }) { const [expanded, setExpanded] = useState(false); + const noteBadge = checkpoint.note_count > 0 ? getCheckpointNoteBadge(checkpoint) : null; return (