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:
|
||||
"""
|
||||
|
||||
|
||||
+87
-52
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user