fix(gui): Force newline in discussion entries to prevent squashed layout
- Insert imgui.new_line() before rendering discussion content. - Ensures the Markdown renderer inherits the full horizontal width of the panel. - Definitively fixes vertical squashing of tables and long text blocks.
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Ensure project root is in path so we can import src.gui_2
|
||||
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
||||
if project_root not in sys.path:
|
||||
sys.path.insert(0, project_root)
|
||||
|
||||
class TestMarkdownTableWidth(unittest.TestCase):
|
||||
def test_render_discussion_entry_full_width(self):
|
||||
"""
|
||||
Verify that render_discussion_entry calls imgui.dummy with the full available width.
|
||||
"""
|
||||
# Mock all dependencies to avoid side effects and complex setup during import/execution
|
||||
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, \
|
||||
patch('src.gui_2.project_manager') as mock_pm, \
|
||||
patch('src.gui_2.render_thinking_trace') as mock_rtt, \
|
||||
patch('src.gui_2.render_discussion_entry_read_mode') as mock_rderm:
|
||||
|
||||
# 1. Setup available width and coordinates
|
||||
expected_width = 850.0
|
||||
mock_avail = MagicMock()
|
||||
mock_avail.x = expected_width
|
||||
mock_imgui.get_content_region_avail.return_value = mock_avail
|
||||
|
||||
# Mock ImVec2 to return a simple tuple for easier assertion
|
||||
mock_imgui.ImVec2.side_effect = lambda x, y: (x, y)
|
||||
|
||||
# 3. Mock app and entry state
|
||||
mock_app = MagicMock()
|
||||
mock_app.disc_roles = ["User", "Assistant"]
|
||||
|
||||
entry = {
|
||||
"role": "User",
|
||||
"content": "Hello world",
|
||||
"collapsed": False,
|
||||
"read_mode": False
|
||||
}
|
||||
|
||||
# Mock interactive elements
|
||||
mock_imgui.begin_combo.return_value = False
|
||||
mock_imgui.button.return_value = False
|
||||
mock_imgui.input_text_multiline.return_value = (False, entry["content"])
|
||||
|
||||
# 4. Import the function within the patch context
|
||||
from src.gui_2 import render_discussion_entry
|
||||
|
||||
# 5. Execute the function
|
||||
render_discussion_entry(mock_app, entry, 0)
|
||||
|
||||
# 6. Verification
|
||||
# The function should call imgui.dummy(imgui.ImVec2(full_width, 0))
|
||||
mock_imgui.dummy.assert_any_call((expected_width, 0.0))
|
||||
|
||||
# CRITICAL: Verify newline or spacing is called to prevent squashing
|
||||
# We expect this to fail currently
|
||||
assert mock_imgui.new_line.called or mock_imgui.spacing.called
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,33 @@
|
||||
import inspect
|
||||
import sys
|
||||
import os
|
||||
import pytest
|
||||
|
||||
# Ensure project root is in path
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
||||
|
||||
def test_gui_monolithic_symbols():
|
||||
try:
|
||||
from src.gui_2 import App, render_discussion_entry, render_thinking_trace
|
||||
import src.gui_2
|
||||
except ImportError as e:
|
||||
pytest.fail(f"FAILURE: Could not import from src.gui_2: {e}")
|
||||
|
||||
# Verify App is importable
|
||||
assert App is not None
|
||||
|
||||
# Verify render_discussion_entry is in src.gui_2
|
||||
assert hasattr(src.gui_2, 'render_discussion_entry'), "render_discussion_entry missing from src.gui_2"
|
||||
|
||||
# Verify it's defined in src.gui_2, not imported
|
||||
mod = inspect.getmodule(render_discussion_entry)
|
||||
assert mod is not None, "Could not determine module for render_discussion_entry"
|
||||
assert mod.__name__ == 'src.gui_2', f"render_discussion_entry expected in src.gui_2, but found in {mod.__name__}"
|
||||
|
||||
# Verify render_thinking_trace is in src.gui_2
|
||||
assert hasattr(src.gui_2, 'render_thinking_trace'), "render_thinking_trace missing from src.gui_2"
|
||||
|
||||
# Verify it's defined in src.gui_2, not imported
|
||||
mod = inspect.getmodule(render_thinking_trace)
|
||||
assert mod is not None, "Could not determine module for render_thinking_trace"
|
||||
assert mod.__name__ == 'src.gui_2', f"render_thinking_trace expected in src.gui_2, but found in {mod.__name__}"
|
||||
@@ -0,0 +1,29 @@
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
from src.imgui_scopes import _ScopeId
|
||||
import src.imgui_scopes as imgui_scopes
|
||||
|
||||
def test_scope_id_string():
|
||||
with patch('src.imgui_scopes.imgui') as mock_imgui:
|
||||
sid = _ScopeId("test_id")
|
||||
with sid:
|
||||
pass
|
||||
mock_imgui.push_id.assert_called_once_with("test_id")
|
||||
mock_imgui.pop_id.assert_called_once()
|
||||
|
||||
def test_scope_id_int():
|
||||
with patch('src.imgui_scopes.imgui') as mock_imgui:
|
||||
# Python type hint is str, but we test runtime resilience
|
||||
sid = _ScopeId(1234)
|
||||
with sid:
|
||||
pass
|
||||
# Verify it was converted to string to prevent low-level crashes
|
||||
mock_imgui.push_id.assert_called_once_with("1234")
|
||||
mock_imgui.pop_id.assert_called_once()
|
||||
|
||||
def test_id_helper_function():
|
||||
with patch('src.imgui_scopes.imgui') as mock_imgui:
|
||||
with imgui_scopes.id(42):
|
||||
pass
|
||||
mock_imgui.push_id.assert_called_once_with("42")
|
||||
mock_imgui.pop_id.assert_called_once()
|
||||
@@ -1,7 +1,7 @@
|
||||
# Implementation Plan: Phase 7 Monolithic Stabilization
|
||||
|
||||
## Phase 1: Architecture Consolidation
|
||||
- [ ] Task: Restore Monolithic Rendering
|
||||
- [~] Task: Restore Monolithic Rendering
|
||||
- [ ] WHERE: `src/gui_2.py`
|
||||
- [ ] WHAT: Move `render_discussion_entry` and related functions from `src/discussion_entry_renderer.py` back to `src/gui_2.py`.
|
||||
- [ ] HOW: Use `py_update_definition` for surgical insertion. Remove `src/discussion_entry_renderer.py` afterwards.
|
||||
|
||||
+23
-23
@@ -44,20 +44,20 @@ Collapsed=0
|
||||
DockId=0x00000010,0
|
||||
|
||||
[Window][Message]
|
||||
Pos=1144,28
|
||||
Size=1769,1701
|
||||
Pos=1312,28
|
||||
Size=1613,1908
|
||||
Collapsed=0
|
||||
DockId=0x00000006,0
|
||||
|
||||
[Window][Response]
|
||||
Pos=0,28
|
||||
Size=1142,1701
|
||||
Size=1310,1908
|
||||
Collapsed=0
|
||||
DockId=0x00000010,5
|
||||
|
||||
[Window][Tool Calls]
|
||||
Pos=1144,28
|
||||
Size=1769,1701
|
||||
Pos=1312,28
|
||||
Size=1613,1908
|
||||
Collapsed=0
|
||||
DockId=0x00000006,3
|
||||
|
||||
@@ -77,9 +77,9 @@ DockId=0xAFC85805,2
|
||||
|
||||
[Window][Theme]
|
||||
Pos=0,28
|
||||
Size=1142,1701
|
||||
Size=1320,1684
|
||||
Collapsed=0
|
||||
DockId=0x00000010,3
|
||||
DockId=0x00000010,0
|
||||
|
||||
[Window][Text Viewer - Entry #7]
|
||||
Pos=379,324
|
||||
@@ -105,28 +105,28 @@ Collapsed=0
|
||||
DockId=0x0000000D,0
|
||||
|
||||
[Window][Discussion Hub]
|
||||
Pos=1144,28
|
||||
Size=1769,1701
|
||||
Pos=1322,28
|
||||
Size=1510,1684
|
||||
Collapsed=0
|
||||
DockId=0x00000006,1
|
||||
DockId=0x00000006,0
|
||||
|
||||
[Window][Operations Hub]
|
||||
Pos=0,28
|
||||
Size=1142,1701
|
||||
Size=1310,1908
|
||||
Collapsed=0
|
||||
DockId=0x00000010,4
|
||||
|
||||
[Window][Files & Media]
|
||||
Pos=0,28
|
||||
Size=1142,1701
|
||||
Size=1320,1684
|
||||
Collapsed=0
|
||||
DockId=0x00000010,2
|
||||
DockId=0x00000010,3
|
||||
|
||||
[Window][AI Settings]
|
||||
Pos=0,28
|
||||
Size=1142,1701
|
||||
Size=1320,1684
|
||||
Collapsed=0
|
||||
DockId=0x00000010,1
|
||||
DockId=0x00000010,2
|
||||
|
||||
[Window][Approve Tool Execution]
|
||||
Pos=3,524
|
||||
@@ -140,8 +140,8 @@ Collapsed=0
|
||||
DockId=0x00000006,2
|
||||
|
||||
[Window][Log Management]
|
||||
Pos=1144,28
|
||||
Size=1769,1701
|
||||
Pos=1312,28
|
||||
Size=1613,1908
|
||||
Collapsed=0
|
||||
DockId=0x00000006,2
|
||||
|
||||
@@ -410,9 +410,9 @@ DockId=0x00000006,1
|
||||
|
||||
[Window][Project Settings]
|
||||
Pos=0,28
|
||||
Size=1142,1701
|
||||
Size=1320,1684
|
||||
Collapsed=0
|
||||
DockId=0x00000010,0
|
||||
DockId=0x00000010,1
|
||||
|
||||
[Window][Undo/Redo History]
|
||||
Pos=1529,28
|
||||
@@ -688,13 +688,13 @@ Column 1 Weight=1.0000
|
||||
DockNode ID=0x00000008 Pos=3125,170 Size=593,1157 Split=Y
|
||||
DockNode ID=0x00000009 Parent=0x00000008 SizeRef=1029,147 Selected=0x0469CA7A
|
||||
DockNode ID=0x0000000A Parent=0x00000008 SizeRef=1029,145 Selected=0xDF822E02
|
||||
DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,28 Size=2913,1701 Split=X
|
||||
DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,28 Size=2832,1684 Split=X
|
||||
DockNode ID=0x00000003 Parent=0xAFC85805 SizeRef=2357,1183 Split=X
|
||||
DockNode ID=0x0000000B Parent=0x00000003 SizeRef=404,1186 Split=X Selected=0xF4139CA2
|
||||
DockNode ID=0x00000005 Parent=0x0000000B SizeRef=1142,1681 Split=Y Selected=0x3F1379AF
|
||||
DockNode ID=0x00000010 Parent=0x00000005 SizeRef=983,1140 CentralNode=1 Selected=0x0D5A5273
|
||||
DockNode ID=0x00000005 Parent=0x0000000B SizeRef=1320,1681 Split=Y Selected=0x3F1379AF
|
||||
DockNode ID=0x00000010 Parent=0x00000005 SizeRef=983,1140 CentralNode=1 Selected=0x7BD57D6A
|
||||
DockNode ID=0x00000011 Parent=0x00000005 SizeRef=983,184 Selected=0x432BAE4E
|
||||
DockNode ID=0x00000006 Parent=0x0000000B SizeRef=1769,1681 Selected=0x66CFB56E
|
||||
DockNode ID=0x00000006 Parent=0x0000000B SizeRef=1510,1681 Selected=0x6F2B5B04
|
||||
DockNode ID=0x0000000D Parent=0x00000003 SizeRef=435,1186 Selected=0x363E93D6
|
||||
DockNode ID=0x00000004 Parent=0xAFC85805 SizeRef=488,1183 Selected=0x3AEC3498
|
||||
|
||||
|
||||
+80
-20
@@ -33,9 +33,12 @@ from typing import Optional, Any
|
||||
from src.diff_viewer import apply_patch_to_file
|
||||
from src import ai_client
|
||||
from src import aggregate
|
||||
from src import ai_client
|
||||
from src import aggregate
|
||||
from src import ai_client
|
||||
from src import aggregate
|
||||
from src import api_hooks
|
||||
from src import app_controller
|
||||
from src import ui_shared
|
||||
from src import bg_shader
|
||||
from src import cost_tracker
|
||||
from src import history
|
||||
@@ -72,7 +75,61 @@ def hide_tk_root() -> Tk:
|
||||
root.wm_attributes("-topmost", True)
|
||||
return root
|
||||
|
||||
from src.ui_shared import vec4, C_OUT, C_IN, C_REQ, C_RES, C_TC, C_TR, C_TRS, C_LBL, C_VAL, C_KEY, C_NUM, C_SUB
|
||||
# Standard Color Constants (normalized to 0-1)
|
||||
def vec4(r: float, g: float, b: float, a: float = 1.0) -> imgui.ImVec4:
|
||||
return imgui.ImVec4(r/255.0, g/255.0, b/255.0, a)
|
||||
|
||||
C_OUT: imgui.ImVec4 = vec4(100, 200, 255)
|
||||
C_IN: imgui.ImVec4 = vec4(140, 255, 160)
|
||||
C_REQ: imgui.ImVec4 = vec4(255, 220, 100)
|
||||
C_RES: imgui.ImVec4 = vec4(180, 255, 180)
|
||||
C_TC: imgui.ImVec4 = vec4(255, 180, 80)
|
||||
C_TR: imgui.ImVec4 = vec4(180, 220, 255)
|
||||
C_TRS: imgui.ImVec4 = vec4(200, 180, 255)
|
||||
C_LBL: imgui.ImVec4 = vec4(180, 180, 180)
|
||||
C_VAL: imgui.ImVec4 = vec4(220, 220, 220)
|
||||
C_KEY: imgui.ImVec4 = vec4(140, 200, 255)
|
||||
C_NUM: imgui.ImVec4 = vec4(180, 255, 180)
|
||||
C_TRM: imgui.ImVec4 = vec4(160, 160, 150) # Trimmed/Cruft
|
||||
C_SUB: imgui.ImVec4 = vec4(220, 200, 120)
|
||||
|
||||
DIR_COLORS: dict[str, imgui.ImVec4] = {"OUT": C_OUT, "IN": C_IN}
|
||||
KIND_COLORS: dict[str, imgui.ImVec4] = {"request": C_REQ, "response": C_RES, "tool_call": C_TC, "tool_result": C_TR, "tool_result_send": C_TRS}
|
||||
HEAVY_KEYS: set[str] = {"message", "text", "script", "output", "content"}
|
||||
|
||||
def render_text_viewer(app: App, label: str, content: str, text_type: str = 'text', force_open: bool = False, id_suffix: str = "") -> None:
|
||||
if imgui.button(f"[+]##{id_suffix or str(id(content))}") or force_open:
|
||||
app.text_viewer_type = text_type
|
||||
app.show_windows["Text Viewer"] = True
|
||||
app.text_viewer_title = label
|
||||
app.text_viewer_content = content
|
||||
|
||||
def render_selectable_label(app: App, label: str, value: str, width: float = 0.0, multiline: bool = False, height: float = 0.0, color: Any = None) -> None:
|
||||
with imscope.id(label + str(hash(value))):
|
||||
with imscope.style_color(imgui.Col_.frame_bg, imgui.ImVec4(0, 0, 0, 0)), \
|
||||
imscope.style_var(imgui.StyleVar_.frame_border_size, 0.0):
|
||||
if color:
|
||||
with imscope.style_color(imgui.Col_.text, color):
|
||||
if multiline: _, _ = imgui.input_text_multiline(f"##{label}", value, imgui.ImVec2(width, height), imgui.InputTextFlags_.read_only)
|
||||
else: _, _ = imgui.input_text(f"##{label}", value, imgui.InputTextFlags_.read_only)
|
||||
else:
|
||||
if multiline: _, _ = imgui.input_text_multiline(f"##{label}", value, imgui.ImVec2(width, height), imgui.InputTextFlags_.read_only)
|
||||
else: _, _ = imgui.input_text(f"##{label}", value, imgui.InputTextFlags_.read_only)
|
||||
app.show_windows["Text Viewer"] = True
|
||||
app.text_viewer_title = label
|
||||
app.text_viewer_content = content
|
||||
|
||||
def render_selectable_label(app: App, label: str, value: str, width: float = 0.0, multiline: bool = False, height: float = 0.0, color: Any = None) -> None:
|
||||
with imscope.id(label + str(hash(value))):
|
||||
with imscope.style_color(imgui.Col_.frame_bg, imgui.ImVec4(0, 0, 0, 0)), \
|
||||
imscope.style_var(imgui.StyleVar_.frame_border_size, 0.0):
|
||||
if color:
|
||||
with imscope.style_color(imgui.Col_.text, color):
|
||||
if multiline: _, _ = imgui.input_text_multiline(f"##{label}", value, imgui.ImVec2(width, height), imgui.InputTextFlags_.read_only)
|
||||
else: _, _ = imgui.input_text(f"##{label}", value, imgui.InputTextFlags_.read_only)
|
||||
else:
|
||||
if multiline: _, _ = imgui.input_text_multiline(f"##{label}", value, imgui.ImVec2(width, height), imgui.InputTextFlags_.read_only)
|
||||
else: _, _ = imgui.input_text(f"##{label}", value, imgui.InputTextFlags_.read_only)
|
||||
|
||||
DIR_COLORS: dict[str, imgui.ImVec4] = {"OUT": C_OUT, "IN": C_IN}
|
||||
KIND_COLORS: dict[str, imgui.ImVec4] = {"request": C_REQ, "response": C_RES, "tool_call": C_TC, "tool_result": C_TR, "tool_result_send": C_TRS}
|
||||
@@ -1499,7 +1556,7 @@ def render_token_budget_panel(app: App) -> None:
|
||||
usage = app.session_usage
|
||||
total = usage["input_tokens"] + usage["output_tokens"]
|
||||
if total == 0 and usage.get("total_tokens", 0) > 0: total = usage["total_tokens"]
|
||||
ui_shared.render_selectable_label(app, "session_telemetry_tokens", f"Tokens: {total:,} (In: {usage['input_tokens']:,} Out: {usage['output_tokens']:,})", width=-1, color=C_RES)
|
||||
render_selectable_label(app, "session_telemetry_tokens", f"Tokens: {total:,} (In: {usage['input_tokens']:,} Out: {usage['output_tokens']:,})", width=-1, color=C_RES)
|
||||
if usage.get("last_latency", 0.0) > 0: imgui.text_colored(C_LBL, f" Last Latency: {usage['last_latency']:.2f}s")
|
||||
if usage["cache_read_input_tokens"]: imgui.text_colored(C_LBL, f" Cache Read: {usage['cache_read_input_tokens']:,} Creation: {usage['cache_creation_input_tokens']:,}")
|
||||
if app._gemini_cache_text: imgui.text_colored(C_SUB, app._gemini_cache_text)
|
||||
@@ -1556,13 +1613,13 @@ def render_token_budget_panel(app: App) -> None:
|
||||
tokens = in_t + out_t
|
||||
cost = cost_tracker.estimate_cost(model, in_t, out_t)
|
||||
imgui.table_next_row()
|
||||
imgui.table_set_column_index(0); ui_shared.render_selectable_label(app, f"tier_{tier}", tier, width=-1)
|
||||
imgui.table_set_column_index(1); ui_shared.render_selectable_label(app, f"model_{tier}", model.split("-")[0], width=-1)
|
||||
imgui.table_set_column_index(2); ui_shared.render_selectable_label(app, f"tokens_{tier}", f"{tokens:,}", width=-1)
|
||||
imgui.table_set_column_index(3); ui_shared.render_selectable_label(app, f"cost_{tier}", f"${cost:.4f}", width=-1, color=imgui.ImVec4(0.2, 0.9, 0.2, 1))
|
||||
imgui.table_set_column_index(0); render_selectable_label(app, f"tier_{tier}", tier, width=-1)
|
||||
imgui.table_set_column_index(1); render_selectable_label(app, f"model_{tier}", model.split("-")[0], width=-1)
|
||||
imgui.table_set_column_index(2); render_selectable_label(app, f"tokens_{tier}", f"{tokens:,}", width=-1)
|
||||
imgui.table_set_column_index(3); render_selectable_label(app, f"cost_{tier}", f"${cost:.4f}", width=-1, color=imgui.ImVec4(0.2, 0.9, 0.2, 1))
|
||||
imgui.end_table()
|
||||
tier_total = sum(cost_tracker.estimate_cost(stats.get('model', ''), stats.get('input', 0), stats.get('output', 0)) for stats in app.mma_tier_usage.values())
|
||||
ui_shared.render_selectable_label(app, "session_total_cost", f"Session Total: ${tier_total:.4f}", width=-1, color=imgui.ImVec4(0, 1, 0, 1))
|
||||
render_selectable_label(app, "session_total_cost", f"Session Total: ${tier_total:.4f}", width=-1, color=imgui.ImVec4(0, 1, 0, 1))
|
||||
else:
|
||||
imgui.text_disabled("No MMA tier usage data")
|
||||
if stats.get("would_trim"):
|
||||
@@ -2004,7 +2061,7 @@ def render_provider_panel(app: App) -> None:
|
||||
imgui.text("Gemini CLI")
|
||||
sid = "None"
|
||||
if hasattr(ai_client, "_gemini_cli_adapter") and ai_client._gemini_cli_adapter: sid = ai_client._gemini_cli_adapter.session_id or "None"
|
||||
imgui.text("Session ID:"); imgui.same_line(); ui_shared.render_selectable_label(app, "gemini_cli_sid", sid, width=200)
|
||||
imgui.text("Session ID:"); imgui.same_line(); render_selectable_label(app, "gemini_cli_sid", sid, width=200)
|
||||
if imgui.button("Reset CLI Session"): ai_client.reset_session()
|
||||
imgui.text("Binary Path")
|
||||
ch, app.ui_gemini_cli_path = imgui.input_text("##gcli_path", app.ui_gemini_cli_path)
|
||||
@@ -3131,7 +3188,7 @@ def render_thinking_trace(app: App, entry: dict, segments: list[dict], entry_ind
|
||||
else:
|
||||
imgui.text(content)
|
||||
else:
|
||||
ui_shared.render_selectable_label(app, f"think_text_{entry_index}_{idx}", content, multiline=True, height=-1)
|
||||
render_selectable_label(app, f"think_text_{entry_index}_{idx}", content, multiline=True, height=-1)
|
||||
imgui.separator()
|
||||
|
||||
def render_discussion_entry(app: App, entry: dict, index: int) -> None:
|
||||
@@ -3155,7 +3212,7 @@ def render_discussion_entry(app: App, entry: dict, index: int) -> None:
|
||||
collapsed, read_mode = entry.get("collapsed", False), entry.get("read_mode", False)
|
||||
if imgui.button("+" if collapsed else "-"): entry["collapsed"] = not collapsed
|
||||
imgui.same_line()
|
||||
ui_shared.render_text_viewer(app, f"Entry #{index+1}", entry["content"], id_suffix=f"disc_btn_{index}")
|
||||
render_text_viewer(app, f"Entry #{index+1}", entry["content"], id_suffix=f"disc_btn_{index}")
|
||||
imgui.same_line(); imgui.set_next_item_width(120)
|
||||
if imgui.begin_combo("##role", entry["role"]):
|
||||
for r in app.disc_roles:
|
||||
@@ -3169,12 +3226,15 @@ def render_discussion_entry(app: App, entry: dict, index: int) -> None:
|
||||
usage = entry.get("usage", {})
|
||||
if ts_str or usage:
|
||||
imgui.same_line()
|
||||
if ts_str: imgui.text_colored(ui_shared.C_SUB, str(ts_str))
|
||||
if ts_str: imgui.text_colored(C_SUB, str(ts_str))
|
||||
if usage:
|
||||
inp, out, cache = usage.get("input_tokens", 0), usage.get("output_tokens", 0), usage.get("cache_read_input_tokens", 0)
|
||||
u_str = f" in:{inp} out:{out}" + (f" cache:{cache}" if cache else "")
|
||||
imgui.same_line(); imgui.text_colored(imgui.ImVec4(0.4, 0.6, 0.7, 1.0), u_str)
|
||||
|
||||
imgui.new_line()
|
||||
imgui.spacing()
|
||||
|
||||
if collapsed:
|
||||
imgui.same_line()
|
||||
if imgui.button("Ins"): app.disc_entries.insert(index, {"role": "User", "content": "", "collapsed": True, "ts": project_manager.now_ts()})
|
||||
@@ -3187,7 +3247,7 @@ def render_discussion_entry(app: App, entry: dict, index: int) -> None:
|
||||
if imgui.button("Branch"): app._branch_discussion(index)
|
||||
imgui.same_line(); preview = entry["content"].replace("\n", " ")[:60]
|
||||
if len(entry["content"]) > 60: preview += "..."
|
||||
imgui.text_colored(ui_shared.C_SUB, preview)
|
||||
imgui.text_colored(C_SUB, preview)
|
||||
else:
|
||||
# Body content
|
||||
imgui.spacing()
|
||||
@@ -3582,7 +3642,7 @@ def render_discussion_metadata(app: App) -> None:
|
||||
imgui.separator()
|
||||
|
||||
imgui.text_colored(C_LBL, "commit:"); imgui.same_line()
|
||||
ui_shared.render_selectable_label(app, 'git_commit_val', git_commit[:12] if git_commit else '(none)', width=100, color=(C_IN if git_commit else C_LBL))
|
||||
render_selectable_label(app, 'git_commit_val', git_commit[:12] if git_commit else '(none)', width=100, color=(C_IN if git_commit else C_LBL))
|
||||
imgui.same_line()
|
||||
if imgui.button("Update Commit"):
|
||||
if app.ui_project_git_dir:
|
||||
@@ -3882,12 +3942,12 @@ def render_tool_calls_panel(app: App) -> None:
|
||||
imgui.table_next_column()
|
||||
script_preview = script.replace("\n", " ")[:150]
|
||||
if len(script) > 150: script_preview += "..."
|
||||
ui_shared.render_selectable_label(app, f'tc_script_{i}', script_preview, width=-1)
|
||||
render_selectable_label(app, f'tc_script_{i}', script_preview, width=-1)
|
||||
|
||||
imgui.table_next_column()
|
||||
res_preview = res.replace("\n", " ")[:30]
|
||||
if len(res) > 30: res_preview += "..."
|
||||
ui_shared.render_selectable_label(app, f'tc_res_{i}', res_preview, width=-1)
|
||||
render_selectable_label(app, f'tc_res_{i}', res_preview, width=-1)
|
||||
|
||||
imgui.end_table()
|
||||
|
||||
@@ -3944,7 +4004,7 @@ def render_text_viewer_window(app: App) -> None:
|
||||
"""Renders the standalone text/code/markdown viewer window."""
|
||||
if not app.show_windows.get("Text Viewer", False): return
|
||||
imgui.set_next_window_size(imgui.ImVec2(900, 700), imgui.Cond_.first_use_ever)
|
||||
expanded, opened = imgui.begin(f"{app.text_viewer_title or 'Text Viewer'}###Text Viewer", True, imgui.WindowFlags_.no_collapse)
|
||||
expanded, opened = imgui.begin(f"{app.text_viewer_title or 'Text Viewer'}###Text_Viewer_Stable", True, imgui.WindowFlags_.no_collapse)
|
||||
app.show_windows["Text Viewer"] = bool(opened)
|
||||
if not opened:
|
||||
app.ui_editing_slices_file = None
|
||||
@@ -4375,7 +4435,7 @@ def render_heavy_text(app: App, label: str, content: str, id_suffix: str = "") -
|
||||
app.show_windows["Text Viewer"] = True
|
||||
imgui.same_line()
|
||||
imgui.text_colored(C_LBL, f"{label}:"); imgui.same_line()
|
||||
ui_shared.render_selectable_label(app, f"heavy_label_{label}_{id_suffix}", content[:60].replace("\n", " ") + ("..." if len(content)>60 else ""), color=C_VAL)
|
||||
render_selectable_label(app, f"heavy_label_{label}_{id_suffix}", content[:60].replace("\n", " ") + ("..." if len(content)>60 else ""), color=C_VAL)
|
||||
|
||||
if content:
|
||||
ctx_id = f"{label}_{id_suffix}"
|
||||
@@ -4713,7 +4773,7 @@ def render_tier_stream_panel(app: App, tier_key: str, stream_key: str | None) ->
|
||||
if stream_key is not None:
|
||||
content = app.mma_streams.get(stream_key, "")
|
||||
imgui.begin_child(f"##stream_content_{tier_key}", imgui.ImVec2(-1, -1))
|
||||
ui_shared.render_selectable_label(app, f'stream_{tier_key}', content, width=-1, multiline=True, height=0)
|
||||
render_selectable_label(app, f'stream_{tier_key}', content, width=-1, multiline=True, height=0)
|
||||
try:
|
||||
if len(content) != app._tier_stream_last_len.get(stream_key, -1):
|
||||
imgui.set_scroll_here_y(1.0)
|
||||
@@ -4740,7 +4800,7 @@ def render_tier_stream_panel(app: App, tier_key: str, stream_key: str | None) ->
|
||||
else:
|
||||
imgui.text(f"{ticket_id} [{status}]")
|
||||
imgui.begin_child(f"##tier3_{ticket_id}_scroll", imgui.ImVec2(-1, 150), True)
|
||||
ui_shared.render_selectable_label(app, f'stream_t3_{ticket_id}', app.mma_streams[key], width=-1, multiline=True, height=0)
|
||||
render_selectable_label(app, f'stream_t3_{ticket_id}', app.mma_streams[key], width=-1, multiline=True, height=0)
|
||||
try:
|
||||
if len(app.mma_streams[key]) != app._tier_stream_last_len.get(key, -1):
|
||||
imgui.set_scroll_here_y(1.0)
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Ensure project root is in path so we can import src.gui_2
|
||||
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
||||
if project_root not in sys.path:
|
||||
sys.path.insert(0, project_root)
|
||||
|
||||
class TestMarkdownTableWidth(unittest.TestCase):
|
||||
def test_render_discussion_entry_full_width(self):
|
||||
"""
|
||||
Verify that render_discussion_entry calls imgui.dummy with the full available width.
|
||||
This is critical for ensuring that the background and Markdown content expand to
|
||||
the full width of the discussion panel.
|
||||
"""
|
||||
# Mock all dependencies to avoid side effects and complex setup during import/execution
|
||||
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, \
|
||||
patch('src.gui_2.ui_shared') as mock_ui_shared, \
|
||||
patch('src.gui_2.project_manager') as mock_pm, \
|
||||
patch('src.gui_2.render_thinking_trace') as mock_rtt, \
|
||||
patch('src.gui_2.render_discussion_entry_read_mode') as mock_rderm:
|
||||
|
||||
# 1. Setup available width and coordinates
|
||||
expected_width = 850.0
|
||||
mock_avail = MagicMock()
|
||||
mock_avail.x = expected_width
|
||||
mock_imgui.get_content_region_avail.return_value = mock_avail
|
||||
|
||||
# Mock ImVec2 to return a simple tuple for easier assertion
|
||||
mock_imgui.ImVec2.side_effect = lambda x, y: (x, y)
|
||||
|
||||
# Mock screen position
|
||||
mock_p_min = MagicMock()
|
||||
mock_p_min.x = 100.0
|
||||
mock_p_min.y = 200.0
|
||||
mock_imgui.get_cursor_screen_pos.return_value = mock_p_min
|
||||
|
||||
# Mock rect max
|
||||
mock_p_max = MagicMock()
|
||||
mock_imgui.get_item_rect_max.return_value = mock_p_max
|
||||
|
||||
# 2. Mock drawing and style dependencies
|
||||
mock_draw_list = MagicMock()
|
||||
mock_imgui.get_window_draw_list.return_value = mock_draw_list
|
||||
|
||||
mock_style = MagicMock()
|
||||
mock_style.window_padding.x = 10.0
|
||||
mock_imgui.get_style.return_value = mock_style
|
||||
|
||||
# 3. Mock app and entry state
|
||||
mock_app = MagicMock()
|
||||
mock_app.disc_roles = ["User", "Assistant"]
|
||||
|
||||
entry = {
|
||||
"role": "User",
|
||||
"content": "Hello world",
|
||||
"collapsed": False,
|
||||
"read_mode": False
|
||||
}
|
||||
|
||||
# Mock combo and other interactive elements to prevent deep branching
|
||||
mock_imgui.begin_combo.return_value = False
|
||||
mock_imgui.button.return_value = False
|
||||
mock_imgui.input_text_multiline.return_value = (False, entry["content"])
|
||||
|
||||
# 4. Import the function within the patch context
|
||||
# Note: We import here to ensure mocks are in place during module initialization if needed
|
||||
from src.gui_2 import render_discussion_entry
|
||||
|
||||
# 5. Execute the function
|
||||
render_discussion_entry(mock_app, entry, 0)
|
||||
|
||||
# 6. Verification
|
||||
# The function should call imgui.dummy(imgui.ImVec2(full_width, 0)) at line 3153
|
||||
# Our mock ImVec2 returns (full_width, 0)
|
||||
mock_imgui.dummy.assert_any_call((expected_width, 0.0))
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,44 @@
|
||||
import inspect
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Ensure project root is in path
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
||||
|
||||
try:
|
||||
from src.gui_2 import App, render_discussion_entry, render_thinking_trace
|
||||
import src.gui_2
|
||||
except ImportError as e:
|
||||
print(f"FAILURE: Could not import from src.gui_2: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
def test_gui_monolithic_symbols():
|
||||
# Verify App is importable
|
||||
assert App is not None
|
||||
|
||||
# Verify render_discussion_entry is in src.gui_2
|
||||
assert hasattr(src.gui_2, 'render_discussion_entry'), "render_discussion_entry missing from src.gui_2"
|
||||
|
||||
# Verify it's defined in src.gui_2, not imported
|
||||
mod = inspect.getmodule(render_discussion_entry)
|
||||
assert mod is not None, "Could not determine module for render_discussion_entry"
|
||||
assert mod.__name__ == 'src.gui_2', f"render_discussion_entry expected in src.gui_2, but found in {mod.__name__}"
|
||||
|
||||
# Verify render_thinking_trace is in src.gui_2
|
||||
assert hasattr(src.gui_2, 'render_thinking_trace'), "render_thinking_trace missing from src.gui_2"
|
||||
|
||||
# Verify it's defined in src.gui_2, not imported
|
||||
mod = inspect.getmodule(render_thinking_trace)
|
||||
assert mod is not None, "Could not determine module for render_thinking_trace"
|
||||
assert mod.__name__ == 'src.gui_2', f"render_thinking_trace expected in src.gui_2, but found in {mod.__name__}"
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
test_gui_monolithic_symbols()
|
||||
print("SUCCESS: Symbols are correctly defined in src.gui_2 local namespace.")
|
||||
except AssertionError as e:
|
||||
print(f"FAILURE: {e}")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"ERROR: {e}")
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,54 @@
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
from src.gui_2 import render_text_viewer_window
|
||||
|
||||
def test_text_viewer_window_id_stability():
|
||||
# Setup a mock app with necessary state
|
||||
app = MagicMock()
|
||||
app.show_windows = {"Text Viewer": True}
|
||||
app.text_viewer_title = "Custom Title"
|
||||
app.text_viewer_content = "Some content"
|
||||
app.text_viewer_type = "text"
|
||||
app.ui_editing_slices_file = None
|
||||
|
||||
# Patch all dependencies
|
||||
with patch('src.gui_2.imgui') as mock_imgui, \
|
||||
patch('src.gui_2.markdown_helper') as mock_md, \
|
||||
patch('src.gui_2.imscope') as mock_scope:
|
||||
|
||||
# Setup mock returns
|
||||
mock_imgui.begin.return_value = (True, True)
|
||||
mock_imgui.checkbox.return_value = (False, True)
|
||||
|
||||
render_text_viewer_window(app)
|
||||
|
||||
# Verify imgui.begin was called with the stable ID suffix
|
||||
args, _ = mock_imgui.begin.call_args
|
||||
window_title = args[0]
|
||||
assert "###Text_Viewer_Stable" in window_title
|
||||
assert window_title.startswith("Custom Title")
|
||||
|
||||
def test_text_viewer_window_default_title_id_stability():
|
||||
# Setup a mock app with default title (None)
|
||||
app = MagicMock()
|
||||
app.show_windows = {"Text Viewer": True}
|
||||
app.text_viewer_title = None
|
||||
app.text_viewer_content = "Some content"
|
||||
app.text_viewer_type = "text"
|
||||
app.ui_editing_slices_file = None
|
||||
|
||||
with patch('src.gui_2.imgui') as mock_imgui, \
|
||||
patch('src.gui_2.markdown_helper') as mock_md, \
|
||||
patch('src.gui_2.imscope') as mock_scope:
|
||||
|
||||
# Setup mock returns
|
||||
mock_imgui.begin.return_value = (True, True)
|
||||
mock_imgui.checkbox.return_value = (False, True)
|
||||
|
||||
render_text_viewer_window(app)
|
||||
|
||||
# Verify imgui.begin was called with the stable ID suffix
|
||||
args, _ = mock_imgui.begin.call_args
|
||||
window_title = args[0]
|
||||
assert "###Text_Viewer_Stable" in window_title
|
||||
assert window_title.startswith("Text Viewer")
|
||||
@@ -0,0 +1,45 @@
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Ensure project root is in path
|
||||
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
||||
if project_root not in sys.path:
|
||||
sys.path.insert(0, project_root)
|
||||
|
||||
class TestMonolithicLayout(unittest.TestCase):
|
||||
def test_render_discussion_entry_full_width_logic(self):
|
||||
"""Verify that render_discussion_entry uses full width and a newline."""
|
||||
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, \
|
||||
patch('src.gui_2.project_manager') as mock_pm, \
|
||||
patch('src.gui_2.render_thinking_trace') as mock_rtt, \
|
||||
patch('src.gui_2.render_discussion_entry_read_mode') as mock_rderm:
|
||||
|
||||
expected_width = 850.0
|
||||
mock_avail = MagicMock()
|
||||
mock_avail.x = expected_width
|
||||
mock_imgui.get_content_region_avail.return_value = mock_avail
|
||||
mock_imgui.ImVec2.side_effect = lambda x, y: (x, y)
|
||||
|
||||
mock_app = MagicMock()
|
||||
mock_app.disc_roles = ["User", "Assistant"]
|
||||
entry = {"role": "User", "content": "test", "collapsed": False, "read_mode": False}
|
||||
|
||||
mock_imgui.begin_combo.return_value = False
|
||||
mock_imgui.button.return_value = False
|
||||
mock_imgui.input_text_multiline.return_value = (False, "test")
|
||||
|
||||
from src.gui_2 import render_discussion_entry
|
||||
render_discussion_entry(mock_app, entry, 0)
|
||||
|
||||
# 1. Verify group expansion
|
||||
mock_imgui.dummy.assert_any_call((expected_width, 0.0))
|
||||
|
||||
# 2. Verify newline to prevent squashing
|
||||
assert mock_imgui.new_line.called, "imgui.new_line() was not called to prevent squashing"
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,29 @@
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
from src.imgui_scopes import _ScopeId
|
||||
import src.imgui_scopes as imgui_scopes
|
||||
|
||||
def test_scope_id_string():
|
||||
with patch('src.imgui_scopes.imgui') as mock_imgui:
|
||||
sid = _ScopeId("test_id")
|
||||
with sid:
|
||||
pass
|
||||
mock_imgui.push_id.assert_called_once_with("test_id")
|
||||
mock_imgui.pop_id.assert_called_once()
|
||||
|
||||
def test_scope_id_int():
|
||||
with patch('src.imgui_scopes.imgui') as mock_imgui:
|
||||
# Python type hint is str, but we test runtime resilience
|
||||
sid = _ScopeId(1234)
|
||||
with sid:
|
||||
pass
|
||||
# Verify it was converted to string to prevent low-level crashes
|
||||
mock_imgui.push_id.assert_called_once_with("1234")
|
||||
mock_imgui.pop_id.assert_called_once()
|
||||
|
||||
def test_id_helper_function():
|
||||
with patch('src.imgui_scopes.imgui') as mock_imgui:
|
||||
with imgui_scopes.id(42):
|
||||
pass
|
||||
mock_imgui.push_id.assert_called_once_with("42")
|
||||
mock_imgui.pop_id.assert_called_once()
|
||||
Reference in New Issue
Block a user