feat(context): Implement directory grouping and file stats in context composition panel

This commit is contained in:
2026-05-11 11:37:15 -04:00
parent daf887eed4
commit 5112debe14
3 changed files with 151 additions and 52 deletions
+34
View File
@@ -15,6 +15,7 @@ to use the MCP tools to fetch only what it needs.
import tomllib
import re
import glob
import os
from pathlib import Path, PureWindowsPath
from typing import Any, cast
from src import summarize
@@ -59,6 +60,39 @@ def resolve_paths(base_dir: Path, entry: str) -> list[Path]:
filtered.append(p)
return sorted(filtered)
def group_files_by_dir(files: list[Any]) -> dict[str, list[Any]]:
"""Groups FileItem objects by their relative directory path."""
grouped = {}
for f in files:
path_str = f.path if hasattr(f, 'path') else str(f)
# Normalize path separators
path_str = path_str.replace('\\', '/')
dir_name = os.path.dirname(path_str)
if not dir_name:
dir_name = "."
if dir_name not in grouped:
grouped[dir_name] = []
grouped[dir_name].append(f)
return grouped
def compute_file_stats(abs_path: str) -> dict[str, int]:
"""Computes lines and basic AST stats for a file."""
stats = {"lines": 0, "ast_elements": 0}
try:
with open(abs_path, 'r', encoding='utf-8') as f:
content = f.read()
stats["lines"] = len(content.splitlines())
if abs_path.endswith('.py'):
import ast
try:
tree = ast.parse(content)
stats["ast_elements"] = sum(1 for node in ast.walk(tree) if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)))
except Exception:
pass
except Exception:
pass
return stats
def build_discussion_section(history: list[Any]) -> str:
"""
+37 -2
View File
@@ -2800,6 +2800,21 @@ class App:
imgui.text_disabled("Message & Response panels are detached.")
def _render_context_composition_panel(self) -> None:
if not hasattr(self, '_file_stats_cache'):
self._file_stats_cache = {}
total_lines = 0
total_ast = 0
for f in self.context_files:
f_path = f.path if hasattr(f, "path") else str(f)
mtime = os.path.getmtime(f_path) if os.path.exists(f_path) else 0
cache_key = f"{f_path}_{mtime}"
if cache_key not in self._file_stats_cache:
self._file_stats_cache[cache_key] = aggregate.compute_file_stats(f_path)
stats = self._file_stats_cache[cache_key]
total_lines += stats.get("lines", 0)
total_ast += stats.get("ast_elements", 0)
if imgui.collapsing_header("Context Composition"):
#region: Batch Action Bar
imgui.text("Batch:")
@@ -2881,15 +2896,29 @@ class App:
new_files.append(f)
self.context_files = new_files
self.ui_selected_context_files.clear()
imgui.same_line()
imgui.text(f" | Total: {len(self.context_files)} files, {total_lines} lines, {total_ast} AST elements")
#endregion: Batch Action Bar
imgui.dummy(imgui.ImVec2(0, 4))
grouped_files = aggregate.group_files_by_dir(self.context_files)
if imgui.begin_table("ctx_comp_table", 2, imgui.TableFlags_.resizable | imgui.TableFlags_.borders):
imgui.table_setup_column("File", imgui.TableColumnFlags_.width_stretch)
imgui.table_setup_column("Flags", imgui.TableColumnFlags_.width_fixed, 200)
imgui.table_headers_row()
for i, f_item in enumerate(self.context_files):
file_indices = {id(f): idx for idx, f in enumerate(self.context_files)}
for dir_name, g_files in grouped_files.items():
imgui.table_next_row()
imgui.table_set_column_index(0)
is_open = imgui.tree_node_ex(f"{dir_name}##dir_{dir_name}", imgui.TreeNodeFlags_.default_open)
imgui.table_set_column_index(1)
if is_open:
for f_item in g_files:
i = file_indices[id(f_item)]
imgui.table_next_row()
imgui.table_set_column_index(0)
@@ -2915,7 +2944,12 @@ class App:
self.ui_selected_context_files.discard(f_path)
self._last_selected_context_index = i
imgui.same_line()
imgui.text(f_path)
mtime = os.path.getmtime(f_path) if os.path.exists(f_path) else 0
cache_key = f"{f_path}_{mtime}"
stats = self._file_stats_cache.get(cache_key, {"lines": 0, "ast_elements": 0})
f_name = os.path.basename(f_path)
imgui.text(f"{f_name} (L: {stats.get('lines', 0)}, AST: {stats.get('ast_elements', 0)})")
if f_path.lower().endswith(('.c', '.cpp', '.h', '.hpp', '.cxx', '.cc')):
imgui.same_line()
@@ -2945,6 +2979,7 @@ class App:
_, f_item.ast_signatures = imgui.checkbox(f"Sig##cc{i}", f_item.ast_signatures)
imgui.same_line()
_, f_item.ast_definitions = imgui.checkbox(f"Def##cc{i}", f_item.ast_definitions)
imgui.tree_pop()
imgui.end_table()
# Context Composition collasping header
+30
View File
@@ -0,0 +1,30 @@
import pytest
from src.aggregate import group_files_by_dir, compute_file_stats
from src.models import FileItem
def test_group_files_by_dir():
files = [
FileItem(path="src/main.py"),
FileItem(path="src/utils/helpers.py"),
FileItem(path="tests/test_main.py"),
FileItem(path="README.md")
]
grouped = group_files_by_dir(files)
assert len(grouped) == 4
assert grouped["src"] == [files[0]]
assert grouped["src/utils"] == [files[1]]
assert grouped["tests"] == [files[2]]
assert grouped["."] == [files[3]]
def test_compute_file_stats():
import tempfile
import os
with tempfile.TemporaryDirectory() as temp_dir:
# Create a dummy python file
py_path = os.path.join(temp_dir, "test.py")
with open(py_path, "w") as f:
f.write("def foo():\n pass\n\nclass Bar:\n pass\n")
stats = compute_file_stats(py_path)
assert stats["lines"] == 5
assert stats["ast_elements"] == 2 # 1 func, 1 class