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:
"""
+87 -52
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,70 +2896,90 @@ class App:
new_files.append(f)
self.context_files = new_files
self.ui_selected_context_files.clear()
#endregion: Batch Action Bar
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)
# Checkbox for selection
f_path = f_item.path if hasattr(f_item, "path") else str(f_item)
is_sel = f_path in self.ui_selected_context_files
changed_sel, is_sel = imgui.checkbox(f"##sel{i}", is_sel)
if changed_sel:
if imgui.get_io().key_shift and self._last_selected_context_index != -1:
start = min(self._last_selected_context_index, i)
end = max(self._last_selected_context_index, i)
for idx in range(start, end + 1):
item = self.context_files[idx]
item_path = item.path if hasattr(item, "path") else str(item)
if is_sel:
self.ui_selected_context_files.add(item_path)
else:
self.ui_selected_context_files.discard(item_path)
else:
if is_sel:
self.ui_selected_context_files.add(f_path)
else:
self.ui_selected_context_files.discard(f_path)
self._last_selected_context_index = i
imgui.same_line()
imgui.text(f_path)
if f_path.lower().endswith(('.c', '.cpp', '.h', '.hpp', '.cxx', '.cc')):
imgui.same_line()
if imgui.button(f"[Inspect]##{i}"):
self.ui_inspecting_ast_file = f_item
self._show_ast_inspector = True
imgui.same_line()
if imgui.button(f"[Slices]##{i}"):
self.ui_editing_slices_file = f_item
f_path = f_item.path if hasattr(f_item, "path") else str(f_item)
self.text_viewer_title = f"Slices: {f_path}"
try:
self.text_viewer_content = mcp_client.read_file(f_path)
except Exception as e:
self.text_viewer_content = f"Error reading file: {e}"
self.text_viewer_type = 'cpp' if f_path.endswith(('.cpp', '.hpp', '.h')) else 'python' if f_path.endswith('.py') else 'text'
self.show_text_viewer = True
is_open = imgui.tree_node_ex(f"{dir_name}##dir_{dir_name}", imgui.TreeNodeFlags_.default_open)
imgui.table_set_column_index(1)
if hasattr(f_item, "auto_aggregate"):
changed_agg, f_item.auto_aggregate = imgui.checkbox(f"Agg##cc{i}", f_item.auto_aggregate)
imgui.same_line()
changed_full, f_item.force_full = imgui.checkbox(f"Full##cc{i}", f_item.force_full)
if hasattr(f_item, "ast_signatures"):
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)
# Checkbox for selection
f_path = f_item.path if hasattr(f_item, "path") else str(f_item)
is_sel = f_path in self.ui_selected_context_files
changed_sel, is_sel = imgui.checkbox(f"##sel{i}", is_sel)
if changed_sel:
if imgui.get_io().key_shift and self._last_selected_context_index != -1:
start = min(self._last_selected_context_index, i)
end = max(self._last_selected_context_index, i)
for idx in range(start, end + 1):
item = self.context_files[idx]
item_path = item.path if hasattr(item, "path") else str(item)
if is_sel:
self.ui_selected_context_files.add(item_path)
else:
self.ui_selected_context_files.discard(item_path)
else:
if is_sel:
self.ui_selected_context_files.add(f_path)
else:
self.ui_selected_context_files.discard(f_path)
self._last_selected_context_index = i
imgui.same_line()
_, f_item.ast_signatures = imgui.checkbox(f"Sig##cc{i}", f_item.ast_signatures)
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()
if imgui.button(f"[Inspect]##{i}"):
self.ui_inspecting_ast_file = f_item
self._show_ast_inspector = True
imgui.same_line()
_, f_item.ast_definitions = imgui.checkbox(f"Def##cc{i}", f_item.ast_definitions)
if imgui.button(f"[Slices]##{i}"):
self.ui_editing_slices_file = f_item
f_path = f_item.path if hasattr(f_item, "path") else str(f_item)
self.text_viewer_title = f"Slices: {f_path}"
try:
self.text_viewer_content = mcp_client.read_file(f_path)
except Exception as e:
self.text_viewer_content = f"Error reading file: {e}"
self.text_viewer_type = 'cpp' if f_path.endswith(('.cpp', '.hpp', '.h')) else 'python' if f_path.endswith('.py') else 'text'
self.show_text_viewer = True
imgui.table_set_column_index(1)
if hasattr(f_item, "auto_aggregate"):
changed_agg, f_item.auto_aggregate = imgui.checkbox(f"Agg##cc{i}", f_item.auto_aggregate)
imgui.same_line()
changed_full, f_item.force_full = imgui.checkbox(f"Full##cc{i}", f_item.force_full)
if hasattr(f_item, "ast_signatures"):
imgui.same_line()
_, 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