Private
Public Access
feat(gui): Implement selectable thinking monologs and fix UI regressions
- Implement [Pure]/[Read] toggle for AI thinking monologues to allow text selection/copying. - Fix TypeError: render_thinking_trace() missing 'entry_index' argument. - Fix [+] buttons in Discussion and Comms history by correctly updating window state registry. - Remove ListClipper from Discussion and Comms panels to fix variable-height clipping issues. - Increase clipping heights for large entries to improve visibility. - Fix code block scroll snapping in Markdown helper by robustifying text synchronization.
This commit is contained in:
+1
-1
@@ -278,5 +278,5 @@ This file tracks all major tracks for the project. Each track has its own detail
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
- [ ] **Track: Selectable Thinking Monologs**
|
- [~] **Track: Selectable Thinking Monologs**
|
||||||
*Link: [./tracks/selectable_thinking_monologs_20260601/](./tracks/selectable_thinking_monologs_20260601/)*
|
*Link: [./tracks/selectable_thinking_monologs_20260601/](./tracks/selectable_thinking_monologs_20260601/)*
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
# Implementation Plan: Selectable Thinking Monologs
|
# Implementation Plan: Selectable Thinking Monologs
|
||||||
|
|
||||||
## Phase 1: UI Implementation
|
## Phase 1: UI Implementation
|
||||||
- [ ] Task: Update `render_thinking_trace` signature
|
- [x] Task: Update `render_thinking_trace` signature
|
||||||
- [ ] Modify `def render_thinking_trace(app: App, segments: list[dict], entry_index: int, is_standalone: bool = False)` to accept the parent `entry: dict`.
|
- [x] Modify `def render_thinking_trace(app: App, entry: dict, segments: list[dict], entry_index: int, is_standalone: bool = False)` to accept the parent `entry: dict`.
|
||||||
- [ ] Update all calls to `render_thinking_trace` in `src/gui_2.py` (e.g., in `render_discussion_entry` and `render_comms_history_panel`) to pass the appropriate `entry` object. For standalone traces where an entry dictionary might not exist, pass a dummy dict to hold the state.
|
- [x] Update all calls to `render_thinking_trace` in `src/gui_2.py` (e.g., in `render_discussion_entry` and `render_comms_history_panel`) to pass the appropriate `entry` object. For standalone traces where an entry dictionary might not exist, pass a dummy dict to hold the state.
|
||||||
- [ ] Task: Implement the UI Toggle
|
- [x] Task: Implement the UI Toggle
|
||||||
- [ ] Inside `render_thinking_trace`, below or next to the `imgui.collapsing_header`, add an `imgui.button` that toggles `entry.get("thinking_read_mode", True)`.
|
- [x] Inside `render_thinking_trace`, below or next to the `imgui.collapsing_header`, add an `imgui.button` that toggles `entry.get("thinking_read_mode", True)`.
|
||||||
- [ ] Task: Implement Conditional Rendering
|
- [x] Task: Implement Conditional Rendering
|
||||||
- [ ] If `thinking_read_mode` is `True` (Read mode), use `markdown_helper.render(content)` or `imgui.text_wrapped` for a clean reading view.
|
- [x] If `thinking_read_mode` is `True` (Read mode), use `markdown_helper.render(content)` or `imgui.text_wrapped` for a clean reading view.
|
||||||
- [ ] 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.
|
- [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
|
## Phase 2: Verification
|
||||||
- [ ] Task: Verification
|
- [ ] Task: Verification
|
||||||
|
|||||||
+32
-40
@@ -3424,7 +3424,7 @@ def render_discussion_entry(app: App, entry: dict, index: int) -> None:
|
|||||||
with imscope.id(f"disc_{index}"):
|
with imscope.id(f"disc_{index}"):
|
||||||
collapsed, read_mode = entry.get("collapsed", False), entry.get("read_mode", False)
|
collapsed, read_mode = entry.get("collapsed", False), entry.get("read_mode", False)
|
||||||
if imgui.button("+" if collapsed else "-"): entry["collapsed"] = not collapsed
|
if imgui.button("+" if collapsed else "-"): entry["collapsed"] = not collapsed
|
||||||
imgui.same_line(); render_text_viewer(app, f"Entry #{index+1}", entry["content"]); imgui.same_line(); imgui.set_next_item_width(120)
|
imgui.same_line(); 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"]):
|
if imgui.begin_combo("##role", entry["role"]):
|
||||||
for r in app.disc_roles:
|
for r in app.disc_roles:
|
||||||
if imgui.selectable(r, r == entry["role"])[0]: entry["role"] = r
|
if imgui.selectable(r, r == entry["role"])[0]: entry["role"] = r
|
||||||
@@ -3461,7 +3461,7 @@ def render_discussion_entry(app: App, entry: dict, index: int) -> None:
|
|||||||
imgui.text_colored(vec4(160, 160, 150), preview)
|
imgui.text_colored(vec4(160, 160, 150), preview)
|
||||||
if not collapsed:
|
if not collapsed:
|
||||||
thinking_segments, has_content = entry.get("thinking_segments", []), bool(entry.get("content", "").strip())
|
thinking_segments, has_content = entry.get("thinking_segments", []), bool(entry.get("content", "").strip())
|
||||||
if thinking_segments: render_thinking_trace(app, thinking_segments, index, is_standalone=not has_content)
|
if thinking_segments: render_thinking_trace(app, entry, thinking_segments, index, is_standalone=not has_content)
|
||||||
if read_mode: render_discussion_entry_read_mode(app, entry, index)
|
if read_mode: render_discussion_entry_read_mode(app, entry, index)
|
||||||
else:
|
else:
|
||||||
if not (bool(thinking_segments) and not has_content): ch, entry["content"] = imgui.input_text_multiline("##content", entry["content"], imgui.ImVec2(-1, 150))
|
if not (bool(thinking_segments) and not has_content): ch, entry["content"] = imgui.input_text_multiline("##content", entry["content"], imgui.ImVec2(-1, 150))
|
||||||
@@ -3490,7 +3490,7 @@ def render_discussion_entry_read_mode(app: App, entry: dict, index: int) -> None
|
|||||||
with theme.ai_text_style():
|
with theme.ai_text_style():
|
||||||
markdown_helper.render(content, context_id=f'disc_{index}')
|
markdown_helper.render(content, context_id=f'disc_{index}')
|
||||||
else:
|
else:
|
||||||
with imscope.child(f"read_content_{index}", size_y=150, flags=True):
|
with imscope.child(f"read_content_{index}", size_y=400, flags=True):
|
||||||
if app.ui_word_wrap: imgui.push_text_wrap_pos(imgui.get_content_region_avail().x)
|
if app.ui_word_wrap: imgui.push_text_wrap_pos(imgui.get_content_region_avail().x)
|
||||||
last_idx = 0
|
last_idx = 0
|
||||||
for m_idx, match in enumerate(matches):
|
for m_idx, match in enumerate(matches):
|
||||||
@@ -3650,11 +3650,7 @@ def render_comms_history_panel(app: App) -> None:
|
|||||||
|
|
||||||
log_to_render = app._comms_log_cache
|
log_to_render = app._comms_log_cache
|
||||||
|
|
||||||
clipper = imgui.ListClipper()
|
for i, entry in enumerate(log_to_render):
|
||||||
clipper.begin(len(log_to_render))
|
|
||||||
while clipper.step():
|
|
||||||
for i in range(clipper.display_start, clipper.display_end):
|
|
||||||
entry = log_to_render[i]
|
|
||||||
imgui.push_id(f"comms_entry_{i}")
|
imgui.push_id(f"comms_entry_{i}")
|
||||||
|
|
||||||
i_display = i + 1
|
i_display = i + 1
|
||||||
@@ -3714,7 +3710,7 @@ def render_comms_history_panel(app: App) -> None:
|
|||||||
|
|
||||||
text_content = payload.get("text", "")
|
text_content = payload.get("text", "")
|
||||||
segments, parsed_response = thinking_parser.parse_thinking_trace(text_content)
|
segments, parsed_response = thinking_parser.parse_thinking_trace(text_content)
|
||||||
if segments: render_thinking_trace(app, [{"content": s.content, "marker": s.marker} for s in segments], i, is_standalone=not bool(parsed_response.strip()))
|
if segments: render_thinking_trace(app, payload, [{"content": s.content, "marker": s.marker} for s in segments], i, is_standalone=not bool(parsed_response.strip()))
|
||||||
if parsed_response: render_heavy_text(app, "text", parsed_response, idx_str)
|
if parsed_response: render_heavy_text(app, "text", parsed_response, idx_str)
|
||||||
|
|
||||||
tcs = payload.get("tool_calls", [])
|
tcs = payload.get("tool_calls", [])
|
||||||
@@ -3799,10 +3795,7 @@ def render_discussion_entries(app: App) -> None:
|
|||||||
if tier_usage:
|
if tier_usage:
|
||||||
persona_name = tier_usage.get("persona")
|
persona_name = tier_usage.get("persona")
|
||||||
if persona_name: display_entries = [e for e in app.disc_entries if e.get("role") == persona_name or e.get("role") == "User"]
|
if persona_name: display_entries = [e for e in app.disc_entries if e.get("role") == persona_name or e.get("role") == "User"]
|
||||||
clipper = imgui.ListClipper(); clipper.begin(len(display_entries))
|
for i, entry in enumerate(display_entries):
|
||||||
while clipper.step():
|
|
||||||
for i in range(clipper.display_start, clipper.display_end):
|
|
||||||
if i < len(display_entries):
|
|
||||||
render_discussion_entry(app, display_entries[i], i)
|
render_discussion_entry(app, display_entries[i], i)
|
||||||
if app._scroll_disc_to_bottom: imgui.set_scroll_here_y(1.0); app._scroll_disc_to_bottom = False
|
if app._scroll_disc_to_bottom: imgui.set_scroll_here_y(1.0); app._scroll_disc_to_bottom = False
|
||||||
|
|
||||||
@@ -4077,7 +4070,7 @@ def render_response_panel(app: App) -> None:
|
|||||||
with theme.ai_text_style():
|
with theme.ai_text_style():
|
||||||
segments, parsed_response = thinking_parser.parse_thinking_trace(app.ai_response)
|
segments, parsed_response = thinking_parser.parse_thinking_trace(app.ai_response)
|
||||||
if segments:
|
if segments:
|
||||||
render_thinking_trace(app, [{"content": s.content, "marker": s.marker} for s in segments], 9999)
|
render_thinking_trace(app, {}, [{"content": s.content, "marker": s.marker} for s in segments], 9999)
|
||||||
markdown_helper.render(parsed_response, context_id="response")
|
markdown_helper.render(parsed_response, context_id="response")
|
||||||
|
|
||||||
imgui.separator()
|
imgui.separator()
|
||||||
@@ -4620,61 +4613,58 @@ def render_error_tint(app: App) -> None:
|
|||||||
imgui.text_colored(imgui.ImVec4(1, 0, 0, 1), "HOT RELOAD ERROR")
|
imgui.text_colored(imgui.ImVec4(1, 0, 0, 1), "HOT RELOAD ERROR")
|
||||||
imgui.text_wrapped(HotReloader.last_error or "Unknown error")
|
imgui.text_wrapped(HotReloader.last_error or "Unknown error")
|
||||||
|
|
||||||
def render_text_viewer(app: App, label: str, content: str, text_type: str = 'text', force_open: bool = False) -> None:
|
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("[+]##" + str(id(content))) or force_open:
|
if imgui.button(f"[+]##{id_suffix or str(id(content))}") or force_open:
|
||||||
app.text_viewer_type = text_type
|
app.text_viewer_type = text_type
|
||||||
app.show_text_viewer = True
|
app.show_text_viewer = True
|
||||||
app.text_viewer_title = label
|
app.text_viewer_title = label
|
||||||
app.text_viewer_content = content
|
app.text_viewer_content = content
|
||||||
|
app.show_windows["Text Viewer"] = True
|
||||||
|
|
||||||
def render_heavy_text(app: App, label: str, content: str, id_suffix: str = "") -> None:
|
def render_heavy_text(app: App, label: str, content: str, id_suffix: str = "") -> None:
|
||||||
imgui.text_colored(C_LBL, f"{label}:")
|
if imgui.button(f"[+]##{label}{id_suffix}"):
|
||||||
imgui.same_line()
|
|
||||||
if imgui.button("[+]##" + label + id_suffix):
|
|
||||||
app.show_text_viewer = True
|
app.show_text_viewer = True
|
||||||
|
app.show_windows["Text Viewer"] = True
|
||||||
app.text_viewer_type = 'markdown' if label in ('message', 'text', 'content', 'system') else 'json' if label in ('tool_calls', 'data') else 'powershell' if label == 'script' else 'text'
|
app.text_viewer_type = 'markdown' if label in ('message', 'text', 'content', 'system') else 'json' if label in ('tool_calls', 'data') else 'powershell' if label == 'script' else 'text'
|
||||||
app.text_viewer_title = label
|
app.text_viewer_title = label
|
||||||
app.text_viewer_content = content
|
app.text_viewer_content = content
|
||||||
|
imgui.same_line()
|
||||||
|
imgui.text_colored(C_LBL, f"{label}:"); imgui.same_line()
|
||||||
|
render_selectable_label(app, f"heavy_label_{label}_{id_suffix}", content[:60].replace("\n", " ") + ("..." if len(content)>60 else ""), color=C_VAL)
|
||||||
|
|
||||||
if not content:
|
if content:
|
||||||
imgui.text_disabled("(empty)")
|
ctx_id = f"{label}_{id_suffix}"
|
||||||
return
|
is_md = label in ('message', 'text', 'content', 'system')
|
||||||
|
with imscope.indent():
|
||||||
is_md = label in ("message", "text", "content")
|
|
||||||
ctx_id = f"heavy_{label}_{id_suffix}"
|
|
||||||
|
|
||||||
with theme.ai_text_style():
|
|
||||||
if len(content) > COMMS_CLAMP_CHARS:
|
|
||||||
if is_md:
|
if is_md:
|
||||||
imgui.begin_child(f"heavy_text_child_{label}_{id_suffix}", imgui.ImVec2(0, 180), True, imgui.WindowFlags_.always_vertical_scrollbar)
|
imgui.begin_child(f"heavy_text_child_{label}_{id_suffix}", imgui.ImVec2(0, 300), True, imgui.WindowFlags_.always_vertical_scrollbar)
|
||||||
markdown_helper.render(content, context_id=ctx_id)
|
markdown_helper.render(content, context_id=ctx_id)
|
||||||
imgui.end_child()
|
imgui.end_child()
|
||||||
else:
|
else:
|
||||||
imgui.input_text_multiline(f"##heavy_text_input_{label}_{id_suffix}", content, imgui.ImVec2(-1, 180), imgui.InputTextFlags_.read_only)
|
imgui.begin_child(f"heavy_text_child_{label}_{id_suffix}", imgui.ImVec2(0, 200), True, imgui.WindowFlags_.always_vertical_scrollbar)
|
||||||
else:
|
|
||||||
if is_md:
|
|
||||||
markdown_helper.render(content, context_id=ctx_id)
|
|
||||||
else:
|
|
||||||
if app.ui_word_wrap:
|
if app.ui_word_wrap:
|
||||||
imgui.push_text_wrap_pos(imgui.get_content_region_avail().x)
|
with imscope.text_wrap(imgui.get_content_region_avail().x):
|
||||||
imgui.text(content)
|
imgui.text(content)
|
||||||
imgui.pop_text_wrap_pos()
|
|
||||||
else:
|
else:
|
||||||
imgui.text(content)
|
imgui.text(content)
|
||||||
|
imgui.end_child()
|
||||||
|
|
||||||
def render_thinking_trace(app: App, segments: list[dict], entry_index: int, is_standalone: bool = False) -> None:
|
def render_thinking_trace(app: App, entry: dict, segments: list[dict], entry_index: int, is_standalone: bool = False) -> None:
|
||||||
if not segments:
|
if not segments:
|
||||||
return
|
return
|
||||||
with imscope.style_color(imgui.Col_.child_bg, vec4(40, 35, 25, 180)), \
|
with imscope.style_color(imgui.Col_.child_bg, vec4(40, 35, 25, 180)), \
|
||||||
theme.ai_text_style():
|
theme.ai_text_style():
|
||||||
imgui.indent()
|
imgui.indent()
|
||||||
|
|
||||||
show_content = True
|
show_content = True
|
||||||
if not is_standalone:
|
if not is_standalone:
|
||||||
header_label = f"Monologue ({len(segments)} traces)###thinking_header_{entry_index}"
|
header_label = f"Monologue ({len(segments)} traces)###thinking_header_{entry_index}"
|
||||||
show_content = imgui.collapsing_header(header_label)
|
show_content = imgui.collapsing_header(header_label)
|
||||||
|
|
||||||
if show_content:
|
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
|
h = 150 if is_standalone else 100
|
||||||
with imscope.child(f"thinking_content_{entry_index}", 0, h, True):
|
with imscope.child(f"thinking_content_{entry_index}", 0, h, True):
|
||||||
for idx, seg in enumerate(segments):
|
for idx, seg in enumerate(segments):
|
||||||
@@ -4682,13 +4672,15 @@ def render_thinking_trace(app: App, segments: list[dict], entry_index: int, is_s
|
|||||||
marker = seg.get("marker", "thinking")
|
marker = seg.get("marker", "thinking")
|
||||||
with imscope.id(f"think_{entry_index}_{idx}"):
|
with imscope.id(f"think_{entry_index}_{idx}"):
|
||||||
imgui.text_colored(vec4(180, 150, 80), f"[{marker}]")
|
imgui.text_colored(vec4(180, 150, 80), f"[{marker}]")
|
||||||
|
if thinking_read_mode:
|
||||||
if app.ui_word_wrap:
|
if app.ui_word_wrap:
|
||||||
with imscope.text_wrap(imgui.get_content_region_avail().x):
|
with imscope.text_wrap(imgui.get_content_region_avail().x):
|
||||||
imgui.text(content)
|
imgui.text(content)
|
||||||
else:
|
else:
|
||||||
imgui.text(content)
|
imgui.text(content)
|
||||||
|
else:
|
||||||
|
render_selectable_label(app, f"think_text_{entry_index}_{idx}", content, multiline=True, height=-1)
|
||||||
imgui.separator()
|
imgui.separator()
|
||||||
|
|
||||||
imgui.unindent()
|
imgui.unindent()
|
||||||
|
|
||||||
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:
|
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:
|
||||||
|
|||||||
@@ -126,13 +126,13 @@ class MarkdownRenderer:
|
|||||||
|
|
||||||
# Sync text and language
|
# Sync text and language
|
||||||
lang_id = self._lang_map.get(lang_tag, ed.TextEditor.LanguageDefinitionId.none)
|
lang_id = self._lang_map.get(lang_tag, ed.TextEditor.LanguageDefinitionId.none)
|
||||||
target_text = code + "\n"
|
|
||||||
|
|
||||||
if editor.get_text() != target_text:
|
# Robust check to avoid re-setting text every frame (which resets scroll)
|
||||||
|
curr_text = editor.get_text().replace('\r\n', '\n').strip()
|
||||||
|
if curr_text != code.replace('\r\n', '\n').strip():
|
||||||
editor.set_text(code)
|
editor.set_text(code)
|
||||||
editor.set_language_definition(lang_id)
|
editor.set_language_definition(lang_id)
|
||||||
elif editor.get_language_definition_name().lower() != lang_tag:
|
elif editor.get_language_definition_name().lower() != lang_tag:
|
||||||
# get_language_definition_name might not match exactly but good enough check
|
|
||||||
editor.set_language_definition(lang_id)
|
editor.set_language_definition(lang_id)
|
||||||
|
|
||||||
# Dynamic height calculation
|
# Dynamic height calculation
|
||||||
|
|||||||
Reference in New Issue
Block a user