15 Commits

33 changed files with 948 additions and 1125 deletions

View File

@@ -1,26 +0,0 @@
# Implementation Plan: Frosted Glass Background Effect
## Phase 1: Shader Development & Integration
- [ ] Task: Audit `src/shader_manager.py` to identify existing background/post-process integration points.
- [ ] Task: Write Tests: Verify `ShaderManager` can compile and bind a multi-pass blur shader.
- [ ] Task: Implement: Add `FrostedGlassShader` (GLSL) to `src/shader_manager.py`.
- [ ] Task: Implement: Integrate the blur shader into the `ShaderManager` lifecycle.
- [ ] Task: Conductor - User Manual Verification 'Phase 1: Shader Development & Integration' (Protocol in workflow.md)
## Phase 2: Framebuffer Capture Pipeline
- [ ] Task: Write Tests: Verify the FBO capture mechanism correctly samples the back buffer and stores it in a texture.
- [ ] Task: Implement: Update `src/shader_manager.py` or `src/gui_2.py` to handle "pre-rendering" of the background into a texture for blurring.
- [ ] Task: Implement: Ensure the blurred texture is updated every frame or on window move events.
- [ ] Task: Conductor - User Manual Verification 'Phase 2: Framebuffer Capture Pipeline' (Protocol in workflow.md)
## Phase 3: GUI Integration & Rendering
- [ ] Task: Write Tests: Verify that a mocked ImGui window successfully calls the frosted glass rendering logic.
- [ ] Task: Implement: Create a `_render_frosted_background(self, pos, size)` helper in `src/gui_2.py`.
- [ ] Task: Implement: Update panel rendering loops (e.g. `_gui_func`) to inject the frosted background before calling `imgui.begin()` for major panels.
- [ ] Task: Conductor - User Manual Verification 'Phase 3: GUI Integration & Rendering' (Protocol in workflow.md)
## Phase 4: UI Controls & Configuration
- [ ] Task: Write Tests: Verify that modifying blur uniforms via the Live Editor updates the shader state.
- [ ] Task: Implement: Add "Frosted Glass" sliders (Blur, Tint, Opacity) to the **Shader Editor** in `src/gui_2.py`.
- [ ] Task: Implement: Update `src/theme.py` to parse and store frosted glass settings from `config.toml`.
- [ ] Task: Conductor - User Manual Verification 'Phase 4: UI Controls & Configuration' (Protocol in workflow.md)

View File

@@ -17,7 +17,7 @@ For deep implementation details when planning or implementing tracks, consult `d
## Primary Use Cases
- **Full Control over Vendor APIs:** Exposing detailed API metrics and configuring deep agent capabilities directly within the GUI.
- **Context & Memory Management:** Better visualization and management of token usage and context memory. Includes granular per-file flags (**Auto-Aggregate**, **Force Full**), a dedicated **'Context' role** for manual injections, and **Context Presets** for saving and loading named file/screenshot selections. Allows assigning specific context presets to MMA agent personas for granular cognitive load isolation.
- **Context & Memory Management:** Better visualization and management of token usage and context memory. Includes granular per-file flags (**Auto-Aggregate**, **Force Full**) and a dedicated **'Context' role** for manual injections, allowing developers to optimize prompt limits with expert precision.
- **Manual "Vibe Coding" Assistant:** Serving as an auxiliary, multi-provider assistant that natively interacts with the codebase via sandboxed PowerShell scripts and MCP-like file tools, emphasizing manual developer oversight and explicit confirmation.
## Key Features
@@ -33,7 +33,6 @@ For deep implementation details when planning or implementing tracks, consult `d
- **Track Browser:** Real-time visualization of all implementation tracks with status indicators and progress bars. Includes a dedicated **Active Track Summary** featuring a color-coded progress bar, precise ticket status breakdown (Completed, In Progress, Blocked, Todo), and dynamic **ETA estimation** based on historical completion times.
- **Visual Task DAG:** An interactive, node-based visualizer for the active track's task dependencies using `imgui-node-editor`. Features color-coded state tracking (Ready, Running, Blocked, Done), drag-and-drop dependency creation, and right-click deletion.
- **Strategy Visualization:** Dedicated real-time output streams for Tier 1 (Strategic Planning) and Tier 2/3 (Execution) agents, allowing the user to follow the agent's reasoning chains alongside the task DAG.
- **Agent-Focused Filtering:** Allows the user to focus the entire GUI (Session Hub, Discussion Hub, Comms) on a specific agent's activities and scoped context.
- **Track-Scoped State Management:** Segregates discussion history and task progress into per-track state files. Supports **Project-Specific Conductor Directories**, defaulting to `./conductor` relative to each project's TOML file. Projects can define their own conductor path override in `manual_slop.toml` (`[conductor].dir`) via the Projects tab for isolated track management. This prevents global context pollution and ensures the Tech Lead session is isolated to the specific track's objective.
**Native DAG Execution Engine:** Employs a Python-based Directed Acyclic Graph (DAG) engine to manage complex task dependencies. Supports automated topological sorting, robust cycle detection, and **transitive blocking propagation** (cascading `blocked` status to downstream dependents to prevent execution stalls).
@@ -55,8 +54,6 @@ For deep implementation details when planning or implementing tracks, consult `d
- **High-Fidelity Selectable UI:** Most read-only labels and logs across the interface (including discussion history, comms payloads, tool outputs, and telemetry metrics) are now implemented as selectable text fields. This enables standard OS-level text selection and copying (Ctrl+C) while maintaining a high-density, non-editable aesthetic.
- **High-Fidelity UI Rendering:** Employs advanced 3x font oversampling and sub-pixel positioning to ensure crisp, high-clarity text rendering across all resolutions, enhancing readability for dense logs and complex code fragments.
- **Enhanced MMA Observability:** Worker streams and ticket previews now support direct text selection, allowing for easy extraction of specific logs or reasoning fragments during parallel execution.
- **Transparent Context Visibility:** A dedicated **Session Hub** exposes the exact aggregated markdown and resolved system prompt sent to the AI.
- **Injection Timeline:** Discussion history visually indicates the precise moments when files or screenshots were injected into the session context.
- **Detailed History Management:** Rich discussion history with branching, timestamping, and specific git commit linkage per conversation.
- **Advanced Log Management:** Optimizes log storage by offloading large data (AI-generated scripts and tool outputs) to unique files within the session directory, using compact `[REF:filename]` pointers in JSON-L logs to minimize token overhead during analysis. Features a dedicated **Log Management panel** for monitoring, whitelisting, and pruning session logs.
- **Full Session Restoration:** Allows users to load and reconstruct entire historical sessions from their log directories. Includes a dedicated, tinted **'Historical Replay' mode** that populates discussion history and provides a read-only view of prior agent activities.
@@ -69,7 +66,7 @@ For deep implementation details when planning or implementing tracks, consult `d
- **Clean Project Root:** Enforces a "Cruft-Free Root" policy by organizing core implementation into a `src/` directory and redirecting all temporary test data, configurations, and AI-generated artifacts to `tests/artifacts/`.
- **Performance Diagnostics:** Comprehensive, conditional per-component profiling across the entire application. Features a dedicated **Diagnostics Panel** providing real-time telemetry for FPS, Frame Time, CPU usage, and **Detailed Component Timings** for all GUI panels and background threads, including automated threshold-based latency alerts.
- **Automated UX Verification:** A robust IPC mechanism via API hooks and a modular simulation suite allows for human-like simulation walkthroughs and automated regression testing of the full GUI lifecycle across multiple specialized scenarios.
- **Professional UI Theme & Typography:** Implements a high-fidelity visual system featuring **Inter** and **Maple Mono** fonts for optimal readability. Employs a cohesive "Subtle Rounding" aesthetic across all standard widgets, supported by custom **soft shadow shaders** for modals and popups to provide depth and professional polish. Includes a selectable **NERV UI theme** featuring a "Black Void" palette, zero-rounding geometry, and CRT-style visual effects (scanlines, status flickering).
- **Professional UI Theme & Typography:** Implements a high-fidelity visual system featuring **Inter** and **Maple Mono** fonts for optimal readability. Employs a cohesive \"Subtle Rounding\" aesthetic across all standard widgets, supported by custom **soft shadow shaders** for modals and popups, and a high-fidelity **frosted glass (acrylic) background effect** for panels to provide depth and professional polish. Includes a selectable **NERV UI theme** featuring a \"Black Void\" palette, zero-rounding geometry, and CRT-style visual effects (scanlines, status flickering).
- **Rich Text & Syntax Highlighting:** Provides advanced rendering for messages, logs, and tool outputs using a hybrid Markdown system. Supports GitHub-Flavored Markdown (GFM) via `imgui_markdown` and integrates `ImGuiColorTextEdit` for high-performance syntax highlighting of code blocks (Python, JSON, C++, etc.). Includes automated language detection and clickable URL support.
- **Multi-Viewport & Layout Management:** Full support for ImGui Multi-Viewport, allowing users to detach panels into standalone OS windows for complex multi-monitor workflows. Includes a comprehensive **Layout Presets system**, enabling developers to save, name, and instantly restore custom window arrangements, including their Multi-Viewport state.
- **Headless Backend Service & Hook API:** Optional headless mode allowing the core AI and tool execution logic to run as a decoupled service. Features a comprehensive Hook API and WebSocket event streaming for remote orchestration, deep state inspection, and manual worker lifecycle management.

View File

@@ -70,6 +70,6 @@
- **Synchronous IPC Approval Flow:** A specialized bridge mechanism that allows headless AI providers (like Gemini CLI) to synchronously request and receive human approval for tool calls via the GUI's REST API hooks.
- **High-Fidelity Selectable Labels:** Implements a pattern for making read-only UI text selectable by wrapping `imgui.input_text` with `imgui.InputTextFlags_.read_only`. Includes a specialized `_render_selectable_label` helper that resets frame backgrounds, borders, and padding to mimic standard labels while enabling OS-level clipboard support (Ctrl+C).
- **Hybrid Markdown Rendering:** Employs a custom `MarkdownRenderer` that orchestrates `imgui_markdown` for standard text and headers while intercepting code blocks to render them via cached `ImGuiColorTextEdit` instances. This ensures high-performance rich text rendering with robust syntax highlighting and stateful text selection.
- **Hybrid Shader Pipeline:** Utilizes an optimized `ImDrawList`-based batching technique to simulate UI effects such as soft shadows and acrylic glass overlays without the overhead of heavy GPU-resident shaders. Supplemented by a true GPU shader pipeline using `PyOpenGL` and Framebuffer Objects (FBOs) for complex post-processing (CRT scanlines, bloom) and dynamic backgrounds.
- **Hybrid Shader Pipeline:** Utilizes an optimized `ImDrawList`-based batching technique to simulate UI effects such as soft shadows without the overhead of heavy GPU-resident shaders. Supplemented by a true GPU shader pipeline using `PyOpenGL` and Framebuffer Objects (FBOs) for complex post-processing (CRT scanlines, bloom), dynamic backgrounds, and high-fidelity **frosted glass (acrylic) blurring** of the GUI panels via multi-pass Gaussian/Kawase filtering.
- **Interface-Driven Development (IDD):** Enforces a "Stub-and-Resolve" pattern where cross-module dependencies are resolved by generating signatures/contracts before implementation.

View File

@@ -35,7 +35,7 @@ This file tracks all major tracks for the project. Each track has its own detail
7. [ ] **Track: Optimization pass for Data-Oriented Python heuristics**
*Link: [./tracks/data_oriented_optimization_20260312/](./tracks/data_oriented_optimization_20260312/)*
8. [x] **Track: Rich Thinking Trace Handling** - *Parse and display AI thinking/reasoning traces*
8. [ ] **Track: Rich Thinking Trace Handling**
*Link: [./tracks/thinking_trace_handling_20260313/](./tracks/thinking_trace_handling_20260313/)*
---
@@ -60,14 +60,14 @@ This file tracks all major tracks for the project. Each track has its own detail
5. [x] **Track: NERV UI Theme Integration** (Archived 2026-03-09)
6. [X] **Track: Custom Shader and Window Frame Support**
6. [x] **Track: Custom Shader and Window Frame Support**
*Link: [./tracks/custom_shaders_20260309/](./tracks/custom_shaders_20260309/)*
7. [x] **Track: UI/UX Improvements - Presets and AI Settings**
*Link: [./tracks/presets_ai_settings_ux_20260311/](./tracks/presets_ai_settings_ux_20260311/)*
*Goal: Improve the layout, scaling, and control ergonomics of the Preset windows (Personas, Prompts, Tools) and AI Settings panel. Includes dual-control sliders and categorized tool management.*
8. [x] **Track: Session Context Snapshots & Visibility**
8. [ ] **Track: Session Context Snapshots & Visibility**
*Link: [./tracks/session_context_snapshots_20260311/](./tracks/session_context_snapshots_20260311/)*
*Goal: Session-scoped context management, saving Context Presets, MMA assignment, and agent-focused session filtering in the UI.*
@@ -79,9 +79,12 @@ This file tracks all major tracks for the project. Each track has its own detail
*Link: [./tracks/undo_redo_history_20260311/](./tracks/undo_redo_history_20260311/)*
*Goal: Robust, non-provider based undo/redo for text inputs, UI controls, discussion mutations, and context management. Includes hotkey support and a history list view.*
11. [x] **Track: Advanced Text Viewer with Syntax Highlighting**
11. [ ] **Track: Advanced Text Viewer with Syntax Highlighting**
*Link: [./tracks/text_viewer_rich_rendering_20260313/](./tracks/text_viewer_rich_rendering_20260313/)*
12. [x] **Track: Frosted Glass Background Effect**
*Link: [./tracks/frosted_glass_20260313/](./tracks/frosted_glass_20260313/)*
---
### Additional Language Support
@@ -161,10 +164,6 @@ This file tracks all major tracks for the project. Each track has its own detail
### Completed / Archived
-. [ ] ~~**Track: Frosted Glass Background Effect**~~ ***NOT WORTH THE PAIN***
*Link: [./tracks/frosted_glass_20260313/](./tracks/frosted_glass_20260313/)*
- [x] **Track: External MCP Server Support** (Archived 2026-03-12)
- [x] **Track: Project-Specific Conductor Directory** (Archived 2026-03-12)
- [x] **Track: GUI Path Configuration in Context Hub** (Archived 2026-03-12)

View File

