Compare commits
46 Commits
FKING-GLAS
...
0474df5958
| Author | SHA1 | Date | |
|---|---|---|---|
| 0474df5958 | |||
| cf83aeeff3 | |||
| ca7d1b074f | |||
| 038c909ce3 | |||
| 84b6266610 | |||
| c5df29b760 | |||
| 791e1b7a81 | |||
| 573f5ee5d1 | |||
| 1e223b46b0 | |||
| 93a590cdc5 | |||
| b4396697dd | |||
| 31b38f0c77 | |||
| 2826ad53d8 | |||
| a91b8dcc99 | |||
| 74c9d4b992 | |||
| e28af48ae9 | |||
| 5470f2106f | |||
| 0f62eaff6d | |||
| 5285bc68f9 | |||
| 226ffdbd2a | |||
| 6594a50e4e | |||
| 1a305ee614 | |||
| 81ded98198 | |||
| b85b7d9700 | |||
| 3d0c40de45 | |||
| 47c5100ec5 | |||
| bc00fe1197 | |||
| 9515dee44d | |||
| 13199a0008 | |||
| 45c9e15a3c | |||
| d18eabdf4d | |||
| 9fb8b5757f | |||
| e30cbb5047 | |||
| 017a52a90a | |||
| 71269ceb97 | |||
| 0b33cbe023 | |||
| 1164aefffa | |||
| 1ad146b38e | |||
| 084f9429af | |||
| 95e6413017 | |||
| fc7b491f78 | |||
| 44a1d76dc7 | |||
| ea7b3ae3ae | |||
| c5a406eff8 | |||
| c15f38fb09 | |||
| 645f71d674 |
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
description: Tier 2 Tech Lead for architectural design and track execution with persistent memory
|
description: Tier 2 Tech Lead for architectural design and track execution with persistent memory
|
||||||
mode: primary
|
mode: primary
|
||||||
|
model: MiniMax-M2.5
|
||||||
temperature: 0.4
|
temperature: 0.4
|
||||||
permission:
|
permission:
|
||||||
edit: ask
|
edit: ask
|
||||||
@@ -13,9 +14,9 @@ ONLY output the requested text. No pleasantries.
|
|||||||
|
|
||||||
## Context Management
|
## Context Management
|
||||||
|
|
||||||
**MANUAL COMPACTION ONLY** <EFBFBD> Never rely on automatic context summarization.
|
**MANUAL COMPACTION ONLY** — Never rely on automatic context summarization.
|
||||||
Use `/compact` command explicitly when context needs reduction.
|
Use `/compact` command explicitly when context needs reduction.
|
||||||
You maintain PERSISTENT MEMORY throughout track execution <EFBFBD> do NOT apply Context Amnesia to your own session.
|
You maintain PERSISTENT MEMORY throughout track execution — do NOT apply Context Amnesia to your own session.
|
||||||
|
|
||||||
## CRITICAL: MCP Tools Only (Native Tools Banned)
|
## CRITICAL: MCP Tools Only (Native Tools Banned)
|
||||||
|
|
||||||
@@ -133,14 +134,14 @@ Before implementing:
|
|||||||
- Zero-assertion ban: Tests MUST have meaningful assertions
|
- Zero-assertion ban: Tests MUST have meaningful assertions
|
||||||
- Delegate test creation to Tier 3 Worker via Task tool
|
- Delegate test creation to Tier 3 Worker via Task tool
|
||||||
- Run tests and confirm they FAIL as expected
|
- Run tests and confirm they FAIL as expected
|
||||||
- **CONFIRM FAILURE** <EFBFBD> this is the Red phase
|
- **CONFIRM FAILURE** — this is the Red phase
|
||||||
|
|
||||||
### 3. Green Phase: Implement to Pass
|
### 3. Green Phase: Implement to Pass
|
||||||
|
|
||||||
- **Pre-delegation checkpoint**: Stage current progress (`git add .`)
|
- **Pre-delegation checkpoint**: Stage current progress (`git add .`)
|
||||||
- Delegate implementation to Tier 3 Worker via Task tool
|
- Delegate implementation to Tier 3 Worker via Task tool
|
||||||
- Run tests and confirm they PASS
|
- Run tests and confirm they PASS
|
||||||
- **CONFIRM PASS** <EFBFBD> this is the Green phase
|
- **CONFIRM PASS** — this is the Green phase
|
||||||
|
|
||||||
### 4. Refactor Phase (Optional)
|
### 4. Refactor Phase (Optional)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# Track frosted_glass_20260313 Context (REPAIR)
|
# Track frosted_glass_20260313 Context
|
||||||
|
|
||||||
- [Debrief](./debrief.md)
|
|
||||||
- [Specification](./spec.md)
|
- [Specification](./spec.md)
|
||||||
- [Implementation Plan](./plan.md)
|
- [Implementation Plan](./plan.md)
|
||||||
- [Metadata](./metadata.json)
|
- [Metadata](./metadata.json)
|
||||||
8
conductor/archive/frosted_glass_20260313/metadata.json
Normal file
8
conductor/archive/frosted_glass_20260313/metadata.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"track_id": "frosted_glass_20260313",
|
||||||
|
"type": "feature",
|
||||||
|
"status": "new",
|
||||||
|
"created_at": "2026-03-13T14:39:00Z",
|
||||||
|
"updated_at": "2026-03-13T14:39:00Z",
|
||||||
|
"description": "Add 'frosted glass' bg for transparency on panels and popups. This blurring effect will allow drop downs and other elements of these panels to not get hard to discern from background text or elements behind the panel."
|
||||||
|
}
|
||||||
26
conductor/archive/frosted_glass_20260313/plan.md
Normal file
26
conductor/archive/frosted_glass_20260313/plan.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# 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)
|
||||||
34
conductor/archive/frosted_glass_20260313/spec.md
Normal file
34
conductor/archive/frosted_glass_20260313/spec.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Specification: Frosted Glass Background Effect
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Implement a high-fidelity "frosted glass" (acrylic) background effect for all GUI panels and popups within the Manual Slop interface. This effect will use a GPU-resident shader to blur the content behind active windows, improving readability and visual depth while preventing background text from clashing with foreground UI elements.
|
||||||
|
|
||||||
|
## Functional Requirements
|
||||||
|
- **GPU-Accelerated Blur:**
|
||||||
|
- Implement a GLSL fragment shader (e.g., Gaussian or Kawase blur) within the existing `ShaderManager` pipeline.
|
||||||
|
- The shader must sample the current frame buffer background and render a blurred version behind the active window's background.
|
||||||
|
- **Global Integration:**
|
||||||
|
- The effect must automatically apply to all standard ImGui panels and popups.
|
||||||
|
- Integrate with `imgui.begin()` and `imgui.begin_popup()` (or via a reusable wrapper helper).
|
||||||
|
- **Real-Time Tuning:**
|
||||||
|
- Add controls to the **Live Shader Editor** to adjust the following parameters:
|
||||||
|
- **Blur Radius:** Control the intensity of the Gaussian blur.
|
||||||
|
- **Tint Intensity:** Control the strength of the "frost" overlay color.
|
||||||
|
- **Base Opacity:** Control the overall transparency of the frosted layer.
|
||||||
|
- **Persistence:**
|
||||||
|
- Save frosted glass parameters to `config.toml` under the `theme` or `shader` section.
|
||||||
|
|
||||||
|
## Technical Implementation
|
||||||
|
- **Shader Pipeline:** Use `PyOpenGL` to manage a dedicated background texture/FBO for sampling.
|
||||||
|
- **Coordinate Mapping:** Ensure the blur shader correctly maps screen coordinates to the region behind the current ImGui window.
|
||||||
|
- **State Integration:** Store tuning parameters in `App.shader_uniforms` and ensure they are updated every frame.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
- [ ] Panels and popups have a distinct, blurred background that clearly separates them from the content behind them.
|
||||||
|
- [ ] Changing the "Blur Radius" slider in the Shader Editor immediately updates the visual frostiness.
|
||||||
|
- [ ] The effect remains stable during window dragging and resizing.
|
||||||
|
- [ ] No significant performance degradation (maintaining target FPS).
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
- Implementing different blur types (e.g., motion blur, radial blur).
|
||||||
|
- Per-panel unique blur settings (initially global only).
|
||||||
@@ -17,7 +17,7 @@ For deep implementation details when planning or implementing tracks, consult `d
|
|||||||
## Primary Use Cases
|
## Primary Use Cases
|
||||||
|
|
||||||
- **Full Control over Vendor APIs:** Exposing detailed API metrics and configuring deep agent capabilities directly within the GUI.
|
- **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**) and a dedicated **'Context' role** for manual injections, allowing developers to optimize prompt limits with expert precision.
|
- **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.
|
||||||
- **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.
|
- **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
|
## Key Features
|
||||||
@@ -33,6 +33,7 @@ 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.
|
- **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.
|
- **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.
|
- **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.
|
- **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).
|
**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).
|
||||||
|
|
||||||
@@ -54,6 +55,8 @@ 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 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.
|
- **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.
|
- **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.
|
- **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.
|
- **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.
|
- **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.
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ This file tracks all major tracks for the project. Each track has its own detail
|
|||||||
7. [ ] **Track: Optimization pass for Data-Oriented Python heuristics**
|
7. [ ] **Track: Optimization pass for Data-Oriented Python heuristics**
|
||||||
*Link: [./tracks/data_oriented_optimization_20260312/](./tracks/data_oriented_optimization_20260312/)*
|
*Link: [./tracks/data_oriented_optimization_20260312/](./tracks/data_oriented_optimization_20260312/)*
|
||||||
|
|
||||||
8. [ ] **Track: Rich Thinking Trace Handling**
|
8. [x] **Track: Rich Thinking Trace Handling** - *Parse and display AI thinking/reasoning traces*
|
||||||
*Link: [./tracks/thinking_trace_handling_20260313/](./tracks/thinking_trace_handling_20260313/)*
|
*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)
|
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/)*
|
*Link: [./tracks/custom_shaders_20260309/](./tracks/custom_shaders_20260309/)*
|
||||||
|
|
||||||
7. [x] **Track: UI/UX Improvements - Presets and AI Settings**
|
7. [x] **Track: UI/UX Improvements - Presets and AI Settings**
|
||||||
*Link: [./tracks/presets_ai_settings_ux_20260311/](./tracks/presets_ai_settings_ux_20260311/)*
|
*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.*
|
*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. [ ] **Track: Session Context Snapshots & Visibility**
|
8. [x] **Track: Session Context Snapshots & Visibility**
|
||||||
*Link: [./tracks/session_context_snapshots_20260311/](./tracks/session_context_snapshots_20260311/)*
|
*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.*
|
*Goal: Session-scoped context management, saving Context Presets, MMA assignment, and agent-focused session filtering in the UI.*
|
||||||
|
|
||||||
@@ -79,13 +79,9 @@ 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/)*
|
*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.*
|
*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. [ ] **Track: Advanced Text Viewer with Syntax Highlighting**
|
11. [x] **Track: Advanced Text Viewer with Syntax Highlighting**
|
||||||
*Link: [./tracks/text_viewer_rich_rendering_20260313/](./tracks/text_viewer_rich_rendering_20260313/)*
|
*Link: [./tracks/text_viewer_rich_rendering_20260313/](./tracks/text_viewer_rich_rendering_20260313/)*
|
||||||
|
|
||||||
12. [ ] ~~**Track: Frosted Glass Background Effect**~~ THIS IS A LOST CAUSE DON'T BOTHER.
|
|
||||||
*Link: [./tracks/frosted_glass_20260313/](./tracks/frosted_glass_20260313/)*
|
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Additional Language Support
|
### Additional Language Support
|
||||||
@@ -165,6 +161,10 @@ This file tracks all major tracks for the project. Each track has its own detail
|
|||||||
|
|
||||||
### Completed / Archived
|
### 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: External MCP Server Support** (Archived 2026-03-12)
|
||||||
- [x] **Track: Project-Specific Conductor Directory** (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)
|
- [x] **Track: GUI Path Configuration in Context Hub** (Archived 2026-03-12)
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
# Debrief: Failed Frosted Glass Implementation (Attempt 1)
|
|
||||||
|
|
||||||
## 1. Post-Mortem Summary
|
|
||||||
The initial implementation of the Frosted Glass effect was a catastrophic failure resulting in application crashes (`RecursionError`, `AttributeError`, `RuntimeError`) and visual non-functionality (black backgrounds or invisible blurs).
|
|
||||||
|
|
||||||
## 2. Root Causes
|
|
||||||
|
|
||||||
### A. Architectural Blindness (ImGui Timing)
|
|
||||||
I attempted to use `glCopyTexImage2D` to capture the "backbuffer" during the `_gui_func` execution. In an immediate-mode GUI (ImGui), the backbuffer is cleared at the start of the frame and draw commands are only recorded during `_gui_func`. The actual GPU rendering happens **after** `_gui_func` finishes. Consequently, I was capturing and blurring an empty black screen every frame.
|
|
||||||
|
|
||||||
### B. Sub-Agent Fragmentation (Class Scope Breaks)
|
|
||||||
By delegating massive file refactors to the `generalist` sub-agent, I lost control over the strict 1-space indentation required by this project. The sub-agent introduced unindented blocks that silently closed the `App` class scope, causing all subsequent methods to become global functions. This lead to the avalanche of `AttributeError: 'App' object has no attribute '_render_operations_hub_contents'` and similar errors.
|
|
||||||
|
|
||||||
### C. Style Stack Imbalance
|
|
||||||
The implementation of `_begin_window` and `_end_window` wrappers failed to account for mid-render state changes. Toggling the "Frosted Glass" checkbox mid-frame resulted in mismatched `PushStyleColor` and `PopStyleColor` calls, triggering internal ImGui assertions and hard crashes.
|
|
||||||
|
|
||||||
### D. High-DPI Math Errors
|
|
||||||
The UV coordinate math failed to correctly account for `display_framebuffer_scale`. On high-resolution screens, the blur sampling was offset by thousands of pixels, rendering the effect physically invisible or distorted.
|
|
||||||
|
|
||||||
TODO:
|
|
||||||
|
|
||||||
LOOK AT THIS SHIT:
|
|
||||||
https://www.unknowncheats.me/forum/general-programming-and-reversing/617284-blurring-imgui-basically-window-using-acrylic-blur.html
|
|
||||||
https://github.com/Speykious/opengl-playground/blob/main/src/scenes/blurring.rs
|
|
||||||
https://www.intel.com/content/www/us/en/developer/articles/technical/an-investigation-of-fast-real-time-gpu-based-image-blur-algorithms.html
|
|
||||||
https://github.com/cofenberg/unrimp/blob/45aa431286ce597c018675c1a9730d98e6ccfc64/Renderer/RendererRuntime/src/DebugGui/DebugGuiManager.cpp
|
|
||||||
https://github.com/cofenberg/unrimp/blob/45aa431286ce597c018675c1a9730d98e6ccfc64/Renderer/RendererRuntime/src/DebugGui/Detail/Shader/DebugGui_GLSL_410.h
|
|
||||||
https://github.com/itsRythem/ImGui-Blur
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"track_id": "frosted_glass_20260313",
|
|
||||||
"type": "feature",
|
|
||||||
"status": "new",
|
|
||||||
"created_at": "2026-03-13T14:39:00Z",
|
|
||||||
"updated_at": "2026-03-13T18:55:00Z",
|
|
||||||
"description": "REPAIR: Implement stable frosted glass using native Windows DWM APIs."
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# Implementation Plan: Frosted Glass Background Effect (REPAIR - TRUE GPU)
|
|
||||||
|
|
||||||
## Phase 1: Robust Shader & FBO Foundation
|
|
||||||
- [x] Task: Implement: Create `ShaderManager` methods for downsampled FBO setup (scene, temp, blur). [d9148ac]
|
|
||||||
- [x] Task: Implement: Develop the "Deep Sea" background shader and integrate it as the FBO source. [d85dc3a]
|
|
||||||
- [x] Task: Implement: Develop the 2-pass Gaussian blur shaders with a wide tap distribution. [c8b7fca]
|
|
||||||
- [x] Task: Conductor - User Manual Verification 'Phase 1: Robust Foundation' (Protocol in workflow.md)
|
|
||||||
|
|
||||||
## Phase 2: High-Performance Blur Pipeline
|
|
||||||
- [x] Task: Implement: Create the `prepare_global_blur` method that renders the background and blurs it at 1/4 resolution. [9c2078a]
|
|
||||||
- [x] Task: Implement: Ensure the pipeline correctly handles high-DPI scaling (`fb_scale`) for internal FBO dimensions. [9c2078a]
|
|
||||||
- [x] Task: Conductor - User Manual Verification 'Phase 2: High-Performance Pipeline' (Protocol in workflow.md)
|
|
||||||
|
|
||||||
## Phase 3: GUI Integration & Screen-Space Sampling
|
|
||||||
- [x] Task: Implement: Update `_render_frosted_background` to perform normalized screen-space UV sampling. [926318f]
|
|
||||||
- [x] Task: Fix crash when display_size is invalid at startup. [db00fba]
|
|
||||||
## Phase 3: GUI Integration & Screen-Space Sampling
|
|
||||||
- [x] Task: Implement: Update `_render_frosted_background` to perform normalized screen-space UV sampling. [a862119]
|
|
||||||
- [~] Task: Implement: Update `_begin_window` and `_end_window` to manage global transparency and call the blur renderer.
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
# Specification: Frosted Glass Background Effect (REPAIR - TRUE GPU)
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
Implement a high-fidelity "frosted glass" (acrylic) background effect using a dedicated OpenGL pipeline. This implementation follows professional rendering patterns (downsampling, multi-pass blurring, and screen-space sampling) to ensure a smooth, milky look that remains performant on high-DPI displays.
|
|
||||||
|
|
||||||
## Functional Requirements
|
|
||||||
- **Dedicated Background Pipeline:**
|
|
||||||
- Render the animated "Deep Sea" background shader to an off-screen `SceneFBO` once per frame.
|
|
||||||
- **Multi-Scale Downsampled Blur:**
|
|
||||||
- Downsample the `SceneFBO` texture to 1/4 or 1/8 resolution.
|
|
||||||
- Perform 2-pass Gaussian blurring on the downsampled texture to achieve a creamy "milky" aesthetic.
|
|
||||||
- **ImGui Panel Integration:**
|
|
||||||
- Each ImGui panel must sample its background from the blurred texture using screen-space UV coordinates.
|
|
||||||
- Automatically force window transparency (`alpha 0.0`) when the effect is active.
|
|
||||||
- **Real-Time Shader Tuning:**
|
|
||||||
- Control blur radius, tint intensity, and opacity via the Live Shader Editor.
|
|
||||||
- **Stability:**
|
|
||||||
- Balanced style-stack management to prevent ImGui assertion crashes.
|
|
||||||
- Strict 1-space indentation and class scope protection.
|
|
||||||
|
|
||||||
## Technical Implementation
|
|
||||||
- **FBO Management:** Persistent FBOs for scene, temp, and blur textures.
|
|
||||||
- **UV Math:** `(window_pos / screen_res)` mapping to handle high-DPI scaling and vertical flipping.
|
|
||||||
- **DrawList Callbacks:** (If necessary) use callbacks to ensure the background is ready before panels draw.
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
- [ ] Toggling the effect does not crash the app.
|
|
||||||
- [ ] Windows show a deep, high-quality blur of the background shader.
|
|
||||||
- [ ] Blur follows windows perfectly during drag/resize.
|
|
||||||
- [ ] The "Milky" look is highly visible even at low radii.
|
|
||||||
@@ -1,24 +1,24 @@
|
|||||||
# Implementation Plan: Session Context Snapshots & Visibility
|
# Implementation Plan: Session Context Snapshots & Visibility
|
||||||
|
|
||||||
## Phase 1: Backend Support for Context Presets
|
## Phase 1: Backend Support for Context Presets
|
||||||
- [ ] Task: Write failing tests for saving, loading, and listing Context Presets in the project configuration.
|
- [x] Task: Write failing tests for saving, loading, and listing Context Presets in the project configuration. 93a590c
|
||||||
- [ ] Task: Implement Context Preset storage logic (e.g., updating TOML schemas in `project_manager.py`) to manage file/screenshot lists.
|
- [x] Task: Implement Context Preset storage logic (e.g., updating TOML schemas in `project_manager.py`) to manage file/screenshot lists. 93a590c
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Phase 1: Backend Support for Context Presets' (Protocol in workflow.md)
|
- [x] Task: Conductor - User Manual Verification 'Phase 1: Backend Support for Context Presets' (Protocol in workflow.md) 93a590c
|
||||||
|
|
||||||
## Phase 2: GUI Integration & Persona Assignment
|
## Phase 2: GUI Integration & Persona Assignment
|
||||||
- [ ] Task: Write tests for the Context Hub UI components handling preset saving and loading.
|
- [x] Task: Write tests for the Context Hub UI components handling preset saving and loading. 573f5ee
|
||||||
- [ ] Task: Implement the UI controls in the Context Hub to save current selections as a preset and load existing presets.
|
- [x] Task: Implement the UI controls in the Context Hub to save current selections as a preset and load existing presets. 573f5ee
|
||||||
- [ ] Task: Update the Persona configuration UI (`personas.py` / `gui_2.py`) to allow assigning a named Context Preset to an agent persona.
|
- [x] Task: Update the Persona configuration UI (`personas.py` / `gui_2.py`) to allow assigning a named Context Preset to an agent persona. 791e1b7
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Phase 2: GUI Integration & Persona Assignment' (Protocol in workflow.md)
|
- [x] Task: Conductor - User Manual Verification 'Phase 2: GUI Integration & Persona Assignment' (Protocol in workflow.md) 791e1b7
|
||||||
|
|
||||||
## Phase 3: Transparent Context Visibility
|
## Phase 3: Transparent Context Visibility
|
||||||
- [ ] Task: Write tests to ensure the initial aggregate markdown, resolved system prompt, and file injection timestamps are accurately recorded in the session state.
|
- [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
|
||||||
- [ ] Task: Implement UI elements in the Session Hub to expose the aggregated markdown and the active system prompt.
|
- [x] Task: Implement UI elements in the Session Hub to expose the aggregated markdown and the active system prompt. 84b6266
|
||||||
- [ ] Task: Enhance the discussion timeline rendering in `gui_2.py` to visually indicate exactly when files and screenshots were injected into the context.
|
- [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
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Phase 3: Transparent Context Visibility' (Protocol in workflow.md)
|
- [x] Task: Conductor - User Manual Verification 'Phase 3: Transparent Context Visibility' (Protocol in workflow.md) 84b6266
|
||||||
|
|
||||||
## Phase 4: Agent-Focused Session Filtering
|
## Phase 4: Agent-Focused Session Filtering
|
||||||
- [ ] Task: Write tests for the GUI state filtering logic when focusing on a specific agent's session.
|
- [x] Task: Write tests for the GUI state filtering logic when focusing on a specific agent's session. 038c909
|
||||||
- [ ] Task: Relocate the 'Focus Agent' feature from the Operations Hub to the MMA Dashboard.
|
- [x] Task: Relocate the 'Focus Agent' feature from the Operations Hub to the MMA Dashboard. 038c909
|
||||||
- [ ] Task: Implement the action to filter the Session and Discussion hubs based on the selected agent's context.
|
- [x] Task: Implement the action to filter the Session and Discussion hubs based on the selected agent's context. 038c909
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Phase 4: Agent-Focused Session Filtering' (Protocol in workflow.md)
|
- [x] Task: Conductor - User Manual Verification 'Phase 4: Agent-Focused Session Filtering' (Protocol in workflow.md) 038c909
|
||||||
@@ -1,29 +1,29 @@
|
|||||||
# Implementation Plan: Advanced Text Viewer with Syntax Highlighting
|
# Implementation Plan: Advanced Text Viewer with Syntax Highlighting
|
||||||
|
|
||||||
## Phase 1: State & Interface Update
|
## Phase 1: State & Interface Update
|
||||||
- [ ] Task: Audit `src/gui_2.py` to ensure all `text_viewer_*` state variables are explicitly initialized in `App.__init__`.
|
- [x] Task: Audit `src/gui_2.py` to ensure all `text_viewer_*` state variables are explicitly initialized in `App.__init__`. e28af48
|
||||||
- [ ] 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").
|
- [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
|
||||||
- [ ] Task: Implement: Update `self.text_viewer_wrap` (defaulting to True) to allow independent word wrap.
|
- [x] Task: Implement: Update `self.text_viewer_wrap` (defaulting to True) to allow independent word wrap. e28af48
|
||||||
- [ ] Task: Implement: Update `_render_text_viewer(self, label: str, content: str, text_type: str = "text")` signature and caller usage.
|
- [x] Task: Implement: Update `_render_text_viewer(self, label: str, content: str, text_type: str = "text")` signature and caller usage. e28af48
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Phase 1: State & Interface Update' (Protocol in workflow.md)
|
- [x] Task: Conductor - User Manual Verification 'Phase 1: State & Interface Update' (Protocol in workflow.md) e28af48
|
||||||
|
|
||||||
## Phase 2: Core Rendering Logic (Code & MD)
|
## Phase 2: Core Rendering Logic (Code & MD)
|
||||||
- [ ] 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`.
|
- [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
|
||||||
- [ ] Task: Implement: In `src/gui_2.py`, refactor the text viewer window loop to:
|
- [x] Task: Implement: In `src/gui_2.py`, refactor the text viewer window loop to: a91b8dc
|
||||||
- Use `MarkdownRenderer.render` if `text_type == "markdown"`.
|
- Use `MarkdownRenderer.render` if `text_type == "markdown"`. a91b8dc
|
||||||
- Use a cached `ImGuiColorTextEdit.TextEditor` if `text_type` matches a code language.
|
- Use a cached `ImGuiColorTextEdit.TextEditor` if `text_type` matches a code language. a91b8dc
|
||||||
- Fallback to `imgui.input_text_multiline` for plain text.
|
- Fallback to `imgui.input_text_multiline` for plain text. a91b8dc
|
||||||
- [ ] Task: Implement: Ensure the `TextEditor` instance is properly cached using a unique key for the text viewer to maintain state.
|
- [x] Task: Implement: Ensure the `TextEditor` instance is properly cached using a unique key for the text viewer to maintain state. a91b8dc
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Phase 2: Core Rendering Logic' (Protocol in workflow.md)
|
- [x] Task: Conductor - User Manual Verification 'Phase 2: Core Rendering Logic' (Protocol in workflow.md) a91b8dc
|
||||||
|
|
||||||
## Phase 3: UI Features (Copy, Line Numbers, Wrap)
|
## Phase 3: UI Features (Copy, Line Numbers, Wrap)
|
||||||
- [ ] Task: Write Tests: Update `tests/test_gui_text_viewer.py` to verify the copy-to-clipboard functionality and word wrap toggle.
|
- [x] Task: Write Tests: Update `tests/test_gui_text_viewer.py` to verify the copy-to-clipboard functionality and word wrap toggle. a91b8dc
|
||||||
- [ ] Task: Implement: Add a "Copy" button to the text viewer title bar or a small toolbar at the top of the window.
|
- [x] Task: Implement: Add a "Copy" button to the text viewer title bar or a small toolbar at the top of the window. a91b8dc
|
||||||
- [ ] Task: Implement: Add a "Word Wrap" checkbox inside the text viewer window.
|
- [x] Task: Implement: Add a "Word Wrap" checkbox inside the text viewer window. a91b8dc
|
||||||
- [ ] Task: Implement: Configure the `TextEditor` instance to show line numbers and be read-only.
|
- [x] Task: Implement: Configure the `TextEditor` instance to show line numbers and be read-only. a91b8dc
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Phase 3: UI Features' (Protocol in workflow.md)
|
- [x] Task: Conductor - User Manual Verification 'Phase 3: UI Features' (Protocol in workflow.md) a91b8dc
|
||||||
|
|
||||||
## Phase 4: Integration & Rollout
|
## Phase 4: Integration & Rollout
|
||||||
- [ ] 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.
|
- [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
|
||||||
- [ ] Task: Implement: Add "Markdown Preview" support for system prompt presets using the new text viewer logic.
|
- [x] Task: Implement: Add "Markdown Preview" support for system prompt presets using the new text viewer logic. 2826ad5
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Phase 4: Integration & Rollout' (Protocol in workflow.md)
|
- [x] Task: Conductor - User Manual Verification 'Phase 4: Integration & Rollout' (Protocol in workflow.md) 2826ad5
|
||||||
|
|||||||
@@ -1,26 +1,23 @@
|
|||||||
# Implementation Plan: Rich Thinking Trace Handling
|
# Implementation Plan: Rich Thinking Trace Handling
|
||||||
|
|
||||||
## Phase 1: Core Parsing & Model Update
|
## Status: COMPLETE (2026-03-14)
|
||||||
- [ ] 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)
|
|
||||||
|
|
||||||
## Phase 2: Persistence & History Integration
|
## Summary
|
||||||
- [ ] Task: Write Tests: Verify that `ProjectManager` correctly serializes and deserializes messages with thinking segments to/from TOML history files.
|
Implemented thinking trace parsing, model, persistence, and GUI rendering for AI responses containing `<thinking>`, `<thought>`, and `Thinking:` markers.
|
||||||
- [ ] 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)
|
|
||||||
|
|
||||||
## Phase 3: GUI Rendering - Comms & Discussion
|
## Files Created/Modified:
|
||||||
- [ ] Task: Write Tests: Verify the GUI rendering logic correctly handles messages with and without thinking segments.
|
- `src/thinking_parser.py` - Parser for thinking traces
|
||||||
- [ ] Task: Implement: Create a reusable `_render_thinking_trace` helper in `src/gui_2.py` using a collapsible header (e.g., `imgui.collapsing_header`).
|
- `src/models.py` - ThinkingSegment model
|
||||||
- [ ] Task: Implement: Integrate the thinking trace renderer into the **Comms History** panel in `src/gui_2.py`.
|
- `src/gui_2.py` - _render_thinking_trace helper + integration
|
||||||
- [ ] Task: Implement: Integrate the thinking trace renderer into the **Discussion Hub** message loop in `src/gui_2.py`.
|
- `tests/test_thinking_trace.py` - 7 parsing tests
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Phase 3: GUI Rendering - Comms & Discussion' (Protocol in workflow.md)
|
- `tests/test_thinking_persistence.py` - 4 persistence tests
|
||||||
|
- `tests/test_thinking_gui.py` - 4 GUI tests
|
||||||
|
|
||||||
## Phase 4: Final Polish & Theming
|
## Implementation Details:
|
||||||
- [ ] Task: Implement: Apply specialized styling (e.g., tinted background or italicized text) to expanded thinking traces to distinguish them from direct responses.
|
- **Parser**: Extracts thinking segments from `<thinking>`, `<thought>`, `Thinking:` markers
|
||||||
- [ ] Task: Implement: Ensure thinking trace headers show a "Calculating..." or "Monologue" indicator while an agent is active.
|
- **Model**: `ThinkingSegment` dataclass with content and marker fields
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Phase 4: Final Polish & Theming' (Protocol in workflow.md)
|
- **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
|
||||||
|
|||||||
20
config.toml
20
config.toml
@@ -23,23 +23,23 @@ active = "C:/projects/gencpp/gencpp_sloppy.toml"
|
|||||||
separate_message_panel = false
|
separate_message_panel = false
|
||||||
separate_response_panel = false
|
separate_response_panel = false
|
||||||
separate_tool_calls_panel = false
|
separate_tool_calls_panel = false
|
||||||
bg_shader_enabled = false
|
bg_shader_enabled = true
|
||||||
crt_filter_enabled = false
|
crt_filter_enabled = false
|
||||||
separate_task_dag = false
|
separate_task_dag = false
|
||||||
separate_usage_analytics = true
|
separate_usage_analytics = false
|
||||||
separate_tier1 = false
|
separate_tier1 = false
|
||||||
separate_tier2 = false
|
separate_tier2 = false
|
||||||
separate_tier3 = false
|
separate_tier3 = false
|
||||||
separate_tier4 = false
|
separate_tier4 = false
|
||||||
separate_external_tools = true
|
separate_external_tools = false
|
||||||
|
|
||||||
[gui.show_windows]
|
[gui.show_windows]
|
||||||
"Context Hub" = true
|
"Context Hub" = true
|
||||||
"Files & Media" = true
|
"Files & Media" = true
|
||||||
"AI Settings" = true
|
"AI Settings" = true
|
||||||
"MMA Dashboard" = true
|
"MMA Dashboard" = false
|
||||||
"Task DAG" = false
|
"Task DAG" = false
|
||||||
"Usage Analytics" = true
|
"Usage Analytics" = false
|
||||||
"Tier 1" = false
|
"Tier 1" = false
|
||||||
"Tier 2" = false
|
"Tier 2" = false
|
||||||
"Tier 3" = false
|
"Tier 3" = false
|
||||||
@@ -51,21 +51,21 @@ separate_external_tools = true
|
|||||||
"Discussion Hub" = true
|
"Discussion Hub" = true
|
||||||
"Operations Hub" = true
|
"Operations Hub" = true
|
||||||
Message = false
|
Message = false
|
||||||
Response = true
|
Response = false
|
||||||
"Tool Calls" = false
|
"Tool Calls" = false
|
||||||
Theme = true
|
Theme = true
|
||||||
"Log Management" = true
|
"Log Management" = false
|
||||||
Diagnostics = false
|
Diagnostics = false
|
||||||
"External Tools" = false
|
"External Tools" = false
|
||||||
"Shader Editor" = true
|
"Shader Editor" = false
|
||||||
|
|
||||||
[theme]
|
[theme]
|
||||||
palette = "Nord Dark"
|
palette = "Nord Dark"
|
||||||
font_path = "C:/projects/manual_slop/assets/fonts/MapleMono-Regular.ttf"
|
font_path = "C:/projects/manual_slop/assets/fonts/MapleMono-Regular.ttf"
|
||||||
font_size = 18.0
|
font_size = 18.0
|
||||||
scale = 1.0
|
scale = 1.0
|
||||||
transparency = 0.4399999976158142
|
transparency = 1.0
|
||||||
child_transparency = 0.5099999904632568
|
child_transparency = 1.0
|
||||||
|
|
||||||
[mma]
|
[mma]
|
||||||
max_workers = 4
|
max_workers = 4
|
||||||
|
|||||||
@@ -49,8 +49,8 @@ Size=716,455
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Response]
|
[Window][Response]
|
||||||
Pos=2437,925
|
Pos=2258,1377
|
||||||
Size=1111,773
|
Size=1102,575
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Tool Calls]
|
[Window][Tool Calls]
|
||||||
@@ -74,8 +74,8 @@ Collapsed=0
|
|||||||
DockId=0xAFC85805,2
|
DockId=0xAFC85805,2
|
||||||
|
|
||||||
[Window][Theme]
|
[Window][Theme]
|
||||||
Pos=0,543
|
Pos=0,1423
|
||||||
Size=387,737
|
Size=579,737
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000002,2
|
DockId=0x00000002,2
|
||||||
|
|
||||||
@@ -91,8 +91,8 @@ Collapsed=0
|
|||||||
DockId=0x00000010,2
|
DockId=0x00000010,2
|
||||||
|
|
||||||
[Window][Context Hub]
|
[Window][Context Hub]
|
||||||
Pos=0,543
|
Pos=0,1423
|
||||||
Size=387,737
|
Size=579,737
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000002,1
|
DockId=0x00000002,1
|
||||||
|
|
||||||
@@ -103,26 +103,26 @@ Collapsed=0
|
|||||||
DockId=0x0000000D,0
|
DockId=0x0000000D,0
|
||||||
|
|
||||||
[Window][Discussion Hub]
|
[Window][Discussion Hub]
|
||||||
Pos=1169,26
|
Pos=2230,26
|
||||||
Size=950,1254
|
Size=1610,2134
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000013,0
|
DockId=0x00000013,0
|
||||||
|
|
||||||
[Window][Operations Hub]
|
[Window][Operations Hub]
|
||||||
Pos=389,26
|
Pos=581,26
|
||||||
Size=778,1254
|
Size=1647,2134
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000005,0
|
DockId=0x00000005,0
|
||||||
|
|
||||||
[Window][Files & Media]
|
[Window][Files & Media]
|
||||||
Pos=0,543
|
Pos=0,1423
|
||||||
Size=387,737
|
Size=579,737
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000002,0
|
DockId=0x00000002,0
|
||||||
|
|
||||||
[Window][AI Settings]
|
[Window][AI Settings]
|
||||||
Pos=0,26
|
Pos=0,26
|
||||||
Size=387,515
|
Size=579,1395
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000001,0
|
DockId=0x00000001,0
|
||||||
|
|
||||||
@@ -132,16 +132,16 @@ Size=416,325
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][MMA Dashboard]
|
[Window][MMA Dashboard]
|
||||||
Pos=2121,26
|
Pos=3360,26
|
||||||
Size=653,1254
|
Size=480,2134
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000010,0
|
DockId=0x00000010,0
|
||||||
|
|
||||||
[Window][Log Management]
|
[Window][Log Management]
|
||||||
Pos=2121,26
|
Pos=3360,26
|
||||||
Size=653,1254
|
Size=480,2134
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000010,1
|
DockId=0x00000010,0
|
||||||
|
|
||||||
[Window][Track Proposal]
|
[Window][Track Proposal]
|
||||||
Pos=709,326
|
Pos=709,326
|
||||||
@@ -167,7 +167,7 @@ Collapsed=0
|
|||||||
Pos=2822,1717
|
Pos=2822,1717
|
||||||
Size=1018,420
|
Size=1018,420
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000011,0
|
DockId=0x0000000C,0
|
||||||
|
|
||||||
[Window][Approve PowerShell Command]
|
[Window][Approve PowerShell Command]
|
||||||
Pos=649,435
|
Pos=649,435
|
||||||
@@ -175,7 +175,7 @@ Size=381,329
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Last Script Output]
|
[Window][Last Script Output]
|
||||||
Pos=2810,265
|
Pos=927,1365
|
||||||
Size=800,562
|
Size=800,562
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
@@ -220,7 +220,7 @@ Size=900,700
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Text Viewer - text]
|
[Window][Text Viewer - text]
|
||||||
Pos=60,60
|
Pos=1297,550
|
||||||
Size=900,700
|
Size=900,700
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
@@ -330,9 +330,10 @@ Size=967,499
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Usage Analytics]
|
[Window][Usage Analytics]
|
||||||
Pos=1627,680
|
Pos=1739,1107
|
||||||
Size=480,343
|
Size=586,269
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
DockId=0x0000000F,0
|
||||||
|
|
||||||
[Window][Tool Preset Manager]
|
[Window][Tool Preset Manager]
|
||||||
Pos=1301,302
|
Pos=1301,302
|
||||||
@@ -350,7 +351,7 @@ Size=1000,800
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][External Tools]
|
[Window][External Tools]
|
||||||
Pos=1968,516
|
Pos=531,376
|
||||||
Size=616,409
|
Size=616,409
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
@@ -365,7 +366,7 @@ Size=900,700
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Text Viewer - Entry #4]
|
[Window][Text Viewer - Entry #4]
|
||||||
Pos=1127,922
|
Pos=1247,1182
|
||||||
Size=900,700
|
Size=900,700
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
@@ -380,8 +381,8 @@ Size=900,700
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Shader Editor]
|
[Window][Shader Editor]
|
||||||
Pos=998,497
|
Pos=457,710
|
||||||
Size=493,369
|
Size=493,252
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Table][0xFB6E3870,4]
|
[Table][0xFB6E3870,4]
|
||||||
@@ -497,21 +498,23 @@ Column 1 Weight=1.0000
|
|||||||
DockNode ID=0x00000008 Pos=3125,170 Size=593,1157 Split=Y
|
DockNode ID=0x00000008 Pos=3125,170 Size=593,1157 Split=Y
|
||||||
DockNode ID=0x00000009 Parent=0x00000008 SizeRef=1029,147 Selected=0x0469CA7A
|
DockNode ID=0x00000009 Parent=0x00000008 SizeRef=1029,147 Selected=0x0469CA7A
|
||||||
DockNode ID=0x0000000A Parent=0x00000008 SizeRef=1029,145 Selected=0xDF822E02
|
DockNode ID=0x0000000A Parent=0x00000008 SizeRef=1029,145 Selected=0xDF822E02
|
||||||
DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,26 Size=2774,1254 Split=X
|
DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,26 Size=3840,2134 Split=X
|
||||||
DockNode ID=0x00000003 Parent=0xAFC85805 SizeRef=1980,1183 Split=X
|
DockNode ID=0x00000003 Parent=0xAFC85805 SizeRef=3358,1183 Split=X
|
||||||
DockNode ID=0x0000000B Parent=0x00000003 SizeRef=404,1186 Split=X Selected=0xF4139CA2
|
DockNode ID=0x0000000B Parent=0x00000003 SizeRef=404,1186 Split=X Selected=0xF4139CA2
|
||||||
DockNode ID=0x00000007 Parent=0x0000000B SizeRef=680,858 Split=Y Selected=0x8CA2375C
|
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=0x00000001 Parent=0x00000007 SizeRef=824,525 CentralNode=1 Selected=0x7BD57D6A
|
||||||
DockNode ID=0x00000002 Parent=0x00000007 SizeRef=824,737 Selected=0x8CA2375C
|
DockNode ID=0x00000002 Parent=0x00000007 SizeRef=824,737 Selected=0x8CA2375C
|
||||||
DockNode ID=0x0000000E Parent=0x0000000B SizeRef=1730,858 Split=X Selected=0x418C7449
|
DockNode ID=0x0000000E Parent=0x0000000B SizeRef=3259,858 Split=X Selected=0x418C7449
|
||||||
DockNode ID=0x00000012 Parent=0x0000000E SizeRef=778,402 Split=Y 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=0x00000005 Parent=0x00000012 SizeRef=876,1749 Selected=0x418C7449
|
||||||
DockNode ID=0x00000006 Parent=0x00000012 SizeRef=876,362 Selected=0x1D56B311
|
DockNode ID=0x00000006 Parent=0x00000012 SizeRef=876,362 Selected=0x1D56B311
|
||||||
DockNode ID=0x00000013 Parent=0x0000000E SizeRef=950,402 Selected=0x6F2B5B04
|
DockNode ID=0x00000013 Parent=0x0000000E SizeRef=1610,402 Selected=0x6F2B5B04
|
||||||
DockNode ID=0x0000000D Parent=0x00000003 SizeRef=435,1186 Selected=0x363E93D6
|
DockNode ID=0x0000000D Parent=0x00000003 SizeRef=435,1186 Selected=0x363E93D6
|
||||||
DockNode ID=0x00000004 Parent=0xAFC85805 SizeRef=653,1183 Split=Y Selected=0x3AEC3498
|
DockNode ID=0x00000004 Parent=0xAFC85805 SizeRef=480,1183 Split=Y Selected=0x3AEC3498
|
||||||
DockNode ID=0x00000010 Parent=0x00000004 SizeRef=1199,1689 Selected=0x2C0206CE
|
DockNode ID=0x00000010 Parent=0x00000004 SizeRef=1199,1689 Selected=0x2C0206CE
|
||||||
DockNode ID=0x00000011 Parent=0x00000004 SizeRef=1199,420 Selected=0xDEB547B6
|
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
|
||||||
|
|
||||||
;;;<<<Layout_655921752_Default>>>;;;
|
;;;<<<Layout_655921752_Default>>>;;;
|
||||||
;;;<<<HelloImGui_Misc>>>;;;
|
;;;<<<HelloImGui_Misc>>>;;;
|
||||||
|
|||||||
@@ -225,6 +225,9 @@ class HookHandler(BaseHTTPRequestHandler):
|
|||||||
for key, attr in gettable.items():
|
for key, attr in gettable.items():
|
||||||
val = _get_app_attr(app, attr, None)
|
val = _get_app_attr(app, attr, None)
|
||||||
result[key] = _serialize_for_api(val)
|
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()
|
finally: event.set()
|
||||||
lock = _get_app_attr(app, "_pending_gui_tasks_lock")
|
lock = _get_app_attr(app, "_pending_gui_tasks_lock")
|
||||||
tasks = _get_app_attr(app, "_pending_gui_tasks")
|
tasks = _get_app_attr(app, "_pending_gui_tasks")
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ from src import project_manager
|
|||||||
from src import performance_monitor
|
from src import performance_monitor
|
||||||
from src import models
|
from src import models
|
||||||
from src import presets
|
from src import presets
|
||||||
|
from src import thinking_parser
|
||||||
from src.file_cache import ASTParser
|
from src.file_cache import ASTParser
|
||||||
from src import ai_client
|
from src import ai_client
|
||||||
from src import shell_runner
|
from src import shell_runner
|
||||||
@@ -242,6 +243,8 @@ class AppController:
|
|||||||
self.ai_status: str = 'idle'
|
self.ai_status: str = 'idle'
|
||||||
self.ai_response: str = ''
|
self.ai_response: str = ''
|
||||||
self.last_md: 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_md_path: Optional[Path] = None
|
||||||
self.last_file_items: List[Any] = []
|
self.last_file_items: List[Any] = []
|
||||||
self.send_thread: Optional[threading.Thread] = None
|
self.send_thread: Optional[threading.Thread] = None
|
||||||
@@ -251,6 +254,7 @@ class AppController:
|
|||||||
self.show_text_viewer: bool = False
|
self.show_text_viewer: bool = False
|
||||||
self.text_viewer_title: str = ''
|
self.text_viewer_title: str = ''
|
||||||
self.text_viewer_content: str = ''
|
self.text_viewer_content: str = ''
|
||||||
|
self.text_viewer_type: str = 'text'
|
||||||
self._pending_comms: List[Dict[str, Any]] = []
|
self._pending_comms: List[Dict[str, Any]] = []
|
||||||
self._pending_tool_calls: List[Dict[str, Any]] = []
|
self._pending_tool_calls: List[Dict[str, Any]] = []
|
||||||
self._pending_history_adds: List[Dict[str, Any]] = []
|
self._pending_history_adds: List[Dict[str, Any]] = []
|
||||||
@@ -374,7 +378,10 @@ class AppController:
|
|||||||
'ui_separate_tier1': 'ui_separate_tier1',
|
'ui_separate_tier1': 'ui_separate_tier1',
|
||||||
'ui_separate_tier2': 'ui_separate_tier2',
|
'ui_separate_tier2': 'ui_separate_tier2',
|
||||||
'ui_separate_tier3': 'ui_separate_tier3',
|
'ui_separate_tier3': 'ui_separate_tier3',
|
||||||
'ui_separate_tier4': 'ui_separate_tier4'
|
'ui_separate_tier4': 'ui_separate_tier4',
|
||||||
|
'show_text_viewer': 'show_text_viewer',
|
||||||
|
'text_viewer_title': 'text_viewer_title',
|
||||||
|
'text_viewer_type': 'text_viewer_type'
|
||||||
}
|
}
|
||||||
self._gettable_fields = dict(self._settable_fields)
|
self._gettable_fields = dict(self._settable_fields)
|
||||||
self._gettable_fields.update({
|
self._gettable_fields.update({
|
||||||
@@ -421,7 +428,10 @@ class AppController:
|
|||||||
'ui_separate_tier1': 'ui_separate_tier1',
|
'ui_separate_tier1': 'ui_separate_tier1',
|
||||||
'ui_separate_tier2': 'ui_separate_tier2',
|
'ui_separate_tier2': 'ui_separate_tier2',
|
||||||
'ui_separate_tier3': 'ui_separate_tier3',
|
'ui_separate_tier3': 'ui_separate_tier3',
|
||||||
'ui_separate_tier4': 'ui_separate_tier4'
|
'ui_separate_tier4': 'ui_separate_tier4',
|
||||||
|
'show_text_viewer': 'show_text_viewer',
|
||||||
|
'text_viewer_title': 'text_viewer_title',
|
||||||
|
'text_viewer_type': 'text_viewer_type'
|
||||||
})
|
})
|
||||||
self.perf_monitor = performance_monitor.get_monitor()
|
self.perf_monitor = performance_monitor.get_monitor()
|
||||||
self._perf_profiling_enabled = False
|
self._perf_profiling_enabled = False
|
||||||
@@ -610,16 +620,6 @@ class AppController:
|
|||||||
self._token_stats_dirty = True
|
self._token_stats_dirty = True
|
||||||
if not is_streaming:
|
if not is_streaming:
|
||||||
self._autofocus_response_tab = True
|
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"):
|
elif action in ("mma_stream", "mma_stream_append"):
|
||||||
# Some events might have these at top level, some in a 'payload' dict
|
# 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")
|
stream_id = task.get("stream_id") or task.get("payload", {}).get("stream_id")
|
||||||
@@ -1467,9 +1467,22 @@ class AppController:
|
|||||||
|
|
||||||
if kind == "response" and "usage" in payload:
|
if kind == "response" and "usage" in payload:
|
||||||
u = payload["usage"]
|
u = payload["usage"]
|
||||||
for k in ["input_tokens", "output_tokens", "cache_read_input_tokens", "cache_creation_input_tokens", "total_tokens"]:
|
inp = u.get("input_tokens", u.get("prompt_tokens", 0))
|
||||||
if k in u:
|
out = u.get("output_tokens", u.get("completion_tokens", 0))
|
||||||
self.session_usage[k] += u.get(k, 0) or 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
|
||||||
input_t = u.get("input_tokens", 0)
|
input_t = u.get("input_tokens", 0)
|
||||||
output_t = u.get("output_tokens", 0)
|
output_t = u.get("output_tokens", 0)
|
||||||
model = payload.get("model", "unknown")
|
model = payload.get("model", "unknown")
|
||||||
@@ -1490,22 +1503,42 @@ class AppController:
|
|||||||
"ts": entry.get("ts", project_manager.now_ts())
|
"ts": entry.get("ts", project_manager.now_ts())
|
||||||
})
|
})
|
||||||
|
|
||||||
if kind in ("tool_result", "tool_call"):
|
if kind == "response":
|
||||||
role = "Tool" if kind == "tool_result" else "Vendor API"
|
if self.ui_auto_add_history:
|
||||||
content = ""
|
role = payload.get("role", "AI")
|
||||||
if kind == "tool_result":
|
text_content = payload.get("text", "")
|
||||||
content = payload.get("output", "")
|
if text_content.strip():
|
||||||
else:
|
segments, parsed_response = thinking_parser.parse_thinking_trace(text_content)
|
||||||
content = payload.get("script") or payload.get("args") or payload.get("message", "")
|
entry_obj = {
|
||||||
if isinstance(content, dict):
|
|
||||||
content = json.dumps(content, indent=1)
|
|
||||||
with self._pending_history_adds_lock:
|
|
||||||
self._pending_history_adds.append({
|
|
||||||
"role": role,
|
"role": role,
|
||||||
"content": f"[{kind.upper().replace('_', ' ')}]\n{content}",
|
"content": parsed_response.strip() if parsed_response else "",
|
||||||
"collapsed": True,
|
"collapsed": True,
|
||||||
"ts": entry.get("ts", project_manager.now_ts())
|
"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":
|
||||||
|
content = payload.get("output", "")
|
||||||
|
else:
|
||||||
|
content = payload.get("script") or payload.get("args") or payload.get("message", "")
|
||||||
|
if isinstance(content, dict):
|
||||||
|
content = json.dumps(content, indent=1)
|
||||||
|
with self._pending_history_adds_lock:
|
||||||
|
self._pending_history_adds.append({
|
||||||
|
"role": role,
|
||||||
|
"content": f"[{kind.upper().replace('_', ' ')}]\n{content}",
|
||||||
|
"collapsed": True,
|
||||||
|
"ts": entry.get("ts", project_manager.now_ts())
|
||||||
|
})
|
||||||
if kind == "history_add":
|
if kind == "history_add":
|
||||||
payload = entry.get("payload", {})
|
payload = entry.get("payload", {})
|
||||||
with self._pending_history_adds_lock:
|
with self._pending_history_adds_lock:
|
||||||
@@ -2485,6 +2518,11 @@ class AppController:
|
|||||||
# Build discussion history text separately
|
# Build discussion history text separately
|
||||||
history = flat.get("discussion", {}).get("history", [])
|
history = flat.get("discussion", {}).get("history", [])
|
||||||
discussion_text = aggregate.build_discussion_text(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
|
return full_md, path, file_items, stable_md, discussion_text
|
||||||
|
|
||||||
def _cb_plan_epic(self) -> None:
|
def _cb_plan_epic(self) -> None:
|
||||||
|
|||||||
458
src/gui_2.py
458
src/gui_2.py
@@ -28,6 +28,8 @@ from src import app_controller
|
|||||||
from src import mcp_client
|
from src import mcp_client
|
||||||
from src import markdown_helper
|
from src import markdown_helper
|
||||||
from src import bg_shader
|
from src import bg_shader
|
||||||
|
from src import thinking_parser
|
||||||
|
from src import thinking_parser
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
@@ -38,8 +40,7 @@ else:
|
|||||||
win32con = None
|
win32con = None
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from imgui_bundle import imgui, hello_imgui, immapp, imgui_node_editor as ed
|
from imgui_bundle import imgui, hello_imgui, immapp, imgui_node_editor as ed, imgui_color_text_edit as ced
|
||||||
from src.shader_manager import BlurPipeline
|
|
||||||
|
|
||||||
PROVIDERS: list[str] = ["gemini", "anthropic", "gemini_cli", "deepseek", "minimax"]
|
PROVIDERS: list[str] = ["gemini", "anthropic", "gemini_cli", "deepseek", "minimax"]
|
||||||
COMMS_CLAMP_CHARS: int = 300
|
COMMS_CLAMP_CHARS: int = 300
|
||||||
@@ -106,11 +107,29 @@ class App:
|
|||||||
self.controller.init_state()
|
self.controller.init_state()
|
||||||
self.show_windows.setdefault("Diagnostics", False)
|
self.show_windows.setdefault("Diagnostics", False)
|
||||||
self.controller.start_services(self)
|
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_preset_manager_window = False
|
||||||
self.show_tool_preset_manager_window = False
|
self.show_tool_preset_manager_window = False
|
||||||
self.show_persona_editor_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_tool_preset = ""
|
||||||
self.ui_active_bias_profile = ""
|
self.ui_active_bias_profile = ""
|
||||||
|
self.ui_active_context_preset = ""
|
||||||
self.ui_active_persona = ""
|
self.ui_active_persona = ""
|
||||||
self._editing_persona_name = ""
|
self._editing_persona_name = ""
|
||||||
self._editing_persona_description = ""
|
self._editing_persona_description = ""
|
||||||
@@ -122,6 +141,7 @@ class App:
|
|||||||
self._editing_persona_max_tokens = 4096
|
self._editing_persona_max_tokens = 4096
|
||||||
self._editing_persona_tool_preset_id = ""
|
self._editing_persona_tool_preset_id = ""
|
||||||
self._editing_persona_bias_profile_id = ""
|
self._editing_persona_bias_profile_id = ""
|
||||||
|
self._editing_persona_context_preset_id = ""
|
||||||
self._editing_persona_preferred_models_list: list[dict] = []
|
self._editing_persona_preferred_models_list: list[dict] = []
|
||||||
self._editing_persona_scope = "project"
|
self._editing_persona_scope = "project"
|
||||||
self._editing_persona_is_new = True
|
self._editing_persona_is_new = True
|
||||||
@@ -194,6 +214,7 @@ class App:
|
|||||||
self.show_windows.setdefault("Tier 4: QA", False)
|
self.show_windows.setdefault("Tier 4: QA", False)
|
||||||
self.show_windows.setdefault('External Tools', False)
|
self.show_windows.setdefault('External Tools', False)
|
||||||
self.show_windows.setdefault('Shader Editor', 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.ui_multi_viewport = gui_cfg.get("multi_viewport", False)
|
||||||
self.layout_presets = self.config.get("layout_presets", {})
|
self.layout_presets = self.config.get("layout_presets", {})
|
||||||
self._new_preset_name = ""
|
self._new_preset_name = ""
|
||||||
@@ -213,35 +234,8 @@ class App:
|
|||||||
self.ui_tool_filter_category = "All"
|
self.ui_tool_filter_category = "All"
|
||||||
self.ui_discussion_split_h = 300.0
|
self.ui_discussion_split_h = 300.0
|
||||||
self.shader_uniforms = {'crt': 1.0, 'scanline': 0.5, 'bloom': 0.8}
|
self.shader_uniforms = {'crt': 1.0, 'scanline': 0.5, 'bloom': 0.8}
|
||||||
self.ui_frosted_glass_enabled = False
|
self.shader_uniforms = {'crt': 1.0, 'scanline': 0.5, 'bloom': 0.8}
|
||||||
self._blur_pipeline: BlurPipeline | None = None
|
self.ui_new_context_preset_name = ""
|
||||||
self.ui_frosted_glass_enabled = False
|
|
||||||
self._blur_pipeline = None
|
|
||||||
|
|
||||||
def _pre_render_blur(self):
|
|
||||||
if not self.ui_frosted_glass_enabled:
|
|
||||||
return
|
|
||||||
if not self._blur_pipeline:
|
|
||||||
return
|
|
||||||
ws = imgui.get_io().display_size
|
|
||||||
fb_scale = imgui.get_io().display_framebuffer_scale.x
|
|
||||||
import time
|
|
||||||
t = time.time()
|
|
||||||
self._blur_pipeline.prepare_global_blur(int(ws.x), int(ws.y), t, fb_scale)
|
|
||||||
|
|
||||||
def _render_custom_background(self):
|
|
||||||
return # DISABLED - imgui-bundle can't sample OpenGL textures
|
|
||||||
|
|
||||||
def _draw_blurred_rect(self, dl, p_min, p_max, tex_id, uv_min, uv_max):
|
|
||||||
import OpenGL.GL as gl
|
|
||||||
gl.glEnable(gl.GL_BLEND)
|
|
||||||
gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)
|
|
||||||
imgui.push_texture_id(tex_id)
|
|
||||||
dl.add_image_quad(p_min, p_max, uv_min, uv_max, imgui.get_color_u32((1, 1, 1, 1)))
|
|
||||||
imgui.pop_texture_id()
|
|
||||||
gl.glDisable(gl.GL_BLEND)
|
|
||||||
|
|
||||||
def _handle_approve_tool(self, user_data=None) -> None:
|
|
||||||
"""UI-level wrapper for approving a pending tool execution ask."""
|
"""UI-level wrapper for approving a pending tool execution ask."""
|
||||||
self._handle_approve_ask()
|
self._handle_approve_ask()
|
||||||
|
|
||||||
@@ -299,6 +293,54 @@ class App:
|
|||||||
pass
|
pass
|
||||||
self.controller.shutdown()
|
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:
|
def _test_callback_func_write_to_file(self, data: str) -> None:
|
||||||
"""A dummy function that a custom_callback would execute for testing."""
|
"""A dummy function that a custom_callback would execute for testing."""
|
||||||
# Ensure the directory exists if running from a different cwd
|
# Ensure the directory exists if running from a different cwd
|
||||||
@@ -307,8 +349,9 @@ class App:
|
|||||||
f.write(data)
|
f.write(data)
|
||||||
# ---------------------------------------------------------------- helpers
|
# ---------------------------------------------------------------- helpers
|
||||||
|
|
||||||
def _render_text_viewer(self, label: str, content: str) -> None:
|
def _render_text_viewer(self, label: str, content: str, text_type: str = 'text', force_open: bool = False) -> None:
|
||||||
if imgui.button("[+]##" + str(id(content))):
|
self.text_viewer_type = text_type
|
||||||
|
if imgui.button("[+]##" + str(id(content))) or force_open:
|
||||||
self.show_text_viewer = True
|
self.show_text_viewer = True
|
||||||
self.text_viewer_title = label
|
self.text_viewer_title = label
|
||||||
self.text_viewer_content = content
|
self.text_viewer_content = content
|
||||||
@@ -318,6 +361,7 @@ class App:
|
|||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
if imgui.button("[+]##" + label + id_suffix):
|
if imgui.button("[+]##" + label + id_suffix):
|
||||||
self.show_text_viewer = True
|
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_title = label
|
||||||
self.text_viewer_content = content
|
self.text_viewer_content = content
|
||||||
|
|
||||||
@@ -332,21 +376,57 @@ class App:
|
|||||||
if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80))
|
if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80))
|
||||||
|
|
||||||
if len(content) > COMMS_CLAMP_CHARS:
|
if len(content) > COMMS_CLAMP_CHARS:
|
||||||
imgui.begin_child(f"heavy_text_child_{label}_{id_suffix}", imgui.ImVec2(0, 80), True)
|
|
||||||
if is_md:
|
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)
|
markdown_helper.render(content, context_id=ctx_id)
|
||||||
|
imgui.end_child()
|
||||||
else:
|
else:
|
||||||
markdown_helper.render_code(content, context_id=ctx_id)
|
imgui.input_text_multiline(f"##heavy_text_input_{label}_{id_suffix}", content, imgui.ImVec2(-1, 180), imgui.InputTextFlags_.read_only)
|
||||||
imgui.end_child()
|
|
||||||
else:
|
else:
|
||||||
if is_md:
|
if is_md:
|
||||||
markdown_helper.render(content, context_id=ctx_id)
|
markdown_helper.render(content, context_id=ctx_id)
|
||||||
else:
|
else:
|
||||||
markdown_helper.render_code(content, context_id=ctx_id)
|
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)
|
||||||
|
|
||||||
if is_nerv: imgui.pop_style_color()
|
if is_nerv: imgui.pop_style_color()
|
||||||
# ---------------------------------------------------------------- gui
|
# ---------------------------------------------------------------- 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:
|
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)))
|
imgui.push_id(label + str(hash(value)))
|
||||||
@@ -465,8 +545,6 @@ class App:
|
|||||||
exp, opened = imgui.begin('Shader Editor', self.show_windows['Shader Editor'])
|
exp, opened = imgui.begin('Shader Editor', self.show_windows['Shader Editor'])
|
||||||
self.show_windows['Shader Editor'] = bool(opened)
|
self.show_windows['Shader Editor'] = bool(opened)
|
||||||
if exp:
|
if exp:
|
||||||
_, self.ui_frosted_glass_enabled = imgui.checkbox('Frosted Glass', self.ui_frosted_glass_enabled)
|
|
||||||
imgui.separator()
|
|
||||||
changed_crt, self.shader_uniforms['crt'] = imgui.slider_float('CRT Curvature', self.shader_uniforms['crt'], 0.0, 2.0)
|
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_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)
|
changed_bloom, self.shader_uniforms['bloom'] = imgui.slider_float('Bloom Threshold', self.shader_uniforms['bloom'], 0.0, 1.0)
|
||||||
@@ -570,6 +648,9 @@ class App:
|
|||||||
if imgui.begin_tab_item('Paths')[0]:
|
if imgui.begin_tab_item('Paths')[0]:
|
||||||
self._render_paths_panel()
|
self._render_paths_panel()
|
||||||
imgui.end_tab_item()
|
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_tab_bar()
|
||||||
imgui.end()
|
imgui.end()
|
||||||
if self.show_windows.get("Files & Media", False):
|
if self.show_windows.get("Files & Media", False):
|
||||||
@@ -777,6 +858,8 @@ class App:
|
|||||||
if self.show_windows.get("Diagnostics", False):
|
if self.show_windows.get("Diagnostics", False):
|
||||||
self._render_diagnostics_panel()
|
self._render_diagnostics_panel()
|
||||||
|
|
||||||
|
self._render_session_hub()
|
||||||
|
|
||||||
self.perf_monitor.end_frame()
|
self.perf_monitor.end_frame()
|
||||||
# ---- Modals / Popups
|
# ---- Modals / Popups
|
||||||
with self._pending_dialog_lock:
|
with self._pending_dialog_lock:
|
||||||
@@ -989,14 +1072,42 @@ class App:
|
|||||||
expanded, opened = imgui.begin(f"Text Viewer - {self.text_viewer_title}", self.show_text_viewer)
|
expanded, opened = imgui.begin(f"Text Viewer - {self.text_viewer_title}", self.show_text_viewer)
|
||||||
self.show_text_viewer = bool(opened)
|
self.show_text_viewer = bool(opened)
|
||||||
if expanded:
|
if expanded:
|
||||||
if self.ui_word_wrap:
|
# Toolbar
|
||||||
imgui.begin_child("tv_wrap", imgui.ImVec2(-1, -1), False)
|
if imgui.button("Copy"):
|
||||||
imgui.push_text_wrap_pos(imgui.get_content_region_avail().x)
|
imgui.set_clipboard_text(self.text_viewer_content)
|
||||||
imgui.text(self.text_viewer_content)
|
imgui.same_line()
|
||||||
imgui.pop_text_wrap_pos()
|
_, 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()
|
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:
|
else:
|
||||||
imgui.input_text_multiline("##tv_c", self.text_viewer_content, imgui.ImVec2(-1, -1), imgui.InputTextFlags_.read_only)
|
if self.text_viewer_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)
|
||||||
|
imgui.pop_text_wrap_pos()
|
||||||
|
imgui.end_child()
|
||||||
|
else:
|
||||||
|
imgui.input_text_multiline("##tv_c", self.text_viewer_content, imgui.ImVec2(-1, -1), imgui.InputTextFlags_.read_only)
|
||||||
imgui.end()
|
imgui.end()
|
||||||
# Inject File Modal
|
# Inject File Modal
|
||||||
if getattr(self, "show_inject_modal", False):
|
if getattr(self, "show_inject_modal", False):
|
||||||
@@ -1130,16 +1241,14 @@ class App:
|
|||||||
imgui.separator()
|
imgui.separator()
|
||||||
imgui.text("Prompt Content:")
|
imgui.text("Prompt Content:")
|
||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
if imgui.button("MD Preview" if not self._prompt_md_preview else "Edit Mode"):
|
if imgui.button("Pop out MD Preview"):
|
||||||
self._prompt_md_preview = not self._prompt_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
|
||||||
|
|
||||||
rem_y = imgui.get_content_region_avail().y
|
rem_y = imgui.get_content_region_avail().y
|
||||||
if self._prompt_md_preview:
|
_, self._editing_preset_system_prompt = imgui.input_text_multiline("##pcont", self._editing_preset_system_prompt, imgui.ImVec2(-1, rem_y))
|
||||||
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()
|
imgui.end_child()
|
||||||
|
|
||||||
# Footer Buttons
|
# Footer Buttons
|
||||||
@@ -1377,6 +1486,7 @@ class App:
|
|||||||
if imgui.button("New Persona", imgui.ImVec2(-1, 0)):
|
if imgui.button("New Persona", imgui.ImVec2(-1, 0)):
|
||||||
self._editing_persona_name = ""; self._editing_persona_system_prompt = ""
|
self._editing_persona_name = ""; self._editing_persona_system_prompt = ""
|
||||||
self._editing_persona_tool_preset_id = ""; self._editing_persona_bias_profile_id = ""
|
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_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
|
self._editing_persona_scope = "project"; self._editing_persona_is_new = True
|
||||||
imgui.separator()
|
imgui.separator()
|
||||||
@@ -1385,6 +1495,7 @@ 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]:
|
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 ""
|
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_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 []
|
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
|
self._editing_persona_scope = self.controller.persona_manager.get_persona_scope(p.name); self._editing_persona_is_new = False
|
||||||
imgui.end_child()
|
imgui.end_child()
|
||||||
@@ -1470,6 +1581,10 @@ class App:
|
|||||||
imgui.table_next_column(); imgui.text("Bias Profile:"); bn = ["None"] + sorted(self.controller.bias_profiles.keys())
|
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
|
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.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()
|
imgui.end_table()
|
||||||
|
|
||||||
if imgui.button("Manage Tools & Biases", imgui.ImVec2(-1, 0)): self.show_tool_preset_manager_window = True
|
if imgui.button("Manage Tools & Biases", imgui.ImVec2(-1, 0)): self.show_tool_preset_manager_window = True
|
||||||
@@ -1497,7 +1612,7 @@ class App:
|
|||||||
if imgui.button("Save##pers", imgui.ImVec2(100, 0)):
|
if imgui.button("Save##pers", imgui.ImVec2(100, 0)):
|
||||||
if self._editing_persona_name.strip():
|
if self._editing_persona_name.strip():
|
||||||
try:
|
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, 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, context_preset=self._editing_persona_context_preset_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}"
|
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}"
|
except Exception as e: self.ai_status = f"Error: {e}"
|
||||||
else: self.ai_status = "Name required"
|
else: self.ai_status = "Name required"
|
||||||
@@ -1655,6 +1770,30 @@ class App:
|
|||||||
self.ai_status = "paths reset to defaults"
|
self.ai_status = "paths reset to defaults"
|
||||||
|
|
||||||
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_paths_panel")
|
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:
|
def _render_track_proposal_modal(self) -> None:
|
||||||
if self._show_track_proposal_modal:
|
if self._show_track_proposal_modal:
|
||||||
imgui.open_popup("Track Proposal")
|
imgui.open_popup("Track Proposal")
|
||||||
@@ -1959,6 +2098,29 @@ class App:
|
|||||||
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_diagnostics_panel")
|
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_diagnostics_panel")
|
||||||
imgui.end()
|
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:
|
def _render_markdown_test(self) -> None:
|
||||||
imgui.text("Markdown Test Panel")
|
imgui.text("Markdown Test Panel")
|
||||||
imgui.separator()
|
imgui.separator()
|
||||||
@@ -2269,6 +2431,22 @@ def hello():
|
|||||||
if ts_str:
|
if ts_str:
|
||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
imgui.text_colored(vec4(120, 120, 100), str(ts_str))
|
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:
|
if collapsed:
|
||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
if imgui.button("Ins"):
|
if imgui.button("Ins"):
|
||||||
@@ -2281,52 +2459,62 @@ def hello():
|
|||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
preview = entry["content"].replace("\\n", " ")[:60]
|
preview = entry["content"].replace("\\n", " ")[:60]
|
||||||
if len(entry["content"]) > 60: preview += "..."
|
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)
|
imgui.text_colored(vec4(160, 160, 150), preview)
|
||||||
if not collapsed:
|
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:
|
if read_mode:
|
||||||
content = entry["content"]
|
content = entry["content"]
|
||||||
pattern = re.compile(r"\[Definition: (.*?) from (.*?) \(line (\d+)\)\](\s+```[\s\S]*?```)?")
|
if content.strip():
|
||||||
matches = list(pattern.finditer(content))
|
pattern = re.compile(r"\[Definition: (.*?) from (.*?) \(line (\d+)\)\](\s+```[\s\S]*?```)?")
|
||||||
is_nerv = theme.is_nerv_active()
|
matches = list(pattern.finditer(content))
|
||||||
if not matches:
|
is_nerv = theme.is_nerv_active()
|
||||||
if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80))
|
if not matches:
|
||||||
markdown_helper.render(content, context_id=f'disc_{i}')
|
|
||||||
if is_nerv: imgui.pop_style_color()
|
|
||||||
else:
|
|
||||||
imgui.begin_child(f"read_content_{i}", imgui.ImVec2(0, 150), True)
|
|
||||||
if self.ui_word_wrap: imgui.push_text_wrap_pos(imgui.get_content_region_avail().x)
|
|
||||||
last_idx = 0
|
|
||||||
for m_idx, match in enumerate(matches):
|
|
||||||
before = content[last_idx:match.start()]
|
|
||||||
if before:
|
|
||||||
if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80))
|
|
||||||
markdown_helper.render(before, context_id=f'disc_{i}_b_{m_idx}')
|
|
||||||
if is_nerv: imgui.pop_style_color()
|
|
||||||
header_text = match.group(0).split("\n")[0].strip()
|
|
||||||
path = match.group(2)
|
|
||||||
code_block = match.group(4)
|
|
||||||
if imgui.collapsing_header(header_text):
|
|
||||||
if imgui.button(f"[Source]##{i}_{match.start()}"):
|
|
||||||
res = mcp_client.read_file(path)
|
|
||||||
if res:
|
|
||||||
self.text_viewer_title = path
|
|
||||||
self.text_viewer_content = res
|
|
||||||
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()
|
|
||||||
last_idx = match.end()
|
|
||||||
after = content[last_idx:]
|
|
||||||
if after:
|
|
||||||
if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80))
|
if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80))
|
||||||
markdown_helper.render(after, context_id=f'disc_{i}_a')
|
markdown_helper.render(content, context_id=f'disc_{i}')
|
||||||
if is_nerv: imgui.pop_style_color()
|
if is_nerv: imgui.pop_style_color()
|
||||||
if self.ui_word_wrap: imgui.pop_text_wrap_pos()
|
else:
|
||||||
imgui.end_child()
|
imgui.begin_child(f"read_content_{i}", imgui.ImVec2(0, 150), True)
|
||||||
|
if self.ui_word_wrap: imgui.push_text_wrap_pos(imgui.get_content_region_avail().x)
|
||||||
|
last_idx = 0
|
||||||
|
for m_idx, match in enumerate(matches):
|
||||||
|
before = content[last_idx:match.start()]
|
||||||
|
if before:
|
||||||
|
if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80))
|
||||||
|
markdown_helper.render(before, context_id=f'disc_{i}_b_{m_idx}')
|
||||||
|
if is_nerv: imgui.pop_style_color()
|
||||||
|
header_text = match.group(0).split("\n")[0].strip()
|
||||||
|
path = match.group(2)
|
||||||
|
code_block = match.group(4)
|
||||||
|
if imgui.collapsing_header(header_text):
|
||||||
|
if imgui.button(f"[Source]##{i}_{match.start()}"):
|
||||||
|
res = mcp_client.read_file(path)
|
||||||
|
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:
|
||||||
|
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()
|
||||||
|
last_idx = match.end()
|
||||||
|
after = content[last_idx:]
|
||||||
|
if after:
|
||||||
|
if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80))
|
||||||
|
markdown_helper.render(after, context_id=f'disc_{i}_a')
|
||||||
|
if is_nerv: imgui.pop_style_color()
|
||||||
|
if self.ui_word_wrap: imgui.pop_text_wrap_pos()
|
||||||
|
imgui.end_child()
|
||||||
else:
|
else:
|
||||||
ch, entry["content"] = imgui.input_text_multiline("##content", entry["content"], imgui.ImVec2(-1, 150))
|
if not is_standalone:
|
||||||
|
ch, entry["content"] = imgui.input_text_multiline("##content", entry["content"], imgui.ImVec2(-1, 150))
|
||||||
imgui.separator()
|
imgui.separator()
|
||||||
imgui.pop_id()
|
imgui.pop_id()
|
||||||
if self._scroll_disc_to_bottom:
|
if self._scroll_disc_to_bottom:
|
||||||
@@ -2352,6 +2540,7 @@ def hello():
|
|||||||
self._editing_persona_system_prompt = persona.system_prompt or ""
|
self._editing_persona_system_prompt = persona.system_prompt or ""
|
||||||
self._editing_persona_tool_preset_id = persona.tool_preset or ""
|
self._editing_persona_tool_preset_id = persona.tool_preset or ""
|
||||||
self._editing_persona_bias_profile_id = persona.bias_profile or ""
|
self._editing_persona_bias_profile_id = persona.bias_profile or ""
|
||||||
|
self._editing_persona_context_preset_id = getattr(persona, 'context_preset', '') or ""
|
||||||
import copy
|
import copy
|
||||||
self._editing_persona_preferred_models_list = copy.deepcopy(persona.preferred_models) if persona.preferred_models else []
|
self._editing_persona_preferred_models_list = copy.deepcopy(persona.preferred_models) if persona.preferred_models else []
|
||||||
self._editing_persona_is_new = False
|
self._editing_persona_is_new = False
|
||||||
@@ -2380,6 +2569,9 @@ def hello():
|
|||||||
if persona.bias_profile:
|
if persona.bias_profile:
|
||||||
self.ui_active_bias_profile = persona.bias_profile
|
self.ui_active_bias_profile = persona.bias_profile
|
||||||
ai_client.set_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.end_combo()
|
||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
if imgui.button("Manage Personas"):
|
if imgui.button("Manage Personas"):
|
||||||
@@ -2760,14 +2952,24 @@ def hello():
|
|||||||
imgui.begin_child("response_scroll_area", imgui.ImVec2(0, -40), True)
|
imgui.begin_child("response_scroll_area", imgui.ImVec2(0, -40), True)
|
||||||
is_nerv = theme.is_nerv_active()
|
is_nerv = theme.is_nerv_active()
|
||||||
if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80))
|
if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80))
|
||||||
markdown_helper.render(self.ai_response, context_id="response")
|
|
||||||
|
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")
|
||||||
|
|
||||||
if is_nerv: imgui.pop_style_color()
|
if is_nerv: imgui.pop_style_color()
|
||||||
imgui.end_child()
|
imgui.end_child()
|
||||||
|
|
||||||
imgui.separator()
|
imgui.separator()
|
||||||
if imgui.button("-> History"):
|
if imgui.button("-> History"):
|
||||||
if self.ai_response:
|
if self.ai_response:
|
||||||
self.disc_entries.append({"role": "AI", "content": self.ai_response, "collapsed": True, "ts": project_manager.now_ts()})
|
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)
|
||||||
if is_blinking:
|
if is_blinking:
|
||||||
imgui.pop_style_color(2)
|
imgui.pop_style_color(2)
|
||||||
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_response_panel")
|
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_response_panel")
|
||||||
@@ -2883,6 +3085,12 @@ def hello():
|
|||||||
imgui.text_colored(C_LBL, f"#{i_display}")
|
imgui.text_colored(C_LBL, f"#{i_display}")
|
||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
imgui.text_colored(vec4(160, 160, 160), ts)
|
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")
|
ticket_id = entry.get("mma_ticket_id")
|
||||||
if ticket_id:
|
if ticket_id:
|
||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
@@ -2901,14 +3109,34 @@ def hello():
|
|||||||
# Optimized content rendering using _render_heavy_text logic
|
# Optimized content rendering using _render_heavy_text logic
|
||||||
idx_str = str(i)
|
idx_str = str(i)
|
||||||
if kind == "request":
|
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)
|
self._render_heavy_text("message", payload.get("message", ""), idx_str)
|
||||||
if payload.get("system"):
|
if payload.get("system"):
|
||||||
self._render_heavy_text("system", payload.get("system", ""), idx_str)
|
self._render_heavy_text("system", payload.get("system", ""), idx_str)
|
||||||
elif kind == "response":
|
elif kind == "response":
|
||||||
r = payload.get("round", 0)
|
r = payload.get("round", 0)
|
||||||
sr = payload.get("stop_reason", "STOP")
|
sr = payload.get("stop_reason", "STOP")
|
||||||
imgui.text_colored(C_LBL, f"round: {r} stop_reason: {sr}")
|
usage = payload.get("usage", {})
|
||||||
self._render_heavy_text("text", payload.get("text", ""), idx_str)
|
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)
|
||||||
|
|
||||||
tcs = payload.get("tool_calls", [])
|
tcs = payload.get("tool_calls", [])
|
||||||
if tcs:
|
if tcs:
|
||||||
self._render_heavy_text("tool_calls", json.dumps(tcs, indent=1), idx_str)
|
self._render_heavy_text("tool_calls", json.dumps(tcs, indent=1), idx_str)
|
||||||
@@ -2968,7 +3196,7 @@ def hello():
|
|||||||
script = entry.get("script", "")
|
script = entry.get("script", "")
|
||||||
res = entry.get("result", "")
|
res = entry.get("result", "")
|
||||||
# Use a clear, formatted combined view for the detail window
|
# Use a clear, formatted combined view for the detail window
|
||||||
combined = f"COMMAND:\n{script}\n\n{'='*40}\nOUTPUT:\n{res}"
|
combined = f"**COMMAND:**\n```powershell\n{script}\n```\n\n---\n**OUTPUT:**\n```text\n{res}\n```"
|
||||||
|
|
||||||
script_preview = script.replace("\n", " ")[:150]
|
script_preview = script.replace("\n", " ")[:150]
|
||||||
if len(script) > 150: script_preview += "..."
|
if len(script) > 150: script_preview += "..."
|
||||||
@@ -2976,6 +3204,7 @@ def hello():
|
|||||||
if imgui.is_item_clicked():
|
if imgui.is_item_clicked():
|
||||||
self.text_viewer_title = f"Tool Call #{i+1} Details"
|
self.text_viewer_title = f"Tool Call #{i+1} Details"
|
||||||
self.text_viewer_content = combined
|
self.text_viewer_content = combined
|
||||||
|
self.text_viewer_type = 'markdown'
|
||||||
self.show_text_viewer = True
|
self.show_text_viewer = True
|
||||||
|
|
||||||
imgui.table_next_column()
|
imgui.table_next_column()
|
||||||
@@ -2985,6 +3214,7 @@ def hello():
|
|||||||
if imgui.is_item_clicked():
|
if imgui.is_item_clicked():
|
||||||
self.text_viewer_title = f"Tool Call #{i+1} Details"
|
self.text_viewer_title = f"Tool Call #{i+1} Details"
|
||||||
self.text_viewer_content = combined
|
self.text_viewer_content = combined
|
||||||
|
self.text_viewer_type = 'markdown'
|
||||||
self.show_text_viewer = True
|
self.show_text_viewer = True
|
||||||
|
|
||||||
imgui.end_table()
|
imgui.end_table()
|
||||||
@@ -4024,36 +4254,6 @@ def hello():
|
|||||||
def _post_init(self) -> None:
|
def _post_init(self) -> None:
|
||||||
theme.apply_current()
|
theme.apply_current()
|
||||||
|
|
||||||
def _init_blur_pipeline(self):
|
|
||||||
if self._blur_pipeline is None:
|
|
||||||
self._blur_pipeline = BlurPipeline()
|
|
||||||
ws = imgui.get_io().display_size
|
|
||||||
fb_scale = imgui.get_io().display_framebuffer_scale.x
|
|
||||||
if ws.x <= 0 or ws.y <= 0:
|
|
||||||
return False
|
|
||||||
if fb_scale <= 0:
|
|
||||||
fb_scale = 1.0
|
|
||||||
self._blur_pipeline.setup_fbos(int(ws.x), int(ws.y), fb_scale)
|
|
||||||
self._blur_pipeline.compile_deepsea_shader()
|
|
||||||
self._blur_pipeline.compile_blur_shaders()
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _pre_new_frame(self) -> None:
|
|
||||||
if not self.ui_frosted_glass_enabled:
|
|
||||||
return
|
|
||||||
ws = imgui.get_io().display_size
|
|
||||||
fb_scale = imgui.get_io().display_framebuffer_scale.x
|
|
||||||
if ws.x <= 0 or ws.y <= 0:
|
|
||||||
return
|
|
||||||
if fb_scale <= 0:
|
|
||||||
fb_scale = 1.0
|
|
||||||
if self._blur_pipeline is None:
|
|
||||||
if not self._init_blur_pipeline():
|
|
||||||
return
|
|
||||||
import time
|
|
||||||
t = time.time()
|
|
||||||
self._blur_pipeline.prepare_global_blur(int(ws.x), int(ws.y), t, fb_scale)
|
|
||||||
|
|
||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
"""Initializes the ImGui runner and starts the main application loop."""
|
"""Initializes the ImGui runner and starts the main application loop."""
|
||||||
if "--headless" in sys.argv:
|
if "--headless" in sys.argv:
|
||||||
@@ -4109,8 +4309,6 @@ def hello():
|
|||||||
self.runner_params.callbacks.load_additional_fonts = self._load_fonts
|
self.runner_params.callbacks.load_additional_fonts = self._load_fonts
|
||||||
self.runner_params.callbacks.setup_imgui_style = theme.apply_current
|
self.runner_params.callbacks.setup_imgui_style = theme.apply_current
|
||||||
self.runner_params.callbacks.post_init = self._post_init
|
self.runner_params.callbacks.post_init = self._post_init
|
||||||
self.runner_params.callbacks.pre_new_frame = self._pre_new_frame
|
|
||||||
self.runner_params.callbacks.custom_background = self._render_custom_background
|
|
||||||
self._fetch_models(self.current_provider)
|
self._fetch_models(self.current_provider)
|
||||||
md_options = markdown_helper.get_renderer().options
|
md_options = markdown_helper.get_renderer().options
|
||||||
immapp.run(self.runner_params, add_ons_params=immapp.AddOnsParams(with_markdown_options=md_options))
|
immapp.run(self.runner_params, add_ons_params=immapp.AddOnsParams(with_markdown_options=md_options))
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ DEFAULT_TOOL_CATEGORIES: Dict[str, List[str]] = {
|
|||||||
|
|
||||||
def parse_history_entries(history_strings: list[str], roles: list[str]) -> list[dict[str, Any]]:
|
def parse_history_entries(history_strings: list[str], roles: list[str]) -> list[dict[str, Any]]:
|
||||||
import re
|
import re
|
||||||
|
from src import thinking_parser
|
||||||
entries = []
|
entries = []
|
||||||
for raw in history_strings:
|
for raw in history_strings:
|
||||||
ts = ""
|
ts = ""
|
||||||
@@ -128,11 +129,30 @@ def parse_history_entries(history_strings: list[str], roles: list[str]) -> list[
|
|||||||
content = rest[match.end():].strip()
|
content = rest[match.end():].strip()
|
||||||
else:
|
else:
|
||||||
content = rest
|
content = rest
|
||||||
entries.append({"role": role, "content": content, "collapsed": True, "ts": ts})
|
|
||||||
|
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)
|
||||||
return entries
|
return entries
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@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:
|
class Ticket:
|
||||||
id: str
|
id: str
|
||||||
@@ -239,8 +259,6 @@ class Track:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
@dataclass
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class WorkerContext:
|
class WorkerContext:
|
||||||
ticket_id: str
|
ticket_id: str
|
||||||
@@ -339,12 +357,14 @@ class FileItem:
|
|||||||
path: str
|
path: str
|
||||||
auto_aggregate: bool = True
|
auto_aggregate: bool = True
|
||||||
force_full: bool = False
|
force_full: bool = False
|
||||||
|
injected_at: Optional[float] = None
|
||||||
|
|
||||||
def to_dict(self) -> Dict[str, Any]:
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
"path": self.path,
|
"path": self.path,
|
||||||
"auto_aggregate": self.auto_aggregate,
|
"auto_aggregate": self.auto_aggregate,
|
||||||
"force_full": self.force_full,
|
"force_full": self.force_full,
|
||||||
|
"injected_at": self.injected_at,
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -353,6 +373,7 @@ class FileItem:
|
|||||||
path=data["path"],
|
path=data["path"],
|
||||||
auto_aggregate=data.get("auto_aggregate", True),
|
auto_aggregate=data.get("auto_aggregate", True),
|
||||||
force_full=data.get("force_full", False),
|
force_full=data.get("force_full", False),
|
||||||
|
injected_at=data.get("injected_at"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -448,6 +469,7 @@ class Persona:
|
|||||||
system_prompt: str = ''
|
system_prompt: str = ''
|
||||||
tool_preset: Optional[str] = None
|
tool_preset: Optional[str] = None
|
||||||
bias_profile: Optional[str] = None
|
bias_profile: Optional[str] = None
|
||||||
|
context_preset: Optional[str] = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def provider(self) -> Optional[str]:
|
def provider(self) -> Optional[str]:
|
||||||
@@ -490,6 +512,8 @@ class Persona:
|
|||||||
res["tool_preset"] = self.tool_preset
|
res["tool_preset"] = self.tool_preset
|
||||||
if self.bias_profile is not None:
|
if self.bias_profile is not None:
|
||||||
res["bias_profile"] = self.bias_profile
|
res["bias_profile"] = self.bias_profile
|
||||||
|
if self.context_preset is not None:
|
||||||
|
res["context_preset"] = self.context_preset
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -507,7 +531,7 @@ class Persona:
|
|||||||
for k in ["provider", "model", "temperature", "top_p", "max_output_tokens"]:
|
for k in ["provider", "model", "temperature", "top_p", "max_output_tokens"]:
|
||||||
if data.get(k) is not None:
|
if data.get(k) is not None:
|
||||||
legacy[k] = data[k]
|
legacy[k] = data[k]
|
||||||
|
|
||||||
if legacy:
|
if legacy:
|
||||||
if not parsed_models:
|
if not parsed_models:
|
||||||
parsed_models.append(legacy)
|
parsed_models.append(legacy)
|
||||||
@@ -523,8 +547,8 @@ class Persona:
|
|||||||
system_prompt=data.get("system_prompt", ""),
|
system_prompt=data.get("system_prompt", ""),
|
||||||
tool_preset=data.get("tool_preset"),
|
tool_preset=data.get("tool_preset"),
|
||||||
bias_profile=data.get("bias_profile"),
|
bias_profile=data.get("bias_profile"),
|
||||||
|
context_preset=data.get("context_preset"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class MCPServerConfig:
|
class MCPServerConfig:
|
||||||
name: str
|
name: str
|
||||||
|
|||||||
@@ -33,6 +33,14 @@ def entry_to_str(entry: dict[str, Any]) -> str:
|
|||||||
ts = entry.get("ts", "")
|
ts = entry.get("ts", "")
|
||||||
role = entry.get("role", "User")
|
role = entry.get("role", "User")
|
||||||
content = entry.get("content", "")
|
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:
|
if ts:
|
||||||
return f"@{ts}\n{role}:\n{content}"
|
return f"@{ts}\n{role}:\n{content}"
|
||||||
return f"{role}:\n{content}"
|
return f"{role}:\n{content}"
|
||||||
@@ -93,6 +101,7 @@ def default_project(name: str = "unnamed") -> dict[str, Any]:
|
|||||||
"output": {"output_dir": "./md_gen"},
|
"output": {"output_dir": "./md_gen"},
|
||||||
"files": {"base_dir": ".", "paths": [], "tier_assignments": {}},
|
"files": {"base_dir": ".", "paths": [], "tier_assignments": {}},
|
||||||
"screenshots": {"base_dir": ".", "paths": []},
|
"screenshots": {"base_dir": ".", "paths": []},
|
||||||
|
"context_presets": {},
|
||||||
"gemini_cli": {"binary_path": "gemini"},
|
"gemini_cli": {"binary_path": "gemini"},
|
||||||
"deepseek": {"reasoning_effort": "medium"},
|
"deepseek": {"reasoning_effort": "medium"},
|
||||||
"agent": {
|
"agent": {
|
||||||
@@ -240,6 +249,27 @@ def flat_config(proj: dict[str, Any], disc_name: Optional[str] = None, track_id:
|
|||||||
"history": history,
|
"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 ─────────────────────────────────────────────────
|
# ── track state persistence ─────────────────────────────────────────────────
|
||||||
|
|
||||||
def save_track_state(track_id: str, state: 'TrackState', base_dir: Union[str, Path] = ".") -> None:
|
def save_track_state(track_id: str, state: 'TrackState', base_dir: Union[str, Path] = ".") -> None:
|
||||||
|
|||||||
@@ -150,325 +150,4 @@ void main() {
|
|||||||
gl.glUniform1f(u_time_loc, float(time))
|
gl.glUniform1f(u_time_loc, float(time))
|
||||||
gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, 4)
|
gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, 4)
|
||||||
gl.glBindTexture(gl.GL_TEXTURE_2D, 0)
|
gl.glBindTexture(gl.GL_TEXTURE_2D, 0)
|
||||||
|
|
||||||
class BlurPipeline:
|
|
||||||
def __init__(self):
|
|
||||||
self.scene_fbo: int | None = None
|
|
||||||
self.scene_tex: int | None = None
|
|
||||||
self.blur_fbo_a: int | None = None
|
|
||||||
self.blur_tex_a: int | None = None
|
|
||||||
self.blur_fbo_b: int | None = None
|
|
||||||
self.blur_tex_b: int | None = None
|
|
||||||
self.h_blur_program: int | None = None
|
|
||||||
self.v_blur_program: int | None = None
|
|
||||||
self.deepsea_program: int | None = None
|
|
||||||
self._quad_vao: int | None = None
|
|
||||||
self._fb_width: int = 0
|
|
||||||
self._fb_height: int = 0
|
|
||||||
self._fb_scale: int = 1
|
|
||||||
|
|
||||||
def _compile_shader(self, vertex_src: str, fragment_src: str) -> int:
|
|
||||||
program = gl.glCreateProgram()
|
|
||||||
def _compile(src, shader_type):
|
|
||||||
shader = gl.glCreateShader(shader_type)
|
|
||||||
gl.glShaderSource(shader, src)
|
|
||||||
gl.glCompileShader(shader)
|
|
||||||
if not gl.glGetShaderiv(shader, gl.GL_COMPILE_STATUS):
|
|
||||||
info_log = gl.glGetShaderInfoLog(shader)
|
|
||||||
if hasattr(info_log, "decode"):
|
|
||||||
info_log = info_log.decode()
|
|
||||||
raise RuntimeError(f"Shader compilation failed: {info_log}")
|
|
||||||
return shader
|
|
||||||
vert_shader = _compile(vertex_src, gl.GL_VERTEX_SHADER)
|
|
||||||
frag_shader = _compile(fragment_src, gl.GL_FRAGMENT_SHADER)
|
|
||||||
gl.glAttachShader(program, vert_shader)
|
|
||||||
gl.glAttachShader(program, frag_shader)
|
|
||||||
gl.glLinkProgram(program)
|
|
||||||
if not gl.glGetProgramiv(program, gl.GL_LINK_STATUS):
|
|
||||||
info_log = gl.glGetProgramInfoLog(program)
|
|
||||||
if hasattr(info_log, "decode"):
|
|
||||||
info_log = info_log.decode()
|
|
||||||
raise RuntimeError(f"Program linking failed: {info_log}")
|
|
||||||
gl.glDeleteShader(vert_shader)
|
|
||||||
gl.glDeleteShader(frag_shader)
|
|
||||||
return program
|
|
||||||
|
|
||||||
def _create_fbo(self, width: int, height: int) -> tuple[int, int]:
|
|
||||||
if width <= 0 or height <= 0:
|
|
||||||
raise ValueError(f"Invalid FBO dimensions: {width}x{height}")
|
|
||||||
tex = gl.glGenTextures(1)
|
|
||||||
gl.glBindTexture(gl.GL_TEXTURE_2D, tex)
|
|
||||||
gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGBA8, 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)
|
|
||||||
fbo = gl.glGenFramebuffers(1)
|
|
||||||
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, fbo)
|
|
||||||
gl.glFramebufferTexture2D(gl.GL_FRAMEBUFFER, gl.GL_COLOR_ATTACHMENT0, gl.GL_TEXTURE_2D, tex, 0)
|
|
||||||
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0)
|
|
||||||
gl.glBindTexture(gl.GL_TEXTURE_2D, 0)
|
|
||||||
return fbo, tex
|
|
||||||
|
|
||||||
def _create_quad_vao(self) -> int:
|
|
||||||
import ctypes
|
|
||||||
vao = gl.glGenVertexArrays(1)
|
|
||||||
gl.glBindVertexArray(vao)
|
|
||||||
vertices = (ctypes.c_float * 16)(
|
|
||||||
-1.0, -1.0, 0.0, 0.0,
|
|
||||||
1.0, -1.0, 1.0, 0.0,
|
|
||||||
-1.0, 1.0, 0.0, 1.0,
|
|
||||||
1.0, 1.0, 1.0, 1.0
|
|
||||||
)
|
|
||||||
vbo = gl.glGenBuffers(1)
|
|
||||||
gl.glBindBuffer(gl.GL_ARRAY_BUFFER, vbo)
|
|
||||||
gl.glBufferData(gl.GL_ARRAY_BUFFER, ctypes.sizeof(vertices), vertices, gl.GL_STATIC_DRAW)
|
|
||||||
gl.glEnableVertexAttribArray(0)
|
|
||||||
gl.glVertexAttribPointer(0, 2, gl.GL_FLOAT, gl.GL_FALSE, 16, None)
|
|
||||||
gl.glEnableVertexAttribArray(1)
|
|
||||||
gl.glVertexAttribPointer(1, 2, gl.GL_FLOAT, gl.GL_FALSE, 16, ctypes.c_void_p(8))
|
|
||||||
gl.glBindVertexArray(0)
|
|
||||||
return vao
|
|
||||||
|
|
||||||
def setup_fbos(self, width: int, height: int, fb_scale: float = 1.0):
|
|
||||||
scale = max(1, int(fb_scale))
|
|
||||||
blur_w = max(1, (width * scale) // 4)
|
|
||||||
blur_h = max(1, (height * scale) // 4)
|
|
||||||
self._fb_width = blur_w
|
|
||||||
self._fb_height = blur_h
|
|
||||||
self._fb_scale = scale
|
|
||||||
scene_w = width * scale
|
|
||||||
scene_h = height * scale
|
|
||||||
self.scene_fbo, self.scene_tex = self._create_fbo(scene_w, scene_h)
|
|
||||||
self.blur_fbo_a, self.blur_tex_a = self._create_fbo(blur_w, blur_h)
|
|
||||||
self.blur_fbo_b, self.blur_tex_b = self._create_fbo(blur_w, blur_h)
|
|
||||||
|
|
||||||
def compile_blur_shaders(self):
|
|
||||||
vert_src = """
|
|
||||||
#version 330 core
|
|
||||||
layout(location = 0) in vec2 a_position;
|
|
||||||
layout(location = 1) in vec2 a_texcoord;
|
|
||||||
out vec2 v_uv;
|
|
||||||
void main() {
|
|
||||||
gl_Position = vec4(a_position, 0.0, 1.0);
|
|
||||||
v_uv = a_texcoord;
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
h_frag_src = """
|
|
||||||
#version 330 core
|
|
||||||
in vec2 v_uv;
|
|
||||||
uniform sampler2D u_texture;
|
|
||||||
uniform vec2 u_texel_size;
|
|
||||||
out vec4 FragColor;
|
|
||||||
void main() {
|
|
||||||
vec2 offset = vec2(u_texel_size.x, 0.0);
|
|
||||||
vec4 sum = vec4(0.0);
|
|
||||||
sum += texture(u_texture, v_uv - offset * 6.0) * 0.0152;
|
|
||||||
sum += texture(u_texture, v_uv - offset * 5.0) * 0.0300;
|
|
||||||
sum += texture(u_texture, v_uv - offset * 4.0) * 0.0525;
|
|
||||||
sum += texture(u_texture, v_uv - offset * 3.0) * 0.0812;
|
|
||||||
sum += texture(u_texture, v_uv - offset * 2.0) * 0.1110;
|
|
||||||
sum += texture(u_texture, v_uv - offset * 1.0) * 0.1342;
|
|
||||||
sum += texture(u_texture, v_uv) * 0.1432;
|
|
||||||
sum += texture(u_texture, v_uv + offset * 1.0) * 0.1342;
|
|
||||||
sum += texture(u_texture, v_uv + offset * 2.0) * 0.1110;
|
|
||||||
sum += texture(u_texture, v_uv + offset * 3.0) * 0.0812;
|
|
||||||
sum += texture(u_texture, v_uv + offset * 4.0) * 0.0525;
|
|
||||||
sum += texture(u_texture, v_uv + offset * 5.0) * 0.0300;
|
|
||||||
sum += texture(u_texture, v_uv + offset * 6.0) * 0.0152;
|
|
||||||
FragColor = sum;
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
v_frag_src = """
|
|
||||||
#version 330 core
|
|
||||||
in vec2 v_uv;
|
|
||||||
uniform sampler2D u_texture;
|
|
||||||
uniform vec2 u_texel_size;
|
|
||||||
out vec4 FragColor;
|
|
||||||
void main() {
|
|
||||||
vec2 offset = vec2(0.0, u_texel_size.y);
|
|
||||||
vec4 sum = vec4(0.0);
|
|
||||||
sum += texture(u_texture, v_uv - offset * 6.0) * 0.0152;
|
|
||||||
sum += texture(u_texture, v_uv - offset * 5.0) * 0.0300;
|
|
||||||
sum += texture(u_texture, v_uv - offset * 4.0) * 0.0525;
|
|
||||||
sum += texture(u_texture, v_uv - offset * 3.0) * 0.0812;
|
|
||||||
sum += texture(u_texture, v_uv - offset * 2.0) * 0.1110;
|
|
||||||
sum += texture(u_texture, v_uv - offset * 1.0) * 0.1342;
|
|
||||||
sum += texture(u_texture, v_uv) * 0.1432;
|
|
||||||
sum += texture(u_texture, v_uv + offset * 1.0) * 0.1342;
|
|
||||||
sum += texture(u_texture, v_uv + offset * 2.0) * 0.1110;
|
|
||||||
sum += texture(u_texture, v_uv + offset * 3.0) * 0.0812;
|
|
||||||
sum += texture(u_texture, v_uv + offset * 4.0) * 0.0525;
|
|
||||||
sum += texture(u_texture, v_uv + offset * 5.0) * 0.0300;
|
|
||||||
sum += texture(u_texture, v_uv + offset * 6.0) * 0.0152;
|
|
||||||
FragColor = sum;
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
self.h_blur_program = self._compile_shader(vert_src, h_frag_src)
|
|
||||||
self.v_blur_program = self._compile_shader(vert_src, v_frag_src)
|
|
||||||
|
|
||||||
def compile_deepsea_shader(self):
|
|
||||||
vert_src = """
|
|
||||||
#version 330 core
|
|
||||||
layout(location = 0) in vec2 a_position;
|
|
||||||
layout(location = 1) in vec2 a_texcoord;
|
|
||||||
out vec2 v_uv;
|
|
||||||
void main() {
|
|
||||||
gl_Position = vec4(a_position, 0.0, 1.0);
|
|
||||||
v_uv = a_texcoord;
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
frag_src = """
|
|
||||||
#version 330 core
|
|
||||||
in vec2 v_uv;
|
|
||||||
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.5453);
|
|
||||||
}
|
|
||||||
|
|
||||||
float noise(vec2 p) {
|
|
||||||
vec2 i = floor(p);
|
|
||||||
vec2 f = fract(p);
|
|
||||||
f = f * f * (3.0 - 2.0 * f);
|
|
||||||
float a = hash(i);
|
|
||||||
float b = hash(i + vec2(1.0, 0.0));
|
|
||||||
float c = hash(i + vec2(0.0, 1.0));
|
|
||||||
float d = hash(i + vec2(1.0, 1.0));
|
|
||||||
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
float fbm(vec2 p) {
|
|
||||||
float v = 0.0;
|
|
||||||
float a = 0.5;
|
|
||||||
for (int i = 0; i < 4; i++) {
|
|
||||||
v += a * noise(p);
|
|
||||||
p *= 2.0;
|
|
||||||
a *= 0.5;
|
|
||||||
}
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
vec2 uv = v_uv;
|
|
||||||
float t = u_time * 0.3;
|
|
||||||
vec3 col = vec3(0.01, 0.05, 0.12);
|
|
||||||
for (int i = 0; i < 3; i++) {
|
|
||||||
float phase = t * (0.1 + float(i) * 0.05);
|
|
||||||
vec2 blob_uv = uv + vec2(sin(phase), cos(phase * 0.8)) * 0.3;
|
|
||||||
float blob = fbm(blob_uv * 3.0 + t * 0.2);
|
|
||||||
col = mix(col, vec3(0.02, 0.20, 0.40), blob * 0.4);
|
|
||||||
}
|
|
||||||
float line_alpha = 0.0;
|
|
||||||
for (int i = 0; i < 12; i++) {
|
|
||||||
float fi = float(i);
|
|
||||||
float offset = mod(t * 15.0 + fi * (u_resolution.x / 12.0), u_resolution.x);
|
|
||||||
float line_x = offset / u_resolution.x;
|
|
||||||
float dist = abs(uv.x - line_x);
|
|
||||||
float alpha = smoothstep(0.02, 0.0, dist) * (0.1 + 0.05 * sin(t + fi));
|
|
||||||
line_alpha += alpha;
|
|
||||||
}
|
|
||||||
col += vec3(0.04, 0.35, 0.55) * line_alpha;
|
|
||||||
float vignette = 1.0 - length(uv - 0.5) * 0.8;
|
|
||||||
col *= vignette;
|
|
||||||
FragColor = vec4(col, 1.0);
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
self.deepsea_program = self._compile_shader(vert_src, frag_src)
|
|
||||||
self._quad_vao = self._create_quad_vao()
|
|
||||||
|
|
||||||
def render_deepsea_to_fbo(self, width: int, height: int, time: float):
|
|
||||||
if not self.deepsea_program or not self.scene_fbo or not self._quad_vao:
|
|
||||||
return
|
|
||||||
scene_w = width * self._fb_scale
|
|
||||||
scene_h = height * self._fb_scale
|
|
||||||
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.scene_fbo)
|
|
||||||
gl.glViewport(0, 0, scene_w, scene_h)
|
|
||||||
gl.glClearColor(0.01, 0.05, 0.12, 1.0)
|
|
||||||
gl.glClear(gl.GL_COLOR_BUFFER_BIT)
|
|
||||||
gl.glUseProgram(self.deepsea_program)
|
|
||||||
u_time_loc = gl.glGetUniformLocation(self.deepsea_program, "u_time")
|
|
||||||
if u_time_loc != -1:
|
|
||||||
gl.glUniform1f(u_time_loc, time)
|
|
||||||
u_res_loc = gl.glGetUniformLocation(self.deepsea_program, "u_resolution")
|
|
||||||
if u_res_loc != -1:
|
|
||||||
gl.glUniform2f(u_res_loc, float(scene_w), float(scene_h))
|
|
||||||
gl.glBindVertexArray(self._quad_vao)
|
|
||||||
gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, 4)
|
|
||||||
gl.glBindVertexArray(0)
|
|
||||||
gl.glUseProgram(0)
|
gl.glUseProgram(0)
|
||||||
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0)
|
|
||||||
|
|
||||||
def _render_quad(self, program: int, src_tex: int, texel_size: tuple[float, float]):
|
|
||||||
gl.glUseProgram(program)
|
|
||||||
gl.glActiveTexture(gl.GL_TEXTURE0)
|
|
||||||
gl.glBindTexture(gl.GL_TEXTURE_2D, src_tex)
|
|
||||||
u_tex = gl.glGetUniformLocation(program, "u_texture")
|
|
||||||
if u_tex != -1:
|
|
||||||
gl.glUniform1i(u_tex, 0)
|
|
||||||
u_ts = gl.glGetUniformLocation(program, "u_texel_size")
|
|
||||||
if u_ts != -1:
|
|
||||||
gl.glUniform2f(u_ts, texel_size[0], texel_size[1])
|
|
||||||
gl.glBindVertexArray(self._quad_vao)
|
|
||||||
gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, 4)
|
|
||||||
gl.glBindVertexArray(0)
|
|
||||||
gl.glBindTexture(gl.GL_TEXTURE_2D, 0)
|
|
||||||
gl.glUseProgram(0)
|
|
||||||
|
|
||||||
def prepare_blur(self, width: int, height: int, time: float):
|
|
||||||
if not self.h_blur_program or not self.v_blur_program:
|
|
||||||
return
|
|
||||||
if not self.blur_fbo_a or not self.blur_fbo_b:
|
|
||||||
return
|
|
||||||
blur_w = max(1, self._fb_width)
|
|
||||||
blur_h = max(1, self._fb_height)
|
|
||||||
texel_x = 1.0 / float(blur_w)
|
|
||||||
texel_y = 1.0 / float(blur_h)
|
|
||||||
gl.glViewport(0, 0, blur_w, blur_h)
|
|
||||||
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.blur_fbo_a)
|
|
||||||
gl.glClearColor(0.0, 0.0, 0.0, 0.0)
|
|
||||||
gl.glClear(gl.GL_COLOR_BUFFER_BIT)
|
|
||||||
self._render_quad(self.h_blur_program, self.scene_tex, (texel_x, texel_y))
|
|
||||||
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.blur_fbo_b)
|
|
||||||
gl.glClear(gl.GL_COLOR_BUFFER_BIT)
|
|
||||||
self._render_quad(self.v_blur_program, self.blur_tex_a, (texel_x, texel_y))
|
|
||||||
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0)
|
|
||||||
restore_w = width * self._fb_scale
|
|
||||||
restore_h = height * self._fb_scale
|
|
||||||
gl.glViewport(0, 0, restore_w, restore_h)
|
|
||||||
|
|
||||||
def prepare_global_blur(self, width: int, height: int, time: float, fb_scale: float = 1.0):
|
|
||||||
if not self.scene_fbo:
|
|
||||||
if self._fb_scale != int(fb_scale):
|
|
||||||
self.setup_fbos(width, height, fb_scale)
|
|
||||||
self.render_deepsea_to_fbo(width, height, time)
|
|
||||||
self.prepare_blur(width, height, time)
|
|
||||||
|
|
||||||
def get_blur_texture(self) -> int | None:
|
|
||||||
return self.blur_tex_b
|
|
||||||
|
|
||||||
def cleanup(self):
|
|
||||||
fbos = [f for f in [self.scene_fbo, self.blur_fbo_a, self.blur_fbo_b] if f is not None]
|
|
||||||
texs = [t for t in [self.scene_tex, self.blur_tex_a, self.blur_tex_b] if t is not None]
|
|
||||||
progs = [p for p in [self.h_blur_program, self.v_blur_program, self.deepsea_program] if p is not None]
|
|
||||||
if fbos:
|
|
||||||
gl.glDeleteFramebuffers(len(fbos), fbos)
|
|
||||||
if texs:
|
|
||||||
gl.glDeleteTextures(len(texs), texs)
|
|
||||||
if progs:
|
|
||||||
for p in progs:
|
|
||||||
gl.glDeleteProgram(p)
|
|
||||||
if self._quad_vao:
|
|
||||||
gl.glDeleteVertexArrays(1, [self._quad_vao])
|
|
||||||
self.scene_fbo = None
|
|
||||||
self.scene_tex = None
|
|
||||||
self.blur_fbo_a = None
|
|
||||||
self.blur_tex_a = None
|
|
||||||
self.blur_fbo_b = None
|
|
||||||
self.blur_tex_b = None
|
|
||||||
self.h_blur_program = None
|
|
||||||
self.v_blur_program = None
|
|
||||||
self.deepsea_program = None
|
|
||||||
self._quad_vao = None
|
|
||||||
|
|||||||
53
src/thinking_parser.py
Normal file
53
src/thinking_parser.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
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()
|
||||||
BIN
temp_gui.py
Normal file
BIN
temp_gui.py
Normal file
Binary file not shown.
59
tests/test_context_presets.py
Normal file
59
tests/test_context_presets.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
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"]
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import pytest
|
|
||||||
from unittest.mock import patch, MagicMock
|
|
||||||
from src.gui_2 import App
|
|
||||||
|
|
||||||
|
|
||||||
def test_frosted_glass_disabled():
|
|
||||||
with patch("src.gui_2.imgui") as mock_imgui:
|
|
||||||
with patch("src.gui_2.gl") as mock_gl:
|
|
||||||
app = App()
|
|
||||||
app.ui_frosted_glass_enabled = False
|
|
||||||
app._render_frosted_background((0, 0), (100, 100))
|
|
||||||
assert app._blur_pipeline is None
|
|
||||||
mock_gl.glEnable.assert_not_called()
|
|
||||||
mock_gl.glBlendFunc.assert_not_called()
|
|
||||||
mock_gl.glBindTexture.assert_not_called()
|
|
||||||
mock_gl.glBegin.assert_not_called()
|
|
||||||
mock_gl.glEnd.assert_not_called()
|
|
||||||
mock_gl.glDisable.assert_not_called()
|
|
||||||
mock_imgui.get_io().display_size.assert_not_called()
|
|
||||||
mock_imgui.get_io().display_framebuffer_scale.assert_not_called()
|
|
||||||
mock_imgui.get_window_draw_list.assert_not_called()
|
|
||||||
mock_imgui.get_window_pos.assert_not_called()
|
|
||||||
mock_imgui.get_window_size.assert_not_called()
|
|
||||||
mock_imgui.get_color_u32.assert_not_called()
|
|
||||||
mock_imgui.push_texture_id.assert_not_called()
|
|
||||||
mock_imgui.pop_texture_id.assert_not_called()
|
|
||||||
@@ -26,84 +26,5 @@ def test_gui2_old_windows_removed_from_show_windows(app_instance: App) -> None:
|
|||||||
"Provider", "System Prompts",
|
"Provider", "System Prompts",
|
||||||
"Comms History"
|
"Comms History"
|
||||||
]
|
]
|
||||||
for old_win in old_windows:
|
|
||||||
from src.gui_2 import App
|
|
||||||
|
|
||||||
def test_gui2_hubs_exist_in_show_windows(app_instance: App) -> None:
|
|
||||||
expected_hubs = [
|
|
||||||
"Context Hub",
|
|
||||||
"AI Settings",
|
|
||||||
"Discussion Hub",
|
|
||||||
"Operations Hub",
|
|
||||||
"Files & Media",
|
|
||||||
"Theme",
|
|
||||||
]
|
|
||||||
for hub in expected_hubs:
|
|
||||||
assert hub in app_instance.show_windows, f"Expected hub window '{hub}' not found in show_windows"
|
|
||||||
|
|
||||||
def test_gui2_old_windows_removed_from_show_windows(app_instance: App) -> None:
|
|
||||||
old_windows = [
|
|
||||||
"Projects", "Files", "Screenshots",
|
|
||||||
"Provider", "System Prompts",
|
|
||||||
"Comms History"
|
|
||||||
]
|
|
||||||
for old_win in old_windows:
|
for old_win in old_windows:
|
||||||
assert old_win not in app_instance.show_windows, f"Old window '{old_win}' should have been removed from show_windows"
|
assert old_win not in app_instance.show_windows, f"Old window '{old_win}' should have been removed from show_windows"
|
||||||
|
|
||||||
def test_frosted_glass_disabled():
|
|
||||||
with patch("src.gui_2.imgui"):
|
|
||||||
app = App()
|
|
||||||
app.ui_frosted_glass_enabled = False
|
|
||||||
app._render_frosted_background((0, 0), (100, 100))
|
|
||||||
assert not app._blur_pipeline is None or not app._blur_pipeline.prepare_global_blur.called
|
|
||||||
imgui.get_io().display_size.assert_not_called()
|
|
||||||
imgui.get_io().display_framebuffer_scale.assert_not_called()
|
|
||||||
imgui.get_window_draw_list.assert_not_called()
|
|
||||||
imgui.get_window_pos.assert_not_called()
|
|
||||||
imgui.get_window_size.assert_not_called()
|
|
||||||
imgui.get_color_u32.assert_not_called()
|
|
||||||
imgui.push_texture_id.assert_not_called()
|
|
||||||
imgui.pop_texture_id.assert_not_called()
|
|
||||||
dl.add_image_quad.assert_not_called()
|
|
||||||
imgui.pop_texture_id.assert_not_called()
|
|
||||||
gl.glEnable.assert_not_called()
|
|
||||||
gl.glBlendFunc.assert_not_called()
|
|
||||||
gl.glBindTexture.assert_not_called()
|
|
||||||
gl.glBegin.assert_not_called()
|
|
||||||
gl.glEnd.assert_not_called()
|
|
||||||
gl.glDisable.assert_not_called()
|
|
||||||
gl.glUnbindTexture.assert_not_called()
|
|
||||||
gl.glDeleteTexture.assert_not_called()
|
|
||||||
gl.glDisable.assert_not_called()
|
|
||||||
|
|
||||||
def test_frosted_glass_enabled():
|
|
||||||
with patch("src.gui_2.imgui"):
|
|
||||||
with patch("src.gui_2.BlurPipeline") as mock_blur:
|
|
||||||
app = App()
|
|
||||||
app.ui_frosted_glass_enabled = True
|
|
||||||
app._blur_pipeline = mock_blur
|
|
||||||
mock_blur.return_value = BlurPipeline()
|
|
||||||
mock_blur.prepare_global_blur.return_value = None
|
|
||||||
mock_blur.get_blur_texture.return_value = 123
|
|
||||||
imgui.get_io().display_size = MagicMock(x=800.0, y=600.0)
|
|
||||||
imgui.get_io().display_framebuffer_scale = MagicMock(x=1.0, y=1.0)
|
|
||||||
imgui.get_window_draw_list.return_value = MagicMock()
|
|
||||||
imgui.get_window_pos.return_value = (100, 200)
|
|
||||||
imgui.get_window_size.return_value = (300, 400)
|
|
||||||
imgui.get_color_u32.return_value = 0xFFFFFFFF
|
|
||||||
dl = MagicMock()
|
|
||||||
imgui.get_window_draw_list.return_value = dl
|
|
||||||
app._render_frosted_background((100, 200), (300, 400))
|
|
||||||
mock_blur.get_blur_texture.assert_called_once()
|
|
||||||
assert dl.add_callback_texture_id.called
|
|
||||||
assert dl.add_callback_quadsDrawElements.called
|
|
||||||
imgui.push_texture_id.assert_called()
|
|
||||||
imgui.pop_texture_id.assert_called()
|
|
||||||
gl.glEnable.assert_called()
|
|
||||||
gl.glBlendFunc.assert_called()
|
|
||||||
gl.glBindTexture.assert_called()
|
|
||||||
gl.glBegin.assert_called()
|
|
||||||
gl.glEnd.assert_called()
|
|
||||||
gl.glDisable.assert_called()
|
|
||||||
gl.glUnbindTexture.assert_called()
|
|
||||||
gl.glDeleteTexture.assert_not_called()
|
|
||||||
|
|||||||
35
tests/test_gui_context_presets.py
Normal file
35
tests/test_gui_context_presets.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
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
|
||||||
28
tests/test_gui_text_viewer.py
Normal file
28
tests/test_gui_text_viewer.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
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
|
||||||
@@ -1,172 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
def test_blur_pipeline_import():
|
|
||||||
with patch("src.shader_manager.gl") as mock_gl:
|
|
||||||
from src.shader_manager import BlurPipeline
|
|
||||||
pipeline = BlurPipeline()
|
|
||||||
assert pipeline is not None
|
|
||||||
assert pipeline.scene_fbo is None
|
|
||||||
assert pipeline.blur_fbo_a is None
|
|
||||||
assert pipeline.blur_fbo_b is None
|
|
||||||
assert pipeline.scene_tex is None
|
|
||||||
assert pipeline.blur_tex_a is None
|
|
||||||
assert pipeline.blur_tex_b is None
|
|
||||||
assert pipeline.h_blur_program is None
|
|
||||||
assert pipeline.v_blur_program is None
|
|
||||||
|
|
||||||
def test_blur_pipeline_setup_fbos():
|
|
||||||
with patch("src.shader_manager.gl") as mock_gl:
|
|
||||||
tex_counter = iter([10, 20, 30])
|
|
||||||
fbo_counter = iter([1, 2, 3])
|
|
||||||
mock_gl.glGenTextures.side_effect = lambda n: next(tex_counter)
|
|
||||||
mock_gl.glGenFramebuffers.side_effect = lambda n: next(fbo_counter)
|
|
||||||
from src.shader_manager import BlurPipeline
|
|
||||||
pipeline = BlurPipeline()
|
|
||||||
pipeline.setup_fbos(800, 600)
|
|
||||||
assert mock_gl.glGenFramebuffers.called
|
|
||||||
assert mock_gl.glGenTextures.called
|
|
||||||
assert pipeline.scene_fbo is not None
|
|
||||||
assert pipeline.blur_fbo_a is not None
|
|
||||||
assert pipeline.blur_fbo_b is not None
|
|
||||||
|
|
||||||
def test_blur_pipeline_compile_shaders():
|
|
||||||
with patch("src.shader_manager.gl") as mock_gl:
|
|
||||||
mock_gl.glCreateProgram.return_value = 100
|
|
||||||
mock_gl.glCreateShader.return_value = 200
|
|
||||||
mock_gl.glGetShaderiv.return_value = mock_gl.GL_TRUE
|
|
||||||
mock_gl.glGetProgramiv.return_value = mock_gl.GL_TRUE
|
|
||||||
from src.shader_manager import BlurPipeline
|
|
||||||
pipeline = BlurPipeline()
|
|
||||||
pipeline.compile_blur_shaders()
|
|
||||||
assert mock_gl.glCreateProgram.called
|
|
||||||
assert pipeline.h_blur_program is not None
|
|
||||||
assert pipeline.v_blur_program is not None
|
|
||||||
|
|
||||||
def test_blur_pipeline_wide_tap_distribution():
|
|
||||||
with patch("src.shader_manager.gl") as mock_gl:
|
|
||||||
mock_gl.glCreateProgram.return_value = 100
|
|
||||||
mock_gl.glCreateShader.return_value = 200
|
|
||||||
mock_gl.glGetShaderiv.return_value = mock_gl.GL_TRUE
|
|
||||||
mock_gl.glGetProgramiv.return_value = mock_gl.GL_TRUE
|
|
||||||
from src.shader_manager import BlurPipeline
|
|
||||||
pipeline = BlurPipeline()
|
|
||||||
pipeline.compile_blur_shaders()
|
|
||||||
assert mock_gl.glShaderSource.called
|
|
||||||
shader_sources = [call.args[1] for call in mock_gl.glShaderSource.call_args_list]
|
|
||||||
frag_sources = [s for s in shader_sources if 'texture(' in s and 'offset' in s]
|
|
||||||
assert len(frag_sources) >= 2
|
|
||||||
for src in frag_sources:
|
|
||||||
texture_calls = src.count('texture(u_texture')
|
|
||||||
assert texture_calls >= 11, f"Expected at least 11 texture samples for wide tap distribution, got {texture_calls}"
|
|
||||||
|
|
||||||
def test_blur_pipeline_render_deepsea_to_fbo():
|
|
||||||
with patch("src.shader_manager.gl") as mock_gl:
|
|
||||||
tex_counter = iter([10, 20, 30])
|
|
||||||
fbo_counter = iter([1, 2, 3])
|
|
||||||
mock_gl.glGenTextures.side_effect = lambda n: next(tex_counter)
|
|
||||||
mock_gl.glGenFramebuffers.side_effect = lambda n: next(fbo_counter)
|
|
||||||
mock_gl.glCreateProgram.return_value = 300
|
|
||||||
mock_gl.glCreateShader.return_value = 400
|
|
||||||
mock_gl.glGetShaderiv.return_value = mock_gl.GL_TRUE
|
|
||||||
mock_gl.glGetProgramiv.return_value = mock_gl.GL_TRUE
|
|
||||||
from src.shader_manager import BlurPipeline
|
|
||||||
pipeline = BlurPipeline()
|
|
||||||
pipeline.setup_fbos(800, 600)
|
|
||||||
pipeline.compile_deepsea_shader()
|
|
||||||
pipeline.render_deepsea_to_fbo(800, 600, 0.0)
|
|
||||||
assert mock_gl.glBindFramebuffer.called
|
|
||||||
assert mock_gl.glUseProgram.called
|
|
||||||
assert mock_gl.glDrawArrays.called
|
|
||||||
|
|
||||||
def test_blur_pipeline_deepsea_shader_compilation():
|
|
||||||
with patch("src.shader_manager.gl") as mock_gl:
|
|
||||||
mock_gl.glCreateProgram.return_value = 500
|
|
||||||
mock_gl.glCreateShader.return_value = 600
|
|
||||||
mock_gl.glGetShaderiv.return_value = mock_gl.GL_TRUE
|
|
||||||
mock_gl.glGetProgramiv.return_value = mock_gl.GL_TRUE
|
|
||||||
from src.shader_manager import BlurPipeline
|
|
||||||
pipeline = BlurPipeline()
|
|
||||||
pipeline.compile_deepsea_shader()
|
|
||||||
assert mock_gl.glCreateProgram.called
|
|
||||||
assert pipeline.deepsea_program is not None
|
|
||||||
|
|
||||||
def test_blur_pipeline_prepare_blur():
|
|
||||||
with patch("src.shader_manager.gl") as mock_gl:
|
|
||||||
mock_gl.glGenFramebuffers.return_value = None
|
|
||||||
mock_gl.glGenTextures.return_value = None
|
|
||||||
from src.shader_manager import BlurPipeline
|
|
||||||
pipeline = BlurPipeline()
|
|
||||||
pipeline.scene_fbo = 1
|
|
||||||
pipeline.scene_tex = 10
|
|
||||||
pipeline.blur_fbo_a = 2
|
|
||||||
pipeline.blur_tex_a = 20
|
|
||||||
pipeline.blur_fbo_b = 3
|
|
||||||
pipeline.blur_tex_b = 30
|
|
||||||
pipeline.h_blur_program = 100
|
|
||||||
pipeline.v_blur_program = 101
|
|
||||||
pipeline.prepare_blur(800, 600, 0.0)
|
|
||||||
assert mock_gl.glBindFramebuffer.called
|
|
||||||
assert mock_gl.glUseProgram.called
|
|
||||||
|
|
||||||
def test_blur_pipeline_prepare_global_blur():
|
|
||||||
with patch("src.shader_manager.gl") as mock_gl:
|
|
||||||
tex_counter = iter([10, 20, 30])
|
|
||||||
fbo_counter = iter([1, 2, 3])
|
|
||||||
mock_gl.glGenTextures.side_effect = lambda n: next(tex_counter)
|
|
||||||
mock_gl.glGenFramebuffers.side_effect = lambda n: next(fbo_counter)
|
|
||||||
mock_gl.glCreateProgram.return_value = 100
|
|
||||||
mock_gl.glCreateShader.return_value = 200
|
|
||||||
mock_gl.glGetShaderiv.return_value = mock_gl.GL_TRUE
|
|
||||||
mock_gl.glGetProgramiv.return_value = mock_gl.GL_TRUE
|
|
||||||
from src.shader_manager import BlurPipeline
|
|
||||||
pipeline = BlurPipeline()
|
|
||||||
pipeline.setup_fbos(800, 600)
|
|
||||||
pipeline.compile_deepsea_shader()
|
|
||||||
pipeline.compile_blur_shaders()
|
|
||||||
pipeline.prepare_global_blur(800, 600, 0.0)
|
|
||||||
assert mock_gl.glBindFramebuffer.called
|
|
||||||
assert mock_gl.glUseProgram.called
|
|
||||||
assert mock_gl.glViewport.called
|
|
||||||
blur_tex = pipeline.get_blur_texture()
|
|
||||||
assert blur_tex is not None
|
|
||||||
assert blur_tex == 30
|
|
||||||
|
|
||||||
def test_blur_pipeline_high_dpi_scaling():
|
|
||||||
with patch("src.shader_manager.gl") as mock_gl:
|
|
||||||
tex_counter = iter([10, 20, 30])
|
|
||||||
fbo_counter = iter([1, 2, 3])
|
|
||||||
mock_gl.glGenTextures.side_effect = lambda n: next(tex_counter)
|
|
||||||
mock_gl.glGenFramebuffers.side_effect = lambda n: next(fbo_counter)
|
|
||||||
mock_gl.glCreateProgram.return_value = 100
|
|
||||||
mock_gl.glCreateShader.return_value = 200
|
|
||||||
mock_gl.glGetShaderiv.return_value = mock_gl.GL_TRUE
|
|
||||||
mock_gl.glGetProgramiv.return_value = mock_gl.GL_TRUE
|
|
||||||
from src.shader_manager import BlurPipeline
|
|
||||||
pipeline = BlurPipeline()
|
|
||||||
fb_scale = 2.0
|
|
||||||
pipeline.setup_fbos(800, 600, fb_scale)
|
|
||||||
assert pipeline._fb_width == (800 * int(fb_scale)) // 4
|
|
||||||
assert pipeline._fb_height == (600 * int(fb_scale)) // 4
|
|
||||||
assert pipeline._fb_scale == int(fb_scale)
|
|
||||||
|
|
||||||
def test_blur_pipeline_cleanup():
|
|
||||||
with patch("src.shader_manager.gl") as mock_gl:
|
|
||||||
from src.shader_manager import BlurPipeline
|
|
||||||
pipeline = BlurPipeline()
|
|
||||||
pipeline.scene_fbo = 1
|
|
||||||
pipeline.blur_fbo_a = 2
|
|
||||||
pipeline.blur_fbo_b = 3
|
|
||||||
pipeline.scene_tex = 10
|
|
||||||
pipeline.blur_tex_a = 20
|
|
||||||
pipeline.blur_tex_b = 30
|
|
||||||
pipeline.h_blur_program = 100
|
|
||||||
pipeline.v_blur_program = 101
|
|
||||||
pipeline.cleanup()
|
|
||||||
assert mock_gl.glDeleteFramebuffers.called
|
|
||||||
assert mock_gl.glDeleteTextures.called
|
|
||||||
assert mock_gl.glDeleteProgram.called
|
|
||||||
|
|
||||||
def test_shader_manager_initialization_and_compilation():
|
def test_shader_manager_initialization_and_compilation():
|
||||||
# Import inside test to allow patching OpenGL before import if needed
|
# Import inside test to allow patching OpenGL before import if needed
|
||||||
# In this case, we patch the OpenGL.GL functions used by ShaderManager
|
# In this case, we patch the OpenGL.GL functions used by ShaderManager
|
||||||
|
|||||||
53
tests/test_thinking_gui.py
Normal file
53
tests/test_thinking_gui.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
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!")
|
||||||
94
tests/test_thinking_persistence.py
Normal file
94
tests/test_thinking_persistence.py
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
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!")
|
||||||
68
tests/test_thinking_trace.py
Normal file
68
tests/test_thinking_trace.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
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!")
|
||||||
Reference in New Issue
Block a user