fix(gui): use imscope context manager for prior session tint in _gui_func
Convert manual push_style_color / pop_style_color in _gui_func to use the imscope context manager so the pop is exception-safe via Python's with statement. Manual push/pop can desync if render_main_interface raises mid-render, causing 'PopStyleColor() too many times!' imGui assertion on subsequent frames. The try/except around render_main_interface was already there but the pop was outside it, so the pop count could exceed the push count when an exception short-circuited the render.
This commit is contained in:
+7
-9
@@ -781,22 +781,20 @@ class App:
|
|||||||
theme.render_post_fx(ws.x, ws.y, self.ai_status, self.ui_crt_filter)
|
theme.render_post_fx(ws.x, ws.y, self.ai_status, self.ui_crt_filter)
|
||||||
|
|
||||||
if self.perf_profiling_enabled: self.perf_monitor.start_component("_gui_func")
|
if self.perf_profiling_enabled: self.perf_monitor.start_component("_gui_func")
|
||||||
if self.is_viewing_prior_session:
|
|
||||||
imgui.push_style_color(imgui.Col_.window_bg, vec4(50, 40, 20))
|
|
||||||
pushed_prior_tint = True
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
render_main_interface(self)
|
if self.is_viewing_prior_session:
|
||||||
|
with imscope.style_color(imgui.Col_.window_bg, vec4(50, 40, 20)):
|
||||||
|
render_main_interface(self)
|
||||||
|
else:
|
||||||
|
render_main_interface(self)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
sys.stderr.write(f"ERROR in _gui_func: {e}\n")
|
sys.stderr.write(f"ERROR in _gui_func: {e}\n")
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
if pushed_prior_tint:
|
|
||||||
imgui.pop_style_color()
|
|
||||||
|
|
||||||
self._handle_history_logic()
|
self._handle_history_logic()
|
||||||
|
|
||||||
if self.perf_profiling_enabled: self.perf_monitor.end_component("_gui_func")
|
if self.perf_profiling_enabled:
|
||||||
|
self.perf_monitor.end_component("_gui_func")
|
||||||
return
|
return
|
||||||
|
|
||||||
def _render_window_if_open(self, name: str, render_func: Callable[[], None], flag_condition: bool = True) -> None:
|
def _render_window_if_open(self, name: str, render_func: Callable[[], None], flag_condition: bool = True) -> None:
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
|
def test_no_extraneous_pop_when_prior_session_renders():
|
||||||
|
"""Reproduces the prior session load pop_style_color imbalance.
|
||||||
|
When loading a prior session log, _gui_func pushes a window_bg color
|
||||||
|
and pops it after render. If render_main_interface itself raises,
|
||||||
|
the manual pop at line 795 still runs and might pop more than was
|
||||||
|
pushed (if the render path's own push was consumed by its own
|
||||||
|
imscope.__exit__ before the exception propagated).
|
||||||
|
"""
|
||||||
|
from src import gui_2
|
||||||
|
app_instance = MagicMock()
|
||||||
|
app_instance.is_viewing_prior_session = True
|
||||||
|
app_instance.perf_profiling_enabled = False
|
||||||
|
app_instance.show_windows = {"Discussion Hub": True, "Log Management": True, "Tool Calls": True, "Message": True, "Operations Hub": True, "Response": True, "Theme": True}
|
||||||
|
app_instance.active_discussion = "main"
|
||||||
|
app_instance.ui_focus_agent = None
|
||||||
|
app_instance._last_ui_focus_agent = None
|
||||||
|
app_instance._comms_log_dirty = True
|
||||||
|
app_instance._tool_log_dirty = True
|
||||||
|
app_instance._comms_log_cache = []
|
||||||
|
app_instance._tool_log_cache = []
|
||||||
|
app_instance.prior_session_entries = []
|
||||||
|
app_instance.prior_tool_calls = []
|
||||||
|
app_instance.prior_disc_entries = [{"role": "User", "content": "test", "collapsed": False, "ts": "t1"}]
|
||||||
|
app_instance.disc_entries = []
|
||||||
|
app_instance.disc_roles = ["User", "AI"]
|
||||||
|
app_instance._get_discussion_names = MagicMock(return_value=["main"])
|
||||||
|
app_instance.project = {"discussion": {"active": "main", "discussions": {"main": {"history": []}}}}
|
||||||
|
push_count = {"n": 0}
|
||||||
|
pop_count = {"n": 0}
|
||||||
|
with patch("src.gui_2.imgui") as mock_imgui, \
|
||||||
|
patch("src.gui_2.imscope") as mock_imscope, \
|
||||||
|
patch("src.gui_2.theme") as mock_theme, \
|
||||||
|
patch("src.gui_2.markdown_helper") as mock_md:
|
||||||
|
def _track_push(*a, **k): push_count["n"] += 1
|
||||||
|
def _track_pop(*a, **k): pop_count["n"] += 1
|
||||||
|
mock_imgui.push_style_color.side_effect = _track_push
|
||||||
|
mock_imgui.pop_style_color.side_effect = _track_pop
|
||||||
|
mock_imgui.begin = MagicMock(return_value=True)
|
||||||
|
mock_imgui.end = MagicMock()
|
||||||
|
mock_imgui.begin_child = MagicMock(return_value=True)
|
||||||
|
mock_imgui.end_child = MagicMock()
|
||||||
|
mock_imgui.begin_tab_bar = MagicMock(return_value=True)
|
||||||
|
mock_imgui.end_tab_bar = MagicMock()
|
||||||
|
mock_imgui.begin_tab_item = MagicMock(return_value=(True, True))
|
||||||
|
mock_imgui.end_tab_item = MagicMock()
|
||||||
|
mock_imgui.button = MagicMock(return_value=False)
|
||||||
|
mock_imgui.text = MagicMock()
|
||||||
|
mock_imgui.text_colored = MagicMock()
|
||||||
|
mock_imgui.separator = MagicMock()
|
||||||
|
mock_imgui.same_line = MagicMock()
|
||||||
|
mock_imgui.spacing = MagicMock()
|
||||||
|
mock_imgui.new_line = MagicMock()
|
||||||
|
mock_imgui.dummy = MagicMock()
|
||||||
|
mock_imgui.indent = MagicMock()
|
||||||
|
mock_imgui.unindent = MagicMock()
|
||||||
|
mock_imgui.push_id = MagicMock()
|
||||||
|
mock_imgui.pop_id = MagicMock()
|
||||||
|
mock_imgui.push_item_width = MagicMock()
|
||||||
|
mock_imgui.pop_item_width = MagicMock()
|
||||||
|
mock_imgui.set_next_item_width = MagicMock()
|
||||||
|
mock_imgui.checkbox = MagicMock(return_value=(False, False))
|
||||||
|
mock_imgui.input_text = MagicMock(return_value=(False, ""))
|
||||||
|
mock_imgui.input_text_multiline = MagicMock(return_value=(False, ""))
|
||||||
|
mock_imgui.input_int = MagicMock(return_value=(False, 0))
|
||||||
|
mock_imgui.drag_int = MagicMock(return_value=(False, 0))
|
||||||
|
mock_imgui.combo = MagicMock(return_value=(False, 0))
|
||||||
|
mock_imgui.begin_list_box = MagicMock(return_value=True)
|
||||||
|
mock_imgui.end_list_box = MagicMock()
|
||||||
|
mock_imgui.listbox = MagicMock(return_value=True)
|
||||||
|
mock_imgui.collapsing_header = MagicMock(return_value=True)
|
||||||
|
mock_imgui.tree_node = MagicMock(return_value=True)
|
||||||
|
mock_imgui.tree_node_ex = MagicMock(return_value=True)
|
||||||
|
mock_imgui.tree_pop = MagicMock()
|
||||||
|
mock_imgui.selectable = MagicMock(return_value=(False, False))
|
||||||
|
mock_imgui.begin_combo = MagicMock(return_value=True)
|
||||||
|
mock_imgui.end_combo = MagicMock()
|
||||||
|
mock_imgui.begin_popup = MagicMock(return_value=True)
|
||||||
|
mock_imgui.end_popup = MagicMock()
|
||||||
|
mock_imgui.open_popup = MagicMock()
|
||||||
|
mock_imgui.close_current_popup = MagicMock()
|
||||||
|
mock_imgui.set_scroll_here_y = MagicMock()
|
||||||
|
mock_imgui.set_tooltip = MagicMock()
|
||||||
|
mock_imgui.is_item_hovered = MagicMock(return_value=False)
|
||||||
|
mock_imgui.is_item_clicked = MagicMock(return_value=False)
|
||||||
|
mock_imgui.is_item_active = MagicMock(return_value=False)
|
||||||
|
mock_imgui.get_io = MagicMock()
|
||||||
|
mock_imgui.get_content_region_avail = MagicMock(return_value=type("P", (), {"x": 800.0, "y": 600.0})())
|
||||||
|
mock_imgui.get_window_size = MagicMock()
|
||||||
|
mock_imgui.get_window_height = MagicMock()
|
||||||
|
mock_imgui.get_text_line_height = MagicMock()
|
||||||
|
mock_imgui.get_cursor_screen_pos = MagicMock()
|
||||||
|
mock_imgui.ImVec2 = lambda *a: ("ImVec2", a)
|
||||||
|
mock_imgui.ImVec4 = lambda *a: ("ImVec4", a)
|
||||||
|
mock_imgui.Col_ = MagicMock()
|
||||||
|
mock_imgui.WindowFlags_ = MagicMock()
|
||||||
|
mock_imgui.TableFlags_ = MagicMock()
|
||||||
|
mock_imgui.TableColumnFlags_ = MagicMock()
|
||||||
|
mock_imgui.SelectableFlags_ = MagicMock()
|
||||||
|
mock_imgui.TreeNodeFlags_ = MagicMock()
|
||||||
|
mock_imgui.Cond_ = MagicMock()
|
||||||
|
mock_imgui.StyleVar_ = MagicMock()
|
||||||
|
mock_imgui.StyleColor_ = MagicMock()
|
||||||
|
mock_imgui.HoveredFlags_ = MagicMock()
|
||||||
|
mock_imgui.Key = MagicMock()
|
||||||
|
mock_imgui.TabItemFlags_ = MagicMock()
|
||||||
|
mock_imgui.ListClipper = MagicMock(return_value=MagicMock(step=MagicMock(side_effect=[True, False]), display_start=0, display_end=1, begin=MagicMock()))
|
||||||
|
def _scope_enter(): pass
|
||||||
|
def _scope_exit(*a): return False
|
||||||
|
for sc in [mock_imscope.style_color, mock_imscope.style_var, mock_imscope.child, mock_imscope.tab_bar, mock_imscope.tab_item, mock_imscope.tree_node_ex, mock_imscope.group, mock_imscope.indent, mock_imscope.id, mock_imscope.text_wrap, mock_imscope.tooltip, mock_imscope.menu, mock_imscope.menu_bar, mock_imscope.popup, mock_imscope.popup_modal, mock_imscope.window, mock_imscope.table]:
|
||||||
|
sc.return_value.__enter__ = MagicMock(side_effect=_scope_enter)
|
||||||
|
sc.return_value.__exit__ = MagicMock(side_effect=_scope_exit)
|
||||||
|
def _push_side_effect(*a, **k): push_count["n"] += 1
|
||||||
|
def _pop_side_effect(*a, **k): pop_count["n"] += 1
|
||||||
|
mock_imscope.style_color.return_value.__enter__.side_effect = _push_side_effect
|
||||||
|
mock_imscope.style_color.return_value.__exit__.side_effect = lambda *a: (pop_count.__setitem__("n", pop_count["n"] + 1) or False)
|
||||||
|
mock_imscope.style_var.return_value.__enter__.side_effect = _push_side_effect
|
||||||
|
mock_imscope.style_var.return_value.__exit__.side_effect = lambda *a: False
|
||||||
|
mock_imscope.text_wrap.return_value.__enter__ = MagicMock()
|
||||||
|
mock_imscope.text_wrap.return_value.__exit__ = MagicMock(side_effect=_scope_exit)
|
||||||
|
mock_theme.ai_text_style.return_value.__enter__ = MagicMock()
|
||||||
|
mock_theme.ai_text_style.return_value.__exit__ = MagicMock(side_effect=_scope_exit)
|
||||||
|
mock_theme.is_nerv_active = MagicMock(return_value=False)
|
||||||
|
mock_theme.get_current_palette = MagicMock(return_value="default")
|
||||||
|
mock_md.render = MagicMock()
|
||||||
|
mock_md.get_renderer = MagicMock()
|
||||||
|
try:
|
||||||
|
gui_2.render_main_interface(app_instance)
|
||||||
|
except Exception as e:
|
||||||
|
import pytest
|
||||||
|
pytest.fail(f"render_main_interface raised: {e}")
|
||||||
|
assert push_count["n"] == pop_count["n"], f"Push/pop imbalance: pushes={push_count['n']}, pops={pop_count['n']}"
|
||||||
Reference in New Issue
Block a user