Compare commits
6 Commits
5470f2106f
...
b4396697dd
| Author | SHA1 | Date | |
|---|---|---|---|
| b4396697dd | |||
| 31b38f0c77 | |||
| 2826ad53d8 | |||
| a91b8dcc99 | |||
| 74c9d4b992 | |||
| e28af48ae9 |
@@ -79,7 +79,7 @@ This file tracks all major tracks for the project. Each track has its own detail
|
||||
*Link: [./tracks/undo_redo_history_20260311/](./tracks/undo_redo_history_20260311/)*
|
||||
*Goal: Robust, non-provider based undo/redo for text inputs, UI controls, discussion mutations, and context management. Includes hotkey support and a history list view.*
|
||||
|
||||
11. [ ] **Track: Advanced Text Viewer with Syntax Highlighting**
|
||||
11. [x] **Track: Advanced Text Viewer with Syntax Highlighting**
|
||||
*Link: [./tracks/text_viewer_rich_rendering_20260313/](./tracks/text_viewer_rich_rendering_20260313/)*
|
||||
|
||||
---
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
# Implementation Plan: Advanced Text Viewer with Syntax Highlighting
|
||||
|
||||
## Phase 1: State & Interface Update
|
||||
- [ ] Task: Audit `src/gui_2.py` to ensure all `text_viewer_*` state variables are explicitly initialized in `App.__init__`.
|
||||
- [ ] Task: Implement: Update `App.__init__` to initialize `self.show_text_viewer`, `self.text_viewer_title`, `self.text_viewer_content`, and new `self.text_viewer_type` (defaulting to "text").
|
||||
- [ ] Task: Implement: Update `self.text_viewer_wrap` (defaulting to True) to allow independent word wrap.
|
||||
- [ ] Task: Implement: Update `_render_text_viewer(self, label: str, content: str, text_type: str = "text")` signature and caller usage.
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 1: State & Interface Update' (Protocol in workflow.md)
|
||||
- [x] Task: Audit `src/gui_2.py` to ensure all `text_viewer_*` state variables are explicitly initialized in `App.__init__`. e28af48
|
||||
- [x] Task: Implement: Update `App.__init__` to initialize `self.show_text_viewer`, `self.text_viewer_title`, `self.text_viewer_content`, and new `self.text_viewer_type` (defaulting to "text"). e28af48
|
||||
- [x] Task: Implement: Update `self.text_viewer_wrap` (defaulting to True) to allow independent word wrap. e28af48
|
||||
- [x] Task: Implement: Update `_render_text_viewer(self, label: str, content: str, text_type: str = "text")` signature and caller usage. e28af48
|
||||
- [x] Task: Conductor - User Manual Verification 'Phase 1: State & Interface Update' (Protocol in workflow.md) e28af48
|
||||
|
||||
## Phase 2: Core Rendering Logic (Code & MD)
|
||||
- [ ] Task: Write Tests: Create a simulation test in `tests/test_gui_text_viewer.py` to verify the viewer opens and switches rendering paths based on `text_type`.
|
||||
- [ ] Task: Implement: In `src/gui_2.py`, refactor the text viewer window loop to:
|
||||
- Use `MarkdownRenderer.render` if `text_type == "markdown"`.
|
||||
- Use a cached `ImGuiColorTextEdit.TextEditor` if `text_type` matches a code language.
|
||||
- Fallback to `imgui.input_text_multiline` for plain text.
|
||||
- [ ] Task: Implement: Ensure the `TextEditor` instance is properly cached using a unique key for the text viewer to maintain state.
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 2: Core Rendering Logic' (Protocol in workflow.md)
|
||||
- [x] Task: Write Tests: Create a simulation test in `tests/test_gui_text_viewer.py` to verify the viewer opens and switches rendering paths based on `text_type`. a91b8dc
|
||||
- [x] Task: Implement: In `src/gui_2.py`, refactor the text viewer window loop to: a91b8dc
|
||||
- Use `MarkdownRenderer.render` if `text_type == "markdown"`. a91b8dc
|
||||
- Use a cached `ImGuiColorTextEdit.TextEditor` if `text_type` matches a code language. a91b8dc
|
||||
- Fallback to `imgui.input_text_multiline` for plain text. a91b8dc
|
||||
- [x] Task: Implement: Ensure the `TextEditor` instance is properly cached using a unique key for the text viewer to maintain state. a91b8dc
|
||||
- [x] Task: Conductor - User Manual Verification 'Phase 2: Core Rendering Logic' (Protocol in workflow.md) a91b8dc
|
||||
|
||||
## Phase 3: UI Features (Copy, Line Numbers, Wrap)
|
||||
- [ ] Task: Write Tests: Update `tests/test_gui_text_viewer.py` to verify the copy-to-clipboard functionality and word wrap toggle.
|
||||
- [ ] Task: Implement: Add a "Copy" button to the text viewer title bar or a small toolbar at the top of the window.
|
||||
- [ ] Task: Implement: Add a "Word Wrap" checkbox inside the text viewer window.
|
||||
- [ ] Task: Implement: Configure the `TextEditor` instance to show line numbers and be read-only.
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 3: UI Features' (Protocol in workflow.md)
|
||||
- [x] Task: Write Tests: Update `tests/test_gui_text_viewer.py` to verify the copy-to-clipboard functionality and word wrap toggle. a91b8dc
|
||||
- [x] Task: Implement: Add a "Copy" button to the text viewer title bar or a small toolbar at the top of the window. a91b8dc
|
||||
- [x] Task: Implement: Add a "Word Wrap" checkbox inside the text viewer window. a91b8dc
|
||||
- [x] Task: Implement: Configure the `TextEditor` instance to show line numbers and be read-only. a91b8dc
|
||||
- [x] Task: Conductor - User Manual Verification 'Phase 3: UI Features' (Protocol in workflow.md) a91b8dc
|
||||
|
||||
## Phase 4: Integration & Rollout
|
||||
- [ ] Task: Implement: Update all existing calls to `_render_text_viewer` in `src/gui_2.py` (e.g., in `_render_files_panel`, `_render_tool_calls_panel`) to pass the correct `text_type` based on file extension or content.
|
||||
- [ ] Task: Implement: Add "Markdown Preview" support for system prompt presets using the new text viewer logic.
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 4: Integration & Rollout' (Protocol in workflow.md)
|
||||
- [x] Task: Implement: Update all existing calls to `_render_text_viewer` in `src/gui_2.py` (e.g., in `_render_files_panel`, `_render_tool_calls_panel`) to pass the correct `text_type` based on file extension or content. 2826ad5
|
||||
- [x] Task: Implement: Add "Markdown Preview" support for system prompt presets using the new text viewer logic. 2826ad5
|
||||
- [x] Task: Conductor - User Manual Verification 'Phase 4: Integration & Rollout' (Protocol in workflow.md) 2826ad5
|
||||
|
||||
@@ -225,6 +225,9 @@ class HookHandler(BaseHTTPRequestHandler):
|
||||
for key, attr in gettable.items():
|
||||
val = _get_app_attr(app, attr, None)
|
||||
result[key] = _serialize_for_api(val)
|
||||
result['show_text_viewer'] = _get_app_attr(app, 'show_text_viewer', False)
|
||||
result['text_viewer_title'] = _get_app_attr(app, 'text_viewer_title', '')
|
||||
result['text_viewer_type'] = _get_app_attr(app, 'text_viewer_type', 'markdown')
|
||||
finally: event.set()
|
||||
lock = _get_app_attr(app, "_pending_gui_tasks_lock")
|
||||
tasks = _get_app_attr(app, "_pending_gui_tasks")
|
||||
|
||||
@@ -252,6 +252,7 @@ class AppController:
|
||||
self.show_text_viewer: bool = False
|
||||
self.text_viewer_title: str = ''
|
||||
self.text_viewer_content: str = ''
|
||||
self.text_viewer_type: str = 'text'
|
||||
self._pending_comms: List[Dict[str, Any]] = []
|
||||
self._pending_tool_calls: List[Dict[str, Any]] = []
|
||||
self._pending_history_adds: List[Dict[str, Any]] = []
|
||||
@@ -375,7 +376,10 @@ class AppController:
|
||||
'ui_separate_tier1': 'ui_separate_tier1',
|
||||
'ui_separate_tier2': 'ui_separate_tier2',
|
||||
'ui_separate_tier3': 'ui_separate_tier3',
|
||||
'ui_separate_tier4': 'ui_separate_tier4'
|
||||
'ui_separate_tier4': 'ui_separate_tier4',
|
||||
'show_text_viewer': 'show_text_viewer',
|
||||
'text_viewer_title': 'text_viewer_title',
|
||||
'text_viewer_type': 'text_viewer_type'
|
||||
}
|
||||
self._gettable_fields = dict(self._settable_fields)
|
||||
self._gettable_fields.update({
|
||||
@@ -422,7 +426,10 @@ class AppController:
|
||||
'ui_separate_tier1': 'ui_separate_tier1',
|
||||
'ui_separate_tier2': 'ui_separate_tier2',
|
||||
'ui_separate_tier3': 'ui_separate_tier3',
|
||||
'ui_separate_tier4': 'ui_separate_tier4'
|
||||
'ui_separate_tier4': 'ui_separate_tier4',
|
||||
'show_text_viewer': 'show_text_viewer',
|
||||
'text_viewer_title': 'text_viewer_title',
|
||||
'text_viewer_type': 'text_viewer_type'
|
||||
})
|
||||
self.perf_monitor = performance_monitor.get_monitor()
|
||||
self._perf_profiling_enabled = False
|
||||
|
||||
74
src/gui_2.py
74
src/gui_2.py
@@ -40,7 +40,7 @@ else:
|
||||
win32con = None
|
||||
|
||||
from pydantic import BaseModel
|
||||
from imgui_bundle import imgui, hello_imgui, immapp, imgui_node_editor as ed
|
||||
from imgui_bundle import imgui, hello_imgui, immapp, imgui_node_editor as ed, imgui_color_text_edit as ced
|
||||
|
||||
PROVIDERS: list[str] = ["gemini", "anthropic", "gemini_cli", "deepseek", "minimax"]
|
||||
COMMS_CLAMP_CHARS: int = 300
|
||||
@@ -107,9 +107,16 @@ class App:
|
||||
self.controller.init_state()
|
||||
self.show_windows.setdefault("Diagnostics", False)
|
||||
self.controller.start_services(self)
|
||||
self.controller._predefined_callbacks['_render_text_viewer'] = self._render_text_viewer
|
||||
self.show_preset_manager_window = False
|
||||
self.show_tool_preset_manager_window = False
|
||||
self.show_persona_editor_window = False
|
||||
self.show_text_viewer = False
|
||||
self.text_viewer_title = ''
|
||||
self.text_viewer_content = ''
|
||||
self.text_viewer_type = 'text'
|
||||
self.text_viewer_wrap = True
|
||||
self._text_viewer_editor: Optional[ced.TextEditor] = None
|
||||
self.ui_active_tool_preset = ""
|
||||
self.ui_active_bias_profile = ""
|
||||
self.ui_active_persona = ""
|
||||
@@ -281,8 +288,9 @@ class App:
|
||||
f.write(data)
|
||||
# ---------------------------------------------------------------- helpers
|
||||
|
||||
def _render_text_viewer(self, label: str, content: str) -> None:
|
||||
if imgui.button("[+]##" + str(id(content))):
|
||||
def _render_text_viewer(self, label: str, content: str, text_type: str = 'text', force_open: bool = False) -> None:
|
||||
self.text_viewer_type = text_type
|
||||
if imgui.button("[+]##" + str(id(content))) or force_open:
|
||||
self.show_text_viewer = True
|
||||
self.text_viewer_title = label
|
||||
self.text_viewer_content = content
|
||||
@@ -292,6 +300,7 @@ class App:
|
||||
imgui.same_line()
|
||||
if imgui.button("[+]##" + label + id_suffix):
|
||||
self.show_text_viewer = True
|
||||
self.text_viewer_type = 'markdown' if label in ('message', 'text', 'content', 'system') else 'json' if label in ('tool_calls', 'data') else 'powershell' if label == 'script' else 'text'
|
||||
self.text_viewer_title = label
|
||||
self.text_viewer_content = content
|
||||
|
||||
@@ -997,14 +1006,42 @@ class App:
|
||||
expanded, opened = imgui.begin(f"Text Viewer - {self.text_viewer_title}", self.show_text_viewer)
|
||||
self.show_text_viewer = bool(opened)
|
||||
if expanded:
|
||||
if self.ui_word_wrap:
|
||||
imgui.begin_child("tv_wrap", imgui.ImVec2(-1, -1), False)
|
||||
imgui.push_text_wrap_pos(imgui.get_content_region_avail().x)
|
||||
imgui.text(self.text_viewer_content)
|
||||
imgui.pop_text_wrap_pos()
|
||||
# Toolbar
|
||||
if imgui.button("Copy"):
|
||||
imgui.set_clipboard_text(self.text_viewer_content)
|
||||
imgui.same_line()
|
||||
_, self.text_viewer_wrap = imgui.checkbox("Word Wrap", self.text_viewer_wrap)
|
||||
imgui.separator()
|
||||
|
||||
renderer = markdown_helper.get_renderer()
|
||||
tv_type = getattr(self, "text_viewer_type", "text")
|
||||
|
||||
if tv_type == 'markdown':
|
||||
imgui.begin_child("tv_md_scroll", imgui.ImVec2(-1, -1), True)
|
||||
markdown_helper.render(self.text_viewer_content, context_id='text_viewer')
|
||||
imgui.end_child()
|
||||
elif tv_type in renderer._lang_map:
|
||||
if self._text_viewer_editor is None:
|
||||
self._text_viewer_editor = ced.TextEditor()
|
||||
self._text_viewer_editor.set_read_only_enabled(True)
|
||||
self._text_viewer_editor.set_show_line_numbers_enabled(True)
|
||||
|
||||
# Sync text and language
|
||||
lang_id = renderer._lang_map[tv_type]
|
||||
if self._text_viewer_editor.get_text().strip() != self.text_viewer_content.strip():
|
||||
self._text_viewer_editor.set_text(self.text_viewer_content)
|
||||
self._text_viewer_editor.set_language_definition(lang_id)
|
||||
|
||||
self._text_viewer_editor.render('##tv_editor', a_size=imgui.ImVec2(-1, -1))
|
||||
else:
|
||||
imgui.input_text_multiline("##tv_c", self.text_viewer_content, imgui.ImVec2(-1, -1), imgui.InputTextFlags_.read_only)
|
||||
if self.text_viewer_wrap:
|
||||
imgui.begin_child("tv_wrap", imgui.ImVec2(-1, -1), False)
|
||||
imgui.push_text_wrap_pos(imgui.get_content_region_avail().x)
|
||||
imgui.text(self.text_viewer_content)
|
||||
imgui.pop_text_wrap_pos()
|
||||
imgui.end_child()
|
||||
else:
|
||||
imgui.input_text_multiline("##tv_c", self.text_viewer_content, imgui.ImVec2(-1, -1), imgui.InputTextFlags_.read_only)
|
||||
imgui.end()
|
||||
# Inject File Modal
|
||||
if getattr(self, "show_inject_modal", False):
|
||||
@@ -1138,16 +1175,14 @@ class App:
|
||||
imgui.separator()
|
||||
imgui.text("Prompt Content:")
|
||||
imgui.same_line()
|
||||
if imgui.button("MD Preview" if not self._prompt_md_preview else "Edit Mode"):
|
||||
self._prompt_md_preview = not self._prompt_md_preview
|
||||
if imgui.button("Pop out MD Preview"):
|
||||
self.text_viewer_title = f"Preset: {self._editing_preset_name}"
|
||||
self.text_viewer_content = self._editing_preset_system_prompt
|
||||
self.text_viewer_type = "markdown"
|
||||
self.show_text_viewer = True
|
||||
|
||||
rem_y = imgui.get_content_region_avail().y
|
||||
if self._prompt_md_preview:
|
||||
if imgui.begin_child("prompt_preview", imgui.ImVec2(-1, rem_y), True):
|
||||
markdown_helper.render(self._editing_preset_system_prompt, context_id="prompt_preset_preview")
|
||||
imgui.end_child()
|
||||
else:
|
||||
_, self._editing_preset_system_prompt = imgui.input_text_multiline("##pcont", self._editing_preset_system_prompt, imgui.ImVec2(-1, rem_y))
|
||||
_, self._editing_preset_system_prompt = imgui.input_text_multiline("##pcont", self._editing_preset_system_prompt, imgui.ImVec2(-1, rem_y))
|
||||
imgui.end_child()
|
||||
|
||||
# Footer Buttons
|
||||
@@ -2328,6 +2363,7 @@ def hello():
|
||||
if res:
|
||||
self.text_viewer_title = path
|
||||
self.text_viewer_content = res
|
||||
self.text_viewer_type = Path(path).suffix.lstrip('.') if Path(path).suffix else 'text'
|
||||
self.show_text_viewer = True
|
||||
if code_block:
|
||||
if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80))
|
||||
@@ -3021,7 +3057,7 @@ def hello():
|
||||
script = entry.get("script", "")
|
||||
res = entry.get("result", "")
|
||||
# Use a clear, formatted combined view for the detail window
|
||||
combined = f"COMMAND:\n{script}\n\n{'='*40}\nOUTPUT:\n{res}"
|
||||
combined = f"**COMMAND:**\n```powershell\n{script}\n```\n\n---\n**OUTPUT:**\n```text\n{res}\n```"
|
||||
|
||||
script_preview = script.replace("\n", " ")[:150]
|
||||
if len(script) > 150: script_preview += "..."
|
||||
@@ -3029,6 +3065,7 @@ def hello():
|
||||
if imgui.is_item_clicked():
|
||||
self.text_viewer_title = f"Tool Call #{i+1} Details"
|
||||
self.text_viewer_content = combined
|
||||
self.text_viewer_type = 'markdown'
|
||||
self.show_text_viewer = True
|
||||
|
||||
imgui.table_next_column()
|
||||
@@ -3038,6 +3075,7 @@ def hello():
|
||||
if imgui.is_item_clicked():
|
||||
self.text_viewer_title = f"Tool Call #{i+1} Details"
|
||||
self.text_viewer_content = combined
|
||||
self.text_viewer_type = 'markdown'
|
||||
self.show_text_viewer = True
|
||||
|
||||
imgui.end_table()
|
||||
|
||||
28
tests/test_gui_text_viewer.py
Normal file
28
tests/test_gui_text_viewer.py
Normal file
@@ -0,0 +1,28 @@
|
||||
import pytest
|
||||
import time
|
||||
from src.api_hook_client import ApiHookClient
|
||||
|
||||
def test_text_viewer_state_update(live_gui) -> None:
|
||||
"""
|
||||
Verifies that we can set text viewer state and it is reflected in GUI state.
|
||||
"""
|
||||
client = ApiHookClient()
|
||||
label = "Test Viewer Label"
|
||||
content = "This is test content for the viewer."
|
||||
text_type = "markdown"
|
||||
|
||||
# Add a task to push a custom callback that mutates the app state
|
||||
def set_viewer_state(app):
|
||||
app.show_text_viewer = True
|
||||
app.text_viewer_title = label
|
||||
app.text_viewer_content = content
|
||||
app.text_viewer_type = text_type
|
||||
|
||||
client.push_event("custom_callback", {"callback": set_viewer_state})
|
||||
time.sleep(0.5)
|
||||
|
||||
state = client.get_gui_state()
|
||||
assert state is not None
|
||||
assert state.get('show_text_viewer') == True
|
||||
assert state.get('text_viewer_title') == label
|
||||
assert state.get('text_viewer_type') == text_type
|
||||
Reference in New Issue
Block a user