mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
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:
@@ -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,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
|
||||
|
||||
Reference in New Issue
Block a user