Private
Public Access
fix(gui): use imscope.child for comms_scroll (was inside conditional, leaving child open)
ROOT CAUSE: render_comms_history_panel had imgui.end_child() nested INSIDE an 'if app._scroll_comms_to_bottom:' block at line 3758. When _scroll_comms_to_bottom was False (the common case), end_child was NOT called, leaving the comms_scroll child window open. This caused the imGui state to corrupt: tab_item.end_tab_item, tab_bar.end_tab_bar, and the outer window.end all saw that the child was still open (WithinEndChildID was set), triggering 'Must call EndChild() and not End()!' assertion. FIX: Convert the entire comms_scroll block to imscope.child (which uses Python's with statement for exception-safe end_child). The scroll-to-bottom logic is now correctly nested INSIDE the with block, and there's no manual end_child to forget. Tests: - Updated test_comms_scroll_no_clipping.py to check imscope.child instead of begin_child - 28/28 broad regression pass
This commit is contained in:
+1
-3
@@ -3676,7 +3676,7 @@ def render_comms_history_panel(app: App) -> None:
|
|||||||
imgui.separator()
|
imgui.separator()
|
||||||
|
|
||||||
avail = imgui.get_content_region_avail()
|
avail = imgui.get_content_region_avail()
|
||||||
imgui.begin_child("comms_scroll", imgui.ImVec2(avail.x, avail.y), False, imgui.WindowFlags_.horizontal_scrollbar)
|
with imscope.child("comms_scroll", imgui.ImVec2(avail.x, avail.y), imgui.WindowFlags_.horizontal_scrollbar):
|
||||||
log_to_render = app._comms_log_cache
|
log_to_render = app._comms_log_cache
|
||||||
|
|
||||||
for i, entry in enumerate(log_to_render):
|
for i, entry in enumerate(log_to_render):
|
||||||
@@ -3748,7 +3748,6 @@ def render_comms_history_panel(app: App) -> None:
|
|||||||
elif kind == "tool_call": render_heavy_text(app, payload.get("name", "call"), payload.get("script") or json.dumps(payload.get("args", {}), indent=1), idx_str)
|
elif kind == "tool_call": render_heavy_text(app, payload.get("name", "call"), payload.get("script") or json.dumps(payload.get("args", {}), indent=1), idx_str)
|
||||||
elif kind == "tool_result": render_heavy_text(app, payload.get("name", "result"), payload.get("output", ""), idx_str)
|
elif kind == "tool_result": render_heavy_text(app, payload.get("name", "result"), payload.get("output", ""), idx_str)
|
||||||
else: render_heavy_text(app, "data", str(payload), idx_str)
|
else: render_heavy_text(app, "data", str(payload), idx_str)
|
||||||
|
|
||||||
imgui.separator()
|
imgui.separator()
|
||||||
imgui.pop_id()
|
imgui.pop_id()
|
||||||
|
|
||||||
@@ -3756,7 +3755,6 @@ def render_comms_history_panel(app: App) -> None:
|
|||||||
imgui.set_scroll_here_y(1.0)
|
imgui.set_scroll_here_y(1.0)
|
||||||
app._scroll_comms_to_bottom = False
|
app._scroll_comms_to_bottom = False
|
||||||
|
|
||||||
imgui.end_child()
|
|
||||||
if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_comms_history_panel")
|
if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_comms_history_panel")
|
||||||
|
|
||||||
def render_takes_panel(app: App) -> None:
|
def render_takes_panel(app: App) -> None:
|
||||||
|
|||||||
@@ -18,8 +18,10 @@ def test_comms_history_renders_all_entries_not_just_early_subset(app_instance):
|
|||||||
mock_imgui.push_style_color = MagicMock()
|
mock_imgui.push_style_color = MagicMock()
|
||||||
mock_imgui.pop_style_color = MagicMock()
|
mock_imgui.pop_style_color = MagicMock()
|
||||||
mock_imgui.set_scroll_here_y = MagicMock()
|
mock_imgui.set_scroll_here_y = MagicMock()
|
||||||
mock_imgui.get_content_region_avail = MagicMock(return_value=MagicMock(x=800.0, y=600.0))
|
mock_imgui.get_content_region_avail = MagicMock(return_value=type("P", (), {"x": 800.0, "y": 600.0})())
|
||||||
mock_imgui.ImVec2 = lambda *a: ("ImVec2", a)
|
def _imvec2(x, y=0):
|
||||||
|
m = MagicMock(); m.x = float(x); m.y = float(y); return m
|
||||||
|
mock_imgui.ImVec2 = _imvec2
|
||||||
mock_imgui.ImVec4 = lambda *a: ("ImVec4", a)
|
mock_imgui.ImVec4 = lambda *a: ("ImVec4", a)
|
||||||
mock_imscope.child = MagicMock()
|
mock_imscope.child = MagicMock()
|
||||||
mock_imscope.child.return_value.__enter__ = MagicMock()
|
mock_imscope.child.return_value.__enter__ = MagicMock()
|
||||||
@@ -35,14 +37,14 @@ def test_comms_history_renders_all_entries_not_just_early_subset(app_instance):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
import pytest
|
import pytest
|
||||||
pytest.fail(f"render_comms_history_panel raised: {e}")
|
pytest.fail(f"render_comms_history_panel raised: {e}")
|
||||||
comms_calls = [call for call in mock_imgui.begin_child.call_args_list if call[0][0] == "comms_scroll"]
|
comms_calls = [call for call in mock_imscope.child.call_args_list if call[0][0] == "comms_scroll"]
|
||||||
assert len(comms_calls) == 1, "comms_scroll child should be opened once"
|
assert len(comms_calls) == 1, "comms_scroll child should be opened once"
|
||||||
comms_call = next((c for c in mock_imgui.begin_child.call_args_list if c[0][0] == "comms_scroll"), None)
|
comms_call = comms_calls[0]
|
||||||
assert comms_call is not None, "comms_scroll child should be among begin_child calls"
|
|
||||||
args, _ = comms_call
|
args, _ = comms_call
|
||||||
size_arg = args[1]
|
size_arg = args[1] if len(args) > 1 else 0
|
||||||
if isinstance(size_arg, tuple) and len(size_arg) == 2 and isinstance(size_arg[0], str):
|
if hasattr(size_arg, 'x'):
|
||||||
actual = size_arg[1]
|
size_x, size_y = size_arg.x, size_arg.y
|
||||||
else:
|
else:
|
||||||
actual = size_arg
|
size_x = size_arg
|
||||||
assert actual != (0, 0), f"comms_scroll child should use explicit content region size, got {actual}"
|
size_y = 0
|
||||||
|
assert size_x > 0 and size_y > 0, f"comms_scroll child should use explicit content region size, got ({size_x}, {size_y})"
|
||||||
|
|||||||
Reference in New Issue
Block a user