diff --git a/conductor/tracks/gui_refactor_stabilization_20260512/plan_nerv_cleanup.md b/conductor/tracks/gui_refactor_stabilization_20260512/plan_nerv_cleanup.md new file mode 100644 index 0000000..0095254 --- /dev/null +++ b/conductor/tracks/gui_refactor_stabilization_20260512/plan_nerv_cleanup.md @@ -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. diff --git a/src/gui_2.py b/src/gui_2.py index 407794a..e5b3229 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -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,35 +575,31 @@ 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)) - - 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) - 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() + 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) + markdown_helper.render(content, context_id=ctx_id) + imgui.end_child() else: - imgui.text(content) - - if is_nerv: imgui.pop_style_color() + 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: + imgui.text(content) # ---------------------------------------------------------------- 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)) - markdown_helper.render(content, context_id=f'disc_{index}') - if is_nerv: imgui.pop_style_color() + 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): 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)) - markdown_helper.render(before, context_id=f'disc_{index}_b_{m_idx}') - if is_nerv: imgui.pop_style_color() + 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): 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)) - markdown_helper.render(code_block, context_id=f'disc_{index}_c_{m_idx}') - if is_nerv: imgui.pop_style_color() + 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: - if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80)) - markdown_helper.render(after, context_id=f'disc_{index}_a') - if is_nerv: imgui.pop_style_color() + 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() def _load_fonts(self) -> None: diff --git a/src/imgui_scopes.py b/src/imgui_scopes.py index 506b5fe..f46194d 100644 --- a/src/imgui_scopes.py +++ b/src/imgui_scopes.py @@ -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): diff --git a/src/theme_2.py b/src/theme_2.py index 920417f..e458390 100644 --- a/src/theme_2.py +++ b/src/theme_2.py @@ -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)