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)
except Exception:
pass
elif _provider == "gemini_cli":
elif p == "gemini_cli":
try:
_ensure_gemini_client()
if _gemini_client:
@@ -2527,22 +2527,22 @@ def send(
md_content, user_message, base_dir, file_items, discussion_history,
pre_tool_callback, qa_callback, enable_tools, stream_callback, patch_callback
)
elif _provider == "gemini_cli":
elif p == "gemini_cli":
res = _send_gemini_cli(
md_content, user_message, base_dir, file_items, discussion_history,
pre_tool_callback, qa_callback, stream_callback, patch_callback
)
elif _provider == "anthropic":
elif p == "anthropic":
res = _send_anthropic(
md_content, user_message, base_dir, file_items, discussion_history,
pre_tool_callback, qa_callback, stream_callback=stream_callback, patch_callback=patch_callback
)
elif _provider == "deepseek":
elif p == "deepseek":
res = _send_deepseek(
md_content, user_message, base_dir, file_items, discussion_history,
stream, pre_tool_callback, qa_callback, stream_callback, patch_callback
)
elif _provider == "minimax":
elif p == "minimax":
res = _send_minimax(
md_content, user_message, base_dir, file_items, discussion_history,
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 ""
elif _provider == "anthropic":
elif p == "anthropic":
_ensure_anthropic_client()
if _anthropic_client:
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}]
)
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()
api_key = creds.get("deepseek", {}).get("api_key")
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"]
except Exception as e:
return f"ERROR: DeepSeek summarization failed: {e}"
elif _provider == "gemini_cli":
elif p == "gemini_cli":
# Using the adapter for a one-off call
adapter = GeminiCliAdapter(binary_path="gemini")
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"
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}"
if _provider == "gemini":
if p == "gemini":
_ensure_gemini_client()
if _gemini_client:
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)
)
return resp.text or ""
elif _provider == "anthropic":
elif p == "anthropic":
_ensure_anthropic_client()
if _anthropic_client:
resp = _anthropic_client.messages.create(
@@ -2649,7 +2650,7 @@ def run_discussion_compression(discussion_text: str) -> str:
messages=[{"role": "user", "content": prompt}]
)
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()
api_key = creds.get("deepseek", {}).get("api_key")
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"]
except Exception as e:
return f"ERROR: DeepSeek compression failed: {e}"
elif _provider == "minimax":
elif p == "minimax":
_ensure_minimax_client()
if _minimax_client:
resp = _minimax_client.chat.completions.create(
@@ -2669,10 +2670,10 @@ def run_discussion_compression(discussion_text: str) -> str:
max_tokens=2048
)
return resp.choices[0].message.content or ""
elif _provider == "gemini_cli":
elif p == "gemini_cli":
adapter = GeminiCliAdapter(binary_path="gemini")
resp_data = adapter.send(prompt, model=_model)
return resp_data.get("text", "")
return "ERROR: Unsupported provider for discussion compression"
return f"ERROR: Unsupported provider for discussion compression: '{p}'"
#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 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
@@ -3112,129 +3113,12 @@ def render_discussion_hub(app: App) -> None:
return
def render_discussion_entry(app: App, entry: dict, index: int) -> None:
with imscope.id(f"disc_{index}"):
role = entry.get("role", "User")
# 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()
from src import discussion_entry_renderer
discussion_entry_renderer.render_discussion_entry(app, entry, index)
def render_discussion_entry_read_mode(app: App, entry: dict, index: int) -> None:
content = entry["content"]
if not content.strip(): return
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()
from src import discussion_entry_renderer
discussion_entry_renderer.render_discussion_entry_read_mode(app, entry, index)
def render_history_window(app: App) -> None:
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