@@ -0,0 +1,26 @@
# Implementation Plan: Frosted Glass Background Effect
## Phase 1: Shader Development & Integration [checkpoint: 55f3bd8]
- [x] Task: Audit `src/shader_manager.py` to identify existing background/post-process integration points. [1328bc1]
- [x] Task: Write Tests: Verify `ShaderManager` can compile and bind a multi-pass blur shader. [1328bc1]
- [x] Task: Implement: Add `FrostedGlassShader` (GLSL) to `src/shader_manager.py`. [1328bc1]
- [x] Task: Implement: Integrate the blur shader into the `ShaderManager` lifecycle. [1328bc1]
- [x] Task: Conductor - User Manual Verification 'Phase 1: Shader Development & Integration' (Protocol in workflow.md) [55f3bd8]
## Phase 2: Framebuffer Capture Pipeline [checkpoint: e9b7875]
- [x] Task: Write Tests: Verify the FBO capture mechanism correctly samples the back buffer and stores it in a texture. [f297e7a]
- [x] Task: Implement: Update `src/shader_manager.py` or `src/gui_2.py` to handle "pre-rendering" of the background into a texture for blurring. [f297e7a]
- [x] Task: Implement: Ensure the blurred texture is updated every frame or on window move events. [f297e7a]
- [x] Task: Conductor - User Manual Verification 'Phase 2: Framebuffer Capture Pipeline' (Protocol in workflow.md) [e9b7875]
## Phase 3: GUI Integration & Rendering [checkpoint: cecbe22]
- [x] Task: Write Tests: Verify that a mocked ImGui window successfully calls the frosted glass rendering logic. [cecbe22]
- [x] Task: Implement: Create a `_render_frosted_background(self, pos, size)` helper in `src/gui_2.py`. [cecbe22]
- [x] Task: Implement: Update panel rendering loops (e.g. `_gui_func`) to inject the frosted background before calling `imgui.begin()` for major panels. [cecbe22]
- [x] Task: Conductor - User Manual Verification 'Phase 3: GUI Integration & Rendering' (Protocol in workflow.md) [cecbe22]
## Phase 4: UI Controls & Configuration [checkpoint: cecbe22]
- [x] Task: Write Tests: Verify that modifying blur uniforms via the Live Editor updates the shader state. [cecbe22]
- [x] Task: Implement: Add "Frosted Glass" sliders (Blur, Tint, Opacity) to the **Shader Editor** in `src/gui_2.py`. [cecbe22]
- [x] Task: Implement: Update `src/theme.py` to parse and store frosted glass settings from `config.toml`. [cecbe22]
- [x] Task: Conductor - User Manual Verification 'Phase 4: UI Controls & Configuration' (Protocol in workflow.md) [cecbe22]

View File

@@ -1,24 +1,24 @@
# Implementation Plan: Session Context Snapshots & Visibility
## Phase 1: Backend Support for Context Presets
- [x] Task: Write failing tests for saving, loading, and listing Context Presets in the project configuration. 93a590c
- [x] Task: Implement Context Preset storage logic (e.g., updating TOML schemas in `project_manager.py`) to manage file/screenshot lists. 93a590c
- [x] Task: Conductor - User Manual Verification 'Phase 1: Backend Support for Context Presets' (Protocol in workflow.md) 93a590c
- [ ] Task: Write failing tests for saving, loading, and listing Context Presets in the project configuration.
- [ ] Task: Implement Context Preset storage logic (e.g., updating TOML schemas in `project_manager.py`) to manage file/screenshot lists.
- [ ] Task: Conductor - User Manual Verification 'Phase 1: Backend Support for Context Presets' (Protocol in workflow.md)
## Phase 2: GUI Integration & Persona Assignment
- [x] Task: Write tests for the Context Hub UI components handling preset saving and loading. 573f5ee
- [x] Task: Implement the UI controls in the Context Hub to save current selections as a preset and load existing presets. 573f5ee
- [x] Task: Update the Persona configuration UI (`personas.py` / `gui_2.py`) to allow assigning a named Context Preset to an agent persona. 791e1b7
- [x] Task: Conductor - User Manual Verification 'Phase 2: GUI Integration & Persona Assignment' (Protocol in workflow.md) 791e1b7
- [ ] Task: Write tests for the Context Hub UI components handling preset saving and loading.
- [ ] Task: Implement the UI controls in the Context Hub to save current selections as a preset and load existing presets.
- [ ] Task: Update the Persona configuration UI (`personas.py` / `gui_2.py`) to allow assigning a named Context Preset to an agent persona.
- [ ] Task: Conductor - User Manual Verification 'Phase 2: GUI Integration & Persona Assignment' (Protocol in workflow.md)
## Phase 3: Transparent Context Visibility
- [x] Task: Write tests to ensure the initial aggregate markdown, resolved system prompt, and file injection timestamps are accurately recorded in the session state. 84b6266
- [x] Task: Implement UI elements in the Session Hub to expose the aggregated markdown and the active system prompt. 84b6266
- [x] Task: Enhance the discussion timeline rendering in `gui_2.py` to visually indicate exactly when files and screenshots were injected into the context. 84b6266
- [x] Task: Conductor - User Manual Verification 'Phase 3: Transparent Context Visibility' (Protocol in workflow.md) 84b6266
- [ ] Task: Write tests to ensure the initial aggregate markdown, resolved system prompt, and file injection timestamps are accurately recorded in the session state.
- [ ] Task: Implement UI elements in the Session Hub to expose the aggregated markdown and the active system prompt.
- [ ] Task: Enhance the discussion timeline rendering in `gui_2.py` to visually indicate exactly when files and screenshots were injected into the context.
- [ ] Task: Conductor - User Manual Verification 'Phase 3: Transparent Context Visibility' (Protocol in workflow.md)
## Phase 4: Agent-Focused Session Filtering
- [x] Task: Write tests for the GUI state filtering logic when focusing on a specific agent's session. 038c909
- [x] Task: Relocate the 'Focus Agent' feature from the Operations Hub to the MMA Dashboard. 038c909
- [x] Task: Implement the action to filter the Session and Discussion hubs based on the selected agent's context. 038c909
- [x] Task: Conductor - User Manual Verification 'Phase 4: Agent-Focused Session Filtering' (Protocol in workflow.md) 038c909
- [ ] Task: Write tests for the GUI state filtering logic when focusing on a specific agent's session.
- [ ] Task: Relocate the 'Focus Agent' feature from the Operations Hub to the MMA Dashboard.
- [ ] Task: Implement the action to filter the Session and Discussion hubs based on the selected agent's context.
- [ ] Task: Conductor - User Manual Verification 'Phase 4: Agent-Focused Session Filtering' (Protocol in workflow.md)

View File

@@ -1,29 +1,29 @@
# Implementation Plan: Advanced Text Viewer with Syntax Highlighting
## Phase 1: State & Interface Update
- [x] Task: Audit `src/gui_2.py` to ensure all `text_viewer_*` state variables are explicitly initialized in `App.__init__`. e28af48
- [x] Task: Implement: Update `App.__init__` to initialize `self.show_text_viewer`, `self.text_viewer_title`, `self.text_viewer_content`, and new `self.text_viewer_type` (defaulting to "text"). e28af48
- [x] Task: Implement: Update `self.text_viewer_wrap` (defaulting to True) to allow independent word wrap. e28af48
- [x] Task: Implement: Update `_render_text_viewer(self, label: str, content: str, text_type: str = "text")` signature and caller usage. e28af48
- [x] Task: Conductor - User Manual Verification 'Phase 1: State & Interface Update' (Protocol in workflow.md) e28af48
- [ ] Task: Audit `src/gui_2.py` to ensure all `text_viewer_*` state variables are explicitly initialized in `App.__init__`.
- [ ] Task: Implement: Update `App.__init__` to initialize `self.show_text_viewer`, `self.text_viewer_title`, `self.text_viewer_content`, and new `self.text_viewer_type` (defaulting to "text").
- [ ] Task: Implement: Update `self.text_viewer_wrap` (defaulting to True) to allow independent word wrap.
- [ ] Task: Implement: Update `_render_text_viewer(self, label: str, content: str, text_type: str = "text")` signature and caller usage.
- [ ] Task: Conductor - User Manual Verification 'Phase 1: State & Interface Update' (Protocol in workflow.md)
## Phase 2: Core Rendering Logic (Code & MD)
- [x] Task: Write Tests: Create a simulation test in `tests/test_gui_text_viewer.py` to verify the viewer opens and switches rendering paths based on `text_type`. a91b8dc
- [x] Task: Implement: In `src/gui_2.py`, refactor the text viewer window loop to: a91b8dc
- Use `MarkdownRenderer.render` if `text_type == "markdown"`. a91b8dc
- Use a cached `ImGuiColorTextEdit.TextEditor` if `text_type` matches a code language. a91b8dc
- Fallback to `imgui.input_text_multiline` for plain text. a91b8dc
- [x] Task: Implement: Ensure the `TextEditor` instance is properly cached using a unique key for the text viewer to maintain state. a91b8dc
- [x] Task: Conductor - User Manual Verification 'Phase 2: Core Rendering Logic' (Protocol in workflow.md) a91b8dc
- [ ] Task: Write Tests: Create a simulation test in `tests/test_gui_text_viewer.py` to verify the viewer opens and switches rendering paths based on `text_type`.
- [ ] Task: Implement: In `src/gui_2.py`, refactor the text viewer window loop to:
- Use `MarkdownRenderer.render` if `text_type == "markdown"`.
- Use a cached `ImGuiColorTextEdit.TextEditor` if `text_type` matches a code language.
- Fallback to `imgui.input_text_multiline` for plain text.
- [ ] Task: Implement: Ensure the `TextEditor` instance is properly cached using a unique key for the text viewer to maintain state.
- [ ] Task: Conductor - User Manual Verification 'Phase 2: Core Rendering Logic' (Protocol in workflow.md)
## Phase 3: UI Features (Copy, Line Numbers, Wrap)
- [x] Task: Write Tests: Update `tests/test_gui_text_viewer.py` to verify the copy-to-clipboard functionality and word wrap toggle. a91b8dc
- [x] Task: Implement: Add a "Copy" button to the text viewer title bar or a small toolbar at the top of the window. a91b8dc
- [x] Task: Implement: Add a "Word Wrap" checkbox inside the text viewer window. a91b8dc
- [x] Task: Implement: Configure the `TextEditor` instance to show line numbers and be read-only. a91b8dc
- [x] Task: Conductor - User Manual Verification 'Phase 3: UI Features' (Protocol in workflow.md) a91b8dc
- [ ] Task: Write Tests: Update `tests/test_gui_text_viewer.py` to verify the copy-to-clipboard functionality and word wrap toggle.
- [ ] Task: Implement: Add a "Copy" button to the text viewer title bar or a small toolbar at the top of the window.
- [ ] Task: Implement: Add a "Word Wrap" checkbox inside the text viewer window.
- [ ] Task: Implement: Configure the `TextEditor` instance to show line numbers and be read-only.
- [ ] Task: Conductor - User Manual Verification 'Phase 3: UI Features' (Protocol in workflow.md)
## Phase 4: Integration & Rollout
- [x] Task: Implement: Update all existing calls to `_render_text_viewer` in `src/gui_2.py` (e.g., in `_render_files_panel`, `_render_tool_calls_panel`) to pass the correct `text_type` based on file extension or content. 2826ad5
- [x] Task: Implement: Add "Markdown Preview" support for system prompt presets using the new text viewer logic. 2826ad5
- [x] Task: Conductor - User Manual Verification 'Phase 4: Integration & Rollout' (Protocol in workflow.md) 2826ad5
- [ ] Task: Implement: Update all existing calls to `_render_text_viewer` in `src/gui_2.py` (e.g., in `_render_files_panel`, `_render_tool_calls_panel`) to pass the correct `text_type` based on file extension or content.
- [ ] Task: Implement: Add "Markdown Preview" support for system prompt presets using the new text viewer logic.
- [ ] Task: Conductor - User Manual Verification 'Phase 4: Integration & Rollout' (Protocol in workflow.md)

View File

@@ -1,23 +1,26 @@
# Implementation Plan: Rich Thinking Trace Handling
## Status: COMPLETE (2026-03-14)
## Phase 1: Core Parsing & Model Update
- [ ] Task: Audit `src/models.py` and `src/project_manager.py` to identify current message serialization schemas.
- [ ] Task: Write Tests: Verify that raw AI responses with `<thinking>`, `<thought>`, and `Thinking:` markers are correctly parsed into segmented data structures (Thinking vs. Response).
- [ ] Task: Implement: Add `ThinkingSegment` model and update `ChatMessage` schema in `src/models.py` to support optional thinking traces.
- [ ] Task: Implement: Update parsing logic in `src/ai_client.py` or a dedicated utility to extract segments from raw provider responses.
- [ ] Task: Conductor - User Manual Verification 'Phase 1: Core Parsing & Model Update' (Protocol in workflow.md)
## Summary
Implemented thinking trace parsing, model, persistence, and GUI rendering for AI responses containing `<thinking>`, `<thought>`, and `Thinking:` markers.
## Phase 2: Persistence & History Integration
- [ ] Task: Write Tests: Verify that `ProjectManager` correctly serializes and deserializes messages with thinking segments to/from TOML history files.
- [ ] Task: Implement: Update `src/project_manager.py` to handle the new `ChatMessage` schema during session save/load.
- [ ] Task: Implement: Ensure `src/aggregate.py` or relevant context builders include thinking traces in the "Discussion History" sent back to the AI.
- [ ] Task: Conductor - User Manual Verification 'Phase 2: Persistence & History Integration' (Protocol in workflow.md)
## Files Created/Modified:
- `src/thinking_parser.py` - Parser for thinking traces
- `src/models.py` - ThinkingSegment model
- `src/gui_2.py` - _render_thinking_trace helper + integration
- `tests/test_thinking_trace.py` - 7 parsing tests
- `tests/test_thinking_persistence.py` - 4 persistence tests
- `tests/test_thinking_gui.py` - 4 GUI tests
## Phase 3: GUI Rendering - Comms & Discussion
- [ ] Task: Write Tests: Verify the GUI rendering logic correctly handles messages with and without thinking segments.
- [ ] Task: Implement: Create a reusable `_render_thinking_trace` helper in `src/gui_2.py` using a collapsible header (e.g., `imgui.collapsing_header`).
- [ ] Task: Implement: Integrate the thinking trace renderer into the **Comms History** panel in `src/gui_2.py`.
- [ ] Task: Implement: Integrate the thinking trace renderer into the **Discussion Hub** message loop in `src/gui_2.py`.
- [ ] Task: Conductor - User Manual Verification 'Phase 3: GUI Rendering - Comms & Discussion' (Protocol in workflow.md)
## Implementation Details:
- **Parser**: Extracts thinking segments from `<thinking>`, `<thought>`, `Thinking:` markers
- **Model**: `ThinkingSegment` dataclass with content and marker fields
- **GUI**: `_render_thinking_trace` with collapsible "Monologue" header
- **Styling**: Tinted background (dark brown), gold/amber text
- **Indicator**: Existing "THINKING..." in Discussion Hub
## Total Tests: 15 passing
## Phase 4: Final Polish & Theming
- [ ] Task: Implement: Apply specialized styling (e.g., tinted background or italicized text) to expanded thinking traces to distinguish them from direct responses.
- [ ] Task: Implement: Ensure thinking trace headers show a "Calculating..." or "Monologue" indicator while an agent is active.
- [ ] Task: Conductor - User Manual Verification 'Phase 4: Final Polish & Theming' (Protocol in workflow.md)

