refactor(theme): Introduce semantic theme layer and clean NERV cruft from gui_2.py
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
# Implementation Plan: Clean Theme Abstraction
|
||||
|
||||
## Objective
|
||||
Decouple the NERV theme logic and FX from `src/gui_2.py` by introducing a semantic theme layer in `src/theme_2.py`. This will remove scattered `is_nerv_active()` checks and keep the ImGui hierarchy clean.
|
||||
|
||||
## Key Files & Context
|
||||
- `src/theme_2.py`: The primary theming interface.
|
||||
- `src/gui_2.py`: The main GUI module containing the "cruft".
|
||||
- `src/theme_nerv.py` & `src/theme_nerv_fx.py`: NERV-specific colors and effects.
|
||||
- `src/imgui_scopes.py`: Context managers for ImGui scopes.
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Phase 1: Semantic Theme Layer Foundations
|
||||
- [ ] Task: In `src/theme_2.py`, define semantic color functions (e.g., `ai_text_color()`, `alert_color()`, `warning_color()`) that return theme-specific colors.
|
||||
- [ ] Task: In `src/theme_2.py`, implement context manager helpers (e.g., `ai_text_style()`, `alert_style()`) that wrap `imscope.style_color`.
|
||||
- [ ] Task: In `src/theme_2.py`, add a `render_post_fx(width, height, ai_status, crt_enabled)` hook that encapsulates CRT and Alert pulsing effects.
|
||||
- [ ] Task: Move `CRTFilter`, `AlertPulsing`, and `StatusFlicker` instances from `App` class to `src/theme_2.py` (private module state).
|
||||
|
||||
### Phase 2: Refactor `gui_2.py`
|
||||
- [ ] Task: In `gui_2.py`, remove all NERV-specific filter/flicker/alert instances from `__init__`.
|
||||
- [ ] Task: In `_gui_func`, replace the NERV FX rendering block with a single call to `theme.render_post_fx()`.
|
||||
- [ ] Task: Systematically replace scattered `if is_nerv_active(): push_style_color(...)` blocks with the new semantic style context managers.
|
||||
- [ ] Task: Standardize status indicators (e.g., "PIPELINE PAUSED", "LIVE") to use semantic theme colors rather than manual `vec4` overrides.
|
||||
|
||||
### Phase 3: Verification & Cleanup
|
||||
- [ ] Task: Run the custom AST linter to ensure no unclosed scopes were introduced during refactoring.
|
||||
- [ ] Task: Run fast render tests to ensure UI stability.
|
||||
- [ ] Task: Verify that both NERV and standard themes still render correctly (visual verification).
|
||||
|
||||
## Verification & Testing
|
||||
- **AST Linting**: `uv run python scripts/check_imgui_scopes.py src/gui_2.py`
|
||||
- **Fast Render Tests**: `uv run pytest tests/test_gui_fast_render.py`
|
||||
- **Manual Verification**: Toggle NERV theme and verify CRT filter, alert pulsing, and DATA highlights are still active and correctly colored.
|
||||
+356
-373
@@ -238,10 +238,7 @@ class App:
|
||||
self._last_ui_focus_agent: Optional[str] = None
|
||||
self._log_registry: Optional[log_registry.LogRegistry] = None
|
||||
self.perf_show_graphs: dict[str, bool] = {}
|
||||
self._nerv_crt = theme_fx.CRTFilter()
|
||||
self.ui_crt_filter = True
|
||||
self._nerv_alert = theme_fx.AlertPulsing()
|
||||
self._nerv_flicker = theme_fx.StatusFlicker()
|
||||
self.ui_tool_filter_category = "All"
|
||||
self.ui_discussion_split_h = 300.0
|
||||
self.shader_uniforms = {'crt': 1.0, 'scanline': 0.5, 'bloom': 0.8}
|
||||
@@ -578,9 +575,7 @@ class App:
|
||||
is_md = label in ("message", "text", "content")
|
||||
ctx_id = f"heavy_{label}_{id_suffix}"
|
||||
|
||||
is_nerv = theme.is_nerv_active()
|
||||
if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80))
|
||||
|
||||
with theme.ai_text_style():
|
||||
if len(content) > COMMS_CLAMP_CHARS:
|
||||
if is_md:
|
||||
imgui.begin_child(f"heavy_text_child_{label}_{id_suffix}", imgui.ImVec2(0, 180), True, imgui.WindowFlags_.always_vertical_scrollbar)
|
||||
@@ -598,15 +593,13 @@ class App:
|
||||
imgui.pop_text_wrap_pos()
|
||||
else:
|
||||
imgui.text(content)
|
||||
|
||||
if is_nerv: imgui.pop_style_color()
|
||||
# ---------------------------------------------------------------- gui
|
||||
|
||||
def _render_thinking_trace(self, segments: list[dict], entry_index: int, is_standalone: bool = False) -> None:
|
||||
if not segments:
|
||||
return
|
||||
with imscope.style_color(imgui.Col_.child_bg, vec4(40, 35, 25, 180)), \
|
||||
imscope.style_color(imgui.Col_.text, vec4(200, 200, 150)):
|
||||
theme.ai_text_style():
|
||||
imgui.indent()
|
||||
|
||||
show_content = True
|
||||
@@ -624,9 +617,9 @@ class App:
|
||||
imgui.text_colored(vec4(180, 150, 80), f"[{marker}]")
|
||||
if self.ui_word_wrap:
|
||||
with imscope.text_wrap(imgui.get_content_region_avail().x):
|
||||
imgui.text_colored(vec4(200, 200, 150), content)
|
||||
imgui.text(content)
|
||||
else:
|
||||
imgui.text_colored(vec4(200, 200, 150), content)
|
||||
imgui.text(content)
|
||||
imgui.separator()
|
||||
|
||||
imgui.unindent()
|
||||
@@ -927,102 +920,6 @@ class App:
|
||||
if p not in self.screenshots: self.screenshots.append(p)
|
||||
return
|
||||
|
||||
def _render_main_interface(self) -> None:
|
||||
self.perf_monitor.start_frame()
|
||||
self._autofocus_response_tab = self.controller._autofocus_response_tab
|
||||
|
||||
#region: Process GUI task queue
|
||||
# DEBUG: Check if tasks exist before processing
|
||||
if hasattr(self, 'controller') and hasattr(self.controller, '_pending_gui_tasks'):
|
||||
pending_count = len(self.controller._pending_gui_tasks)
|
||||
if pending_count > 0:
|
||||
sys.stderr.write(f"[DEBUG gui_2] _gui_func: found {pending_count} pending tasks\n")
|
||||
sys.stderr.flush()
|
||||
self._process_pending_gui_tasks()
|
||||
self._process_pending_history_adds()
|
||||
if self.controller._process_pending_tool_calls(): self._tool_log_dirty = True
|
||||
#endregion: Process GUI task queue
|
||||
|
||||
self._render_track_proposal_modal()
|
||||
self._render_patch_modal()
|
||||
self._render_base_prompt_diff_modal()
|
||||
self._render_save_preset_modal()
|
||||
self._render_save_workspace_profile_modal()
|
||||
self._render_add_context_files_modal()
|
||||
self._render_preset_manager_window()
|
||||
self._render_tool_preset_manager_window()
|
||||
self._render_persona_editor_window()
|
||||
|
||||
# Auto-save (every 60s)
|
||||
now = time.time()
|
||||
if now - self._last_autosave >= self._autosave_interval:
|
||||
self._last_autosave = now
|
||||
try:
|
||||
self._flush_to_project()
|
||||
self._flush_to_config()
|
||||
models.save_config(self.config)
|
||||
except Exception:
|
||||
pass # silent — don't disrupt the GUI loop
|
||||
|
||||
# Sync pending comms
|
||||
with self._pending_comms_lock:
|
||||
if self._pending_comms:
|
||||
if self.ui_auto_scroll_comms: self._scroll_comms_to_bottom = True
|
||||
self._comms_log_dirty = True
|
||||
for c in self._pending_comms: self._comms_log.append(c)
|
||||
self._pending_comms.clear()
|
||||
|
||||
if self.ui_focus_agent != self._last_ui_focus_agent:
|
||||
self._comms_log_dirty = True
|
||||
self._tool_log_dirty = True
|
||||
self._last_ui_focus_agent = self.ui_focus_agent
|
||||
|
||||
if self._comms_log_dirty:
|
||||
if self.is_viewing_prior_session: self._comms_log_cache = self.prior_session_entries
|
||||
else:
|
||||
log_raw = list(self._comms_log)
|
||||
if self.ui_focus_agent: self._comms_log_cache = [e for e in log_raw if e.get("source_tier", "").startswith(self.ui_focus_agent)]
|
||||
else: self._comms_log_cache = log_raw
|
||||
self._comms_log_dirty = False
|
||||
|
||||
if self._tool_log_dirty:
|
||||
if self.is_viewing_prior_session: self._tool_log_cache = self.prior_tool_calls
|
||||
else:
|
||||
log_raw = list(self._tool_log)
|
||||
if self.ui_focus_agent: self._tool_log_cache = [e for e in log_raw if e.get("source_tier", "").startswith(self.ui_focus_agent)]
|
||||
else: self._tool_log_cache = log_raw
|
||||
self._tool_log_dirty = False
|
||||
|
||||
self._render_window_if_open("Project Settings", self._render_project_settings_hub)
|
||||
self._render_window_if_open("Files & Media", self._render_files_and_media)
|
||||
self._render_window_if_open("AI Settings", self._render_ai_settings_hub)
|
||||
self._render_window_if_open("Usage Analytics", self._render_usage_analytics_panel, self.ui_separate_usage_analytics)
|
||||
self._render_window_if_open("MMA Dashboard", self._render_mma_dashboard)
|
||||
self._render_window_if_open("Task DAG", self._render_task_dag_panel, self.ui_separate_task_dag)
|
||||
|
||||
self._render_window_if_open("Tier 1: Strategy", lambda: self._render_tier_stream_panel("Tier 1", "Tier 1"), self.ui_separate_tier1)
|
||||
self._render_window_if_open("Tier 2: Tech Lead", lambda: self._render_tier_stream_panel("Tier 2", "Tier 2 (Tech Lead)"), self.ui_separate_tier2)
|
||||
self._render_window_if_open("Tier 3: Workers", lambda: self._render_tier_stream_panel("Tier 3", None), self.ui_separate_tier3)
|
||||
self._render_window_if_open("Tier 4: QA", lambda: self._render_tier_stream_panel("Tier 4", "Tier 4 (QA)"), self.ui_separate_tier4)
|
||||
|
||||
if self.show_windows.get("Theme", False): self._render_theme_panel()
|
||||
|
||||
self._render_window_if_open("Discussion Hub", self._render_discussion_hub)
|
||||
self._render_window_if_open("Operations Hub", self._render_operations_hub)
|
||||
|
||||
self._render_window_if_open("Message", self._render_message_panel, self.ui_separate_message_panel)
|
||||
self._render_window_if_open("Response", self._render_response_panel, self.ui_separate_response_panel)
|
||||
self._render_window_if_open("Tool Calls", self._render_tool_calls_panel, self.ui_separate_tool_calls_panel)
|
||||
self._render_window_if_open("External Tools", self._render_external_tools_panel, self.ui_separate_external_tools)
|
||||
self._render_window_if_open("Log Management", self._render_log_management)
|
||||
self._render_window_if_open("Diagnostics", self._render_diagnostics_panel)
|
||||
|
||||
self.perf_monitor.end_frame()
|
||||
|
||||
# Modals / Popups
|
||||
self._render_approve_script_modal()
|
||||
self._render_mma_modals()
|
||||
|
||||
def _render_mma_modals(self) -> None:
|
||||
"""Renders all MMA-specific approval and info modals."""
|
||||
is_nerv = theme.is_nerv_active()
|
||||
@@ -1240,8 +1137,7 @@ class App:
|
||||
if len(content) > 80: preview += "..."
|
||||
imgui.text_colored(vec4(180, 180, 180), preview)
|
||||
else:
|
||||
is_nerv = theme.is_nerv_active()
|
||||
with imscope.style_color(imgui.Col_.text, vec4(80, 255, 80)) if is_nerv else nullcontext():
|
||||
with theme.ai_text_style():
|
||||
markdown_helper.render(content, context_id=f'prior_disc_{idx}')
|
||||
imgui.separator()
|
||||
|
||||
@@ -1422,7 +1318,7 @@ class App:
|
||||
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.style_color(imgui.Col_.text, vec4(80, 255, 80)) if is_nerv else nullcontext():
|
||||
with theme.ai_text_style():
|
||||
markdown_helper.render(content, context_id=f'disc_{index}')
|
||||
else:
|
||||
with imscope.child(f"read_content_{index}", size_y=150, flags=True):
|
||||
@@ -1431,7 +1327,7 @@ class App:
|
||||
for m_idx, match in enumerate(matches):
|
||||
before = content[last_idx:match.start()]
|
||||
if before:
|
||||
with imscope.style_color(imgui.Col_.text, vec4(80, 255, 80)) if is_nerv else nullcontext():
|
||||
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):
|
||||
@@ -1439,12 +1335,12 @@ class App:
|
||||
res = mcp_client.read_file(path)
|
||||
if res: self.text_viewer_title, self.text_viewer_content, self.text_viewer_type, self.show_text_viewer = path, res, (Path(path).suffix.lstrip('.') if Path(path).suffix else 'text'), True
|
||||
if code_block:
|
||||
with imscope.style_color(imgui.Col_.text, vec4(80, 255, 80)) if is_nerv else nullcontext():
|
||||
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 imscope.style_color(imgui.Col_.text, vec4(80, 255, 80)) if is_nerv else nullcontext():
|
||||
with theme.ai_text_style():
|
||||
markdown_helper.render(after, context_id=f'disc_{index}_a')
|
||||
if self.ui_word_wrap: imgui.pop_text_wrap_pos()
|
||||
|
||||
@@ -1620,6 +1516,347 @@ class App:
|
||||
if exp: self._render_beads_tab()
|
||||
imgui.end_tab_bar()
|
||||
|
||||
def _gui_func(self) -> None:
|
||||
self._render_custom_title_bar()
|
||||
self._render_shader_live_editor()
|
||||
self._render_history_window()
|
||||
pushed_prior_tint = False
|
||||
|
||||
# Render background shader
|
||||
bg = bg_shader.get_bg()
|
||||
ws = imgui.get_io().display_size
|
||||
if bg.enabled:
|
||||
bg.render(ws.x, ws.y)
|
||||
|
||||
theme.render_post_fx(ws.x, ws.y, self.ai_status, self.ui_crt_filter)
|
||||
|
||||
if self.perf_profiling_enabled: self.perf_monitor.start_component("_gui_func")
|
||||
if self.is_viewing_prior_session:
|
||||
imgui.push_style_color(imgui.Col_.window_bg, vec4(50, 40, 20))
|
||||
pushed_prior_tint = True
|
||||
|
||||
try:
|
||||
self._render_main_interface()
|
||||
except Exception as e:
|
||||
print(f"ERROR in _gui_func: {e}")
|
||||
traceback.print_exc()
|
||||
|
||||
if pushed_prior_tint:
|
||||
imgui.pop_style_color()
|
||||
|
||||
self._handle_history_logic()
|
||||
|
||||
if self.perf_profiling_enabled: self.perf_monitor.end_component("_gui_func")
|
||||
return
|
||||
|
||||
def _render_main_interface(self) -> None:
|
||||
self.perf_monitor.start_frame()
|
||||
self._autofocus_response_tab = self.controller._autofocus_response_tab
|
||||
|
||||
#region: Process GUI task queue
|
||||
# DEBUG: Check if tasks exist before processing
|
||||
if hasattr(self, 'controller') and hasattr(self.controller, '_pending_gui_tasks'):
|
||||
pending_count = len(self.controller._pending_gui_tasks)
|
||||
if pending_count > 0:
|
||||
sys.stderr.write(f"[DEBUG gui_2] _gui_func: found {pending_count} pending tasks\n")
|
||||
sys.stderr.flush()
|
||||
self._process_pending_gui_tasks()
|
||||
self._process_pending_history_adds()
|
||||
if self.controller._process_pending_tool_calls(): self._tool_log_dirty = True
|
||||
#endregion: Process GUI task queue
|
||||
|
||||
self._render_track_proposal_modal()
|
||||
self._render_patch_modal()
|
||||
self._render_base_prompt_diff_modal()
|
||||
self._render_save_preset_modal()
|
||||
self._render_save_workspace_profile_modal()
|
||||
self._render_add_context_files_modal()
|
||||
self._render_preset_manager_window()
|
||||
self._render_tool_preset_manager_window()
|
||||
self._render_persona_editor_window()
|
||||
|
||||
# Auto-save (every 60s)
|
||||
now = time.time()
|
||||
if now - self._last_autosave >= self._autosave_interval:
|
||||
self._last_autosave = now
|
||||
try:
|
||||
self._flush_to_project()
|
||||
self._flush_to_config()
|
||||
models.save_config(self.config)
|
||||
except Exception:
|
||||
pass # silent — don't disrupt the GUI loop
|
||||
|
||||
# Sync pending comms
|
||||
with self._pending_comms_lock:
|
||||
if self._pending_comms:
|
||||
if self.ui_auto_scroll_comms: self._scroll_comms_to_bottom = True
|
||||
self._comms_log_dirty = True
|
||||
for c in self._pending_comms: self._comms_log.append(c)
|
||||
self._pending_comms.clear()
|
||||
|
||||
if self.ui_focus_agent != self._last_ui_focus_agent:
|
||||
self._comms_log_dirty = True
|
||||
self._tool_log_dirty = True
|
||||
self._last_ui_focus_agent = self.ui_focus_agent
|
||||
|
||||
if self._comms_log_dirty:
|
||||
if self.is_viewing_prior_session: self._comms_log_cache = self.prior_session_entries
|
||||
else:
|
||||
log_raw = list(self._comms_log)
|
||||
if self.ui_focus_agent: self._comms_log_cache = [e for e in log_raw if e.get("source_tier", "").startswith(self.ui_focus_agent)]
|
||||
else: self._comms_log_cache = log_raw
|
||||
self._comms_log_dirty = False
|
||||
|
||||
if self._tool_log_dirty:
|
||||
if self.is_viewing_prior_session: self._tool_log_cache = self.prior_tool_calls
|
||||
else:
|
||||
log_raw = list(self._tool_log)
|
||||
if self.ui_focus_agent: self._tool_log_cache = [e for e in log_raw if e.get("source_tier", "").startswith(self.ui_focus_agent)]
|
||||
else: self._tool_log_cache = log_raw
|
||||
self._tool_log_dirty = False
|
||||
|
||||
self._render_window_if_open("Project Settings", self._render_project_settings_hub)
|
||||
self._render_window_if_open("Files & Media", self._render_files_and_media)
|
||||
self._render_window_if_open("AI Settings", self._render_ai_settings_hub)
|
||||
self._render_window_if_open("Usage Analytics", self._render_usage_analytics_panel, self.ui_separate_usage_analytics)
|
||||
self._render_window_if_open("MMA Dashboard", self._render_mma_dashboard)
|
||||
self._render_window_if_open("Task DAG", self._render_task_dag_panel, self.ui_separate_task_dag)
|
||||
|
||||
self._render_window_if_open("Tier 1: Strategy", lambda: self._render_tier_stream_panel("Tier 1", "Tier 1"), self.ui_separate_tier1)
|
||||
self._render_window_if_open("Tier 2: Tech Lead", lambda: self._render_tier_stream_panel("Tier 2", "Tier 2 (Tech Lead)"), self.ui_separate_tier2)
|
||||
self._render_window_if_open("Tier 3: Workers", lambda: self._render_tier_stream_panel("Tier 3", None), self.ui_separate_tier3)
|
||||
self._render_window_if_open("Tier 4: QA", lambda: self._render_tier_stream_panel("Tier 4", "Tier 4 (QA)"), self.ui_separate_tier4)
|
||||
|
||||
if self.show_windows.get("Theme", False): self._render_theme_panel()
|
||||
|
||||
self._render_window_if_open("Discussion Hub", self._render_discussion_hub)
|
||||
self._render_window_if_open("Operations Hub", self._render_operations_hub)
|
||||
|
||||
self._render_window_if_open("Message", self._render_message_panel, self.ui_separate_message_panel)
|
||||
self._render_window_if_open("Response", self._render_response_panel, self.ui_separate_response_panel)
|
||||
self._render_window_if_open("Tool Calls", self._render_tool_calls_panel, self.ui_separate_tool_calls_panel)
|
||||
self._render_window_if_open("External Tools", self._render_external_tools_panel, self.ui_separate_external_tools)
|
||||
self._render_window_if_open("Log Management", self._render_log_management)
|
||||
self._render_window_if_open("Diagnostics", self._render_diagnostics_panel)
|
||||
|
||||
self.perf_monitor.end_frame()
|
||||
|
||||
# Modals / Popups
|
||||
self._render_approve_script_modal()
|
||||
self._render_mma_modals()
|
||||
|
||||
def _render_base_prompt_diff_modal(self) -> None:
|
||||
if not getattr(self.controller, "_show_base_prompt_diff_modal", False):
|
||||
return
|
||||
imgui.open_popup("Base Prompt Diff")
|
||||
if imgui.begin_popup_modal("Base Prompt Diff", True, imgui.WindowFlags_.always_auto_resize)[0]:
|
||||
imgui.text_colored(C_IN, "Difference between Default and Custom Base System Prompt")
|
||||
imgui.separator()
|
||||
|
||||
default_lines = ai_client._SYSTEM_PROMPT.splitlines(keepends=True)
|
||||
custom_lines = self.ui_base_system_prompt.splitlines(keepends=True)
|
||||
|
||||
diff = list(difflib.unified_diff(default_lines, custom_lines, fromfile='Default', tofile='Custom'))
|
||||
|
||||
if not diff:
|
||||
imgui.text("No differences found.")
|
||||
else:
|
||||
imgui.begin_child("base_prompt_diff_scroll", imgui.ImVec2(800, 500), True)
|
||||
for line in diff:
|
||||
if line.startswith("+++") or line.startswith("---") or line.startswith("@@"):
|
||||
imgui.text_colored(vec4(77, 178, 255), line.rstrip())
|
||||
elif line.startswith("+"):
|
||||
imgui.text_colored(vec4(51, 230, 51), line.rstrip())
|
||||
elif line.startswith("-"):
|
||||
imgui.text_colored(vec4(230, 51, 51), line.rstrip())
|
||||
else:
|
||||
imgui.text(line.rstrip())
|
||||
imgui.end_child()
|
||||
|
||||
imgui.separator()
|
||||
if imgui.button("Close", imgui.ImVec2(120, 0)):
|
||||
self.controller._show_base_prompt_diff_modal = False
|
||||
imgui.close_current_popup()
|
||||
imgui.end_popup()
|
||||
|
||||
def _handle_history_logic(self) -> None:
|
||||
"""
|
||||
Logic for capturing UI state for undo/redo.
|
||||
"""
|
||||
if self._is_applying_snapshot:
|
||||
return
|
||||
|
||||
try:
|
||||
# 2. Debounced snapshotting
|
||||
current = self._take_snapshot()
|
||||
if self._last_ui_snapshot is None:
|
||||
self._last_ui_snapshot = current
|
||||
return
|
||||
|
||||
# Compare only core fields for performance
|
||||
changed = (
|
||||
current.ai_input != self._last_ui_snapshot.ai_input or
|
||||
current.project_system_prompt != self._last_ui_snapshot.project_system_prompt or
|
||||
current.global_system_prompt != self._last_ui_snapshot.global_system_prompt or
|
||||
current.base_system_prompt != self._last_ui_snapshot.base_system_prompt or
|
||||
current.use_default_base_prompt != self._last_ui_snapshot.use_default_base_prompt or
|
||||
abs(current.temperature - self._last_ui_snapshot.temperature) > 1e-5 or
|
||||
abs(current.top_p - self._last_ui_snapshot.top_p) > 1e-5 or
|
||||
current.max_tokens != self._last_ui_snapshot.max_tokens or
|
||||
current.auto_add_history != self._last_ui_snapshot.auto_add_history or
|
||||
len(current.disc_entries) != len(self._last_ui_snapshot.disc_entries) or
|
||||
len(current.files) != len(self._last_ui_snapshot.files) or
|
||||
len(current.context_files) != len(self._last_ui_snapshot.context_files) or
|
||||
len(current.screenshots) != len(self._last_ui_snapshot.screenshots)
|
||||
)
|
||||
|
||||
if not changed and len(current.disc_entries) > 0:
|
||||
if current.disc_entries[-1].get('content') != self._last_ui_snapshot.disc_entries[-1].get('content'):
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
if not self._pending_snapshot:
|
||||
self._pending_snapshot = True
|
||||
self._snapshot_timer = time.time()
|
||||
# Capture state BEFORE current change
|
||||
self._state_to_push = self._last_ui_snapshot
|
||||
else:
|
||||
# Reset timer for settle debounce
|
||||
self._snapshot_timer = time.time()
|
||||
|
||||
self._last_ui_snapshot = current
|
||||
|
||||
if self._pending_snapshot and (time.time() - self._snapshot_timer > self._snapshot_debounce):
|
||||
if self._state_to_push:
|
||||
self.history.push(self._state_to_push, "UI Update")
|
||||
self._state_to_push = None
|
||||
self._pending_snapshot = False
|
||||
except Exception as e:
|
||||
import sys, traceback
|
||||
sys.stderr.write(f"[DEBUG History] ERROR in _handle_history_logic: {e}\n")
|
||||
traceback.print_exc(file=sys.stderr)
|
||||
sys.stderr.flush()
|
||||
|
||||
def _render_patch_modal(self) -> None:
|
||||
if not self._show_patch_modal:
|
||||
return
|
||||
imgui.open_popup("Apply Patch?")
|
||||
with imscope.popup_modal("Apply Patch?", True, imgui.WindowFlags_.always_auto_resize) as (opened, _):
|
||||
if opened:
|
||||
from src import shaders
|
||||
p_min = imgui.get_window_pos()
|
||||
p_max = imgui.ImVec2(p_min.x + imgui.get_window_size().x, p_min.y + imgui.get_window_size().y)
|
||||
shaders.draw_soft_shadow(imgui.get_background_draw_list(), p_min, p_max, imgui.ImVec4(0, 0, 0, 0.6), 25.0, 6.0)
|
||||
|
||||
imgui.text_colored(vec4(255, 230, 77), "Tier 4 QA Generated a Patch")
|
||||
imgui.separator()
|
||||
if self._pending_patch_files:
|
||||
imgui.text("Files to modify:")
|
||||
for f in self._pending_patch_files:
|
||||
imgui.text(f" - {f}")
|
||||
imgui.separator()
|
||||
if self._patch_error_message:
|
||||
imgui.text_colored(vec4(255, 77, 77), f"Error: {self._patch_error_message}")
|
||||
imgui.separator()
|
||||
imgui.text("Diff Preview:")
|
||||
imgui.begin_child("patch_diff_scroll", imgui.ImVec2(-1, 280), True)
|
||||
if self._pending_patch_text:
|
||||
diff_lines = self._pending_patch_text.split("\n")
|
||||
for line in diff_lines:
|
||||
if line.startswith("+++") or line.startswith("---") or line.startswith("@@"):
|
||||
imgui.text_colored(vec4(77, 178, 255), line)
|
||||
elif line.startswith("+"):
|
||||
imgui.text_colored(vec4(51, 230, 51), line)
|
||||
elif line.startswith("-"):
|
||||
imgui.text_colored(vec4(230, 51, 51), line)
|
||||
else:
|
||||
imgui.text(line)
|
||||
imgui.end_child()
|
||||
imgui.separator()
|
||||
if imgui.button("Open in External Editor"):
|
||||
self._open_patch_in_external_editor()
|
||||
imgui.same_line()
|
||||
if imgui.button("Apply Patch"):
|
||||
self._apply_pending_patch()
|
||||
self._close_vscode_diff()
|
||||
imgui.same_line()
|
||||
if imgui.button("Reject"):
|
||||
self._close_vscode_diff()
|
||||
self._show_patch_modal = False
|
||||
self._pending_patch_text = None
|
||||
self._pending_patch_files = []
|
||||
self._patch_error_message = None
|
||||
imgui.close_current_popup()
|
||||
|
||||
def _render_save_preset_modal(self) -> None:
|
||||
if not self._show_save_preset_modal: return
|
||||
imgui.open_popup("Save Layout Preset")
|
||||
with imscope.popup_modal("Save Layout Preset", True, imgui.WindowFlags_.always_auto_resize) as (opened, _):
|
||||
if opened:
|
||||
imgui.text("Preset Name:")
|
||||
_, self._new_preset_name = imgui.input_text("##preset_name", self._new_preset_name)
|
||||
if imgui.button("Save", imgui.ImVec2(120, 0)):
|
||||
if self._new_preset_name.strip():
|
||||
ini_data = imgui.save_ini_settings_to_memory()
|
||||
self.layout_presets[self._new_preset_name.strip()] = {
|
||||
"ini": ini_data,
|
||||
"multi_viewport": self.ui_multi_viewport
|
||||
}
|
||||
self.config["layout_presets"] = self.layout_presets
|
||||
models.save_config(self.config)
|
||||
self._show_save_preset_modal = False
|
||||
self._new_preset_name = ""
|
||||
imgui.close_current_popup()
|
||||
imgui.same_line()
|
||||
if imgui.button("Cancel", imgui.ImVec2(120, 0)):
|
||||
self._show_save_preset_modal = False
|
||||
imgui.close_current_popup()
|
||||
|
||||
def _render_track_proposal_modal(self) -> None:
|
||||
if self._show_track_proposal_modal:
|
||||
imgui.open_popup("Track Proposal")
|
||||
if imgui.begin_popup_modal("Track Proposal", True, imgui.WindowFlags_.always_auto_resize)[0]:
|
||||
from src import shaders
|
||||
p_min = imgui.get_window_pos()
|
||||
p_max = imgui.ImVec2(p_min.x + imgui.get_window_size().x, p_min.y + imgui.get_window_size().y)
|
||||
# Render soft shadow behind the modal
|
||||
shaders.draw_soft_shadow(imgui.get_background_draw_list(), p_min, p_max, imgui.ImVec4(0, 0, 0, 0.6), 25.0, 6.0)
|
||||
|
||||
if self._show_track_proposal_modal:
|
||||
imgui.text_colored(C_IN, "Proposed Implementation Tracks")
|
||||
imgui.separator()
|
||||
if not self.proposed_tracks:
|
||||
imgui.text("No tracks generated.")
|
||||
else:
|
||||
for idx, track in enumerate(self.proposed_tracks):
|
||||
# Title Edit
|
||||
changed_t, new_t = imgui.input_text(f"Title##{idx}", track.get('title', ''))
|
||||
if changed_t:
|
||||
track['title'] = new_t
|
||||
# Goal Edit
|
||||
changed_g, new_g = imgui.input_text_multiline(f"Goal##{idx}", track.get('goal', ''), imgui.ImVec2(-1, 60))
|
||||
if changed_g:
|
||||
track['goal'] = new_g
|
||||
# Buttons
|
||||
if imgui.button(f"Remove##{idx}"):
|
||||
self.proposed_tracks.pop(idx)
|
||||
break
|
||||
imgui.same_line()
|
||||
if imgui.button(f"Start This Track##{idx}"):
|
||||
self._cb_start_track(idx)
|
||||
imgui.separator()
|
||||
if imgui.button("Accept", imgui.ImVec2(120, 0)):
|
||||
self._cb_accept_tracks()
|
||||
self._show_track_proposal_modal = False
|
||||
imgui.close_current_popup()
|
||||
imgui.same_line()
|
||||
if imgui.button("Cancel", imgui.ImVec2(120, 0)):
|
||||
self._show_track_proposal_modal = False
|
||||
imgui.close_current_popup()
|
||||
else:
|
||||
imgui.close_current_popup()
|
||||
imgui.end_popup()
|
||||
|
||||
def _render_text_viewer_window(self) -> None:
|
||||
"""Renders the standalone text/code/markdown viewer window."""
|
||||
if not self.show_text_viewer: return
|
||||
@@ -1741,160 +1978,6 @@ class App:
|
||||
self._render_ast_inspector_modal()
|
||||
return
|
||||
|
||||
def _gui_func(self) -> None:
|
||||
self._render_custom_title_bar()
|
||||
self._render_shader_live_editor()
|
||||
self._render_history_window()
|
||||
pushed_prior_tint = False
|
||||
|
||||
# Render background shader
|
||||
bg = bg_shader.get_bg()
|
||||
if bg.enabled:
|
||||
ws = imgui.get_io().display_size
|
||||
bg.render(ws.x, ws.y)
|
||||
|
||||
if theme.is_nerv_active():
|
||||
ws = imgui.get_io().display_size
|
||||
self._nerv_alert.update(self.ai_status)
|
||||
self._nerv_alert.render(ws.x, ws.y)
|
||||
self._nerv_crt.enabled = self.ui_crt_filter
|
||||
self._nerv_crt.render(ws.x, ws.y)
|
||||
|
||||
if self.perf_profiling_enabled: self.perf_monitor.start_component("_gui_func")
|
||||
if self.is_viewing_prior_session:
|
||||
imgui.push_style_color(imgui.Col_.window_bg, vec4(50, 40, 20))
|
||||
pushed_prior_tint = True
|
||||
|
||||
try:
|
||||
self._render_main_interface()
|
||||
except Exception as e:
|
||||
print(f"ERROR in _gui_func: {e}")
|
||||
traceback.print_exc()
|
||||
|
||||
if pushed_prior_tint:
|
||||
imgui.pop_style_color()
|
||||
|
||||
self._handle_history_logic()
|
||||
|
||||
if self.perf_profiling_enabled: self.perf_monitor.end_component("_gui_func")
|
||||
return
|
||||
|
||||
def _handle_history_logic(self) -> None:
|
||||
"""
|
||||
Logic for capturing UI state for undo/redo.
|
||||
"""
|
||||
if self._is_applying_snapshot:
|
||||
return
|
||||
|
||||
try:
|
||||
# 2. Debounced snapshotting
|
||||
current = self._take_snapshot()
|
||||
if self._last_ui_snapshot is None:
|
||||
self._last_ui_snapshot = current
|
||||
return
|
||||
|
||||
# Compare only core fields for performance
|
||||
changed = (
|
||||
current.ai_input != self._last_ui_snapshot.ai_input or
|
||||
current.project_system_prompt != self._last_ui_snapshot.project_system_prompt or
|
||||
current.global_system_prompt != self._last_ui_snapshot.global_system_prompt or
|
||||
current.base_system_prompt != self._last_ui_snapshot.base_system_prompt or
|
||||
current.use_default_base_prompt != self._last_ui_snapshot.use_default_base_prompt or
|
||||
abs(current.temperature - self._last_ui_snapshot.temperature) > 1e-5 or
|
||||
abs(current.top_p - self._last_ui_snapshot.top_p) > 1e-5 or
|
||||
current.max_tokens != self._last_ui_snapshot.max_tokens or
|
||||
current.auto_add_history != self._last_ui_snapshot.auto_add_history or
|
||||
len(current.disc_entries) != len(self._last_ui_snapshot.disc_entries) or
|
||||
len(current.files) != len(self._last_ui_snapshot.files) or
|
||||
len(current.context_files) != len(self._last_ui_snapshot.context_files) or
|
||||
len(current.screenshots) != len(self._last_ui_snapshot.screenshots)
|
||||
)
|
||||
|
||||
if not changed and len(current.disc_entries) > 0:
|
||||
if current.disc_entries[-1].get('content') != self._last_ui_snapshot.disc_entries[-1].get('content'):
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
if not self._pending_snapshot:
|
||||
self._pending_snapshot = True
|
||||
self._snapshot_timer = time.time()
|
||||
# Capture state BEFORE current change
|
||||
self._state_to_push = self._last_ui_snapshot
|
||||
else:
|
||||
# Reset timer for settle debounce
|
||||
self._snapshot_timer = time.time()
|
||||
|
||||
self._last_ui_snapshot = current
|
||||
|
||||
if self._pending_snapshot and (time.time() - self._snapshot_timer > self._snapshot_debounce):
|
||||
if self._state_to_push:
|
||||
self.history.push(self._state_to_push, "UI Update")
|
||||
self._state_to_push = None
|
||||
self._pending_snapshot = False
|
||||
except Exception as e:
|
||||
import sys, traceback
|
||||
sys.stderr.write(f"[DEBUG History] ERROR in _handle_history_logic: {e}\n")
|
||||
traceback.print_exc(file=sys.stderr)
|
||||
sys.stderr.flush()
|
||||
|
||||
def _render_base_prompt_diff_modal(self) -> None:
|
||||
if not getattr(self.controller, "_show_base_prompt_diff_modal", False):
|
||||
return
|
||||
imgui.open_popup("Base Prompt Diff")
|
||||
if imgui.begin_popup_modal("Base Prompt Diff", True, imgui.WindowFlags_.always_auto_resize)[0]:
|
||||
imgui.text_colored(C_IN, "Difference between Default and Custom Base System Prompt")
|
||||
imgui.separator()
|
||||
|
||||
default_lines = ai_client._SYSTEM_PROMPT.splitlines(keepends=True)
|
||||
custom_lines = self.ui_base_system_prompt.splitlines(keepends=True)
|
||||
|
||||
diff = list(difflib.unified_diff(default_lines, custom_lines, fromfile='Default', tofile='Custom'))
|
||||
|
||||
if not diff:
|
||||
imgui.text("No differences found.")
|
||||
else:
|
||||
imgui.begin_child("base_prompt_diff_scroll", imgui.ImVec2(800, 500), True)
|
||||
for line in diff:
|
||||
if line.startswith("+++") or line.startswith("---") or line.startswith("@@"):
|
||||
imgui.text_colored(vec4(77, 178, 255), line.rstrip())
|
||||
elif line.startswith("+"):
|
||||
imgui.text_colored(vec4(51, 230, 51), line.rstrip())
|
||||
elif line.startswith("-"):
|
||||
imgui.text_colored(vec4(230, 51, 51), line.rstrip())
|
||||
else:
|
||||
imgui.text(line.rstrip())
|
||||
imgui.end_child()
|
||||
|
||||
imgui.separator()
|
||||
if imgui.button("Close", imgui.ImVec2(120, 0)):
|
||||
self.controller._show_base_prompt_diff_modal = False
|
||||
imgui.close_current_popup()
|
||||
imgui.end_popup()
|
||||
|
||||
def _render_save_preset_modal(self) -> None:
|
||||
if not self._show_save_preset_modal: return
|
||||
imgui.open_popup("Save Layout Preset")
|
||||
if imgui.begin_popup_modal("Save Layout Preset", True, imgui.WindowFlags_.always_auto_resize)[0]:
|
||||
imgui.text("Preset Name:")
|
||||
_, self._new_preset_name = imgui.input_text("##preset_name", self._new_preset_name)
|
||||
if imgui.button("Save", imgui.ImVec2(120, 0)):
|
||||
if self._new_preset_name.strip():
|
||||
ini_data = imgui.save_ini_settings_to_memory()
|
||||
self.layout_presets[self._new_preset_name.strip()] = {
|
||||
"ini": ini_data,
|
||||
"multi_viewport": self.ui_multi_viewport
|
||||
}
|
||||
self.config["layout_presets"] = self.layout_presets
|
||||
models.save_config(self.config)
|
||||
self._show_save_preset_modal = False
|
||||
self._new_preset_name = ""
|
||||
imgui.close_current_popup()
|
||||
imgui.same_line()
|
||||
if imgui.button("Cancel", imgui.ImVec2(120, 0)):
|
||||
self._show_save_preset_modal = False
|
||||
imgui.close_current_popup()
|
||||
imgui.end_popup()
|
||||
|
||||
def _render_ast_inspector_modal(self) -> None:
|
||||
if self._show_ast_inspector:
|
||||
imgui.open_popup('AST Inspector')
|
||||
@@ -2377,6 +2460,7 @@ class App:
|
||||
if not is_embedded:
|
||||
if imgui.button("Close##tp", imgui.ImVec2(100, 0)): self.show_tool_preset_manager_window = False
|
||||
imgui.end_table()
|
||||
|
||||
def _render_tool_preset_manager_window(self, is_embedded: bool = False) -> None:
|
||||
if not self.show_tool_preset_manager_window and not is_embedded: return
|
||||
if not is_embedded:
|
||||
@@ -2730,102 +2814,6 @@ class App:
|
||||
if imgui.button(f"Delete##{name}"):
|
||||
self.delete_context_preset(name)
|
||||
|
||||
def _render_track_proposal_modal(self) -> None:
|
||||
if self._show_track_proposal_modal:
|
||||
imgui.open_popup("Track Proposal")
|
||||
if imgui.begin_popup_modal("Track Proposal", True, imgui.WindowFlags_.always_auto_resize)[0]:
|
||||
from src import shaders
|
||||
p_min = imgui.get_window_pos()
|
||||
p_max = imgui.ImVec2(p_min.x + imgui.get_window_size().x, p_min.y + imgui.get_window_size().y)
|
||||
# Render soft shadow behind the modal
|
||||
shaders.draw_soft_shadow(imgui.get_background_draw_list(), p_min, p_max, imgui.ImVec4(0, 0, 0, 0.6), 25.0, 6.0)
|
||||
|
||||
if self._show_track_proposal_modal:
|
||||
imgui.text_colored(C_IN, "Proposed Implementation Tracks")
|
||||
imgui.separator()
|
||||
if not self.proposed_tracks:
|
||||
imgui.text("No tracks generated.")
|
||||
else:
|
||||
for idx, track in enumerate(self.proposed_tracks):
|
||||
# Title Edit
|
||||
changed_t, new_t = imgui.input_text(f"Title##{idx}", track.get('title', ''))
|
||||
if changed_t:
|
||||
track['title'] = new_t
|
||||
# Goal Edit
|
||||
changed_g, new_g = imgui.input_text_multiline(f"Goal##{idx}", track.get('goal', ''), imgui.ImVec2(-1, 60))
|
||||
if changed_g:
|
||||
track['goal'] = new_g
|
||||
# Buttons
|
||||
if imgui.button(f"Remove##{idx}"):
|
||||
self.proposed_tracks.pop(idx)
|
||||
break
|
||||
imgui.same_line()
|
||||
if imgui.button(f"Start This Track##{idx}"):
|
||||
self._cb_start_track(idx)
|
||||
imgui.separator()
|
||||
if imgui.button("Accept", imgui.ImVec2(120, 0)):
|
||||
self._cb_accept_tracks()
|
||||
self._show_track_proposal_modal = False
|
||||
imgui.close_current_popup()
|
||||
imgui.same_line()
|
||||
if imgui.button("Cancel", imgui.ImVec2(120, 0)):
|
||||
self._show_track_proposal_modal = False
|
||||
imgui.close_current_popup()
|
||||
else:
|
||||
imgui.close_current_popup()
|
||||
imgui.end_popup()
|
||||
|
||||
def _render_patch_modal(self) -> None:
|
||||
if not self._show_patch_modal:
|
||||
return
|
||||
imgui.open_popup("Apply Patch?")
|
||||
if imgui.begin_popup_modal("Apply Patch?", True, imgui.WindowFlags_.always_auto_resize)[0]:
|
||||
from src import shaders
|
||||
p_min = imgui.get_window_pos()
|
||||
p_max = imgui.ImVec2(p_min.x + imgui.get_window_size().x, p_min.y + imgui.get_window_size().y)
|
||||
shaders.draw_soft_shadow(imgui.get_background_draw_list(), p_min, p_max, imgui.ImVec4(0, 0, 0, 0.6), 25.0, 6.0)
|
||||
|
||||
imgui.text_colored(vec4(255, 230, 77), "Tier 4 QA Generated a Patch")
|
||||
imgui.separator()
|
||||
if self._pending_patch_files:
|
||||
imgui.text("Files to modify:")
|
||||
for f in self._pending_patch_files:
|
||||
imgui.text(f" - {f}")
|
||||
imgui.separator()
|
||||
if self._patch_error_message:
|
||||
imgui.text_colored(vec4(255, 77, 77), f"Error: {self._patch_error_message}")
|
||||
imgui.separator()
|
||||
imgui.text("Diff Preview:")
|
||||
imgui.begin_child("patch_diff_scroll", imgui.ImVec2(-1, 280), True)
|
||||
if self._pending_patch_text:
|
||||
diff_lines = self._pending_patch_text.split("\n")
|
||||
for line in diff_lines:
|
||||
if line.startswith("+++") or line.startswith("---") or line.startswith("@@"):
|
||||
imgui.text_colored(vec4(77, 178, 255), line)
|
||||
elif line.startswith("+"):
|
||||
imgui.text_colored(vec4(51, 230, 51), line)
|
||||
elif line.startswith("-"):
|
||||
imgui.text_colored(vec4(230, 51, 51), line)
|
||||
else:
|
||||
imgui.text(line)
|
||||
imgui.end_child()
|
||||
imgui.separator()
|
||||
if imgui.button("Open in External Editor"):
|
||||
self._open_patch_in_external_editor()
|
||||
imgui.same_line()
|
||||
if imgui.button("Apply Patch"):
|
||||
self._apply_pending_patch()
|
||||
self._close_vscode_diff()
|
||||
imgui.same_line()
|
||||
if imgui.button("Reject"):
|
||||
self._close_vscode_diff()
|
||||
self._show_patch_modal = False
|
||||
self._pending_patch_text = None
|
||||
self._pending_patch_files = []
|
||||
self._patch_error_message = None
|
||||
imgui.close_current_popup()
|
||||
imgui.end_popup()
|
||||
|
||||
def _apply_pending_patch(self) -> None:
|
||||
if not self._pending_patch_text:
|
||||
self._patch_error_message = "No patch to apply"
|
||||
@@ -4085,8 +4073,7 @@ def hello():
|
||||
with imscope.style_color(imgui.Col_.frame_bg, blink_color) if is_blinking else nullcontext():
|
||||
with imscope.style_color(imgui.Col_.child_bg, blink_color) if is_blinking else nullcontext():
|
||||
with imscope.child("response_scroll_area", imgui.ImVec2(0, -40), True):
|
||||
is_nerv = theme.is_nerv_active()
|
||||
with imscope.style_color(imgui.Col_.text, vec4(80, 255, 80)) if is_nerv else nullcontext():
|
||||
with theme.ai_text_style():
|
||||
segments, parsed_response = thinking_parser.parse_thinking_trace(self.ai_response)
|
||||
if segments:
|
||||
self._render_thinking_trace([{"content": s.content, "marker": s.marker} for s in segments], 9999)
|
||||
@@ -5340,9 +5327,8 @@ def hello():
|
||||
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:
|
||||
if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80))
|
||||
with theme.ai_text_style():
|
||||
markdown_helper.render(content, context_id=f'disc_{index}')
|
||||
if is_nerv: imgui.pop_style_color()
|
||||
else:
|
||||
with imscope.child(f"read_content_{index}", size_y=150, flags=True):
|
||||
if self.ui_word_wrap: imgui.push_text_wrap_pos(imgui.get_content_region_avail().x)
|
||||
@@ -5350,24 +5336,21 @@ def hello():
|
||||
for m_idx, match in enumerate(matches):
|
||||
before = content[last_idx:match.start()]
|
||||
if before:
|
||||
if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80))
|
||||
with theme.ai_text_style():
|
||||
markdown_helper.render(before, context_id=f'disc_{index}_b_{m_idx}')
|
||||
if is_nerv: imgui.pop_style_color()
|
||||
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: self.text_viewer_title, self.text_viewer_content, self.text_viewer_type, self.show_text_viewer = path, res, (Path(path).suffix.lstrip('.') if Path(path).suffix else 'text'), True
|
||||
if code_block:
|
||||
if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80))
|
||||
with theme.ai_text_style():
|
||||
markdown_helper.render(code_block, context_id=f'disc_{index}_c_{m_idx}')
|
||||
if is_nerv: imgui.pop_style_color()
|
||||
last_idx = match.end()
|
||||
after = content[last_idx:]
|
||||
if after:
|
||||
if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80))
|
||||
with theme.ai_text_style():
|
||||
markdown_helper.render(after, context_id=f'disc_{index}_a')
|
||||
if is_nerv: imgui.pop_style_color()
|
||||
if self.ui_word_wrap: imgui.pop_text_wrap_pos()
|
||||
|
||||
def _load_fonts(self) -> None:
|
||||
|
||||
@@ -86,6 +86,21 @@ class _ScopePopup:
|
||||
imgui.end_popup()
|
||||
return False
|
||||
|
||||
def popup_modal(name: str, visible: bool = True, flags: int = 0): return _ScopePopupModal(name, visible, flags)
|
||||
class _ScopePopupModal:
|
||||
def __init__(self, name: str, visible: bool, flags: int):
|
||||
self._name = name
|
||||
self._visible = visible
|
||||
self._flags = flags
|
||||
self._active = False
|
||||
def __enter__(self):
|
||||
self._active, self._visible = imgui.begin_popup_modal(self._name, self._visible, self._flags)
|
||||
return self._active, self._visible
|
||||
def __exit__(self, *args):
|
||||
if self._active:
|
||||
imgui.end_popup()
|
||||
return False
|
||||
|
||||
def tooltip(): return _ScopeTooltip()
|
||||
class _ScopeTooltip:
|
||||
def __enter__(self):
|
||||
|
||||
@@ -10,7 +10,11 @@ Scale uses imgui.get_style().font_scale_main.
|
||||
|
||||
from imgui_bundle import imgui, hello_imgui
|
||||
from typing import Any, Optional
|
||||
from contextlib import nullcontext
|
||||
from src import imgui_scopes as imscope
|
||||
import src.theme_nerv
|
||||
from src.theme_nerv import DATA_GREEN
|
||||
from src.theme_nerv_fx import CRTFilter, AlertPulsing, StatusFlicker
|
||||
|
||||
# ------------------------------------------------------------------ palettes
|
||||
|
||||
@@ -239,6 +243,10 @@ _current_scale: float = 1.0
|
||||
_transparency: float = 1.0
|
||||
_child_transparency: float = 1.0
|
||||
|
||||
_crt_filter = CRTFilter()
|
||||
_alert_pulsing = AlertPulsing()
|
||||
_status_flicker = StatusFlicker()
|
||||
|
||||
# ------------------------------------------------------------------ public API
|
||||
|
||||
def get_current_palette() -> str:
|
||||
@@ -396,3 +404,20 @@ def get_tweaked_theme() -> hello_imgui.ImGuiTweakedTheme:
|
||||
# Sync tweaks
|
||||
tt.tweaks.rounding = 6.0
|
||||
return tt
|
||||
|
||||
def ai_text_color() -> imgui.ImVec4:
|
||||
"""Returns DATA_GREEN if NERV is active, otherwise standard text color."""
|
||||
if is_nerv_active():
|
||||
return imgui.ImVec4(*DATA_GREEN)
|
||||
return imgui.get_style().color_(imgui.Col_.text)
|
||||
|
||||
def ai_text_style():
|
||||
"""Context manager for AI response text styling."""
|
||||
return imscope.style_color(imgui.Col_.text, ai_text_color())
|
||||
|
||||
def render_post_fx(width: float, height: float, ai_status: str, crt_enabled: bool) -> None:
|
||||
"""Updates and renders the alert and CRT filters."""
|
||||
_alert_pulsing.update(ai_status)
|
||||
_alert_pulsing.render(width, height)
|
||||
_crt_filter.enabled = crt_enabled
|
||||
_crt_filter.render(width, height)
|
||||
|
||||
Reference in New Issue
Block a user