fix(cli): render frontmatter and observations in Rich TTY output

- `_display_read_note`: render frontmatter key/value panel when present so
  `--include-frontmatter` is not silently dropped in the Rich path
- `_display_build_context`: render ContextResult.observations under each
  primary node (category + truncated content) so interactive users see the
  same core facts as `--json` output; update subtitle to include count
- Update BUILD_CONTEXT_RESULT fixture with real ObservationSummary shape
  (type/category/content/permalink/file_path/created_at)
- Add test_read_note_rich_include_frontmatter asserting frontmatter keys appear
- Add test_build_context_rich_renders_observations asserting category/content visible

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Drew Cain <groksrc@gmail.com>
This commit is contained in:
Drew Cain
2026-06-11 09:37:51 -05:00
parent 457cd0ed3b
commit e079eca42b
2 changed files with 100 additions and 3 deletions
+34 -2
View File
@@ -113,10 +113,11 @@ def _display_search_results(result: dict[str, Any], query: str = "") -> None:
def _display_read_note(result: dict[str, Any]) -> None:
"""Render read-note result: header panel + rendered Markdown content."""
"""Render read-note result: header panel + optional frontmatter + rendered Markdown content."""
title = result.get("title", "")
permalink = result.get("permalink", "")
content = result.get("content", "")
frontmatter: dict[str, Any] = result.get("frontmatter") or {}
header = Text()
header.append(title, style="bold cyan")
@@ -125,6 +126,18 @@ def _display_read_note(result: dict[str, Any]) -> None:
console.print(Panel(header, expand=False))
# Trigger: --include-frontmatter was passed; the MCP tool populates "frontmatter".
# Why: without rendering it here, the explicitly requested metadata is silently
# dropped in the Rich path — users must also know to add --json to see it.
# Outcome: print a dim key/value block above the content when frontmatter is present.
if frontmatter:
fm_table = Table(show_header=False, box=None, padding=(0, 1), expand=False)
fm_table.add_column("key", style="dim")
fm_table.add_column("value", style="dim")
for key, value in frontmatter.items():
fm_table.add_row(str(key), str(value))
console.print(Panel(fm_table, title="[dim]frontmatter[/dim]", expand=False))
if content:
console.print(Markdown(content))
else:
@@ -165,6 +178,24 @@ def _display_build_context(result: dict[str, Any]) -> None:
primary_label = f"[dim]{p_type}[/dim] {primary_label}"
primary_node = tree.add(primary_label)
# --- Observations as children (category + truncated content) ---
# Trigger: ContextResult.observations exists in the JSON output but was
# never rendered in the Rich path.
# Why: users running interactively lost core entity facts (observations)
# that the --json path exposes; the TTY view must be at least as
# informative as the JSON view for the primary entity.
# Outcome: each observation appears as a dim "[category] content" leaf
# under its primary node, truncated at 120 chars.
observations: list[dict[str, Any]] = list(context_result.get("observations", []))
for obs in observations:
category = obs.get("category", "")
obs_content = obs.get("content", "")
# Truncate long observations so the tree stays readable.
if len(obs_content) > 120:
obs_content = obs_content[:117] + "..."
obs_label = f"[dim][{category}] {obs_content}[/dim]"
primary_node.add(obs_label)
# --- Related items as children ---
related: list[dict[str, Any]] = list(context_result.get("related_results", []))
for rel_item in related:
@@ -182,7 +213,8 @@ def _display_build_context(result: dict[str, Any]) -> None:
# Count total related items across all primary results.
total_related = sum(len(cr.get("related_results", [])) for cr in context_items)
subtitle = f"{len(context_items)} primary • {total_related} related"
total_observations = sum(len(cr.get("observations", [])) for cr in context_items)
subtitle = f"{len(context_items)} primary • {total_observations} observations • {total_related} related"
console.print(Panel(tree, subtitle=subtitle, expand=False))
+66 -1
View File
@@ -66,6 +66,7 @@ SEARCH_RESULT_EMPTY = {
BUILD_CONTEXT_RESULT = {
# Real GraphContext.model_dump() shape: results is a list of ContextResult dicts.
# Each ContextResult has primary_result + observations + related_results.
# ObservationSummary fields: type, category, content, permalink, file_path, created_at.
"results": [
{
"primary_result": {
@@ -76,7 +77,16 @@ BUILD_CONTEXT_RESULT = {
"file_path": "notes/Test Note.md",
"created_at": "2025-01-01T00:00:00",
},
"observations": [],
"observations": [
{
"type": "observation",
"category": "fact",
"content": "This is a key fact about the test note",
"permalink": "notes/test-note",
"file_path": "notes/Test Note.md",
"created_at": "2025-01-01T00:00:00",
}
],
"related_results": [
{
"type": "relation",
@@ -398,3 +408,58 @@ def test_recent_activity_non_tty_gives_json(mock_mcp):
data = json.loads(result.output)
assert isinstance(data, list)
assert len(data) == 2
# ---------------------------------------------------------------------------
# read-note frontmatter rendering (issue #678)
# ---------------------------------------------------------------------------
@patch(
"basic_memory.cli.commands.tool.mcp_read_note",
new_callable=AsyncMock,
return_value=READ_NOTE_RESULT,
)
def test_read_note_rich_include_frontmatter(mock_mcp):
"""read-note --include-frontmatter renders frontmatter keys in Rich path.
Regression: previously the Rich renderer silently dropped frontmatter even
when --include-frontmatter was passed, requiring --json to see the data.
"""
result = _tty_runner(["tool", "read-note", "test-note", "--include-frontmatter"])
assert result.exit_code == 0, f"CLI failed: {result.output}"
# Frontmatter section header should appear
assert "frontmatter" in result.output
# The frontmatter key and value from READ_NOTE_RESULT should be visible
assert "tags" in result.output
assert "test" in result.output
# The note content should still appear
assert "hello world" in result.output
# ---------------------------------------------------------------------------
# build-context observations rendering (issue #678)
# ---------------------------------------------------------------------------
@patch(
"basic_memory.cli.commands.tool.mcp_build_context",
new_callable=AsyncMock,
return_value=BUILD_CONTEXT_RESULT,
)
def test_build_context_rich_renders_observations(mock_mcp):
"""build-context Rich tree includes observations under each primary node.
Regression: ContextResult.observations was exposed in JSON output but never
rendered in the Rich path, so interactive users lost core entity facts.
"""
result = _tty_runner(["tool", "build-context", "memory://notes/test-note"])
assert result.exit_code == 0, f"CLI failed: {result.output}"
# The observation category should appear in the tree
assert "fact" in result.output
# The observation content should appear (possibly truncated)
assert "key fact" in result.output
# The subtitle should include an observations count
assert "observations" in result.output