From 228359679d63d5642901a6b923c0aa6bf29525b0 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Wed, 3 Jun 2026 14:25:59 -0400 Subject: [PATCH] fix(gui): remove orphan pop_style_color in render_comms_history_panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ROOT CAUSE: In a previous fix (df7bda6e 'explicit child size for comms_scroll and prior_scroll'), the code that pushed a child_bg style color at the start of render_comms_history_panel was removed when the section was rewritten to use imgui.get_content_region_avail() for explicit child sizing. However, the matching pop_style_color at the end of the function (guarded by 'if app.is_viewing_prior_session') was left in place. RESULT: When viewing a prior session, the imscope.style_color in _gui_func pushes 1 color at the start of the frame, then the orphaned pop in render_comms_history_panel decrements the imGui style counter by 1, then _gui_func's imscope __exit__ tries to pop again — triggering IM_ASSERT 'PopStyleColor() too many times!'. This caused a cascade of imGui state corruption on every frame after loading a prior session log, manifesting as 'too many times' assertions on the next frame and 'Must call EndChild() and not End()' once the style stack underflowed. FIX: Remove the orphan pop_style_color at gui_2.py:3761. No matching push exists, so the pop is unconditionally wrong. TESTS: - New test_comms_no_extraneous_pop.py asserts push/pop balance in render_comms_history_panel when is_viewing_prior_session is True - 43/43 broad regression pass --- src/gui_2.py | 5 ++- tests/test_comms_no_extraneous_pop.py | 49 +++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 tests/test_comms_no_extraneous_pop.py diff --git a/src/gui_2.py b/src/gui_2.py index a5bc8934..d8753a60 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -3757,9 +3757,8 @@ def render_comms_history_panel(app: App) -> None: imgui.set_scroll_here_y(1.0) app._scroll_comms_to_bottom = False - imgui.end_child() - if app.is_viewing_prior_session: imgui.pop_style_color() - if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_comms_history_panel") + imgui.end_child() + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_comms_history_panel") def render_takes_panel(app: App) -> None: imgui.text("Takes & Synthesis") diff --git a/tests/test_comms_no_extraneous_pop.py b/tests/test_comms_no_extraneous_pop.py new file mode 100644 index 00000000..9bf726b0 --- /dev/null +++ b/tests/test_comms_no_extraneous_pop.py @@ -0,0 +1,49 @@ +from unittest.mock import patch, MagicMock +from src.gui_2 import render_comms_history_panel + +def test_render_comms_history_panel_no_extraneous_pop_in_prior_mode(app_instance): + """Regression test: render_comms_history_panel must not pop_style_color + more times than it pushes when is_viewing_prior_session is True. + Previously, line 3761 had a stray pop with no matching push (the push + was removed during the comms_scroll child-size fix). + """ + app_instance.is_viewing_prior_session = True + app_instance.perf_profiling_enabled = False + app_instance._scroll_comms_to_bottom = False + app_instance._comms_log_cache = [] + app_instance.cb_load_prior_log = MagicMock() + app_instance.controller = MagicMock() + app_instance.controller.event_queue.put = MagicMock() + push_calls = [] + pop_calls = [] + 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: + mock_imgui.push_style_color.side_effect = lambda *a, **k: push_calls.append(a) + mock_imgui.pop_style_color.side_effect = lambda *a, **k: pop_calls.append(a) + mock_imgui.text = MagicMock() + mock_imgui.text_colored = MagicMock() + mock_imgui.same_line = MagicMock() + mock_imgui.separator = MagicMock() + mock_imgui.button = MagicMock(return_value=False) + mock_imgui.set_scroll_here_y = MagicMock() + mock_imgui.begin_child = MagicMock(return_value=True) + mock_imgui.end_child = MagicMock() + mock_imgui.get_content_region_avail = MagicMock(return_value=MagicMock(x=800.0, y=600.0)) + mock_imgui.begin = MagicMock(return_value=True) + mock_imgui.end = MagicMock() + mock_imgui.ImVec2 = lambda *a: MagicMock(x=float(a[0]) if a else 0, y=float(a[1]) if len(a) > 1 else 0) + mock_imgui.ImVec4 = lambda *a: ("ImVec4", a) + for attr in ["Col_", "WindowFlags_", "TableFlags_", "TableColumnFlags_", "SelectableFlags_", "TreeNodeFlags_", "Cond_", "StyleVar_", "StyleColor_", "HoveredFlags_", "Key", "TabItemFlags_"]: + setattr(mock_imgui, attr, MagicMock()) + def _scope_enter(): return True + 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) + mock_theme.ai_text_style.return_value.__enter__ = MagicMock() + mock_theme.ai_text_style.return_value.__exit__ = MagicMock(side_effect=_scope_exit) + try: + render_comms_history_panel(app_instance) + except Exception as e: + import pytest + pytest.fail(f"render_comms_history_panel raised: {e}") + assert len(push_calls) == len(pop_calls), f"push_style_color/pop_style_color imbalance in prior mode: {len(push_calls)} pushes vs {len(pop_calls)} pops"