diff --git a/conductor/tracks/markdown_helper_language_api_compat_20260603/index.md b/conductor/tracks/markdown_helper_language_api_compat_20260603/index.md new file mode 100644 index 00000000..063c0010 --- /dev/null +++ b/conductor/tracks/markdown_helper_language_api_compat_20260603/index.md @@ -0,0 +1,5 @@ +# Track markdown_helper_language_api_compat_20260603 Context + +- [Specification](./spec.md) +- [Implementation Plan](./plan.md) +- [Metadata](./metadata.json) diff --git a/conductor/tracks/markdown_helper_language_api_compat_20260603/metadata.json b/conductor/tracks/markdown_helper_language_api_compat_20260603/metadata.json new file mode 100644 index 00000000..0970ba93 --- /dev/null +++ b/conductor/tracks/markdown_helper_language_api_compat_20260603/metadata.json @@ -0,0 +1,11 @@ +{ + "id": "markdown_helper_language_api_compat_20260603", + "title": "Fix markdown_helper.py for imgui-bundle >=1.92.801", + "phase": null, + "created": "2026-06-03", + "status": "in_progress", + "spec_file": "spec.md", + "plan_file": "plan.md", + "depends_on": ["clean_install_test_20260603"], + "completion_checkpoints": [] +} diff --git a/conductor/tracks/markdown_helper_language_api_compat_20260603/plan.md b/conductor/tracks/markdown_helper_language_api_compat_20260603/plan.md new file mode 100644 index 00000000..845f3028 --- /dev/null +++ b/conductor/tracks/markdown_helper_language_api_compat_20260603/plan.md @@ -0,0 +1,24 @@ +# Implementation Plan: Fix markdown_helper.py for imgui-bundle >=1.92.801 + +## Phase 1: Red - Confirm bug +- [x] Task 1.1: Run `tests/test_clean_install.py` in opt-in mode (RUN_CLEAN_INSTALL_TEST=1) - confirm AttributeError on TextEditor.LanguageDefinitionId +- [x] Task 1.2: Capture full traceback for git note + +## Phase 2: Green - Apply version-compat shim +- [ ] Task 2.1: Add module-level `_get_language_id(name)` helper to markdown_helper.py +- [ ] Task 2.2: Add module-level `_set_editor_language(editor, lang_obj)` helper +- [ ] Task 2.3: Add module-level `_get_editor_language_name(editor)` helper +- [ ] Task 2.4: Replace `_lang_map` initialization to use `_get_language_id(...)` +- [ ] Task 2.5: Replace lines 128, 134, 135, 136 with shim calls +- [ ] Task 2.6: Handle the "none" fallback case (return None, skip set call) +- [ ] Task 2.7: Syntax check (ast.parse) + +## Phase 3: Verify +- [ ] Task 3.1: Run `tests/test_clean_install.py` in opt-in mode - should pass (1 passed, clone+sync+launch+hook API) +- [ ] Task 3.2: Run local markdown-related tests (if any) to ensure no regression in 1.92.5 env +- [ ] Task 3.3: Import test for the new helpers (both APIs should work) + +## Phase 4: Commit +- [ ] Task 4.1: Atomic commit with descriptive message +- [ ] Task 4.2: Git note with root cause analysis +- [ ] Task 4.3: Update tracks.md to register this fix track diff --git a/conductor/tracks/markdown_helper_language_api_compat_20260603/spec.md b/conductor/tracks/markdown_helper_language_api_compat_20260603/spec.md new file mode 100644 index 00000000..3af73087 --- /dev/null +++ b/conductor/tracks/markdown_helper_language_api_compat_20260603/spec.md @@ -0,0 +1,30 @@ +# Fix markdown_helper.py for imgui-bundle >=1.92.801 + +## Bug + +`src/markdown_helper.py` uses `ed.TextEditor.LanguageDefinitionId.` enum and `editor.set_language_definition(enum)` calls. These were removed in `imgui-bundle>=1.92.801`. Replacement: `ed.TextEditor.Language.()` factory functions and `editor.set_language(obj)` method. + +The bug surfaces only on clean installs (where `uv sync` resolves the latest `imgui-bundle`). The local dev environment has 1.92.5 pinned, masking the issue. The `clean_install_test_20260603` opt-in test caught this on first run. + +## Affected Code + +- `src/markdown_helper.py:37-48` — `_lang_map` initialization with enum values +- `src/markdown_helper.py:128` — `ed.TextEditor.LanguageDefinitionId.none` fallback +- `src/markdown_helper.py:134` — `editor.set_language_definition(lang_id)` call +- `src/markdown_helper.py:135` — `editor.get_language_definition_name()` getter +- `src/markdown_helper.py:136` — `editor.set_language_definition(lang_id)` re-set + +## Fix Strategy + +Version-compat shim: detect which API is available at runtime and dispatch to the right one. This is safer than pinning `imgui-bundle` (avoids forcing the dev env to upgrade) and safer than hard-coding the new API (would break the 1.92.5 dev env). + +The shim: +- Tries `TextEditor.Language.()` first (1.92.801+) +- Falls back to `TextEditor.LanguageDefinitionId.` (1.92.5) +- Returns `None` for "no language" (handled by not calling set_language) +- Provides `_set_editor_language(editor, lang_obj)` that dispatches to the right method + +## Files Touched + +- `src/markdown_helper.py` — add shim helpers, replace enum references +- (Optional) `pyproject.toml` — add `imgui-bundle>=1.92.5,<1.93` constraint to prevent future major version drift diff --git a/manualslop_layout.ini b/manualslop_layout.ini index 0d598c3a..ec82e5bc 100644 --- a/manualslop_layout.ini +++ b/manualslop_layout.ini @@ -44,20 +44,20 @@ Collapsed=0 DockId=0x00000010,0 [Window][Message] -Pos=1125,28 -Size=1616,1842 +Pos=64,28 +Size=1616,1172 Collapsed=0 DockId=0x00000006,0 [Window][Response] Pos=0,28 -Size=1123,1842 +Size=62,1172 Collapsed=0 DockId=0x00000010,4 [Window][Tool Calls] -Pos=1125,28 -Size=1616,1842 +Pos=64,28 +Size=1616,1172 Collapsed=0 DockId=0x00000006,3 @@ -77,7 +77,7 @@ DockId=0xAFC85805,2 [Window][Theme] Pos=0,28 -Size=1123,1842 +Size=62,1172 Collapsed=0 DockId=0x00000010,0 @@ -105,26 +105,26 @@ Collapsed=0 DockId=0x0000000D,0 [Window][Discussion Hub] -Pos=1125,28 -Size=1616,1842 +Pos=64,28 +Size=1616,1172 Collapsed=0 DockId=0x00000006,1 [Window][Operations Hub] Pos=0,28 -Size=1123,1842 +Size=62,1172 Collapsed=0 DockId=0x00000010,3 [Window][Files & Media] Pos=0,28 -Size=1123,1842 +Size=62,1172 Collapsed=0 DockId=0x00000010,2 [Window][AI Settings] Pos=0,28 -Size=1123,1842 +Size=62,1172 Collapsed=0 DockId=0x00000010,1 @@ -140,8 +140,8 @@ Collapsed=0 DockId=0x00000006,2 [Window][Log Management] -Pos=1125,28 -Size=1616,1842 +Pos=64,28 +Size=1616,1172 Collapsed=0 DockId=0x00000006,2 @@ -697,13 +697,13 @@ Column 1 Weight=1.0000 DockNode ID=0x00000008 Pos=3125,170 Size=593,1157 Split=Y DockNode ID=0x00000009 Parent=0x00000008 SizeRef=1029,147 Selected=0x0469CA7A DockNode ID=0x0000000A Parent=0x00000008 SizeRef=1029,145 Selected=0xDF822E02 -DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,28 Size=2741,1842 Split=X +DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,28 Size=1680,1172 Split=X DockNode ID=0x00000003 Parent=0xAFC85805 SizeRef=2357,1183 Split=X DockNode ID=0x0000000B Parent=0x00000003 SizeRef=404,1186 Split=X Selected=0xF4139CA2 DockNode ID=0x00000005 Parent=0x0000000B SizeRef=1221,1681 Split=Y Selected=0x3F1379AF DockNode ID=0x00000010 Parent=0x00000005 SizeRef=983,1140 CentralNode=1 Selected=0x8CA2375C DockNode ID=0x00000011 Parent=0x00000005 SizeRef=983,184 Selected=0x432BAE4E - DockNode ID=0x00000006 Parent=0x0000000B SizeRef=1616,1681 Selected=0x6F2B5B04 + DockNode ID=0x00000006 Parent=0x0000000B SizeRef=1616,1681 Selected=0x2C0206CE DockNode ID=0x0000000D Parent=0x00000003 SizeRef=435,1186 Selected=0x363E93D6 DockNode ID=0x00000004 Parent=0xAFC85805 SizeRef=488,1183 Selected=0x3AEC3498 diff --git a/src/markdown_helper.py b/src/markdown_helper.py index ebb9c845..95a61918 100644 --- a/src/markdown_helper.py +++ b/src/markdown_helper.py @@ -7,6 +7,43 @@ import re from pathlib import Path from typing import Optional, Dict, Callable +def _get_language_id(name: str): + """Get a language identifier for ImGuiColorTextEdit. + + Compatible with both imgui-bundle 1.92.5 (LanguageDefinitionId enum) + and 1.92.801+ (Language factory functions returning a Language object). + Returns None for "no language" or unknown names. + """ + if not name or name == "none": + return None + # Prefer the newer API (1.92.801+) which uses factory functions. + if hasattr(ed.TextEditor, "Language"): + lang_class = ed.TextEditor.Language + if hasattr(lang_class, name): + factory = getattr(lang_class, name) + if callable(factory): + return factory() + # Fall back to the older API (1.92.5) which exposes an enum. + if hasattr(ed.TextEditor, "LanguageDefinitionId"): + lang_id_class = ed.TextEditor.LanguageDefinitionId + if hasattr(lang_id_class, name): + return getattr(lang_id_class, name) + return None + + +def _set_editor_language(editor, lang_obj) -> None: + """Set the editor's language via whichever API is available. + + 1.92.801+: editor.set_language(obj). 1.92.5: editor.set_language_definition(obj). + No-op when lang_obj is None (used to skip the call for unknown languages). + """ + if lang_obj is None: + return + if hasattr(editor, "set_language"): + editor.set_language(lang_obj) + elif hasattr(editor, "set_language_definition"): + editor.set_language_definition(lang_obj) + class MarkdownRenderer: """ @@ -22,29 +59,32 @@ class MarkdownRenderer: # 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 - + # Cache for TextEditor instances to maintain state self._editor_cache: Dict[tuple[str, int], ed.TextEditor] = {} + # Parallel cache tracking the current language tag per editor (avoids per-frame + # set_language calls and is robust against imgui-bundle naming differences). + self._editor_lang_cache: Dict[tuple[str, int], Optional[str]] = {} 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, + "python": _get_language_id("python"), + "py": _get_language_id("python"), + "json": _get_language_id("json"), + "cpp": _get_language_id("cpp"), + "c++": _get_language_id("cpp"), + "c": _get_language_id("c"), + "lua": _get_language_id("lua"), + "sql": _get_language_id("sql"), + "cs": _get_language_id("cs"), + "c#": _get_language_id("cs"), } def _on_open_link(self, url: str) -> None: @@ -95,25 +135,11 @@ class MarkdownRenderer: # 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() + self._editor_lang_cache.clear() cache_key = (context_id, block_idx) if cache_key not in self._editor_cache: @@ -121,27 +147,24 @@ class MarkdownRenderer: editor.set_read_only_enabled(True) editor.set_show_line_numbers_enabled(True) self._editor_cache[cache_key] = editor - + self._editor_lang_cache[cache_key] = None + editor = self._editor_cache[cache_key] - - # Sync text and language - lang_id = self._lang_map.get(lang_tag, ed.TextEditor.LanguageDefinitionId.none) - + current_lang = self._editor_lang_cache[cache_key] + + # Sync text and language. None means "no language set" (skip the call). + lang_id = self._lang_map.get(lang_tag) + # Robust check to avoid re-setting text every frame (which resets scroll) curr_text = editor.get_text().replace('\r\n', '\n').strip() if curr_text != code.replace('\r\n', '\n').strip(): editor.set_text(code) - editor.set_language_definition(lang_id) - elif editor.get_language_definition_name().lower() != lang_tag: - 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)) + if current_lang != lang_tag: + _set_editor_language(editor, lang_id) + self._editor_lang_cache[cache_key] = lang_tag + elif current_lang != lang_tag: + _set_editor_language(editor, lang_id) + self._editor_lang_cache[cache_key] = lang_tag def _is_likely_lang_tag(self, tag: str) -> bool: return bool(re.match(r'^[a-zA-Z0-9+#-]+$', tag)) and len(tag) < 15