View File

@@ -23,23 +23,23 @@ active = "C:/projects/gencpp/gencpp_sloppy.toml"
separate_message_panel = false
separate_response_panel = false
separate_tool_calls_panel = false
bg_shader_enabled = true
bg_shader_enabled = false
crt_filter_enabled = false
separate_task_dag = false
separate_usage_analytics = false
separate_usage_analytics = true
separate_tier1 = false
separate_tier2 = false
separate_tier3 = false
separate_tier4 = false
separate_external_tools = false
separate_external_tools = true
[gui.show_windows]
"Context Hub" = true
"Files & Media" = true
"AI Settings" = true
"MMA Dashboard" = false
"MMA Dashboard" = true
"Task DAG" = false
"Usage Analytics" = false
"Usage Analytics" = true
"Tier 1" = false
"Tier 2" = false
"Tier 3" = false
@@ -51,21 +51,25 @@ separate_external_tools = false
"Discussion Hub" = true
"Operations Hub" = true
Message = false
Response = false
"Tool Calls" = false
Response = true
"Tool Calls" = true
Theme = true
"Log Management" = false
"Log Management" = true
Diagnostics = false
"External Tools" = false
"Shader Editor" = false
"Shader Editor" = true
[theme]
palette = "Nord Dark"
font_path = "C:/projects/manual_slop/assets/fonts/MapleMono-Regular.ttf"
font_size = 18.0
font_size = 16.0
scale = 1.0
transparency = 1.0
child_transparency = 1.0
transparency = 0.699999988079071
child_transparency = 0.6899999976158142
frosted_blur_radius = 29.68400001525879
frosted_tint_intensity = 0.5659999847412109
frosted_opacity = 0.5389999747276306
frosted_glass_enabled = true
[mma]
max_workers = 4

View File

