diff --git a/src/gui_2.py b/src/gui_2.py index aa403ea..80e71a9 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -8,6 +8,7 @@ import sys import os import shutil import copy +import threading from pathlib import Path from tkinter import filedialog, Tk from typing import Optional, Any @@ -1769,133 +1770,144 @@ class App: if self._show_ast_inspector: imgui.open_popup('AST Inspector') self._show_ast_inspector = False - expanded, opened = imgui.begin_popup_modal('AST Inspector', True, imgui.WindowFlags_.always_auto_resize) - if not opened: - return - if self.ui_inspecting_ast_file is None: - imgui.close_current_popup() - imgui.end_popup() - return - f_item = self.ui_inspecting_ast_file - f_path = f_item.path if hasattr(f_item, "path") else str(f_item) - - if f_path != self._cached_ast_file_path: - outline = "" - try: - if f_path.lower().endswith('.py'): - outline = mcp_client.py_get_code_outline(f_path) - elif f_path.lower().endswith(('.c', '.h')): - outline = mcp_client.ts_c_get_code_outline(f_path) - else: - outline = mcp_client.ts_cpp_get_code_outline(f_path) - except Exception as e: - outline = f"Error fetching outline: {e}" - - self._cached_ast_nodes = [] - import re - pattern = re.compile(r'^(\s*)\[(.*?)\] (.*?) \(Lines (\d+)-(\d+)\)') - stack = [] # (indent, name) - for line in outline.splitlines(): - m = pattern.match(line) - if m: - indent_str, kind, name, start_ln, end_ln = m.groups() - indent = len(indent_str) - while stack and stack[-1][0] >= indent: - stack.pop() - stack.append((indent, name)) - full_path = '::'.join([s[1] for s in stack]) - self._cached_ast_nodes.append({ - 'indent': indent, - 'kind': kind, - 'name': name, - 'full_path': full_path, - 'start_line': int(start_ln), - 'end_line': int(end_ln) - }) - try: - content = mcp_client.read_file(f_path) - self._cached_ast_file_lines = content.splitlines() - except Exception: - self._cached_ast_file_lines = ["Error loading file content."] - self._cached_ast_file_path = f_path - imgui.text(f"Inspecting AST: {f_path}") - imgui.separator() + #region: AST Inspector + expanded, opened = imgui.begin_popup_modal('AST Inspector', True, imgui.WindowFlags_.always_auto_resize) + if opened: + if expanded: + if self.ui_inspecting_ast_file is None: + imgui.close_current_popup() + else: + f_item = self.ui_inspecting_ast_file + f_path = f_item.path if hasattr(f_item, "path") else str(f_item) + + if f_path != self._cached_ast_file_path: + outline = "" + try: + if f_path.lower().endswith('.py'): + outline = mcp_client.py_get_code_outline(f_path) + elif f_path.lower().endswith(('.c', '.h')): + outline = mcp_client.ts_c_get_code_outline(f_path) + else: + outline = mcp_client.ts_cpp_get_code_outline(f_path) + except Exception as e: + outline = f"Error fetching outline: {e}" + + self._cached_ast_nodes = [] + import re + pattern = re.compile(r'^(\s*)\[(.*?)\] (.*?) \(Lines (\d+)-(\d+)\)') + stack = [] # (indent, name) + for line in outline.splitlines(): + m = pattern.match(line) + if m: + indent_str, kind, name, start_ln, end_ln = m.groups() + indent = len(indent_str) + while stack and stack[-1][0] >= indent: + stack.pop() + stack.append((indent, name)) + full_path = '::'.join([s[1] for s in stack]) + self._cached_ast_nodes.append({ + 'indent': indent, + 'kind': kind, + 'name': name, + 'full_path': full_path, + 'start_line': int(start_ln), + 'end_line': int(end_ln) + }) + try: + content = mcp_client.read_file(f_path) + self._cached_ast_file_lines = content.splitlines() + except Exception: + self._cached_ast_file_lines = ["Error loading file content."] + self._cached_ast_file_path = f_path + + imgui.text(f"Inspecting AST: {f_path}") + imgui.separator() + + #region: ast_dual_pane + if imgui.begin_table('ast_dual_pane', 2, imgui.TableFlags_.resizable | imgui.TableFlags_.borders_inner_v): + imgui.table_next_column() + + #region: LEFT COLUMN (Tree) --- + if imgui.begin_child("ast_tree_scroll", imgui.ImVec2(0, 600), True): + if not self._cached_ast_nodes: + imgui.text("No AST nodes found or error fetching outline.") + else: + for node in self._cached_ast_nodes: + indent = node['indent'] + kind = node['kind'] + name = node['name'] + full_path = node['full_path'] + + imgui.dummy(imgui.ImVec2(indent * 10, 0)) + imgui.same_line() + imgui.text(f"[{kind}] {name}") + imgui.same_line(imgui.get_window_width() - 200) + + current_mode = f_item.ast_mask.get(full_path, 'hide') + + imgui.push_id(full_path) + if imgui.radio_button("Def", current_mode == 'def'): + f_item.ast_mask[full_path] = 'def' + imgui.same_line() + if imgui.radio_button("Sig", current_mode == 'sig'): + f_item.ast_mask[full_path] = 'sig' + imgui.same_line() + if imgui.radio_button("Hide", current_mode == 'hide'): + f_item.ast_mask[full_path] = 'hide' + imgui.pop_id() + imgui.end_child() + #endregion: LEFT COLUMN (Tree) + + imgui.table_next_column() + + #region: RIGHT COLUMN (Content) --- + if imgui.begin_child("ast_content_scroll", imgui.ImVec2(0, 600), True): + if not hasattr(self, '_cached_ast_file_lines') or not self._cached_ast_file_lines: + imgui.text("No file content loaded.") + else: + draw_list = imgui.get_window_draw_list() + for i, line_text in enumerate(self._cached_ast_file_lines): + line_num = i + 1 + + # Prioritize the most specific node (deepest indent) that covers the line + deepest_node = None + for node in self._cached_ast_nodes: + if node['start_line'] <= line_num <= node['end_line']: + if deepest_node is None or node['indent'] > deepest_node['indent']: + deepest_node = node + + mode = 'hide' + if deepest_node: + mode = f_item.ast_mask.get(deepest_node['full_path'], 'hide') + + pos = imgui.get_cursor_screen_pos() + line_height = imgui.get_text_line_height() + + if mode == 'def': + # Green, alpha 0.2 + draw_list.add_rect_filled(pos, imgui.ImVec2(pos.x + imgui.get_content_region_avail().x, pos.y + line_height), imgui.get_color_u32(vec4(0, 255, 0, 0.2))) + elif mode == 'sig': + # Blue, alpha 0.2 + draw_list.add_rect_filled(pos, imgui.ImVec2(pos.x + imgui.get_content_region_avail().x, pos.y + line_height), imgui.get_color_u32(vec4(0, 0, 255, 0.2))) + + imgui.text(f"{line_num:4} | {line_text}") + imgui.end_child() + #endregion: RIGHT COLUMN (Content) --- + imgui.end_table() + #endregion: ast_dual_pane + + imgui.separator() + + if imgui.button("Close", imgui.ImVec2(120, 0)): + self.ui_inspecting_ast_file = None + imgui.close_current_popup() + + imgui.end_popup() + #endregion: AST Inspector - if imgui.begin_table('ast_dual_pane', 2, imgui.TableFlags_.resizable | imgui.TableFlags_.borders_inner_v): - imgui.table_next_column() - - # --- LEFT COLUMN (Tree) --- - imgui.begin_child("ast_tree_scroll", imgui.ImVec2(0, 600), True) - if not self._cached_ast_nodes: - imgui.text("No AST nodes found or error fetching outline.") - else: - for node in self._cached_ast_nodes: - indent = node['indent'] - kind = node['kind'] - name = node['name'] - full_path = node['full_path'] - - imgui.dummy(imgui.ImVec2(indent * 10, 0)) - imgui.same_line() - imgui.text(f"[{kind}] {name}") - imgui.same_line(imgui.get_window_width() - 200) - - current_mode = f_item.ast_mask.get(full_path, 'hide') - - imgui.push_id(full_path) - if imgui.radio_button("Def", current_mode == 'def'): - f_item.ast_mask[full_path] = 'def' - imgui.same_line() - if imgui.radio_button("Sig", current_mode == 'sig'): - f_item.ast_mask[full_path] = 'sig' - imgui.same_line() - if imgui.radio_button("Hide", current_mode == 'hide'): - f_item.ast_mask[full_path] = 'hide' - imgui.pop_id() - imgui.end_child() - - imgui.table_next_column() - - # --- RIGHT COLUMN (Content) --- - imgui.begin_child("ast_content_scroll", imgui.ImVec2(0, 600), True) - if not hasattr(self, '_cached_ast_file_lines') or not self._cached_ast_file_lines: - imgui.text("No file content loaded.") - else: - draw_list = imgui.get_window_draw_list() - for i, line_text in enumerate(self._cached_ast_file_lines): - line_num = i + 1 - - # Prioritize the most specific node (deepest indent) that covers the line - deepest_node = None - for node in self._cached_ast_nodes: - if node['start_line'] <= line_num <= node['end_line']: - if deepest_node is None or node['indent'] > deepest_node['indent']: - deepest_node = node - - mode = 'hide' - if deepest_node: - mode = f_item.ast_mask.get(deepest_node['full_path'], 'hide') - - pos = imgui.get_cursor_screen_pos() - line_height = imgui.get_text_line_height() - - if mode == 'def': - # Green, alpha 0.2 - draw_list.add_rect_filled(pos, imgui.ImVec2(pos.x + imgui.get_content_region_avail().x, pos.y + line_height), imgui.get_color_u32(vec4(0, 255, 0, 0.2))) - elif mode == 'sig': - # Blue, alpha 0.2 - draw_list.add_rect_filled(pos, imgui.ImVec2(pos.x + imgui.get_content_region_avail().x, pos.y + line_height), imgui.get_color_u32(vec4(0, 0, 255, 0.2))) - - imgui.text(f"{line_num:4} | {line_text}") - imgui.end_child() - imgui.end_table() - - imgui.separator() - if imgui.button("Close", imgui.ImVec2(120, 0)): + if not opened: self.ui_inspecting_ast_file = None - imgui.close_current_popup() - imgui.end_popup() def _render_save_workspace_profile_modal(self) -> None: if self._show_save_workspace_profile_modal: @@ -1929,26 +1941,25 @@ class App: def _render_add_context_files_modal(self) -> None: if imgui.begin_popup_modal("Select Context Files", None, imgui.WindowFlags_.always_auto_resize)[0]: imgui.text("Select files from project to add to context:") - imgui.begin_child("ctx_picker_list", imgui.ImVec2(600, 300), True) - - from src import models - # Create a temporary selection set if not initialized - if not hasattr(self, '_ui_picker_selected'): - self._ui_picker_selected = set() - - for f in self.files: - fpath = f.path if hasattr(f, 'path') else str(f) - # Skip if already in context - if any((cf.path if hasattr(cf, 'path') else str(cf)) == fpath for cf in self.context_files): - continue - is_sel = fpath in self._ui_picker_selected - clicked, new_sel = imgui.checkbox(f"{fpath}##picker_{fpath}", is_sel) - if clicked: - if new_sel: - self._ui_picker_selected.add(fpath) - else: - self._ui_picker_selected.discard(fpath) - imgui.end_child() + if imgui.begin_child("ctx_picker_list", imgui.ImVec2(600, 300), True): + from src import models + # Create a temporary selection set if not initialized + if not hasattr(self, '_ui_picker_selected'): + self._ui_picker_selected = set() + + for f in self.files: + fpath = f.path if hasattr(f, 'path') else str(f) + # Skip if already in context + if any((cf.path if hasattr(cf, 'path') else str(cf)) == fpath for cf in self.context_files): + continue + is_sel = fpath in self._ui_picker_selected + clicked, new_sel = imgui.checkbox(f"{fpath}##picker_{fpath}", is_sel) + if clicked: + if new_sel: + self._ui_picker_selected.add(fpath) + else: + self._ui_picker_selected.discard(fpath) + imgui.end_child() imgui.separator() if imgui.button("Add Selected", imgui.ImVec2(120, 0)): @@ -2993,11 +3004,11 @@ class App: self._file_stats_queue = [] if not hasattr(self, '_file_stats_worker_active'): self._file_stats_worker_active = False - + if imgui.collapsing_header("Context Composition"): total_lines = 0 total_ast = 0 - + missing_keys = [] for f in self.context_files: f_path = f.path if hasattr(f, "path") else str(f) @@ -3009,7 +3020,7 @@ class App: stats = self._file_stats_cache[cache_key] total_lines += stats.get("lines", 0) total_ast += stats.get("ast_elements", 0) - + # Process one missing key per frame or spawn a worker if missing_keys and not self._file_stats_worker_active: def _stats_worker(): @@ -3020,12 +3031,11 @@ class App: self._file_stats_cache[key] = aggregate.compute_file_stats(path) finally: self._file_stats_worker_active = False - - import threading + threading.Thread(target=_stats_worker, daemon=True).start() - + #region: Batch Action Bar imgui.text("Batch:") - imgui.same_line() + # imgui.same_line() for mode in ["full", "summary", "skeleton", "outline", "masked", "none"]: if imgui.button(f"{mode.capitalize()}##batch"): for f in self.context_files: @@ -3067,9 +3077,9 @@ class App: #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) @@ -3180,6 +3190,7 @@ class App: imgui.same_line() imgui.text_colored(imgui.ImVec4(1.0, 0.5, 0.0, 1.0), "[Slices Active]") imgui.tree_pop() + imgui.end_table() # Context Composition collasping header