feat(context): Implement directory grouping and file stats in context composition panel
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user