Private
Public Access
0
0

UX UX UX UX UX

This commit is contained in:
2026-06-02 02:58:33 -04:00
parent 964b5c5aa4
commit e9ff6efe20
6 changed files with 57 additions and 325 deletions
+3 -3
View File
@@ -288,7 +288,7 @@ This file tracks all major tracks for the project. Each track has its own detail
---
- [~] **Track: Preserve context selection on discussion switch and add empty context warning**
- [x] **Track: Preserve context selection on discussion switch and add empty context warning**
*Link: [./tracks/context_preservation_and_warnings_20260601/](./tracks/context_preservation_and_warnings_20260601/)*
---
@@ -303,7 +303,7 @@ This file tracks all major tracks for the project. Each track has its own detail
---
- [~] **Track: Combine AST Inspector and Slices Editor into a unified Structural File Editor**
- [x] **Track: Combine AST Inspector and Slices Editor into a unified Structural File Editor**
*Link: [./tracks/structural_file_editor_20260601/](./tracks/structural_file_editor_20260601/)*
---
@@ -313,5 +313,5 @@ This file tracks all major tracks for the project. Each track has its own detail
---
- [~] **Track: Fix Approve Modal sizing and inline full preview**
- [x] **Track: Fix Approve Modal sizing and inline full preview**
*Link: [./tracks/approve_modal_ux_20260601/](./tracks/approve_modal_ux_20260601/)*
@@ -1,17 +1,17 @@
# Implementation Plan: Structural File Editor
## Phase 1: Unification
- [ ] Task: Create Structural File Editor Modal
- [ ] Add `app.show_structural_editor_modal = False` to `App.__init__`.
- [ ] Create `render_structural_file_editor_modal(app: App)` in `src/gui_2.py`.
- [ ] Port the tree rendering logic from `render_ast_inspector_modal` into this new modal.
- [ ] Port the custom slice management UI from `render_slices_editor_modal` into this new modal, positioning it logically alongside or above the AST tree.
- [ ] Task: Update File Row UI
- [ ] In `render_context_files_table`, replace the separate "AST" and "Slices" buttons with a single "Structure" button that sets `app.show_structural_editor_modal = True` and sets the target file.
- [x] Task: Create Structural File Editor Modal
- [x] Add `app.show_structural_editor_modal = False` to `App.__init__`.
- [x] Create `render_structural_file_editor_modal(app: App)` in `src/gui_2.py` (implemented in `src/structural_editor_modal.py`).
- [x] Port the tree rendering logic from `render_ast_inspector_modal` into this new modal.
- [x] Port the custom slice management UI from `render_slices_editor_modal` into this new modal, positioning it logically alongside or above the AST tree.
- [x] Task: Update File Row UI
- [x] In `render_context_files_table`, replace the separate "AST" and "Slices" buttons with a single "Structure" button that sets `app.show_structural_editor_modal = True` and sets the target file.
## Phase 2: Verification
- [ ] Task: Verification
- [ ] Open the Structural File Editor for a complex Python file.
- [ ] Verify both AST nodes and custom slices are visible and interactive.
- [ ] Verify that adding a custom slice works correctly within the unified interface.
- [ ] Task: Conductor - User Manual Verification 'Phase 2: Verification' (Protocol in workflow.md)
- [x] Task: Verification
- [x] Open the Structural File Editor for a complex Python file.
- [x] Verify both AST nodes and custom slices are visible and interactive.
- [x] Verify that adding a custom slice works correctly within the unified interface.
- [x] Task: Conductor - User Manual Verification 'Phase 2: Verification' (Protocol in workflow.md)
+1 -1
View File
@@ -2631,7 +2631,7 @@ def run_subagent_summarization(file_path: str, content: str, is_code: bool, outl
return "ERROR: Unsupported provider for sub-agent summarization"
def run_discussion_compression(discussion_text: str) -> str:
prompt = f"The following is a long conversation history.\\nPlease provide a highly compact, dense summary of the key facts, decisions, bugs encountered, and outcomes that should be retained for context going forward. Categorize into User intent, Tool outputs, and AI reasoning. Omit pleasantries and redundant thoughts.\\n\\n[HISTORY]\\n{discussion_text}"
prompt = f"The following is a long conversation history.\n\nPlease provide a highly compact, dense summary of the key facts, decisions, bugs encountered, and outcomes that should be retained for context going forward. Categorize into User intent, Tool outputs, and AI reasoning. Omit pleasantries and redundant thoughts.\n\n[HISTORY]\n{discussion_text}"
if _provider == "gemini":
_ensure_gemini_client()
if _gemini_client:
+3 -1
View File
@@ -3277,7 +3277,9 @@ class AppController:
response_text = ai_client.run_discussion_compression(disc_text)
if response_text and not response_text.startswith("ERROR:"):
self.disc_entries = [{"role": "System", "content": f"[COMPRESSED HISTORY]\n{response_text}", "collapsed": False, "ts": project_manager.now_ts()}]
with self._disc_entries_lock:
self.disc_entries.clear()
self.disc_entries.append({"role": "System", "content": f"[COMPRESSED HISTORY]\n{response_text}", "collapsed": False, "ts": project_manager.now_ts()})
self.ai_status = "compression complete"
else:
self.ai_status = f"compression failed: {response_text}"
+34 -304
View File
@@ -2632,24 +2632,22 @@ def render_files_and_media(app: App) -> None:
"""
[C: tests/test_gui_fast_render.py:test_render_files_and_media_fast]
"""
avail = imgui.get_content_region_avail().y
if not hasattr(app, 'files_screenshots_split'): app.files_screenshots_split = 0.65
split_y = int(avail * app.files_screenshots_split)
if imgui.collapsing_header("Files", imgui.TreeNodeFlags_.default_open):
with imscope.child("Files_child", -1, split_y, True):
if not hasattr(app, 'files_last_selected'): app.files_last_selected = -1
with imscope.table("files_table", 3, imgui.TableFlags_.resizable | imgui.TableFlags_.borders):
imgui.table_setup_column("", imgui.TableColumnFlags_.width_fixed, 25)
with imscope.group():
if imgui.begin_table("files_table", 3, imgui.TableFlags_.resizable | imgui.TableFlags_.borders | imgui.TableFlags_.row_bg):
imgui.table_setup_column("Act", imgui.TableColumnFlags_.width_fixed, 60)
imgui.table_setup_column("Path", imgui.TableColumnFlags_.width_stretch)
imgui.table_setup_column("Status", imgui.TableColumnFlags_.width_fixed, 60)
imgui.table_setup_column("Status", imgui.TableColumnFlags_.width_fixed, 70)
imgui.table_headers_row()
to_remove_idx = -1
app.files.sort(key=lambda f: f.path.lower() if hasattr(f, 'path') else str(f).lower())
for i, f_item in enumerate(app.files):
imgui.table_next_row(); imgui.table_set_column_index(0)
imgui.table_next_row()
imgui.table_set_column_index(0)
fpath = f_item.path if hasattr(f_item, 'path') else str(f_item)
in_context = any((cf.path if hasattr(cf, 'path') else str(cf)) == fpath for cf in app.context_files)
is_cached = any(fpath in c for c in getattr(app, '_cached_files', []))
if imgui.button(f"+##add_f_{i}"):
if not in_context:
@@ -2657,157 +2655,48 @@ def render_files_and_media(app: App) -> None:
new_item = models.FileItem(path=fpath)
app.context_files.append(new_item)
app._populate_auto_slices(new_item)
imgui.same_line()
if imgui.button(f"x##rem_f_{i}"):
to_remove_idx = i
imgui.table_set_column_index(1); imgui.text(fpath)
imgui.table_set_column_index(1)
imgui.text(fpath)
if imgui.is_item_hovered(): imgui.set_tooltip(fpath)
imgui.table_set_column_index(2)
if in_context:
imgui.text_colored(imgui.ImVec4(0.3, 0.8, 0.3, 1), "Active")
elif is_cached:
imgui.text_colored(imgui.ImVec4(0.3, 0.8, 1, 1), "Cached")
else:
imgui.text_disabled(" - ")
if is_cached: imgui.text_colored(imgui.ImVec4(0, 1, 0, 1), "Y")
else: imgui.text_colored(imgui.ImVec4(0.5, 0.5, 0.5, 1), "-")
if imgui.button("Add Files to Inventory"):
r = hide_tk_root(); paths = filedialog.askopenfilenames(); r.destroy()
for p in paths:
if p not in [f.path if hasattr(f, 'path') else f for f in app.files]: app.files.append(models.FileItem(path=p))
imgui.same_line()
if imgui.button("Clear Selection##inv"):
for f in app.files: f.selected = False
imgui.same_line()
if imgui.button("Remove from Inventory"):
app.files = [f for f in app.files if not f.selected]
imgui.end_table()
if to_remove_idx != -1: app.files.pop(to_remove_idx)
imgui.dummy(imgui.ImVec2(0, 5))
if imgui.button("Add Files to Inventory"):
r = hide_tk_root(); paths = filedialog.askopenfilenames(); r.destroy()
for p in paths:
if p not in [f.path if hasattr(f, 'path') else f for f in app.files]: app.files.append(models.FileItem(path=p))
imgui.separator()
if imgui.collapsing_header("Screenshots", imgui.TreeNodeFlags_.default_open):
with imscope.child("Shots_child", -1, -1, True):
with imscope.child("Shots_child", -1, 150, True):
to_rem_shot = -1
for i, s in enumerate(app.screenshots):
if imgui.button(f"x##s{i}"):
app.screenshots.pop(i)
break
if imgui.button(f"x##s{i}"): to_rem_shot = i
imgui.same_line(); imgui.text(s)
if to_rem_shot != -1: app.screenshots.pop(to_rem_shot)
if imgui.button("Add Screenshots##adds"):
r = hide_tk_root(); paths = filedialog.askopenfilenames(filetypes=[("Images", "*.png *.jpg *.jpeg *.gif *.bmp *.webp"), ("All", "*.*")]); r.destroy()
for p in paths:
if p not in app.screenshots: app.screenshots.append(p)
return
def render_files_panel(app: App, height_override: float = 0) -> None:
if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_files_panel")
imgui.text("Paths")
imgui.same_line()
imgui.text("| Base Dir:")
imgui.same_line()
imgui.set_next_item_width(-100)
ch, app.ui_files_base_dir = imgui.input_text("##f_base", app.ui_files_base_dir)
imgui.same_line()
if imgui.button("Browse##fb"):
r = hide_tk_root()
d = filedialog.askdirectory()
r.destroy()
if d: app.ui_files_base_dir = d
imgui.separator()
# Calculate content-based height: use override if provided, else content-based
if height_override > 0:
child_h = height_override
else:
row_count = max(len(app.files), 1)
child_h = min(row_count * 28 + 40, 300)
# BEGIN f_paths child window
imgui.begin_child("f_paths", imgui.ImVec2(0, child_h), True)
if imgui.begin_table("files_table", 4, imgui.TableFlags_.resizable | imgui.TableFlags_.borders):
imgui.table_setup_column("Actions", imgui.TableColumnFlags_.width_fixed, 40)
imgui.table_setup_column("File Path", imgui.TableColumnFlags_.width_stretch)
imgui.table_setup_column("Flags", imgui.TableColumnFlags_.width_fixed, 150)
imgui.table_setup_column("Cache", imgui.TableColumnFlags_.width_fixed, 40)
imgui.table_headers_row()
for i, f_item in enumerate(app.files):
imgui.table_next_row()
# Actions
imgui.table_set_column_index(0)
if imgui.button(f"x##f{i}"):
app.files.pop(i)
break
# File Path
imgui.table_set_column_index(1)
imgui.text(f_item.path if hasattr(f_item, "path") else str(f_item))
# Flags
imgui.table_set_column_index(2)
if hasattr(f_item, "auto_aggregate"):
changed_agg, f_item.auto_aggregate = imgui.checkbox(f"Agg##a{i}", f_item.auto_aggregate)
imgui.same_line()
changed_full, f_item.force_full = imgui.checkbox(f"Full##f{i}", f_item.force_full)
# Cache
imgui.table_set_column_index(3)
path = f_item.path if hasattr(f_item, "path") else str(f_item)
is_cached = any(path in c for c in getattr(app, "_cached_files", []))
if is_cached:
imgui.text_colored("", imgui.ImVec4(0, 1, 0, 1)) # Green dot
else:
imgui.text_disabled("")
imgui.end_table()
imgui.end_child()
if imgui.button("Add File(s)"):
r = hide_tk_root()
paths = filedialog.askopenfilenames()
r.destroy()
for p in paths:
if p not in [f.path if hasattr(f, "path") else f for f in app.files]:
app.files.append(models.FileItem(path=p))
imgui.same_line()
if imgui.button("Add Wildcard"):
r = hide_tk_root()
d = filedialog.askdirectory()
r.destroy()
if d: app.files.append(models.FileItem(path=str(Path(d) / "**" / "*")))
imgui.separator()
from src import summarize
stats = summarize._summary_cache.get_stats()
imgui.text_disabled(f"Summary Cache: {stats['entries']} entries ({stats['size_bytes']} bytes)")
imgui.same_line()
if imgui.button("Clear Summary Cache##btn_clear_summary_cache"):
app.controller._cb_clear_summary_cache()
if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_files_panel")
def render_screenshots_panel(app: App, height_override: float = 0) -> None:
if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_screenshots_panel")
imgui.text("Paths"); imgui.same_line(); imgui.text("| Base Dir:"); imgui.same_line();
imgui.set_next_item_width(-100)
ch, app.ui_shots_base_dir = imgui.input_text("##s_base", app.ui_shots_base_dir)
imgui.same_line()
if imgui.button("Browse##sb"):
r = hide_tk_root(); d = filedialog.askdirectory(); r.destroy()
if d: app.ui_shots_base_dir = d
imgui.separator()
# Calculate content-based height: use override if provided, else content-based
if height_override > 0: shot_h = height_override
else:
shot_count = max(len(app.screenshots), 1)
shot_h = min(shot_count * 28 + 40, 200)
# BEGIN s_paths child window
imgui.begin_child("s_paths", imgui.ImVec2(0, shot_h), True)
for i, s in enumerate(app.screenshots):
if imgui.button(f"x##s{i}"):
app.screenshots.pop(i)
break
imgui.same_line(); imgui.text(s)
imgui.end_child()
if imgui.button("Add Screenshot(s)"):
r = hide_tk_root()
paths = filedialog.askopenfilenames(
title="Select Screenshots",
filetypes=[("Images", "*.png *.jpg *.jpeg *.gif *.bmp *.webp"), ("All", "*.*")],
)
r.destroy()
for p in paths:
if p not in app.screenshots: app.screenshots.append(p)
if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_screenshots_panel")
#endregion: Context Management
def render_context_batch_actions(app: App, total_lines: int, total_ast: int) -> None:
imgui.text("Batch:")
@@ -2914,166 +2803,7 @@ def render_context_composition_panel(app: App) -> None:
render_context_screenshots(app)
def render_ast_inspector_modal(app: App) -> None:
"""
[C: tests/test_ast_inspector_extended.py:test_ast_inspector_line_range_parsing]
"""
if app._show_ast_inspector:
imgui.open_popup('AST Inspector')
app._show_ast_inspector = False
#region: AST Inspector
imgui.set_next_window_size(imgui.ImVec2(1200, 800), imgui.Cond_.first_use_ever)
expanded, opened = imgui.begin_popup_modal('AST Inspector', True, imgui.WindowFlags_.none)
if opened:
if expanded:
if app.ui_inspecting_ast_file is None:
imgui.close_current_popup()
else:
f_item = app.ui_inspecting_ast_file
f_path = f_item.path if hasattr(f_item, "path") else str(f_item)
if f_path != app._cached_ast_file_path:
outline = ""
try:
from src import mcp_client
from pathlib import Path
proj_dir = str(Path(app.controller.active_project_path).parent.resolve()) if getattr(app, 'controller', None) and app.controller.active_project_path else None
mcp_client.configure([{"path": f_path}], [proj_dir] if proj_dir else None)
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}"
app._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])
app._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)
app._cached_ast_file_lines = content.splitlines()
except Exception:
app._cached_ast_file_lines = ["Error loading file content."]
app._cached_ast_file_path = f_path
imgui.text(f"Inspecting AST: {f_path}")
imgui.separator()
avail = imgui.get_content_region_avail()
table_height = max(100.0, avail.y - imgui.get_frame_height_with_spacing() - 10)
#region: ast_dual_pane
if imgui.begin_table('ast_dual_pane', 2, imgui.TableFlags_.resizable | imgui.TableFlags_.borders_inner_v, imgui.ImVec2(0, table_height)):
imgui.table_next_column()
#region: LEFT COLUMN (Tree) ---
imgui.begin_child("ast_tree_scroll", imgui.ImVec2(0, 0), True)
if True:
if not app._cached_ast_nodes: imgui.text("No AST nodes found or error fetching outline.")
else:
for node in app._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}")
if imgui.is_item_hovered():
app._hovered_ast_node = full_path
btn_width = 150
avail_width = imgui.get_content_region_avail().x
try:
do_align = avail_width > btn_width
except TypeError:
do_align = False
if do_align:
imgui.same_line(imgui.get_window_width() - btn_width)
else:
imgui.same_line()
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) ---
imgui.begin_child("ast_content_scroll", imgui.ImVec2(0, 0), True)
if True:
if not hasattr(app, '_cached_ast_file_lines') or not app._cached_ast_file_lines:
imgui.text("No file content loaded.")
else:
draw_list = imgui.get_window_draw_list()
for i, line_text in enumerate(app._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 app._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)))
elif deepest_node and deepest_node['full_path'] == getattr(app, '_hovered_ast_node', None):
# Yellow, alpha 0.3 for hover
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(255, 255, 0, 0.3)))
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)):
app.ui_inspecting_ast_file = None
imgui.close_current_popup()
imgui.end_popup()
#endregion: AST Inspector
if not opened: app.ui_inspecting_ast_file = None
pass
def render_save_workspace_profile_modal(app: App) -> None:
if app._show_save_workspace_profile_modal:
+4 -4
View File
@@ -2,8 +2,10 @@ from __future__ import annotations
from imgui_bundle import imgui
import re
from typing import TYPE_CHECKING
from src import imscope
from src.theme import C_IN
from src import imgui_scopes as imscope
def vec4(r: float, g: float, b: float, a: float = 1.0) -> imgui.ImVec4: return imgui.ImVec4(r/255, g/255, b/255, a)
C_IN = vec4(140, 255, 160)
if TYPE_CHECKING:
from src.gui_2 import App
@@ -157,8 +159,6 @@ def render_structural_file_editor_modal(app: 'App') -> None:
imgui.text("No file content loaded.")
else:
draw_list = imgui.get_window_draw_list()
# We need vec4 locally
from src.imgui_scopes import vec4
for i, line_text in enumerate(app._cached_ast_file_lines):
line_num = i + 1
pos = imgui.get_cursor_screen_pos()