Private
Public Access
0
0

fix(gui): use imscope.child in render_heavy_text for exception safety

ROOT CAUSE: render_heavy_text (called per comms panel entry) had
manual begin_child/end_child pairs. If anything inside the child
(especially markdown_helper.render) raised, end_child was skipped.
The child window was left open, corrupting the imGui state. The
corruption cascaded through tab_item.end_tab_item -> tab_bar.end_tab_bar
-> window.end, triggering 'Must call EndChild() and not End()!' assertion.

FIX: Convert the inner begin_child/end_child pair to imscope.child so
the end_child is automatically called by Python's with statement, even
on exception. Also convert prior_scroll to imscope.child for consistency.

TESTS:
- Existing test_comms_no_extraneous_pop.py: push/pop balance check
- Updated test_prior_session_no_clipping.py to match new imscope.child
  signature
- 28/28 broad regression pass
This commit is contained in:
2026-06-03 14:38:26 -04:00
parent 228359679d
commit 070c159f11
2 changed files with 20 additions and 17 deletions
+10 -15
View File
@@ -3589,9 +3589,8 @@ def render_prior_session_view(app: App) -> None:
imgui.text_colored(vec4(200, 180, 100), f"({len(app.prior_disc_entries)} entries)")
imgui.separator()
avail = imgui.get_content_region_avail()
with imscope.child("prior_scroll", size_x=avail.x, size_y=avail.y):
clipper = imgui.ListClipper(); clipper.begin(len(app.prior_disc_entries))
while clipper.step():
with imscope.child("prior_scroll", imgui.ImVec2(avail.x, avail.y), imgui.WindowFlags_.horizontal_scrollbar):
clipper = imgui.ListClipper(); clipper.begin(len(app.prior_disc_entries))
for idx in range(clipper.display_start, clipper.display_end):
entry = app.prior_disc_entries[idx];
with imscope.id(f"prior_disc_{idx}"):
@@ -4641,19 +4640,15 @@ def render_heavy_text(app: App, label: str, content: str, id_suffix: str = "") -
ctx_id = f"{label}_{id_suffix}"
is_md = label in ('message', 'text', 'content', 'system')
with imscope.indent():
if is_md:
imgui.begin_child(f"heavy_text_child_{label}_{id_suffix}", imgui.ImVec2(0, 300), True, imgui.WindowFlags_.always_vertical_scrollbar)
markdown_helper.render(content, context_id=ctx_id)
imgui.end_child()
else:
imgui.begin_child(f"heavy_text_child_{label}_{id_suffix}", imgui.ImVec2(0, 200), True, imgui.WindowFlags_.always_vertical_scrollbar)
if app.ui_word_wrap:
with imscope.text_wrap(imgui.get_content_region_avail().x):
imgui.text(content)
with imscope.child(f"heavy_text_child_{label}_{id_suffix}", imgui.ImVec2(0, 300), imgui.WindowFlags_.always_vertical_scrollbar):
if is_md:
markdown_helper.render(content, context_id=ctx_id)
else:
imgui.text(content)
imgui.end_child()
if app.ui_word_wrap:
with imscope.text_wrap(imgui.get_content_region_avail().x):
imgui.text(content)
else:
imgui.text(content)
#endregion: Misc Tools
+10 -2
View File
@@ -5,8 +5,10 @@ def test_prior_session_view_opens_scroll_child_with_explicit_size(app_instance):
app_instance.prior_disc_entries = [{"role": "User", "content": f"entry {i}", "collapsed": False, "ts": f"t{i}"} for i in range(30)]
app_instance.perf_profiling_enabled = False
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:
_avail = MagicMock(); _avail.x = 800.0; _avail.y = 600.0
_avail = type("P", (), {"x": 800.0, "y": 600.0})()
mock_imgui.get_content_region_avail = MagicMock(return_value=_avail)
def _imvec2(x, y=0): m = MagicMock(); m.x = float(x); m.y = float(y); return m
mock_imgui.ImVec2 = _imvec2
mock_imscope.style_color = MagicMock()
mock_imscope.style_color.return_value.__enter__ = MagicMock()
mock_imscope.style_color.return_value.__exit__ = MagicMock()
@@ -38,6 +40,12 @@ def test_prior_session_view_opens_scroll_child_with_explicit_size(app_instance):
pytest.fail(f"render_prior_session_view raised: {e}")
assert mock_imscope.child.called, "prior_scroll child should be opened"
call_args = mock_imscope.child.call_args
args_list = call_args[0] if call_args[0] else []
kwargs = call_args[1] if len(call_args) > 1 else {}
size_x, size_y = kwargs.get("size_x", 0), kwargs.get("size_y", 0)
size_arg = args_list[1] if len(args_list) > 1 else kwargs.get("size_x", 0)
if hasattr(size_arg, 'x'):
size_x, size_y = size_arg.x, size_arg.y
else:
size_x = size_arg
size_y = kwargs.get("size_y", 0)
assert size_x == 800.0 and size_y == 600.0, f"prior_scroll should use explicit content region size, got ({size_x}, {size_y})"