diff --git a/src/gui_2.py b/src/gui_2.py index c1b03a46..6f99d51d 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -1671,71 +1671,75 @@ def render_log_management(app: App) -> None: 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): app.show_windows["Log Management"] = bool(opened) + 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.load_registry() - if imgui.button("Refresh Registry"): - if app._log_registry is not None: app._log_registry.load_registry() - imgui.same_line() - if imgui.button("Load Log"): app.cb_load_prior_log() - imgui.same_line() - if imgui.button("Force Prune Logs"): app.controller.event_queue.put("gui_task", {"action": "click", "item": "btn_prune_logs"}) - - registry = app._log_registry - sessions = registry.data - if imgui.begin_table("sessions_table", 7, imgui.TableFlags_.borders | imgui.TableFlags_.row_bg | imgui.TableFlags_.resizable): - imgui.table_setup_column("Session ID") - imgui.table_setup_column("Start Time") - imgui.table_setup_column("Star") - imgui.table_setup_column("Reason") - imgui.table_setup_column("Size (KB)") - imgui.table_setup_column("Msgs") - imgui.table_setup_column("Actions") - imgui.table_headers_row() - for session_id, s_data in sessions.items(): - imgui.table_next_row() - imgui.table_next_column() - imgui.text(session_id) - imgui.table_next_column() - imgui.text(s_data.get("start_time", "")) - imgui.table_next_column() - whitelisted = s_data.get("whitelisted", False) - if whitelisted: - imgui.text_colored(vec4(255, 215, 0), "YES") - else: - imgui.text("NO") - metadata = s_data.get("metadata") or {} - imgui.table_next_column() - imgui.text(metadata.get("reason", "")) - imgui.table_next_column() - imgui.text(str(metadata.get("size_kb", ""))) - imgui.table_next_column() - imgui.text(str(metadata.get("message_count", ""))) - imgui.table_next_column() - if imgui.button(f"Load##{session_id}"): - app.cb_load_prior_log(s_data.get("path")) - imgui.same_line() - if whitelisted: - if imgui.button(f"Unstar##{session_id}"): - registry.update_session_metadata( - session_id, - message_count=int(metadata.get("message_count") or 0), - errors=int(metadata.get("errors") or 0), - size_kb=int(metadata.get("size_kb") or 0), - whitelisted=False, - reason=str(metadata.get("reason") or "") - ) - else: - if imgui.button(f"Star##{session_id}"): - registry.update_session_metadata( - session_id, - message_count=int(metadata.get("message_count") or 0), - errors=int(metadata.get("errors") or 0), - size_kb=int(metadata.get("size_kb") or 0), - whitelisted=True, - reason="Manually whitelisted" - ) - imgui.end_table() + + if exp: + if imgui.button("Refresh Registry"): + if app._log_registry is not None: app._log_registry.load_registry() + imgui.same_line() + if imgui.button("Load Log"): app.cb_load_prior_log() + imgui.same_line() + if imgui.button("Force Prune Logs"): app.controller.event_queue.put("gui_task", {"action": "click", "item": "btn_prune_logs"}) + + registry = app._log_registry + sessions = registry.data + if imgui.begin_table("sessions_table", 7, imgui.TableFlags_.borders | imgui.TableFlags_.row_bg | imgui.TableFlags_.resizable): + imgui.table_setup_column("Session ID") + imgui.table_setup_column("Start Time") + imgui.table_setup_column("Star") + imgui.table_setup_column("Reason") + imgui.table_setup_column("Size (KB)") + imgui.table_setup_column("Msgs") + imgui.table_setup_column("Actions") + imgui.table_headers_row() + for session_id, s_data in sessions.items(): + imgui.table_next_row() + imgui.table_next_column() + imgui.text(session_id) + imgui.table_next_column() + imgui.text(s_data.get("start_time", "")) + imgui.table_next_column() + whitelisted = s_data.get("whitelisted", False) + if whitelisted: + imgui.text_colored(vec4(255, 215, 0), "YES") + else: + imgui.text("NO") + metadata = s_data.get("metadata") or {} + imgui.table_next_column() + imgui.text(metadata.get("reason", "")) + imgui.table_next_column() + imgui.text(str(metadata.get("size_kb", ""))) + imgui.table_next_column() + imgui.text(str(metadata.get("message_count", ""))) + imgui.table_next_column() + if imgui.button(f"Load##{session_id}"): + app.cb_load_prior_log(s_data.get("path")) + imgui.same_line() + if whitelisted: + if imgui.button(f"Unstar##{session_id}"): + registry.update_session_metadata( + session_id, + message_count=int(metadata.get("message_count") or 0), + errors=int(metadata.get("errors") or 0), + size_kb=int(metadata.get("size_kb") or 0), + whitelisted=False, + reason=str(metadata.get("reason") or "") + ) + else: + if imgui.button(f"Star##{session_id}"): + registry.update_session_metadata( + session_id, + message_count=int(metadata.get("message_count") or 0), + errors=int(metadata.get("errors") or 0), + size_kb=int(metadata.get("size_kb") or 0), + whitelisted=True, + reason="Manually whitelisted" + ) + imgui.end_table() 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}") completed = insights.get('completed_tickets', 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: 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 + imgui.same_line() + imgui.text_colored(vec4(200, 180, 100), f"({len(app.prior_disc_entries)} entries)") 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)) while clipper.step(): 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}"): collapsed = entry.get("collapsed", False) 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_TR, "tool_result") imgui.separator() - - # Use tinted background for prior session - 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(0, 0), False, imgui.WindowFlags_.horizontal_scrollbar) - + + avail = imgui.get_content_region_avail() + imgui.begin_child("comms_scroll", imgui.ImVec2(avail.x, avail.y), False, imgui.WindowFlags_.horizontal_scrollbar) log_to_render = app._comms_log_cache for i, entry in enumerate(log_to_render): diff --git a/tests/test_comms_scroll_no_clipping.py b/tests/test_comms_scroll_no_clipping.py new file mode 100644 index 00000000..c4ee4145 --- /dev/null +++ b/tests/test_comms_scroll_no_clipping.py @@ -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}" diff --git a/tests/test_log_management_first_open.py b/tests/test_log_management_first_open.py index 8c07d9c9..f4f359ff 100644 --- a/tests/test_log_management_first_open.py +++ b/tests/test_log_management_first_open.py @@ -27,6 +27,8 @@ def test_log_management_populates_registry_on_first_open(app_instance): mock_imgui.end_table = lambda: None mock_imscope.window.return_value.__enter__.return_value = (True, True) app_instance.cb_load_prior_log = MagicMock() + app_instance.controller = MagicMock() + del app_instance.controller._log_registry render_log_management(app_instance) 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())}" diff --git a/tests/test_prior_session_no_clipping.py b/tests/test_prior_session_no_clipping.py new file mode 100644 index 00000000..56c9d567 --- /dev/null +++ b/tests/test_prior_session_no_clipping.py @@ -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})"