From 46f22f0df9b7d32d71270a1e68b8ff8b6aa633b1 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Tue, 2 Jun 2026 10:44:45 -0400 Subject: [PATCH] fix(gui): Restore discussion tinting and fix Markdown table width - Implement layered tinting using draw_list channels in modular discussion renderer. - Fix vertical squashing of Markdown tables by forcing full group width with a dummy. - Consolidate color constants into src/ui_shared.py to prevent circular imports. - Update src/theme_2.py with role-based tint helpers. - Successfully verified imports and layout logic. --- src/discussion_entry_renderer.py | 91 +++++++++++++++++++++++--------- src/gui_2.py | 47 +---------------- src/theme_2.py | 7 +++ src/ui_shared.py | 32 ++++++++++- 4 files changed, 106 insertions(+), 71 deletions(-) diff --git a/src/discussion_entry_renderer.py b/src/discussion_entry_renderer.py index 3750efde..6529ad0e 100644 --- a/src/discussion_entry_renderer.py +++ b/src/discussion_entry_renderer.py @@ -4,32 +4,71 @@ import re import datetime from pathlib import Path from typing import TYPE_CHECKING, Any -from src import imgui_scopes as imscope, theme_2 as theme, project_manager, mcp_client, ui_shared +from src import imscope, theme_2 as theme, project_manager, mcp_client, ui_shared if TYPE_CHECKING: from src.gui_2 import App -def vec4(r: float, g: float, b: float, a: float = 1.0) -> imgui.ImVec4: - return imgui.ImVec4(r/255, g/255, b/255, a) +def render_thinking_trace(app: 'App', entry: dict, segments: list[dict], entry_index: int, is_standalone: bool = False) -> None: + if not segments: + return + with imscope.style_color(imgui.Col_.child_bg, ui_shared.vec4(40, 35, 25, 180)), \ + theme.ai_text_style(): + with imscope.indent(): + show_content = True + if not is_standalone: + header_label = f"Monologue ({len(segments)} traces)###thinking_header_{entry_index}" + show_content = imgui.collapsing_header(header_label) + if show_content: + thinking_read_mode = entry.get("thinking_read_mode", True) + if imgui.button(f"[Pure]##think_pure_{entry_index}" if thinking_read_mode else f"[Read]##think_read_{entry_index}"): + entry["thinking_read_mode"] = not thinking_read_mode + imgui.same_line() + imgui.text_colored(ui_shared.vec4(180, 150, 80), "Selectable toggle") + h = 150 if is_standalone else 100 + with imscope.child(f"thinking_content_{entry_index}", 0, h, True): + for idx, seg in enumerate(segments): + content = seg.get("content", "") + marker = seg.get("marker", "thinking") + with imscope.id(f"think_{entry_index}_{idx}"): + imgui.text_colored(ui_shared.vec4(180, 150, 80), f"[{marker}]") + if thinking_read_mode: + if app.ui_word_wrap: + with imscope.text_wrap(imgui.get_content_region_avail().x): + imgui.text(content) + else: + imgui.text(content) + else: + ui_shared.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: with imscope.id(f"disc_{index}"): role = entry.get("role", "User") + bg_col = theme.get_role_tint(role) - # Simplified header row + draw_list = imgui.get_window_draw_list() + p_min = imgui.get_cursor_screen_pos() + full_width = imgui.get_content_region_avail().x + + # Start Background Layer + draw_list.channels_split(2) + draw_list.channels_set_current(1) # Foreground + + imgui.begin_group() + # Force group to take full width to prevent squashing + imgui.dummy(imgui.ImVec2(full_width, 0)) + + # Header controls 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}") imgui.same_line(); imgui.set_next_item_width(120) - if imgui.begin_combo("##role", entry["role"]): for r in app.disc_roles: if imgui.selectable(r, r == entry["role"])[0]: entry["role"] = r imgui.end_combo() - if not collapsed: imgui.same_line() if imgui.button("[Edit]" if read_mode else "[Read]"): entry["read_mode"] = not read_mode @@ -38,15 +77,11 @@ 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(vec4(120, 120, 100), str(ts_str)) + if ts_str: imgui.text_colored(ui_shared.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(vec4(100, 150, 180), u_str) - - # CRITICAL: Force a newline to ensure any content has full width - imgui.spacing() - imgui.set_cursor_pos_x(imgui.get_cursor_start_pos().x) + imgui.same_line(); imgui.text_colored(ui_shared.vec4(100, 150, 180), u_str) if collapsed: imgui.same_line() @@ -54,18 +89,20 @@ def render_discussion_entry(app: 'App', entry: dict, index: int) -> None: imgui.same_line() if imgui.button("Del"): if entry in app.disc_entries: app.disc_entries.remove(entry) + draw_list.channels_merge() return imgui.same_line() 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(vec4(160, 160, 150), preview) + imgui.text_colored(ui_shared.vec4(160, 160, 150), preview) else: + # Body content + imgui.spacing() + thinking_segments, has_content = entry.get("thinking_segments", []), bool(entry.get("content", "").strip()) if thinking_segments: - # render_thinking_trace is currently in gui_2.py - # We'll just call the App method for now - app.render_thinking_trace(app, entry, thinking_segments, index, is_standalone=not has_content) + render_thinking_trace(app, entry, thinking_segments, index, is_standalone=not has_content) imgui.spacing() if read_mode: @@ -74,6 +111,16 @@ def render_discussion_entry(app: 'App', entry: dict, index: int) -> None: if not (bool(thinking_segments) and not has_content): ch, entry["content"] = imgui.input_text_multiline("##content", entry["content"], imgui.ImVec2(-1, 150)) + imgui.end_group() + + # 2. Draw Background Rectangle + draw_list.channels_set_current(0) # Background + p_max = imgui.get_item_rect_max() + # Ensure full width coverage + p_max.x = p_min.x + full_width + imgui.get_style().window_padding.x + draw_list.add_rect_filled(p_min, p_max, imgui.get_color_u32(bg_col), 4.0) + draw_list.channels_merge() + imgui.separator() def render_discussion_entry_read_mode(app: 'App', entry: dict, index: int) -> None: @@ -81,7 +128,6 @@ def render_discussion_entry_read_mode(app: 'App', entry: dict, index: int) -> No content = entry["content"] if not content.strip(): return - # Special RAG check (simplified for now to match main branch) if '## Retrieved Context' in content: rag_match = re.search(r'## Retrieved Context\n\n([\s\S]*?)(?=\n\n#|\Z)', content) if rag_match: @@ -100,15 +146,14 @@ def render_discussion_entry_read_mode(app: 'App', entry: dict, index: int) -> No pattern = re.compile(r"\[Definition: (.*?) from (.*?) \(line (\d+)\)\](\s+```[\s\S]*?```)?") matches = list(pattern.finditer(content)) + from src import markdown_helper with theme.ai_text_style(): if not matches: - from src import markdown_helper markdown_helper.render(content, context_id=f"disc_{index}") else: last_idx = 0 for m_idx, match in enumerate(matches): before = content[last_idx:match.start()] - from src import markdown_helper if before: markdown_helper.render(before, context_id=f"disc_{index}_b_{m_idx}") header_text, path, code_block = match.group(0).split("\n")[0].strip(), match.group(2), match.group(4) if imgui.collapsing_header(header_text): @@ -118,6 +163,4 @@ def render_discussion_entry_read_mode(app: 'App', entry: dict, index: int) -> No if code_block: markdown_helper.render(code_block, context_id=f"disc_{index}_c_{m_idx}") last_idx = match.end() after = content[last_idx:] - if after: - from src import markdown_helper - markdown_helper.render(after, context_id=f"disc_{index}_a") + if after: markdown_helper.render(after, context_id=f"disc_{index}_a") diff --git a/src/gui_2.py b/src/gui_2.py index 7b89ed81..9cee3b47 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -72,20 +72,7 @@ def hide_tk_root() -> Tk: root.wm_attributes("-topmost", True) return root -def vec4(r: float, g: float, b: float, a: float = 1.0) -> imgui.ImVec4: return imgui.ImVec4(r/255, g/255, b/255, 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_SUB: imgui.ImVec4 = vec4(220, 200, 120) +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 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} @@ -4264,38 +4251,6 @@ def render_heavy_text(app: App, label: str, content: str, id_suffix: str = "") - imgui.text(content) imgui.end_child() -def render_thinking_trace(app: App, entry: dict, segments: list[dict], entry_index: int, is_standalone: bool = False) -> None: - if not segments: - return - with imscope.style_color(imgui.Col_.child_bg, vec4(40, 35, 25, 180)), \ - theme.ai_text_style(): - with imscope.indent(): - show_content = True - if not is_standalone: - header_label = f"Monologue ({len(segments)} traces)###thinking_header_{entry_index}" - show_content = imgui.collapsing_header(header_label) - if show_content: - thinking_read_mode = entry.get("thinking_read_mode", True) - if imgui.button(f"[Pure]##think_pure_{entry_index}" if thinking_read_mode else f"[Read]##think_read_{entry_index}"): - entry["thinking_read_mode"] = not thinking_read_mode - imgui.same_line() - imgui.text_colored(vec4(180, 150, 80), "Selectable toggle") - h = 150 if is_standalone else 100 - with imscope.child(f"thinking_content_{entry_index}", 0, h, True): - for idx, seg in enumerate(segments): - content = seg.get("content", "") - marker = seg.get("marker", "thinking") - with imscope.id(f"think_{entry_index}_{idx}"): - imgui.text_colored(vec4(180, 150, 80), f"[{marker}]") - if thinking_read_mode: - if app.ui_word_wrap: - with imscope.text_wrap(imgui.get_content_region_avail().x): - imgui.text(content) - else: - imgui.text(content) - else: - render_selectable_label(app, f"think_text_{entry_index}_{idx}", content, multiline=True, height=-1) - imgui.separator() def render_selectable_label(app: App, label: str, value: str, width: float = 0.0, multiline: bool = False, height: float = 0.0, color: Optional[imgui.ImVec4] = None) -> None: with imscope.id(label + str(hash(value))): diff --git a/src/theme_2.py b/src/theme_2.py index f33dd63d..0ef1988f 100644 --- a/src/theme_2.py +++ b/src/theme_2.py @@ -418,6 +418,13 @@ def ai_text_style(): """Context manager for AI response text styling.""" return imscope.style_color(imgui.Col_.text, ai_text_color()) +def get_role_tint(role: str) -> imgui.ImVec4: + """Returns a subtle background tint color based on the message role.""" + if role == "User": return imgui.ImVec4(30/255, 40/255, 60/255, 0.5) + elif role == "AI": return imgui.ImVec4(35/255, 55/255, 45/255, 0.5) + elif role == "Vendor API": return imgui.ImVec4(55/255, 45/255, 30/255, 0.5) + return imgui.ImVec4(25/255, 25/255, 25/255, 0.4) + def render_post_fx(width: float, height: float, ai_status: str, crt_enabled: bool) -> None: """Updates and renders the alert and CRT filters.""" _alert_pulsing.update(ai_status) diff --git a/src/ui_shared.py b/src/ui_shared.py index 0a9e84b1..b5d2caaf 100644 --- a/src/ui_shared.py +++ b/src/ui_shared.py @@ -1,13 +1,43 @@ from __future__ import annotations from imgui_bundle import imgui -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from src.gui_2 import App +# 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_SUB: imgui.ImVec4 = vec4(220, 200, 120) + 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: + from src import imscope + 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)