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:
+15
-14
@@ -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
|
||||
|
||||
@@ -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
@@ -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):
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user