feat: replace project info with htop-inspired dashboard

Replace Layout-based display (expanded to terminal width) with a compact
Panel using Table.grid(expand=False). Add horizontal bar charts for note
types (top 5), embedding coverage bar with Unicode blocks, and colored
status dots. Removes verbose sections (most connected, recent activity,
available projects) in favor of a dense, visually engaging dashboard. 📊

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
This commit is contained in:
phernandez
2026-02-25 11:11:58 -06:00
parent b6369d3d14
commit 3004d0d1fe
+106 -80
View File
@@ -6,9 +6,10 @@ from datetime import datetime
from pathlib import Path from pathlib import Path
import typer import typer
from rich.console import Console from rich.console import Console, Group
from rich.panel import Panel from rich.panel import Panel
from rich.table import Table from rich.table import Table
from rich.text import Text
from basic_memory.cli.app import app from basic_memory.cli.app import app
from basic_memory.cli.auth import CLIAuth from basic_memory.cli.auth import CLIAuth
@@ -44,6 +45,17 @@ def format_path(path: str) -> str:
return path return path
def make_bar(value: int, max_value: int, width: int = 40) -> Text:
"""Create a horizontal bar chart element using Unicode blocks."""
if max_value == 0:
return Text("" * width, style="dim")
filled = max(1, round(value / max_value * width)) if value > 0 else 0
bar = Text()
bar.append("" * filled, style="cyan")
bar.append("" * (width - filled), style="dim")
return bar
@project_app.command("list") @project_app.command("list")
def list_projects( def list_projects(
local: bool = typer.Option(False, "--local", help="Force local routing for this command"), local: bool = typer.Option(False, "--local", help="Force local routing for this command"),
@@ -758,99 +770,113 @@ def display_project_info(
# Convert to JSON and print # Convert to JSON and print
print(json.dumps(info.model_dump(), indent=2, default=str)) print(json.dumps(info.model_dump(), indent=2, default=str))
else: else:
# Project configuration section # --- Left column: Knowledge Graph stats ---
console.print( left = Table.grid(padding=(0, 2))
Panel( left.add_column("metric", style="cyan")
f"Basic Memory version: [bold green]{info.system.version}[/bold green]\n" left.add_column("value", style="green", justify="right")
f"[bold]Project:[/bold] {info.project_name}\n"
f"[bold]Path:[/bold] {info.project_path}\n"
f"[bold]Default Project:[/bold] {info.default_project}\n",
title="Basic Memory Project Info",
expand=False,
)
)
# Statistics section left.add_row("[bold]Knowledge Graph[/bold]", "")
stats_table = Table(title="Statistics") left.add_row("Entities", str(info.statistics.total_entities))
stats_table.add_column("Metric", style="cyan") left.add_row("Observations", str(info.statistics.total_observations))
stats_table.add_column("Count", style="green") left.add_row("Relations", str(info.statistics.total_relations))
left.add_row("Unresolved", str(info.statistics.total_unresolved_relations))
left.add_row("Isolated", str(info.statistics.isolated_entities))
stats_table.add_row("Entities", str(info.statistics.total_entities)) # --- Right column: Embeddings ---
stats_table.add_row("Observations", str(info.statistics.total_observations)) right = Table.grid(padding=(0, 2))
stats_table.add_row("Relations", str(info.statistics.total_relations)) right.add_column("property", style="cyan")
stats_table.add_row( right.add_column("value", style="green")
"Unresolved Relations", str(info.statistics.total_unresolved_relations)
)
stats_table.add_row("Isolated Entities", str(info.statistics.isolated_entities))
console.print(stats_table) right.add_row("[bold]Embeddings[/bold]", "")
if info.embedding_status:
es = info.embedding_status
if not es.semantic_search_enabled:
right.add_row("[green]●[/green] Semantic Search", "Disabled")
else:
right.add_row("[green]●[/green] Semantic Search", "Enabled")
if es.embedding_provider:
right.add_row(" Provider", es.embedding_provider)
if es.embedding_model:
right.add_row(" Model", es.embedding_model)
# Embedding coverage bar
if es.total_indexed_entities > 0:
coverage_bar = make_bar(
es.total_entities_with_chunks,
es.total_indexed_entities,
width=20,
)
count_text = Text(
f" {es.total_entities_with_chunks}/{es.total_indexed_entities}",
style="green",
)
bar_with_count = Text.assemble(" Indexed ", coverage_bar, count_text)
right.add_row(bar_with_count, "")
right.add_row(" Chunks", str(es.total_chunks))
if es.reindex_recommended:
right.add_row(
"[yellow]●[/yellow] Status",
"[yellow]Reindex recommended[/yellow]",
)
if es.reindex_reason:
right.add_row(" Reason", f"[yellow]{es.reindex_reason}[/yellow]")
else:
right.add_row("[green]●[/green] Status", "[green]Up to date[/green]")
# Note types # --- Compose two-column layout (content-sized, NOT Layout) ---
columns = Table.grid(padding=(0, 4), expand=False)
columns.add_row(left, right)
# --- Note Types bar chart (top 5 by count) ---
bars_section = None
if info.statistics.note_types: if info.statistics.note_types:
note_types_table = Table(title="Note Types") sorted_types = sorted(
note_types_table.add_column("Type", style="blue") info.statistics.note_types.items(), key=lambda x: x[1], reverse=True
note_types_table.add_column("Count", style="green") )
top_types = sorted_types[:5]
max_count = top_types[0][1] if top_types else 1
for note_type, count in info.statistics.note_types.items(): bars = Table.grid(padding=(0, 2), expand=False)
note_types_table.add_row(note_type, str(count)) bars.add_column("type", style="cyan", width=16, justify="right")
bars.add_column("bar")
bars.add_column("count", style="green", justify="right")
console.print(note_types_table) for note_type, count in top_types:
bars.add_row(note_type, make_bar(count, max_count), str(count))
# Most connected entities remaining = len(sorted_types) - len(top_types)
if info.statistics.most_connected_entities: # pragma: no cover bars_section = Group(
connected_table = Table(title="Most Connected Entities") "[bold]Note Types[/bold]",
connected_table.add_column("Title", style="blue") bars,
connected_table.add_column("Permalink", style="cyan") f"[dim]+{remaining} more types[/dim]" if remaining > 0 else "",
connected_table.add_column("Relations", style="green") )
for entity in info.statistics.most_connected_entities: # --- Footer ---
connected_table.add_row(
entity["title"], entity["permalink"], str(entity["relation_count"])
)
console.print(connected_table)
# Recent activity
if info.activity.recently_updated: # pragma: no cover
recent_table = Table(title="Recent Activity")
recent_table.add_column("Title", style="blue")
recent_table.add_column("Type", style="cyan")
recent_table.add_column("Last Updated", style="green")
for entity in info.activity.recently_updated[:5]: # Show top 5
updated_at = (
datetime.fromisoformat(entity["updated_at"])
if isinstance(entity["updated_at"], str)
else entity["updated_at"]
)
recent_table.add_row(
entity["title"],
entity["note_type"],
updated_at.strftime("%Y-%m-%d %H:%M"),
)
console.print(recent_table)
# Available projects
projects_table = Table(title="Available Projects")
projects_table.add_column("Name", style="blue")
projects_table.add_column("Path", style="cyan")
projects_table.add_column("Default", style="green")
for name, proj_info in info.available_projects.items():
is_default = name == info.default_project
project_path = proj_info["path"]
projects_table.add_row(name, project_path, "[X]" if is_default else "")
console.print(projects_table)
# Timestamp
current_time = ( current_time = (
datetime.fromisoformat(str(info.system.timestamp)) datetime.fromisoformat(str(info.system.timestamp))
if isinstance(info.system.timestamp, str) if isinstance(info.system.timestamp, str)
else info.system.timestamp else info.system.timestamp
) )
console.print(f"\nTimestamp: [cyan]{current_time.strftime('%Y-%m-%d %H:%M:%S')}[/cyan]") footer = (
f"[dim]{format_path(info.project_path)} "
f"default: {info.default_project} "
f"{current_time.strftime('%Y-%m-%d %H:%M')}[/dim]"
)
# --- Assemble dashboard ---
parts: list = [columns, ""]
if bars_section:
parts.extend([bars_section, ""])
parts.append(footer)
body = Group(*parts)
console.print(
Panel(
body,
title=f"[bold]{info.project_name}[/bold]",
subtitle=f"Basic Memory {info.system.version}",
expand=False,
)
)
except typer.Exit: except typer.Exit:
raise raise