ok
This commit is contained in:
@@ -799,7 +799,6 @@ class AppController:
|
||||
"Tool Calls": False,
|
||||
"Theme": True,
|
||||
"Log Management": False,
|
||||
"Markdown Test": False,
|
||||
}
|
||||
saved = self.config.get("gui", {}).get("show_windows", {})
|
||||
self.show_windows = {k: saved.get(k, v) for k, v in _default_windows.items()}
|
||||
|
||||
63
src/gui_2.py
63
src/gui_2.py
@@ -202,10 +202,10 @@ class App:
|
||||
self.text_viewer_title = label
|
||||
self.text_viewer_content = content
|
||||
|
||||
def _render_heavy_text(self, label: str, content: str) -> None:
|
||||
def _render_heavy_text(self, label: str, content: str, id_suffix: str = "") -> None:
|
||||
imgui.text_colored(C_LBL, f"{label}:")
|
||||
imgui.same_line()
|
||||
if imgui.button("[+]##" + label):
|
||||
if imgui.button("[+]##" + label + id_suffix):
|
||||
self.show_text_viewer = True
|
||||
self.text_viewer_title = label
|
||||
self.text_viewer_content = content
|
||||
@@ -214,13 +214,21 @@ class App:
|
||||
imgui.text_disabled("(empty)")
|
||||
return
|
||||
|
||||
is_md = label in ("message", "text", "content")
|
||||
ctx_id = f"heavy_{label}_{id_suffix}"
|
||||
|
||||
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
|
||||
imgui.begin_child(f"heavy_text_child_{label}_{hash(content)}", imgui.ImVec2(0, 80), True)
|
||||
self._render_selectable_label(f'heavy_val_{label}_{hash(content)}', content, width=-1, multiline=True, height=80)
|
||||
imgui.begin_child(f"heavy_text_child_{label}_{id_suffix}", imgui.ImVec2(0, 80), True)
|
||||
if is_md:
|
||||
markdown_helper.render(content, context_id=ctx_id)
|
||||
else:
|
||||
markdown_helper.render_code(content, context_id=ctx_id)
|
||||
imgui.end_child()
|
||||
else:
|
||||
self._render_selectable_label(f'heavy_val_{label}_{hash(content)}', content, width=-1, multiline=self.ui_word_wrap, height=0)
|
||||
if is_md:
|
||||
markdown_helper.render(content, context_id=ctx_id)
|
||||
else:
|
||||
markdown_helper.render_code(content, context_id=ctx_id)
|
||||
# ---------------------------------------------------------------- gui
|
||||
|
||||
|
||||
@@ -521,13 +529,6 @@ class App:
|
||||
if self.show_windows.get("Diagnostics", False):
|
||||
self._render_diagnostics_panel()
|
||||
|
||||
if self.show_windows.get("Markdown Test", False):
|
||||
exp, opened = imgui.begin("Markdown Test", self.show_windows["Markdown Test"])
|
||||
self.show_windows["Markdown Test"] = bool(opened)
|
||||
if exp:
|
||||
self._render_markdown_test()
|
||||
imgui.end()
|
||||
|
||||
self.perf_monitor.end_frame()
|
||||
# ---- Modals / Popups
|
||||
with self._pending_dialog_lock:
|
||||
@@ -1387,7 +1388,7 @@ def hello():
|
||||
if len(content) > 80: preview += "..."
|
||||
imgui.text_colored(vec4(180, 180, 180), preview)
|
||||
else:
|
||||
self._render_selectable_label(f'prior_content_val_{idx}', content, width=-1, multiline=True, height=150)
|
||||
markdown_helper.render(content, context_id=f'prior_disc_{idx}')
|
||||
|
||||
imgui.separator()
|
||||
imgui.pop_id()
|
||||
@@ -1546,14 +1547,14 @@ def hello():
|
||||
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)
|
||||
markdown_helper.render(content, context_id=f'disc_{i}')
|
||||
else:
|
||||
imgui.begin_child("read_content", imgui.ImVec2(0, 150), True)
|
||||
imgui.begin_child(f"read_content_{i}", imgui.ImVec2(0, 150), True)
|
||||
if self.ui_word_wrap: imgui.push_text_wrap_pos(imgui.get_content_region_avail().x)
|
||||
last_idx = 0
|
||||
for m_idx, match in enumerate(matches):
|
||||
before = content[last_idx:match.start()]
|
||||
if before: self._render_selectable_label(f'read_before_{i}_{m_idx}', before, width=-1, multiline=True, height=0)
|
||||
if before: markdown_helper.render(before, context_id=f'disc_{i}_b_{m_idx}')
|
||||
header_text = match.group(0).split("\n")[0].strip()
|
||||
path = match.group(2)
|
||||
code_block = match.group(4)
|
||||
@@ -1565,16 +1566,11 @@ def hello():
|
||||
self.text_viewer_content = res
|
||||
self.show_text_viewer = True
|
||||
if code_block:
|
||||
code_content = code_block.strip()
|
||||
if code_content.count("\n") + 1 > 50:
|
||||
imgui.begin_child(f"code_{i}_{match.start()}", imgui.ImVec2(0, 200), True)
|
||||
imgui.text(code_content)
|
||||
imgui.end_child()
|
||||
else:
|
||||
imgui.text(code_content)
|
||||
# Render code block with highlighting
|
||||
markdown_helper.render(code_block, context_id=f'disc_{i}_c_{m_idx}')
|
||||
last_idx = match.end()
|
||||
after = content[last_idx:]
|
||||
if after: self._render_selectable_label(f'read_after_{i}_{last_idx}', after, width=-1, multiline=True, height=0)
|
||||
if after: markdown_helper.render(after, context_id=f'disc_{i}_a')
|
||||
if self.ui_word_wrap: imgui.pop_text_wrap_pos()
|
||||
imgui.end_child()
|
||||
else:
|
||||
@@ -1892,7 +1888,7 @@ def hello():
|
||||
# --- Always Render Content ---
|
||||
|
||||
imgui.begin_child("response_scroll_area", imgui.ImVec2(0, -40), True)
|
||||
imgui.input_text_multiline("##ai_out", self.ai_response, imgui.ImVec2(-1, -1), imgui.InputTextFlags_.read_only)
|
||||
markdown_helper.render(self.ai_response, context_id="response")
|
||||
imgui.end_child()
|
||||
|
||||
imgui.separator()
|
||||
@@ -1981,24 +1977,25 @@ def hello():
|
||||
imgui.text_colored(C_SUB, f"[{tier}]")
|
||||
|
||||
# Optimized content rendering using _render_heavy_text logic
|
||||
idx_str = str(i)
|
||||
if kind == "request":
|
||||
self._render_heavy_text("message", payload.get("message", ""))
|
||||
self._render_heavy_text("message", payload.get("message", ""), idx_str)
|
||||
if payload.get("system"):
|
||||
self._render_heavy_text("system", payload.get("system", ""))
|
||||
self._render_heavy_text("system", payload.get("system", ""), idx_str)
|
||||
elif kind == "response":
|
||||
r = payload.get("round", 0)
|
||||
sr = payload.get("stop_reason", "STOP")
|
||||
imgui.text_colored(C_LBL, f"round: {r} stop_reason: {sr}")
|
||||
self._render_heavy_text("text", payload.get("text", ""))
|
||||
self._render_heavy_text("text", payload.get("text", ""), idx_str)
|
||||
tcs = payload.get("tool_calls", [])
|
||||
if tcs:
|
||||
self._render_heavy_text("tool_calls", json.dumps(tcs, indent=1))
|
||||
self._render_heavy_text("tool_calls", json.dumps(tcs, indent=1), idx_str)
|
||||
elif kind == "tool_call":
|
||||
self._render_heavy_text(payload.get("name", "call"), payload.get("script") or json.dumps(payload.get("args", {}), indent=1))
|
||||
self._render_heavy_text(payload.get("name", "call"), payload.get("script") or json.dumps(payload.get("args", {}), indent=1), idx_str)
|
||||
elif kind == "tool_result":
|
||||
self._render_heavy_text(payload.get("name", "result"), payload.get("output", ""))
|
||||
self._render_heavy_text(payload.get("name", "result"), payload.get("output", ""), idx_str)
|
||||
else:
|
||||
self._render_heavy_text("data", str(payload))
|
||||
self._render_heavy_text("data", str(payload), idx_str)
|
||||
|
||||
imgui.separator()
|
||||
imgui.pop_id()
|
||||
|
||||
@@ -1,46 +1,155 @@
|
||||
# src/markdown_helper.py
|
||||
from __future__ import annotations
|
||||
from imgui_bundle import imgui_md, imgui, immapp
|
||||
from imgui_bundle import imgui_md, imgui, immapp, imgui_color_text_edit as ed
|
||||
import webbrowser
|
||||
import os
|
||||
from typing import Optional
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Callable
|
||||
|
||||
class MarkdownRenderer:
|
||||
"""
|
||||
Wrapper for imgui_md to manage styling, callbacks, and specialized rendering
|
||||
(like syntax highlighting integration).
|
||||
Hybrid Markdown renderer that uses imgui_md for text/headers
|
||||
and ImGuiColorTextEdit for syntax-highlighted code blocks.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.options = imgui_md.MarkdownOptions()
|
||||
# Use Inter as the base font for Markdown (matches professional theme)
|
||||
# It expects fonts like Inter-Regular.ttf, Inter-Bold.ttf, etc. in the assets folder
|
||||
# Base path for fonts (Inter family)
|
||||
self.options.font_options.font_base_path = "fonts/Inter"
|
||||
self.options.font_options.regular_size = 16.0
|
||||
|
||||
# Configure callbacks
|
||||
self.options.callbacks.on_open_link = self._on_open_link
|
||||
|
||||
# Note: Syntax highlighting will be integrated in Phase 2
|
||||
# Cache for TextEditor instances to maintain state
|
||||
self._editor_cache: Dict[tuple[str, int], ed.TextEditor] = {}
|
||||
self._max_cache_size = 100
|
||||
|
||||
# Optional callback for custom local link handling (e.g., opening in IDE)
|
||||
self.on_local_link: Optional[Callable[[str], None]] = None
|
||||
|
||||
# Language mapping for ImGuiColorTextEdit
|
||||
self._lang_map = {
|
||||
"python": ed.TextEditor.LanguageDefinitionId.python,
|
||||
"py": ed.TextEditor.LanguageDefinitionId.python,
|
||||
"json": ed.TextEditor.LanguageDefinitionId.json,
|
||||
"cpp": ed.TextEditor.LanguageDefinitionId.cpp,
|
||||
"c++": ed.TextEditor.LanguageDefinitionId.cpp,
|
||||
"c": ed.TextEditor.LanguageDefinitionId.c,
|
||||
"lua": ed.TextEditor.LanguageDefinitionId.lua,
|
||||
"sql": ed.TextEditor.LanguageDefinitionId.sql,
|
||||
"cs": ed.TextEditor.LanguageDefinitionId.cs,
|
||||
"c#": ed.TextEditor.LanguageDefinitionId.cs,
|
||||
}
|
||||
|
||||
def _on_open_link(self, url: str) -> None:
|
||||
"""Handle link clicks in Markdown."""
|
||||
# If it's a URL, open in browser
|
||||
if url.startswith("http"):
|
||||
webbrowser.open(url)
|
||||
else:
|
||||
# Handle local files or internal links
|
||||
# For now, just print. Could integrate with app_controller in Phase 4.
|
||||
print(f"Clicked local link: {url}")
|
||||
# Try to handle as a local file path
|
||||
try:
|
||||
p = Path(url)
|
||||
if p.exists():
|
||||
if self.on_local_link:
|
||||
self.on_local_link(str(p.absolute()))
|
||||
else:
|
||||
# Fallback to OS default handler
|
||||
webbrowser.open(str(p.absolute()))
|
||||
else:
|
||||
print(f"Link target does not exist: {url}")
|
||||
except Exception as e:
|
||||
print(f"Error opening link {url}: {e}")
|
||||
|
||||
def render(self, text: str) -> None:
|
||||
"""Render Markdown text using imgui_md."""
|
||||
imgui_md.render(text)
|
||||
def render(self, text: str, context_id: str = "default") -> None:
|
||||
"""Render Markdown text with code block interception."""
|
||||
if not text:
|
||||
return
|
||||
|
||||
# Split into markdown and code blocks
|
||||
parts = re.split(r'(```[\s\S]*?```)', text)
|
||||
|
||||
block_idx = 0
|
||||
for part in parts:
|
||||
if part.startswith('```') and part.endswith('```'):
|
||||
self._render_code_block(part, context_id, block_idx)
|
||||
block_idx += 1
|
||||
elif part.strip():
|
||||
imgui_md.render(part)
|
||||
|
||||
def render_unindented(self, text: str) -> None:
|
||||
"""Render Markdown text with automatic unindentation."""
|
||||
imgui_md.render_unindented(text)
|
||||
|
||||
# Global instance for easy access
|
||||
def render_code(self, code: str, lang: str = "", context_id: str = "default", block_idx: int = 0) -> None:
|
||||
"""Render a code block directly with syntax highlighting."""
|
||||
# Wrap in fake markdown markers for the internal renderer
|
||||
self._render_code_block(f"```{lang}\n{code}```", context_id, block_idx)
|
||||
|
||||
def _render_code_block(self, block: str, context_id: str, block_idx: int) -> None:
|
||||
"""Render a code block using TextEditor for syntax highlighting."""
|
||||
lines = block.strip('`').split('\n')
|
||||
lang_tag = lines[0].strip().lower() if lines else ""
|
||||
|
||||
# Heuristic to separate lang tag from code
|
||||
if lang_tag and lang_tag not in self._lang_map and not self._is_likely_lang_tag(lang_tag):
|
||||
lang_tag = ""
|
||||
code = '\n'.join(lines)
|
||||
else:
|
||||
code = '\n'.join(lines[1:]) if len(lines) > 1 else ""
|
||||
|
||||
if not lang_tag:
|
||||
lang_tag = self.detect_language(code)
|
||||
|
||||
# Cache management
|
||||
if len(self._editor_cache) > self._max_cache_size:
|
||||
# Simple LRU-ish: just clear it all if it gets too big
|
||||
self._editor_cache.clear()
|
||||
|
||||
cache_key = (context_id, block_idx)
|
||||
if cache_key not in self._editor_cache:
|
||||
editor = ed.TextEditor()
|
||||
editor.set_read_only_enabled(True)
|
||||
editor.set_show_line_numbers_enabled(True)
|
||||
self._editor_cache[cache_key] = editor
|
||||
|
||||
editor = self._editor_cache[cache_key]
|
||||
|
||||
# Sync text and language
|
||||
lang_id = self._lang_map.get(lang_tag, ed.TextEditor.LanguageDefinitionId.none)
|
||||
target_text = code + "\n"
|
||||
|
||||
if editor.get_text() != target_text:
|
||||
editor.set_text(code)
|
||||
editor.set_language_definition(lang_id)
|
||||
elif editor.get_language_definition_name().lower() != lang_tag:
|
||||
# get_language_definition_name might not match exactly but good enough check
|
||||
editor.set_language_definition(lang_id)
|
||||
|
||||
# Dynamic height calculation
|
||||
line_count = code.count('\n') + 1
|
||||
line_height = imgui.get_text_line_height()
|
||||
height = (line_count * line_height) + 20
|
||||
height = min(max(height, 40), 500)
|
||||
|
||||
editor.render(f"##code_{context_id}_{block_idx}", a_size=imgui.ImVec2(0, height))
|
||||
|
||||
def _is_likely_lang_tag(self, tag: str) -> bool:
|
||||
return bool(re.match(r'^[a-zA-Z0-9+#-]+$', tag)) and len(tag) < 15
|
||||
|
||||
def detect_language(self, code: str) -> str:
|
||||
if "def " in code or "import " in code:
|
||||
return "python"
|
||||
if "{" in code and '"' in code and ":" in code:
|
||||
return "json"
|
||||
if "$" in code and ("{" in code or "if" in code):
|
||||
return "powershell"
|
||||
return ""
|
||||
|
||||
def clear_cache(self) -> None:
|
||||
self._editor_cache.clear()
|
||||
|
||||
# Global instance
|
||||
_renderer: Optional[MarkdownRenderer] = None
|
||||
|
||||
def get_renderer() -> MarkdownRenderer:
|
||||
@@ -49,8 +158,11 @@ def get_renderer() -> MarkdownRenderer:
|
||||
_renderer = MarkdownRenderer()
|
||||
return _renderer
|
||||
|
||||
def render(text: str) -> None:
|
||||
get_renderer().render(text)
|
||||
def render(text: str, context_id: str = "default") -> None:
|
||||
get_renderer().render(text, context_id)
|
||||
|
||||
def render_unindented(text: str) -> None:
|
||||
get_renderer().render_unindented(text)
|
||||
|
||||
def render_code(code: str, lang: str = "", context_id: str = "default", block_idx: int = 0) -> None:
|
||||
get_renderer().render_code(code, lang, context_id, block_idx)
|
||||
|
||||
@@ -32,7 +32,7 @@ def draw_soft_shadow(draw_list: imgui.ImDrawList, p_min: imgui.ImVec2, p_max: im
|
||||
c_max,
|
||||
u32_color,
|
||||
rounding + expand if rounding > 0 else 0.0,
|
||||
flags=imgui.DrawFlags_.round_corners_all if rounding > 0 else imgui.DrawFlags_.none,
|
||||
flags=imgui.ImDrawFlags_.round_corners_all if rounding > 0 else imgui.ImDrawFlags_.none,
|
||||
thickness=1.0
|
||||
)
|
||||
|
||||
@@ -47,7 +47,7 @@ def apply_faux_acrylic_glass(draw_list: imgui.ImDrawList, p_min: imgui.ImVec2, p
|
||||
fill_color = imgui.get_color_u32(imgui.ImVec4(r, g, b, a * 0.7))
|
||||
draw_list.add_rect_filled(
|
||||
p_min, p_max, fill_color, rounding,
|
||||
flags=imgui.DrawFlags_.round_corners_all if rounding > 0 else imgui.DrawFlags_.none
|
||||
flags=imgui.ImDrawFlags_.round_corners_all if rounding > 0 else imgui.ImDrawFlags_.none
|
||||
)
|
||||
|
||||
# 2. Gradient overlay to simulate light scattering (acrylic reflection)
|
||||
@@ -67,6 +67,6 @@ def apply_faux_acrylic_glass(draw_list: imgui.ImDrawList, p_min: imgui.ImVec2, p
|
||||
imgui.ImVec2(p_min.x + 1, p_min.y + 1),
|
||||
imgui.ImVec2(p_max.x - 1, p_max.y - 1),
|
||||
inner_glow, rounding,
|
||||
flags=imgui.DrawFlags_.round_corners_all if rounding > 0 else imgui.DrawFlags_.none,
|
||||
flags=imgui.ImDrawFlags_.round_corners_all if rounding > 0 else imgui.ImDrawFlags_.none,
|
||||
thickness=1.0
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user