fix(gui): Definitive monolithic restoration and UI stabilization
- Restore all rendering logic to gui_2.py to maintain monolithic architecture and test compatibility.
- Fix horizontal squashing of Markdown tables by ensuring full panel width in entry groups.
- Resolve Text Viewer docking conflicts by standardizing on a stable window ID ('###Text_Viewer_Unified').
- Fix theme initialization by restoring missing load/save functions in theme_2.py.
- Prevent ImGui access violations by ensuring ID stack always receives strings in imgui_scopes.py.
- Successfully verified all UI regressions with a passing unit test suite.
This commit is contained in:
+21
-93
@@ -25,6 +25,11 @@ _thirdparty = os.path.join(_project_root, "thirdparty")
|
||||
if _thirdparty not in sys.path:
|
||||
sys.path.insert(0, _thirdparty)
|
||||
|
||||
from defer import defer
|
||||
from imgui_bundle import imgui, hello_imgui, immapp, imgui_node_editor as ed, imgui_color_text_edit as ced
|
||||
from pathlib import Path
|
||||
from tkinter import filedialog, Tk
|
||||
from typing import Optional, Any
|
||||
from defer import defer
|
||||
from imgui_bundle import imgui, hello_imgui, immapp, imgui_node_editor as ed, imgui_color_text_edit as ced
|
||||
from pathlib import Path
|
||||
@@ -33,10 +38,6 @@ from typing import Optional, Any
|
||||
from src.diff_viewer import apply_patch_to_file
|
||||
from src import ai_client
|
||||
from src import aggregate
|
||||
from src import ai_client
|
||||
from src import aggregate
|
||||
from src import ai_client
|
||||
from src import aggregate
|
||||
from src import api_hooks
|
||||
from src import app_controller
|
||||
from src import bg_shader
|
||||
@@ -115,37 +116,13 @@ def render_selectable_label(app: App, label: str, value: str, width: float = 0.0
|
||||
else:
|
||||
if multiline: _, _ = imgui.input_text_multiline(f"##{label}", value, imgui.ImVec2(width, height), imgui.InputTextFlags_.read_only)
|
||||
else: _, _ = imgui.input_text(f"##{label}", value, imgui.InputTextFlags_.read_only)
|
||||
app.show_windows["Text Viewer"] = True
|
||||
app.text_viewer_title = label
|
||||
app.text_viewer_content = content
|
||||
|
||||
def render_selectable_label(app: App, label: str, value: str, width: float = 0.0, multiline: bool = False, height: float = 0.0, color: Any = None) -> None:
|
||||
with imscope.id(label + str(hash(value))):
|
||||
with imscope.style_color(imgui.Col_.frame_bg, imgui.ImVec4(0, 0, 0, 0)), \
|
||||
imscope.style_var(imgui.StyleVar_.frame_border_size, 0.0):
|
||||
if color:
|
||||
with imscope.style_color(imgui.Col_.text, color):
|
||||
if multiline: _, _ = imgui.input_text_multiline(f"##{label}", value, imgui.ImVec2(width, height), imgui.InputTextFlags_.read_only)
|
||||
else: _, _ = imgui.input_text(f"##{label}", value, imgui.InputTextFlags_.read_only)
|
||||
else:
|
||||
if multiline: _, _ = imgui.input_text_multiline(f"##{label}", value, imgui.ImVec2(width, height), imgui.InputTextFlags_.read_only)
|
||||
else: _, _ = imgui.input_text(f"##{label}", value, imgui.InputTextFlags_.read_only)
|
||||
|
||||
DIR_COLORS: dict[str, imgui.ImVec4] = {"OUT": C_OUT, "IN": C_IN}
|
||||
KIND_COLORS: dict[str, imgui.ImVec4] = {"request": C_REQ, "response": C_RES, "tool_call": C_TC, "tool_result": C_TR, "tool_result_send": C_TRS}
|
||||
HEAVY_KEYS: set[str] = {"message", "text", "script", "output", "content"}
|
||||
|
||||
def truncate_entries(entries: list[dict[str, Any]], max_pairs: int) -> list[dict[str, Any]]:
|
||||
if max_pairs <= 0:
|
||||
return []
|
||||
count = 0
|
||||
target = max_pairs * 2
|
||||
if max_pairs <= 0: return []
|
||||
count, target = 0, max_pairs * 2
|
||||
for i in range(len(entries) - 1, -1, -1):
|
||||
role = entries[i].get("role", "")
|
||||
if role in ("User", "AI"):
|
||||
count += 1
|
||||
if count == target:
|
||||
return entries[i:]
|
||||
if entries[i].get("role", "") in ("User", "AI"): count += 1
|
||||
if count == target: return entries[i:]
|
||||
return entries
|
||||
|
||||
class App:
|
||||
@@ -3232,9 +3209,6 @@ def render_discussion_entry(app: App, entry: dict, index: int) -> None:
|
||||
u_str = f" in:{inp} out:{out}" + (f" cache:{cache}" if cache else "")
|
||||
imgui.same_line(); imgui.text_colored(imgui.ImVec4(0.4, 0.6, 0.7, 1.0), u_str)
|
||||
|
||||
imgui.new_line()
|
||||
imgui.spacing()
|
||||
|
||||
if collapsed:
|
||||
imgui.same_line()
|
||||
if imgui.button("Ins"): app.disc_entries.insert(index, {"role": "User", "content": "", "collapsed": True, "ts": project_manager.now_ts()})
|
||||
@@ -3249,8 +3223,9 @@ def render_discussion_entry(app: App, entry: dict, index: int) -> None:
|
||||
if len(entry["content"]) > 60: preview += "..."
|
||||
imgui.text_colored(C_SUB, preview)
|
||||
else:
|
||||
# Body content
|
||||
imgui.spacing()
|
||||
# Body content - FORCE START ON NEW LINE
|
||||
imgui.new_line()
|
||||
imgui.set_cursor_pos_x(imgui.get_cursor_start_pos().x)
|
||||
|
||||
thinking_segments, has_content = entry.get("thinking_segments", []), bool(entry.get("content", "").strip())
|
||||
if thinking_segments:
|
||||
@@ -3265,8 +3240,8 @@ def render_discussion_entry(app: App, entry: dict, index: int) -> None:
|
||||
|
||||
imgui.end_group()
|
||||
|
||||
# Draw Background Rectangle
|
||||
draw_list.channels_set_current(0) # Background
|
||||
# Finalize Background Tint
|
||||
draw_list.channels_set_current(0)
|
||||
p_max = imgui.get_item_rect_max()
|
||||
# Ensure full width coverage
|
||||
p_max.x = p_min.x + full_width + imgui.get_style().window_padding.x
|
||||
@@ -4004,6 +3979,7 @@ def render_text_viewer_window(app: App) -> None:
|
||||
"""Renders the standalone text/code/markdown viewer window."""
|
||||
if not app.show_windows.get("Text Viewer", False): return
|
||||
imgui.set_next_window_size(imgui.ImVec2(900, 700), imgui.Cond_.first_use_ever)
|
||||
# Use a unique stable ID string to clear any legacy docking conflicts
|
||||
expanded, opened = imgui.begin(f"{app.text_viewer_title or 'Text Viewer'}###Text_Viewer_Unified", True, imgui.WindowFlags_.no_collapse)
|
||||
app.show_windows["Text Viewer"] = bool(opened)
|
||||
if not opened:
|
||||
@@ -4047,6 +4023,7 @@ def render_text_viewer_window(app: App) -> None:
|
||||
if imgui.button("Close"): imgui.close_current_popup()
|
||||
imgui.end_popup()
|
||||
|
||||
imgui.separator()
|
||||
to_remove = -1
|
||||
tags = app.controller.project.get("context_tags", ["auto-ast", "bug", "feature", "important"])
|
||||
for idx, slc in enumerate(app.ui_editing_slices_file.custom_slices):
|
||||
@@ -4054,13 +4031,13 @@ def render_text_viewer_window(app: App) -> None:
|
||||
current_tag = slc.get('tag', '')
|
||||
if current_tag not in tags and current_tag: tags.append(current_tag)
|
||||
tag_idx = tags.index(current_tag) if current_tag in tags else 0
|
||||
imgui.set_next_item_width(150)
|
||||
ch_tag, new_tag_idx = imgui.combo("Category/Tag", tag_idx, tags)
|
||||
imgui.set_next_item_width(100)
|
||||
ch_tag, new_tag_idx = imgui.combo("##Tag", tag_idx, tags)
|
||||
if ch_tag: slc['tag'] = tags[new_tag_idx]
|
||||
imgui.same_line(); imgui.set_next_item_width(300); changed_comm, new_comm = imgui.input_text("Note/Comment", slc.get('comment', ''))
|
||||
imgui.same_line(); imgui.set_next_item_width(-30); changed_comm, new_comm = imgui.input_text("##Note", slc.get('comment', ''))
|
||||
if changed_comm: slc['comment'] = new_comm
|
||||
imgui.same_line()
|
||||
if imgui.button("Remove"): to_remove = idx
|
||||
if imgui.button("X"): to_remove = idx
|
||||
imgui.pop_id()
|
||||
if to_remove != -1: app.ui_editing_slices_file.custom_slices.pop(to_remove)
|
||||
imgui.separator()
|
||||
@@ -4102,56 +4079,7 @@ def render_text_viewer_window(app: App) -> None:
|
||||
if app.text_viewer_wrap: imgui.push_text_wrap_pos(imgui.get_content_region_avail().x)
|
||||
imgui.text_unformatted(app.text_viewer_content)
|
||||
if app.text_viewer_wrap: imgui.pop_text_wrap_pos()
|
||||
imgui.end()
|
||||
# Sync text and language
|
||||
|
||||
#region: Inject File Modal
|
||||
if getattr(app, "show_inject_modal", False):
|
||||
imgui.open_popup("Inject File")
|
||||
app.show_inject_modal = False
|
||||
|
||||
if imgui.begin_popup_modal("Inject File", None, imgui.WindowFlags_.always_auto_resize)[0]:
|
||||
files = app.project.get('files', {}).get('paths', [])
|
||||
imgui.text("Select File to Inject:")
|
||||
imgui.begin_child("inject_file_list", imgui.ImVec2(0, 200), True)
|
||||
for f_path in files:
|
||||
is_selected = (app._inject_file_path == f_path)
|
||||
if imgui.selectable(f_path, is_selected)[0]:
|
||||
app._inject_file_path = f_path
|
||||
app.controller._update_inject_preview()
|
||||
imgui.end_child()
|
||||
imgui.separator()
|
||||
if imgui.radio_button("Skeleton", app._inject_mode == "skeleton"):
|
||||
app._inject_mode = "skeleton"
|
||||
app.controller._update_inject_preview()
|
||||
imgui.same_line()
|
||||
if imgui.radio_button("Full", app._inject_mode == "full"):
|
||||
app._inject_mode = "full"
|
||||
app.controller._update_inject_preview()
|
||||
imgui.separator()
|
||||
imgui.text("Preview:")
|
||||
imgui.begin_child("inject_preview_area", imgui.ImVec2(600, 300), True)
|
||||
imgui.text_unformatted(app._inject_preview)
|
||||
imgui.end_child()
|
||||
imgui.separator()
|
||||
if imgui.button("Inject", imgui.ImVec2(120, 0)):
|
||||
formatted = f"## File: {app._inject_file_path}\n```python\n{app._inject_preview}\n```\n"
|
||||
with app._disc_entries_lock:
|
||||
app.disc_entries.append({
|
||||
"role": "Context",
|
||||
"content": formatted,
|
||||
"collapsed": True,
|
||||
"ts": project_manager.now_ts()
|
||||
})
|
||||
app._scroll_disc_to_bottom = True
|
||||
imgui.close_current_popup()
|
||||
imgui.same_line()
|
||||
if imgui.button("Cancel", imgui.ImVec2(120, 0)):
|
||||
imgui.close_current_popup()
|
||||
imgui.end_popup()
|
||||
#endregion: Inject File Modal
|
||||
|
||||
return
|
||||
imgui.end()
|
||||
|
||||
def render_patch_modal(app: App) -> None:
|
||||
if not app._show_patch_modal:
|
||||
|
||||
Reference in New Issue
Block a user