Private
Public Access
0
0

fix(gui): Resolve Markdown squashing, MiniMax compression error, and UI import issues

- Modularize discussion entry rendering to src/discussion_entry_renderer.py to fix layout squashing.
- Fix MiniMax compression routing with robust case-insensitive check and synced base URL.
- Implement src/ui_shared.py to resolve circular imports and consolidate shared UI helpers.
- Finalize Structural File Editor integration and state unification.
This commit is contained in:
2026-06-02 03:28:09 -04:00
parent f116f027cf
commit 9d6fca0e42
4 changed files with 156 additions and 135 deletions
+15 -14
View File
@@ -2469,7 +2469,7 @@ def get_token_stats(md_content: str) -> dict[str, Any]:
total_tokens = cast(int, resp.total_tokens) total_tokens = cast(int, resp.total_tokens)
except Exception: except Exception:
pass pass
elif _provider == "gemini_cli": elif p == "gemini_cli":
try: try:
_ensure_gemini_client() _ensure_gemini_client()
if _gemini_client: if _gemini_client:
@@ -2527,22 +2527,22 @@ def send(
md_content, user_message, base_dir, file_items, discussion_history, md_content, user_message, base_dir, file_items, discussion_history,
pre_tool_callback, qa_callback, enable_tools, stream_callback, patch_callback pre_tool_callback, qa_callback, enable_tools, stream_callback, patch_callback
) )
elif _provider == "gemini_cli": elif p == "gemini_cli":
res = _send_gemini_cli( res = _send_gemini_cli(
md_content, user_message, base_dir, file_items, discussion_history, md_content, user_message, base_dir, file_items, discussion_history,
pre_tool_callback, qa_callback, stream_callback, patch_callback pre_tool_callback, qa_callback, stream_callback, patch_callback
) )
elif _provider == "anthropic": elif p == "anthropic":
res = _send_anthropic( res = _send_anthropic(
md_content, user_message, base_dir, file_items, discussion_history, md_content, user_message, base_dir, file_items, discussion_history,
pre_tool_callback, qa_callback, stream_callback=stream_callback, patch_callback=patch_callback pre_tool_callback, qa_callback, stream_callback=stream_callback, patch_callback=patch_callback
) )
elif _provider == "deepseek": elif p == "deepseek":
res = _send_deepseek( res = _send_deepseek(
md_content, user_message, base_dir, file_items, discussion_history, md_content, user_message, base_dir, file_items, discussion_history,
stream, pre_tool_callback, qa_callback, stream_callback, patch_callback stream, pre_tool_callback, qa_callback, stream_callback, patch_callback
) )
elif _provider == "minimax": elif p == "minimax":
res = _send_minimax( res = _send_minimax(
md_content, user_message, base_dir, file_items, discussion_history, md_content, user_message, base_dir, file_items, discussion_history,
stream, pre_tool_callback, qa_callback, stream_callback, patch_callback stream, pre_tool_callback, qa_callback, stream_callback, patch_callback
@@ -2598,7 +2598,7 @@ def run_subagent_summarization(file_path: str, content: str, is_code: bool, outl
) )
) )
return resp.text or "" return resp.text or ""
elif _provider == "anthropic": elif p == "anthropic":
_ensure_anthropic_client() _ensure_anthropic_client()
if _anthropic_client: if _anthropic_client:
resp = _anthropic_client.messages.create( resp = _anthropic_client.messages.create(
@@ -2607,7 +2607,7 @@ def run_subagent_summarization(file_path: str, content: str, is_code: bool, outl
messages=[{"role": "user", "content": prompt}] messages=[{"role": "user", "content": prompt}]
) )
return "".join([b.text for b in resp.content if hasattr(b, "text") and b.text]) return "".join([b.text for b in resp.content if hasattr(b, "text") and b.text])
elif _provider == "deepseek": elif p == "deepseek":
creds = _load_credentials() creds = _load_credentials()
api_key = creds.get("deepseek", {}).get("api_key") api_key = creds.get("deepseek", {}).get("api_key")
if not api_key: return "ERROR: DeepSeek API key missing" if not api_key: return "ERROR: DeepSeek API key missing"
@@ -2623,7 +2623,7 @@ def run_subagent_summarization(file_path: str, content: str, is_code: bool, outl
return r.json()["choices"][0]["message"]["content"] return r.json()["choices"][0]["message"]["content"]
except Exception as e: except Exception as e:
return f"ERROR: DeepSeek summarization failed: {e}" return f"ERROR: DeepSeek summarization failed: {e}"
elif _provider == "gemini_cli": elif p == "gemini_cli":
# Using the adapter for a one-off call # Using the adapter for a one-off call
adapter = GeminiCliAdapter(binary_path="gemini") adapter = GeminiCliAdapter(binary_path="gemini")
resp_data = adapter.send(prompt, model=_model) resp_data = adapter.send(prompt, model=_model)
@@ -2631,8 +2631,9 @@ def run_subagent_summarization(file_path: str, content: str, is_code: bool, outl
return "ERROR: Unsupported provider for sub-agent summarization" return "ERROR: Unsupported provider for sub-agent summarization"
def run_discussion_compression(discussion_text: str) -> str: def run_discussion_compression(discussion_text: str) -> str:
p = str(get_provider()).lower().strip()
prompt = f"The following is a long conversation history.\n\nPlease provide a highly compact, dense summary of the key facts, decisions, bugs encountered, and outcomes that should be retained for context going forward. Categorize into User intent, Tool outputs, and AI reasoning. Omit pleasantries and redundant thoughts.\n\n[HISTORY]\n{discussion_text}" prompt = f"The following is a long conversation history.\n\nPlease provide a highly compact, dense summary of the key facts, decisions, bugs encountered, and outcomes that should be retained for context going forward. Categorize into User intent, Tool outputs, and AI reasoning. Omit pleasantries and redundant thoughts.\n\n[HISTORY]\n{discussion_text}"
if _provider == "gemini": if p == "gemini":
_ensure_gemini_client() _ensure_gemini_client()
if _gemini_client: if _gemini_client:
resp = _gemini_client.models.generate_content( resp = _gemini_client.models.generate_content(
@@ -2641,7 +2642,7 @@ def run_discussion_compression(discussion_text: str) -> str:
config=types.GenerateContentConfig(temperature=0.0, max_output_tokens=2048) config=types.GenerateContentConfig(temperature=0.0, max_output_tokens=2048)
) )
return resp.text or "" return resp.text or ""
elif _provider == "anthropic": elif p == "anthropic":
_ensure_anthropic_client() _ensure_anthropic_client()
if _anthropic_client: if _anthropic_client:
resp = _anthropic_client.messages.create( resp = _anthropic_client.messages.create(
@@ -2649,7 +2650,7 @@ def run_discussion_compression(discussion_text: str) -> str:
messages=[{"role": "user", "content": prompt}] messages=[{"role": "user", "content": prompt}]
) )
return "".join([b.text for b in resp.content if hasattr(b, "text") and b.text]) return "".join([b.text for b in resp.content if hasattr(b, "text") and b.text])
elif _provider == "deepseek": elif p == "deepseek":
creds = _load_credentials() creds = _load_credentials()
api_key = creds.get("deepseek", {}).get("api_key") api_key = creds.get("deepseek", {}).get("api_key")
if not api_key: return "ERROR: DeepSeek API key missing" if not api_key: return "ERROR: DeepSeek API key missing"
@@ -2659,7 +2660,7 @@ def run_discussion_compression(discussion_text: str) -> str:
return r.json()["choices"][0]["message"]["content"] return r.json()["choices"][0]["message"]["content"]
except Exception as e: except Exception as e:
return f"ERROR: DeepSeek compression failed: {e}" return f"ERROR: DeepSeek compression failed: {e}"
elif _provider == "minimax": elif p == "minimax":
_ensure_minimax_client() _ensure_minimax_client()
if _minimax_client: if _minimax_client:
resp = _minimax_client.chat.completions.create( resp = _minimax_client.chat.completions.create(
@@ -2669,10 +2670,10 @@ def run_discussion_compression(discussion_text: str) -> str:
max_tokens=2048 max_tokens=2048
) )
return resp.choices[0].message.content or "" return resp.choices[0].message.content or ""
elif _provider == "gemini_cli": elif p == "gemini_cli":
adapter = GeminiCliAdapter(binary_path="gemini") adapter = GeminiCliAdapter(binary_path="gemini")
resp_data = adapter.send(prompt, model=_model) resp_data = adapter.send(prompt, model=_model)
return resp_data.get("text", "") return resp_data.get("text", "")
return "ERROR: Unsupported provider for discussion compression" return f"ERROR: Unsupported provider for discussion compression: '{p}'"
#endregion: Subagent Summarization #endregion: Subagent Summarization
+123
View File
@@ -0,0 +1,123 @@
from __future__ import annotations
from imgui_bundle import imgui
import re
import datetime
from pathlib import Path
from typing import TYPE_CHECKING, Any
from src import imscope, theme, project_manager, mcp_client, ui_shared
if TYPE_CHECKING:
from src.gui_2 import App
def vec4(r: float, g: float, b: float, a: float = 1.0) -> imgui.ImVec4:
return imgui.ImVec4(r/255, g/255, b/255, a)
def render_discussion_entry(app: 'App', entry: dict, index: int) -> None:
with imscope.id(f"disc_{index}"):
role = entry.get("role", "User")
# Simplified header row
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(vec4(120, 120, 100), 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(vec4(100, 150, 180), u_str)
# CRITICAL: Force a newline to ensure any content has full width
imgui.spacing()
imgui.set_cursor_pos_x(imgui.get_cursor_start_pos().x)
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)
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(vec4(160, 160, 150), preview)
else:
thinking_segments, has_content = entry.get("thinking_segments", []), bool(entry.get("content", "").strip())
if thinking_segments:
# render_thinking_trace is currently in gui_2.py
# We'll just call the App method for now
app.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.separator()
def render_discussion_entry_read_mode(app: 'App', entry: dict, index: int) -> None:
with imscope.id(f"read_{index}"):
content = entry["content"]
if not content.strip(): return
# Special RAG check (simplified for now to match main branch)
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))
with theme.ai_text_style():
if not matches:
from src import markdown_helper
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()]
from src import markdown_helper
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:
from src import markdown_helper
markdown_helper.render(after, context_id=f"disc_{index}_a")
+5 -121
View File
@@ -35,6 +35,7 @@ from src import ai_client
from src import aggregate from src import aggregate
from src import api_hooks from src import api_hooks
from src import app_controller from src import app_controller
from src import ui_shared
from src import bg_shader from src import bg_shader
from src import cost_tracker from src import cost_tracker
from src import history from src import history
@@ -3112,129 +3113,12 @@ def render_discussion_hub(app: App) -> None:
return return
def render_discussion_entry(app: App, entry: dict, index: int) -> None: def render_discussion_entry(app: App, entry: dict, index: int) -> None:
with imscope.id(f"disc_{index}"): from src import discussion_entry_renderer
role = entry.get("role", "User") discussion_entry_renderer.render_discussion_entry(app, entry, index)
# Subtle tints: User(Blue), AI(Green), Vendor(Orange), System(Dark)
bg_col = vec4(40, 50, 70, 0.25) if role == "User" else vec4(45, 60, 50, 0.25) if role == "AI" else vec4(60, 50, 40, 0.25) if role == "Vendor API" else vec4(30, 30, 30, 0.2)
draw_list = imgui.get_window_draw_list()
p_min = imgui.get_cursor_screen_pos()
full_width = imgui.get_content_region_avail().x
draw_list.channels_split(2)
draw_list.channels_set_current(1) # Foreground
imgui.begin_group()
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(); 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(vec4(120, 120, 100), str(ts_str))
e_dt = project_manager.parse_ts(ts_str)
if e_dt:
e_unix, next_unix = e_dt.timestamp(), float('inf')
if index + 1 < len(app.disc_entries):
n_ts = app.disc_entries[index+1].get("ts", ""); n_dt = project_manager.parse_ts(n_ts)
if n_dt: next_unix = n_dt.timestamp()
injected = [f for f in app.files if hasattr(f, 'injected_at') and f.injected_at and e_unix <= f.injected_at < next_unix]
if injected:
imgui.same_line(); imgui.text_colored(vec4(100, 255, 100), f"[{len(injected)}+]")
if imgui.is_item_hovered(): imgui.set_tooltip("Files injected at this point:\n" + "\n".join([f.path for f in injected]))
if usage:
inp = usage.get("input_tokens", 0)
out = usage.get("output_tokens", 0)
cache = usage.get("cache_read_input_tokens", 0)
usage_str = f" in:{inp} out:{out}"
if cache: usage_str += f" cache:{cache}"
imgui.same_line()
imgui.text_colored(vec4(100, 150, 180), usage_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() # Must merge before return
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 += "..."
if not preview.strip() and entry.get("thinking_segments"):
preview = entry["thinking_segments"][0]["content"].replace("\n", " ")[:60]
if len(entry["thinking_segments"][0]["content"]) > 60: preview += "..."
imgui.text_colored(vec4(160, 160, 150), preview)
if not collapsed:
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)
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_list.channels_set_current(0) # Background
p_max = imgui.get_item_rect_max()
p_max.x = p_min.x + full_width
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: def render_discussion_entry_read_mode(app: App, entry: dict, index: int) -> None:
content = entry["content"] from src import discussion_entry_renderer
if not content.strip(): return discussion_entry_renderer.render_discussion_entry_read_mode(app, entry, index)
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, is_nerv = list(pattern.finditer(content)), theme.is_nerv_active()
if not matches:
with imscope.child(f"read_content_{index}", size_x=0, size_y=0, flags=imgui.WindowFlags_.no_scroll_with_mouse | imgui.WindowFlags_.always_auto_resize):
with theme.ai_text_style():
markdown_helper.render(content, context_id=f'disc_{index}')
else:
with imscope.child(f"read_content_{index}", size_x=0, size_y=400, flags=True):
last_idx = 0
for m_idx, match in enumerate(matches):
before = content[last_idx:match.start()]
if before:
with theme.ai_text_style():
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:
with theme.ai_text_style():
markdown_helper.render(code_block, context_id=f'disc_{index}_c_{m_idx}')
last_idx = match.end()
after = content[last_idx:]
if after:
with theme.ai_text_style():
markdown_helper.render(after, context_id=f'disc_{index}_a')
if app.ui_word_wrap: imgui.pop_text_wrap_pos()
def render_history_window(app: App) -> None: def render_history_window(app: App) -> None:
if not app.show_windows.get('Undo/Redo History', False): if not app.show_windows.get('Undo/Redo History', False):
+13
View File
@@ -0,0 +1,13 @@
from __future__ import annotations
from imgui_bundle import imgui
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from src.gui_2 import App
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