feat(gui): Implement frosted glass UI controls and config persistence

This commit is contained in:
2026-03-13 15:04:49 -04:00
parent ad9ffb68f5
commit cecbe2245a
4 changed files with 195 additions and 54 deletions

View File

@@ -21,6 +21,7 @@ from src import theme_2 as theme
from src import theme_nerv_fx as theme_fx
from src import api_hooks
import numpy as np
import OpenGL.GL as gl
from src import log_registry
from src import log_pruner
from src import models
@@ -28,6 +29,7 @@ from src import app_controller
from src import mcp_client
from src import markdown_helper
from src import bg_shader
from src.shader_manager import ShaderManager
import re
import subprocess
if sys.platform == "win32":
@@ -211,7 +213,16 @@ class App:
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}
self.shader_uniforms = {
'crt': 1.0,
'scanline': 0.5,
'bloom': 0.8,
'frosted_blur_radius': theme.get_frosted_blur_radius(),
'frosted_tint_intensity': theme.get_frosted_tint_intensity(),
'frosted_opacity': theme.get_frosted_opacity()
}
self.shader_manager = ShaderManager()
def _handle_approve_tool(self, user_data=None) -> None:
"""UI-level wrapper for approving a pending tool execution ask."""
@@ -341,6 +352,84 @@ class App:
imgui.pop_style_var(2)
imgui.pop_id()
def _render_frosted_background(self, pos: imgui.ImVec2, size: imgui.ImVec2) -> None:
if size.x <= 0 or size.y <= 0: return
if self.shader_manager.fbo_width != int(size.x) or self.shader_manager.fbo_height != int(size.y):
self.shader_manager.setup_capture_fbo(int(size.x), int(size.y))
display_size = imgui.get_io().display_size
gl.glBindTexture(gl.GL_TEXTURE_2D, self.shader_manager.capture_tex)
gl_y = int(display_size.y - pos.y - size.y)
gl.glCopyTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGBA, int(pos.x), gl_y, int(size.x), int(size.y), 0)
gl.glBindTexture(gl.GL_TEXTURE_2D, 0)
self.shader_manager.capture_begin(int(size.x), int(size.y))
self.shader_manager.render_blur(
self.shader_manager.capture_tex,
int(size.x),
int(size.y),
self.shader_uniforms['frosted_blur_radius'],
self.shader_uniforms['frosted_tint_intensity'],
self.shader_uniforms['frosted_opacity']
)
self.shader_manager.capture_end()
imgui.get_background_draw_list().add_image(
self.shader_manager.blur_tex,
pos,
imgui.ImVec2(pos.x + size.x, pos.y + size.y)
)
def _render_operations_hub(self) -> None:
exp, opened = imgui.begin("Operations Hub", self.show_windows["Operations Hub"])
self.show_windows["Operations Hub"] = bool(opened)
if exp:
self._render_frosted_background(imgui.get_window_pos(), imgui.get_window_size())
imgui.text("Focus Agent:")
imgui.same_line()
focus_label = self.ui_focus_agent or "All"
if imgui.begin_combo("##focus_agent", focus_label, imgui.ComboFlags_.width_fit_preview):
if imgui.selectable("All", self.ui_focus_agent is None)[0]:
self.ui_focus_agent = None
for tier in ["Tier 2", "Tier 3", "Tier 4"]:
if imgui.selectable(tier, self.ui_focus_agent == tier)[0]:
self.ui_focus_agent = tier
imgui.end_combo()
imgui.same_line()
if self.ui_focus_agent:
if imgui.button("x##clear_focus"):
self.ui_focus_agent = None
imgui.push_style_var(imgui.StyleVar_.item_spacing, imgui.ImVec2(10, 4))
ch1, self.ui_separate_tool_calls_panel = imgui.checkbox("Pop Out Tool Calls", self.ui_separate_tool_calls_panel)
if ch1: self.show_windows["Tool Calls"] = self.ui_separate_tool_calls_panel
imgui.same_line()
ch2, self.ui_separate_usage_analytics = imgui.checkbox("Pop Out Usage Analytics", self.ui_separate_usage_analytics)
if ch2: self.show_windows["Usage Analytics"] = self.ui_separate_usage_analytics
imgui.same_line()
ch3, self.ui_separate_external_tools = imgui.checkbox('Pop Out External Tools', self.ui_separate_external_tools)
if ch3: self.show_windows['External Tools'] = self.ui_separate_external_tools
imgui.pop_style_var()
show_tc_tab = not self.ui_separate_tool_calls_panel
show_usage_tab = not self.ui_separate_usage_analytics
if imgui.begin_tab_bar("ops_tabs"):
if imgui.begin_tab_item("Comms History")[0]:
self._render_comms_history_panel()
imgui.end_tab_item()
if show_tc_tab:
if imgui.begin_tab_item("Tool Calls")[0]:
self._render_tool_calls_panel()
imgui.end_tab_item()
if show_usage_tab:
if imgui.begin_tab_item("Usage Analytics")[0]:
self._render_usage_analytics_panel()
imgui.end_tab_item()
if not self.ui_separate_external_tools:
if imgui.begin_tab_item("External Tools")[0]:
self._render_external_tools_panel()
imgui.end_tab_item()
imgui.end_tab_bar()
imgui.end()
def _show_menus(self) -> None:
if imgui.begin_menu("manual slop"):
if imgui.menu_item("Quit", "Ctrl+Q", False)[0]:
@@ -440,6 +529,17 @@ class App:
changed_crt, self.shader_uniforms['crt'] = imgui.slider_float('CRT Curvature', self.shader_uniforms['crt'], 0.0, 2.0)
changed_scan, self.shader_uniforms['scanline'] = imgui.slider_float('Scanline Intensity', self.shader_uniforms['scanline'], 0.0, 1.0)
changed_bloom, self.shader_uniforms['bloom'] = imgui.slider_float('Bloom Threshold', self.shader_uniforms['bloom'], 0.0, 1.0)
imgui.separator()
imgui.text("Frosted Glass")
changed_fbr, self.shader_uniforms['frosted_blur_radius'] = imgui.slider_float('Blur Radius', self.shader_uniforms['frosted_blur_radius'], 0.0, 32.0)
if changed_fbr: theme.set_frosted_blur_radius(self.shader_uniforms['frosted_blur_radius'])
changed_fti, self.shader_uniforms['frosted_tint_intensity'] = imgui.slider_float('Tint Intensity', self.shader_uniforms['frosted_tint_intensity'], 0.0, 1.0)
if changed_fti: theme.set_frosted_tint_intensity(self.shader_uniforms['frosted_tint_intensity'])
changed_fo, self.shader_uniforms['frosted_opacity'] = imgui.slider_float('Frosted Opacity', self.shader_uniforms['frosted_opacity'], 0.0, 1.0)
if changed_fo: theme.set_frosted_opacity(self.shader_uniforms['frosted_opacity'])
imgui.end()
def _gui_func(self) -> None:
@@ -660,56 +760,7 @@ class App:
imgui.end()
if self.show_windows.get("Operations Hub", False):
exp, opened = imgui.begin("Operations Hub", self.show_windows["Operations Hub"])
self.show_windows["Operations Hub"] = bool(opened)
if exp:
imgui.text("Focus Agent:")
imgui.same_line()
focus_label = self.ui_focus_agent or "All"
if imgui.begin_combo("##focus_agent", focus_label, imgui.ComboFlags_.width_fit_preview):
if imgui.selectable("All", self.ui_focus_agent is None)[0]:
self.ui_focus_agent = None
for tier in ["Tier 2", "Tier 3", "Tier 4"]:
if imgui.selectable(tier, self.ui_focus_agent == tier)[0]:
self.ui_focus_agent = tier
imgui.end_combo()
imgui.same_line()
if self.ui_focus_agent:
if imgui.button("x##clear_focus"):
self.ui_focus_agent = None
if exp:
imgui.push_style_var(imgui.StyleVar_.item_spacing, imgui.ImVec2(10, 4))
ch1, self.ui_separate_tool_calls_panel = imgui.checkbox("Pop Out Tool Calls", self.ui_separate_tool_calls_panel)
if ch1: self.show_windows["Tool Calls"] = self.ui_separate_tool_calls_panel
imgui.same_line()
ch2, self.ui_separate_usage_analytics = imgui.checkbox("Pop Out Usage Analytics", self.ui_separate_usage_analytics)
if ch2: self.show_windows["Usage Analytics"] = self.ui_separate_usage_analytics
imgui.same_line()
ch3, self.ui_separate_external_tools = imgui.checkbox('Pop Out External Tools', self.ui_separate_external_tools)
if ch3: self.show_windows['External Tools'] = self.ui_separate_external_tools
imgui.pop_style_var()
show_tc_tab = not self.ui_separate_tool_calls_panel
show_usage_tab = not self.ui_separate_usage_analytics
if imgui.begin_tab_bar("ops_tabs"):
if imgui.begin_tab_item("Comms History")[0]:
self._render_comms_history_panel()
imgui.end_tab_item()
if show_tc_tab:
if imgui.begin_tab_item("Tool Calls")[0]:
self._render_tool_calls_panel()
imgui.end_tab_item()
if show_usage_tab:
if imgui.begin_tab_item("Usage Analytics")[0]:
self._render_usage_analytics_panel()
imgui.end_tab_item()
if not self.ui_separate_external_tools:
if imgui.begin_tab_item("External Tools")[0]:
self._render_external_tools_panel()
imgui.end_tab_item()
imgui.end_tab_bar()
imgui.end()
self._render_operations_hub()
if self.ui_separate_message_panel and self.show_windows.get("Message", False):
exp, opened = imgui.begin("Message", self.show_windows["Message"])
@@ -1904,7 +1955,6 @@ class App:
imgui.text(f"History: {key}")
hist_data = self.perf_monitor.get_history(key)
if hist_data:
import numpy as np
imgui.plot_lines(f"##plot_{key}", np.array(hist_data, dtype=np.float32), graph_size=imgui.ImVec2(-1, 60))
else:
imgui.text_disabled(f"(no history data for {key})")
@@ -2052,6 +2102,7 @@ def hello():
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_screenshots_panel")
def _render_discussion_panel(self) -> None:
self._render_frosted_background(imgui.get_window_pos(), imgui.get_window_size())
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_discussion_panel")
# THINKING indicator
is_thinking = self.ai_status in ['sending...', 'streaming...', 'running powershell...']
@@ -3993,6 +4044,8 @@ def hello():
def _post_init(self) -> None:
theme.apply_current()
self.shader_manager.setup_background_shader()
self.shader_manager.setup_frosted_glass_shader()
def run(self) -> None:
"""Initializes the ImGui runner and starts the main application loop."""