@@ -12,7 +12,7 @@ ViewportPos=43,95
ViewportId=0x78C57832
Size=897,649
Collapsed=0
DockId=0x00000001,0
DockId=0x00000005,0
[Window][Files]
ViewportPos=3125,170
@@ -33,7 +33,7 @@ DockId=0x0000000A,0
Pos=0,17
Size=1680,730
Collapsed=0
DockId=0x00000001,0
DockId=0x00000005,0
[Window][Provider]
ViewportPos=43,95
@@ -41,23 +41,22 @@ ViewportId=0x78C57832
Pos=0,651
Size=897,468
Collapsed=0
DockId=0x00000001,0
DockId=0x00000005,0
[Window][Message]
Pos=661,1426
Pos=661,1321
Size=716,455
Collapsed=0
[Window][Response]
Pos=2258,1377
Size=1102,575
Pos=2437,925
Size=1111,773
Collapsed=0
[Window][Tool Calls]
Pos=520,1144
Size=663,232
Pos=1039,464
Size=587,510
Collapsed=0
DockId=0x00000006,0
[Window][Comms History]
ViewportPos=43,95
@@ -74,10 +73,10 @@ Collapsed=0
DockId=0xAFC85805,2
[Window][Theme]
Pos=0,1423
Size=579,737
Pos=2671,24
Size=1169,2136
Collapsed=0
DockId=0x00000002,2
DockId=0x00000002,1
[Window][Text Viewer - Entry #7]
Pos=379,324
@@ -88,13 +87,13 @@ Collapsed=0
Pos=1649,24
Size=580,1284
Collapsed=0
DockId=0x00000010,2
DockId=0x00000004,2
[Window][Context Hub]
Pos=0,1423
Size=579,737
Pos=0,1719
Size=999,441
Collapsed=0
DockId=0x00000002,1
DockId=0x00000006,0
[Window][AI Settings Hub]
Pos=406,17
@@ -103,28 +102,28 @@ Collapsed=0
DockId=0x0000000D,0
[Window][Discussion Hub]
Pos=2230,26
Size=1610,2134
Pos=1762,24
Size=907,2136
Collapsed=0
DockId=0x00000013,0
DockId=0x00000011,0
[Window][Operations Hub]
Pos=581,26
Size=1647,2134
Pos=1001,24
Size=759,2136
Collapsed=0
DockId=0x00000005,0
DockId=0x00000010,0
[Window][Files & Media]
Pos=0,1423
Size=579,737
Pos=0,1719
Size=999,441
Collapsed=0
DockId=0x00000002,0
DockId=0x00000006,1
[Window][AI Settings]
Pos=0,26
Size=579,1395
Pos=0,24
Size=999,1693
Collapsed=0
DockId=0x00000001,0
DockId=0x00000005,0
[Window][Approve Tool Execution]
Pos=3,524
@@ -132,16 +131,16 @@ Size=416,325
Collapsed=0
[Window][MMA Dashboard]
Pos=3360,26
Size=480,2134
Pos=2671,24
Size=1169,2136
Collapsed=0
DockId=0x00000010,0
DockId=0x00000002,0
[Window][Log Management]
Pos=3360,26
Size=480,2134
Pos=1931,24
Size=629,1416
Collapsed=0
DockId=0x00000010,0
DockId=0x00000002,1
[Window][Track Proposal]
Pos=709,326
@@ -167,7 +166,7 @@ Collapsed=0
Pos=2822,1717
Size=1018,420
Collapsed=0
DockId=0x0000000C,0
DockId=0x00000004,0
[Window][Approve PowerShell Command]
Pos=649,435
@@ -175,7 +174,7 @@ Size=381,329
Collapsed=0
[Window][Last Script Output]
Pos=927,1365
Pos=2810,265
Size=800,562
Collapsed=0
@@ -210,7 +209,7 @@ Size=3840,32
Collapsed=0
[Window][Text Viewer - message]
Pos=568,1226
Pos=562,588
Size=900,700
Collapsed=0
@@ -220,7 +219,7 @@ Size=900,700
Collapsed=0
[Window][Text Viewer - text]
Pos=1297,550
Pos=555,644
Size=900,700
Collapsed=0
@@ -240,7 +239,7 @@ Size=900,700
Collapsed=0
[Window][Text Viewer - tool_calls]
Pos=60,60
Pos=589,490
Size=900,700
Collapsed=0
@@ -330,10 +329,9 @@ Size=967,499
Collapsed=0
[Window][Usage Analytics]
Pos=1739,1107
Size=586,269
Pos=1702,689
Size=566,438
Collapsed=0
DockId=0x0000000F,0
[Window][Tool Preset Manager]
Pos=1301,302
@@ -366,7 +364,7 @@ Size=900,700
Collapsed=0
[Window][Text Viewer - Entry #4]
Pos=1247,1182
Pos=828,397
Size=900,700
Collapsed=0
@@ -381,8 +379,13 @@ Size=900,700
Collapsed=0
[Window][Shader Editor]
Pos=457,710
Size=493,252
Pos=753,637
Size=493,487
Collapsed=0
[Window][Text Viewer - list_directory]
Pos=60,60
Size=900,700
Collapsed=0
[Table][0xFB6E3870,4]
@@ -442,7 +445,7 @@ Column 3 Width=120
RefScale=16
Column 0 Width=48
Column 1 Weight=1.0000
Column 2 Width=118
Column 2 Width=117
Column 3 Width=48
[Table][0xD99F45C5,4]
@@ -498,23 +501,19 @@ Column 1 Weight=1.0000
DockNode ID=0x00000008 Pos=3125,170 Size=593,1157 Split=Y
DockNode ID=0x00000009 Parent=0x00000008 SizeRef=1029,147 Selected=0x0469CA7A
DockNode ID=0x0000000A Parent=0x00000008 SizeRef=1029,145 Selected=0xDF822E02
DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,26 Size=3840,2134 Split=X
DockNode ID=0x00000003 Parent=0xAFC85805 SizeRef=3358,1183 Split=X
DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,24 Size=3840,2136 Split=X
DockNode ID=0x00000003 Parent=0xAFC85805 SizeRef=1617,1183 Split=X
DockNode ID=0x0000000B Parent=0x00000003 SizeRef=404,1186 Split=X Selected=0xF4139CA2
DockNode ID=0x00000007 Parent=0x0000000B SizeRef=579,858 Split=Y Selected=0x8CA2375C
DockNode ID=0x00000001 Parent=0x00000007 SizeRef=824,525 CentralNode=1 Selected=0x7BD57D6A
DockNode ID=0x00000002 Parent=0x00000007 SizeRef=824,737 Selected=0x8CA2375C
DockNode ID=0x0000000E Parent=0x0000000B SizeRef=3259,858 Split=X Selected=0x418C7449
DockNode ID=0x00000012 Parent=0x0000000E SizeRef=1647,402 Split=Y Selected=0x418C7449
DockNode ID=0x00000005 Parent=0x00000012 SizeRef=876,1749 Selected=0x418C7449
DockNode ID=0x00000006 Parent=0x00000012 SizeRef=876,362 Selected=0x1D56B311
DockNode ID=0x00000013 Parent=0x0000000E SizeRef=1610,402 Selected=0x6F2B5B04
DockNode ID=0x00000007 Parent=0x0000000B SizeRef=999,858 Split=Y Selected=0x7BD57D6A
DockNode ID=0x00000005 Parent=0x00000007 SizeRef=639,904 CentralNode=1 Selected=0x7BD57D6A
DockNode ID=0x00000006 Parent=0x00000007 SizeRef=639,441 Selected=0x1DCB2623
DockNode ID=0x0000000E Parent=0x0000000B SizeRef=2839,858 Split=X Selected=0x418C7449
DockNode ID=0x00000001 Parent=0x0000000E SizeRef=1668,1288 Split=X Selected=0x6F2B5B04
DockNode ID=0x00000010 Parent=0x00000001 SizeRef=759,1416 Selected=0x418C7449
DockNode ID=0x00000011 Parent=0x00000001 SizeRef=907,1416 Selected=0x6F2B5B04
DockNode ID=0x00000002 Parent=0x0000000E SizeRef=1169,1288 Selected=0x8CA2375C
DockNode ID=0x0000000D Parent=0x00000003 SizeRef=435,1186 Selected=0x363E93D6
DockNode ID=0x00000004 Parent=0xAFC85805 SizeRef=480,1183 Split=Y Selected=0x3AEC3498
DockNode ID=0x00000010 Parent=0x00000004 SizeRef=1199,1689 Selected=0x2C0206CE
DockNode ID=0x00000011 Parent=0x00000004 SizeRef=1199,420 Split=X Selected=0xDEB547B6
DockNode ID=0x0000000C Parent=0x00000011 SizeRef=916,380 Selected=0x655BC6E9
DockNode ID=0x0000000F Parent=0x00000011 SizeRef=281,380 Selected=0xDEB547B6
DockNode ID=0x00000004 Parent=0xAFC85805 SizeRef=511,1183 Selected=0x3AEC3498
;;;<<<Layout_655921752_Default>>>;;;
;;;<<<HelloImGui_Misc>>>;;;

View File

@@ -225,9 +225,6 @@ class HookHandler(BaseHTTPRequestHandler):
for key, attr in gettable.items():
val = _get_app_attr(app, attr, None)
result[key] = _serialize_for_api(val)
result['show_text_viewer'] = _get_app_attr(app, 'show_text_viewer', False)
result['text_viewer_title'] = _get_app_attr(app, 'text_viewer_title', '')
result['text_viewer_type'] = _get_app_attr(app, 'text_viewer_type', 'markdown')
finally: event.set()
lock = _get_app_attr(app, "_pending_gui_tasks_lock")
tasks = _get_app_attr(app, "_pending_gui_tasks")

View File

@@ -25,7 +25,6 @@ from src import project_manager
from src import performance_monitor
from src import models
from src import presets
from src import thinking_parser
from src.file_cache import ASTParser
from src import ai_client
from src import shell_runner
@@ -243,8 +242,6 @@ class AppController:
self.ai_status: str = 'idle'
self.ai_response: str = ''
self.last_md: str = ''
self.last_aggregate_markdown: str = ''
self.last_resolved_system_prompt: str = ''
self.last_md_path: Optional[Path] = None
self.last_file_items: List[Any] = []
self.send_thread: Optional[threading.Thread] = None
@@ -254,7 +251,6 @@ class AppController:
self.show_text_viewer: bool = False
self.text_viewer_title: str = ''
self.text_viewer_content: str = ''
self.text_viewer_type: str = 'text'
self._pending_comms: List[Dict[str, Any]] = []
self._pending_tool_calls: List[Dict[str, Any]] = []
self._pending_history_adds: List[Dict[str, Any]] = []
@@ -378,10 +374,7 @@ class AppController:
'ui_separate_tier1': 'ui_separate_tier1',
'ui_separate_tier2': 'ui_separate_tier2',
'ui_separate_tier3': 'ui_separate_tier3',
'ui_separate_tier4': 'ui_separate_tier4',
'show_text_viewer': 'show_text_viewer',
'text_viewer_title': 'text_viewer_title',
'text_viewer_type': 'text_viewer_type'
'ui_separate_tier4': 'ui_separate_tier4'
}
self._gettable_fields = dict(self._settable_fields)
self._gettable_fields.update({
@@ -428,10 +421,7 @@ class AppController:
'ui_separate_tier1': 'ui_separate_tier1',
'ui_separate_tier2': 'ui_separate_tier2',
'ui_separate_tier3': 'ui_separate_tier3',
'ui_separate_tier4': 'ui_separate_tier4',
'show_text_viewer': 'show_text_viewer',
'text_viewer_title': 'text_viewer_title',
'text_viewer_type': 'text_viewer_type'
'ui_separate_tier4': 'ui_separate_tier4'
})
self.perf_monitor = performance_monitor.get_monitor()
self._perf_profiling_enabled = False
@@ -620,6 +610,16 @@ class AppController:
self._token_stats_dirty = True
if not is_streaming:
self._autofocus_response_tab = True
# ONLY add to history when turn is complete
if self.ui_auto_add_history and not stream_id and not is_streaming:
role = payload.get("role", "AI")
with self._pending_history_adds_lock:
self._pending_history_adds.append({
"role": role,
"content": self.ai_response,
"collapsed": True,
"ts": project_manager.now_ts()
})
elif action in ("mma_stream", "mma_stream_append"):
# Some events might have these at top level, some in a 'payload' dict
stream_id = task.get("stream_id") or task.get("payload", {}).get("stream_id")
@@ -1467,22 +1467,9 @@ class AppController:
if kind == "response" and "usage" in payload:
u = payload["usage"]
inp = u.get("input_tokens", u.get("prompt_tokens", 0))
out = u.get("output_tokens", u.get("completion_tokens", 0))
cache_read = u.get("cache_read_input_tokens", 0)
cache_create = u.get("cache_creation_input_tokens", 0)
total = u.get("total_tokens", 0)
# Store normalized usage back in payload for history rendering
u["input_tokens"] = inp
u["output_tokens"] = out
u["cache_read_input_tokens"] = cache_read
self.session_usage["input_tokens"] += inp
self.session_usage["output_tokens"] += out
self.session_usage["cache_read_input_tokens"] += cache_read
self.session_usage["cache_creation_input_tokens"] += cache_create
self.session_usage["total_tokens"] += total
for k in ["input_tokens", "output_tokens", "cache_read_input_tokens", "cache_creation_input_tokens", "total_tokens"]:
if k in u:
self.session_usage[k] += u.get(k, 0) or 0
input_t = u.get("input_tokens", 0)
output_t = u.get("output_tokens", 0)
model = payload.get("model", "unknown")
@@ -1503,27 +1490,7 @@ class AppController:
"ts": entry.get("ts", project_manager.now_ts())
})
if kind == "response":
if self.ui_auto_add_history:
role = payload.get("role", "AI")
text_content = payload.get("text", "")
if text_content.strip():
segments, parsed_response = thinking_parser.parse_thinking_trace(text_content)
entry_obj = {
"role": role,
"content": parsed_response.strip() if parsed_response else "",
"collapsed": True,
"ts": entry.get("ts", project_manager.now_ts())
}
if segments:
entry_obj["thinking_segments"] = [{"content": s.content, "marker": s.marker} for s in segments]
if entry_obj["content"] or segments:
with self._pending_history_adds_lock:
self._pending_history_adds.append(entry_obj)
if kind in ("tool_result", "tool_call"):
if self.ui_auto_add_history:
role = "Tool" if kind == "tool_result" else "Vendor API"
content = ""
if kind == "tool_result":
@@ -2518,11 +2485,6 @@ class AppController:
# Build discussion history text separately
history = flat.get("discussion", {}).get("history", [])
discussion_text = aggregate.build_discussion_text(history)
csp = filter(bool, [self.ui_global_system_prompt.strip(), self.ui_project_system_prompt.strip()])
self.last_resolved_system_prompt = "\n\n".join(csp)
self.last_aggregate_markdown = full_md
return full_md, path, file_items, stable_md, discussion_text
def _cb_plan_epic(self) -> None:

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,8 +29,7 @@ from src import app_controller
from src import mcp_client
from src import markdown_helper
from src import bg_shader
from src import thinking_parser
from src import thinking_parser
from src.shader_manager import ShaderManager
import re
import subprocess
if sys.platform == "win32":
@@ -40,7 +40,7 @@ else:
win32con = None
from pydantic import BaseModel
from imgui_bundle import imgui, hello_imgui, immapp, imgui_node_editor as ed, imgui_color_text_edit as ced
from imgui_bundle import imgui, hello_imgui, immapp, imgui_node_editor as ed
PROVIDERS: list[str] = ["gemini", "anthropic", "gemini_cli", "deepseek", "minimax"]
COMMS_CLAMP_CHARS: int = 300
@@ -107,29 +107,11 @@ class App:
self.controller.init_state()
self.show_windows.setdefault("Diagnostics", False)
self.controller.start_services(self)
self.controller._predefined_callbacks['_render_text_viewer'] = self._render_text_viewer
self.controller._predefined_callbacks['save_context_preset'] = self.save_context_preset
self.controller._predefined_callbacks['load_context_preset'] = self.load_context_preset
self.controller._predefined_callbacks['set_ui_file_paths'] = lambda p: setattr(self, 'ui_file_paths', p)
self.controller._predefined_callbacks['set_ui_screenshot_paths'] = lambda p: setattr(self, 'ui_screenshot_paths', p)
def simulate_save_preset(name: str):
from src import models
self.files = [models.FileItem(path='test.py')]
self.screenshots = ['test.png']
self.save_context_preset(name)
self.controller._predefined_callbacks['simulate_save_preset'] = simulate_save_preset
self.show_preset_manager_window = False
self.show_tool_preset_manager_window = False
self.show_persona_editor_window = False
self.show_text_viewer = False
self.text_viewer_title = ''
self.text_viewer_content = ''
self.text_viewer_type = 'text'
self.text_viewer_wrap = True
self._text_viewer_editor: Optional[ced.TextEditor] = None
self.ui_active_tool_preset = ""
self.ui_active_bias_profile = ""
self.ui_active_context_preset = ""
self.ui_active_persona = ""
self._editing_persona_name = ""
self._editing_persona_description = ""
@@ -141,7 +123,6 @@ class App:
self._editing_persona_max_tokens = 4096
self._editing_persona_tool_preset_id = ""
self._editing_persona_bias_profile_id = ""
self._editing_persona_context_preset_id = ""
self._editing_persona_preferred_models_list: list[dict] = []
self._editing_persona_scope = "project"
self._editing_persona_is_new = True
@@ -214,7 +195,6 @@ class App:
self.show_windows.setdefault("Tier 4: QA", False)
self.show_windows.setdefault('External Tools', False)
self.show_windows.setdefault('Shader Editor', False)
self.show_windows.setdefault('Session Hub', False)
self.ui_multi_viewport = gui_cfg.get("multi_viewport", False)
self.layout_presets = self.config.get("layout_presets", {})
self._new_preset_name = ""
@@ -233,9 +213,19 @@ 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}
self.ui_new_context_preset_name = ""
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()
self.start_time = time.time()
def _handle_approve_tool(self, user_data=None) -> None:
"""UI-level wrapper for approving a pending tool execution ask."""
self._handle_approve_ask()
@@ -293,54 +283,6 @@ class App:
pass
self.controller.shutdown()
def save_context_preset(self, name: str) -> None:
sys.stderr.write(f"[DEBUG] save_context_preset called with: {name}\n")
sys.stderr.flush()
if 'context_presets' not in self.controller.project:
self.controller.project['context_presets'] = {}
self.controller.project['context_presets'][name] = {
'files': [f.to_dict() if hasattr(f, 'to_dict') else {'path': str(f)} for f in self.files],
'screenshots': list(self.screenshots)
}
self.controller._save_active_project()
sys.stderr.write(f"[DEBUG] save_context_preset finished. Project keys: {list(self.controller.project.keys())}\n")
sys.stderr.flush()
def load_context_preset(self, name: str) -> None:
presets = self.controller.project.get('context_presets', {})
if name in presets:
preset = presets[name]
self.files = [models.FileItem.from_dict(f) if isinstance(f, dict) else models.FileItem(path=str(f)) for f in preset.get('files', [])]
self.screenshots = list(preset.get('screenshots', []))
def delete_context_preset(self, name: str) -> None:
if 'context_presets' in self.controller.project:
self.controller.project['context_presets'].pop(name, None)
self.controller._save_active_project()
@property
def ui_file_paths(self) -> list[str]:
return [f.path if hasattr(f, 'path') else str(f) for f in self.files]
@ui_file_paths.setter
def ui_file_paths(self, paths: list[str]) -> None:
old_files = {f.path: f for f in self.files if hasattr(f, 'path')}
new_files = []
now = time.time()
for p in paths:
if p in old_files:
new_files.append(old_files[p])
else:
new_files.append(models.FileItem(path=p, injected_at=now))
self.files = new_files
@property
def ui_screenshot_paths(self) -> list[str]:
return self.screenshots
@ui_screenshot_paths.setter
def ui_screenshot_paths(self, paths: list[str]) -> None:
self.screenshots = paths
def _test_callback_func_write_to_file(self, data: str) -> None:
"""A dummy function that a custom_callback would execute for testing."""
# Ensure the directory exists if running from a different cwd
@@ -349,9 +291,8 @@ class App:
f.write(data)
# ---------------------------------------------------------------- helpers
def _render_text_viewer(self, label: str, content: str, text_type: str = 'text', force_open: bool = False) -> None:
self.text_viewer_type = text_type
if imgui.button("[+]##" + str(id(content))) or force_open:
def _render_text_viewer(self, label: str, content: str) -> None:
if imgui.button("[+]##" + str(id(content))):
self.show_text_viewer = True
self.text_viewer_title = label
self.text_viewer_content = content
@@ -361,7 +302,6 @@ class App:
imgui.same_line()
if imgui.button("[+]##" + label + id_suffix):
self.show_text_viewer = True
self.text_viewer_type = 'markdown' if label in ('message', 'text', 'content', 'system') else 'json' if label in ('tool_calls', 'data') else 'powershell' if label == 'script' else 'text'
self.text_viewer_title = label
self.text_viewer_content = content
@@ -376,57 +316,21 @@ class App:
if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80))
if len(content) > COMMS_CLAMP_CHARS:
imgui.begin_child(f"heavy_text_child_{label}_{id_suffix}", imgui.ImVec2(0, 80), True)
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)
else:
markdown_helper.render_code(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:
imgui.text(content)
markdown_helper.render_code(content, context_id=ctx_id)
if is_nerv: imgui.pop_style_color()
# ---------------------------------------------------------------- gui
def _render_thinking_trace(self, segments: list[dict], entry_index: int, is_standalone: bool = False) -> None:
if not segments:
return
imgui.push_style_color(imgui.Col_.child_bg, vec4(40, 35, 25, 180))
imgui.push_style_color(imgui.Col_.text, vec4(200, 200, 150))
imgui.indent()
show_content = True
if not is_standalone:
header_label = f"Monologue ({len(segments)} traces)###thinking_header_{entry_index}"
show_content = imgui.collapsing_header(header_label)
if show_content:
h = 150 if is_standalone else 100
imgui.begin_child(f"thinking_content_{entry_index}", imgui.ImVec2(0, h), True)
for idx, seg in enumerate(segments):
content = seg.get("content", "")
marker = seg.get("marker", "thinking")
imgui.push_id(f"think_{entry_index}_{idx}")
imgui.text_colored(vec4(180, 150, 80), f"[{marker}]")
if self.ui_word_wrap:
imgui.push_text_wrap_pos(imgui.get_content_region_avail().x)
imgui.text_colored(vec4(200, 200, 150), content)
imgui.pop_text_wrap_pos()
else:
imgui.text_colored(vec4(200, 200, 150), content)
imgui.pop_id()
imgui.separator()
imgui.end_child()
imgui.unindent()
imgui.pop_style_color(2)
def _render_selectable_label(self, label: str, value: str, width: float = 0.0, multiline: bool = False, height: float = 0.0, color: Optional[imgui.ImVec4] = None) -> None:
imgui.push_id(label + str(hash(value)))
@@ -449,6 +353,111 @@ class App:
imgui.pop_style_var(2)
imgui.pop_id()
def _render_frosted_background(self, pos: imgui.ImVec2, size: imgui.ImVec2) -> None:
if not theme.get_frosted_glass_enabled(): return
if size.x <= 0 or size.y <= 0: return
try:
ws = immapp.get_window_size()
display_size = imgui.ImVec2(float(ws[0]), float(ws[1]))
except Exception:
display_size = imgui.get_io().display_size
if display_size.x <= 0 or display_size.y <= 0: return
uv_min = imgui.ImVec2(pos.x / display_size.x, 1.0 - pos.y / display_size.y)
uv_max = imgui.ImVec2((pos.x + size.x) / display_size.x, 1.0 - (pos.y + size.y) / display_size.y)
if self.shader_manager.blur_tex:
imgui.get_window_draw_list().add_image(
imgui.ImTextureRef(self.shader_manager.blur_tex),
pos,
imgui.ImVec2(pos.x + size.x, pos.y + size.y),
uv_min,
uv_max
)
else:
# Fallback: semi-transparent rectangle to help debug visibility
imgui.get_window_draw_list().add_rect_filled(
pos,
imgui.ImVec2(pos.x + size.x, pos.y + size.y),
imgui.get_color_u32(vec4(30, 30, 40, 0.7))
)
# Increase contrast with a 0.2 alpha tint overlay
imgui.get_window_draw_list().add_rect_filled(
pos,
imgui.ImVec2(pos.x + size.x, pos.y + size.y),
imgui.get_color_u32(vec4(0, 0, 0, 0.2))
)
def _begin_window(self, name: str, p_open: Any = None, flags: int = 0) -> tuple[bool, Any]:
frosted = theme.get_frosted_glass_enabled()
if frosted:
imgui.push_style_color(imgui.Col_.window_bg, vec4(0, 0, 0, 0))
expanded, opened = imgui.begin(name, p_open, flags)
if expanded and frosted:
self._render_frosted_background(imgui.get_window_pos(), imgui.get_window_size())
return expanded, opened
def _end_window(self) -> None:
imgui.end()
if theme.get_frosted_glass_enabled():
imgui.pop_style_color()
def _render_operations_hub(self) -> None:
exp, opened = self._begin_window("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
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()
self._end_window()
def _show_menus(self) -> None:
if imgui.begin_menu("manual slop"):
if imgui.menu_item("Quit", "Ctrl+Q", False)[0]:
@@ -548,24 +557,55 @@ 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, 64.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:
self._render_custom_title_bar()
self._render_shader_live_editor()
pushed_prior_tint = False
pushed_frosted_style = False
# Render background shader
bg = bg_shader.get_bg()
if bg.enabled:
frosted_enabled = theme.get_frosted_glass_enabled()
if bg.enabled and not frosted_enabled:
ws = imgui.get_io().display_size
bg.render(ws.x, ws.y)
if frosted_enabled:
ws = imgui.get_io().display_size
self.shader_manager.prepare_global_blur(
int(ws.x), int(ws.y),
self.shader_uniforms['frosted_blur_radius'],
self.shader_uniforms['frosted_tint_intensity'],
self.shader_uniforms['frosted_opacity'],
time.time()
)
# Draw background texture
dl = imgui.get_background_draw_list()
dl.add_image(imgui.ImTextureRef(self.shader_manager.scene_tex), (0, 0), (ws.x, ws.y))
imgui.push_style_color(imgui.Col_.window_bg, vec4(0, 0, 0, 0))
pushed_frosted_style = True
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))
@@ -638,7 +678,7 @@ class App:
self._tool_log_dirty = False
if self.show_windows.get("Context Hub", False):
exp, opened = imgui.begin("Context Hub", self.show_windows["Context Hub"])
exp, opened = self._begin_window("Context Hub", self.show_windows["Context Hub"])
self.show_windows["Context Hub"] = bool(opened)
if exp:
if imgui.begin_tab_bar('context_hub_tabs'):
@@ -648,22 +688,19 @@ class App:
if imgui.begin_tab_item('Paths')[0]:
self._render_paths_panel()
imgui.end_tab_item()
if imgui.begin_tab_item('Context Presets')[0]:
self._render_context_presets_panel()
imgui.end_tab_item()
imgui.end_tab_bar()
imgui.end()
self._end_window()
if self.show_windows.get("Files & Media", False):
exp, opened = imgui.begin("Files & Media", self.show_windows["Files & Media"])
exp, opened = self._begin_window("Files & Media", self.show_windows["Files & Media"])
self.show_windows["Files & Media"] = bool(opened)
if exp:
if imgui.collapsing_header("Files"):
self._render_files_panel()
if imgui.collapsing_header("Screenshots"):
self._render_screenshots_panel()
imgui.end()
self._end_window()
if self.show_windows.get("AI Settings", False):
exp, opened = imgui.begin("AI Settings", self.show_windows["AI Settings"])
exp, opened = self._begin_window("AI Settings", self.show_windows["AI Settings"])
self.show_windows["AI Settings"] = bool(opened)
if exp:
self._render_persona_selector_panel()
@@ -673,56 +710,56 @@ class App:
self._render_system_prompts_panel()
self._render_agent_tools_panel()
imgui.end()
self._end_window()
if self.ui_separate_usage_analytics and self.show_windows.get("Usage Analytics", False):
exp, opened = imgui.begin("Usage Analytics", self.show_windows["Usage Analytics"])
exp, opened = self._begin_window("Usage Analytics", self.show_windows["Usage Analytics"])
self.show_windows["Usage Analytics"] = bool(opened)
if exp:
self._render_usage_analytics_panel()
imgui.end()
self._end_window()
if self.show_windows.get("MMA Dashboard", False):
exp, opened = imgui.begin("MMA Dashboard", self.show_windows["MMA Dashboard"])
exp, opened = self._begin_window("MMA Dashboard", self.show_windows["MMA Dashboard"])
self.show_windows["MMA Dashboard"] = bool(opened)
if exp:
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_mma_dashboard")
self._render_mma_dashboard()
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_mma_dashboard")
imgui.end()
self._end_window()
if self.ui_separate_task_dag and self.show_windows.get("Task DAG", False):
exp, opened = imgui.begin("Task DAG", self.show_windows["Task DAG"])
exp, opened = self._begin_window("Task DAG", self.show_windows["Task DAG"])
self.show_windows["Task DAG"] = bool(opened)
if exp:
self._render_task_dag_panel()
imgui.end()
self._end_window()
if self.ui_separate_tier1 and self.show_windows.get("Tier 1: Strategy", False):
exp, opened = imgui.begin("Tier 1: Strategy", self.show_windows["Tier 1: Strategy"])
exp, opened = self._begin_window("Tier 1: Strategy", self.show_windows["Tier 1: Strategy"])
self.show_windows["Tier 1: Strategy"] = bool(opened)
if exp:
self._render_tier_stream_panel("Tier 1", "Tier 1")
imgui.end()
self._end_window()
if self.ui_separate_tier2 and self.show_windows.get("Tier 2: Tech Lead", False):
exp, opened = imgui.begin("Tier 2: Tech Lead", self.show_windows["Tier 2: Tech Lead"])
exp, opened = self._begin_window("Tier 2: Tech Lead", self.show_windows["Tier 2: Tech Lead"])
self.show_windows["Tier 2: Tech Lead"] = bool(opened)
if exp:
self._render_tier_stream_panel("Tier 2", "Tier 2 (Tech Lead)")
imgui.end()
self._end_window()
if self.ui_separate_tier3 and self.show_windows.get("Tier 3: Workers", False):
exp, opened = imgui.begin("Tier 3: Workers", self.show_windows["Tier 3: Workers"])
exp, opened = self._begin_window("Tier 3: Workers", self.show_windows["Tier 3: Workers"])
self.show_windows["Tier 3: Workers"] = bool(opened)
if exp:
self._render_tier_stream_panel("Tier 3", None)
imgui.end()
self._end_window()
if self.ui_separate_tier4 and self.show_windows.get("Tier 4: QA", False):
exp, opened = imgui.begin("Tier 4: QA", self.show_windows["Tier 4: QA"])
exp, opened = self._begin_window("Tier 4: QA", self.show_windows["Tier 4: QA"])
self.show_windows["Tier 4: QA"] = bool(opened)
if exp:
self._render_tier_stream_panel("Tier 4", "Tier 4 (QA)")
imgui.end()
self._end_window()
if self.show_windows.get("Theme", False):
self._render_theme_panel()
if self.show_windows.get("Discussion Hub", False):
exp, opened = imgui.begin("Discussion Hub", self.show_windows["Discussion Hub"])
exp, opened = self._begin_window("Discussion Hub", self.show_windows["Discussion Hub"])
self.show_windows["Discussion Hub"] = bool(opened)
if exp:
# Top part for the history
@@ -769,86 +806,37 @@ class App:
else:
imgui.text_disabled("Message & Response panels are detached.")
imgui.end()
self._end_window()
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"])
exp, opened = self._begin_window("Message", self.show_windows["Message"])
self.show_windows["Message"] = bool(opened)
if exp:
self._render_message_panel()
imgui.end()
self._end_window()
if self.ui_separate_response_panel and self.show_windows.get("Response", False):
exp, opened = imgui.begin("Response", self.show_windows["Response"])
exp, opened = self._begin_window("Response", self.show_windows["Response"])
self.show_windows["Response"] = bool(opened)
if exp:
self._render_response_panel()
imgui.end()
self._end_window()
if self.ui_separate_tool_calls_panel and self.show_windows.get("Tool Calls", False):
exp, opened = imgui.begin("Tool Calls", self.show_windows["Tool Calls"])
exp, opened = self._begin_window("Tool Calls", self.show_windows["Tool Calls"])
self.show_windows["Tool Calls"] = bool(opened)
if exp:
self._render_tool_calls_panel()
imgui.end()
self._end_window()
if self.ui_separate_external_tools and self.show_windows.get('External Tools', False):
exp, opened = imgui.begin('External Tools', self.show_windows['External Tools'])
exp, opened = self._begin_window('External Tools', self.show_windows['External Tools'])
self.show_windows['External Tools'] = bool(opened)
if exp:
self._render_external_tools_panel()
imgui.end()
self._end_window()
if self.show_windows.get("Log Management", False):
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_log_management")
@@ -858,8 +846,6 @@ class App:
if self.show_windows.get("Diagnostics", False):
self._render_diagnostics_panel()
self._render_session_hub()
self.perf_monitor.end_frame()
# ---- Modals / Popups
with self._pending_dialog_lock:
@@ -1041,6 +1027,7 @@ class App:
expanded, opened = imgui.begin("Last Script Output", self.show_script_output)
self.show_script_output = bool(opened)
if expanded:
self._render_frosted_background(imgui.get_window_pos(), imgui.get_window_size())
imgui.text("Script:")
imgui.same_line()
self._render_text_viewer("Last Script", self.ui_last_script_text)
@@ -1072,35 +1059,8 @@ class App:
expanded, opened = imgui.begin(f"Text Viewer - {self.text_viewer_title}", self.show_text_viewer)
self.show_text_viewer = bool(opened)
if expanded:
# Toolbar
if imgui.button("Copy"):
imgui.set_clipboard_text(self.text_viewer_content)
imgui.same_line()
_, self.text_viewer_wrap = imgui.checkbox("Word Wrap", self.text_viewer_wrap)
imgui.separator()
renderer = markdown_helper.get_renderer()
tv_type = getattr(self, "text_viewer_type", "text")
if tv_type == 'markdown':
imgui.begin_child("tv_md_scroll", imgui.ImVec2(-1, -1), True)
markdown_helper.render(self.text_viewer_content, context_id='text_viewer')
imgui.end_child()
elif tv_type in renderer._lang_map:
if self._text_viewer_editor is None:
self._text_viewer_editor = ced.TextEditor()
self._text_viewer_editor.set_read_only_enabled(True)
self._text_viewer_editor.set_show_line_numbers_enabled(True)
# Sync text and language
lang_id = renderer._lang_map[tv_type]
if self._text_viewer_editor.get_text().strip() != self.text_viewer_content.strip():
self._text_viewer_editor.set_text(self.text_viewer_content)
self._text_viewer_editor.set_language_definition(lang_id)
self._text_viewer_editor.render('##tv_editor', a_size=imgui.ImVec2(-1, -1))
else:
if self.text_viewer_wrap:
self._render_frosted_background(imgui.get_window_pos(), imgui.get_window_size())
if self.ui_word_wrap:
imgui.begin_child("tv_wrap", imgui.ImVec2(-1, -1), False)
imgui.push_text_wrap_pos(imgui.get_content_region_avail().x)
imgui.text(self.text_viewer_content)
@@ -1159,6 +1119,8 @@ class App:
if pushed_prior_tint:
imgui.pop_style_color()
if pushed_frosted_style:
imgui.pop_style_color()
if self.perf_profiling_enabled: self.perf_monitor.end_component("_gui_func")
@@ -1241,13 +1203,15 @@ class App:
imgui.separator()
imgui.text("Prompt Content:")
imgui.same_line()
if imgui.button("Pop out MD Preview"):
self.text_viewer_title = f"Preset: {self._editing_preset_name}"
self.text_viewer_content = self._editing_preset_system_prompt
self.text_viewer_type = "markdown"
self.show_text_viewer = True
if imgui.button("MD Preview" if not self._prompt_md_preview else "Edit Mode"):
self._prompt_md_preview = not self._prompt_md_preview
rem_y = imgui.get_content_region_avail().y
if self._prompt_md_preview:
if imgui.begin_child("prompt_preview", imgui.ImVec2(-1, rem_y), True):
markdown_helper.render(self._editing_preset_system_prompt, context_id="prompt_preset_preview")
imgui.end_child()
else:
_, self._editing_preset_system_prompt = imgui.input_text_multiline("##pcont", self._editing_preset_system_prompt, imgui.ImVec2(-1, rem_y))
imgui.end_child()
@@ -1486,7 +1450,6 @@ class App:
if imgui.button("New Persona", imgui.ImVec2(-1, 0)):
self._editing_persona_name = ""; self._editing_persona_system_prompt = ""
self._editing_persona_tool_preset_id = ""; self._editing_persona_bias_profile_id = ""
self._editing_persona_context_preset_id = ""
self._editing_persona_preferred_models_list = [{"provider": self.current_provider, "model": self.current_model, "temperature": 0.7, "top_p": 1.0, "max_output_tokens": 4096, "history_trunc_limit": 900000}]
self._editing_persona_scope = "project"; self._editing_persona_is_new = True
imgui.separator()
@@ -1495,7 +1458,6 @@ class App:
if name and imgui.selectable(f"{name}##p_list", name == self._editing_persona_name and not getattr(self, '_editing_persona_is_new', False))[0]:
p = personas[name]; self._editing_persona_name = p.name; self._editing_persona_system_prompt = p.system_prompt or ""
self._editing_persona_tool_preset_id = p.tool_preset or ""; self._editing_persona_bias_profile_id = p.bias_profile or ""
self._editing_persona_context_preset_id = getattr(p, 'context_preset', '') or ""
import copy; self._editing_persona_preferred_models_list = copy.deepcopy(p.preferred_models) if p.preferred_models else []
self._editing_persona_scope = self.controller.persona_manager.get_persona_scope(p.name); self._editing_persona_is_new = False
imgui.end_child()
@@ -1581,10 +1543,6 @@ class App:
imgui.table_next_column(); imgui.text("Bias Profile:"); bn = ["None"] + sorted(self.controller.bias_profiles.keys())
b_idx = bn.index(self._editing_persona_bias_profile_id) if getattr(self, '_editing_persona_bias_profile_id', '') in bn else 0
imgui.set_next_item_width(-1); _, b_idx = imgui.combo("##pbp", b_idx, bn); self._editing_persona_bias_profile_id = bn[b_idx] if b_idx > 0 else ""
imgui.table_next_row()
imgui.table_next_column(); imgui.text("Context Preset:"); cn = ["None"] + sorted(self.controller.project.get("context_presets", {}).keys())
c_idx = cn.index(self._editing_persona_context_preset_id) if getattr(self, '_editing_persona_context_preset_id', '') in cn else 0
imgui.set_next_item_width(-1); _, c_idx = imgui.combo("##pcp", c_idx, cn); self._editing_persona_context_preset_id = cn[c_idx] if c_idx > 0 else ""
imgui.end_table()
if imgui.button("Manage Tools & Biases", imgui.ImVec2(-1, 0)): self.show_tool_preset_manager_window = True
@@ -1612,7 +1570,7 @@ class App:
if imgui.button("Save##pers", imgui.ImVec2(100, 0)):
if self._editing_persona_name.strip():
try:
import copy; persona = models.Persona(name=self._editing_persona_name.strip(), system_prompt=self._editing_persona_system_prompt, tool_preset=self._editing_persona_tool_preset_id or None, bias_profile=self._editing_persona_bias_profile_id or None, context_preset=self._editing_persona_context_preset_id or None, preferred_models=copy.deepcopy(self._editing_persona_preferred_models_list))
import copy; persona = models.Persona(name=self._editing_persona_name.strip(), system_prompt=self._editing_persona_system_prompt, tool_preset=self._editing_persona_tool_preset_id or None, bias_profile=self._editing_persona_bias_profile_id or None, preferred_models=copy.deepcopy(self._editing_persona_preferred_models_list))
self.controller._cb_save_persona(persona, getattr(self, '_editing_persona_scope', 'project')); self.ai_status = f"Saved: {persona.name}"
except Exception as e: self.ai_status = f"Error: {e}"
else: self.ai_status = "Name required"
@@ -1770,30 +1728,6 @@ class App:
self.ai_status = "paths reset to defaults"
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_paths_panel")
def _render_context_presets_panel(self) -> None:
imgui.text_colored(C_IN, "Context Presets")
imgui.separator()
changed, new_name = imgui.input_text("Preset Name##new_ctx", self.ui_new_context_preset_name)
if changed: self.ui_new_context_preset_name = new_name
imgui.same_line()
if imgui.button("Save Current"):
if self.ui_new_context_preset_name.strip():
self.save_context_preset(self.ui_new_context_preset_name.strip())
imgui.separator()
presets = self.controller.project.get('context_presets', {})
for name in sorted(presets.keys()):
preset = presets[name]
n_files = len(preset.get('files', []))
n_shots = len(preset.get('screenshots', []))
imgui.text(f"{name} ({n_files} files, {n_shots} shots)")
imgui.same_line()
if imgui.button(f"Load##{name}"):
self.load_context_preset(name)
imgui.same_line()
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")
@@ -2073,7 +2007,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})")
@@ -2098,29 +2031,6 @@ class App:
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_diagnostics_panel")
imgui.end()
def _render_session_hub(self) -> None:
if self.show_windows.get('Session Hub', False):
exp, opened = imgui.begin('Session Hub', self.show_windows['Session Hub'])
self.show_windows['Session Hub'] = bool(opened)
if exp:
if imgui.begin_tab_bar('session_hub_tabs'):
if imgui.begin_tab_item('Aggregate MD')[0]:
if imgui.button("Copy"):
imgui.set_clipboard_text(self.last_aggregate_markdown)
imgui.begin_child("last_agg_md", imgui.ImVec2(0, 0), True)
markdown_helper.render(self.last_aggregate_markdown, context_id="session_hub_agg")
imgui.end_child()
imgui.end_tab_item()
if imgui.begin_tab_item('System Prompt')[0]:
if imgui.button("Copy"):
imgui.set_clipboard_text(self.last_resolved_system_prompt)
imgui.begin_child("last_sys_prompt", imgui.ImVec2(0, 0), True)
markdown_helper.render(self.last_resolved_system_prompt, context_id="session_hub_sys")
imgui.end_child()
imgui.end_tab_item()
imgui.end_tab_bar()
imgui.end()
def _render_markdown_test(self) -> None:
imgui.text("Markdown Test Panel")
imgui.separator()
@@ -2244,6 +2154,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...']
@@ -2431,22 +2342,6 @@ def hello():
if ts_str:
imgui.same_line()
imgui.text_colored(vec4(120, 120, 100), str(ts_str))
# Visual indicator for file injections
e_dt = project_manager.parse_ts(ts_str)
if e_dt:
e_unix = e_dt.timestamp()
next_unix = float('inf')
if i + 1 < len(self.disc_entries):
n_ts = self.disc_entries[i+1].get("ts", "")
n_dt = project_manager.parse_ts(n_ts)
if n_dt: next_unix = n_dt.timestamp()
injected_here = [f for f in self.files if hasattr(f, 'injected_at') and f.injected_at and e_unix <= f.injected_at < next_unix]
if injected_here:
imgui.same_line()
imgui.text_colored(vec4(100, 255, 100), f"[{len(injected_here)}+]")
if imgui.is_item_hovered():
tooltip = "Files injected at this point:\n" + "\n".join([f.path for f in injected_here])
imgui.set_tooltip(tooltip)
if collapsed:
imgui.same_line()
if imgui.button("Ins"):
@@ -2459,19 +2354,10 @@ def hello():
imgui.same_line()
preview = entry["content"].replace("\\n", " ")[:60]
if len(entry["content"]) > 60: preview += "..."
if not preview.strip() and entry.get("thinking_segments"):
preview = entry["thinking_segments"][0]["content"].replace("\\n", " ")[:60]
if len(entry["thinking_segments"][0]["content"]) > 60: preview += "..."
imgui.text_colored(vec4(160, 160, 150), preview)
if not collapsed:
thinking_segments = entry.get("thinking_segments", [])
has_content = bool(entry.get("content", "").strip())
is_standalone = bool(thinking_segments) and not has_content
if thinking_segments:
self._render_thinking_trace(thinking_segments, i, is_standalone=is_standalone)
if read_mode:
content = entry["content"]
if content.strip():
pattern = re.compile(r"\[Definition: (.*?) from (.*?) \(line (\d+)\)\](\s+```[\s\S]*?```)?")
matches = list(pattern.finditer(content))
is_nerv = theme.is_nerv_active()
@@ -2498,9 +2384,9 @@ def hello():
if res:
self.text_viewer_title = path
self.text_viewer_content = res
self.text_viewer_type = Path(path).suffix.lstrip('.') if Path(path).suffix else 'text'
self.show_text_viewer = True
if code_block:
# Render code block with highlighting
if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80))
markdown_helper.render(code_block, context_id=f'disc_{i}_c_{m_idx}')
if is_nerv: imgui.pop_style_color()
@@ -2513,7 +2399,6 @@ def hello():
if self.ui_word_wrap: imgui.pop_text_wrap_pos()
imgui.end_child()
else:
if not is_standalone:
ch, entry["content"] = imgui.input_text_multiline("##content", entry["content"], imgui.ImVec2(-1, 150))
imgui.separator()
imgui.pop_id()
@@ -2540,7 +2425,6 @@ def hello():
self._editing_persona_system_prompt = persona.system_prompt or ""
self._editing_persona_tool_preset_id = persona.tool_preset or ""
self._editing_persona_bias_profile_id = persona.bias_profile or ""
self._editing_persona_context_preset_id = getattr(persona, 'context_preset', '') or ""
import copy
self._editing_persona_preferred_models_list = copy.deepcopy(persona.preferred_models) if persona.preferred_models else []
self._editing_persona_is_new = False
@@ -2569,9 +2453,6 @@ def hello():
if persona.bias_profile:
self.ui_active_bias_profile = persona.bias_profile
ai_client.set_bias_profile(persona.bias_profile)
if getattr(persona, 'context_preset', None):
self.ui_active_context_preset = persona.context_preset
self.load_context_preset(persona.context_preset)
imgui.end_combo()
imgui.same_line()
if imgui.button("Manage Personas"):
@@ -2952,24 +2833,14 @@ def hello():
imgui.begin_child("response_scroll_area", imgui.ImVec2(0, -40), True)
is_nerv = theme.is_nerv_active()
if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80))
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)
markdown_helper.render(parsed_response, context_id="response")
markdown_helper.render(self.ai_response, context_id="response")
if is_nerv: imgui.pop_style_color()
imgui.end_child()
imgui.separator()
if imgui.button("-> History"):
if self.ai_response:
segments, response = thinking_parser.parse_thinking_trace(self.ai_response)
entry = {"role": "AI", "content": response, "collapsed": True, "ts": project_manager.now_ts()}
if segments:
entry["thinking_segments"] = [{"content": s.content, "marker": s.marker} for s in segments]
self.disc_entries.append(entry)
self.disc_entries.append({"role": "AI", "content": self.ai_response, "collapsed": True, "ts": project_manager.now_ts()})
if is_blinking:
imgui.pop_style_color(2)
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_response_panel")
@@ -3085,12 +2956,6 @@ def hello():
imgui.text_colored(C_LBL, f"#{i_display}")
imgui.same_line()
imgui.text_colored(vec4(160, 160, 160), ts)
latency = entry.get("latency") or entry.get("metadata", {}).get("latency")
if latency:
imgui.same_line()
imgui.text_colored(C_SUB, f" ({latency:.2f}s)")
ticket_id = entry.get("mma_ticket_id")
if ticket_id:
imgui.same_line()
@@ -3109,34 +2974,14 @@ def hello():
# Optimized content rendering using _render_heavy_text logic
idx_str = str(i)
if kind == "request":
usage = payload.get("usage", {})
if usage:
inp = usage.get("input_tokens", 0)
imgui.text_colored(C_LBL, f" tokens in:{inp}")
self._render_heavy_text("message", payload.get("message", ""), idx_str)
if payload.get("system"):
self._render_heavy_text("system", payload.get("system", ""), idx_str)
elif kind == "response":
r = payload.get("round", 0)
sr = payload.get("stop_reason", "STOP")
usage = payload.get("usage", {})
usage_str = ""
if usage:
inp = usage.get("input_tokens", 0)
out = usage.get("output_tokens", 0)
cache = usage.get("cache_read_input_tokens", 0)
usage_str = f" in:{inp} out:{out}"
if cache:
usage_str += f" cache:{cache}"
imgui.text_colored(C_LBL, f"round: {r} stop_reason: {sr}{usage_str}")
text_content = payload.get("text", "")
segments, parsed_response = thinking_parser.parse_thinking_trace(text_content)
if segments:
self._render_thinking_trace([{"content": s.content, "marker": s.marker} for s in segments], i, is_standalone=not bool(parsed_response.strip()))
if parsed_response:
self._render_heavy_text("text", parsed_response, idx_str)
imgui.text_colored(C_LBL, f"round: {r} stop_reason: {sr}")
self._render_heavy_text("text", payload.get("text", ""), idx_str)
tcs = payload.get("tool_calls", [])
if tcs:
self._render_heavy_text("tool_calls", json.dumps(tcs, indent=1), idx_str)
@@ -3196,7 +3041,7 @@ def hello():
script = entry.get("script", "")
res = entry.get("result", "")
# Use a clear, formatted combined view for the detail window
combined = f"**COMMAND:**\n```powershell\n{script}\n```\n\n---\n**OUTPUT:**\n```text\n{res}\n```"
combined = f"COMMAND:\n{script}\n\n{'='*40}\nOUTPUT:\n{res}"
script_preview = script.replace("\n", " ")[:150]
if len(script) > 150: script_preview += "..."
@@ -3204,7 +3049,6 @@ def hello():
if imgui.is_item_clicked():
self.text_viewer_title = f"Tool Call #{i+1} Details"
self.text_viewer_content = combined
self.text_viewer_type = 'markdown'
self.show_text_viewer = True
imgui.table_next_column()
@@ -3214,7 +3058,6 @@ def hello():
if imgui.is_item_clicked():
self.text_viewer_title = f"Tool Call #{i+1} Details"
self.text_viewer_content = combined
self.text_viewer_type = 'markdown'
self.show_text_viewer = True
imgui.end_table()
@@ -4135,6 +3978,7 @@ def hello():
exp, opened = imgui.begin("Theme", self.show_windows["Theme"])
self.show_windows["Theme"] = bool(opened)
if exp:
self._render_frosted_background(imgui.get_window_pos(), imgui.get_window_size())
imgui.text("Palette")
cp = theme.get_current_palette()
if imgui.begin_combo("##pal", cp):
@@ -4208,6 +4052,10 @@ def hello():
gui_cfg["crt_filter_enabled"] = self.ui_crt_filter
self._flush_to_config()
models.save_config(self.config)
ch_fg, fg_val = imgui.checkbox("Frosted Glass Effect", theme.get_frosted_glass_enabled())
if ch_fg:
theme.set_frosted_glass_enabled(fg_val)
self._flush_to_config()
models.save_config(self.config)
imgui.end()
@@ -4253,6 +4101,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

