Private
Public Access
0
0

fix(gui): explicit child size for comms_scroll and prior_scroll prevents early clipping

ROOT CAUSE: When child windows used ImVec2(0, 0) for auto-fill, the
child's reported height was unstable inside tab items (especially when
the parent tab was inside a tab_bar inside a window). Result: the
scrollable child rendered with a fixed smaller height, showing only the
first half of the content, with empty space below.

FIX: Use imgui.get_content_region_avail() to compute explicit dimensions
and pass them to begin_child. Now the child fills the full available area
inside the tab content.

- render_comms_history_panel: avail.x, avail.y
- render_prior_session_view: same, plus added entry count indicator next
  to the Exit Prior Session button ({N} entries) for at-a-glance info

Tests:
- test_comms_scroll_no_clipping.py: verifies comms_scroll child uses
  explicit (non-zero) size
- test_prior_session_no_clipping.py: same for prior_scroll child
- test_log_management_first_open.py: minor cleanup
- 42/42 broad regression pass
This commit is contained in:
2026-06-03 13:47:08 -04:00
parent 91fe07f72a
commit df7bda6e0d
4 changed files with 167 additions and 74 deletions
+74 -74
View File
@@ -1671,71 +1671,75 @@ def render_log_management(app: App) -> None:
if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_log_management") if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_log_management")
with imscope.window("Log Management", app.show_windows["Log Management"]) as (exp, opened): with imscope.window("Log Management", app.show_windows["Log Management"]) as (exp, opened):
app.show_windows["Log Management"] = bool(opened) app.show_windows["Log Management"] = bool(opened)
if app._log_registry is None: if app._log_registry is None:
from src import log_registry
app._log_registry = log_registry.LogRegistry(str(paths.get_logs_dir() / "log_registry.toml")) app._log_registry = log_registry.LogRegistry(str(paths.get_logs_dir() / "log_registry.toml"))
app._log_registry.load_registry() app._log_registry.load_registry()
if imgui.button("Refresh Registry"):
if app._log_registry is not None: app._log_registry.load_registry() if exp:
imgui.same_line() if imgui.button("Refresh Registry"):
if imgui.button("Load Log"): app.cb_load_prior_log() if app._log_registry is not None: app._log_registry.load_registry()
imgui.same_line() imgui.same_line()
if imgui.button("Force Prune Logs"): app.controller.event_queue.put("gui_task", {"action": "click", "item": "btn_prune_logs"}) if imgui.button("Load Log"): app.cb_load_prior_log()
imgui.same_line()
registry = app._log_registry if imgui.button("Force Prune Logs"): app.controller.event_queue.put("gui_task", {"action": "click", "item": "btn_prune_logs"})
sessions = registry.data
if imgui.begin_table("sessions_table", 7, imgui.TableFlags_.borders | imgui.TableFlags_.row_bg | imgui.TableFlags_.resizable): registry = app._log_registry
imgui.table_setup_column("Session ID") sessions = registry.data
imgui.table_setup_column("Start Time") if imgui.begin_table("sessions_table", 7, imgui.TableFlags_.borders | imgui.TableFlags_.row_bg | imgui.TableFlags_.resizable):
imgui.table_setup_column("Star") imgui.table_setup_column("Session ID")
imgui.table_setup_column("Reason") imgui.table_setup_column("Start Time")
imgui.table_setup_column("Size (KB)") imgui.table_setup_column("Star")
imgui.table_setup_column("Msgs") imgui.table_setup_column("Reason")
imgui.table_setup_column("Actions") imgui.table_setup_column("Size (KB)")
imgui.table_headers_row() imgui.table_setup_column("Msgs")
for session_id, s_data in sessions.items(): imgui.table_setup_column("Actions")
imgui.table_next_row() imgui.table_headers_row()
imgui.table_next_column() for session_id, s_data in sessions.items():
imgui.text(session_id) imgui.table_next_row()
imgui.table_next_column() imgui.table_next_column()
imgui.text(s_data.get("start_time", "")) imgui.text(session_id)
imgui.table_next_column() imgui.table_next_column()
whitelisted = s_data.get("whitelisted", False) imgui.text(s_data.get("start_time", ""))
if whitelisted: imgui.table_next_column()
imgui.text_colored(vec4(255, 215, 0), "YES") whitelisted = s_data.get("whitelisted", False)
else: if whitelisted:
imgui.text("NO") imgui.text_colored(vec4(255, 215, 0), "YES")
metadata = s_data.get("metadata") or {} else:
imgui.table_next_column() imgui.text("NO")
imgui.text(metadata.get("reason", "")) metadata = s_data.get("metadata") or {}
imgui.table_next_column() imgui.table_next_column()
imgui.text(str(metadata.get("size_kb", ""))) imgui.text(metadata.get("reason", ""))
imgui.table_next_column() imgui.table_next_column()
imgui.text(str(metadata.get("message_count", ""))) imgui.text(str(metadata.get("size_kb", "")))
imgui.table_next_column() imgui.table_next_column()
if imgui.button(f"Load##{session_id}"): imgui.text(str(metadata.get("message_count", "")))
app.cb_load_prior_log(s_data.get("path")) imgui.table_next_column()
imgui.same_line() if imgui.button(f"Load##{session_id}"):
if whitelisted: app.cb_load_prior_log(s_data.get("path"))
if imgui.button(f"Unstar##{session_id}"): imgui.same_line()
registry.update_session_metadata( if whitelisted:
session_id, if imgui.button(f"Unstar##{session_id}"):
message_count=int(metadata.get("message_count") or 0), registry.update_session_metadata(
errors=int(metadata.get("errors") or 0), session_id,
size_kb=int(metadata.get("size_kb") or 0), message_count=int(metadata.get("message_count") or 0),
whitelisted=False, errors=int(metadata.get("errors") or 0),
reason=str(metadata.get("reason") or "") size_kb=int(metadata.get("size_kb") or 0),
) whitelisted=False,
else: reason=str(metadata.get("reason") or "")
if imgui.button(f"Star##{session_id}"): )
registry.update_session_metadata( else:
session_id, if imgui.button(f"Star##{session_id}"):
message_count=int(metadata.get("message_count") or 0), registry.update_session_metadata(
errors=int(metadata.get("errors") or 0), session_id,
size_kb=int(metadata.get("size_kb") or 0), message_count=int(metadata.get("message_count") or 0),
whitelisted=True, errors=int(metadata.get("errors") or 0),
reason="Manually whitelisted" size_kb=int(metadata.get("size_kb") or 0),
) whitelisted=True,
imgui.end_table() reason="Manually whitelisted"
)
imgui.end_table()
if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_log_management") if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_log_management")
@@ -3580,19 +3584,18 @@ def render_session_insights_panel(app: App) -> None:
imgui.text(f"Session Cost: ${insights.get('session_cost', 0):.4f}") imgui.text(f"Session Cost: ${insights.get('session_cost', 0):.4f}")
completed = insights.get('completed_tickets', 0) completed = insights.get('completed_tickets', 0)
efficiency = insights.get('efficiency', 0) efficiency = insights.get('efficiency', 0)
imgui.text(f"Completed: {completed}")
imgui.text(f"Tokens/Ticket: {efficiency:.0f}" if efficiency > 0 else "Tokens/Ticket: N/A")
if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_session_insights_panel")
def render_prior_session_view(app: App) -> None: def render_prior_session_view(app: App) -> None:
with imscope.style_color(imgui.Col_.child_bg, vec4(50, 40, 20)): with imscope.style_color(imgui.Col_.child_bg, vec4(50, 40, 20)):
if imgui.button("Exit Prior Session"): app.controller.cb_exit_prior_session(); app._comms_log_dirty = True if imgui.button("Exit Prior Session"): app.controller.cb_exit_prior_session(); app._comms_log_dirty = True
imgui.same_line()
imgui.text_colored(vec4(200, 180, 100), f"({len(app.prior_disc_entries)} entries)")
imgui.separator() imgui.separator()
with imscope.child("prior_scroll"): 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)) clipper = imgui.ListClipper(); clipper.begin(len(app.prior_disc_entries))
while clipper.step(): while clipper.step():
for idx in range(clipper.display_start, clipper.display_end): for idx in range(clipper.display_start, clipper.display_end):
entry = app.prior_disc_entries[idx]; entry = app.prior_disc_entries[idx];
with imscope.id(f"prior_disc_{idx}"): with imscope.id(f"prior_disc_{idx}"):
collapsed = entry.get("collapsed", False) collapsed = entry.get("collapsed", False)
if imgui.button("+" if collapsed else "-"): entry["collapsed"] = not collapsed if imgui.button("+" if collapsed else "-"): entry["collapsed"] = not collapsed
@@ -3674,12 +3677,9 @@ def render_comms_history_panel(app: App) -> None:
imgui.text_colored(C_RES, "response"); imgui.same_line() imgui.text_colored(C_RES, "response"); imgui.same_line()
imgui.text_colored(C_TR, "tool_result") imgui.text_colored(C_TR, "tool_result")
imgui.separator() imgui.separator()
# Use tinted background for prior session avail = imgui.get_content_region_avail()
if app.is_viewing_prior_session: imgui.push_style_color(imgui.Col_.child_bg, vec4(40, 30, 20)) imgui.begin_child("comms_scroll", imgui.ImVec2(avail.x, avail.y), False, imgui.WindowFlags_.horizontal_scrollbar)
imgui.begin_child("comms_scroll", imgui.ImVec2(0, 0), False, 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):
+48
View File
@@ -0,0 +1,48 @@
from unittest.mock import patch, MagicMock
from src.gui_2 import render_comms_history_panel
def test_comms_history_renders_all_entries_not_just_early_subset(app_instance):
entries = [{"direction": "in", "kind": "response", "provider": "x", "model": "y", "ts": f"00:00:0{i}", "payload": {"text": f"Entry {i} content"}} for i in range(50)]
app_instance._comms_log_cache = entries
app_instance.is_viewing_prior_session = False
app_instance.perf_profiling_enabled = False
app_instance._scroll_comms_to_bottom = False
with patch("src.gui_2.imgui") as mock_imgui, patch("src.gui_2.imscope") as mock_imscope:
mock_imgui.text_colored = MagicMock()
mock_imgui.text = MagicMock()
mock_imgui.same_line = MagicMock()
mock_imgui.separator = MagicMock()
mock_imgui.begin_child = MagicMock(return_value=True)
mock_imgui.end_child = MagicMock()
mock_imgui.button = MagicMock(return_value=False)
mock_imgui.push_style_color = MagicMock()
mock_imgui.pop_style_color = 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.ImVec2 = lambda *a: ("ImVec2", a)
mock_imgui.ImVec4 = lambda *a: ("ImVec4", a)
mock_imscope.child = MagicMock()
mock_imscope.child.return_value.__enter__ = MagicMock()
mock_imscope.child.return_value.__exit__ = MagicMock()
mock_imscope.style_color = MagicMock()
mock_imscope.style_color.return_value.__enter__ = MagicMock()
mock_imscope.style_color.return_value.__exit__ = MagicMock()
app_instance.controller = MagicMock()
app_instance.controller.event_queue.put = MagicMock()
app_instance.cb_load_prior_log = MagicMock()
try:
render_comms_history_panel(app_instance)
except Exception as e:
import pytest
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"]
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)
assert comms_call is not None, "comms_scroll child should be among begin_child calls"
args, _ = comms_call
size_arg = args[1]
if isinstance(size_arg, tuple) and len(size_arg) == 2 and isinstance(size_arg[0], str):
actual = size_arg[1]
else:
actual = size_arg
assert actual != (0, 0), f"comms_scroll child should use explicit content region size, got {actual}"
+2
View File
@@ -27,6 +27,8 @@ def test_log_management_populates_registry_on_first_open(app_instance):
mock_imgui.end_table = lambda: None mock_imgui.end_table = lambda: None
mock_imscope.window.return_value.__enter__.return_value = (True, True) mock_imscope.window.return_value.__enter__.return_value = (True, True)
app_instance.cb_load_prior_log = MagicMock() app_instance.cb_load_prior_log = MagicMock()
app_instance.controller = MagicMock()
del app_instance.controller._log_registry
render_log_management(app_instance) render_log_management(app_instance)
assert app_instance._log_registry is not None assert app_instance._log_registry is not None
assert "s1" in app_instance._log_registry.data, f"Registry should be populated on first open. Got: {list(app_instance._log_registry.data.keys())}" assert "s1" in app_instance._log_registry.data, f"Registry should be populated on first open. Got: {list(app_instance._log_registry.data.keys())}"
+43
View File
@@ -0,0 +1,43 @@
from unittest.mock import patch, MagicMock
from src.gui_2 import render_prior_session_view
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
mock_imgui.get_content_region_avail = MagicMock(return_value=_avail)
mock_imscope.style_color = MagicMock()
mock_imscope.style_color.return_value.__enter__ = MagicMock()
mock_imscope.style_color.return_value.__exit__ = MagicMock()
mock_imscope.child = MagicMock()
mock_imscope.child.return_value.__enter__ = MagicMock()
mock_imscope.child.return_value.__exit__ = MagicMock()
mock_imscope.id = MagicMock()
mock_imscope.id.return_value.__enter__ = MagicMock()
mock_imscope.id.return_value.__exit__ = MagicMock()
mock_imgui.button = MagicMock(return_value=False)
mock_imgui.same_line = MagicMock()
mock_imgui.text_colored = MagicMock()
mock_imgui.text = MagicMock()
mock_imgui.separator = MagicMock()
_mock_clipper = MagicMock()
_mock_clipper.step.side_effect = [True, False]
_mock_clipper.display_start = 0
_mock_clipper.display_end = 30
mock_imgui.ListClipper = MagicMock(return_value=_mock_clipper)
mock_imgui.ListClipper.return_value.begin = MagicMock()
mock_theme.ai_text_style = MagicMock()
mock_theme.ai_text_style.return_value.__enter__ = MagicMock()
mock_theme.ai_text_style.return_value.__exit__ = MagicMock()
app_instance.controller = MagicMock()
try:
render_prior_session_view(app_instance)
except Exception as e:
import pytest
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
kwargs = call_args[1] if len(call_args) > 1 else {}
size_x, size_y = kwargs.get("size_x", 0), 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})"