View File

@@ -8,6 +8,7 @@ class ShaderManager:
self.blur_program = None
self.capture_fbo = None
self.capture_tex = None
self.blur_tex = None
self.fbo_width = 0
self.fbo_height = 0
@@ -16,14 +17,21 @@ class ShaderManager:
gl.glDeleteFramebuffers(1, [self.capture_fbo])
if self.capture_tex is not None:
gl.glDeleteTextures(1, [self.capture_tex])
if self.blur_tex is not None:
gl.glDeleteTextures(1, [self.blur_tex])
self.capture_fbo = gl.glGenFramebuffers(1)
self.capture_tex = gl.glGenTextures(1)
gl.glBindTexture(gl.GL_TEXTURE_2D, self.capture_tex)
gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGBA, width, height, 0, gl.GL_RGBA, gl.GL_UNSIGNED_BYTE, None)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR)
self.blur_tex = gl.glGenTextures(1)
gl.glBindTexture(gl.GL_TEXTURE_2D, self.blur_tex)
gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGBA, width, height, 0, gl.GL_RGBA, gl.GL_UNSIGNED_BYTE, None)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR)
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.capture_fbo)
gl.glFramebufferTexture2D(gl.GL_FRAMEBUFFER, gl.GL_COLOR_ATTACHMENT0, gl.GL_TEXTURE_2D, self.capture_tex, 0)
gl.glFramebufferTexture2D(gl.GL_FRAMEBUFFER, gl.GL_COLOR_ATTACHMENT0, gl.GL_TEXTURE_2D, self.blur_tex, 0)
if gl.glCheckFramebufferStatus(gl.GL_FRAMEBUFFER) != gl.GL_FRAMEBUFFER_COMPLETE:
raise RuntimeError("Framebuffer not complete")
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0)
@@ -232,3 +240,25 @@ void main() {
}
"""
self.blur_program = self.compile_shader(vertex_src, fragment_src)
def render_blur(self, texture_id, width, height, radius, tint, opacity):
if not self.blur_program:
return
gl.glUseProgram(self.blur_program)
gl.glActiveTexture(gl.GL_TEXTURE0)
gl.glBindTexture(gl.GL_TEXTURE_2D, texture_id)
u_tex_loc = gl.glGetUniformLocation(self.blur_program, "u_texture")
if u_tex_loc != -1:
gl.glUniform1i(u_tex_loc, 0)
u_radius_loc = gl.glGetUniformLocation(self.blur_program, "u_blur_radius")
if u_radius_loc != -1:
gl.glUniform1f(u_radius_loc, float(radius))
u_tint_loc = gl.glGetUniformLocation(self.blur_program, "u_tint_intensity")
if u_tint_loc != -1:
gl.glUniform1f(u_tint_loc, float(tint))
u_opacity_loc = gl.glGetUniformLocation(self.blur_program, "u_opacity")
if u_opacity_loc != -1:
gl.glUniform1f(u_opacity_loc, float(opacity))
gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, 4)
gl.glBindTexture(gl.GL_TEXTURE_2D, 0)
gl.glUseProgram(0)

View File

@@ -235,6 +235,9 @@ _current_font_size: float = 16.0
_current_scale: float = 1.0
_transparency: float = 1.0
_child_transparency: float = 1.0
_frosted_blur_radius: float = 8.0
_frosted_tint_intensity: float = 0.1
_frosted_opacity: float = 1.0
# ------------------------------------------------------------------ public API
@@ -269,6 +272,27 @@ def set_child_transparency(val: float) -> None:
_child_transparency = val
apply(_current_palette)
def get_frosted_blur_radius() -> float:
return _frosted_blur_radius
def set_frosted_blur_radius(val: float) -> None:
global _frosted_blur_radius
_frosted_blur_radius = val
def get_frosted_tint_intensity() -> float:
return _frosted_tint_intensity
def set_frosted_tint_intensity(val: float) -> None:
global _frosted_tint_intensity
_frosted_tint_intensity = val
def get_frosted_opacity() -> float:
return _frosted_opacity
def set_frosted_opacity(val: float) -> None:
global _frosted_opacity
_frosted_opacity = val
def apply(palette_name: str) -> None:
"""
Apply a named palette by setting all ImGui style colors and applying global professional styling.
@@ -350,13 +374,16 @@ def save_to_config(config: dict) -> None:
config["theme"]["scale"] = _current_scale
config["theme"]["transparency"] = _transparency
config["theme"]["child_transparency"] = _child_transparency
config["theme"]["frosted_blur_radius"] = _frosted_blur_radius
config["theme"]["frosted_tint_intensity"] = _frosted_tint_intensity
config["theme"]["frosted_opacity"] = _frosted_opacity
sys.stderr.write(f"[DEBUG theme_2] save_to_config: palette={_current_palette}, transparency={_transparency}\n")
sys.stderr.flush()
def load_from_config(config: dict) -> None:
"""Read [theme] from config. Font is handled separately at startup."""
import sys
global _current_font_path, _current_font_size, _current_scale, _current_palette, _transparency, _child_transparency
global _current_font_path, _current_font_size, _current_scale, _current_palette, _transparency, _child_transparency, _frosted_blur_radius, _frosted_tint_intensity, _frosted_opacity
t = config.get("theme", {})
sys.stderr.write(f"[DEBUG theme_2] load_from_config raw: {t}\n")
sys.stderr.flush()
@@ -369,6 +396,9 @@ def load_from_config(config: dict) -> None:
_current_scale = float(t.get("scale", 1.0))
_transparency = float(t.get("transparency", 1.0))
_child_transparency = float(t.get("child_transparency", 1.0))
_frosted_blur_radius = float(t.get("frosted_blur_radius", 8.0))
_frosted_tint_intensity = float(t.get("frosted_tint_intensity", 0.1))
_frosted_opacity = float(t.get("frosted_opacity", 1.0))
sys.stderr.write(f"[DEBUG theme_2] load_from_config effective: palette={_current_palette}, transparency={_transparency}\n")
sys.stderr.flush()

View File

@@ -0,0 +1,28 @@
import pytest
from unittest.mock import patch, MagicMock
def test_gui_frosted_background_call():
# Mock ShaderManager and OpenGL functions
with patch("src.gui_2.ShaderManager") as mock_sm_class, \
patch("src.gui_2.gl") as mock_gl, \
patch("src.gui_2.imgui") as mock_imgui:
mock_sm = mock_sm_class.return_value
mock_sm.fbo_width = 0
mock_sm.fbo_height = 0
mock_sm.capture_tex = 1
mock_sm.blur_tex = 2
mock_imgui.get_io().display_size = MagicMock(x=1920, y=1080)
from src.gui_2 import App
app = App()
# Simulate frame
app._render_frosted_background(pos=MagicMock(x=10, y=10), size=MagicMock(x=100, y=100))
assert mock_sm.setup_capture_fbo.called
assert mock_gl.glCopyTexImage2D.called
assert mock_sm.render_blur.called
assert mock_sm.capture_begin.called
assert mock_sm.capture_end.called