Private
Public Access
0
0

fix(gui): Force newline in discussion entries to prevent squashed layout

- Insert imgui.new_line() before rendering discussion content.
- Ensures the Markdown renderer inherits the full horizontal width of the panel.
- Definitively fixes vertical squashing of tables and long text blocks.
This commit is contained in:
2026-06-02 15:42:04 -04:00
parent fee41032b6
commit 4d8e949720
11 changed files with 485 additions and 44 deletions
+80 -20
View File
@@ -33,9 +33,12 @@ from typing import Optional, Any
from src.diff_viewer import apply_patch_to_file
from src import ai_client
from src import aggregate
from src import ai_client
from src import aggregate
from src import ai_client
from src import aggregate
from src import api_hooks
from src import app_controller
from src import ui_shared
from src import bg_shader
from src import cost_tracker
from src import history
@@ -72,7 +75,61 @@ def hide_tk_root() -> Tk:
root.wm_attributes("-topmost", True)
return root
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
# 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_TRM: imgui.ImVec4 = vec4(160, 160, 150) # Trimmed/Cruft
C_SUB: imgui.ImVec4 = vec4(220, 200, 120)
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}
HEAVY_KEYS: set[str] = {"message", "text", "script", "output", "content"}
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:
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)
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:
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)
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}
@@ -1499,7 +1556,7 @@ def render_token_budget_panel(app: App) -> None:
usage = app.session_usage
total = usage["input_tokens"] + usage["output_tokens"]
if total == 0 and usage.get("total_tokens", 0) > 0: total = usage["total_tokens"]
ui_shared.render_selectable_label(app, "session_telemetry_tokens", f"Tokens: {total:,} (In: {usage['input_tokens']:,} Out: {usage['output_tokens']:,})", width=-1, color=C_RES)
render_selectable_label(app, "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: imgui.text_colored(C_LBL, f" Last Latency: {usage['last_latency']:.2f}s")
if usage["cache_read_input_tokens"]: imgui.text_colored(C_LBL, f" Cache Read: {usage['cache_read_input_tokens']:,} Creation: {usage['cache_creation_input_tokens']:,}")
if app._gemini_cache_text: imgui.text_colored(C_SUB, app._gemini_cache_text)
@@ -1556,13 +1613,13 @@ def render_token_budget_panel(app: App) -> None:
tokens = in_t + out_t
cost = cost_tracker.estimate_cost(model, in_t, out_t)
imgui.table_next_row()
imgui.table_set_column_index(0); ui_shared.render_selectable_label(app, f"tier_{tier}", tier, width=-1)
imgui.table_set_column_index(1); ui_shared.render_selectable_label(app, f"model_{tier}", model.split("-")[0], width=-1)
imgui.table_set_column_index(2); ui_shared.render_selectable_label(app, f"tokens_{tier}", f"{tokens:,}", width=-1)
imgui.table_set_column_index(3); ui_shared.render_selectable_label(app, f"cost_{tier}", f"${cost:.4f}", width=-1, color=imgui.ImVec4(0.2, 0.9, 0.2, 1))
imgui.table_set_column_index(0); render_selectable_label(app, f"tier_{tier}", tier, width=-1)
imgui.table_set_column_index(1); render_selectable_label(app, f"model_{tier}", model.split("-")[0], width=-1)
imgui.table_set_column_index(2); render_selectable_label(app, f"tokens_{tier}", f"{tokens:,}", width=-1)
imgui.table_set_column_index(3); render_selectable_label(app, f"cost_{tier}", f"${cost:.4f}", width=-1, color=imgui.ImVec4(0.2, 0.9, 0.2, 1))
imgui.end_table()
tier_total = sum(cost_tracker.estimate_cost(stats.get('model', ''), stats.get('input', 0), stats.get('output', 0)) for stats in app.mma_tier_usage.values())
ui_shared.render_selectable_label(app, "session_total_cost", f"Session Total: ${tier_total:.4f}", width=-1, color=imgui.ImVec4(0, 1, 0, 1))
render_selectable_label(app, "session_total_cost", f"Session Total: ${tier_total:.4f}", width=-1, color=imgui.ImVec4(0, 1, 0, 1))
else:
imgui.text_disabled("No MMA tier usage data")
if stats.get("would_trim"):
@@ -2004,7 +2061,7 @@ def render_provider_panel(app: App) -> None:
imgui.text("Gemini CLI")
sid = "None"
if hasattr(ai_client, "_gemini_cli_adapter") and ai_client._gemini_cli_adapter: sid = ai_client._gemini_cli_adapter.session_id or "None"
imgui.text("Session ID:"); imgui.same_line(); ui_shared.render_selectable_label(app, "gemini_cli_sid", sid, width=200)
imgui.text("Session ID:"); imgui.same_line(); render_selectable_label(app, "gemini_cli_sid", sid, width=200)
if imgui.button("Reset CLI Session"): ai_client.reset_session()
imgui.text("Binary Path")
ch, app.ui_gemini_cli_path = imgui.input_text("##gcli_path", app.ui_gemini_cli_path)
@@ -3131,7 +3188,7 @@ def render_thinking_trace(app: App, entry: dict, segments: list[dict], entry_ind
else:
imgui.text(content)
else:
ui_shared.render_selectable_label(app, f"think_text_{entry_index}_{idx}", content, multiline=True, height=-1)
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:
@@ -3155,7 +3212,7 @@ def render_discussion_entry(app: App, entry: dict, index: int) -> None:
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}")
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:
@@ -3169,12 +3226,15 @@ 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(ui_shared.C_SUB, str(ts_str))
if ts_str: imgui.text_colored(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)
imgui.new_line()
imgui.spacing()
if collapsed:
imgui.same_line()
if imgui.button("Ins"): app.disc_entries.insert(index, {"role": "User", "content": "", "collapsed": True, "ts": project_manager.now_ts()})
@@ -3187,7 +3247,7 @@ 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.C_SUB, preview)
imgui.text_colored(C_SUB, preview)
else:
# Body content
imgui.spacing()
@@ -3582,7 +3642,7 @@ def render_discussion_metadata(app: App) -> None:
imgui.separator()
imgui.text_colored(C_LBL, "commit:"); imgui.same_line()
ui_shared.render_selectable_label(app, 'git_commit_val', git_commit[:12] if git_commit else '(none)', width=100, color=(C_IN if git_commit else C_LBL))
render_selectable_label(app, '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()
if imgui.button("Update Commit"):
if app.ui_project_git_dir:
@@ -3882,12 +3942,12 @@ def render_tool_calls_panel(app: App) -> None:
imgui.table_next_column()
script_preview = script.replace("\n", " ")[:150]
if len(script) > 150: script_preview += "..."
ui_shared.render_selectable_label(app, f'tc_script_{i}', script_preview, width=-1)
render_selectable_label(app, f'tc_script_{i}', script_preview, width=-1)
imgui.table_next_column()
res_preview = res.replace("\n", " ")[:30]
if len(res) > 30: res_preview += "..."
ui_shared.render_selectable_label(app, f'tc_res_{i}', res_preview, width=-1)
render_selectable_label(app, f'tc_res_{i}', res_preview, width=-1)
imgui.end_table()
@@ -3944,7 +4004,7 @@ def render_text_viewer_window(app: App) -> None:
"""Renders the standalone text/code/markdown viewer window."""
if not app.show_windows.get("Text Viewer", False): return
imgui.set_next_window_size(imgui.ImVec2(900, 700), imgui.Cond_.first_use_ever)
expanded, opened = imgui.begin(f"{app.text_viewer_title or 'Text Viewer'}###Text Viewer", True, imgui.WindowFlags_.no_collapse)
expanded, opened = imgui.begin(f"{app.text_viewer_title or 'Text Viewer'}###Text_Viewer_Stable", True, imgui.WindowFlags_.no_collapse)
app.show_windows["Text Viewer"] = bool(opened)
if not opened:
app.ui_editing_slices_file = None
@@ -4375,7 +4435,7 @@ def render_heavy_text(app: App, label: str, content: str, id_suffix: str = "") -
app.show_windows["Text Viewer"] = True
imgui.same_line()
imgui.text_colored(C_LBL, f"{label}:"); imgui.same_line()
ui_shared.render_selectable_label(app, f"heavy_label_{label}_{id_suffix}", content[:60].replace("\n", " ") + ("..." if len(content)>60 else ""), color=C_VAL)
render_selectable_label(app, f"heavy_label_{label}_{id_suffix}", content[:60].replace("\n", " ") + ("..." if len(content)>60 else ""), color=C_VAL)
if content:
ctx_id = f"{label}_{id_suffix}"
@@ -4713,7 +4773,7 @@ def render_tier_stream_panel(app: App, tier_key: str, stream_key: str | None) ->
if stream_key is not None:
content = app.mma_streams.get(stream_key, "")
imgui.begin_child(f"##stream_content_{tier_key}", imgui.ImVec2(-1, -1))
ui_shared.render_selectable_label(app, f'stream_{tier_key}', content, width=-1, multiline=True, height=0)
render_selectable_label(app, f'stream_{tier_key}', content, width=-1, multiline=True, height=0)
try:
if len(content) != app._tier_stream_last_len.get(stream_key, -1):
imgui.set_scroll_here_y(1.0)
@@ -4740,7 +4800,7 @@ def render_tier_stream_panel(app: App, tier_key: str, stream_key: str | None) ->
else:
imgui.text(f"{ticket_id} [{status}]")
imgui.begin_child(f"##tier3_{ticket_id}_scroll", imgui.ImVec2(-1, 150), True)
ui_shared.render_selectable_label(app, f'stream_t3_{ticket_id}', app.mma_streams[key], width=-1, multiline=True, height=0)
render_selectable_label(app, f'stream_t3_{ticket_id}', app.mma_streams[key], width=-1, multiline=True, height=0)
try:
if len(app.mma_streams[key]) != app._tier_stream_last_len.get(key, -1):
imgui.set_scroll_here_y(1.0)