Compare commits
72 Commits
BOTCHED-SH
...
d89f971270
| Author | SHA1 | Date | |
|---|---|---|---|
| d89f971270 | |||
| f53e417aec | |||
| f770a4e093 | |||
| dcf10a55b3 | |||
| 2a8af5f728 | |||
| b9e8d70a53 | |||
| 2352a8251e | |||
| ab30c15422 | |||
| 253d3862cc | |||
| 0738f62d98 | |||
| a452c72e1b | |||
| 7d100fb340 | |||
| f0b8f7dedc | |||
| 343fb48959 | |||
| 510527c400 | |||
| 45bffb7387 | |||
| 9c67ee743c | |||
| b077aa8165 | |||
| 1f7880a8c6 | |||
| e48835f7ff | |||
| 3225125af0 | |||
| 54cc85b4f3 | |||
| 40395893c5 | |||
| 9f4fe8e313 | |||
| fefa06beb0 | |||
| 8ee8862ae8 | |||
| 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 |
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)
|
||||||
@@ -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,7 +55,9 @@ 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.
|
||||||
- **Detailed History Management:** Rich discussion history with branching, timestamping, and specific git commit linkage per conversation.
|
- **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 non-linear timeline branching ("takes"), tabbed interface navigation, specific git commit linkage per conversation, and automated multi-take synthesis.
|
||||||
- **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.
|
||||||
- **Dedicated Diagnostics Hub:** Consolidates real-time telemetry (FPS, CPU, Frame Time) and transient system warnings into a standalone **Diagnostics panel**, providing deep visibility into application health without polluting the discussion history.
|
- **Dedicated Diagnostics Hub:** Consolidates real-time telemetry (FPS, CPU, Frame Time) and transient system warnings into a standalone **Diagnostics panel**, providing deep visibility into application health without polluting the discussion history.
|
||||||
@@ -66,7 +69,7 @@ For deep implementation details when planning or implementing tracks, consult `d
|
|||||||
- **Clean Project Root:** Enforces a "Cruft-Free Root" policy by organizing core implementation into a `src/` directory and redirecting all temporary test data, configurations, and AI-generated artifacts to `tests/artifacts/`.
|
- **Clean Project Root:** Enforces a "Cruft-Free Root" policy by organizing core implementation into a `src/` directory and redirecting all temporary test data, configurations, and AI-generated artifacts to `tests/artifacts/`.
|
||||||
- **Performance Diagnostics:** Comprehensive, conditional per-component profiling across the entire application. Features a dedicated **Diagnostics Panel** providing real-time telemetry for FPS, Frame Time, CPU usage, and **Detailed Component Timings** for all GUI panels and background threads, including automated threshold-based latency alerts.
|
- **Performance Diagnostics:** Comprehensive, conditional per-component profiling across the entire application. Features a dedicated **Diagnostics Panel** providing real-time telemetry for FPS, Frame Time, CPU usage, and **Detailed Component Timings** for all GUI panels and background threads, including automated threshold-based latency alerts.
|
||||||
- **Automated UX Verification:** A robust IPC mechanism via API hooks and a modular simulation suite allows for human-like simulation walkthroughs and automated regression testing of the full GUI lifecycle across multiple specialized scenarios.
|
- **Automated UX Verification:** A robust IPC mechanism via API hooks and a modular simulation suite allows for human-like simulation walkthroughs and automated regression testing of the full GUI lifecycle across multiple specialized scenarios.
|
||||||
- **Professional UI Theme & Typography:** Implements a high-fidelity visual system featuring **Inter** and **Maple Mono** fonts for optimal readability. Employs a cohesive \"Subtle Rounding\" aesthetic across all standard widgets, supported by custom **soft shadow shaders** for modals and popups, and a high-fidelity **frosted glass (acrylic) background effect** for panels to provide depth and professional polish. Includes a selectable **NERV UI theme** featuring a \"Black Void\" palette, zero-rounding geometry, and CRT-style visual effects (scanlines, status flickering).
|
- **Professional UI Theme & Typography:** Implements a high-fidelity visual system featuring **Inter** and **Maple Mono** fonts for optimal readability. Employs a cohesive "Subtle Rounding" aesthetic across all standard widgets, supported by custom **soft shadow shaders** for modals and popups to provide depth and professional polish. Includes a selectable **NERV UI theme** featuring a "Black Void" palette, zero-rounding geometry, and CRT-style visual effects (scanlines, status flickering).
|
||||||
- **Rich Text & Syntax Highlighting:** Provides advanced rendering for messages, logs, and tool outputs using a hybrid Markdown system. Supports GitHub-Flavored Markdown (GFM) via `imgui_markdown` and integrates `ImGuiColorTextEdit` for high-performance syntax highlighting of code blocks (Python, JSON, C++, etc.). Includes automated language detection and clickable URL support.
|
- **Rich Text & Syntax Highlighting:** Provides advanced rendering for messages, logs, and tool outputs using a hybrid Markdown system. Supports GitHub-Flavored Markdown (GFM) via `imgui_markdown` and integrates `ImGuiColorTextEdit` for high-performance syntax highlighting of code blocks (Python, JSON, C++, etc.). Includes automated language detection and clickable URL support.
|
||||||
- **Multi-Viewport & Layout Management:** Full support for ImGui Multi-Viewport, allowing users to detach panels into standalone OS windows for complex multi-monitor workflows. Includes a comprehensive **Layout Presets system**, enabling developers to save, name, and instantly restore custom window arrangements, including their Multi-Viewport state.
|
- **Multi-Viewport & Layout Management:** Full support for ImGui Multi-Viewport, allowing users to detach panels into standalone OS windows for complex multi-monitor workflows. Includes a comprehensive **Layout Presets system**, enabling developers to save, name, and instantly restore custom window arrangements, including their Multi-Viewport state.
|
||||||
- **Headless Backend Service & Hook API:** Optional headless mode allowing the core AI and tool execution logic to run as a decoupled service. Features a comprehensive Hook API and WebSocket event streaming for remote orchestration, deep state inspection, and manual worker lifecycle management.
|
- **Headless Backend Service & Hook API:** Optional headless mode allowing the core AI and tool execution logic to run as a decoupled service. Features a comprehensive Hook API and WebSocket event streaming for remote orchestration, deep state inspection, and manual worker lifecycle management.
|
||||||
|
|||||||
@@ -70,6 +70,6 @@
|
|||||||
- **Synchronous IPC Approval Flow:** A specialized bridge mechanism that allows headless AI providers (like Gemini CLI) to synchronously request and receive human approval for tool calls via the GUI's REST API hooks.
|
- **Synchronous IPC Approval Flow:** A specialized bridge mechanism that allows headless AI providers (like Gemini CLI) to synchronously request and receive human approval for tool calls via the GUI's REST API hooks.
|
||||||
- **High-Fidelity Selectable Labels:** Implements a pattern for making read-only UI text selectable by wrapping `imgui.input_text` with `imgui.InputTextFlags_.read_only`. Includes a specialized `_render_selectable_label` helper that resets frame backgrounds, borders, and padding to mimic standard labels while enabling OS-level clipboard support (Ctrl+C).
|
- **High-Fidelity Selectable Labels:** Implements a pattern for making read-only UI text selectable by wrapping `imgui.input_text` with `imgui.InputTextFlags_.read_only`. Includes a specialized `_render_selectable_label` helper that resets frame backgrounds, borders, and padding to mimic standard labels while enabling OS-level clipboard support (Ctrl+C).
|
||||||
- **Hybrid Markdown Rendering:** Employs a custom `MarkdownRenderer` that orchestrates `imgui_markdown` for standard text and headers while intercepting code blocks to render them via cached `ImGuiColorTextEdit` instances. This ensures high-performance rich text rendering with robust syntax highlighting and stateful text selection.
|
- **Hybrid Markdown Rendering:** Employs a custom `MarkdownRenderer` that orchestrates `imgui_markdown` for standard text and headers while intercepting code blocks to render them via cached `ImGuiColorTextEdit` instances. This ensures high-performance rich text rendering with robust syntax highlighting and stateful text selection.
|
||||||
- **Hybrid Shader Pipeline:** Utilizes an optimized `ImDrawList`-based batching technique to simulate UI effects such as soft shadows without the overhead of heavy GPU-resident shaders. Supplemented by a true GPU shader pipeline using `PyOpenGL` and Framebuffer Objects (FBOs) for complex post-processing (CRT scanlines, bloom), dynamic backgrounds, and high-fidelity **frosted glass (acrylic) blurring** of the GUI panels via multi-pass Gaussian/Kawase filtering.
|
- **Hybrid Shader Pipeline:** Utilizes an optimized `ImDrawList`-based batching technique to simulate UI effects such as soft shadows and acrylic glass overlays without the overhead of heavy GPU-resident shaders. Supplemented by a true GPU shader pipeline using `PyOpenGL` and Framebuffer Objects (FBOs) for complex post-processing (CRT scanlines, bloom) and dynamic backgrounds.
|
||||||
- **Interface-Driven Development (IDD):** Enforces a "Stub-and-Resolve" pattern where cross-module dependencies are resolved by generating signatures/contracts before implementation.
|
- **Interface-Driven Development (IDD):** Enforces a "Stub-and-Resolve" pattern where cross-module dependencies are resolved by generating signatures/contracts before implementation.
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ This file tracks all major tracks for the project. Each track has its own detail
|
|||||||
7. [ ] **Track: Optimization pass for Data-Oriented Python heuristics**
|
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,18 +60,18 @@ 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.*
|
||||||
|
|
||||||
9. [ ] **Track: Discussion Takes & Timeline Branching**
|
9. [x] **Track: Discussion Takes & Timeline Branching**
|
||||||
*Link: [./tracks/discussion_takes_branching_20260311/](./tracks/discussion_takes_branching_20260311/)*
|
*Link: [./tracks/discussion_takes_branching_20260311/](./tracks/discussion_takes_branching_20260311/)*
|
||||||
*Goal: Non-linear discussion timelines via tabbed "takes", message branching, and synthesis generation workflows.*
|
*Goal: Non-linear discussion timelines via tabbed "takes", message branching, and synthesis generation workflows.*
|
||||||
|
|
||||||
@@ -79,12 +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. [x] **Track: Frosted Glass Background Effect**
|
|
||||||
*Link: [./tracks/frosted_glass_20260313/](./tracks/frosted_glass_20260313/)*
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Additional Language Support
|
### Additional Language Support
|
||||||
@@ -164,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,25 +1,28 @@
|
|||||||
# Implementation Plan: Discussion Takes & Timeline Branching
|
# Implementation Plan: Discussion Takes & Timeline Branching
|
||||||
|
|
||||||
## Phase 1: Backend Support for Timeline Branching
|
## Phase 1: Backend Support for Timeline Branching [checkpoint: 4039589]
|
||||||
- [ ] Task: Write failing tests for extending the session state model to support branching (tree-like history or parallel linear "takes" with a shared ancestor).
|
- [x] Task: Write failing tests for extending the session state model to support branching (tree-like history or parallel linear "takes" with a shared ancestor). [fefa06b]
|
||||||
- [ ] Task: Implement backend logic to branch a session history at a specific message index into a new take ID.
|
- [x] Task: Implement backend logic to branch a session history at a specific message index into a new take ID. [fefa06b]
|
||||||
- [ ] Task: Implement backend logic to promote a specific take ID into an independent, top-level session.
|
- [x] Task: Implement backend logic to promote a specific take ID into an independent, top-level session. [fefa06b]
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Phase 1: Backend Support for Timeline Branching' (Protocol in workflow.md)
|
- [x] Task: Conductor - User Manual Verification 'Phase 1: Backend Support for Timeline Branching' (Protocol in workflow.md)
|
||||||
|
|
||||||
## Phase 2: GUI Implementation for Tabbed Takes
|
## Phase 2: GUI Implementation for Tabbed Takes [checkpoint: 9c67ee7]
|
||||||
- [ ] Task: Write GUI tests verifying the rendering and navigation of multiple tabs for a single session.
|
- [x] Task: Write GUI tests verifying the rendering and navigation of multiple tabs for a single session. [3225125]
|
||||||
- [ ] Task: Implement a tabbed interface within the Discussion window to switch between different takes of the active session.
|
- [x] Task: Implement a tabbed interface within the Discussion window to switch between different takes of the active session. [3225125]
|
||||||
- [ ] Task: Add a "Split/Branch from here" action to individual message entries in the discussion history.
|
- [x] Task: Add a "Split/Branch from here" action to individual message entries in the discussion history. [e48835f]
|
||||||
- [ ] Task: Add a UI button/action to promote the currently active take to a new separate session.
|
- [x] Task: Add a UI button/action to promote the currently active take to a new separate session. [1f7880a]
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Phase 2: GUI Implementation for Tabbed Takes' (Protocol in workflow.md)
|
- [x] Task: Conductor - User Manual Verification 'Phase 2: GUI Implementation for Tabbed Takes' (Protocol in workflow.md)
|
||||||
|
|
||||||
## Phase 3: Synthesis Workflow Formatting
|
## Phase 3: Synthesis Workflow Formatting [checkpoint: f0b8f7d]
|
||||||
- [ ] Task: Write tests for a new text formatting utility that takes multiple history sequences and generates a compressed, diff-like text representation.
|
- [x] Task: Write tests for a new text formatting utility that takes multiple history sequences and generates a compressed, diff-like text representation. [510527c]
|
||||||
- [ ] Task: Implement the sequence differencing and compression logic to clearly highlight variances between takes.
|
- [x] Task: Implement the sequence differencing and compression logic to clearly highlight variances between takes. [510527c]
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Phase 3: Synthesis Workflow Formatting' (Protocol in workflow.md)
|
- [x] Task: Conductor - User Manual Verification 'Phase 3: Synthesis Workflow Formatting' (Protocol in workflow.md)
|
||||||
|
|
||||||
## Phase 4: Synthesis UI & Agent Integration
|
## Phase 4: Synthesis UI & Agent Integration [checkpoint: 253d386]
|
||||||
- [ ] Task: Write GUI tests for the multi-take selection interface and synthesis action.
|
- [x] Task: Write GUI tests for the multi-take selection interface and synthesis action. [a452c72]
|
||||||
- [ ] Task: Implement a UI mechanism allowing users to select multiple takes and provide a synthesis prompt.
|
- [x] Task: Implement a UI mechanism allowing users to select multiple takes and provide a synthesis prompt. [a452c72]
|
||||||
- [ ] Task: Implement the execution pipeline to feed the compressed differences and user prompt to an AI agent, and route the generated synthesis to a new "take" tab.
|
- [x] Task: Implement the execution pipeline to feed the compressed differences and user prompt to an AI agent, and route the generated synthesis to a new "take" tab. [a452c72]
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Phase 4: Synthesis UI & Agent Integration' (Protocol in workflow.md)
|
- [x] Task: Conductor - User Manual Verification 'Phase 4: Synthesis UI & Agent Integration' (Protocol in workflow.md)
|
||||||
|
|
||||||
|
## Phase: Review Fixes
|
||||||
|
- [x] Task: Apply review suggestions [2a8af5f]
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
# Implementation Plan: Frosted Glass Background Effect
|
|
||||||
|
|
||||||
## Phase 1: Shader Development & Integration [checkpoint: 55f3bd8]
|
|
||||||
- [x] Task: Audit `src/shader_manager.py` to identify existing background/post-process integration points. [1328bc1]
|
|
||||||
- [x] Task: Write Tests: Verify `ShaderManager` can compile and bind a multi-pass blur shader. [1328bc1]
|
|
||||||
- [x] Task: Implement: Add `FrostedGlassShader` (GLSL) to `src/shader_manager.py`. [1328bc1]
|
|
||||||
- [x] Task: Implement: Integrate the blur shader into the `ShaderManager` lifecycle. [1328bc1]
|
|
||||||
- [x] Task: Conductor - User Manual Verification 'Phase 1: Shader Development & Integration' (Protocol in workflow.md) [55f3bd8]
|
|
||||||
|
|
||||||
## Phase 2: Framebuffer Capture Pipeline [checkpoint: e9b7875]
|
|
||||||
- [x] Task: Write Tests: Verify the FBO capture mechanism correctly samples the back buffer and stores it in a texture. [f297e7a]
|
|
||||||
- [x] Task: Implement: Update `src/shader_manager.py` or `src/gui_2.py` to handle "pre-rendering" of the background into a texture for blurring. [f297e7a]
|
|
||||||
- [x] Task: Implement: Ensure the blurred texture is updated every frame or on window move events. [f297e7a]
|
|
||||||
- [x] Task: Conductor - User Manual Verification 'Phase 2: Framebuffer Capture Pipeline' (Protocol in workflow.md) [e9b7875]
|
|
||||||
|
|
||||||
## Phase 3: GUI Integration & Rendering [checkpoint: cecbe22]
|
|
||||||
- [x] Task: Write Tests: Verify that a mocked ImGui window successfully calls the frosted glass rendering logic. [cecbe22]
|
|
||||||
- [x] Task: Implement: Create a `_render_frosted_background(self, pos, size)` helper in `src/gui_2.py`. [cecbe22]
|
|
||||||
- [x] Task: Implement: Update panel rendering loops (e.g. `_gui_func`) to inject the frosted background before calling `imgui.begin()` for major panels. [cecbe22]
|
|
||||||
- [x] Task: Conductor - User Manual Verification 'Phase 3: GUI Integration & Rendering' (Protocol in workflow.md) [cecbe22]
|
|
||||||
|
|
||||||
## Phase 4: UI Controls & Configuration [checkpoint: cecbe22]
|
|
||||||
- [x] Task: Write Tests: Verify that modifying blur uniforms via the Live Editor updates the shader state. [cecbe22]
|
|
||||||
- [x] Task: Implement: Add "Frosted Glass" sliders (Blur, Tint, Opacity) to the **Shader Editor** in `src/gui_2.py`. [cecbe22]
|
|
||||||
- [x] Task: Implement: Update `src/theme.py` to parse and store frosted glass settings from `config.toml`. [cecbe22]
|
|
||||||
- [x] Task: Conductor - User Manual Verification 'Phase 4: UI Controls & Configuration' (Protocol in workflow.md) [cecbe22]
|
|
||||||
@@ -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
|
||||||
|
|||||||
37
config.toml
37
config.toml
@@ -1,12 +1,12 @@
|
|||||||
[ai]
|
[ai]
|
||||||
provider = "minimax"
|
provider = "gemini_cli"
|
||||||
model = "MiniMax-M2.5"
|
model = "gemini-2.5-flash-lite"
|
||||||
temperature = 0.0
|
temperature = 0.0
|
||||||
top_p = 1.0
|
top_p = 1.0
|
||||||
max_tokens = 32000
|
max_tokens = 32000
|
||||||
history_trunc_limit = 900000
|
history_trunc_limit = 900000
|
||||||
active_preset = "Default"
|
active_preset = ""
|
||||||
system_prompt = ""
|
system_prompt = "Overridden Prompt"
|
||||||
|
|
||||||
[projects]
|
[projects]
|
||||||
paths = [
|
paths = [
|
||||||
@@ -26,19 +26,19 @@ separate_tool_calls_panel = false
|
|||||||
bg_shader_enabled = false
|
bg_shader_enabled = false
|
||||||
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" = true
|
||||||
"Usage Analytics" = true
|
"Usage Analytics" = true
|
||||||
"Tier 1" = false
|
"Tier 1" = false
|
||||||
"Tier 2" = false
|
"Tier 2" = false
|
||||||
@@ -51,25 +51,22 @@ 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" = true
|
"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
|
||||||
|
"Session Hub" = false
|
||||||
|
|
||||||
[theme]
|
[theme]
|
||||||
palette = "Nord Dark"
|
palette = "10x Dark"
|
||||||
font_path = "C:/projects/manual_slop/assets/fonts/MapleMono-Regular.ttf"
|
font_path = "fonts/Inter-Regular.ttf"
|
||||||
font_size = 16.0
|
font_size = 16.0
|
||||||
scale = 1.0
|
scale = 1.0
|
||||||
transparency = 0.699999988079071
|
transparency = 1.0
|
||||||
child_transparency = 0.6899999976158142
|
child_transparency = 1.0
|
||||||
frosted_blur_radius = 29.68400001525879
|
|
||||||
frosted_tint_intensity = 0.5659999847412109
|
|
||||||
frosted_opacity = 0.5389999747276306
|
|
||||||
frosted_glass_enabled = true
|
|
||||||
|
|
||||||
[mma]
|
[mma]
|
||||||
max_workers = 4
|
max_workers = 4
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ ViewportPos=43,95
|
|||||||
ViewportId=0x78C57832
|
ViewportId=0x78C57832
|
||||||
Size=897,649
|
Size=897,649
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000005,0
|
DockId=0x00000001,0
|
||||||
|
|
||||||
[Window][Files]
|
[Window][Files]
|
||||||
ViewportPos=3125,170
|
ViewportPos=3125,170
|
||||||
@@ -33,7 +33,7 @@ DockId=0x0000000A,0
|
|||||||
Pos=0,17
|
Pos=0,17
|
||||||
Size=1680,730
|
Size=1680,730
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000005,0
|
DockId=0x00000001,0
|
||||||
|
|
||||||
[Window][Provider]
|
[Window][Provider]
|
||||||
ViewportPos=43,95
|
ViewportPos=43,95
|
||||||
@@ -41,22 +41,23 @@ ViewportId=0x78C57832
|
|||||||
Pos=0,651
|
Pos=0,651
|
||||||
Size=897,468
|
Size=897,468
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000005,0
|
DockId=0x00000001,0
|
||||||
|
|
||||||
[Window][Message]
|
[Window][Message]
|
||||||
Pos=661,1321
|
Pos=711,694
|
||||||
Size=716,455
|
Size=716,455
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Response]
|
[Window][Response]
|
||||||
Pos=2437,925
|
Pos=245,1014
|
||||||
Size=1111,773
|
Size=1492,948
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Tool Calls]
|
[Window][Tool Calls]
|
||||||
Pos=1039,464
|
Pos=1028,1668
|
||||||
Size=587,510
|
Size=1397,340
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
DockId=0x00000006,0
|
||||||
|
|
||||||
[Window][Comms History]
|
[Window][Comms History]
|
||||||
ViewportPos=43,95
|
ViewportPos=43,95
|
||||||
@@ -73,10 +74,10 @@ Collapsed=0
|
|||||||
DockId=0xAFC85805,2
|
DockId=0xAFC85805,2
|
||||||
|
|
||||||
[Window][Theme]
|
[Window][Theme]
|
||||||
Pos=2671,24
|
Pos=0,249
|
||||||
Size=1169,2136
|
Size=32,951
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000002,1
|
DockId=0x00000002,2
|
||||||
|
|
||||||
[Window][Text Viewer - Entry #7]
|
[Window][Text Viewer - Entry #7]
|
||||||
Pos=379,324
|
Pos=379,324
|
||||||
@@ -84,16 +85,16 @@ Size=900,700
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Diagnostics]
|
[Window][Diagnostics]
|
||||||
Pos=1649,24
|
Pos=2177,26
|
||||||
Size=580,1284
|
Size=1162,1777
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000004,2
|
DockId=0x00000010,0
|
||||||
|
|
||||||
[Window][Context Hub]
|
[Window][Context Hub]
|
||||||
Pos=0,1719
|
Pos=0,249
|
||||||
Size=999,441
|
Size=32,951
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000006,0
|
DockId=0x00000002,1
|
||||||
|
|
||||||
[Window][AI Settings Hub]
|
[Window][AI Settings Hub]
|
||||||
Pos=406,17
|
Pos=406,17
|
||||||
@@ -102,45 +103,45 @@ Collapsed=0
|
|||||||
DockId=0x0000000D,0
|
DockId=0x0000000D,0
|
||||||
|
|
||||||
[Window][Discussion Hub]
|
[Window][Discussion Hub]
|
||||||
Pos=1762,24
|
Pos=807,26
|
||||||
Size=907,2136
|
Size=873,1174
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000011,0
|
DockId=0x00000013,0
|
||||||
|
|
||||||
[Window][Operations Hub]
|
[Window][Operations Hub]
|
||||||
Pos=1001,24
|
Pos=34,26
|
||||||
Size=759,2136
|
Size=771,1174
|
||||||
Collapsed=0
|
|
||||||
DockId=0x00000010,0
|
|
||||||
|
|
||||||
[Window][Files & Media]
|
|
||||||
Pos=0,1719
|
|
||||||
Size=999,441
|
|
||||||
Collapsed=0
|
|
||||||
DockId=0x00000006,1
|
|
||||||
|
|
||||||
[Window][AI Settings]
|
|
||||||
Pos=0,24
|
|
||||||
Size=999,1693
|
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000005,0
|
DockId=0x00000005,0
|
||||||
|
|
||||||
|
[Window][Files & Media]
|
||||||
|
Pos=0,249
|
||||||
|
Size=32,951
|
||||||
|
Collapsed=0
|
||||||
|
DockId=0x00000002,0
|
||||||
|
|
||||||
|
[Window][AI Settings]
|
||||||
|
Pos=0,26
|
||||||
|
Size=32,221
|
||||||
|
Collapsed=0
|
||||||
|
DockId=0x00000001,0
|
||||||
|
|
||||||
[Window][Approve Tool Execution]
|
[Window][Approve Tool Execution]
|
||||||
Pos=3,524
|
Pos=3,524
|
||||||
Size=416,325
|
Size=416,325
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][MMA Dashboard]
|
[Window][MMA Dashboard]
|
||||||
Pos=2671,24
|
Pos=3360,26
|
||||||
Size=1169,2136
|
Size=480,2134
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000002,0
|
DockId=0x00000010,0
|
||||||
|
|
||||||
[Window][Log Management]
|
[Window][Log Management]
|
||||||
Pos=1931,24
|
Pos=3360,26
|
||||||
Size=629,1416
|
Size=480,2134
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000002,1
|
DockId=0x00000010,0
|
||||||
|
|
||||||
[Window][Track Proposal]
|
[Window][Track Proposal]
|
||||||
Pos=709,326
|
Pos=709,326
|
||||||
@@ -166,7 +167,7 @@ Collapsed=0
|
|||||||
Pos=2822,1717
|
Pos=2822,1717
|
||||||
Size=1018,420
|
Size=1018,420
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000004,0
|
DockId=0x0000000C,0
|
||||||
|
|
||||||
[Window][Approve PowerShell Command]
|
[Window][Approve PowerShell Command]
|
||||||
Pos=649,435
|
Pos=649,435
|
||||||
@@ -174,8 +175,8 @@ Size=381,329
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Last Script Output]
|
[Window][Last Script Output]
|
||||||
Pos=2810,265
|
Pos=1076,794
|
||||||
Size=800,562
|
Size=1085,1154
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Text Viewer - Log Entry #1 (request)]
|
[Window][Text Viewer - Log Entry #1 (request)]
|
||||||
@@ -189,7 +190,7 @@ Size=1005,366
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Text Viewer - Entry #11]
|
[Window][Text Viewer - Entry #11]
|
||||||
Pos=60,60
|
Pos=1010,564
|
||||||
Size=1529,925
|
Size=1529,925
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
@@ -209,7 +210,7 @@ Size=3840,32
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Text Viewer - message]
|
[Window][Text Viewer - message]
|
||||||
Pos=562,588
|
Pos=568,1226
|
||||||
Size=900,700
|
Size=900,700
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
@@ -219,13 +220,13 @@ Size=900,700
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Text Viewer - text]
|
[Window][Text Viewer - text]
|
||||||
Pos=555,644
|
Pos=1297,550
|
||||||
Size=900,700
|
Size=900,700
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Text Viewer - system]
|
[Window][Text Viewer - system]
|
||||||
Pos=377,705
|
Pos=901,1502
|
||||||
Size=900,340
|
Size=876,536
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Text Viewer - Entry #15]
|
[Window][Text Viewer - Entry #15]
|
||||||
@@ -239,8 +240,8 @@ Size=900,700
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Text Viewer - tool_calls]
|
[Window][Text Viewer - tool_calls]
|
||||||
Pos=589,490
|
Pos=1106,942
|
||||||
Size=900,700
|
Size=831,482
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Text Viewer - Tool Script #1]
|
[Window][Text Viewer - Tool Script #1]
|
||||||
@@ -284,7 +285,7 @@ Size=900,700
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Text Viewer - Tool Call #1 Details]
|
[Window][Text Viewer - Tool Call #1 Details]
|
||||||
Pos=165,1081
|
Pos=963,716
|
||||||
Size=727,725
|
Size=727,725
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
@@ -329,9 +330,10 @@ Size=967,499
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Usage Analytics]
|
[Window][Usage Analytics]
|
||||||
Pos=1702,689
|
Pos=2678,26
|
||||||
Size=566,438
|
Size=1162,2134
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
DockId=0x0000000F,0
|
||||||
|
|
||||||
[Window][Tool Preset Manager]
|
[Window][Tool Preset Manager]
|
||||||
Pos=1301,302
|
Pos=1301,302
|
||||||
@@ -364,7 +366,7 @@ Size=900,700
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Text Viewer - Entry #4]
|
[Window][Text Viewer - Entry #4]
|
||||||
Pos=828,397
|
Pos=1165,782
|
||||||
Size=900,700
|
Size=900,700
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
@@ -374,17 +376,27 @@ Size=1593,1240
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Text Viewer - Entry #5]
|
[Window][Text Viewer - Entry #5]
|
||||||
|
Pos=989,778
|
||||||
|
Size=1366,1032
|
||||||
|
Collapsed=0
|
||||||
|
|
||||||
|
[Window][Shader Editor]
|
||||||
|
Pos=457,710
|
||||||
|
Size=573,280
|
||||||
|
Collapsed=0
|
||||||
|
|
||||||
|
[Window][Text Viewer - list_directory]
|
||||||
|
Pos=1376,796
|
||||||
|
Size=882,656
|
||||||
|
Collapsed=0
|
||||||
|
|
||||||
|
[Window][Text Viewer - Last Output]
|
||||||
Pos=60,60
|
Pos=60,60
|
||||||
Size=900,700
|
Size=900,700
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Shader Editor]
|
[Window][Text Viewer - Entry #2]
|
||||||
Pos=753,637
|
Pos=1518,488
|
||||||
Size=493,487
|
|
||||||
Collapsed=0
|
|
||||||
|
|
||||||
[Window][Text Viewer - list_directory]
|
|
||||||
Pos=60,60
|
|
||||||
Size=900,700
|
Size=900,700
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
@@ -419,11 +431,11 @@ Column 3 Width=20
|
|||||||
Column 4 Weight=1.0000
|
Column 4 Weight=1.0000
|
||||||
|
|
||||||
[Table][0x2A6000B6,4]
|
[Table][0x2A6000B6,4]
|
||||||
RefScale=16
|
RefScale=18
|
||||||
Column 0 Width=48
|
Column 0 Width=54
|
||||||
Column 1 Width=68
|
Column 1 Width=76
|
||||||
Column 2 Weight=1.0000
|
Column 2 Weight=1.0000
|
||||||
Column 3 Width=120
|
Column 3 Width=274
|
||||||
|
|
||||||
[Table][0x8BCC69C7,6]
|
[Table][0x8BCC69C7,6]
|
||||||
RefScale=13
|
RefScale=13
|
||||||
@@ -435,18 +447,18 @@ Column 4 Weight=1.0000
|
|||||||
Column 5 Width=50
|
Column 5 Width=50
|
||||||
|
|
||||||
[Table][0x3751446B,4]
|
[Table][0x3751446B,4]
|
||||||
RefScale=16
|
RefScale=18
|
||||||
Column 0 Width=48
|
Column 0 Width=54
|
||||||
Column 1 Width=72
|
Column 1 Width=81
|
||||||
Column 2 Weight=1.0000
|
Column 2 Weight=1.0000
|
||||||
Column 3 Width=120
|
Column 3 Width=135
|
||||||
|
|
||||||
[Table][0x2C515046,4]
|
[Table][0x2C515046,4]
|
||||||
RefScale=16
|
RefScale=18
|
||||||
Column 0 Width=48
|
Column 0 Width=54
|
||||||
Column 1 Weight=1.0000
|
Column 1 Weight=1.0000
|
||||||
Column 2 Width=117
|
Column 2 Width=132
|
||||||
Column 3 Width=48
|
Column 3 Width=54
|
||||||
|
|
||||||
[Table][0xD99F45C5,4]
|
[Table][0xD99F45C5,4]
|
||||||
Column 0 Sort=0v
|
Column 0 Sort=0v
|
||||||
@@ -467,9 +479,9 @@ Column 1 Width=100
|
|||||||
Column 2 Weight=1.0000
|
Column 2 Weight=1.0000
|
||||||
|
|
||||||
[Table][0xA02D8C87,3]
|
[Table][0xA02D8C87,3]
|
||||||
RefScale=16
|
RefScale=18
|
||||||
Column 0 Width=180
|
Column 0 Width=202
|
||||||
Column 1 Width=120
|
Column 1 Width=135
|
||||||
Column 2 Weight=1.0000
|
Column 2 Weight=1.0000
|
||||||
|
|
||||||
[Table][0xD0277E63,2]
|
[Table][0xD0277E63,2]
|
||||||
@@ -483,13 +495,13 @@ Column 0 Width=150
|
|||||||
Column 1 Weight=1.0000
|
Column 1 Weight=1.0000
|
||||||
|
|
||||||
[Table][0x8D8494AB,2]
|
[Table][0x8D8494AB,2]
|
||||||
RefScale=16
|
RefScale=18
|
||||||
Column 0 Width=132
|
Column 0 Width=148
|
||||||
Column 1 Weight=1.0000
|
Column 1 Weight=1.0000
|
||||||
|
|
||||||
[Table][0x2C261E6E,2]
|
[Table][0x2C261E6E,2]
|
||||||
RefScale=16
|
RefScale=18
|
||||||
Column 0 Width=99
|
Column 0 Width=111
|
||||||
Column 1 Weight=1.0000
|
Column 1 Weight=1.0000
|
||||||
|
|
||||||
[Table][0x9CB1E6FD,2]
|
[Table][0x9CB1E6FD,2]
|
||||||
@@ -501,19 +513,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,24 Size=3840,2136 Split=X
|
DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,26 Size=1680,1174 Split=X
|
||||||
DockNode ID=0x00000003 Parent=0xAFC85805 SizeRef=1617,1183 Split=X
|
DockNode ID=0x00000003 Parent=0xAFC85805 SizeRef=2175,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=999,858 Split=Y Selected=0x7BD57D6A
|
DockNode ID=0x00000007 Parent=0x0000000B SizeRef=1071,858 Split=Y Selected=0x8CA2375C
|
||||||
DockNode ID=0x00000005 Parent=0x00000007 SizeRef=639,904 CentralNode=1 Selected=0x7BD57D6A
|
DockNode ID=0x00000001 Parent=0x00000007 SizeRef=824,1037 CentralNode=1 Selected=0x7BD57D6A
|
||||||
DockNode ID=0x00000006 Parent=0x00000007 SizeRef=639,441 Selected=0x1DCB2623
|
DockNode ID=0x00000002 Parent=0x00000007 SizeRef=824,951 Selected=0x1DCB2623
|
||||||
DockNode ID=0x0000000E Parent=0x0000000B SizeRef=2839,858 Split=X Selected=0x418C7449
|
DockNode ID=0x0000000E Parent=0x0000000B SizeRef=2767,858 Split=X Selected=0x418C7449
|
||||||
DockNode ID=0x00000001 Parent=0x0000000E SizeRef=1668,1288 Split=X Selected=0x6F2B5B04
|
DockNode ID=0x00000012 Parent=0x0000000E SizeRef=1297,402 Split=Y Selected=0x418C7449
|
||||||
DockNode ID=0x00000010 Parent=0x00000001 SizeRef=759,1416 Selected=0x418C7449
|
DockNode ID=0x00000005 Parent=0x00000012 SizeRef=876,1749 Selected=0x418C7449
|
||||||
DockNode ID=0x00000011 Parent=0x00000001 SizeRef=907,1416 Selected=0x6F2B5B04
|
DockNode ID=0x00000006 Parent=0x00000012 SizeRef=876,362 Selected=0x1D56B311
|
||||||
DockNode ID=0x00000002 Parent=0x0000000E SizeRef=1169,1288 Selected=0x8CA2375C
|
DockNode ID=0x00000013 Parent=0x0000000E SizeRef=1468,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=511,1183 Selected=0x3AEC3498
|
DockNode ID=0x00000004 Parent=0xAFC85805 SizeRef=1162,1183 Split=Y Selected=0x3AEC3498
|
||||||
|
DockNode ID=0x00000010 Parent=0x00000004 SizeRef=1199,1689 Selected=0xB4CBF21A
|
||||||
|
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>>>;;;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,8 @@ paths = []
|
|||||||
base_dir = "."
|
base_dir = "."
|
||||||
paths = []
|
paths = []
|
||||||
|
|
||||||
|
[context_presets]
|
||||||
|
|
||||||
[gemini_cli]
|
[gemini_cli]
|
||||||
binary_path = "gemini"
|
binary_path = "gemini"
|
||||||
|
|
||||||
|
|||||||
@@ -9,5 +9,5 @@ active = "main"
|
|||||||
|
|
||||||
[discussions.main]
|
[discussions.main]
|
||||||
git_commit = ""
|
git_commit = ""
|
||||||
last_updated = "2026-03-12T20:34:43"
|
last_updated = "2026-03-21T15:21:34"
|
||||||
history = []
|
history = []
|
||||||
|
|||||||
@@ -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")
|
||||||
@@ -250,7 +253,7 @@ class HookHandler(BaseHTTPRequestHandler):
|
|||||||
self.end_headers()
|
self.end_headers()
|
||||||
files = _get_app_attr(app, "files", [])
|
files = _get_app_attr(app, "files", [])
|
||||||
screenshots = _get_app_attr(app, "screenshots", [])
|
screenshots = _get_app_attr(app, "screenshots", [])
|
||||||
self.wfile.write(json.dumps({"files": files, "screenshots": screenshots}).encode("utf-8"))
|
self.wfile.write(json.dumps({"files": _serialize_for_api(files), "screenshots": _serialize_for_api(screenshots)}).encode("utf-8"))
|
||||||
elif self.path == "/api/metrics/financial":
|
elif self.path == "/api/metrics/financial":
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
self.send_header("Content-Type", "application/json")
|
self.send_header("Content-Type", "application/json")
|
||||||
|
|||||||
@@ -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,7 +1503,27 @@ class AppController:
|
|||||||
"ts": entry.get("ts", project_manager.now_ts())
|
"ts": entry.get("ts", project_manager.now_ts())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if kind == "response":
|
||||||
|
if self.ui_auto_add_history:
|
||||||
|
role = payload.get("role", "AI")
|
||||||
|
text_content = payload.get("text", "")
|
||||||
|
if text_content.strip():
|
||||||
|
segments, parsed_response = thinking_parser.parse_thinking_trace(text_content)
|
||||||
|
entry_obj = {
|
||||||
|
"role": role,
|
||||||
|
"content": parsed_response.strip() if parsed_response else "",
|
||||||
|
"collapsed": True,
|
||||||
|
"ts": entry.get("ts", project_manager.now_ts())
|
||||||
|
}
|
||||||
|
if segments:
|
||||||
|
entry_obj["thinking_segments"] = [{"content": s.content, "marker": s.marker} for s in segments]
|
||||||
|
|
||||||
|
if entry_obj["content"] or segments:
|
||||||
|
with self._pending_history_adds_lock:
|
||||||
|
self._pending_history_adds.append(entry_obj)
|
||||||
|
|
||||||
if kind in ("tool_result", "tool_call"):
|
if kind in ("tool_result", "tool_call"):
|
||||||
|
if self.ui_auto_add_history:
|
||||||
role = "Tool" if kind == "tool_result" else "Vendor API"
|
role = "Tool" if kind == "tool_result" else "Vendor API"
|
||||||
content = ""
|
content = ""
|
||||||
if kind == "tool_result":
|
if kind == "tool_result":
|
||||||
@@ -2158,6 +2191,20 @@ class AppController:
|
|||||||
discussions[name] = project_manager.default_discussion()
|
discussions[name] = project_manager.default_discussion()
|
||||||
self._switch_discussion(name)
|
self._switch_discussion(name)
|
||||||
|
|
||||||
|
def _branch_discussion(self, index: int) -> None:
|
||||||
|
self._flush_disc_entries_to_project()
|
||||||
|
# Generate a unique branch name
|
||||||
|
base_name = self.active_discussion.split("_take_")[0]
|
||||||
|
counter = 1
|
||||||
|
new_name = f"{base_name}_take_{counter}"
|
||||||
|
disc_sec = self.project.get("discussion", {})
|
||||||
|
discussions = disc_sec.get("discussions", {})
|
||||||
|
while new_name in discussions:
|
||||||
|
counter += 1
|
||||||
|
new_name = f"{base_name}_take_{counter}"
|
||||||
|
|
||||||
|
project_manager.branch_discussion(self.project, self.active_discussion, new_name, index)
|
||||||
|
self._switch_discussion(new_name)
|
||||||
def _rename_discussion(self, old_name: str, new_name: str) -> None:
|
def _rename_discussion(self, old_name: str, new_name: str) -> None:
|
||||||
disc_sec = self.project.get("discussion", {})
|
disc_sec = self.project.get("discussion", {})
|
||||||
discussions = disc_sec.get("discussions", {})
|
discussions = disc_sec.get("discussions", {})
|
||||||
@@ -2485,6 +2532,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:
|
||||||
|
|||||||
@@ -91,7 +91,14 @@ class AsyncEventQueue:
|
|||||||
"""
|
"""
|
||||||
self._queue.put((event_name, payload))
|
self._queue.put((event_name, payload))
|
||||||
if self.websocket_server:
|
if self.websocket_server:
|
||||||
self.websocket_server.broadcast("events", {"event": event_name, "payload": payload})
|
# Ensure payload is JSON serializable for websocket broadcast
|
||||||
|
serializable_payload = payload
|
||||||
|
if hasattr(payload, 'to_dict'):
|
||||||
|
serializable_payload = payload.to_dict()
|
||||||
|
elif hasattr(payload, '__dict__'):
|
||||||
|
serializable_payload = vars(payload)
|
||||||
|
|
||||||
|
self.websocket_server.broadcast("events", {"event": event_name, "payload": serializable_payload})
|
||||||
|
|
||||||
def get(self) -> Tuple[str, Any]:
|
def get(self) -> Tuple[str, Any]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
745
src/gui_2.py
745
src/gui_2.py
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
@@ -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": {
|
||||||
@@ -235,11 +244,33 @@ def flat_config(proj: dict[str, Any], disc_name: Optional[str] = None, track_id:
|
|||||||
"output": proj.get("output", {}),
|
"output": proj.get("output", {}),
|
||||||
"files": proj.get("files", {}),
|
"files": proj.get("files", {}),
|
||||||
"screenshots": proj.get("screenshots", {}),
|
"screenshots": proj.get("screenshots", {}),
|
||||||
|
"context_presets": proj.get("context_presets", {}),
|
||||||
"discussion": {
|
"discussion": {
|
||||||
"roles": disc_sec.get("roles", []),
|
"roles": disc_sec.get("roles", []),
|
||||||
"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:
|
||||||
@@ -393,3 +424,36 @@ def calculate_track_progress(tickets: list) -> dict:
|
|||||||
"todo": todo
|
"todo": todo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def branch_discussion(project_dict: dict, source_id: str, new_id: str, message_index: int) -> None:
|
||||||
|
"""
|
||||||
|
Creates a new discussion in project_dict['discussion']['discussions'] by copying
|
||||||
|
the history from source_id up to (and including) message_index, and sets active to new_id.
|
||||||
|
"""
|
||||||
|
if "discussion" not in project_dict or "discussions" not in project_dict["discussion"]:
|
||||||
|
return
|
||||||
|
if source_id not in project_dict["discussion"]["discussions"]:
|
||||||
|
return
|
||||||
|
|
||||||
|
source_disc = project_dict["discussion"]["discussions"][source_id]
|
||||||
|
new_disc = default_discussion()
|
||||||
|
new_disc["git_commit"] = source_disc.get("git_commit", "")
|
||||||
|
# Copy history up to and including message_index
|
||||||
|
new_disc["history"] = source_disc["history"][:message_index + 1]
|
||||||
|
|
||||||
|
project_dict["discussion"]["discussions"][new_id] = new_disc
|
||||||
|
project_dict["discussion"]["active"] = new_id
|
||||||
|
|
||||||
|
def promote_take(project_dict: dict, take_id: str, new_id: str) -> None:
|
||||||
|
"""Renames a take_id to new_id in the discussions dict."""
|
||||||
|
if "discussion" not in project_dict or "discussions" not in project_dict["discussion"]:
|
||||||
|
return
|
||||||
|
if take_id not in project_dict["discussion"]["discussions"]:
|
||||||
|
return
|
||||||
|
|
||||||
|
disc = project_dict["discussion"]["discussions"].pop(take_id)
|
||||||
|
project_dict["discussion"]["discussions"][new_id] = disc
|
||||||
|
|
||||||
|
# If the take was active, update the active pointer
|
||||||
|
if project_dict["discussion"].get("active") == take_id:
|
||||||
|
project_dict["discussion"]["active"] = new_id
|
||||||
|
|||||||
@@ -5,113 +5,6 @@ class ShaderManager:
|
|||||||
self.program = None
|
self.program = None
|
||||||
self.bg_program = None
|
self.bg_program = None
|
||||||
self.pp_program = None
|
self.pp_program = None
|
||||||
self.blur_h_program = None
|
|
||||||
self.blur_v_program = None
|
|
||||||
self.blur_fbo = None
|
|
||||||
self.scene_fbo = None
|
|
||||||
self.temp_fbo = None
|
|
||||||
self.scene_tex = None
|
|
||||||
self.blur_tex = None
|
|
||||||
self.temp_tex = None
|
|
||||||
self.fbo_width = 0
|
|
||||||
self.fbo_height = 0
|
|
||||||
self._vao = None
|
|
||||||
|
|
||||||
def _ensure_vao(self):
|
|
||||||
if self._vao is None:
|
|
||||||
try:
|
|
||||||
import sys
|
|
||||||
if sys.platform == "win32":
|
|
||||||
self._vao = gl.glGenVertexArrays(1)
|
|
||||||
else:
|
|
||||||
# Some non-win32 environments might not support VAOs or need different handling
|
|
||||||
self._vao = gl.glGenVertexArrays(1)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
if self._vao is not None:
|
|
||||||
gl.glBindVertexArray(self._vao)
|
|
||||||
|
|
||||||
def setup_capture_fbo(self, width, height):
|
|
||||||
if self.blur_fbo is not None:
|
|
||||||
gl.glDeleteFramebuffers(1, [self.blur_fbo])
|
|
||||||
if self.scene_fbo is not None:
|
|
||||||
gl.glDeleteFramebuffers(1, [self.scene_fbo])
|
|
||||||
if self.temp_fbo is not None:
|
|
||||||
gl.glDeleteFramebuffers(1, [self.temp_fbo])
|
|
||||||
if self.scene_tex is not None:
|
|
||||||
gl.glDeleteTextures(1, [self.scene_tex])
|
|
||||||
if self.blur_tex is not None:
|
|
||||||
gl.glDeleteTextures(1, [self.blur_tex])
|
|
||||||
if self.temp_tex is not None:
|
|
||||||
gl.glDeleteTextures(1, [self.temp_tex])
|
|
||||||
|
|
||||||
self.scene_tex = gl.glGenTextures(1)
|
|
||||||
gl.glBindTexture(gl.GL_TEXTURE_2D, self.scene_tex)
|
|
||||||
gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGBA, width, height, 0, gl.GL_RGBA, gl.GL_UNSIGNED_BYTE, None)
|
|
||||||
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR)
|
|
||||||
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR)
|
|
||||||
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE)
|
|
||||||
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_T, gl.GL_CLAMP_TO_EDGE)
|
|
||||||
|
|
||||||
self.scene_fbo = gl.glGenFramebuffers(1)
|
|
||||||
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.scene_fbo)
|
|
||||||
gl.glFramebufferTexture2D(gl.GL_FRAMEBUFFER, gl.GL_COLOR_ATTACHMENT0, gl.GL_TEXTURE_2D, self.scene_tex, 0)
|
|
||||||
if gl.glCheckFramebufferStatus(gl.GL_FRAMEBUFFER) != gl.GL_FRAMEBUFFER_COMPLETE:
|
|
||||||
raise RuntimeError("Scene Framebuffer not complete")
|
|
||||||
|
|
||||||
self.temp_tex = gl.glGenTextures(1)
|
|
||||||
gl.glBindTexture(gl.GL_TEXTURE_2D, self.temp_tex)
|
|
||||||
gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGBA, width, height, 0, gl.GL_RGBA, gl.GL_UNSIGNED_BYTE, None)
|
|
||||||
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR)
|
|
||||||
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR)
|
|
||||||
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE)
|
|
||||||
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_T, gl.GL_CLAMP_TO_EDGE)
|
|
||||||
|
|
||||||
self.temp_fbo = gl.glGenFramebuffers(1)
|
|
||||||
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.temp_fbo)
|
|
||||||
gl.glFramebufferTexture2D(gl.GL_FRAMEBUFFER, gl.GL_COLOR_ATTACHMENT0, gl.GL_TEXTURE_2D, self.temp_tex, 0)
|
|
||||||
if gl.glCheckFramebufferStatus(gl.GL_FRAMEBUFFER) != gl.GL_FRAMEBUFFER_COMPLETE:
|
|
||||||
raise RuntimeError("Temp Framebuffer not complete")
|
|
||||||
|
|
||||||
self.blur_tex = gl.glGenTextures(1)
|
|
||||||
gl.glBindTexture(gl.GL_TEXTURE_2D, self.blur_tex)
|
|
||||||
gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGBA, width, height, 0, gl.GL_RGBA, gl.GL_UNSIGNED_BYTE, None)
|
|
||||||
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR)
|
|
||||||
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR)
|
|
||||||
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE)
|
|
||||||
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_T, gl.GL_CLAMP_TO_EDGE)
|
|
||||||
|
|
||||||
self.blur_fbo = gl.glGenFramebuffers(1)
|
|
||||||
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.blur_fbo)
|
|
||||||
gl.glFramebufferTexture2D(gl.GL_FRAMEBUFFER, gl.GL_COLOR_ATTACHMENT0, gl.GL_TEXTURE_2D, self.blur_tex, 0)
|
|
||||||
if gl.glCheckFramebufferStatus(gl.GL_FRAMEBUFFER) != gl.GL_FRAMEBUFFER_COMPLETE:
|
|
||||||
raise RuntimeError("Blur Framebuffer not complete")
|
|
||||||
|
|
||||||
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0)
|
|
||||||
self.fbo_width = width
|
|
||||||
self.fbo_height = height
|
|
||||||
|
|
||||||
def render_background_to_fbo(self, width, height, time):
|
|
||||||
if self.scene_fbo is None or self.fbo_width != width or self.fbo_height != height:
|
|
||||||
self.setup_capture_fbo(width, height)
|
|
||||||
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.scene_fbo)
|
|
||||||
gl.glViewport(0, 0, width, height)
|
|
||||||
self.render_background(width, height, time)
|
|
||||||
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0)
|
|
||||||
|
|
||||||
def prepare_global_blur(self, width, height, radius, tint, opacity, time):
|
|
||||||
self.render_background_to_fbo(width, height, time)
|
|
||||||
self.render_blur(self.scene_tex, width, height, radius, tint, opacity)
|
|
||||||
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0)
|
|
||||||
|
|
||||||
def capture_begin(self, width, height):
|
|
||||||
if self.blur_fbo is None or self.fbo_width != width or self.fbo_height != height:
|
|
||||||
self.setup_capture_fbo(width, height)
|
|
||||||
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.blur_fbo)
|
|
||||||
gl.glViewport(0, 0, width, height)
|
|
||||||
|
|
||||||
def capture_end(self):
|
|
||||||
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0)
|
|
||||||
|
|
||||||
def compile_shader(self, vertex_src: str, fragment_src: str) -> int:
|
def compile_shader(self, vertex_src: str, fragment_src: str) -> int:
|
||||||
program = gl.glCreateProgram()
|
program = gl.glCreateProgram()
|
||||||
@@ -186,44 +79,9 @@ void main() {
|
|||||||
uniform float u_time;
|
uniform float u_time;
|
||||||
uniform vec2 u_resolution;
|
uniform vec2 u_resolution;
|
||||||
out vec4 FragColor;
|
out vec4 FragColor;
|
||||||
|
|
||||||
float hash(vec2 p) {
|
|
||||||
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
|
|
||||||
}
|
|
||||||
|
|
||||||
float noise(vec2 p) {
|
|
||||||
vec2 i = floor(p);
|
|
||||||
vec2 f = fract(p);
|
|
||||||
vec2 u = f * f * (3.0 - 2.0 * f);
|
|
||||||
return mix(mix(hash(i + vec2(0.0, 0.0)), hash(i + vec2(1.0, 0.0)), u.x),
|
|
||||||
mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), u.x), u.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
vec2 uv = gl_FragCoord.xy / u_resolution.xy;
|
vec2 uv = gl_FragCoord.xy / u_resolution.xy;
|
||||||
vec2 p = uv * 2.0 - 1.0;
|
vec3 col = 0.5 + 0.5 * cos(u_time + uv.xyx + vec3(0, 2, 4));
|
||||||
p.x *= u_resolution.x / u_resolution.y;
|
|
||||||
|
|
||||||
// Deep sea background gradient (dark blue)
|
|
||||||
vec3 col = mix(vec3(0.01, 0.03, 0.08), vec3(0.0, 0.08, 0.15), uv.y);
|
|
||||||
|
|
||||||
// Moving blobs / caustics
|
|
||||||
float n = 0.0;
|
|
||||||
float t = u_time * 0.15;
|
|
||||||
n += noise(p * 1.2 + vec2(t * 0.8, t * 0.5)) * 0.4;
|
|
||||||
n += noise(p * 2.5 - vec2(t * 0.4, t * 0.9)) * 0.2;
|
|
||||||
|
|
||||||
col += vec3(0.05, 0.12, 0.22) * n;
|
|
||||||
|
|
||||||
// Bright highlights (caustics approximation)
|
|
||||||
float c = 0.0;
|
|
||||||
for(int i=0; i<3; i++) {
|
|
||||||
vec2 p2 = p * (float(i) + 1.0) * 0.4;
|
|
||||||
p2 += vec2(sin(t + p2.y * 1.5), cos(t + p2.x * 1.5));
|
|
||||||
c += abs(0.015 / (length(p2) - 0.4));
|
|
||||||
}
|
|
||||||
col += vec3(0.1, 0.25, 0.45) * c * 0.12;
|
|
||||||
|
|
||||||
FragColor = vec4(col, 1.0);
|
FragColor = vec4(col, 1.0);
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
@@ -239,7 +97,6 @@ void main() {
|
|||||||
u_res_loc = gl.glGetUniformLocation(self.bg_program, "u_resolution")
|
u_res_loc = gl.glGetUniformLocation(self.bg_program, "u_resolution")
|
||||||
if u_res_loc != -1:
|
if u_res_loc != -1:
|
||||||
gl.glUniform2f(u_res_loc, float(width), float(height))
|
gl.glUniform2f(u_res_loc, float(width), float(height))
|
||||||
self._ensure_vao()
|
|
||||||
gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, 4)
|
gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, 4)
|
||||||
gl.glUseProgram(0)
|
gl.glUseProgram(0)
|
||||||
|
|
||||||
@@ -291,114 +148,6 @@ void main() {
|
|||||||
u_time_loc = gl.glGetUniformLocation(self.pp_program, "u_time")
|
u_time_loc = gl.glGetUniformLocation(self.pp_program, "u_time")
|
||||||
if u_time_loc != -1:
|
if u_time_loc != -1:
|
||||||
gl.glUniform1f(u_time_loc, float(time))
|
gl.glUniform1f(u_time_loc, float(time))
|
||||||
self._ensure_vao()
|
|
||||||
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)
|
||||||
gl.glUseProgram(0)
|
gl.glUseProgram(0)
|
||||||
|
|
||||||
def setup_frosted_glass_shader(self):
|
|
||||||
vertex_src = """
|
|
||||||
#version 330 core
|
|
||||||
const vec2 positions[4] = vec2[](
|
|
||||||
vec2(-1.0, -1.0),
|
|
||||||
vec2( 1.0, -1.0),
|
|
||||||
vec2(-1.0, 1.0),
|
|
||||||
vec2( 1.0, 1.0)
|
|
||||||
);
|
|
||||||
const vec2 uvs[4] = vec2[](
|
|
||||||
vec2(0.0, 0.0),
|
|
||||||
vec2(1.0, 0.0),
|
|
||||||
vec2(0.0, 1.0),
|
|
||||||
vec2(1.0, 1.0)
|
|
||||||
);
|
|
||||||
out vec2 v_uv;
|
|
||||||
void main() {
|
|
||||||
gl_Position = vec4(positions[gl_VertexID], 0.0, 1.0);
|
|
||||||
v_uv = uvs[gl_VertexID];
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
fragment_src_h = """
|
|
||||||
#version 330 core
|
|
||||||
in vec2 v_uv;
|
|
||||||
uniform sampler2D u_texture;
|
|
||||||
uniform float u_blur_radius;
|
|
||||||
uniform vec2 u_direction;
|
|
||||||
out vec4 FragColor;
|
|
||||||
void main() {
|
|
||||||
float weight[5] = float[](0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216);
|
|
||||||
vec2 res = vec2(textureSize(u_texture, 0));
|
|
||||||
vec2 tex_offset = (u_blur_radius / res) * u_direction * 2.5; // Multiplied by 2.5 for milky effect
|
|
||||||
vec4 result = texture(u_texture, v_uv) * weight[0];
|
|
||||||
for(int i = 1; i < 5; ++i) {
|
|
||||||
result += texture(u_texture, v_uv + tex_offset * float(i)) * weight[i];
|
|
||||||
result += texture(u_texture, v_uv - tex_offset * float(i)) * weight[i];
|
|
||||||
}
|
|
||||||
FragColor = result;
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
fragment_src_v = """
|
|
||||||
#version 330 core
|
|
||||||
in vec2 v_uv;
|
|
||||||
uniform sampler2D u_texture;
|
|
||||||
uniform float u_blur_radius;
|
|
||||||
uniform vec2 u_direction;
|
|
||||||
uniform float u_tint_intensity;
|
|
||||||
uniform float u_opacity;
|
|
||||||
out vec4 FragColor;
|
|
||||||
void main() {
|
|
||||||
float weight[5] = float[](0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216);
|
|
||||||
vec2 res = vec2(textureSize(u_texture, 0));
|
|
||||||
vec2 tex_offset = (u_blur_radius / res) * u_direction * 2.5; // Multiplied by 2.5 for milky effect
|
|
||||||
vec4 result = texture(u_texture, v_uv) * weight[0];
|
|
||||||
for(int i = 1; i < 5; ++i) {
|
|
||||||
result += texture(u_texture, v_uv + tex_offset * float(i)) * weight[i];
|
|
||||||
result += texture(u_texture, v_uv - tex_offset * float(i)) * weight[i];
|
|
||||||
}
|
|
||||||
vec3 tint_color = vec3(0.05, 0.07, 0.12); // Slightly deeper tint
|
|
||||||
vec3 tinted = mix(result.rgb, tint_color, u_tint_intensity);
|
|
||||||
FragColor = vec4(tinted, result.a * u_opacity);
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
self.blur_h_program = self.compile_shader(vertex_src, fragment_src_h)
|
|
||||||
self.blur_v_program = self.compile_shader(vertex_src, fragment_src_v)
|
|
||||||
|
|
||||||
def render_blur(self, texture_id, width, height, radius, tint, opacity):
|
|
||||||
if not self.blur_h_program or not self.blur_v_program:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._ensure_vao()
|
|
||||||
|
|
||||||
# Pass 1: Horizontal blur to temp_fbo
|
|
||||||
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.temp_fbo)
|
|
||||||
gl.glViewport(0, 0, width, height)
|
|
||||||
gl.glClear(gl.GL_COLOR_BUFFER_BIT)
|
|
||||||
|
|
||||||
gl.glUseProgram(self.blur_h_program)
|
|
||||||
gl.glActiveTexture(gl.GL_TEXTURE0)
|
|
||||||
gl.glBindTexture(gl.GL_TEXTURE_2D, texture_id)
|
|
||||||
|
|
||||||
gl.glUniform1i(gl.glGetUniformLocation(self.blur_h_program, "u_texture"), 0)
|
|
||||||
gl.glUniform1f(gl.glGetUniformLocation(self.blur_h_program, "u_blur_radius"), float(radius))
|
|
||||||
gl.glUniform2f(gl.glGetUniformLocation(self.blur_h_program, "u_direction"), 1.0, 0.0)
|
|
||||||
|
|
||||||
gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, 4)
|
|
||||||
|
|
||||||
# Pass 2: Vertical blur to blur_fbo
|
|
||||||
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.blur_fbo)
|
|
||||||
gl.glViewport(0, 0, width, height)
|
|
||||||
gl.glClear(gl.GL_COLOR_BUFFER_BIT)
|
|
||||||
|
|
||||||
gl.glUseProgram(self.blur_v_program)
|
|
||||||
gl.glActiveTexture(gl.GL_TEXTURE0)
|
|
||||||
gl.glBindTexture(gl.GL_TEXTURE_2D, self.temp_tex)
|
|
||||||
|
|
||||||
gl.glUniform1i(gl.glGetUniformLocation(self.blur_v_program, "u_texture"), 0)
|
|
||||||
gl.glUniform1f(gl.glGetUniformLocation(self.blur_v_program, "u_blur_radius"), float(radius))
|
|
||||||
gl.glUniform2f(gl.glGetUniformLocation(self.blur_v_program, "u_direction"), 0.0, 1.0)
|
|
||||||
gl.glUniform1f(gl.glGetUniformLocation(self.blur_v_program, "u_tint_intensity"), float(tint))
|
|
||||||
gl.glUniform1f(gl.glGetUniformLocation(self.blur_v_program, "u_opacity"), float(opacity))
|
|
||||||
|
|
||||||
gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, 4)
|
|
||||||
|
|
||||||
gl.glBindTexture(gl.GL_TEXTURE_2D, 0)
|
|
||||||
gl.glUseProgram(0)
|
|
||||||
|
|||||||
42
src/synthesis_formatter.py
Normal file
42
src/synthesis_formatter.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
def format_takes_diff(takes: dict[str, list[dict]]) -> str:
|
||||||
|
if not takes:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
histories = list(takes.values())
|
||||||
|
if not histories:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
min_len = min(len(h) for h in histories)
|
||||||
|
common_prefix_len = 0
|
||||||
|
for i in range(min_len):
|
||||||
|
first_msg = histories[0][i]
|
||||||
|
if all(h[i] == first_msg for h in histories):
|
||||||
|
common_prefix_len += 1
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
shared_lines = []
|
||||||
|
for i in range(common_prefix_len):
|
||||||
|
msg = histories[0][i]
|
||||||
|
shared_lines.append(f"{msg.get('role', 'unknown')}: {msg.get('content', '')}")
|
||||||
|
|
||||||
|
shared_text = "=== Shared History ==="
|
||||||
|
if shared_lines:
|
||||||
|
shared_text += "\n" + "\n".join(shared_lines)
|
||||||
|
|
||||||
|
variation_lines = []
|
||||||
|
if len(takes) > 1:
|
||||||
|
for take_name, history in takes.items():
|
||||||
|
if len(history) > common_prefix_len:
|
||||||
|
variation_lines.append(f"[{take_name}]")
|
||||||
|
for i in range(common_prefix_len, len(history)):
|
||||||
|
msg = history[i]
|
||||||
|
variation_lines.append(f"{msg.get('role', 'unknown')}: {msg.get('content', '')}")
|
||||||
|
variation_lines.append("")
|
||||||
|
else:
|
||||||
|
# Single take case
|
||||||
|
pass
|
||||||
|
|
||||||
|
variations_text = "=== Variations ===\n" + "\n".join(variation_lines)
|
||||||
|
|
||||||
|
return shared_text + "\n\n" + variations_text
|
||||||
@@ -235,10 +235,6 @@ _current_font_size: float = 16.0
|
|||||||
_current_scale: float = 1.0
|
_current_scale: float = 1.0
|
||||||
_transparency: float = 1.0
|
_transparency: float = 1.0
|
||||||
_child_transparency: float = 1.0
|
_child_transparency: float = 1.0
|
||||||
_frosted_glass_enabled: bool = False
|
|
||||||
_frosted_blur_radius: float = 8.0
|
|
||||||
_frosted_tint_intensity: float = 0.1
|
|
||||||
_frosted_opacity: float = 1.0
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ public API
|
# ------------------------------------------------------------------ public API
|
||||||
|
|
||||||
@@ -273,34 +269,6 @@ def set_child_transparency(val: float) -> None:
|
|||||||
_child_transparency = val
|
_child_transparency = val
|
||||||
apply(_current_palette)
|
apply(_current_palette)
|
||||||
|
|
||||||
def get_frosted_glass_enabled() -> bool:
|
|
||||||
return _frosted_glass_enabled
|
|
||||||
|
|
||||||
def set_frosted_glass_enabled(val: bool) -> None:
|
|
||||||
global _frosted_glass_enabled
|
|
||||||
_frosted_glass_enabled = val
|
|
||||||
|
|
||||||
def get_frosted_blur_radius() -> float:
|
|
||||||
return _frosted_blur_radius
|
|
||||||
|
|
||||||
def set_frosted_blur_radius(val: float) -> None:
|
|
||||||
global _frosted_blur_radius
|
|
||||||
_frosted_blur_radius = val
|
|
||||||
|
|
||||||
def get_frosted_tint_intensity() -> float:
|
|
||||||
return _frosted_tint_intensity
|
|
||||||
|
|
||||||
def set_frosted_tint_intensity(val: float) -> None:
|
|
||||||
global _frosted_tint_intensity
|
|
||||||
_frosted_tint_intensity = val
|
|
||||||
|
|
||||||
def get_frosted_opacity() -> float:
|
|
||||||
return _frosted_opacity
|
|
||||||
|
|
||||||
def set_frosted_opacity(val: float) -> None:
|
|
||||||
global _frosted_opacity
|
|
||||||
_frosted_opacity = val
|
|
||||||
|
|
||||||
def apply(palette_name: str) -> None:
|
def apply(palette_name: str) -> None:
|
||||||
"""
|
"""
|
||||||
Apply a named palette by setting all ImGui style colors and applying global professional styling.
|
Apply a named palette by setting all ImGui style colors and applying global professional styling.
|
||||||
@@ -382,17 +350,13 @@ def save_to_config(config: dict) -> None:
|
|||||||
config["theme"]["scale"] = _current_scale
|
config["theme"]["scale"] = _current_scale
|
||||||
config["theme"]["transparency"] = _transparency
|
config["theme"]["transparency"] = _transparency
|
||||||
config["theme"]["child_transparency"] = _child_transparency
|
config["theme"]["child_transparency"] = _child_transparency
|
||||||
config["theme"]["frosted_glass_enabled"] = _frosted_glass_enabled
|
|
||||||
config["theme"]["frosted_blur_radius"] = _frosted_blur_radius
|
|
||||||
config["theme"]["frosted_tint_intensity"] = _frosted_tint_intensity
|
|
||||||
config["theme"]["frosted_opacity"] = _frosted_opacity
|
|
||||||
sys.stderr.write(f"[DEBUG theme_2] save_to_config: palette={_current_palette}, transparency={_transparency}\n")
|
sys.stderr.write(f"[DEBUG theme_2] save_to_config: palette={_current_palette}, transparency={_transparency}\n")
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
|
|
||||||
def load_from_config(config: dict) -> None:
|
def load_from_config(config: dict) -> None:
|
||||||
"""Read [theme] from config. Font is handled separately at startup."""
|
"""Read [theme] from config. Font is handled separately at startup."""
|
||||||
import sys
|
import sys
|
||||||
global _current_font_path, _current_font_size, _current_scale, _current_palette, _transparency, _child_transparency, _frosted_glass_enabled, _frosted_blur_radius, _frosted_tint_intensity, _frosted_opacity
|
global _current_font_path, _current_font_size, _current_scale, _current_palette, _transparency, _child_transparency
|
||||||
t = config.get("theme", {})
|
t = config.get("theme", {})
|
||||||
sys.stderr.write(f"[DEBUG theme_2] load_from_config raw: {t}\n")
|
sys.stderr.write(f"[DEBUG theme_2] load_from_config raw: {t}\n")
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
@@ -405,10 +369,6 @@ def load_from_config(config: dict) -> None:
|
|||||||
_current_scale = float(t.get("scale", 1.0))
|
_current_scale = float(t.get("scale", 1.0))
|
||||||
_transparency = float(t.get("transparency", 1.0))
|
_transparency = float(t.get("transparency", 1.0))
|
||||||
_child_transparency = float(t.get("child_transparency", 1.0))
|
_child_transparency = float(t.get("child_transparency", 1.0))
|
||||||
_frosted_glass_enabled = bool(t.get("frosted_glass_enabled", False))
|
|
||||||
_frosted_blur_radius = float(t.get("frosted_blur_radius", 8.0))
|
|
||||||
_frosted_tint_intensity = float(t.get("frosted_tint_intensity", 0.1))
|
|
||||||
_frosted_opacity = float(t.get("frosted_opacity", 1.0))
|
|
||||||
sys.stderr.write(f"[DEBUG theme_2] load_from_config effective: palette={_current_palette}, transparency={_transparency}\n")
|
sys.stderr.write(f"[DEBUG theme_2] load_from_config effective: palette={_current_palette}, transparency={_transparency}\n")
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
|
|
||||||
|
|||||||
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"]
|
||||||
50
tests/test_discussion_takes.py
Normal file
50
tests/test_discussion_takes.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import unittest
|
||||||
|
from src import project_manager
|
||||||
|
|
||||||
|
class TestDiscussionTakes(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.project_dict = project_manager.default_project("test_branching")
|
||||||
|
# Populate initial history in 'main'
|
||||||
|
self.project_dict["discussion"]["discussions"]["main"]["history"] = [
|
||||||
|
"User: Message 0",
|
||||||
|
"AI: Response 0",
|
||||||
|
"User: Message 1",
|
||||||
|
"AI: Response 1",
|
||||||
|
"User: Message 2"
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_branch_discussion_creates_new_take(self):
|
||||||
|
"""Verify that branch_discussion copies history up to index and sets active."""
|
||||||
|
source_id = "main"
|
||||||
|
new_id = "take_1"
|
||||||
|
message_index = 1
|
||||||
|
|
||||||
|
# This will fail with AttributeError until implemented in project_manager.py
|
||||||
|
project_manager.branch_discussion(self.project_dict, source_id, new_id, message_index)
|
||||||
|
|
||||||
|
# Asserts
|
||||||
|
self.assertIn(new_id, self.project_dict["discussion"]["discussions"])
|
||||||
|
new_history = self.project_dict["discussion"]["discussions"][new_id]["history"]
|
||||||
|
self.assertEqual(len(new_history), 2)
|
||||||
|
self.assertEqual(new_history[0], "User: Message 0")
|
||||||
|
self.assertEqual(new_history[1], "AI: Response 0")
|
||||||
|
self.assertEqual(self.project_dict["discussion"]["active"], new_id)
|
||||||
|
|
||||||
|
def test_promote_take_renames_discussion(self):
|
||||||
|
"""Verify that promote_take renames a discussion key."""
|
||||||
|
take_id = "take_experimental"
|
||||||
|
self.project_dict["discussion"]["discussions"][take_id] = project_manager.default_discussion()
|
||||||
|
self.project_dict["discussion"]["discussions"][take_id]["history"] = ["User: Experimental"]
|
||||||
|
|
||||||
|
new_id = "feature_refined"
|
||||||
|
|
||||||
|
# This will fail with AttributeError until implemented in project_manager.py
|
||||||
|
project_manager.promote_take(self.project_dict, take_id, new_id)
|
||||||
|
|
||||||
|
# Asserts
|
||||||
|
self.assertNotIn(take_id, self.project_dict["discussion"]["discussions"])
|
||||||
|
self.assertIn(new_id, self.project_dict["discussion"]["discussions"])
|
||||||
|
self.assertEqual(self.project_dict["discussion"]["discussions"][new_id]["history"], ["User: Experimental"])
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
96
tests/test_discussion_takes_gui.py
Normal file
96
tests/test_discussion_takes_gui.py
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import pytest
|
||||||
|
from unittest.mock import MagicMock, patch, call
|
||||||
|
from src.gui_2 import App
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def app_instance():
|
||||||
|
with (
|
||||||
|
patch('src.models.load_config', return_value={'ai': {'provider': 'gemini', 'model': 'gemini-2.5-flash-lite'}, 'projects': {}}),
|
||||||
|
patch('src.models.save_config'),
|
||||||
|
patch('src.gui_2.project_manager'),
|
||||||
|
patch('src.gui_2.session_logger'),
|
||||||
|
patch('src.gui_2.immapp.run'),
|
||||||
|
patch('src.app_controller.AppController._load_active_project'),
|
||||||
|
patch('src.app_controller.AppController._fetch_models'),
|
||||||
|
patch.object(App, '_load_fonts'),
|
||||||
|
patch.object(App, '_post_init'),
|
||||||
|
patch('src.app_controller.AppController._prune_old_logs'),
|
||||||
|
patch('src.app_controller.AppController.start_services'),
|
||||||
|
patch('src.api_hooks.HookServer'),
|
||||||
|
patch('src.ai_client.set_provider'),
|
||||||
|
patch('src.ai_client.reset_session')
|
||||||
|
):
|
||||||
|
app = App()
|
||||||
|
# Setup project discussions
|
||||||
|
app.project = {
|
||||||
|
"discussion": {
|
||||||
|
"active": "main",
|
||||||
|
"discussions": {
|
||||||
|
"main": {"history": []},
|
||||||
|
"take_1": {"history": []},
|
||||||
|
"take_2": {"history": []}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
app.active_discussion = "main"
|
||||||
|
app.is_viewing_prior_session = False
|
||||||
|
app.ui_disc_new_name_input = ""
|
||||||
|
app.ui_disc_truncate_pairs = 1
|
||||||
|
yield app
|
||||||
|
|
||||||
|
def test_render_discussion_tabs(app_instance):
|
||||||
|
"""Verify that _render_discussion_panel uses tabs for discussions."""
|
||||||
|
with patch('src.gui_2.imgui') as mock_imgui:
|
||||||
|
# Setup defaults for common imgui calls to avoid unpacking errors
|
||||||
|
mock_imgui.collapsing_header.return_value = True
|
||||||
|
mock_imgui.begin_combo.return_value = False
|
||||||
|
mock_imgui.input_text.return_value = (False, "")
|
||||||
|
mock_imgui.input_int.return_value = (False, 0)
|
||||||
|
mock_imgui.button.return_value = False
|
||||||
|
mock_imgui.checkbox.return_value = (False, False)
|
||||||
|
mock_imgui.begin_child.return_value = True
|
||||||
|
mock_imgui.selectable.return_value = (False, False)
|
||||||
|
|
||||||
|
# Mock tab bar calls
|
||||||
|
mock_imgui.begin_tab_bar.return_value = True
|
||||||
|
mock_imgui.begin_tab_item.return_value = (False, False)
|
||||||
|
|
||||||
|
app_instance._render_discussion_panel()
|
||||||
|
|
||||||
|
# Check if begin_tab_bar was called
|
||||||
|
# This SHOULD fail if it's not implemented yet
|
||||||
|
mock_imgui.begin_tab_bar.assert_called_with("##discussion_tabs")
|
||||||
|
|
||||||
|
# Check if begin_tab_item was called for each discussion
|
||||||
|
names = sorted(["main", "take_1", "take_2"])
|
||||||
|
for name in names:
|
||||||
|
mock_imgui.begin_tab_item.assert_any_call(name)
|
||||||
|
|
||||||
|
def test_switching_discussion_via_tabs(app_instance):
|
||||||
|
"""Verify that clicking a tab switches the discussion."""
|
||||||
|
with patch('src.gui_2.imgui') as mock_imgui, \
|
||||||
|
patch('src.app_controller.AppController._switch_discussion') as mock_switch:
|
||||||
|
# Setup defaults
|
||||||
|
mock_imgui.collapsing_header.return_value = True
|
||||||
|
mock_imgui.begin_combo.return_value = False
|
||||||
|
mock_imgui.input_text.return_value = (False, "")
|
||||||
|
mock_imgui.input_int.return_value = (False, 0)
|
||||||
|
mock_imgui.button.return_value = False
|
||||||
|
mock_imgui.checkbox.return_value = (False, False)
|
||||||
|
mock_imgui.begin_child.return_value = True
|
||||||
|
mock_imgui.selectable.return_value = (False, False)
|
||||||
|
|
||||||
|
mock_imgui.begin_tab_bar.return_value = True
|
||||||
|
|
||||||
|
# Simulate 'take_1' being active/selected
|
||||||
|
def side_effect(name, flags=None):
|
||||||
|
if name == "take_1":
|
||||||
|
return (True, True)
|
||||||
|
return (False, True)
|
||||||
|
|
||||||
|
mock_imgui.begin_tab_item.side_effect = side_effect
|
||||||
|
|
||||||
|
app_instance._render_discussion_panel()
|
||||||
|
|
||||||
|
# If implemented with tabs, this should be called
|
||||||
|
mock_switch.assert_called_with("take_1")
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import pytest
|
|
||||||
from unittest.mock import patch, MagicMock
|
|
||||||
import OpenGL.GL as gl
|
|
||||||
|
|
||||||
def test_shader_manager_fbo_initialization():
|
|
||||||
with patch("src.shader_manager.gl") as mock_gl:
|
|
||||||
mock_gl.glGenFramebuffers.side_effect = [1, 2]
|
|
||||||
mock_gl.glGenTextures.side_effect = [3, 4]
|
|
||||||
mock_gl.glCheckFramebufferStatus.return_value = mock_gl.GL_FRAMEBUFFER_COMPLETE
|
|
||||||
|
|
||||||
from src.shader_manager import ShaderManager
|
|
||||||
manager = ShaderManager()
|
|
||||||
|
|
||||||
manager.setup_capture_fbo(800, 600)
|
|
||||||
|
|
||||||
assert manager.scene_fbo == 1
|
|
||||||
assert manager.blur_fbo == 2
|
|
||||||
assert manager.scene_tex == 3
|
|
||||||
assert manager.blur_tex == 4
|
|
||||||
assert mock_gl.glGenFramebuffers.call_count == 2
|
|
||||||
assert mock_gl.glGenTextures.call_count == 2
|
|
||||||
assert mock_gl.glCheckFramebufferStatus.call_count == 2
|
|
||||||
|
|
||||||
def test_shader_manager_capture_lifecycle():
|
|
||||||
with patch("src.shader_manager.gl") as mock_gl:
|
|
||||||
mock_gl.glCheckFramebufferStatus.return_value = mock_gl.GL_FRAMEBUFFER_COMPLETE
|
|
||||||
mock_gl.glGenFramebuffers.side_effect = [1, 2]
|
|
||||||
mock_gl.glGenTextures.side_effect = [3, 4]
|
|
||||||
from src.shader_manager import ShaderManager
|
|
||||||
manager = ShaderManager()
|
|
||||||
|
|
||||||
# Ensure setup is called on first capture
|
|
||||||
manager.capture_begin(1024, 768)
|
|
||||||
assert manager.fbo_width == 1024
|
|
||||||
assert manager.fbo_height == 768
|
|
||||||
# Should bind the blur FBO
|
|
||||||
mock_gl.glBindFramebuffer.assert_any_call(mock_gl.GL_FRAMEBUFFER, manager.blur_fbo)
|
|
||||||
|
|
||||||
mock_gl.glBindFramebuffer.reset_mock()
|
|
||||||
manager.capture_end()
|
|
||||||
# Verify unbind (glBindFramebuffer(..., 0))
|
|
||||||
mock_gl.glBindFramebuffer.assert_called_with(mock_gl.GL_FRAMEBUFFER, 0)
|
|
||||||
@@ -7,6 +7,7 @@ def test_file_item_fields():
|
|||||||
assert item.path == "src/models.py"
|
assert item.path == "src/models.py"
|
||||||
assert item.auto_aggregate is True
|
assert item.auto_aggregate is True
|
||||||
assert item.force_full is False
|
assert item.force_full is False
|
||||||
|
assert item.injected_at is None
|
||||||
|
|
||||||
def test_file_item_to_dict():
|
def test_file_item_to_dict():
|
||||||
"""Test that FileItem can be serialized to a dict."""
|
"""Test that FileItem can be serialized to a dict."""
|
||||||
@@ -14,7 +15,8 @@ def test_file_item_to_dict():
|
|||||||
expected = {
|
expected = {
|
||||||
"path": "test.py",
|
"path": "test.py",
|
||||||
"auto_aggregate": False,
|
"auto_aggregate": False,
|
||||||
"force_full": True
|
"force_full": True,
|
||||||
|
"injected_at": None
|
||||||
}
|
}
|
||||||
assert item.to_dict() == expected
|
assert item.to_dict() == expected
|
||||||
|
|
||||||
@@ -23,12 +25,14 @@ def test_file_item_from_dict():
|
|||||||
data = {
|
data = {
|
||||||
"path": "test.py",
|
"path": "test.py",
|
||||||
"auto_aggregate": False,
|
"auto_aggregate": False,
|
||||||
"force_full": True
|
"force_full": True,
|
||||||
|
"injected_at": 123.456
|
||||||
}
|
}
|
||||||
item = FileItem.from_dict(data)
|
item = FileItem.from_dict(data)
|
||||||
assert item.path == "test.py"
|
assert item.path == "test.py"
|
||||||
assert item.auto_aggregate is False
|
assert item.auto_aggregate is False
|
||||||
assert item.force_full is True
|
assert item.force_full is True
|
||||||
|
assert item.injected_at == 123.456
|
||||||
|
|
||||||
def test_file_item_from_dict_defaults():
|
def test_file_item_from_dict_defaults():
|
||||||
"""Test that FileItem.from_dict handles missing fields."""
|
"""Test that FileItem.from_dict handles missing fields."""
|
||||||
@@ -37,3 +41,4 @@ def test_file_item_from_dict_defaults():
|
|||||||
assert item.path == "test.py"
|
assert item.path == "test.py"
|
||||||
assert item.auto_aggregate is True
|
assert item.auto_aggregate is True
|
||||||
assert item.force_full is False
|
assert item.force_full is False
|
||||||
|
assert item.injected_at is None
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
import pytest
|
|
||||||
from unittest.mock import patch, MagicMock
|
|
||||||
|
|
||||||
def test_shader_manager_frosted_glass_compilation():
|
|
||||||
# Mock OpenGL before importing ShaderManager
|
|
||||||
with patch("src.shader_manager.gl") as mock_gl:
|
|
||||||
mock_gl.glCreateProgram.return_value = 1
|
|
||||||
mock_gl.glCreateShader.return_value = 2
|
|
||||||
mock_gl.glGetShaderiv.return_value = mock_gl.GL_TRUE
|
|
||||||
mock_gl.glGetProgramiv.return_value = mock_gl.GL_TRUE
|
|
||||||
|
|
||||||
from src.shader_manager import ShaderManager
|
|
||||||
manager = ShaderManager()
|
|
||||||
|
|
||||||
# This should fail initially because the method doesn't exist
|
|
||||||
manager.setup_frosted_glass_shader()
|
|
||||||
|
|
||||||
assert manager.blur_program is not None
|
|
||||||
assert mock_gl.glCreateProgram.called
|
|
||||||
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
|
||||||
53
tests/test_gui_discussion_tabs.py
Normal file
53
tests/test_gui_discussion_tabs.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import pytest
|
||||||
|
from unittest.mock import patch, MagicMock, PropertyMock
|
||||||
|
|
||||||
|
from src import gui_2
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_gui():
|
||||||
|
gui = gui_2.App()
|
||||||
|
gui.project = {
|
||||||
|
'discussion': {
|
||||||
|
'active': 'main',
|
||||||
|
'discussions': {
|
||||||
|
'main': {'history': []},
|
||||||
|
'main_take_1': {'history': []},
|
||||||
|
'other_topic': {'history': []}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gui.active_discussion = 'main'
|
||||||
|
gui.perf_profiling_enabled = False
|
||||||
|
gui.is_viewing_prior_session = False
|
||||||
|
gui._get_discussion_names = lambda: ['main', 'main_take_1', 'other_topic']
|
||||||
|
return gui
|
||||||
|
|
||||||
|
def test_discussion_tabs_rendered(mock_gui):
|
||||||
|
with patch('src.gui_2.imgui') as mock_imgui, \
|
||||||
|
patch('src.app_controller.AppController.active_project_root', new_callable=PropertyMock, return_value='.'):
|
||||||
|
|
||||||
|
# We expect a combo box for base discussion
|
||||||
|
mock_imgui.begin_combo.return_value = True
|
||||||
|
mock_imgui.selectable.return_value = (False, False)
|
||||||
|
|
||||||
|
# We expect a tab bar for takes
|
||||||
|
mock_imgui.begin_tab_bar.return_value = True
|
||||||
|
mock_imgui.begin_tab_item.return_value = (True, True)
|
||||||
|
mock_imgui.input_text.return_value = (False, "")
|
||||||
|
mock_imgui.input_text_multiline.return_value = (False, "")
|
||||||
|
mock_imgui.checkbox.return_value = (False, False)
|
||||||
|
mock_imgui.input_int.return_value = (False, 0)
|
||||||
|
|
||||||
|
mock_clipper = MagicMock()
|
||||||
|
mock_clipper.step.return_value = False
|
||||||
|
mock_imgui.ListClipper.return_value = mock_clipper
|
||||||
|
|
||||||
|
mock_gui._render_discussion_panel()
|
||||||
|
|
||||||
|
mock_imgui.begin_combo.assert_called_once_with("##disc_sel", 'main')
|
||||||
|
mock_imgui.begin_tab_bar.assert_called_once_with('discussion_takes_tabs')
|
||||||
|
|
||||||
|
calls = [c[0][0] for c in mock_imgui.begin_tab_item.call_args_list]
|
||||||
|
assert 'Original###main' in calls
|
||||||
|
assert 'Take 1###main_take_1' in calls
|
||||||
|
assert 'Synthesis###Synthesis' in calls
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
import pytest
|
|
||||||
import time
|
|
||||||
from unittest.mock import patch, MagicMock
|
|
||||||
|
|
||||||
def test_gui_frosted_background_call():
|
|
||||||
# Mock ShaderManager and OpenGL functions
|
|
||||||
with patch("src.gui_2.ShaderManager") as mock_sm_class, \
|
|
||||||
patch("src.gui_2.gl") as mock_gl, \
|
|
||||||
patch("src.gui_2.imgui") as mock_imgui, \
|
|
||||||
patch("src.gui_2.theme") as mock_theme:
|
|
||||||
|
|
||||||
mock_sm = mock_sm_class.return_value
|
|
||||||
mock_sm.blur_tex = 2
|
|
||||||
mock_theme.get_frosted_glass_enabled.return_value = True
|
|
||||||
|
|
||||||
mock_imgui.get_io().display_size = MagicMock(x=1920, y=1080)
|
|
||||||
|
|
||||||
from src.gui_2 import App
|
|
||||||
with patch.object(App, '__init__', return_value=None):
|
|
||||||
app = App()
|
|
||||||
app.shader_manager = mock_sm
|
|
||||||
|
|
||||||
# Simulate frame
|
|
||||||
app._render_frosted_background(pos=MagicMock(x=10, y=10), size=MagicMock(x=100, y=100))
|
|
||||||
|
|
||||||
# Now it should only call add_image
|
|
||||||
assert mock_imgui.get_window_draw_list().add_image.called
|
|
||||||
# It no longer calls these
|
|
||||||
assert not mock_sm.setup_capture_fbo.called
|
|
||||||
assert not mock_sm.render_blur.called
|
|
||||||
assert not mock_sm.capture_begin.called
|
|
||||||
assert not mock_sm.capture_end.called
|
|
||||||
|
|
||||||
def test_gui_global_blur_call():
|
|
||||||
with patch("src.gui_2.ShaderManager") as mock_sm_class, \
|
|
||||||
patch("src.gui_2.imgui") as mock_imgui, \
|
|
||||||
patch("src.gui_2.theme") as mock_theme, \
|
|
||||||
patch("src.gui_2.bg_shader") as mock_bg:
|
|
||||||
|
|
||||||
mock_sm = mock_sm_class.return_value
|
|
||||||
mock_theme.get_frosted_glass_enabled.return_value = True
|
|
||||||
mock_theme.is_nerv_active.return_value = False
|
|
||||||
mock_imgui.get_io().display_size = MagicMock(x=1920, y=1080)
|
|
||||||
mock_bg.get_bg.return_value = MagicMock(enabled=False)
|
|
||||||
|
|
||||||
from src.gui_2 import App
|
|
||||||
with patch.object(App, '__init__', return_value=None):
|
|
||||||
app = App()
|
|
||||||
app.shader_manager = mock_sm
|
|
||||||
app.shader_uniforms = {
|
|
||||||
'frosted_blur_radius': 10.0,
|
|
||||||
'frosted_tint_intensity': 0.5,
|
|
||||||
'frosted_opacity': 0.8
|
|
||||||
}
|
|
||||||
app.ai_status = "idle"
|
|
||||||
app.ui_crt_filter = False
|
|
||||||
app.controller = MagicMock()
|
|
||||||
app.perf_profiling_enabled = False
|
|
||||||
app.is_viewing_prior_session = False
|
|
||||||
app.config = {}
|
|
||||||
app.project = {}
|
|
||||||
app.show_windows = {}
|
|
||||||
app.ui_auto_scroll_comms = False
|
|
||||||
app._comms_log_dirty = False
|
|
||||||
app._tool_log_dirty = False
|
|
||||||
app._pending_comms_lock = MagicMock()
|
|
||||||
app._pending_comms = []
|
|
||||||
app.ui_focus_agent = None
|
|
||||||
app._last_ui_focus_agent = None
|
|
||||||
app.perf_monitor = MagicMock()
|
|
||||||
app._process_pending_gui_tasks = MagicMock()
|
|
||||||
app._process_pending_history_adds = MagicMock()
|
|
||||||
app._render_track_proposal_modal = MagicMock()
|
|
||||||
app._render_patch_modal = MagicMock()
|
|
||||||
app._render_save_preset_modal = MagicMock()
|
|
||||||
app._render_preset_manager_window = MagicMock()
|
|
||||||
app._render_tool_preset_manager_window = MagicMock()
|
|
||||||
app._render_persona_editor_window = MagicMock()
|
|
||||||
app._last_autosave = time.time()
|
|
||||||
app._autosave_interval = 60
|
|
||||||
app._render_custom_title_bar = MagicMock()
|
|
||||||
app._render_shader_live_editor = MagicMock()
|
|
||||||
|
|
||||||
try:
|
|
||||||
app._gui_func()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
assert mock_sm.prepare_global_blur.called
|
|
||||||
@@ -91,6 +91,7 @@ def test_track_discussion_toggle(mock_app: App):
|
|||||||
mock_imgui.button.return_value = False
|
mock_imgui.button.return_value = False
|
||||||
mock_imgui.collapsing_header.return_value = True # For Discussions header
|
mock_imgui.collapsing_header.return_value = True # For Discussions header
|
||||||
mock_imgui.input_text.side_effect = lambda label, value, *args, **kwargs: (False, value)
|
mock_imgui.input_text.side_effect = lambda label, value, *args, **kwargs: (False, value)
|
||||||
|
mock_imgui.input_text_multiline.side_effect = lambda label, value, *args, **kwargs: (False, value)
|
||||||
mock_imgui.input_int.side_effect = lambda label, value, *args, **kwargs: (False, value)
|
mock_imgui.input_int.side_effect = lambda label, value, *args, **kwargs: (False, value)
|
||||||
mock_imgui.begin_child.return_value = True
|
mock_imgui.begin_child.return_value = True
|
||||||
# Mock clipper to avoid the while loop hang
|
# Mock clipper to avoid the while loop hang
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ def test_render_discussion_panel_symbol_lookup(mock_app, role):
|
|||||||
with (
|
with (
|
||||||
patch('src.gui_2.imgui') as mock_imgui,
|
patch('src.gui_2.imgui') as mock_imgui,
|
||||||
patch('src.gui_2.mcp_client') as mock_mcp,
|
patch('src.gui_2.mcp_client') as mock_mcp,
|
||||||
patch('src.gui_2.project_manager') as mock_pm
|
patch('src.gui_2.project_manager') as mock_pm,
|
||||||
|
patch('src.markdown_helper.imgui_md') as mock_md
|
||||||
):
|
):
|
||||||
# Set up App instance state
|
# Set up App instance state
|
||||||
mock_app.perf_profiling_enabled = False
|
mock_app.perf_profiling_enabled = False
|
||||||
|
|||||||
56
tests/test_gui_synthesis.py
Normal file
56
tests/test_gui_synthesis.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import pytest
|
||||||
|
from unittest.mock import MagicMock, patch, ANY
|
||||||
|
from src.gui_2 import App
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def app_instance():
|
||||||
|
with (
|
||||||
|
patch('src.models.load_config', return_value={'ai': {'provider': 'gemini', 'model': 'gemini-2.5-flash-lite'}, 'projects': {}}),
|
||||||
|
patch('src.models.save_config'),
|
||||||
|
patch('src.gui_2.project_manager'),
|
||||||
|
patch('src.gui_2.session_logger'),
|
||||||
|
patch('src.gui_2.immapp.run'),
|
||||||
|
patch('src.app_controller.AppController._load_active_project'),
|
||||||
|
patch('src.app_controller.AppController._fetch_models'),
|
||||||
|
patch.object(App, '_load_fonts'),
|
||||||
|
patch.object(App, '_post_init'),
|
||||||
|
patch('src.app_controller.AppController._prune_old_logs'),
|
||||||
|
patch('src.app_controller.AppController.start_services'),
|
||||||
|
patch('src.api_hooks.HookServer'),
|
||||||
|
patch('src.ai_client.set_provider'),
|
||||||
|
patch('src.ai_client.reset_session')
|
||||||
|
):
|
||||||
|
app = App()
|
||||||
|
app.project = {
|
||||||
|
"discussion": {
|
||||||
|
"active": "main",
|
||||||
|
"discussions": {
|
||||||
|
"main": {"history": []},
|
||||||
|
"take_1": {"history": []},
|
||||||
|
"take_2": {"history": []}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
app.ui_synthesis_prompt = "Summarize these takes"
|
||||||
|
yield app
|
||||||
|
|
||||||
|
def test_render_synthesis_panel(app_instance):
|
||||||
|
"""Verify that _render_synthesis_panel renders checkboxes for takes and input for prompt."""
|
||||||
|
with patch('src.gui_2.imgui') as mock_imgui:
|
||||||
|
mock_imgui.checkbox.return_value = (False, False)
|
||||||
|
mock_imgui.input_text_multiline.return_value = (False, app_instance.ui_synthesis_prompt)
|
||||||
|
mock_imgui.button.return_value = False
|
||||||
|
|
||||||
|
# Call the method we are testing
|
||||||
|
app_instance._render_synthesis_panel()
|
||||||
|
|
||||||
|
# 1. Assert imgui.checkbox is called for each take in project_dict['discussion']['discussions']
|
||||||
|
discussions = app_instance.project['discussion']['discussions']
|
||||||
|
for name in discussions:
|
||||||
|
mock_imgui.checkbox.assert_any_call(name, ANY)
|
||||||
|
|
||||||
|
# 2. Assert imgui.input_text_multiline is called for the prompt
|
||||||
|
mock_imgui.input_text_multiline.assert_called_with("##synthesis_prompt", app_instance.ui_synthesis_prompt, ANY)
|
||||||
|
|
||||||
|
# 3. Assert imgui.button is called for 'Generate Synthesis'
|
||||||
|
mock_imgui.button.assert_any_call("Generate Synthesis")
|
||||||
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
|
||||||
@@ -5,7 +5,7 @@ from src.gui_2 import App
|
|||||||
|
|
||||||
|
|
||||||
def _make_app(**kwargs):
|
def _make_app(**kwargs):
|
||||||
app = MagicMock(spec=App)
|
app = MagicMock()
|
||||||
app.mma_streams = kwargs.get("mma_streams", {})
|
app.mma_streams = kwargs.get("mma_streams", {})
|
||||||
app.mma_tier_usage = kwargs.get("mma_tier_usage", {
|
app.mma_tier_usage = kwargs.get("mma_tier_usage", {
|
||||||
"Tier 1": {"input": 0, "output": 0, "model": "gemini-3.1-pro-preview"},
|
"Tier 1": {"input": 0, "output": 0, "model": "gemini-3.1-pro-preview"},
|
||||||
@@ -13,6 +13,7 @@ def _make_app(**kwargs):
|
|||||||
"Tier 3": {"input": 0, "output": 0, "model": "gemini-2.5-flash-lite"},
|
"Tier 3": {"input": 0, "output": 0, "model": "gemini-2.5-flash-lite"},
|
||||||
"Tier 4": {"input": 0, "output": 0, "model": "gemini-2.5-flash-lite"},
|
"Tier 4": {"input": 0, "output": 0, "model": "gemini-2.5-flash-lite"},
|
||||||
})
|
})
|
||||||
|
app.ui_focus_agent = kwargs.get("ui_focus_agent", None)
|
||||||
app.tracks = kwargs.get("tracks", [])
|
app.tracks = kwargs.get("tracks", [])
|
||||||
app.active_track = kwargs.get("active_track", None)
|
app.active_track = kwargs.get("active_track", None)
|
||||||
app.active_tickets = kwargs.get("active_tickets", [])
|
app.active_tickets = kwargs.get("active_tickets", [])
|
||||||
|
|||||||
59
tests/test_synthesis_formatter.py
Normal file
59
tests/test_synthesis_formatter.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import pytest
|
||||||
|
from src.synthesis_formatter import format_takes_diff
|
||||||
|
|
||||||
|
def test_format_takes_diff_empty():
|
||||||
|
assert format_takes_diff({}) == ""
|
||||||
|
|
||||||
|
def test_format_takes_diff_single_take():
|
||||||
|
takes = {
|
||||||
|
"take1": [
|
||||||
|
{"role": "user", "content": "hello"},
|
||||||
|
{"role": "assistant", "content": "hi"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
expected = "=== Shared History ===\nuser: hello\nassistant: hi\n\n=== Variations ===\n"
|
||||||
|
assert format_takes_diff(takes) == expected
|
||||||
|
|
||||||
|
def test_format_takes_diff_common_prefix():
|
||||||
|
takes = {
|
||||||
|
"take1": [
|
||||||
|
{"role": "user", "content": "hello"},
|
||||||
|
{"role": "assistant", "content": "hi"},
|
||||||
|
{"role": "user", "content": "how are you?"},
|
||||||
|
{"role": "assistant", "content": "I am fine."}
|
||||||
|
],
|
||||||
|
"take2": [
|
||||||
|
{"role": "user", "content": "hello"},
|
||||||
|
{"role": "assistant", "content": "hi"},
|
||||||
|
{"role": "user", "content": "what is the time?"},
|
||||||
|
{"role": "assistant", "content": "It is noon."}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
expected = (
|
||||||
|
"=== Shared History ===\n"
|
||||||
|
"user: hello\n"
|
||||||
|
"assistant: hi\n\n"
|
||||||
|
"=== Variations ===\n"
|
||||||
|
"[take1]\n"
|
||||||
|
"user: how are you?\n"
|
||||||
|
"assistant: I am fine.\n\n"
|
||||||
|
"[take2]\n"
|
||||||
|
"user: what is the time?\n"
|
||||||
|
"assistant: It is noon.\n"
|
||||||
|
)
|
||||||
|
assert format_takes_diff(takes) == expected
|
||||||
|
|
||||||
|
def test_format_takes_diff_no_common_prefix():
|
||||||
|
takes = {
|
||||||
|
"take1": [{"role": "user", "content": "a"}],
|
||||||
|
"take2": [{"role": "user", "content": "b"}]
|
||||||
|
}
|
||||||
|
expected = (
|
||||||
|
"=== Shared History ===\n\n"
|
||||||
|
"=== Variations ===\n"
|
||||||
|
"[take1]\n"
|
||||||
|
"user: a\n\n"
|
||||||
|
"[take2]\n"
|
||||||
|
"user: b\n"
|
||||||
|
)
|
||||||
|
assert format_takes_diff(takes) == expected
|
||||||
@@ -33,18 +33,3 @@ def test_theme_apply_sets_rounding_and_padding(monkeypatch):
|
|||||||
assert mock_style.item_spacing == (8.0, 4.0)
|
assert mock_style.item_spacing == (8.0, 4.0)
|
||||||
assert mock_style.item_inner_spacing == (4.0, 4.0)
|
assert mock_style.item_inner_spacing == (4.0, 4.0)
|
||||||
assert mock_style.scrollbar_size == 14.0
|
assert mock_style.scrollbar_size == 14.0
|
||||||
|
|
||||||
def test_frosted_glass_enabled_toggle():
|
|
||||||
theme.set_frosted_glass_enabled(True)
|
|
||||||
assert theme.get_frosted_glass_enabled() is True
|
|
||||||
theme.set_frosted_glass_enabled(False)
|
|
||||||
assert theme.get_frosted_glass_enabled() is False
|
|
||||||
|
|
||||||
def test_theme_config_frosted_glass():
|
|
||||||
config = {"theme": {"frosted_glass_enabled": True}}
|
|
||||||
theme.load_from_config(config)
|
|
||||||
assert theme.get_frosted_glass_enabled() is True
|
|
||||||
|
|
||||||
new_config = {}
|
|
||||||
theme.save_to_config(new_config)
|
|
||||||
assert new_config["theme"]["frosted_glass_enabled"] is True
|
|
||||||
|
|||||||
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