From 7a434adb7c1829700cc3f7cf023d8104797ec32b Mon Sep 17 00:00:00 2001 From: Ed_ Date: Mon, 1 Jun 2026 19:26:53 -0400 Subject: [PATCH] fix(gui): Resolve AttributeError in imscope.indent and stabilize render loop - Add 'indent' context manager to src/imgui_scopes.py. - Refactor manual imgui.indent/unindent calls in src/gui_2.py to use imscope.indent. - Fix cascading ImGui assertion failures caused by Open/Close mismatches during exceptions. - Finalize 'Selectable Thinking Monologs' track. --- conductor/tracks.md | 2 +- .../plan.md | 10 +-- src/gui_2.py | 90 +++++++++---------- src/imgui_scopes.py | 13 ++- 4 files changed, 62 insertions(+), 53 deletions(-) diff --git a/conductor/tracks.md b/conductor/tracks.md index e50a555d..94f90673 100644 --- a/conductor/tracks.md +++ b/conductor/tracks.md @@ -278,5 +278,5 @@ This file tracks all major tracks for the project. Each track has its own detail --- -- [~] **Track: Selectable Thinking Monologs** +- [x] **Track: Selectable Thinking Monologs** *Link: [./tracks/selectable_thinking_monologs_20260601/](./tracks/selectable_thinking_monologs_20260601/)* diff --git a/conductor/tracks/selectable_thinking_monologs_20260601/plan.md b/conductor/tracks/selectable_thinking_monologs_20260601/plan.md index 6e5b147b..fe4eb1cc 100644 --- a/conductor/tracks/selectable_thinking_monologs_20260601/plan.md +++ b/conductor/tracks/selectable_thinking_monologs_20260601/plan.md @@ -11,8 +11,8 @@ - [x] If `thinking_read_mode` is `False` (Pure mode), use `render_selectable_label(app, f"think_text_...", content, multiline=True, height=-1)` to make the text selectable and copyable. ## Phase 2: Verification -- [ ] Task: Verification - - [ ] Verify that thinking traces in the Discussion Hub can be toggled between Pure and Read modes. - - [ ] Verify that text can be selected and copied via Ctrl+C in Pure mode. - - [ ] Verify that the change does not crash other areas rendering thinking traces (like Comms History). -- [ ] Task: Conductor - User Manual Verification 'Phase 2: Verification' (Protocol in workflow.md) \ No newline at end of file +- [x] Task: Verification + - [x] Verify that thinking traces in the Discussion Hub can be toggled between Pure and Read modes. + - [x] Verify that text can be selected and copied via Ctrl+C in Pure mode. + - [x] Verify that the change does not crash other areas rendering thinking traces (like Comms History). +- [x] Task: Conductor - User Manual Verification 'Phase 2: Verification' (Protocol in workflow.md) \ No newline at end of file diff --git a/src/gui_2.py b/src/gui_2.py index 80c53f3c..52ee1b4d 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -2517,26 +2517,25 @@ def render_persona_editor_window(app: App, is_embedded: bool = False) -> None: imgui.same_line(imgui.get_content_region_avail().x - 30); if imgui.button("x"): to_remove.append(i) if is_expanded: - imgui.indent(20) - if imgui.begin_table("model_settings", 2, imgui.TableFlags_.borders_inner_v): - imgui.table_setup_column("Label", imgui.TableColumnFlags_.width_fixed, 120); imgui.table_setup_column("Control", imgui.TableColumnFlags_.width_stretch) - imgui.table_next_row(); imgui.table_next_column(); imgui.text("Provider:"); imgui.table_next_column(); imgui.set_next_item_width(-1) - p_idx = providers.index(prov) + 1 if prov in providers else 0; ch_p, p_idx = imgui.combo("##prov", p_idx, ["None"] + providers) - if ch_p: entry["provider"] = providers[p_idx-1] if p_idx > 0 else "" - imgui.table_next_row(); imgui.table_next_column(); imgui.text("Model:"); imgui.table_next_column(); imgui.set_next_item_width(-1) - m_list = app.controller.all_available_models.get(entry.get("provider", ""), []); m_idx = m_list.index(mod) + 1 if mod in m_list else 0 - ch_m, m_idx = imgui.combo("##model", m_idx, ["None"] + m_list) - if ch_m: entry["model"] = m_list[m_idx-1] if m_idx > 0 else "" - imgui.table_next_row(); imgui.table_next_column(); imgui.text("Temperature:"); imgui.table_next_column(); cw = imgui.get_content_region_avail().x - imgui.set_next_item_width(cw * 0.7); _, entry["temperature"] = imgui.slider_float("##ts", entry.get("temperature", 0.7), 0.0, 2.0, "%.1f") - imgui.same_line(); imgui.set_next_item_width(-1); _, entry["temperature"] = imgui.input_float("##ti", entry.get("temperature", 0.7), 0.1, 0.1, "%.1f") - imgui.table_next_row(); imgui.table_next_column(); imgui.text("Top-P:"); imgui.table_next_column() - imgui.set_next_item_width(cw * 0.7); _, entry["top_p"] = imgui.slider_float("##tp_s", entry.get("top_p", 1.0), 0.0, 1.0, "%.2f") - imgui.same_line(); imgui.set_next_item_width(-1); _, entry["top_p"] = imgui.input_float("##tp_i", entry.get("top_p", 1.0), 0.05, 0.05, "%.2f") - imgui.table_next_row(); imgui.table_next_column(); imgui.text("Max Tokens:"); imgui.table_next_column(); imgui.set_next_item_width(-1); _, entry["max_output_tokens"] = imgui.input_int("##maxt", entry.get("max_output_tokens", 4096)) - imgui.table_next_row(); imgui.table_next_column(); imgui.text("History Limit:"); imgui.table_next_column(); imgui.set_next_item_width(-1); _, entry["history_trunc_limit"] = imgui.input_int("##hist", entry.get("history_trunc_limit", 900000)) - imgui.end_table() - imgui.unindent(20) + with imscope.indent(20): + if imgui.begin_table("model_settings", 2, imgui.TableFlags_.borders_inner_v): + imgui.table_setup_column("Label", imgui.TableColumnFlags_.width_fixed, 120); imgui.table_setup_column("Control", imgui.TableColumnFlags_.width_stretch) + imgui.table_next_row(); imgui.table_next_column(); imgui.text("Provider:"); imgui.table_next_column(); imgui.set_next_item_width(-1) + p_idx = providers.index(prov) + 1 if prov in providers else 0; ch_p, p_idx = imgui.combo("##prov", p_idx, ["None"] + providers) + if ch_p: entry["provider"] = providers[p_idx-1] if p_idx > 0 else "" + imgui.table_next_row(); imgui.table_next_column(); imgui.text("Model:"); imgui.table_next_column(); imgui.set_next_item_width(-1) + m_list = app.controller.all_available_models.get(entry.get("provider", ""), []); m_idx = m_list.index(mod) + 1 if mod in m_list else 0 + ch_m, m_idx = imgui.combo("##model", m_idx, ["None"] + m_list) + if ch_m: entry["model"] = m_list[m_idx-1] if m_idx > 0 else "" + imgui.table_next_row(); imgui.table_next_column(); imgui.text("Temperature:"); imgui.table_next_column(); cw = imgui.get_content_region_avail().x + imgui.set_next_item_width(cw * 0.7); _, entry["temperature"] = imgui.slider_float("##ts", entry.get("temperature", 0.7), 0.0, 2.0, "%.1f") + imgui.same_line(); imgui.set_next_item_width(-1); _, entry["temperature"] = imgui.input_float("##ti", entry.get("temperature", 0.7), 0.1, 0.1, "%.1f") + imgui.table_next_row(); imgui.table_next_column(); imgui.text("Top-P:"); imgui.table_next_column() + imgui.set_next_item_width(cw * 0.7); _, entry["top_p"] = imgui.slider_float("##tp_s", entry.get("top_p", 1.0), 0.0, 1.0, "%.2f") + imgui.same_line(); imgui.set_next_item_width(-1); _, entry["top_p"] = imgui.input_float("##tp_i", entry.get("top_p", 1.0), 0.05, 0.05, "%.2f") + imgui.table_next_row(); imgui.table_next_column(); imgui.text("Max Tokens:"); imgui.table_next_column(); imgui.set_next_item_width(-1); _, entry["max_output_tokens"] = imgui.input_int("##maxt", entry.get("max_output_tokens", 4096)) + imgui.table_next_row(); imgui.table_next_column(); imgui.text("History Limit:"); imgui.table_next_column(); imgui.set_next_item_width(-1); _, entry["history_trunc_limit"] = imgui.input_int("##hist", entry.get("history_trunc_limit", 900000)) + imgui.end_table() imgui.pop_id() for i in reversed(to_remove): app._editing_persona_preferred_models_list.pop(i) imgui.end_child() @@ -4654,34 +4653,33 @@ def render_thinking_trace(app: App, entry: dict, segments: list[dict], entry_ind return with imscope.style_color(imgui.Col_.child_bg, vec4(40, 35, 25, 180)), \ theme.ai_text_style(): - imgui.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): + 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: - imgui.text(content) - else: - render_selectable_label(app, f"think_text_{entry_index}_{idx}", content, multiline=True, height=-1) - imgui.separator() - imgui.unindent() + 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/imgui_scopes.py b/src/imgui_scopes.py index 227e349a..6aeac891 100644 --- a/src/imgui_scopes.py +++ b/src/imgui_scopes.py @@ -1,4 +1,5 @@ from __future__ import annotations +from typing import Any from imgui_bundle import imgui from imgui_bundle import imgui_node_editor @@ -43,6 +44,16 @@ class _ScopeId: imgui.pop_id() return False +def indent(width: float = 0.0): return _ScopeIndent(width) +class _ScopeIndent: + def __init__(self, width: float): + self._width = width + def __enter__(self): + imgui.indent(self._width) + def __exit__(self, *args): + imgui.unindent(self._width) + return False + def menu(label: str): return _ScopeMenu(label) class _ScopeMenu: def __init__(self, label: str): @@ -256,4 +267,4 @@ class _ScopeWindow: return self._result def __exit__(self, *args): imgui.end() - return False \ No newline at end of file + return False