diff --git a/chat/templates/_drawer.html b/chat/templates/_drawer.html
index 621d4af..6c1b2c1 100644
--- a/chat/templates/_drawer.html
+++ b/chat/templates/_drawer.html
@@ -456,6 +456,52 @@
+
+ Significance review
+ {% set total_mem = significance_distribution.values()|sum %}
+ {% if total_mem %}
+
+ {% for level in [0, 1, 2, 3] %}
+ {% set count = significance_distribution[level] %}
+ {% set marker = ['·','•','★','★★'][level] %}
+ {% set pct = (100 * count / total_mem)|round(0, 'floor')|int if total_mem else 0 %}
+ -
+ {{ marker }} ({{ level }})
+
+ {{ count }}
+
+ {% endfor %}
+
+ {% else %}
+ No memories yet.
+ {% endif %}
+ {% if recent_memories %}
+
+ Edit significance (recent memories)
+
+ {% for m in recent_memories %}
+ -
+ {{ ['·','•','★','★★'][m.significance|default(0)] }}
+ {{ m.pov_summary[:80] }}{% if m.pov_summary|length > 80 %}…{% endif %}
+
+
+ {% endfor %}
+
+
+ {% endif %}
+
+
Pinned memories ({{ pinned|length }} / {{ pin_cap }})
{% if pinned %}
diff --git a/chat/web/drawer.py b/chat/web/drawer.py
index 93c017d..251e2ab 100644
--- a/chat/web/drawer.py
+++ b/chat/web/drawer.py
@@ -181,6 +181,21 @@ async def drawer(chat_id: str, request: Request, conn=Depends(get_conn)):
# returns both flavours and the template highlights the active one).
branches = list_branches_with_metadata(conn, chat_id)
+ # T98.2: significance distribution across this chat's memories. Powers
+ # the "Significance review" panel — a small histogram letting authors
+ # spot lopsided buckets (e.g. nothing significant=3 yet) and triage by
+ # editing individual memory significance values.
+ sig_rows = conn.execute(
+ "SELECT significance, COUNT(*) FROM memories "
+ "WHERE chat_id = ? GROUP BY significance ORDER BY significance",
+ (chat_id,),
+ ).fetchall()
+ significance_distribution = {int(r[0]): int(r[1]) for r in sig_rows}
+ # Ensure every bucket 0..3 is present so the bar-chart template can
+ # render a stable axis even when a level has zero rows.
+ for level in (0, 1, 2, 3):
+ significance_distribution.setdefault(level, 0)
+
return TEMPLATES.TemplateResponse(
request,
"_drawer.html",
@@ -209,6 +224,7 @@ async def drawer(chat_id: str, request: Request, conn=Depends(get_conn)):
"active_events": active_events,
"open_threads": open_threads,
"branches": branches,
+ "significance_distribution": significance_distribution,
},
)
diff --git a/tests/test_drawer_phase4.py b/tests/test_drawer_phase4.py
index 3e0f875..e20f01d 100644
--- a/tests/test_drawer_phase4.py
+++ b/tests/test_drawer_phase4.py
@@ -187,3 +187,96 @@ def test_t98_1_branch_from_turn_emits_branch_created(client, tmp_path):
)
assert dup.status_code == 400
assert seed_id < turn_id # sanity: turn is after chat_created
+
+
+# ---------------------------------------------------------------------------
+# T98.2 — significance review panel.
+# ---------------------------------------------------------------------------
+
+
+def _seed_memories_for_significance(db: Path) -> list[int]:
+ """Seed three memories with significance levels 0, 1, 2. Returns ids.
+
+ Uses ``append_and_apply`` (vs ``append_event`` + a final ``project``)
+ so each row is applied exactly once — the chat row was already
+ materialised by ``_seed_chat`` and a re-projection would conflict
+ on ``chats.id`` UNIQUE.
+ """
+ ids: list[int] = []
+ with open_db(db) as conn:
+ for sig in (0, 1, 2):
+ append_and_apply(
+ conn,
+ kind="memory_written",
+ payload={
+ "owner_id": "bot_a",
+ "chat_id": "chat_bot_a",
+ "pov_summary": f"memory at significance {sig}",
+ "witness_you": 1,
+ "witness_host": 1,
+ "witness_guest": 0,
+ "significance": sig,
+ },
+ )
+ rows = conn.execute(
+ "SELECT id FROM memories WHERE chat_id = 'chat_bot_a' "
+ "ORDER BY id ASC"
+ ).fetchall()
+ ids = [int(r[0]) for r in rows]
+ return ids
+
+
+def test_t98_2_distribution_renders_per_significance_bucket(client, tmp_path):
+ db = tmp_path / "test.db"
+ _seed_chat(db)
+ _seed_memories_for_significance(db)
+
+ response = client.get("/chats/chat_bot_a/drawer")
+ assert response.status_code == 200
+ body = response.text
+
+ # Section heading + bar entries for each significance level.
+ assert "Significance review
" in body
+ # All four buckets appear by their canonical label even when count=0.
+ assert ">★★ (3)<" in body or "(3)" in body
+ # The distribution markup names each level explicitly.
+ for level in (0, 1, 2, 3):
+ assert f"sig-bar sig-{level}" in body
+ # Three seeded memories (sigs 0, 1, 2) — each has a count = 1 bar.
+ # We don't pin exact text formatting, just verify the per-level bars
+ # are present.
+
+
+def test_t98_2_edit_significance_via_existing_route_lands_manual_edit(
+ client, tmp_path
+):
+ db = tmp_path / "test.db"
+ _seed_chat(db)
+ ids = _seed_memories_for_significance(db)
+
+ target_id = ids[0] # initially significance=0
+ response = client.post(
+ f"/chats/chat_bot_a/drawer/memory/{target_id}/significance",
+ data={"significance": "3"},
+ )
+ assert response.status_code == 200
+
+ with open_db(db) as conn:
+ # Significance updated in the projected table.
+ row = conn.execute(
+ "SELECT significance FROM memories WHERE id = ?", (target_id,)
+ ).fetchone()
+ assert int(row[0]) == 3
+
+ # manual_edit landed in the event log with the prior snapshot.
+ import json as _json
+
+ log_rows = conn.execute(
+ "SELECT payload_json FROM event_log "
+ "WHERE kind = 'manual_edit' ORDER BY id DESC LIMIT 1"
+ ).fetchone()
+ payload = _json.loads(log_rows[0])
+ assert payload["target_kind"] == "memory_significance"
+ assert int(payload["target_id"]) == target_id
+ assert payload["prior_value"] == 0
+ assert payload["new_value"] == 3