Skip to content

Commit 3004d0d

Browse files
phernandezclaude
andcommitted
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>
1 parent b6369d3 commit 3004d0d

1 file changed

Lines changed: 113 additions & 87 deletions

File tree

src/basic_memory/cli/commands/project.py

Lines changed: 113 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66
from pathlib import Path
77

88
import typer
9-
from rich.console import Console
9+
from rich.console import Console, Group
1010
from rich.panel import Panel
1111
from rich.table import Table
12+
from rich.text import Text
1213

1314
from basic_memory.cli.app import app
1415
from basic_memory.cli.auth import CLIAuth
@@ -44,6 +45,17 @@ def format_path(path: str) -> str:
4445
return path
4546

4647

48+
def make_bar(value: int, max_value: int, width: int = 40) -> Text:
49+
"""Create a horizontal bar chart element using Unicode blocks."""
50+
if max_value == 0:
51+
return Text("░" * width, style="dim")
52+
filled = max(1, round(value / max_value * width)) if value > 0 else 0
53+
bar = Text()
54+
bar.append("█" * filled, style="cyan")
55+
bar.append("░" * (width - filled), style="dim")
56+
return bar
57+
58+
4759
@project_app.command("list")
4860
def list_projects(
4961
local: bool = typer.Option(False, "--local", help="Force local routing for this command"),
@@ -758,99 +770,113 @@ def display_project_info(
758770
# Convert to JSON and print
759771
print(json.dumps(info.model_dump(), indent=2, default=str))
760772
else:
761-
# Project configuration section
762-
console.print(
763-
Panel(
764-
f"Basic Memory version: [bold green]{info.system.version}[/bold green]\n"
765-
f"[bold]Project:[/bold] {info.project_name}\n"
766-
f"[bold]Path:[/bold] {info.project_path}\n"
767-
f"[bold]Default Project:[/bold] {info.default_project}\n",
768-
title="Basic Memory Project Info",
769-
expand=False,
773+
# --- Left column: Knowledge Graph stats ---
774+
left = Table.grid(padding=(0, 2))
775+
left.add_column("metric", style="cyan")
776+
left.add_column("value", style="green", justify="right")
777+
778+
left.add_row("[bold]Knowledge Graph[/bold]", "")
779+
left.add_row("Entities", str(info.statistics.total_entities))
780+
left.add_row("Observations", str(info.statistics.total_observations))
781+
left.add_row("Relations", str(info.statistics.total_relations))
782+
left.add_row("Unresolved", str(info.statistics.total_unresolved_relations))
783+
left.add_row("Isolated", str(info.statistics.isolated_entities))
784+
785+
# --- Right column: Embeddings ---
786+
right = Table.grid(padding=(0, 2))
787+
right.add_column("property", style="cyan")
788+
right.add_column("value", style="green")
789+
790+
right.add_row("[bold]Embeddings[/bold]", "")
791+
if info.embedding_status:
792+
es = info.embedding_status
793+
if not es.semantic_search_enabled:
794+
right.add_row("[green]●[/green] Semantic Search", "Disabled")
795+
else:
796+
right.add_row("[green]●[/green] Semantic Search", "Enabled")
797+
if es.embedding_provider:
798+
right.add_row(" Provider", es.embedding_provider)
799+
if es.embedding_model:
800+
right.add_row(" Model", es.embedding_model)
801+
# Embedding coverage bar
802+
if es.total_indexed_entities > 0:
803+
coverage_bar = make_bar(
804+
es.total_entities_with_chunks,
805+
es.total_indexed_entities,
806+
width=20,
807+
)
808+
count_text = Text(
809+
f" {es.total_entities_with_chunks}/{es.total_indexed_entities}",
810+
style="green",
811+
)
812+
bar_with_count = Text.assemble(" Indexed ", coverage_bar, count_text)
813+
right.add_row(bar_with_count, "")
814+
right.add_row(" Chunks", str(es.total_chunks))
815+
if es.reindex_recommended:
816+
right.add_row(
817+
"[yellow]●[/yellow] Status",
818+
"[yellow]Reindex recommended[/yellow]",
819+
)
820+
if es.reindex_reason:
821+
right.add_row(" Reason", f"[yellow]{es.reindex_reason}[/yellow]")
822+
else:
823+
right.add_row("[green]●[/green] Status", "[green]Up to date[/green]")
824+
825+
# --- Compose two-column layout (content-sized, NOT Layout) ---
826+
columns = Table.grid(padding=(0, 4), expand=False)
827+
columns.add_row(left, right)
828+
829+
# --- Note Types bar chart (top 5 by count) ---
830+
bars_section = None
831+
if info.statistics.note_types:
832+
sorted_types = sorted(
833+
info.statistics.note_types.items(), key=lambda x: x[1], reverse=True
834+
)
835+
top_types = sorted_types[:5]
836+
max_count = top_types[0][1] if top_types else 1
837+
838+
bars = Table.grid(padding=(0, 2), expand=False)
839+
bars.add_column("type", style="cyan", width=16, justify="right")
840+
bars.add_column("bar")
841+
bars.add_column("count", style="green", justify="right")
842+
843+
for note_type, count in top_types:
844+
bars.add_row(note_type, make_bar(count, max_count), str(count))
845+
846+
remaining = len(sorted_types) - len(top_types)
847+
bars_section = Group(
848+
"[bold]Note Types[/bold]",
849+
bars,
850+
f"[dim]+{remaining} more types[/dim]" if remaining > 0 else "",
770851
)
771-
)
772-
773-
# Statistics section
774-
stats_table = Table(title="Statistics")
775-
stats_table.add_column("Metric", style="cyan")
776-
stats_table.add_column("Count", style="green")
777-
778-
stats_table.add_row("Entities", str(info.statistics.total_entities))
779-
stats_table.add_row("Observations", str(info.statistics.total_observations))
780-
stats_table.add_row("Relations", str(info.statistics.total_relations))
781-
stats_table.add_row(
782-
"Unresolved Relations", str(info.statistics.total_unresolved_relations)
783-
)
784-
stats_table.add_row("Isolated Entities", str(info.statistics.isolated_entities))
785-
786-
console.print(stats_table)
787852

788-
# Note types
789-
if info.statistics.note_types:
790-
note_types_table = Table(title="Note Types")
791-
note_types_table.add_column("Type", style="blue")
792-
note_types_table.add_column("Count", style="green")
793-
794-
for note_type, count in info.statistics.note_types.items():
795-
note_types_table.add_row(note_type, str(count))
796-
797-
console.print(note_types_table)
798-
799-
# Most connected entities
800-
if info.statistics.most_connected_entities: # pragma: no cover
801-
connected_table = Table(title="Most Connected Entities")
802-
connected_table.add_column("Title", style="blue")
803-
connected_table.add_column("Permalink", style="cyan")
804-
connected_table.add_column("Relations", style="green")
805-
806-
for entity in info.statistics.most_connected_entities:
807-
connected_table.add_row(
808-
entity["title"], entity["permalink"], str(entity["relation_count"])
809-
)
810-
811-
console.print(connected_table)
812-
813-
# Recent activity
814-
if info.activity.recently_updated: # pragma: no cover
815-
recent_table = Table(title="Recent Activity")
816-
recent_table.add_column("Title", style="blue")
817-
recent_table.add_column("Type", style="cyan")
818-
recent_table.add_column("Last Updated", style="green")
819-
820-
for entity in info.activity.recently_updated[:5]: # Show top 5
821-
updated_at = (
822-
datetime.fromisoformat(entity["updated_at"])
823-
if isinstance(entity["updated_at"], str)
824-
else entity["updated_at"]
825-
)
826-
recent_table.add_row(
827-
entity["title"],
828-
entity["note_type"],
829-
updated_at.strftime("%Y-%m-%d %H:%M"),
830-
)
831-
832-
console.print(recent_table)
833-
834-
# Available projects
835-
projects_table = Table(title="Available Projects")
836-
projects_table.add_column("Name", style="blue")
837-
projects_table.add_column("Path", style="cyan")
838-
projects_table.add_column("Default", style="green")
839-
840-
for name, proj_info in info.available_projects.items():
841-
is_default = name == info.default_project
842-
project_path = proj_info["path"]
843-
projects_table.add_row(name, project_path, "[X]" if is_default else "")
844-
845-
console.print(projects_table)
846-
847-
# Timestamp
853+
# --- Footer ---
848854
current_time = (
849855
datetime.fromisoformat(str(info.system.timestamp))
850856
if isinstance(info.system.timestamp, str)
851857
else info.system.timestamp
852858
)
853-
console.print(f"\nTimestamp: [cyan]{current_time.strftime('%Y-%m-%d %H:%M:%S')}[/cyan]")
859+
footer = (
860+
f"[dim]{format_path(info.project_path)} "
861+
f"default: {info.default_project} "
862+
f"{current_time.strftime('%Y-%m-%d %H:%M')}[/dim]"
863+
)
864+
865+
# --- Assemble dashboard ---
866+
parts: list = [columns, ""]
867+
if bars_section:
868+
parts.extend([bars_section, ""])
869+
parts.append(footer)
870+
body = Group(*parts)
871+
872+
console.print(
873+
Panel(
874+
body,
875+
title=f"[bold]{info.project_name}[/bold]",
876+
subtitle=f"Basic Memory {info.system.version}",
877+
expand=False,
878+
)
879+
)
854880

855881
except typer.Exit:
856882
raise

0 commit comments

Comments
 (0)