@@ -111,7 +111,6 @@ DEFAULT_TOOL_CATEGORIES: Dict[str, List[str]] = {
def parse_history_entries(history_strings: list[str], roles: list[str]) -> list[dict[str, Any]]:
import re
from src import thinking_parser
entries = []
for raw in history_strings:
ts = ""
@@ -129,30 +128,11 @@ def parse_history_entries(history_strings: list[str], roles: list[str]) -> list[
content = rest[match.end():].strip()
else:
content = rest
entry_obj = {"role": role, "content": content, "collapsed": True, "ts": ts}
if role == "AI" and ("<thinking>" in content or "<thought>" in content or "Thinking:" in content):
segments, parsed_content = thinking_parser.parse_thinking_trace(content)
if segments:
entry_obj["content"] = parsed_content
entry_obj["thinking_segments"] = [{"content": s.content, "marker": s.marker} for s in segments]
entries.append(entry_obj)
entries.append({"role": role, "content": content, "collapsed": True, "ts": ts})
return entries
@dataclass
class ThinkingSegment:
content: str
marker: str # 'thinking', 'thought', or 'Thinking:'
def to_dict(self) -> Dict[str, Any]:
return {"content": self.content, "marker": self.marker}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "ThinkingSegment":
return cls(content=data["content"], marker=data["marker"])
@dataclass
@dataclass
class Ticket:
id: str
@@ -259,6 +239,8 @@ class Track:
)
@dataclass
@dataclass
@dataclass
class WorkerContext:
ticket_id: str
@@ -357,14 +339,12 @@ class FileItem:
path: str
auto_aggregate: bool = True
force_full: bool = False
injected_at: Optional[float] = None
def to_dict(self) -> Dict[str, Any]:
return {
"path": self.path,
"auto_aggregate": self.auto_aggregate,
"force_full": self.force_full,
"injected_at": self.injected_at,
}
@classmethod
@@ -373,7 +353,6 @@ class FileItem:
path=data["path"],
auto_aggregate=data.get("auto_aggregate", True),
force_full=data.get("force_full", False),
injected_at=data.get("injected_at"),
)
@dataclass
@@ -469,7 +448,6 @@ class Persona:
system_prompt: str = ''
tool_preset: Optional[str] = None
bias_profile: Optional[str] = None
context_preset: Optional[str] = None
@property
def provider(self) -> Optional[str]:
@@ -512,8 +490,6 @@ class Persona:
res["tool_preset"] = self.tool_preset
if self.bias_profile is not None:
res["bias_profile"] = self.bias_profile
if self.context_preset is not None:
res["context_preset"] = self.context_preset
return res
@classmethod
@@ -547,8 +523,8 @@ class Persona:
system_prompt=data.get("system_prompt", ""),
tool_preset=data.get("tool_preset"),
bias_profile=data.get("bias_profile"),
context_preset=data.get("context_preset"),
)
@dataclass
class MCPServerConfig:
name: str

