feat(ui): Implement selectable text across primary GUI panels

This commit is contained in:
2026-03-08 21:37:22 -04:00
parent 74737ac9c7
commit e34a2e6355
2 changed files with 104 additions and 57 deletions

View File

@@ -211,33 +211,34 @@ class App:
if len(content) > COMMS_CLAMP_CHARS: if len(content) > COMMS_CLAMP_CHARS:
# Use a fixed-height child window with unformatted text for large text to avoid expensive frame-by-frame wrapping or input_text_multiline overhead # Use a fixed-height child window with unformatted text for large text to avoid expensive frame-by-frame wrapping or input_text_multiline overhead
imgui.begin_child(f"heavy_text_child_{label}_{hash(content)}", imgui.ImVec2(0, 80), True) imgui.begin_child(f"heavy_text_child_{label}_{hash(content)}", imgui.ImVec2(0, 80), True)
imgui.text_unformatted(content) self._render_selectable_label(f'heavy_val_{label}_{hash(content)}', content, width=-1, multiline=True, height=80)
imgui.end_child() imgui.end_child()
else: else:
if self.ui_word_wrap: self._render_selectable_label(f'heavy_val_{label}_{hash(content)}', content, width=-1, multiline=self.ui_word_wrap, height=0)
imgui.push_text_wrap_pos(0.0)
imgui.text_unformatted(content)
imgui.pop_text_wrap_pos()
else:
imgui.text_unformatted(content)
# ---------------------------------------------------------------- gui # ---------------------------------------------------------------- gui
def _render_selectable_label(self, 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(self, label: str, value: str, width: float = 0.0, multiline: bool = False, height: float = 0.0, color: Optional[imgui.ImVec4] = None) -> None:
imgui.push_id(label + str(hash(value))) imgui.push_id(label + str(hash(value)))
pops = 2 pops = 4
imgui.push_style_color(imgui.Col_.frame_bg, vec4(0, 0, 0, 0)) imgui.push_style_color(imgui.Col_.frame_bg, vec4(0, 0, 0, 0))
imgui.push_style_color(imgui.Col_.frame_bg_hovered, vec4(0, 0, 0, 0))
imgui.push_style_color(imgui.Col_.frame_bg_active, vec4(0, 0, 0, 0))
imgui.push_style_color(imgui.Col_.border, vec4(0, 0, 0, 0)) imgui.push_style_color(imgui.Col_.border, vec4(0, 0, 0, 0))
if color: if color:
imgui.push_style_color(imgui.Col_.text, color) imgui.push_style_color(imgui.Col_.text, color)
pops += 1 pops += 1
imgui.push_style_var(imgui.StyleVar_.frame_border_size, 0.0)
imgui.push_style_var(imgui.StyleVar_.frame_padding, imgui.ImVec2(0, 0))
if multiline: if multiline:
imgui.input_text_multiline("##" + label, value, imgui.ImVec2(width, height), imgui.InputTextFlags_.read_only) imgui.input_text_multiline("##" + label, value, imgui.ImVec2(width, height), imgui.InputTextFlags_.read_only)
else: else:
if width > 0: imgui.set_next_item_width(width) if width > 0: imgui.set_next_item_width(width)
imgui.input_text("##" + label, value, imgui.InputTextFlags_.read_only) imgui.input_text("##" + label, value, imgui.InputTextFlags_.read_only)
imgui.pop_style_color(pops) imgui.pop_style_color(pops)
imgui.pop_style_var(2)
imgui.pop_id() imgui.pop_id()
def _show_menus(self) -> None: def _show_menus(self) -> None:
if imgui.begin_menu("manual slop"): if imgui.begin_menu("manual slop"):
if imgui.menu_item("Quit", "Ctrl+Q", False)[0]: if imgui.menu_item("Quit", "Ctrl+Q", False)[0]:
@@ -1305,14 +1306,7 @@ class App:
if len(content) > 80: preview += "..." if len(content) > 80: preview += "..."
imgui.text_colored(vec4(180, 180, 180), preview) imgui.text_colored(vec4(180, 180, 180), preview)
else: else:
imgui.begin_child(f"prior_content_{idx}", imgui.ImVec2(0, 150), True) self._render_selectable_label(f'prior_content_val_{idx}', content, width=-1, multiline=True, height=150)
if self.ui_word_wrap:
imgui.push_text_wrap_pos(imgui.get_content_region_avail().x)
imgui.text_unformatted(content)
imgui.pop_text_wrap_pos()
else:
imgui.text_unformatted(content)
imgui.end_child()
imgui.separator() imgui.separator()
imgui.pop_id() imgui.pop_id()
@@ -1349,7 +1343,7 @@ class App:
last_updated = disc_data.get("last_updated", "") last_updated = disc_data.get("last_updated", "")
imgui.text_colored(C_LBL, "commit:") imgui.text_colored(C_LBL, "commit:")
imgui.same_line() imgui.same_line()
imgui.text_colored(C_IN if git_commit else C_LBL, git_commit[:12] if git_commit else "(none)") self._render_selectable_label('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() imgui.same_line()
if imgui.button("Update Commit"): if imgui.button("Update Commit"):
git_dir = self.ui_project_git_dir git_dir = self.ui_project_git_dir
@@ -1467,14 +1461,18 @@ class App:
imgui.text_colored(vec4(160, 160, 150), preview) imgui.text_colored(vec4(160, 160, 150), preview)
if not collapsed: if not collapsed:
if read_mode: if read_mode:
content = entry["content"]
pattern = re.compile(r"\[Definition: (.*?) from (.*?) \(line (\d+)\)\](\s+```[\s\S]*?```)?")
matches = list(pattern.finditer(content))
if not matches:
self._render_selectable_label(f'read_content_{i}', content, width=-1, multiline=True, height=150)
else:
imgui.begin_child("read_content", imgui.ImVec2(0, 150), True) imgui.begin_child("read_content", imgui.ImVec2(0, 150), True)
if self.ui_word_wrap: imgui.push_text_wrap_pos(imgui.get_content_region_avail().x) if self.ui_word_wrap: imgui.push_text_wrap_pos(imgui.get_content_region_avail().x)
content = entry["content"]
last_idx = 0 last_idx = 0
pattern = re.compile(r"\[Definition: (.*?) from (.*?) \(line (\d+)\)\](\s+```[\s\S]*?```)?") for m_idx, match in enumerate(matches):
for match in pattern.finditer(content):
before = content[last_idx:match.start()] before = content[last_idx:match.start()]
if before: imgui.text(before) if before: self._render_selectable_label(f'read_before_{i}_{m_idx}', before, width=-1, multiline=True, height=0)
header_text = match.group(0).split("\n")[0].strip() header_text = match.group(0).split("\n")[0].strip()
path = match.group(2) path = match.group(2)
code_block = match.group(4) code_block = match.group(4)
@@ -1495,7 +1493,7 @@ class App:
imgui.text(code_content) imgui.text(code_content)
last_idx = match.end() last_idx = match.end()
after = content[last_idx:] after = content[last_idx:]
if after: imgui.text(after) if after: self._render_selectable_label(f'read_after_{i}_{last_idx}', after, width=-1, multiline=True, height=0)
if self.ui_word_wrap: imgui.pop_text_wrap_pos() if self.ui_word_wrap: imgui.pop_text_wrap_pos()
imgui.end_child() imgui.end_child()
else: else:
@@ -1536,7 +1534,7 @@ class App:
sid = "None" sid = "None"
if hasattr(ai_client, "_gemini_cli_adapter") and ai_client._gemini_cli_adapter: if hasattr(ai_client, "_gemini_cli_adapter") and ai_client._gemini_cli_adapter:
sid = ai_client._gemini_cli_adapter.session_id or "None" sid = ai_client._gemini_cli_adapter.session_id or "None"
imgui.text(f"Session ID: {sid}") imgui.text("Session ID:"); imgui.same_line(); self._render_selectable_label("gemini_cli_sid", sid, width=200)
if imgui.button("Reset CLI Session"): if imgui.button("Reset CLI Session"):
ai_client.reset_session() ai_client.reset_session()
imgui.text("Binary Path") imgui.text("Binary Path")
@@ -1560,7 +1558,7 @@ class App:
total = usage["input_tokens"] + usage["output_tokens"] total = usage["input_tokens"] + usage["output_tokens"]
if total == 0 and usage.get("total_tokens", 0) > 0: if total == 0 and usage.get("total_tokens", 0) > 0:
total = usage["total_tokens"] total = usage["total_tokens"]
imgui.text_colored(C_RES, f"Tokens: {total:,} (In: {usage['input_tokens']:,} Out: {usage['output_tokens']:,})") self._render_selectable_label("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: if usage.get("last_latency", 0.0) > 0:
imgui.text_colored(C_LBL, f" Last Latency: {usage['last_latency']:.2f}s") imgui.text_colored(C_LBL, f" Last Latency: {usage['last_latency']:.2f}s")
if usage["cache_read_input_tokens"]: if usage["cache_read_input_tokens"]:
@@ -1623,13 +1621,13 @@ class App:
tokens = in_t + out_t tokens = in_t + out_t
cost = cost_tracker.estimate_cost(model, in_t, out_t) cost = cost_tracker.estimate_cost(model, in_t, out_t)
imgui.table_next_row() imgui.table_next_row()
imgui.table_set_column_index(0); imgui.text(tier) imgui.table_set_column_index(0); self._render_selectable_label(f"tier_{tier}", tier, width=-1)
imgui.table_set_column_index(1); imgui.text(model.split('-')[0]) imgui.table_set_column_index(1); self._render_selectable_label(f"model_{tier}", model.split("-")[0], width=-1)
imgui.table_set_column_index(2); imgui.text(f"{tokens:,}") imgui.table_set_column_index(2); self._render_selectable_label(f"tokens_{tier}", f"{tokens:,}", width=-1)
imgui.table_set_column_index(3); imgui.text_colored(imgui.ImVec4(0.2, 0.9, 0.2, 1), f"${cost:.4f}") imgui.table_set_column_index(3); self._render_selectable_label(f"cost_{tier}", f"${cost:.4f}", width=-1, color=imgui.ImVec4(0.2, 0.9, 0.2, 1))
imgui.end_table() imgui.end_table()
tier_total = sum(cost_tracker.estimate_cost(stats.get('model', ''), stats.get('input', 0), stats.get('output', 0)) for stats in self.mma_tier_usage.values()) tier_total = sum(cost_tracker.estimate_cost(stats.get('model', ''), stats.get('input', 0), stats.get('output', 0)) for stats in self.mma_tier_usage.values())
imgui.text_colored(imgui.ImVec4(0, 1, 0, 1), f"Session Total: ${tier_total:.4f}") self._render_selectable_label("session_total_cost", f"Session Total: ${tier_total:.4f}", width=-1, color=imgui.ImVec4(0, 1, 0, 1))
else: else:
imgui.text_disabled("No MMA tier usage data") imgui.text_disabled("No MMA tier usage data")
if stats.get("would_trim"): if stats.get("would_trim"):
@@ -1974,7 +1972,8 @@ class App:
script_preview = script.replace("\n", " ")[:150] script_preview = script.replace("\n", " ")[:150]
if len(script) > 150: script_preview += "..." if len(script) > 150: script_preview += "..."
if imgui.selectable(f"{script_preview}##tc_{i}", False, imgui.SelectableFlags_.span_all_columns)[0]: self._render_selectable_label(f'tc_script_{i}', script_preview, width=-1)
if imgui.is_item_clicked():
self.text_viewer_title = f"Tool Call #{i+1} Details" self.text_viewer_title = f"Tool Call #{i+1} Details"
self.text_viewer_content = combined self.text_viewer_content = combined
self.show_text_viewer = True self.show_text_viewer = True
@@ -1982,7 +1981,8 @@ class App:
imgui.table_next_column() imgui.table_next_column()
res_preview = res.replace("\n", " ")[:30] res_preview = res.replace("\n", " ")[:30]
if len(res) > 30: res_preview += "..." if len(res) > 30: res_preview += "..."
if imgui.button(f"View##res_{i}"): self._render_selectable_label(f'tc_res_{i}', res_preview, width=-1)
if imgui.is_item_clicked():
self.text_viewer_title = f"Tool Call #{i+1} Details" self.text_viewer_title = f"Tool Call #{i+1} Details"
self.text_viewer_content = combined self.text_viewer_content = combined
self.show_text_viewer = True self.show_text_viewer = True
@@ -2648,7 +2648,7 @@ class App:
if stream_key is not None: if stream_key is not None:
content = self.mma_streams.get(stream_key, "") content = self.mma_streams.get(stream_key, "")
imgui.begin_child(f"##stream_content_{tier_key}", imgui.ImVec2(-1, -1)) imgui.begin_child(f"##stream_content_{tier_key}", imgui.ImVec2(-1, -1))
imgui.text_wrapped(content) self._render_selectable_label(f'stream_{tier_key}', content, width=-1, multiline=True, height=0)
try: try:
if len(content) != self._tier_stream_last_len.get(stream_key, -1): if len(content) != self._tier_stream_last_len.get(stream_key, -1):
imgui.set_scroll_here_y(1.0) imgui.set_scroll_here_y(1.0)
@@ -2674,7 +2674,7 @@ class App:
else: else:
imgui.text(f"{ticket_id} [{status}]") imgui.text(f"{ticket_id} [{status}]")
imgui.begin_child(f"##tier3_{ticket_id}_scroll", imgui.ImVec2(-1, 150), True) imgui.begin_child(f"##tier3_{ticket_id}_scroll", imgui.ImVec2(-1, 150), True)
imgui.text_wrapped(self.mma_streams[key]) self._render_selectable_label(f'stream_t3_{ticket_id}', self.mma_streams[key], width=-1, multiline=True, height=0)
try: try:
if len(self.mma_streams[key]) != self._tier_stream_last_len.get(key, -1): if len(self.mma_streams[key]) != self._tier_stream_last_len.get(key, -1):
imgui.set_scroll_here_y(1.0) imgui.set_scroll_here_y(1.0)

View File

@@ -0,0 +1,47 @@
import pytest
import time
import os
import sys
# Ensure project root is in path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
from src.api_hook_client import ApiHookClient
from src.gui_2 import App
@pytest.mark.integration
def test_selectable_label_stability(live_gui) -> None:
"""
Verifies that the application starts correctly with --enable-test-hooks
and that the selectable label infrastructure is present and stable.
"""
client = ApiHookClient()
assert client.wait_for_server(timeout=20), "Hook server failed to start"
# 1. Check initial state via diagnostics: is_viewing_prior_session should be False
diag = client.get_gui_diagnostics()
# Based on src/api_hooks.py: result["prior"] = _get_app_attr(app, "is_viewing_prior_session", False)
assert diag.get("prior") is False, "Initial state should not be viewing prior session"
# 2. Verify _render_selectable_label exists in the App class
# This satisfies the requirement to check if it exists in the App class.
assert hasattr(App, '_render_selectable_label'), "App class must have _render_selectable_label method"
# 3. Check performance to ensure stability
perf = client.get_performance()
metrics = perf.get("performance", {})
# We check if FPS is reported; in some CI environments it might be low but should be > 0 if rendering
assert "fps" in metrics, "Performance metrics should include FPS"
# 4. Basic smoke test: set and get a value to ensure GUI thread is responsive
# ai_response is a known field that is often rendered using selectable labels in various contexts
client.set_value("ai_response", "Test selectable text stability")
# Give it a few frames to process the task
time.sleep(1)
val = client.get_value("ai_response")
assert val == "Test selectable text stability", f"Expected 'Test selectable text stability', got '{val}'"
# 5. Verify prior session indicator specifically via the gettable field
# prior_session_indicator is mapped to AppController.is_viewing_prior_session
prior_val = client.get_value("prior_session_indicator")
assert prior_val is False, "prior_session_indicator field should be False initially"