Private
Public Access
0
0

fix(gui): Final Phase 7 stabilization and polish

- Resolve ImportError by correctly prefixing 'src' in modular renderers.
- Fix ImGui access violation by ensuring push_id always receives string IDs.
- Restore visible role-based background tints using layered rendering (channels).
- Definitively fix horizontal Markdown table widths by forcing group expansion.
- Centralize color management in theme_2.py and ui_shared.py.
- Standardize Files & Media inventory layout and remove legacy controls.
- Update test mocks to support modular UI and theme-driven styling.
This commit is contained in:
2026-06-02 13:27:38 -04:00
parent 46f22f0df9
commit c4811f00c1
13 changed files with 310 additions and 63 deletions
+23 -12
View File
@@ -4,16 +4,24 @@ import re
import datetime
from pathlib import Path
from typing import TYPE_CHECKING, Any
from src import imscope, theme_2 as theme, project_manager, mcp_client, ui_shared
from src import imgui_scopes as imscope, theme_2 as theme, project_manager, mcp_client, ui_shared, markdown_helper
if TYPE_CHECKING:
from src.gui_2 import App
def get_role_tint(role: str) -> imgui.ImVec4:
"""Returns a subtle background tint color based on the message role."""
# Tints: User(Blue), AI(Green), Vendor(Orange), System(Dark)
if role == "User": return imgui.ImVec4(30/255, 45/255, 75/255, 0.5)
elif role == "AI": return imgui.ImVec4(35/255, 65/255, 45/255, 0.5)
elif role == "Vendor API": return imgui.ImVec4(65/255, 55/255, 35/255, 0.5)
return imgui.ImVec4(20/255, 20/255, 20/255, 0.4)
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():
# Tint thinking trace background slightly differently
with imscope.style_color(imgui.Col_.child_bg, imgui.ImVec4(40/255, 35/255, 25/255, 180/255)):
with imscope.indent():
show_content = True
if not is_standalone:
@@ -24,14 +32,14 @@ def render_thinking_trace(app: 'App', entry: dict, segments: list[dict], entry_i
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")
imgui.text_colored(ui_shared.C_TC, "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}]")
imgui.text_colored(ui_shared.C_TC, f"[{marker}]")
if thinking_read_mode:
if app.ui_word_wrap:
with imscope.text_wrap(imgui.get_content_region_avail().x):
@@ -45,7 +53,7 @@ def render_thinking_trace(app: 'App', entry: dict, segments: list[dict], entry_i
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)
bg_col = get_role_tint(role)
draw_list = imgui.get_window_draw_list()
p_min = imgui.get_cursor_screen_pos()
@@ -81,7 +89,7 @@ def render_discussion_entry(app: 'App', entry: dict, index: int) -> None:
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(ui_shared.vec4(100, 150, 180), u_str)
imgui.same_line(); imgui.text_colored(imgui.ImVec4(0.4, 0.6, 0.7, 1.0), u_str)
if collapsed:
imgui.same_line()
@@ -95,15 +103,16 @@ 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.vec4(160, 160, 150), preview)
imgui.text_colored(ui_shared.C_SUB, preview)
else:
# Body content
imgui.spacing()
# Body content - FORCE START ON NEW LINE
imgui.dummy(imgui.ImVec2(0, 4))
imgui.set_cursor_pos_x(imgui.get_cursor_start_pos().x)
thinking_segments, has_content = entry.get("thinking_segments", []), bool(entry.get("content", "").strip())
if thinking_segments:
render_thinking_trace(app, entry, thinking_segments, index, is_standalone=not has_content)
imgui.spacing()
imgui.dummy(imgui.ImVec2(0, 4))
if read_mode:
render_discussion_entry_read_mode(app, entry, index)
@@ -146,7 +155,8 @@ 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
# FORCE A NEW GROUP with no extra constraints
imgui.begin_group()
with theme.ai_text_style():
if not matches:
markdown_helper.render(content, context_id=f"disc_{index}")
@@ -164,3 +174,4 @@ def render_discussion_entry_read_mode(app: 'App', entry: dict, index: int) -> No
last_idx = match.end()
after = content[last_idx:]
if after: markdown_helper.render(after, context_id=f"disc_{index}_a")
imgui.end_group()
+154 -4
View File
@@ -3099,13 +3099,163 @@ def render_discussion_hub(app: App) -> None:
if exp: render_takes_panel(app)
return
def render_thinking_trace(app: App, entry: dict, segments: list[dict], entry_index: int, is_standalone: bool = False) -> None:
if not segments:
return
# Tint thinking trace background slightly differently
with imscope.style_color(imgui.Col_.child_bg, imgui.ImVec4(0.15, 0.14, 0.10, 0.7)), \
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(C_TC, "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(C_TC, 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_discussion_entry(app: App, entry: dict, index: int) -> None:
from src import discussion_entry_renderer
discussion_entry_renderer.render_discussion_entry(app, entry, index)
with imscope.id(f"disc_{index}"):
role = entry.get("role", "User")
bg_col = theme.get_role_tint(role)
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
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
ts_str = entry.get("ts", "")
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 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)
if collapsed:
imgui.same_line()
if imgui.button("Ins"): app.disc_entries.insert(index, {"role": "User", "content": "", "collapsed": True, "ts": project_manager.now_ts()})
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(ui_shared.C_SUB, 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(app, entry, thinking_segments, index, is_standalone=not has_content)
imgui.spacing()
if read_mode:
render_discussion_entry_read_mode(app, entry, index)
else:
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()
# 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:
from src import discussion_entry_renderer
discussion_entry_renderer.render_discussion_entry_read_mode(app, entry, index)
with imscope.id(f"read_{index}"):
content = entry["content"]
if not content.strip(): return
if '## Retrieved Context' in content:
rag_match = re.search(r'## Retrieved Context\n\n([\s\S]*?)(?=\n\n#|\Z)', content)
if rag_match:
rag_section = rag_match.group(1)
if imgui.collapsing_header('Retrieved Context'):
chunks = re.finditer(r'### Chunk (\d+) \(Source: (.*?)\)\n([\s\S]*?)(?=\n### Chunk|\Z)', rag_section)
for chunk_match in chunks:
idx, path, chunk_content = chunk_match.group(1), chunk_match.group(2), chunk_match.group(3)
if imgui.collapsing_header(f'Chunk {idx}: {path}'):
if imgui.button(f'[Source]##rag_{index}_{idx}'):
res = mcp_client.read_file(path)
if res: app.text_viewer_title, app.text_viewer_content, app.text_viewer_type = path, res, (Path(path).suffix.lstrip('.') if Path(path).suffix else 'text'); app.show_windows["Text Viewer"] = True
imgui.text_unformatted(chunk_content)
content = content[:rag_match.start()] + content[rag_match.end():]
pattern = re.compile(r"\[Definition: (.*?) from (.*?) \(line (\d+)\)\](\s+```[\s\S]*?```)?")
matches = list(pattern.finditer(content))
imgui.begin_group()
with theme.ai_text_style():
if not matches:
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()]
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):
if imgui.button(f"[Source]##{index}_{match.start()}"):
res = mcp_client.read_file(path)
if res: app.text_viewer_title, app.text_viewer_content, app.text_viewer_type = path, res, (Path(path).suffix.lstrip(".") if Path(path).suffix else "text"); app.show_windows["Text Viewer"] = True
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: markdown_helper.render(after, context_id=f"disc_{index}_a")
imgui.end_group()
def render_history_window(app: App) -> None:
if not app.show_windows.get('Undo/Redo History', False):
+3 -1
View File
@@ -39,7 +39,9 @@ class _ScopeId:
"""
self._id = str_id
def __enter__(self):
imgui.push_id(self._id)
# Use explicit conversion to avoid any possible nanobind ambiguity
# and access violations. String IDs are the most stable in this binding.
imgui.push_id(str(self._id))
def __exit__(self, *args):
imgui.pop_id()
return False
+5 -4
View File
@@ -420,10 +420,11 @@ def ai_text_style():
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)
# Slightly more opaque and distinct tints for role-based structure
if role == "User": return imgui.ImVec4(0.12, 0.18, 0.30, 0.6) # Deep Blue
elif role == "AI": return imgui.ImVec4(0.14, 0.25, 0.18, 0.6) # Deep Green
elif role == "Vendor API": return imgui.ImVec4(0.25, 0.22, 0.12, 0.5) # Earthy Gold
return imgui.ImVec4(0.1, 0.1, 0.1, 0.4) # Dim System
def render_post_fx(width: float, height: float, ai_status: str, crt_enabled: bool) -> None:
"""Updates and renders the alert and CRT filters."""
+2 -1
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
from imgui_bundle import imgui
from typing import TYPE_CHECKING, Any
from src import imgui_scopes as imscope
if TYPE_CHECKING:
from src.gui_2 import App
@@ -20,6 +21,7 @@ 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)
def render_text_viewer(app: 'App', label: str, content: str, text_type: str = 'text', force_open: bool = False, id_suffix: str = "") -> None:
@@ -30,7 +32,6 @@ def render_text_viewer(app: 'App', label: str, content: str, text_type: str = 't
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):