Private
Public Access
0
0

fix(markdown): rewrite render() to walk lines (no text replacement)

ROOT CAUSE: src/markdown_helper.py:render() used a 'mask text with placeholders
then re.split' approach that failed when AI responses contained CRLF or when
the same table content appeared twice. The replace() either didn't match
(CRLF mismatch) or only replaced the first occurrence, leaving the second
table as raw markdown for imgui_md to render badly. Result: the same table
appeared twice (bad rendering via imgui_md, good rendering via my new code).

FIX: rewrite render() to walk lines directly. Per-line, decide whether to
buffer for imgui_md, skip into a table renderer, or accumulate into a
code-block renderer. No text replacement needed.

- src/markdown_helper.py: new render() walks lines, handles code fences
  and table intervals inline via lookup dicts.
- src/gui_2.py: render_log_management now calls load_registry() on the
  newly-created LogRegistry when _log_registry was None. Previously the
  initial construction populated an empty table, AND the 'Refresh Registry'
  button was inside the else branch, so users had no way to load data.
  User re-indented the surrounding block during debugging.

Tests:
- test_markdown_render_robust.py: 2 tests (CRLF text, duplicate content)
- test_log_management_first_open.py: 1 test (registry populated on open)
40/40 broad regression pass.
This commit is contained in:
2026-06-03 13:14:49 -04:00
parent 9396154779
commit 91fe07f72a
4 changed files with 214 additions and 84 deletions
+65 -64
View File
@@ -1671,71 +1671,71 @@ def render_log_management(app: App) -> None:
if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_log_management")
with imscope.window("Log Management", app.show_windows["Log Management"]) as (exp, opened):
app.show_windows["Log Management"] = bool(opened)
if exp:
if app._log_registry is None: app._log_registry = log_registry.LogRegistry(str(paths.get_logs_dir() / "log_registry.toml"))
else:
if imgui.button("Refresh Registry"):
if app._log_registry is not None: app._log_registry.load_registry()
imgui.same_line()
if imgui.button("Load Log"): app.cb_load_prior_log()
imgui.same_line()
if imgui.button("Force Prune Logs"): app.controller.event_queue.put("gui_task", {"action": "click", "item": "btn_prune_logs"})
if app._log_registry is None:
app._log_registry = log_registry.LogRegistry(str(paths.get_logs_dir() / "log_registry.toml"))
app._log_registry.load_registry()
if imgui.button("Refresh Registry"):
if app._log_registry is not None: app._log_registry.load_registry()
imgui.same_line()
if imgui.button("Load Log"): app.cb_load_prior_log()
imgui.same_line()
if imgui.button("Force Prune Logs"): app.controller.event_queue.put("gui_task", {"action": "click", "item": "btn_prune_logs"})
registry = app._log_registry
sessions = registry.data
if imgui.begin_table("sessions_table", 7, imgui.TableFlags_.borders | imgui.TableFlags_.row_bg | imgui.TableFlags_.resizable):
imgui.table_setup_column("Session ID")
imgui.table_setup_column("Start Time")
imgui.table_setup_column("Star")
imgui.table_setup_column("Reason")
imgui.table_setup_column("Size (KB)")
imgui.table_setup_column("Msgs")
imgui.table_setup_column("Actions")
imgui.table_headers_row()
for session_id, s_data in sessions.items():
imgui.table_next_row()
imgui.table_next_column()
imgui.text(session_id)
imgui.table_next_column()
imgui.text(s_data.get("start_time", ""))
imgui.table_next_column()
whitelisted = s_data.get("whitelisted", False)
if whitelisted:
imgui.text_colored(vec4(255, 215, 0), "YES")
else:
imgui.text("NO")
metadata = s_data.get("metadata") or {}
imgui.table_next_column()
imgui.text(metadata.get("reason", ""))
imgui.table_next_column()
imgui.text(str(metadata.get("size_kb", "")))
imgui.table_next_column()
imgui.text(str(metadata.get("message_count", "")))
imgui.table_next_column()
if imgui.button(f"Load##{session_id}"):
app.cb_load_prior_log(s_data.get("path"))
imgui.same_line()
if whitelisted:
if imgui.button(f"Unstar##{session_id}"):
registry.update_session_metadata(
session_id,
message_count=int(metadata.get("message_count") or 0),
errors=int(metadata.get("errors") or 0),
size_kb=int(metadata.get("size_kb") or 0),
whitelisted=False,
reason=str(metadata.get("reason") or "")
)
else:
if imgui.button(f"Star##{session_id}"):
registry.update_session_metadata(
session_id,
message_count=int(metadata.get("message_count") or 0),
errors=int(metadata.get("errors") or 0),
size_kb=int(metadata.get("size_kb") or 0),
whitelisted=True,
reason="Manually whitelisted"
)
imgui.end_table()
registry = app._log_registry
sessions = registry.data
if imgui.begin_table("sessions_table", 7, imgui.TableFlags_.borders | imgui.TableFlags_.row_bg | imgui.TableFlags_.resizable):
imgui.table_setup_column("Session ID")
imgui.table_setup_column("Start Time")
imgui.table_setup_column("Star")
imgui.table_setup_column("Reason")
imgui.table_setup_column("Size (KB)")
imgui.table_setup_column("Msgs")
imgui.table_setup_column("Actions")
imgui.table_headers_row()
for session_id, s_data in sessions.items():
imgui.table_next_row()
imgui.table_next_column()
imgui.text(session_id)
imgui.table_next_column()
imgui.text(s_data.get("start_time", ""))
imgui.table_next_column()
whitelisted = s_data.get("whitelisted", False)
if whitelisted:
imgui.text_colored(vec4(255, 215, 0), "YES")
else:
imgui.text("NO")
metadata = s_data.get("metadata") or {}
imgui.table_next_column()
imgui.text(metadata.get("reason", ""))
imgui.table_next_column()
imgui.text(str(metadata.get("size_kb", "")))
imgui.table_next_column()
imgui.text(str(metadata.get("message_count", "")))
imgui.table_next_column()
if imgui.button(f"Load##{session_id}"):
app.cb_load_prior_log(s_data.get("path"))
imgui.same_line()
if whitelisted:
if imgui.button(f"Unstar##{session_id}"):
registry.update_session_metadata(
session_id,
message_count=int(metadata.get("message_count") or 0),
errors=int(metadata.get("errors") or 0),
size_kb=int(metadata.get("size_kb") or 0),
whitelisted=False,
reason=str(metadata.get("reason") or "")
)
else:
if imgui.button(f"Star##{session_id}"):
registry.update_session_metadata(
session_id,
message_count=int(metadata.get("message_count") or 0),
errors=int(metadata.get("errors") or 0),
size_kb=int(metadata.get("size_kb") or 0),
whitelisted=True,
reason="Manually whitelisted"
)
imgui.end_table()
if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_log_management")
@@ -3463,6 +3463,7 @@ def render_discussion_entry(app: App, entry: dict, index: int) -> None:
imgui.same_line()
if imgui.button("Del"):
if entry in app.disc_entries: app.disc_entries.remove(entry)
imgui.end_group()
draw_list.channels_merge()
return
imgui.same_line()
+61 -20
View File
@@ -117,30 +117,71 @@ class MarkdownRenderer:
from src.markdown_table import parse_tables, render_table
blocks = parse_tables(text)
sentinel = "\x00TBL{}\x00"
masked = text
for idx, block in enumerate(blocks):
start, end = block.span
original_block = "\n".join(masked.splitlines()[start:end])
masked = masked.replace(original_block, sentinel.format(idx), 1)
lines = text.splitlines(keepends=True)
if not lines:
return
parts = re.split(r"(```[\s\S]*?```)", masked)
table_at_line: dict[int, int] = {b.span[0]: i for i, b in enumerate(blocks)}
table_end: dict[int, int] = {b.span[0]: b.span[1] for i, b in enumerate(blocks)}
md_buf: list[str] = []
code_buf: list[str] = []
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():
sub_parts = re.split(r"(\x00TBL\d+\x00)", part)
for sp in sub_parts:
if sp.startswith("\x00TBL") and sp.endswith("\x00"):
tbl_idx = int(sp[4:-1])
try: render_table(blocks[tbl_idx])
except Exception: imgui.text(sp)
else:
if sp.strip(): imgui_md.render(sp)
in_fence = False
fence_marker = ""
def flush_md() -> None:
if md_buf:
chunk = "".join(md_buf)
if chunk.strip():
imgui_md.render(chunk)
md_buf.clear()
def flush_code() -> None:
nonlocal block_idx
if code_buf:
chunk = "".join(code_buf)
self._render_code_block(chunk, context_id, block_idx)
block_idx += 1
code_buf.clear()
i = 0
while i < len(lines):
line = lines[i]
stripped = line.lstrip().rstrip("\r\n")
if stripped.startswith("```"):
if not in_fence:
in_fence = True
fence_marker = stripped[:3]
flush_md()
code_buf.append(line)
i += 1
continue
if fence_marker and stripped.startswith(fence_marker):
in_fence = False
code_buf.append(line)
flush_code()
fence_marker = ""
i += 1
continue
code_buf.append(line)
i += 1
continue
if in_fence:
code_buf.append(line)
i += 1
continue
if i in table_at_line:
flush_md()
try: render_table(blocks[table_at_line[i]])
except Exception: pass
i = table_end[i]
continue
md_buf.append(line)
i += 1
flush_md()
flush_code()
def render_unindented(self, text: str) -> None:
"""Render Markdown text with automatic unindentation."""
imgui_md.render_unindented(text)