refactor(theme): Introduce semantic theme layer and clean NERV cruft from gui_2.py

This commit is contained in:
2026-05-12 20:24:47 -04:00
parent b53fc19f99
commit 12465fd04c
4 changed files with 450 additions and 393 deletions
@@ -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.
+376 -393
View File
@@ -238,10 +238,7 @@ class App:
self._last_ui_focus_agent: Optional[str] = None self._last_ui_focus_agent: Optional[str] = None
self._log_registry: Optional[log_registry.LogRegistry] = None self._log_registry: Optional[log_registry.LogRegistry] = None
self.perf_show_graphs: dict[str, bool] = {} self.perf_show_graphs: dict[str, bool] = {}
self._nerv_crt = theme_fx.CRTFilter()
self.ui_crt_filter = True 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_tool_filter_category = "All"
self.ui_discussion_split_h = 300.0 self.ui_discussion_split_h = 300.0
self.shader_uniforms = {'crt': 1.0, 'scanline': 0.5, 'bloom': 0.8} self.shader_uniforms = {'crt': 1.0, 'scanline': 0.5, 'bloom': 0.8}
@@ -578,35 +575,31 @@ class App:
is_md = label in ("message", "text", "content") is_md = label in ("message", "text", "content")
ctx_id = f"heavy_{label}_{id_suffix}" ctx_id = f"heavy_{label}_{id_suffix}"
is_nerv = theme.is_nerv_active() with theme.ai_text_style():
if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80)) if len(content) > COMMS_CLAMP_CHARS:
if is_md:
if len(content) > COMMS_CLAMP_CHARS: imgui.begin_child(f"heavy_text_child_{label}_{id_suffix}", imgui.ImVec2(0, 180), True, imgui.WindowFlags_.always_vertical_scrollbar)
if is_md: markdown_helper.render(content, context_id=ctx_id)
imgui.begin_child(f"heavy_text_child_{label}_{id_suffix}", imgui.ImVec2(0, 180), True, imgui.WindowFlags_.always_vertical_scrollbar) imgui.end_child()
markdown_helper.render(content, context_id=ctx_id)
imgui.end_child()
else:
imgui.input_text_multiline(f"##heavy_text_input_{label}_{id_suffix}", content, imgui.ImVec2(-1, 180), imgui.InputTextFlags_.read_only)
else:
if is_md:
markdown_helper.render(content, context_id=ctx_id)
else:
if self.ui_word_wrap:
imgui.push_text_wrap_pos(imgui.get_content_region_avail().x)
imgui.text(content)
imgui.pop_text_wrap_pos()
else: else:
imgui.text(content) imgui.input_text_multiline(f"##heavy_text_input_{label}_{id_suffix}", content, imgui.ImVec2(-1, 180), imgui.InputTextFlags_.read_only)
else:
if is_nerv: imgui.pop_style_color() if is_md:
markdown_helper.render(content, context_id=ctx_id)
else:
if self.ui_word_wrap:
imgui.push_text_wrap_pos(imgui.get_content_region_avail().x)
imgui.text(content)
imgui.pop_text_wrap_pos()
else:
imgui.text(content)
# ---------------------------------------------------------------- gui # ---------------------------------------------------------------- gui
def _render_thinking_trace(self, segments: list[dict], entry_index: int, is_standalone: bool = False) -> None: def _render_thinking_trace(self, segments: list[dict], entry_index: int, is_standalone: bool = False) -> None:
if not segments: if not segments:
return return
with imscope.style_color(imgui.Col_.child_bg, vec4(40, 35, 25, 180)), \ 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() imgui.indent()
show_content = True show_content = True
@@ -624,9 +617,9 @@ class App:
imgui.text_colored(vec4(180, 150, 80), f"[{marker}]") imgui.text_colored(vec4(180, 150, 80), f"[{marker}]")
if self.ui_word_wrap: if self.ui_word_wrap:
with imscope.text_wrap(imgui.get_content_region_avail().x): with imscope.text_wrap(imgui.get_content_region_avail().x):
imgui.text_colored(vec4(200, 200, 150), content) imgui.text(content)
else: else:
imgui.text_colored(vec4(200, 200, 150), content) imgui.text(content)
imgui.separator() imgui.separator()
imgui.unindent() imgui.unindent()
@@ -927,102 +920,6 @@ class App:
if p not in self.screenshots: self.screenshots.append(p) if p not in self.screenshots: self.screenshots.append(p)
return 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: def _render_mma_modals(self) -> None:
"""Renders all MMA-specific approval and info modals.""" """Renders all MMA-specific approval and info modals."""
is_nerv = theme.is_nerv_active() is_nerv = theme.is_nerv_active()
@@ -1240,8 +1137,7 @@ class App:
if len(content) > 80: preview += "..." if len(content) > 80: preview += "..."
imgui.text_colored(vec4(180, 180, 180), preview) imgui.text_colored(vec4(180, 180, 180), preview)
else: else:
is_nerv = theme.is_nerv_active() with theme.ai_text_style():
with imscope.style_color(imgui.Col_.text, vec4(80, 255, 80)) if is_nerv else nullcontext():
markdown_helper.render(content, context_id=f'prior_disc_{idx}') markdown_helper.render(content, context_id=f'prior_disc_{idx}')
imgui.separator() imgui.separator()
@@ -1422,7 +1318,7 @@ class App:
pattern = re.compile(r"\[Definition: (.*?) from (.*?) \(line (\d+)\)\](\s+```[\s\S]*?```)?") pattern = re.compile(r"\[Definition: (.*?) from (.*?) \(line (\d+)\)\](\s+```[\s\S]*?```)?")
matches, is_nerv = list(pattern.finditer(content)), theme.is_nerv_active() matches, is_nerv = list(pattern.finditer(content)), theme.is_nerv_active()
if not matches: 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}') markdown_helper.render(content, context_id=f'disc_{index}')
else: else:
with imscope.child(f"read_content_{index}", size_y=150, flags=True): 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): for m_idx, match in enumerate(matches):
before = content[last_idx:match.start()] before = content[last_idx:match.start()]
if before: 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}') 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) 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.collapsing_header(header_text):
@@ -1439,12 +1335,12 @@ class App:
res = mcp_client.read_file(path) 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 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 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}') markdown_helper.render(code_block, context_id=f'disc_{index}_c_{m_idx}')
last_idx = match.end() last_idx = match.end()
after = content[last_idx:] after = content[last_idx:]
if after: 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') markdown_helper.render(after, context_id=f'disc_{index}_a')
if self.ui_word_wrap: imgui.pop_text_wrap_pos() if self.ui_word_wrap: imgui.pop_text_wrap_pos()
@@ -1620,6 +1516,347 @@ class App:
if exp: self._render_beads_tab() if exp: self._render_beads_tab()
imgui.end_tab_bar() 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: def _render_text_viewer_window(self) -> None:
"""Renders the standalone text/code/markdown viewer window.""" """Renders the standalone text/code/markdown viewer window."""
if not self.show_text_viewer: return if not self.show_text_viewer: return
@@ -1741,160 +1978,6 @@ class App:
self._render_ast_inspector_modal() self._render_ast_inspector_modal()
return 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: def _render_ast_inspector_modal(self) -> None:
if self._show_ast_inspector: if self._show_ast_inspector:
imgui.open_popup('AST Inspector') imgui.open_popup('AST Inspector')
@@ -2377,6 +2460,7 @@ class App:
if not is_embedded: if not is_embedded:
if imgui.button("Close##tp", imgui.ImVec2(100, 0)): self.show_tool_preset_manager_window = False if imgui.button("Close##tp", imgui.ImVec2(100, 0)): self.show_tool_preset_manager_window = False
imgui.end_table() imgui.end_table()
def _render_tool_preset_manager_window(self, is_embedded: bool = False) -> None: 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 self.show_tool_preset_manager_window and not is_embedded: return
if not is_embedded: if not is_embedded:
@@ -2730,102 +2814,6 @@ class App:
if imgui.button(f"Delete##{name}"): if imgui.button(f"Delete##{name}"):
self.delete_context_preset(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: def _apply_pending_patch(self) -> None:
if not self._pending_patch_text: if not self._pending_patch_text:
self._patch_error_message = "No patch to apply" 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_.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.style_color(imgui.Col_.child_bg, blink_color) if is_blinking else nullcontext():
with imscope.child("response_scroll_area", imgui.ImVec2(0, -40), True): with imscope.child("response_scroll_area", imgui.ImVec2(0, -40), True):
is_nerv = theme.is_nerv_active() with theme.ai_text_style():
with imscope.style_color(imgui.Col_.text, vec4(80, 255, 80)) if is_nerv else nullcontext():
segments, parsed_response = thinking_parser.parse_thinking_trace(self.ai_response) segments, parsed_response = thinking_parser.parse_thinking_trace(self.ai_response)
if segments: if segments:
self._render_thinking_trace([{"content": s.content, "marker": s.marker} for s in segments], 9999) 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]*?```)?") pattern = re.compile(r"\[Definition: (.*?) from (.*?) \(line (\d+)\)\](\s+```[\s\S]*?```)?")
matches, is_nerv = list(pattern.finditer(content)), theme.is_nerv_active() matches, is_nerv = list(pattern.finditer(content)), theme.is_nerv_active()
if not matches: 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}') markdown_helper.render(content, context_id=f'disc_{index}')
if is_nerv: imgui.pop_style_color()
else: else:
with imscope.child(f"read_content_{index}", size_y=150, flags=True): 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) 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): for m_idx, match in enumerate(matches):
before = content[last_idx:match.start()] before = content[last_idx:match.start()]
if before: 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}') 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) 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.collapsing_header(header_text):
if imgui.button(f"[Source]##{index}_{match.start()}"): if imgui.button(f"[Source]##{index}_{match.start()}"):
res = mcp_client.read_file(path) 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 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 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}') markdown_helper.render(code_block, context_id=f'disc_{index}_c_{m_idx}')
if is_nerv: imgui.pop_style_color()
last_idx = match.end() last_idx = match.end()
after = content[last_idx:] after = content[last_idx:]
if after: 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') 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() if self.ui_word_wrap: imgui.pop_text_wrap_pos()
def _load_fonts(self) -> None: def _load_fonts(self) -> None:
+15
View File
@@ -86,6 +86,21 @@ class _ScopePopup:
imgui.end_popup() imgui.end_popup()
return False 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() def tooltip(): return _ScopeTooltip()
class _ScopeTooltip: class _ScopeTooltip:
def __enter__(self): def __enter__(self):
+25
View File
@@ -10,7 +10,11 @@ Scale uses imgui.get_style().font_scale_main.
from imgui_bundle import imgui, hello_imgui from imgui_bundle import imgui, hello_imgui
from typing import Any, Optional from typing import Any, Optional
from contextlib import nullcontext
from src import imgui_scopes as imscope
import src.theme_nerv import src.theme_nerv
from src.theme_nerv import DATA_GREEN
from src.theme_nerv_fx import CRTFilter, AlertPulsing, StatusFlicker
# ------------------------------------------------------------------ palettes # ------------------------------------------------------------------ palettes
@@ -239,6 +243,10 @@ _current_scale: float = 1.0
_transparency: float = 1.0 _transparency: float = 1.0
_child_transparency: float = 1.0 _child_transparency: float = 1.0
_crt_filter = CRTFilter()
_alert_pulsing = AlertPulsing()
_status_flicker = StatusFlicker()
# ------------------------------------------------------------------ public API # ------------------------------------------------------------------ public API
def get_current_palette() -> str: def get_current_palette() -> str:
@@ -396,3 +404,20 @@ def get_tweaked_theme() -> hello_imgui.ImGuiTweakedTheme:
# Sync tweaks # Sync tweaks
tt.tweaks.rounding = 6.0 tt.tweaks.rounding = 6.0
return tt 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)