View File

@@ -33,14 +33,6 @@ def entry_to_str(entry: dict[str, Any]) -> str:
ts = entry.get("ts", "")
role = entry.get("role", "User")
content = entry.get("content", "")
segments = entry.get("thinking_segments")
if segments:
for s in segments:
marker = s.get("marker", "thinking")
s_content = s.get("content", "")
content = f"<{marker}>\n{s_content}\n</{marker}>\n{content}"
if ts:
return f"@{ts}\n{role}:\n{content}"
return f"{role}:\n{content}"
@@ -101,7 +93,6 @@ def default_project(name: str = "unnamed") -> dict[str, Any]:
"output": {"output_dir": "./md_gen"},
"files": {"base_dir": ".", "paths": [], "tier_assignments": {}},
"screenshots": {"base_dir": ".", "paths": []},
"context_presets": {},
"gemini_cli": {"binary_path": "gemini"},
"deepseek": {"reasoning_effort": "medium"},
"agent": {
@@ -249,27 +240,6 @@ def flat_config(proj: dict[str, Any], disc_name: Optional[str] = None, track_id:
"history": history,
},
}
# ── context presets ──────────────────────────────────────────────────────────
def save_context_preset(project_dict: dict, preset_name: str, files: list[str], screenshots: list[str]) -> None:
"""Save a named context preset (files + screenshots) into the project dict."""
if "context_presets" not in project_dict:
project_dict["context_presets"] = {}
project_dict["context_presets"][preset_name] = {
"files": files,
"screenshots": screenshots
}
def load_context_preset(project_dict: dict, preset_name: str) -> dict:
"""Return the files and screenshots for a named preset."""
if "context_presets" not in project_dict or preset_name not in project_dict["context_presets"]:
raise KeyError(f"Preset '{preset_name}' not found in project context_presets.")
return project_dict["context_presets"][preset_name]
def delete_context_preset(project_dict: dict, preset_name: str) -> None:
"""Remove a named preset if it exists."""
if "context_presets" in project_dict:
project_dict["context_presets"].pop(preset_name, None)
# ── track state persistence ─────────────────────────────────────────────────
def save_track_state(track_id: str, state: 'TrackState', base_dir: Union[str, Path] = ".") -> None:

View File

@@ -5,6 +5,113 @@ class ShaderManager:
self.program = None
self.bg_program = None
self.pp_program = None
self.blur_h_program = None
self.blur_v_program = None
self.blur_fbo = None
self.scene_fbo = None
self.temp_fbo = None
self.scene_tex = None
self.blur_tex = None
self.temp_tex = None
self.fbo_width = 0
self.fbo_height = 0
self._vao = None
def _ensure_vao(self):
if self._vao is None:
try:
import sys
if sys.platform == "win32":
self._vao = gl.glGenVertexArrays(1)
else:
# Some non-win32 environments might not support VAOs or need different handling
self._vao = gl.glGenVertexArrays(1)
except Exception:
pass
if self._vao is not None:
gl.glBindVertexArray(self._vao)
def setup_capture_fbo(self, width, height):
if self.blur_fbo is not None:
gl.glDeleteFramebuffers(1, [self.blur_fbo])
if self.scene_fbo is not None:
gl.glDeleteFramebuffers(1, [self.scene_fbo])
if self.temp_fbo is not None:
gl.glDeleteFramebuffers(1, [self.temp_fbo])
if self.scene_tex is not None:
gl.glDeleteTextures(1, [self.scene_tex])
if self.blur_tex is not None:
gl.glDeleteTextures(1, [self.blur_tex])
if self.temp_tex is not None:
gl.glDeleteTextures(1, [self.temp_tex])
self.scene_tex = gl.glGenTextures(1)
gl.glBindTexture(gl.GL_TEXTURE_2D, self.scene_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.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_T, gl.GL_CLAMP_TO_EDGE)
self.scene_fbo = gl.glGenFramebuffers(1)
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.scene_fbo)
gl.glFramebufferTexture2D(gl.GL_FRAMEBUFFER, gl.GL_COLOR_ATTACHMENT0, gl.GL_TEXTURE_2D, self.scene_tex, 0)
if gl.glCheckFramebufferStatus(gl.GL_FRAMEBUFFER) != gl.GL_FRAMEBUFFER_COMPLETE:
raise RuntimeError("Scene Framebuffer not complete")
self.temp_tex = gl.glGenTextures(1)
gl.glBindTexture(gl.GL_TEXTURE_2D, self.temp_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.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_T, gl.GL_CLAMP_TO_EDGE)
self.temp_fbo = gl.glGenFramebuffers(1)
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.temp_fbo)
gl.glFramebufferTexture2D(gl.GL_FRAMEBUFFER, gl.GL_COLOR_ATTACHMENT0, gl.GL_TEXTURE_2D, self.temp_tex, 0)
if gl.glCheckFramebufferStatus(gl.GL_FRAMEBUFFER) != gl.GL_FRAMEBUFFER_COMPLETE:
raise RuntimeError("Temp Framebuffer not complete")
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.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_T, gl.GL_CLAMP_TO_EDGE)
self.blur_fbo = gl.glGenFramebuffers(1)
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.blur_fbo)
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("Blur Framebuffer not complete")
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0)
self.fbo_width = width
self.fbo_height = height
def render_background_to_fbo(self, width, height, time):
if self.scene_fbo is None or self.fbo_width != width or self.fbo_height != height:
self.setup_capture_fbo(width, height)
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.scene_fbo)
gl.glViewport(0, 0, width, height)
self.render_background(width, height, time)
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0)
def prepare_global_blur(self, width, height, radius, tint, opacity, time):
self.render_background_to_fbo(width, height, time)
self.render_blur(self.scene_tex, width, height, radius, tint, opacity)
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0)
def capture_begin(self, width, height):
if self.blur_fbo is None or self.fbo_width != width or self.fbo_height != height:
self.setup_capture_fbo(width, height)
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.blur_fbo)
gl.glViewport(0, 0, width, height)
def capture_end(self):
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0)
def compile_shader(self, vertex_src: str, fragment_src: str) -> int:
program = gl.glCreateProgram()
@@ -79,9 +186,44 @@ void main() {
uniform float u_time;
uniform vec2 u_resolution;
out vec4 FragColor;
float hash(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
}
float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
vec2 u = f * f * (3.0 - 2.0 * f);
return mix(mix(hash(i + vec2(0.0, 0.0)), hash(i + vec2(1.0, 0.0)), u.x),
mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), u.x), u.y);
}
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution.xy;
vec3 col = 0.5 + 0.5 * cos(u_time + uv.xyx + vec3(0, 2, 4));
vec2 p = uv * 2.0 - 1.0;
p.x *= u_resolution.x / u_resolution.y;
// Deep sea background gradient (dark blue)
vec3 col = mix(vec3(0.01, 0.03, 0.08), vec3(0.0, 0.08, 0.15), uv.y);
// Moving blobs / caustics
float n = 0.0;
float t = u_time * 0.15;
n += noise(p * 1.2 + vec2(t * 0.8, t * 0.5)) * 0.4;
n += noise(p * 2.5 - vec2(t * 0.4, t * 0.9)) * 0.2;
col += vec3(0.05, 0.12, 0.22) * n;
// Bright highlights (caustics approximation)
float c = 0.0;
for(int i=0; i<3; i++) {
vec2 p2 = p * (float(i) + 1.0) * 0.4;
p2 += vec2(sin(t + p2.y * 1.5), cos(t + p2.x * 1.5));
c += abs(0.015 / (length(p2) - 0.4));
}
col += vec3(0.1, 0.25, 0.45) * c * 0.12;
FragColor = vec4(col, 1.0);
}
"""
@@ -97,6 +239,7 @@ void main() {
u_res_loc = gl.glGetUniformLocation(self.bg_program, "u_resolution")
if u_res_loc != -1:
gl.glUniform2f(u_res_loc, float(width), float(height))
self._ensure_vao()
gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, 4)
gl.glUseProgram(0)
@@ -148,6 +291,114 @@ void main() {
u_time_loc = gl.glGetUniformLocation(self.pp_program, "u_time")
if u_time_loc != -1:
gl.glUniform1f(u_time_loc, float(time))
self._ensure_vao()
gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, 4)
gl.glBindTexture(gl.GL_TEXTURE_2D, 0)
gl.glUseProgram(0)
def setup_frosted_glass_shader(self):
vertex_src = """
#version 330 core
const vec2 positions[4] = vec2[](
vec2(-1.0, -1.0),
vec2( 1.0, -1.0),
vec2(-1.0, 1.0),
vec2( 1.0, 1.0)
);
const vec2 uvs[4] = vec2[](
vec2(0.0, 0.0),
vec2(1.0, 0.0),
vec2(0.0, 1.0),
vec2(1.0, 1.0)
);
out vec2 v_uv;
void main() {
gl_Position = vec4(positions[gl_VertexID], 0.0, 1.0);
v_uv = uvs[gl_VertexID];
}
"""
fragment_src_h = """
#version 330 core
in vec2 v_uv;
uniform sampler2D u_texture;
uniform float u_blur_radius;
uniform vec2 u_direction;
out vec4 FragColor;
void main() {
float weight[5] = float[](0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216);
vec2 res = vec2(textureSize(u_texture, 0));
vec2 tex_offset = (u_blur_radius / res) * u_direction * 2.5; // Multiplied by 2.5 for milky effect
vec4 result = texture(u_texture, v_uv) * weight[0];
for(int i = 1; i < 5; ++i) {
result += texture(u_texture, v_uv + tex_offset * float(i)) * weight[i];
result += texture(u_texture, v_uv - tex_offset * float(i)) * weight[i];
}
FragColor = result;
}
"""
fragment_src_v = """
#version 330 core
in vec2 v_uv;
uniform sampler2D u_texture;
uniform float u_blur_radius;
uniform vec2 u_direction;
uniform float u_tint_intensity;
uniform float u_opacity;
out vec4 FragColor;
void main() {
float weight[5] = float[](0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216);
vec2 res = vec2(textureSize(u_texture, 0));
vec2 tex_offset = (u_blur_radius / res) * u_direction * 2.5; // Multiplied by 2.5 for milky effect
vec4 result = texture(u_texture, v_uv) * weight[0];
for(int i = 1; i < 5; ++i) {
result += texture(u_texture, v_uv + tex_offset * float(i)) * weight[i];
result += texture(u_texture, v_uv - tex_offset * float(i)) * weight[i];
}
vec3 tint_color = vec3(0.05, 0.07, 0.12); // Slightly deeper tint
vec3 tinted = mix(result.rgb, tint_color, u_tint_intensity);
FragColor = vec4(tinted, result.a * u_opacity);
}
"""
self.blur_h_program = self.compile_shader(vertex_src, fragment_src_h)
self.blur_v_program = self.compile_shader(vertex_src, fragment_src_v)
def render_blur(self, texture_id, width, height, radius, tint, opacity):
if not self.blur_h_program or not self.blur_v_program:
return
self._ensure_vao()
# Pass 1: Horizontal blur to temp_fbo
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.temp_fbo)
gl.glViewport(0, 0, width, height)
gl.glClear(gl.GL_COLOR_BUFFER_BIT)
gl.glUseProgram(self.blur_h_program)
gl.glActiveTexture(gl.GL_TEXTURE0)
gl.glBindTexture(gl.GL_TEXTURE_2D, texture_id)
gl.glUniform1i(gl.glGetUniformLocation(self.blur_h_program, "u_texture"), 0)
gl.glUniform1f(gl.glGetUniformLocation(self.blur_h_program, "u_blur_radius"), float(radius))
gl.glUniform2f(gl.glGetUniformLocation(self.blur_h_program, "u_direction"), 1.0, 0.0)
gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, 4)
# Pass 2: Vertical blur to blur_fbo
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.blur_fbo)
gl.glViewport(0, 0, width, height)
gl.glClear(gl.GL_COLOR_BUFFER_BIT)
gl.glUseProgram(self.blur_v_program)
gl.glActiveTexture(gl.GL_TEXTURE0)
gl.glBindTexture(gl.GL_TEXTURE_2D, self.temp_tex)
gl.glUniform1i(gl.glGetUniformLocation(self.blur_v_program, "u_texture"), 0)
gl.glUniform1f(gl.glGetUniformLocation(self.blur_v_program, "u_blur_radius"), float(radius))
gl.glUniform2f(gl.glGetUniformLocation(self.blur_v_program, "u_direction"), 0.0, 1.0)
gl.glUniform1f(gl.glGetUniformLocation(self.blur_v_program, "u_tint_intensity"), float(tint))
gl.glUniform1f(gl.glGetUniformLocation(self.blur_v_program, "u_opacity"), 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,10 @@ _current_font_size: float = 16.0
_current_scale: float = 1.0
_transparency: float = 1.0
_child_transparency: float = 1.0
_frosted_glass_enabled: bool = False
_frosted_blur_radius: float = 8.0
_frosted_tint_intensity: float = 0.1
_frosted_opacity: float = 1.0
# ------------------------------------------------------------------ public API
@@ -269,6 +273,34 @@ def set_child_transparency(val: float) -> None:
_child_transparency = val
apply(_current_palette)
def get_frosted_glass_enabled() -> bool:
return _frosted_glass_enabled
def set_frosted_glass_enabled(val: bool) -> None:
global _frosted_glass_enabled
_frosted_glass_enabled = val
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 +382,17 @@ 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_glass_enabled"] = _frosted_glass_enabled
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_glass_enabled, _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 +405,10 @@ 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_glass_enabled = bool(t.get("frosted_glass_enabled", False))
_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

@@ -1,53 +0,0 @@
import re
from typing import List, Tuple
from src.models import ThinkingSegment
def parse_thinking_trace(text: str) -> Tuple[List[ThinkingSegment], str]:
"""
Parses thinking segments from text and returns (segments, response_content).
Support extraction of thinking traces from <thinking>...</thinking>, <thought>...</thought>,
and blocks prefixed with Thinking:.
"""
segments = []
# 1. Extract <thinking> and <thought> tags
current_text = text
# Combined pattern for tags
tag_pattern = re.compile(r'<(thinking|thought)>(.*?)</\1>', re.DOTALL | re.IGNORECASE)
def extract_tags(txt: str) -> Tuple[List[ThinkingSegment], str]:
found_segments = []
def replace_func(match):
marker = match.group(1).lower()
content = match.group(2).strip()
found_segments.append(ThinkingSegment(content=content, marker=marker))
return ""
remaining = tag_pattern.sub(replace_func, txt)
return found_segments, remaining
tag_segments, remaining = extract_tags(current_text)
segments.extend(tag_segments)
# 2. Extract Thinking: prefix
# This usually appears at the start of a block and ends with a double newline or a response marker.
thinking_colon_pattern = re.compile(r'(?:^|\n)Thinking:\s*(.*?)(?:\n\n|\nResponse:|\nAnswer:|$)', re.DOTALL | re.IGNORECASE)
def extract_colon_blocks(txt: str) -> Tuple[List[ThinkingSegment], str]:
found_segments = []
def replace_func(match):
content = match.group(1).strip()
if content:
found_segments.append(ThinkingSegment(content=content, marker="Thinking:"))
return "\n\n"
res = thinking_colon_pattern.sub(replace_func, txt)
return found_segments, res
colon_segments, final_remaining = extract_colon_blocks(remaining)
segments.extend(colon_segments)
return segments, final_remaining.strip()

Binary file not shown.

View File

@@ -1,59 +0,0 @@
import pytest
from src.project_manager import (
save_context_preset,
load_context_preset,
delete_context_preset
)
def test_save_context_preset():
project_dict = {}
preset_name = "test_preset"
files = ["file1.py", "file2.py"]
screenshots = ["screenshot1.png"]
save_context_preset(project_dict, preset_name, files, screenshots)
assert "context_presets" in project_dict
assert preset_name in project_dict["context_presets"]
assert project_dict["context_presets"][preset_name]["files"] == files
assert project_dict["context_presets"][preset_name]["screenshots"] == screenshots
def test_load_context_preset():
project_dict = {
"context_presets": {
"test_preset": {
"files": ["file1.py"],
"screenshots": ["screenshot1.png"]
}
}
}
preset = load_context_preset(project_dict, "test_preset")
assert preset["files"] == ["file1.py"]
assert preset["screenshots"] == ["screenshot1.png"]
def test_load_nonexistent_preset():
project_dict = {"context_presets": {}}
with pytest.raises(KeyError):
load_context_preset(project_dict, "nonexistent")
def test_delete_context_preset():
project_dict = {
"context_presets": {
"test_preset": {
"files": ["file1.py"],
"screenshots": []
}
}
}
delete_context_preset(project_dict, "test_preset")
assert "test_preset" not in project_dict["context_presets"]
def test_delete_nonexistent_preset_no_error():
project_dict = {"context_presets": {}}
# Should not raise error if it doesn't exist
delete_context_preset(project_dict, "nonexistent")
assert "nonexistent" not in project_dict["context_presets"]

42
tests/test_fbo_capture.py Normal file
View File

@@ -0,0 +1,42 @@
import pytest
from unittest.mock import patch, MagicMock
import OpenGL.GL as gl
def test_shader_manager_fbo_initialization():
with patch("src.shader_manager.gl") as mock_gl:
mock_gl.glGenFramebuffers.side_effect = [1, 2]
mock_gl.glGenTextures.side_effect = [3, 4]
mock_gl.glCheckFramebufferStatus.return_value = mock_gl.GL_FRAMEBUFFER_COMPLETE
from src.shader_manager import ShaderManager
manager = ShaderManager()
manager.setup_capture_fbo(800, 600)
assert manager.scene_fbo == 1
assert manager.blur_fbo == 2
assert manager.scene_tex == 3
assert manager.blur_tex == 4
assert mock_gl.glGenFramebuffers.call_count == 2
assert mock_gl.glGenTextures.call_count == 2
assert mock_gl.glCheckFramebufferStatus.call_count == 2
def test_shader_manager_capture_lifecycle():
with patch("src.shader_manager.gl") as mock_gl:
mock_gl.glCheckFramebufferStatus.return_value = mock_gl.GL_FRAMEBUFFER_COMPLETE
mock_gl.glGenFramebuffers.side_effect = [1, 2]
mock_gl.glGenTextures.side_effect = [3, 4]
from src.shader_manager import ShaderManager
manager = ShaderManager()
# Ensure setup is called on first capture
manager.capture_begin(1024, 768)
assert manager.fbo_width == 1024
assert manager.fbo_height == 768
# Should bind the blur FBO
mock_gl.glBindFramebuffer.assert_any_call(mock_gl.GL_FRAMEBUFFER, manager.blur_fbo)
mock_gl.glBindFramebuffer.reset_mock()
manager.capture_end()
# Verify unbind (glBindFramebuffer(..., 0))
mock_gl.glBindFramebuffer.assert_called_with(mock_gl.GL_FRAMEBUFFER, 0)

View File

@@ -0,0 +1,19 @@
import pytest
from unittest.mock import patch, MagicMock
def test_shader_manager_frosted_glass_compilation():
# Mock OpenGL before importing ShaderManager
with patch("src.shader_manager.gl") as mock_gl:
mock_gl.glCreateProgram.return_value = 1
mock_gl.glCreateShader.return_value = 2
mock_gl.glGetShaderiv.return_value = mock_gl.GL_TRUE
mock_gl.glGetProgramiv.return_value = mock_gl.GL_TRUE
from src.shader_manager import ShaderManager
manager = ShaderManager()
# This should fail initially because the method doesn't exist
manager.setup_frosted_glass_shader()
assert manager.blur_program is not None
assert mock_gl.glCreateProgram.called

View File

@@ -1,35 +0,0 @@
import pytest
import time
from src.api_hook_client import ApiHookClient
def test_gui_context_preset_save_load(live_gui) -> None:
"""Verify that saving and loading context presets works via the GUI app."""
client = ApiHookClient()
assert client.wait_for_server(timeout=15)
preset_name = "test_gui_preset"
test_files = ["test.py"]
test_screenshots = ["test.png"]
client.push_event("custom_callback", {"callback": "simulate_save_preset", "args": [preset_name]})
time.sleep(1.5)
project_data = client.get_project()
project = project_data.get("project", {})
presets = project.get("context_presets", {})
assert preset_name in presets, f"Preset '{preset_name}' not found in project context_presets"
preset_entry = presets[preset_name]
preset_files = [f["path"] if isinstance(f, dict) else str(f) for f in preset_entry.get("files", [])]
assert preset_files == test_files
assert preset_entry.get("screenshots", []) == test_screenshots
# Load the preset
client.push_event("custom_callback", {"callback": "load_context_preset", "args": [preset_name]})
time.sleep(1.0)
context = client.get_context_state()
loaded_files = [f["path"] if isinstance(f, dict) else str(f) for f in context.get("files", [])]
assert loaded_files == test_files
assert context.get("screenshots", []) == test_screenshots

View File

@@ -0,0 +1,89 @@
import pytest
import time
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, \
patch("src.gui_2.theme") as mock_theme:
mock_sm = mock_sm_class.return_value
mock_sm.blur_tex = 2
mock_theme.get_frosted_glass_enabled.return_value = True
mock_imgui.get_io().display_size = MagicMock(x=1920, y=1080)
from src.gui_2 import App
with patch.object(App, '__init__', return_value=None):
app = App()
app.shader_manager = mock_sm
# Simulate frame
app._render_frosted_background(pos=MagicMock(x=10, y=10), size=MagicMock(x=100, y=100))
# Now it should only call add_image
assert mock_imgui.get_window_draw_list().add_image.called
# It no longer calls these
assert not mock_sm.setup_capture_fbo.called
assert not mock_sm.render_blur.called
assert not mock_sm.capture_begin.called
assert not mock_sm.capture_end.called
def test_gui_global_blur_call():
with patch("src.gui_2.ShaderManager") as mock_sm_class, \
patch("src.gui_2.imgui") as mock_imgui, \
patch("src.gui_2.theme") as mock_theme, \
patch("src.gui_2.bg_shader") as mock_bg:
mock_sm = mock_sm_class.return_value
mock_theme.get_frosted_glass_enabled.return_value = True
mock_theme.is_nerv_active.return_value = False
mock_imgui.get_io().display_size = MagicMock(x=1920, y=1080)
mock_bg.get_bg.return_value = MagicMock(enabled=False)
from src.gui_2 import App
with patch.object(App, '__init__', return_value=None):
app = App()
app.shader_manager = mock_sm
app.shader_uniforms = {
'frosted_blur_radius': 10.0,
'frosted_tint_intensity': 0.5,
'frosted_opacity': 0.8
}
app.ai_status = "idle"
app.ui_crt_filter = False
app.controller = MagicMock()
app.perf_profiling_enabled = False
app.is_viewing_prior_session = False
app.config = {}
app.project = {}
app.show_windows = {}
app.ui_auto_scroll_comms = False
app._comms_log_dirty = False
app._tool_log_dirty = False
app._pending_comms_lock = MagicMock()
app._pending_comms = []
app.ui_focus_agent = None
app._last_ui_focus_agent = None
app.perf_monitor = MagicMock()
app._process_pending_gui_tasks = MagicMock()
app._process_pending_history_adds = MagicMock()
app._render_track_proposal_modal = MagicMock()
app._render_patch_modal = MagicMock()
app._render_save_preset_modal = MagicMock()
app._render_preset_manager_window = MagicMock()
app._render_tool_preset_manager_window = MagicMock()
app._render_persona_editor_window = MagicMock()
app._last_autosave = time.time()
app._autosave_interval = 60
app._render_custom_title_bar = MagicMock()
app._render_shader_live_editor = MagicMock()
try:
app._gui_func()
except Exception:
pass
assert mock_sm.prepare_global_blur.called

View File

@@ -1,28 +0,0 @@
import pytest
import time
from src.api_hook_client import ApiHookClient
def test_text_viewer_state_update(live_gui) -> None:
"""
Verifies that we can set text viewer state and it is reflected in GUI state.
"""
client = ApiHookClient()
label = "Test Viewer Label"
content = "This is test content for the viewer."
text_type = "markdown"
# Add a task to push a custom callback that mutates the app state
def set_viewer_state(app):
app.show_text_viewer = True
app.text_viewer_title = label
app.text_viewer_content = content
app.text_viewer_type = text_type
client.push_event("custom_callback", {"callback": set_viewer_state})
time.sleep(0.5)
state = client.get_gui_state()
assert state is not None
assert state.get('show_text_viewer') == True
assert state.get('text_viewer_title') == label
assert state.get('text_viewer_type') == text_type

View File

@@ -33,3 +33,18 @@ def test_theme_apply_sets_rounding_and_padding(monkeypatch):
assert mock_style.item_spacing == (8.0, 4.0)
assert mock_style.item_inner_spacing == (4.0, 4.0)
assert mock_style.scrollbar_size == 14.0
def test_frosted_glass_enabled_toggle():
theme.set_frosted_glass_enabled(True)
assert theme.get_frosted_glass_enabled() is True
theme.set_frosted_glass_enabled(False)
assert theme.get_frosted_glass_enabled() is False
def test_theme_config_frosted_glass():
config = {"theme": {"frosted_glass_enabled": True}}
theme.load_from_config(config)
assert theme.get_frosted_glass_enabled() is True
new_config = {}
theme.save_to_config(new_config)
assert new_config["theme"]["frosted_glass_enabled"] is True

View File

@@ -1,53 +0,0 @@
import pytest
def test_render_thinking_trace_helper_exists():
from src.gui_2 import App
assert hasattr(App, "_render_thinking_trace"), (
"_render_thinking_trace helper should exist in App class"
)
def test_discussion_entry_with_thinking_segments():
entry = {
"role": "AI",
"content": "Here's my response",
"thinking_segments": [
{"content": "Let me analyze this step by step...", "marker": "thinking"},
{"content": "I should consider edge cases...", "marker": "thought"},
],
"ts": "2026-03-13T10:00:00",
"collapsed": False,
}
assert "thinking_segments" in entry
assert len(entry["thinking_segments"]) == 2
def test_discussion_entry_without_thinking():
entry = {
"role": "User",
"content": "Hello",
"ts": "2026-03-13T10:00:00",
"collapsed": False,
}
assert "thinking_segments" not in entry
def test_thinking_segment_model_compatibility():
from src.models import ThinkingSegment
segment = ThinkingSegment(content="test", marker="thinking")
assert segment.content == "test"
assert segment.marker == "thinking"
d = segment.to_dict()
assert d["content"] == "test"
assert d["marker"] == "thinking"
if __name__ == "__main__":
test_render_thinking_trace_helper_exists()
test_discussion_entry_with_thinking_segments()
test_discussion_entry_without_thinking()
test_thinking_segment_model_compatibility()
print("All GUI thinking trace tests passed!")

View File

@@ -1,94 +0,0 @@
import pytest
import tempfile
import os
from pathlib import Path
from src import project_manager
from src.models import ThinkingSegment
def test_save_and_load_history_with_thinking_segments():
with tempfile.TemporaryDirectory() as tmpdir:
project_path = Path(tmpdir) / "test_project"
project_path.mkdir()
project_file = project_path / "test_project.toml"
project_file.write_text("[project]\nname = 'test'\n")
history_data = {
"entries": [
{
"role": "AI",
"content": "Here's the response",
"thinking_segments": [
{"content": "Let me think about this...", "marker": "thinking"}
],
"ts": "2026-03-13T10:00:00",
"collapsed": False,
},
{
"role": "User",
"content": "Hello",
"ts": "2026-03-13T09:00:00",
"collapsed": False,
},
]
}
project_manager.save_project(
{"project": {"name": "test"}}, project_file, disc_data=history_data
)
loaded = project_manager.load_history(project_file)
assert "entries" in loaded
assert len(loaded["entries"]) == 2
ai_entry = loaded["entries"][0]
assert ai_entry["role"] == "AI"
assert ai_entry["content"] == "Here's the response"
assert "thinking_segments" in ai_entry
assert len(ai_entry["thinking_segments"]) == 1
assert (
ai_entry["thinking_segments"][0]["content"] == "Let me think about this..."
)
user_entry = loaded["entries"][1]
assert user_entry["role"] == "User"
assert "thinking_segments" not in user_entry
def test_entry_to_str_with_thinking():
entry = {
"role": "AI",
"content": "Response text",
"thinking_segments": [{"content": "Thinking...", "marker": "thinking"}],
"ts": "2026-03-13T10:00:00",
}
result = project_manager.entry_to_str(entry)
assert "@2026-03-13T10:00:00" in result
assert "AI:" in result
assert "Response text" in result
def test_str_to_entry_with_thinking():
raw = "@2026-03-13T10:00:00\nAI:\nResponse text"
roles = ["User", "AI", "Vendor API", "System", "Reasoning"]
result = project_manager.str_to_entry(raw, roles)
assert result["role"] == "AI"
assert result["content"] == "Response text"
assert "ts" in result
def test_clean_nones_removes_thinking():
entry = {"role": "AI", "content": "Test", "thinking_segments": None, "ts": None}
cleaned = project_manager.clean_nones(entry)
assert "thinking_segments" not in cleaned
assert "ts" not in cleaned
if __name__ == "__main__":
test_save_and_load_history_with_thinking_segments()
test_entry_to_str_with_thinking()
test_str_to_entry_with_thinking()
test_clean_nones_removes_thinking()
print("All project_manager thinking tests passed!")

View File

@@ -1,68 +0,0 @@
from src.thinking_parser import parse_thinking_trace
def test_parse_xml_thinking_tag():
raw = "<thinking>\nLet me analyze this problem step by step.\n</thinking>\nHere is the answer."
segments, response = parse_thinking_trace(raw)
assert len(segments) == 1
assert segments[0].content == "Let me analyze this problem step by step."
assert segments[0].marker == "thinking"
assert response == "Here is the answer."
def test_parse_xml_thought_tag():
raw = "<thought>This is my reasoning process</thought>\nFinal response here."
segments, response = parse_thinking_trace(raw)
assert len(segments) == 1
assert segments[0].content == "This is my reasoning process"
assert segments[0].marker == "thought"
assert response == "Final response here."
def test_parse_text_thinking_prefix():
raw = "Thinking:\nThis is a text-based thinking trace.\n\nNow for the actual response."
segments, response = parse_thinking_trace(raw)
assert len(segments) == 1
assert segments[0].content == "This is a text-based thinking trace."
assert segments[0].marker == "Thinking:"
assert response == "Now for the actual response."
def test_parse_no_thinking():
raw = "This is a normal response without any thinking markers."
segments, response = parse_thinking_trace(raw)
assert len(segments) == 0
assert response == raw
def test_parse_empty_response():
segments, response = parse_thinking_trace("")
assert len(segments) == 0
assert response == ""
def test_parse_multiple_markers():
raw = "<thinking>First thinking</thinking>\n<thought>Second thought</thought>\nResponse"
segments, response = parse_thinking_trace(raw)
assert len(segments) == 2
assert segments[0].content == "First thinking"
assert segments[1].content == "Second thought"
def test_parse_thinking_with_empty_response():
raw = "<thinking>Just thinking, no response</thinking>"
segments, response = parse_thinking_trace(raw)
assert len(segments) == 1
assert segments[0].content == "Just thinking, no response"
assert response == ""
if __name__ == "__main__":
test_parse_xml_thinking_tag()
test_parse_xml_thought_tag()
test_parse_text_thinking_prefix()
test_parse_no_thinking()
test_parse_empty_response()
test_parse_multiple_markers()
test_parse_thinking_with_empty_response()
print("All thinking trace tests passed!")