Compare commits
5 Commits
d8a4ec121d
...
451d19126f
| Author | SHA1 | Date | |
|---|---|---|---|
| 451d19126f | |||
| 9323983881 | |||
| cd3b0ff277 | |||
| 95381c258c | |||
| e2a403a187 |
@@ -69,3 +69,8 @@ For deep implementation details when planning or implementing tracks, consult `d
|
|||||||
- **Context & Token Visualization:** Detailed UI panels for monitoring real-time token usage, history depth, and **visual cache awareness** (tracking specific files currently live in the provider's context cache).
|
- **Context & Token Visualization:** Detailed UI panels for monitoring real-time token usage, history depth, and **visual cache awareness** (tracking specific files currently live in the provider's context cache).
|
||||||
- **On-Demand Definition Lookup:** Allows developers to request specific class or function definitions during discussions using `@SymbolName` syntax. Injected definitions feature syntax highlighting, intelligent collapsing for long blocks, and a **[Source]** button for instant navigation to the full file.
|
- **On-Demand Definition Lookup:** Allows developers to request specific class or function definitions during discussions using `@SymbolName` syntax. Injected definitions feature syntax highlighting, intelligent collapsing for long blocks, and a **[Source]** button for instant navigation to the full file.
|
||||||
- **Manual Ticket Queue Management:** Provides a dedicated GUI panel for granular control over the implementation queue. Features include color-coded priority assignment (High, Medium, Low), multi-select bulk operations (Execute, Skip, Block), and interactive drag-and-drop reordering with real-time Directed Acyclic Graph (DAG) validation.
|
- **Manual Ticket Queue Management:** Provides a dedicated GUI panel for granular control over the implementation queue. Features include color-coded priority assignment (High, Medium, Low), multi-select bulk operations (Execute, Skip, Block), and interactive drag-and-drop reordering with real-time Directed Acyclic Graph (DAG) validation.
|
||||||
|
- **System Prompt Presets:** Comprehensive management system for saving and switching between complex system prompt configurations.
|
||||||
|
- **Scoped Inheritance:** Supports **Global** (application-wide) and **Project-Specific** presets. Project presets with the same name automatically override global counterparts, allowing for fine-tuned context tailoring.
|
||||||
|
- **Full AI Profiles:** Presets capture not only the system prompt text but also critical model parameters like **Temperature**, **Top-P**, and **Max Output Tokens**.
|
||||||
|
- **Preset Manager Modal:** A dedicated high-density GUI for creating, editing, and deleting presets with real-time validation and instant application to the active session.
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,9 @@
|
|||||||
|
|
||||||
- **src/paths.py:** Centralized module for path resolution, allowing directory paths (logs, conductor, scripts) to be configured via `config.toml` or environment variables, eliminating hardcoded filesystem dependencies.
|
- **src/paths.py:** Centralized module for path resolution, allowing directory paths (logs, conductor, scripts) to be configured via `config.toml` or environment variables, eliminating hardcoded filesystem dependencies.
|
||||||
|
|
||||||
|
- **src/presets.py:** Implements `PresetManager` for high-performance CRUD operations on system prompt presets stored in TOML format (`presets.toml`, `project_presets.toml`). Supports dynamic path resolution and scope-based inheritance.
|
||||||
|
|
||||||
|
|
||||||
- **tree-sitter / AST Parsing:** For deterministic AST parsing and automated generation of curated "Skeleton Views" and "Targeted Views" (extracting specific functions and their dependencies). Features an integrated AST cache with mtime-based invalidation to minimize re-parsing overhead.
|
- **tree-sitter / AST Parsing:** For deterministic AST parsing and automated generation of curated "Skeleton Views" and "Targeted Views" (extracting specific functions and their dependencies). Features an integrated AST cache with mtime-based invalidation to minimize re-parsing overhead.
|
||||||
- **pydantic / dataclasses:** For defining strict state schemas (Tracks, Tickets) used in linear orchestration.
|
- **pydantic / dataclasses:** For defining strict state schemas (Tracks, Tickets) used in linear orchestration.
|
||||||
- **tomli-w:** For writing TOML configuration files.
|
- **tomli-w:** For writing TOML configuration files.
|
||||||
|
|||||||
+1
-1
@@ -85,7 +85,7 @@ This file tracks all major tracks for the project. Each track has its own detail
|
|||||||
|
|
||||||
### Manual UX Controls
|
### Manual UX Controls
|
||||||
|
|
||||||
1. [ ] **Track: Saved System Prompt Presets**
|
1. [x] **Track: Saved System Prompt Presets**
|
||||||
*Link: [./tracks/saved_presets_20260308/](./tracks/saved_presets_20260308/)*
|
*Link: [./tracks/saved_presets_20260308/](./tracks/saved_presets_20260308/)*
|
||||||
*Goal: Ability to have saved presets for global and project system prompts. Includes full AI profiles with temperature and top_p settings, managed via a dedicated GUI modal.*
|
*Goal: Ability to have saved presets for global and project system prompts. Includes full AI profiles with temperature and top_p settings, managed via a dedicated GUI modal.*
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Track Debrief: Saved System Prompt Presets
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
Implemented foundational "System Prompt Presets" with scoped inheritance (Global/Project) and integrated model parameters (Temperature, Top-P, Max Tokens).
|
||||||
|
|
||||||
|
## Conceptual Dilemma
|
||||||
|
During implementation, a conflict was identified between "Prompt Presets" and "Model Settings." Selecting a preset from a prompt dropdown currently overrides global model parameters, which is unintuitive when multiple prompts (Global, Project, MMA) contribute to a single agent turn.
|
||||||
|
|
||||||
|
## Future Direction: Agent Personas
|
||||||
|
To resolve this, we will move toward a **Unified Persona** model.
|
||||||
|
- **Consolidation:** Provider, Model, Parameters, Prompts (all scopes), and Tool Presets will be grouped into a single "Persona" object.
|
||||||
|
- **UI Overhaul:** The "AI Settings" panel will be refactored to focus on "Active Persona" selection rather than fragmented prompt/model controls.
|
||||||
|
- **MMA Integration:** MMA agents will eventually be assigned specific Personas, allowing for differentiated behaviors (e.g., a "Creative Worker" vs. a "Strict QA").
|
||||||
|
|
||||||
|
## Implementation Sequence
|
||||||
|
1. **Track: Saved Tool Presets** (Upcoming)
|
||||||
|
2. **Track: Agent Tool Preference & Bias Tuning** (Upcoming)
|
||||||
|
3. **Track: Agent Personas: Unified Profiles & Tool Presets** (Final Consolidation) - *This track will consume the findings from this debrief and the components from the preceding tracks.*
|
||||||
@@ -1,46 +1,46 @@
|
|||||||
# Implementation Plan: Saved System Prompt Presets
|
# Implementation Plan: Saved System Prompt Presets
|
||||||
|
|
||||||
## Phase 1: Foundation & Data Model
|
## Phase 1: Foundation & Data Model
|
||||||
- [ ] Task: Define the `Preset` data model and storage logic.
|
- [x] Task: Define the `Preset` data model and storage logic.
|
||||||
- [ ] Create `src/models.py` (if not existing) or update it with a `Preset` dataclass/Pydantic model.
|
- [x] Create `src/models.py` (if not existing) or update it with a `Preset` dataclass/Pydantic model.
|
||||||
- [ ] Implement `PresetManager` in a new file `src/presets.py` to handle loading/saving to `presets.toml` and `project_presets.toml`.
|
- [x] Implement `PresetManager` in a new file `src/presets.py` to handle loading/saving to `presets.toml` and `project_presets.toml`.
|
||||||
- [ ] Implement the inheritance logic where project presets override global ones.
|
- [x] Implement the inheritance logic where project presets override global ones.
|
||||||
- [ ] Task: Write unit tests for `PresetManager`.
|
- [x] Task: Write unit tests for `PresetManager`.
|
||||||
- [ ] Test loading global presets.
|
- [x] Test loading global presets.
|
||||||
- [ ] Test loading project presets.
|
- [x] Test loading project presets.
|
||||||
- [ ] Test the override logic (same name).
|
- [x] Test the override logic (same name).
|
||||||
- [ ] Test saving/updating presets.
|
- [x] Test saving/updating presets.
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Phase 1: Foundation & Data Model' (Protocol in workflow.md)
|
- [x] Task: Conductor - User Manual Verification 'Phase 1: Foundation & Data Model' (Protocol in workflow.md)
|
||||||
|
|
||||||
## Phase 2: UI: Settings Integration
|
## Phase 2: UI: Settings Integration
|
||||||
- [ ] Task: Add Preset Dropdown to Global AI Settings.
|
- [x] Task: Add Preset Dropdown to Global AI Settings.
|
||||||
- [ ] Modify `gui_2.py` to include a dropdown in the "AI Settings" panel.
|
- [x] Modify `gui_2.py` to include a dropdown in the "AI Settings" panel.
|
||||||
- [ ] Populated the dropdown with available global presets.
|
- [x] Populated the dropdown with available global presets.
|
||||||
- [ ] Task: Add Preset Dropdown to Project Settings.
|
- [x] Task: Add Preset Dropdown to Project Settings.
|
||||||
- [ ] Modify `gui_2.py` to include a dropdown in the "Project Settings" panel.
|
- [x] Modify `gui_2.py` to include a dropdown in the "Project Settings" panel.
|
||||||
- [ ] Populated the dropdown with available project-specific presets (including overridden globals).
|
- [x] Populated the dropdown with available project-specific presets (including overridden globals).
|
||||||
- [ ] Task: Implement "Auto-Load" logic.
|
- [x] Task: Implement "Auto-Load" logic.
|
||||||
- [ ] When a preset is selected, update the active system prompt and model settings in `gui_2.py`.
|
- [x] When a preset is selected, update the active system prompt and model settings in `gui_2.py`.
|
||||||
- [ ] Task: Write integration tests for settings integration using `live_gui`.
|
- [x] Task: Write integration tests for settings integration using `live_gui`.
|
||||||
- [ ] Verify global dropdown shows global presets.
|
- [x] Verify global dropdown shows global presets.
|
||||||
- [ ] Verify project dropdown shows project + global presets.
|
- [x] Verify project dropdown shows project + global presets.
|
||||||
- [ ] Verify selecting a preset updates the UI fields (prompt, temperature).
|
- [x] Verify selecting a preset updates the UI fields (prompt, temperature).
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Phase 2: UI: Settings Integration' (Protocol in workflow.md)
|
- [x] Task: Conductor - User Manual Verification 'Phase 2: UI: Settings Integration' (Protocol in workflow.md)
|
||||||
|
|
||||||
## Phase 3: UI: Preset Manager Modal
|
## Phase 3: UI: Preset Manager Modal
|
||||||
- [ ] Task: Create the `PresetManagerModal` in `gui_2.py` (or a separate module).
|
- [x] Task: Create the `PresetManagerModal` in `gui_2.py` (or a separate module).
|
||||||
- [ ] Implement a list view of all presets (global and project).
|
- [x] Implement a list view of all presets (global and project).
|
||||||
- [ ] Implement "Add", "Edit", and "Delete" functionality.
|
- [x] Implement "Add", "Edit", and "Delete" functionality.
|
||||||
- [ ] Ensure validation for unique names.
|
- [x] Ensure validation for unique names.
|
||||||
- [ ] Task: Add a button to open the manager modal from the settings panels.
|
- [x] Task: Add a button to open the manager modal from the settings panels.
|
||||||
- [ ] Task: Write integration tests for the Preset Manager using `live_gui`.
|
- [x] Task: Write integration tests for the Preset Manager using `live_gui`.
|
||||||
- [ ] Verify creating a new preset adds it to the list and dropdown.
|
- [x] Verify creating a new preset adds it to the list and dropdown.
|
||||||
- [ ] Verify editing an existing preset updates it correctly.
|
- [x] Verify editing an existing preset updates it correctly.
|
||||||
- [ ] Verify deleting a preset removes it from the list and dropdown.
|
- [x] Verify deleting a preset removes it from the list and dropdown.
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Phase 3: UI: Preset Manager Modal' (Protocol in workflow.md)
|
- [x] Task: Conductor - User Manual Verification 'Phase 3: UI: Preset Manager Modal' (Protocol in workflow.md)
|
||||||
|
|
||||||
## Phase 4: Final Integration & Polish
|
## Phase 4: Final Integration & Polish
|
||||||
- [ ] Task: Ensure robust error handling for missing or malformed `.toml` files.
|
- [x] Task: Ensure robust error handling for missing or malformed `.toml` files.
|
||||||
- [ ] Task: Final UI polish (spacing, icons, tooltips).
|
- [x] Task: Final UI polish (spacing, icons, tooltips).
|
||||||
- [ ] Task: Run full suite of relevant tests.
|
- [x] Task: Run full suite of relevant tests.
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Phase 4: Final Integration & Polish' (Protocol in workflow.md)
|
- [x] Task: Conductor - User Manual Verification 'Phase 4: Final Integration & Polish' (Protocol in workflow.md)
|
||||||
|
|||||||
@@ -33,6 +33,14 @@ This feature adds the ability to create, save, and manage "Tool Presets" for age
|
|||||||
- [ ] The AI Settings panel correctly reflects the categorized tool list.
|
- [ ] The AI Settings panel correctly reflects the categorized tool list.
|
||||||
|
|
||||||
## Out of Scope
|
## Out of Scope
|
||||||
- Support for other file formats (e.g., JSON, YAML) for tool presets.
|
- Support for other file formats (e.g., JSON, YAML) for presets.
|
||||||
- Presets for specific files or folders (scoped only to global or project level).
|
- Presets for specific files or folders (scoped only to global or project level).
|
||||||
- Cloud syncing of tool presets.
|
- Cloud syncing of presets.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Technical Note: Future Persona Consolidation
|
||||||
|
This track is a prerequisite for the **"Agent Personas: Unified Profiles & Tool Presets"** overhaul. Implementation should align with the modular preset pattern established in `src/presets.py`.
|
||||||
|
|
||||||
|
Consult the **Debrief** in `conductor/tracks/saved_presets_20260308/debrief.md` for context on how these tool presets will eventually be merged with system prompts and model parameters into a unified configuration panel.
|
||||||
|
|
||||||
|
|||||||
@@ -38,3 +38,11 @@ This track introduces a mechanism to influence AI agent tool selection by implem
|
|||||||
## Out of Scope
|
## Out of Scope
|
||||||
- Implementing reinforcement learning to "learn" tool weights automatically.
|
- Implementing reinforcement learning to "learn" tool weights automatically.
|
||||||
- Hardcoding weights into the AI client (all weights must be user-configurable via presets).
|
- Hardcoding weights into the AI client (all weights must be user-configurable via presets).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Technical Note: Future Persona Consolidation
|
||||||
|
This track is a prerequisite for the **"Agent Personas: Unified Profiles & Tool Presets"** overhaul. The weighting system implemented here must be architected to be saveable within a unified "Persona" profile.
|
||||||
|
|
||||||
|
Consult the **Debrief** in `conductor/tracks/saved_presets_20260308/debrief.md` for context on how these bias settings will eventually be merged with system prompts and tool presets into a single configuration entity.
|
||||||
|
|
||||||
|
|||||||
+5
-4
@@ -22,6 +22,7 @@ separate_message_panel = false
|
|||||||
separate_response_panel = false
|
separate_response_panel = false
|
||||||
separate_tool_calls_panel = false
|
separate_tool_calls_panel = false
|
||||||
bg_shader_enabled = true
|
bg_shader_enabled = true
|
||||||
|
crt_filter_enabled = false
|
||||||
|
|
||||||
[gui.show_windows]
|
[gui.show_windows]
|
||||||
"Context Hub" = true
|
"Context Hub" = true
|
||||||
@@ -39,15 +40,15 @@ Response = false
|
|||||||
"Tool Calls" = false
|
"Tool Calls" = false
|
||||||
Theme = true
|
Theme = true
|
||||||
"Log Management" = true
|
"Log Management" = true
|
||||||
Diagnostics = false
|
Diagnostics = true
|
||||||
|
|
||||||
[theme]
|
[theme]
|
||||||
palette = "Nord Dark"
|
palette = "Nord Dark"
|
||||||
font_path = "C:/projects/manual_slop/assets/fonts/Inter-Regular.ttf"
|
font_path = "C:/projects/manual_slop/assets/fonts/Inter-Regular.ttf"
|
||||||
font_size = 12.0
|
font_size = 12.0
|
||||||
scale = 1.25
|
scale = 1.2999999523162842
|
||||||
transparency = 0.6499999761581421
|
transparency = 0.550000011920929
|
||||||
child_transparency = 0.7200000286102295
|
child_transparency = 0.6399999856948853
|
||||||
|
|
||||||
[mma]
|
[mma]
|
||||||
max_workers = 4
|
max_workers = 4
|
||||||
|
|||||||
+59
-65
@@ -44,12 +44,12 @@ Collapsed=0
|
|||||||
DockId=0x00000001,0
|
DockId=0x00000001,0
|
||||||
|
|
||||||
[Window][Message]
|
[Window][Message]
|
||||||
Pos=1005,726
|
Pos=1890,1100
|
||||||
Size=1065,548
|
Size=1065,548
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Response]
|
[Window][Response]
|
||||||
Pos=1009,1217
|
Pos=2086,1780
|
||||||
Size=1036,351
|
Size=1036,351
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
@@ -73,8 +73,8 @@ Collapsed=0
|
|||||||
DockId=0xAFC85805,2
|
DockId=0xAFC85805,2
|
||||||
|
|
||||||
[Window][Theme]
|
[Window][Theme]
|
||||||
Pos=0,26
|
Pos=0,30
|
||||||
Size=848,827
|
Size=762,943
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000005,1
|
DockId=0x00000005,1
|
||||||
|
|
||||||
@@ -84,14 +84,14 @@ Size=900,700
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Diagnostics]
|
[Window][Diagnostics]
|
||||||
Pos=2622,27
|
Pos=2674,26
|
||||||
Size=1218,2110
|
Size=1166,1678
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000004,1
|
DockId=0x0000000C,2
|
||||||
|
|
||||||
[Window][Context Hub]
|
[Window][Context Hub]
|
||||||
Pos=0,26
|
Pos=0,30
|
||||||
Size=848,827
|
Size=762,943
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000005,0
|
DockId=0x00000005,0
|
||||||
|
|
||||||
@@ -102,26 +102,26 @@ Collapsed=0
|
|||||||
DockId=0x0000000D,0
|
DockId=0x0000000D,0
|
||||||
|
|
||||||
[Window][Discussion Hub]
|
[Window][Discussion Hub]
|
||||||
Pos=1736,26
|
Pos=1668,30
|
||||||
Size=884,1463
|
Size=1004,2107
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000013,0
|
DockId=0x00000013,0
|
||||||
|
|
||||||
[Window][Operations Hub]
|
[Window][Operations Hub]
|
||||||
Pos=850,26
|
Pos=764,30
|
||||||
Size=884,1463
|
Size=902,2107
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000012,0
|
DockId=0x00000012,0
|
||||||
|
|
||||||
[Window][Files & Media]
|
[Window][Files & Media]
|
||||||
Pos=0,1885
|
Pos=0,1849
|
||||||
Size=848,252
|
Size=762,288
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000002,0
|
DockId=0x00000002,0
|
||||||
|
|
||||||
[Window][AI Settings]
|
[Window][AI Settings]
|
||||||
Pos=0,855
|
Pos=0,975
|
||||||
Size=848,1028
|
Size=762,872
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000001,0
|
DockId=0x00000001,0
|
||||||
|
|
||||||
@@ -131,16 +131,16 @@ Size=416,325
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][MMA Dashboard]
|
[Window][MMA Dashboard]
|
||||||
Pos=2622,26
|
Pos=2674,30
|
||||||
Size=1218,2111
|
Size=1166,1675
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000004,1
|
DockId=0x0000000C,0
|
||||||
|
|
||||||
[Window][Log Management]
|
[Window][Log Management]
|
||||||
Pos=2622,26
|
Pos=2674,30
|
||||||
Size=1218,2111
|
Size=1166,1675
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000004,0
|
DockId=0x0000000C,1
|
||||||
|
|
||||||
[Window][Track Proposal]
|
[Window][Track Proposal]
|
||||||
Pos=709,326
|
Pos=709,326
|
||||||
@@ -148,28 +148,28 @@ Size=262,209
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Tier 1: Strategy]
|
[Window][Tier 1: Strategy]
|
||||||
Pos=850,1491
|
Pos=2674,1707
|
||||||
Size=306,646
|
Size=1166,430
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000014,0
|
DockId=0x0000000F,0
|
||||||
|
|
||||||
[Window][Tier 2: Tech Lead]
|
[Window][Tier 2: Tech Lead]
|
||||||
Pos=1158,1491
|
Pos=2674,1707
|
||||||
Size=550,646
|
Size=1166,430
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000016,0
|
DockId=0x0000000F,1
|
||||||
|
|
||||||
[Window][Tier 4: QA]
|
[Window][Tier 4: QA]
|
||||||
Pos=2215,1491
|
Pos=2674,1707
|
||||||
Size=405,646
|
Size=1166,430
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000019,0
|
DockId=0x0000000F,2
|
||||||
|
|
||||||
[Window][Tier 3: Workers]
|
[Window][Tier 3: Workers]
|
||||||
Pos=1710,1491
|
Pos=764,30
|
||||||
Size=503,646
|
Size=902,2107
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000018,0
|
DockId=0x00000012,1
|
||||||
|
|
||||||
[Window][Approve PowerShell Command]
|
[Window][Approve PowerShell Command]
|
||||||
Pos=649,435
|
Pos=649,435
|
||||||
@@ -352,11 +352,11 @@ Column 3 Width=20
|
|||||||
Column 4 Weight=1.0000
|
Column 4 Weight=1.0000
|
||||||
|
|
||||||
[Table][0x2A6000B6,4]
|
[Table][0x2A6000B6,4]
|
||||||
RefScale=14
|
RefScale=18
|
||||||
Column 0 Width=42
|
Column 0 Width=54
|
||||||
Column 1 Width=64
|
Column 1 Width=82
|
||||||
Column 2 Weight=1.0000
|
Column 2 Weight=1.0000
|
||||||
Column 3 Width=107
|
Column 3 Width=137
|
||||||
|
|
||||||
[Table][0x8BCC69C7,6]
|
[Table][0x8BCC69C7,6]
|
||||||
RefScale=13
|
RefScale=13
|
||||||
@@ -375,11 +375,11 @@ Column 2 Weight=1.0000
|
|||||||
Column 3 Width=106
|
Column 3 Width=106
|
||||||
|
|
||||||
[Table][0x2C515046,4]
|
[Table][0x2C515046,4]
|
||||||
RefScale=14
|
RefScale=18
|
||||||
Column 0 Width=61
|
Column 0 Width=58
|
||||||
Column 1 Weight=1.0000
|
Column 1 Weight=1.0000
|
||||||
Column 2 Width=161
|
Column 2 Width=138
|
||||||
Column 3 Width=42
|
Column 3 Width=54
|
||||||
|
|
||||||
[Table][0xD99F45C5,4]
|
[Table][0xD99F45C5,4]
|
||||||
Column 0 Sort=0v
|
Column 0 Sort=0v
|
||||||
@@ -400,36 +400,30 @@ Column 1 Width=100
|
|||||||
Column 2 Weight=1.0000
|
Column 2 Weight=1.0000
|
||||||
|
|
||||||
[Table][0xA02D8C87,3]
|
[Table][0xA02D8C87,3]
|
||||||
RefScale=14
|
RefScale=16
|
||||||
Column 0 Width=160
|
Column 0 Width=182
|
||||||
Column 1 Width=106
|
Column 1 Width=120
|
||||||
Column 2 Weight=1.0000
|
Column 2 Weight=1.0000
|
||||||
|
|
||||||
[Docking][Data]
|
[Docking][Data]
|
||||||
DockNode ID=0x00000008 Pos=3125,170 Size=593,1157 Split=Y
|
DockNode ID=0x00000008 Pos=3125,170 Size=593,1157 Split=Y
|
||||||
DockNode ID=0x00000009 Parent=0x00000008 SizeRef=1029,147 Selected=0x0469CA7A
|
DockNode ID=0x00000009 Parent=0x00000008 SizeRef=1029,147 Selected=0x0469CA7A
|
||||||
DockNode ID=0x0000000A Parent=0x00000008 SizeRef=1029,145 Selected=0xDF822E02
|
DockNode ID=0x0000000A Parent=0x00000008 SizeRef=1029,145 Selected=0xDF822E02
|
||||||
DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,26 Size=3840,2111 Split=X
|
DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,30 Size=3840,2107 Split=X
|
||||||
DockNode ID=0x00000003 Parent=0xAFC85805 SizeRef=2620,1183 Split=X
|
DockNode ID=0x00000003 Parent=0xAFC85805 SizeRef=2672,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=848,858 Split=Y Selected=0x8CA2375C
|
DockNode ID=0x00000007 Parent=0x0000000B SizeRef=762,858 Split=Y Selected=0x8CA2375C
|
||||||
DockNode ID=0x00000005 Parent=0x00000007 SizeRef=295,827 Selected=0x8CA2375C
|
DockNode ID=0x00000005 Parent=0x00000007 SizeRef=295,943 Selected=0xF4139CA2
|
||||||
DockNode ID=0x00000006 Parent=0x00000007 SizeRef=295,1286 Split=Y Selected=0x7BD57D6A
|
DockNode ID=0x00000006 Parent=0x00000007 SizeRef=295,1168 Split=Y Selected=0x7BD57D6A
|
||||||
DockNode ID=0x00000001 Parent=0x00000006 SizeRef=824,1032 CentralNode=1 Selected=0x7BD57D6A
|
DockNode ID=0x00000001 Parent=0x00000006 SizeRef=824,872 CentralNode=1 Selected=0x7BD57D6A
|
||||||
DockNode ID=0x00000002 Parent=0x00000006 SizeRef=824,252 Selected=0x1DCB2623
|
DockNode ID=0x00000002 Parent=0x00000006 SizeRef=824,288 Selected=0x1DCB2623
|
||||||
DockNode ID=0x0000000E Parent=0x0000000B SizeRef=1770,858 Split=Y Selected=0x418C7449
|
DockNode ID=0x0000000E Parent=0x0000000B SizeRef=1908,858 Split=X Selected=0x418C7449
|
||||||
DockNode ID=0x00000010 Parent=0x0000000E SizeRef=868,1466 Split=X Selected=0x418C7449
|
DockNode ID=0x00000012 Parent=0x0000000E SizeRef=902,402 Selected=0x418C7449
|
||||||
DockNode ID=0x00000012 Parent=0x00000010 SizeRef=884,402 Selected=0x418C7449
|
DockNode ID=0x00000013 Parent=0x0000000E SizeRef=1004,402 Selected=0x6F2B5B04
|
||||||
DockNode ID=0x00000013 Parent=0x00000010 SizeRef=884,402 Selected=0x6F2B5B04
|
|
||||||
DockNode ID=0x00000011 Parent=0x0000000E SizeRef=868,647 Split=X Selected=0x5CDB7A4B
|
|
||||||
DockNode ID=0x00000014 Parent=0x00000011 SizeRef=306,837 Selected=0xBB346584
|
|
||||||
DockNode ID=0x00000015 Parent=0x00000011 SizeRef=1462,837 Split=X Selected=0x5CDB7A4B
|
|
||||||
DockNode ID=0x00000016 Parent=0x00000015 SizeRef=550,837 Selected=0x390E7942
|
|
||||||
DockNode ID=0x00000017 Parent=0x00000015 SizeRef=910,837 Split=X Selected=0x655BC6E9
|
|
||||||
DockNode ID=0x00000018 Parent=0x00000017 SizeRef=503,874 Selected=0x655BC6E9
|
|
||||||
DockNode ID=0x00000019 Parent=0x00000017 SizeRef=405,874 Selected=0x5CDB7A4B
|
|
||||||
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=1218,1183 Selected=0x2C0206CE
|
DockNode ID=0x00000004 Parent=0xAFC85805 SizeRef=1166,1183 Split=Y Selected=0x3AEC3498
|
||||||
|
DockNode ID=0x0000000C Parent=0x00000004 SizeRef=1074,1679 Selected=0x2C0206CE
|
||||||
|
DockNode ID=0x0000000F Parent=0x00000004 SizeRef=1074,431 Selected=0x5CDB7A4B
|
||||||
|
|
||||||
;;;<<<Layout_655921752_Default>>>;;;
|
;;;<<<Layout_655921752_Default>>>;;;
|
||||||
;;;<<<HelloImGui_Misc>>>;;;
|
;;;<<<HelloImGui_Misc>>>;;;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+88
-6
@@ -24,6 +24,7 @@ from src import session_logger
|
|||||||
from src import project_manager
|
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.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
|
||||||
@@ -297,6 +298,13 @@ class AppController:
|
|||||||
self._inject_mode: str = "skeleton"
|
self._inject_mode: str = "skeleton"
|
||||||
self._inject_preview: str = ""
|
self._inject_preview: str = ""
|
||||||
self._show_inject_modal: bool = False
|
self._show_inject_modal: bool = False
|
||||||
|
self.show_preset_manager_modal: bool = False
|
||||||
|
self._editing_preset_name: str = ""
|
||||||
|
self._editing_preset_content: str = ""
|
||||||
|
self._editing_preset_temperature: float = 0.0
|
||||||
|
self._editing_preset_top_p: float = 0.0
|
||||||
|
self._editing_preset_max_output_tokens: int = 4096
|
||||||
|
self._editing_preset_scope: str = "project"
|
||||||
self.diagnostic_log: List[Dict[str, Any]] = []
|
self.diagnostic_log: List[Dict[str, Any]] = []
|
||||||
self._settable_fields: Dict[str, str] = {
|
self._settable_fields: Dict[str, str] = {
|
||||||
'ai_input': 'ui_ai_input',
|
'ai_input': 'ui_ai_input',
|
||||||
@@ -322,10 +330,19 @@ class AppController:
|
|||||||
'ui_new_track_name': 'ui_new_track_name',
|
'ui_new_track_name': 'ui_new_track_name',
|
||||||
'ui_new_track_desc': 'ui_new_track_desc',
|
'ui_new_track_desc': 'ui_new_track_desc',
|
||||||
'manual_approve': 'ui_manual_approve',
|
'manual_approve': 'ui_manual_approve',
|
||||||
'inject_file_path': '_inject_file_path',
|
'global_system_prompt': 'ui_global_system_prompt',
|
||||||
'inject_mode': '_inject_mode',
|
'project_system_prompt': 'ui_project_system_prompt',
|
||||||
'show_inject_modal': '_show_inject_modal',
|
'global_preset_name': 'ui_global_preset_name',
|
||||||
'bg_shader_enabled': 'bg_shader_enabled'
|
'project_preset_name': 'ui_project_preset_name',
|
||||||
|
'temperature': 'temperature',
|
||||||
|
'max_tokens': 'max_tokens',
|
||||||
|
'show_preset_manager_modal': 'show_preset_manager_modal',
|
||||||
|
'_editing_preset_name': '_editing_preset_name',
|
||||||
|
'_editing_preset_content': '_editing_preset_content',
|
||||||
|
'_editing_preset_temperature': '_editing_preset_temperature',
|
||||||
|
'_editing_preset_top_p': '_editing_preset_top_p',
|
||||||
|
'_editing_preset_max_output_tokens': '_editing_preset_max_output_tokens',
|
||||||
|
'_editing_preset_scope': '_editing_preset_scope'
|
||||||
}
|
}
|
||||||
self._gettable_fields = dict(self._settable_fields)
|
self._gettable_fields = dict(self._settable_fields)
|
||||||
self._gettable_fields.update({
|
self._gettable_fields.update({
|
||||||
@@ -348,7 +365,20 @@ class AppController:
|
|||||||
'_inject_mode': '_inject_mode',
|
'_inject_mode': '_inject_mode',
|
||||||
'_inject_preview': '_inject_preview',
|
'_inject_preview': '_inject_preview',
|
||||||
'_show_inject_modal': '_show_inject_modal',
|
'_show_inject_modal': '_show_inject_modal',
|
||||||
'bg_shader_enabled': 'bg_shader_enabled'
|
'bg_shader_enabled': 'bg_shader_enabled',
|
||||||
|
'global_system_prompt': 'ui_global_system_prompt',
|
||||||
|
'project_system_prompt': 'ui_project_system_prompt',
|
||||||
|
'global_preset_name': 'ui_global_preset_name',
|
||||||
|
'project_preset_name': 'ui_project_preset_name',
|
||||||
|
'temperature': 'temperature',
|
||||||
|
'max_tokens': 'max_tokens',
|
||||||
|
'show_preset_manager_modal': 'show_preset_manager_modal',
|
||||||
|
'_editing_preset_name': '_editing_preset_name',
|
||||||
|
'_editing_preset_content': '_editing_preset_content',
|
||||||
|
'_editing_preset_temperature': '_editing_preset_temperature',
|
||||||
|
'_editing_preset_top_p': '_editing_preset_top_p',
|
||||||
|
'_editing_preset_max_output_tokens': '_editing_preset_max_output_tokens',
|
||||||
|
'_editing_preset_scope': '_editing_preset_scope'
|
||||||
})
|
})
|
||||||
self.perf_monitor = performance_monitor.get_monitor()
|
self.perf_monitor = performance_monitor.get_monitor()
|
||||||
self._perf_profiling_enabled = False
|
self._perf_profiling_enabled = False
|
||||||
@@ -423,7 +453,12 @@ class AppController:
|
|||||||
}
|
}
|
||||||
self._predefined_callbacks: dict[str, Callable[..., Any]] = {
|
self._predefined_callbacks: dict[str, Callable[..., Any]] = {
|
||||||
'_test_callback_func_write_to_file': self._test_callback_func_write_to_file,
|
'_test_callback_func_write_to_file': self._test_callback_func_write_to_file,
|
||||||
'_set_env_var': lambda k, v: os.environ.update({k: v})
|
'_set_env_var': lambda k, v: os.environ.update({k: v}),
|
||||||
|
'_apply_preset': self._apply_preset,
|
||||||
|
'_cb_save_preset': self._cb_save_preset,
|
||||||
|
'_cb_delete_preset': self._cb_delete_preset,
|
||||||
|
'_switch_project': self._switch_project,
|
||||||
|
'_refresh_from_project': self._refresh_from_project
|
||||||
}
|
}
|
||||||
|
|
||||||
def _update_gcli_adapter(self, path: str) -> None:
|
def _update_gcli_adapter(self, path: str) -> None:
|
||||||
@@ -787,6 +822,11 @@ class AppController:
|
|||||||
self.ui_auto_add_history = disc_sec.get("auto_add", False)
|
self.ui_auto_add_history = disc_sec.get("auto_add", False)
|
||||||
self.ui_global_system_prompt = self.config.get("ai", {}).get("system_prompt", "")
|
self.ui_global_system_prompt = self.config.get("ai", {}).get("system_prompt", "")
|
||||||
|
|
||||||
|
self.preset_manager = presets.PresetManager(Path(self.active_project_path) if self.active_project_path else None)
|
||||||
|
self.presets = self.preset_manager.load_all()
|
||||||
|
self.ui_global_preset_name = ai_cfg.get("active_preset")
|
||||||
|
self.ui_project_preset_name = proj_meta.get("active_preset")
|
||||||
|
|
||||||
gui_cfg = self.config.get("gui", {})
|
gui_cfg = self.config.get("gui", {})
|
||||||
from src import bg_shader
|
from src import bg_shader
|
||||||
bg_shader.get_bg().enabled = gui_cfg.get("bg_shader_enabled", False)
|
bg_shader.get_bg().enabled = gui_cfg.get("bg_shader_enabled", False)
|
||||||
@@ -1689,6 +1729,7 @@ class AppController:
|
|||||||
self.ui_project_git_dir = proj_meta.get("git_dir", "")
|
self.ui_project_git_dir = proj_meta.get("git_dir", "")
|
||||||
self.ui_project_system_prompt = proj_meta.get("system_prompt", "")
|
self.ui_project_system_prompt = proj_meta.get("system_prompt", "")
|
||||||
self.ui_project_main_context = proj_meta.get("main_context", "")
|
self.ui_project_main_context = proj_meta.get("main_context", "")
|
||||||
|
self.ui_project_preset_name = proj_meta.get("active_preset")
|
||||||
self.ui_gemini_cli_path = self.project.get("gemini_cli", {}).get("binary_path", "gemini")
|
self.ui_gemini_cli_path = self.project.get("gemini_cli", {}).get("binary_path", "gemini")
|
||||||
self.ui_auto_add_history = proj.get("discussion", {}).get("auto_add", False)
|
self.ui_auto_add_history = proj.get("discussion", {}).get("auto_add", False)
|
||||||
self.ui_auto_scroll_comms = proj.get("project", {}).get("auto_scroll_comms", True)
|
self.ui_auto_scroll_comms = proj.get("project", {}).get("auto_scroll_comms", True)
|
||||||
@@ -1727,6 +1768,45 @@ class AppController:
|
|||||||
with self._disc_entries_lock:
|
with self._disc_entries_lock:
|
||||||
self.disc_entries = models.parse_history_entries(track_history, self.disc_roles)
|
self.disc_entries = models.parse_history_entries(track_history, self.disc_roles)
|
||||||
|
|
||||||
|
self.preset_manager.project_root = Path(self.ui_files_base_dir)
|
||||||
|
self.presets = self.preset_manager.load_all()
|
||||||
|
|
||||||
|
def _apply_preset(self, name: str, scope: str) -> None:
|
||||||
|
if name == "None":
|
||||||
|
if scope == "global":
|
||||||
|
self.ui_global_preset_name = ""
|
||||||
|
else:
|
||||||
|
self.ui_project_preset_name = ""
|
||||||
|
return
|
||||||
|
preset = self.presets.get(name)
|
||||||
|
if not preset:
|
||||||
|
return
|
||||||
|
if scope == "global":
|
||||||
|
self.ui_global_system_prompt = preset.system_prompt
|
||||||
|
self.ui_global_preset_name = name
|
||||||
|
else:
|
||||||
|
self.ui_project_system_prompt = preset.system_prompt
|
||||||
|
self.ui_project_preset_name = name
|
||||||
|
if preset.temperature is not None:
|
||||||
|
self.temperature = preset.temperature
|
||||||
|
if preset.max_output_tokens is not None:
|
||||||
|
self.max_tokens = preset.max_output_tokens
|
||||||
|
|
||||||
|
def _cb_save_preset(self, name, content, temp, top_p, max_tok, scope):
|
||||||
|
preset = models.Preset(
|
||||||
|
name=name,
|
||||||
|
system_prompt=content,
|
||||||
|
temperature=temp,
|
||||||
|
top_p=top_p,
|
||||||
|
max_output_tokens=max_tok
|
||||||
|
)
|
||||||
|
self.preset_manager.save_preset(preset, scope)
|
||||||
|
self.presets = self.preset_manager.load_all()
|
||||||
|
|
||||||
|
def _cb_delete_preset(self, name, scope):
|
||||||
|
self.preset_manager.delete_preset(name, scope)
|
||||||
|
self.presets = self.preset_manager.load_all()
|
||||||
|
|
||||||
def _cb_load_track(self, track_id: str) -> None:
|
def _cb_load_track(self, track_id: str) -> None:
|
||||||
state = project_manager.load_track_state(track_id, self.ui_files_base_dir)
|
state = project_manager.load_track_state(track_id, self.ui_files_base_dir)
|
||||||
if state:
|
if state:
|
||||||
@@ -2053,6 +2133,7 @@ class AppController:
|
|||||||
proj["project"]["git_dir"] = self.ui_project_git_dir
|
proj["project"]["git_dir"] = self.ui_project_git_dir
|
||||||
proj["project"]["system_prompt"] = self.ui_project_system_prompt
|
proj["project"]["system_prompt"] = self.ui_project_system_prompt
|
||||||
proj["project"]["main_context"] = self.ui_project_main_context
|
proj["project"]["main_context"] = self.ui_project_main_context
|
||||||
|
proj["project"]["active_preset"] = self.ui_project_preset_name
|
||||||
proj["project"]["word_wrap"] = self.ui_word_wrap
|
proj["project"]["word_wrap"] = self.ui_word_wrap
|
||||||
proj["project"]["summary_only"] = self.ui_summary_only
|
proj["project"]["summary_only"] = self.ui_summary_only
|
||||||
proj["project"]["auto_scroll_comms"] = self.ui_auto_scroll_comms
|
proj["project"]["auto_scroll_comms"] = self.ui_auto_scroll_comms
|
||||||
@@ -2082,6 +2163,7 @@ class AppController:
|
|||||||
"temperature": self.temperature,
|
"temperature": self.temperature,
|
||||||
"max_tokens": self.max_tokens,
|
"max_tokens": self.max_tokens,
|
||||||
"history_trunc_limit": self.history_trunc_limit,
|
"history_trunc_limit": self.history_trunc_limit,
|
||||||
|
"active_preset": self.ui_global_preset_name,
|
||||||
}
|
}
|
||||||
self.config["ai"]["system_prompt"] = self.ui_global_system_prompt
|
self.config["ai"]["system_prompt"] = self.ui_global_system_prompt
|
||||||
self.config["projects"] = {"paths": self.project_paths, "active": self.active_project_path}
|
self.config["projects"] = {"paths": self.project_paths, "active": self.active_project_path}
|
||||||
|
|||||||
+120
-2
@@ -14,6 +14,7 @@ from src import cost_tracker
|
|||||||
from src import session_logger
|
from src import session_logger
|
||||||
from src import project_manager
|
from src import project_manager
|
||||||
from src import paths
|
from src import paths
|
||||||
|
from src import presets
|
||||||
from src import theme_2 as theme
|
from src import theme_2 as theme
|
||||||
from src import theme_nerv_fx as theme_fx
|
from src import theme_nerv_fx as theme_fx
|
||||||
from src import api_hooks
|
from src import api_hooks
|
||||||
@@ -94,6 +95,15 @@ class App:
|
|||||||
self.controller.init_state()
|
self.controller.init_state()
|
||||||
self.show_windows.setdefault("Diagnostics", False)
|
self.show_windows.setdefault("Diagnostics", False)
|
||||||
self.controller.start_services(self)
|
self.controller.start_services(self)
|
||||||
|
self.show_preset_manager_modal = False
|
||||||
|
self._editing_preset_name = ""
|
||||||
|
self._editing_preset_content = ""
|
||||||
|
self._editing_preset_temperature = 0.0
|
||||||
|
self._editing_preset_top_p = 1.0
|
||||||
|
self._editing_preset_max_output_tokens = 4096
|
||||||
|
self._editing_preset_scope = "project"
|
||||||
|
self._editing_preset_is_new = False
|
||||||
|
self._presets_list: dict[str, dict] = {}
|
||||||
# Aliases for controller-owned locks
|
# Aliases for controller-owned locks
|
||||||
self._send_thread_lock = self.controller._send_thread_lock
|
self._send_thread_lock = self.controller._send_thread_lock
|
||||||
self._disc_entries_lock = self.controller._disc_entries_lock
|
self._disc_entries_lock = self.controller._disc_entries_lock
|
||||||
@@ -110,7 +120,7 @@ class App:
|
|||||||
self.node_editor_ctx = ed.create_editor(self.node_editor_config)
|
self.node_editor_ctx = ed.create_editor(self.node_editor_config)
|
||||||
self.ui_selected_ticket_id: Optional[str] = None
|
self.ui_selected_ticket_id: Optional[str] = None
|
||||||
self.ui_selected_tickets: set[str] = set()
|
self.ui_selected_tickets: set[str] = set()
|
||||||
self.ui_new_ticket_priority: str = "medium"
|
self.ui_new_ticket_priority: str = 'medium'
|
||||||
self._autofocus_response_tab = False
|
self._autofocus_response_tab = False
|
||||||
gui_cfg = self.config.get("gui", {})
|
gui_cfg = self.config.get("gui", {})
|
||||||
self.ui_separate_message_panel = gui_cfg.get("separate_message_panel", False)
|
self.ui_separate_message_panel = gui_cfg.get("separate_message_panel", False)
|
||||||
@@ -332,6 +342,7 @@ class App:
|
|||||||
self._render_track_proposal_modal()
|
self._render_track_proposal_modal()
|
||||||
self._render_patch_modal()
|
self._render_patch_modal()
|
||||||
self._render_save_preset_modal()
|
self._render_save_preset_modal()
|
||||||
|
self._render_preset_manager_modal()
|
||||||
# Auto-save (every 60s)
|
# Auto-save (every 60s)
|
||||||
now = time.time()
|
now = time.time()
|
||||||
if now - self._last_autosave >= self._autosave_interval:
|
if now - self._last_autosave >= self._autosave_interval:
|
||||||
@@ -850,6 +861,85 @@ class App:
|
|||||||
imgui.close_current_popup()
|
imgui.close_current_popup()
|
||||||
imgui.end_popup()
|
imgui.end_popup()
|
||||||
|
|
||||||
|
def _render_preset_manager_modal(self) -> None:
|
||||||
|
if not self.show_preset_manager_modal: return
|
||||||
|
imgui.open_popup("Preset Manager")
|
||||||
|
if imgui.begin_popup_modal("Preset Manager", True, imgui.WindowFlags_.always_auto_resize)[0]:
|
||||||
|
imgui.begin_child("preset_list_area", imgui.ImVec2(250, 600), True)
|
||||||
|
preset_names = sorted(self.controller.presets.keys())
|
||||||
|
if imgui.button("New Preset", imgui.ImVec2(-1, 0)):
|
||||||
|
self._editing_preset_name = ""
|
||||||
|
self._editing_preset_content = ""
|
||||||
|
self._editing_preset_temperature = 0.0
|
||||||
|
self._editing_preset_top_p = 1.0
|
||||||
|
self._editing_preset_max_output_tokens = 4096
|
||||||
|
self._editing_preset_scope = "project"
|
||||||
|
self._editing_preset_is_new = True
|
||||||
|
imgui.separator()
|
||||||
|
for name in preset_names:
|
||||||
|
p = self.controller.presets[name]
|
||||||
|
is_sel = (name == self._editing_preset_name)
|
||||||
|
if imgui.selectable(name, is_sel)[0]:
|
||||||
|
self._editing_preset_name = name
|
||||||
|
self._editing_preset_content = p.system_prompt
|
||||||
|
self._editing_preset_temperature = p.temperature if p.temperature is not None else 0.0
|
||||||
|
self._editing_preset_top_p = p.top_p if p.top_p is not None else 1.0
|
||||||
|
self._editing_preset_max_output_tokens = p.max_output_tokens if p.max_output_tokens is not None else 4096
|
||||||
|
self._editing_preset_is_new = False
|
||||||
|
imgui.end_child()
|
||||||
|
imgui.same_line()
|
||||||
|
imgui.begin_child("preset_edit_area", imgui.ImVec2(500, 600), False)
|
||||||
|
p_name = self._editing_preset_name or "(New Preset)"
|
||||||
|
imgui.text_colored(C_IN, f"Editing Preset: {p_name}")
|
||||||
|
imgui.separator()
|
||||||
|
imgui.text("Name:")
|
||||||
|
_, self._editing_preset_name = imgui.input_text("##edit_name", self._editing_preset_name)
|
||||||
|
imgui.text("Scope:")
|
||||||
|
if imgui.radio_button("Global", self._editing_preset_scope == "global"):
|
||||||
|
self._editing_preset_scope = "global"
|
||||||
|
imgui.same_line()
|
||||||
|
if imgui.radio_button("Project", self._editing_preset_scope == "project"):
|
||||||
|
self._editing_preset_scope = "project"
|
||||||
|
imgui.text("Content:")
|
||||||
|
_, self._editing_preset_content = imgui.input_text_multiline("##edit_content", self._editing_preset_content, imgui.ImVec2(-1, 280))
|
||||||
|
|
||||||
|
imgui.text("Temperature:")
|
||||||
|
_, self._editing_preset_temperature = imgui.input_float("##edit_temp", self._editing_preset_temperature, 0.1, 1.0, "%.2f")
|
||||||
|
imgui.text("Top P:")
|
||||||
|
_, self._editing_preset_top_p = imgui.input_float("##edit_top_p", self._editing_preset_top_p, 0.1, 1.0, "%.2f")
|
||||||
|
imgui.text("Max Output Tokens:")
|
||||||
|
_, self._editing_preset_max_output_tokens = imgui.input_int("##edit_max_tokens", self._editing_preset_max_output_tokens)
|
||||||
|
|
||||||
|
if imgui.button("Save", imgui.ImVec2(120, 0)):
|
||||||
|
if self._editing_preset_name.strip():
|
||||||
|
self.controller._cb_save_preset(
|
||||||
|
self._editing_preset_name.strip(),
|
||||||
|
self._editing_preset_content,
|
||||||
|
self._editing_preset_temperature,
|
||||||
|
self._editing_preset_top_p,
|
||||||
|
self._editing_preset_max_output_tokens,
|
||||||
|
self._editing_preset_scope
|
||||||
|
)
|
||||||
|
self.ai_status = f"Preset '{self._editing_preset_name.strip()}' saved to {self._editing_preset_scope}"
|
||||||
|
imgui.set_item_tooltip("Save the current preset settings")
|
||||||
|
imgui.same_line()
|
||||||
|
if imgui.button("Delete", imgui.ImVec2(120, 0)):
|
||||||
|
if self._editing_preset_name.strip():
|
||||||
|
try:
|
||||||
|
self.controller._cb_delete_preset(self._editing_preset_name.strip(), self._editing_preset_scope)
|
||||||
|
self.ai_status = f"Preset '{self._editing_preset_name}' deleted from {self._editing_preset_scope}"
|
||||||
|
self._editing_preset_name = ""
|
||||||
|
self._editing_preset_content = ""
|
||||||
|
except Exception as e:
|
||||||
|
self.ai_status = f"Error deleting: {e}"
|
||||||
|
imgui.set_item_tooltip("Delete the selected preset")
|
||||||
|
imgui.same_line()
|
||||||
|
if imgui.button("Close", imgui.ImVec2(120, 0)):
|
||||||
|
self.show_preset_manager_modal = False
|
||||||
|
imgui.close_current_popup()
|
||||||
|
imgui.end_child()
|
||||||
|
imgui.end_popup()
|
||||||
|
|
||||||
def _render_projects_panel(self) -> None:
|
def _render_projects_panel(self) -> None:
|
||||||
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_projects_panel")
|
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_projects_panel")
|
||||||
proj_name = self.project.get("project", {}).get("name", Path(self.active_project_path).stem)
|
proj_name = self.project.get("project", {}).get("name", Path(self.active_project_path).stem)
|
||||||
@@ -2827,11 +2917,39 @@ def hello():
|
|||||||
|
|
||||||
def _render_system_prompts_panel(self) -> None:
|
def _render_system_prompts_panel(self) -> None:
|
||||||
imgui.text("Global System Prompt (all projects)")
|
imgui.text("Global System Prompt (all projects)")
|
||||||
|
preset_names = sorted(self.controller.presets.keys())
|
||||||
|
current_global = self.controller.ui_global_preset_name or "Select Preset..."
|
||||||
|
imgui.set_next_item_width(200)
|
||||||
|
if imgui.begin_combo("##global_preset", current_global):
|
||||||
|
for name in preset_names:
|
||||||
|
is_sel = (name == current_global)
|
||||||
|
if imgui.selectable(name, is_sel)[0]:
|
||||||
|
self.controller._apply_preset(name, "global")
|
||||||
|
if is_sel:
|
||||||
|
imgui.set_item_default_focus()
|
||||||
|
imgui.end_combo()
|
||||||
|
imgui.same_line(0, 8)
|
||||||
|
if imgui.button("Manage Presets##global"):
|
||||||
|
self.show_preset_manager_modal = True
|
||||||
|
imgui.set_item_tooltip("Open preset management modal")
|
||||||
ch, self.ui_global_system_prompt = imgui.input_text_multiline("##gsp", self.ui_global_system_prompt, imgui.ImVec2(-1, 100))
|
ch, self.ui_global_system_prompt = imgui.input_text_multiline("##gsp", self.ui_global_system_prompt, imgui.ImVec2(-1, 100))
|
||||||
imgui.separator()
|
imgui.separator()
|
||||||
imgui.text("Project System Prompt")
|
imgui.text("Project System Prompt")
|
||||||
|
current_project = self.controller.ui_project_preset_name or "Select Preset..."
|
||||||
|
imgui.set_next_item_width(200)
|
||||||
|
if imgui.begin_combo("##project_preset", current_project):
|
||||||
|
for name in preset_names:
|
||||||
|
is_sel = (name == current_project)
|
||||||
|
if imgui.selectable(name, is_sel)[0]:
|
||||||
|
self.controller._apply_preset(name, "project")
|
||||||
|
if is_sel:
|
||||||
|
imgui.set_item_default_focus()
|
||||||
|
imgui.end_combo()
|
||||||
|
imgui.same_line(0, 8)
|
||||||
|
if imgui.button("Manage Presets##project"):
|
||||||
|
self.show_preset_manager_modal = True
|
||||||
|
imgui.set_item_tooltip("Open preset management modal")
|
||||||
ch, self.ui_project_system_prompt = imgui.input_text_multiline("##psp", self.ui_project_system_prompt, imgui.ImVec2(-1, 100))
|
ch, self.ui_project_system_prompt = imgui.input_text_multiline("##psp", self.ui_project_system_prompt, imgui.ImVec2(-1, 100))
|
||||||
|
|
||||||
def _render_theme_panel(self) -> None:
|
def _render_theme_panel(self) -> None:
|
||||||
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_theme_panel")
|
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_theme_panel")
|
||||||
exp, opened = imgui.begin("Theme", self.show_windows["Theme"])
|
exp, opened = imgui.begin("Theme", self.show_windows["Theme"])
|
||||||
|
|||||||
@@ -315,3 +315,33 @@ class FileItem:
|
|||||||
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),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Preset:
|
||||||
|
name: str
|
||||||
|
system_prompt: str
|
||||||
|
temperature: Optional[float] = None
|
||||||
|
top_p: Optional[float] = None
|
||||||
|
max_output_tokens: Optional[int] = None
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
res = {
|
||||||
|
"system_prompt": self.system_prompt,
|
||||||
|
}
|
||||||
|
if self.temperature is not None:
|
||||||
|
res["temperature"] = self.temperature
|
||||||
|
if self.top_p is not None:
|
||||||
|
res["top_p"] = self.top_p
|
||||||
|
if self.max_output_tokens is not None:
|
||||||
|
res["max_output_tokens"] = self.max_output_tokens
|
||||||
|
return res
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, name: str, data: Dict[str, Any]) -> "Preset":
|
||||||
|
return cls(
|
||||||
|
name=name,
|
||||||
|
system_prompt=data.get("system_prompt", ""),
|
||||||
|
temperature=data.get("temperature"),
|
||||||
|
top_p=data.get("top_p"),
|
||||||
|
max_output_tokens=data.get("max_output_tokens"),
|
||||||
|
)
|
||||||
|
|||||||
@@ -51,6 +51,11 @@ _RESOLVED: dict[str, Path] = {}
|
|||||||
def get_config_path() -> Path:
|
def get_config_path() -> Path:
|
||||||
root_dir = Path(__file__).resolve().parent.parent
|
root_dir = Path(__file__).resolve().parent.parent
|
||||||
return Path(os.environ.get("SLOP_CONFIG", root_dir / "config.toml"))
|
return Path(os.environ.get("SLOP_CONFIG", root_dir / "config.toml"))
|
||||||
|
def get_global_presets_path() -> Path:
|
||||||
|
root_dir = Path(__file__).resolve().parent.parent
|
||||||
|
return Path(os.environ.get("SLOP_GLOBAL_PRESETS", root_dir / "presets.toml"))
|
||||||
|
def get_project_presets_path(project_root: Path) -> Path:
|
||||||
|
return project_root / "project_presets.toml"
|
||||||
|
|
||||||
def _resolve_path(env_var: str, config_key: str, default: str) -> Path:
|
def _resolve_path(env_var: str, config_key: str, default: str) -> Path:
|
||||||
if env_var in os.environ:
|
if env_var in os.environ:
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import sys
|
||||||
|
import tomllib
|
||||||
|
import tomli_w
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, Any, Optional
|
||||||
|
from src.models import Preset
|
||||||
|
from src.paths import get_global_presets_path, get_project_presets_path
|
||||||
|
|
||||||
|
class PresetManager:
|
||||||
|
"""Manages system prompt presets across global and project-specific files."""
|
||||||
|
|
||||||
|
def __init__(self, project_root: Optional[Path] = None):
|
||||||
|
self.project_root = project_root
|
||||||
|
self.global_path = get_global_presets_path()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def project_path(self) -> Optional[Path]:
|
||||||
|
return get_project_presets_path(self.project_root) if self.project_root else None
|
||||||
|
|
||||||
|
def load_all(self) -> Dict[str, Preset]:
|
||||||
|
"""Merges global and project presets into a single dictionary."""
|
||||||
|
presets: Dict[str, Preset] = {}
|
||||||
|
|
||||||
|
# Load global presets
|
||||||
|
data_global = self._load_file(self.global_path)
|
||||||
|
for name, p_data in data_global.get("presets", {}).items():
|
||||||
|
try:
|
||||||
|
presets[name] = Preset.from_dict(name, p_data)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error parsing global preset '{name}': {e}", file=sys.stderr)
|
||||||
|
|
||||||
|
# Load project presets (overwriting global ones if names conflict)
|
||||||
|
if self.project_path:
|
||||||
|
data_project = self._load_file(self.project_path)
|
||||||
|
for name, p_data in data_project.get("presets", {}).items():
|
||||||
|
try:
|
||||||
|
presets[name] = Preset.from_dict(name, p_data)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error parsing project preset '{name}': {e}", file=sys.stderr)
|
||||||
|
|
||||||
|
return presets
|
||||||
|
|
||||||
|
def save_preset(self, preset: Preset, scope: str = "project") -> None:
|
||||||
|
"""Saves a preset to either the global or project-specific TOML file."""
|
||||||
|
path = self.global_path if scope == "global" else self.project_path
|
||||||
|
if not path:
|
||||||
|
if scope == "project":
|
||||||
|
raise ValueError("Project scope requested but no project_root provided.")
|
||||||
|
path = self.global_path
|
||||||
|
|
||||||
|
data = self._load_file(path)
|
||||||
|
if "presets" not in data:
|
||||||
|
data["presets"] = {}
|
||||||
|
|
||||||
|
data["presets"][preset.name] = preset.to_dict()
|
||||||
|
self._save_file(path, data)
|
||||||
|
|
||||||
|
def delete_preset(self, name: str, scope: str = "project") -> None:
|
||||||
|
"""Deletes a preset by name from the specified scope."""
|
||||||
|
path = self.global_path if scope == "global" else self.project_path
|
||||||
|
if not path:
|
||||||
|
if scope == "project":
|
||||||
|
raise ValueError("Project scope requested but no project_root provided.")
|
||||||
|
path = self.global_path
|
||||||
|
|
||||||
|
data = self._load_file(path)
|
||||||
|
if "presets" in data and name in data["presets"]:
|
||||||
|
del data["presets"][name]
|
||||||
|
self._save_file(path, data)
|
||||||
|
|
||||||
|
def _load_file(self, path: Path) -> Dict[str, Any]:
|
||||||
|
if not path.exists():
|
||||||
|
return {"presets": {}}
|
||||||
|
try:
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
data = tomllib.load(f)
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
return {"presets": {}}
|
||||||
|
if "presets" not in data:
|
||||||
|
data["presets"] = {}
|
||||||
|
return data
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading presets from {path}: {e}", file=sys.stderr)
|
||||||
|
return {"presets": {}}
|
||||||
|
|
||||||
|
def _save_file(self, path: Path, data: Dict[str, Any]) -> None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(path, "wb") as f:
|
||||||
|
f.write(tomli_w.dumps(data).encode("utf-8"))
|
||||||
+3
-2
@@ -200,13 +200,13 @@ def live_gui() -> Generator[tuple[subprocess.Popen, str], None, None]:
|
|||||||
temp_workspace.mkdir(parents=True, exist_ok=True)
|
temp_workspace.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# Create minimal project files to avoid cluttering root
|
# Create minimal project files to avoid cluttering root
|
||||||
# NOTE: Do NOT create config.toml here - we use SLOP_CONFIG env var
|
|
||||||
# to point to the actual project root config.toml
|
|
||||||
(temp_workspace / "manual_slop.toml").write_text("[project]\nname = 'TestProject'\n", encoding="utf-8")
|
(temp_workspace / "manual_slop.toml").write_text("[project]\nname = 'TestProject'\n", encoding="utf-8")
|
||||||
(temp_workspace / "conductor" / "tracks").mkdir(parents=True, exist_ok=True)
|
(temp_workspace / "conductor" / "tracks").mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# Resolve absolute paths for shared resources
|
# Resolve absolute paths for shared resources
|
||||||
project_root = Path(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
project_root = Path(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||||
|
config_file = temp_workspace / "config.toml"
|
||||||
|
if not config_file.exists():
|
||||||
config_file = project_root / "config.toml"
|
config_file = project_root / "config.toml"
|
||||||
cred_file = project_root / "credentials.toml"
|
cred_file = project_root / "credentials.toml"
|
||||||
mcp_file = project_root / "mcp_env.toml"
|
mcp_file = project_root / "mcp_env.toml"
|
||||||
@@ -249,6 +249,7 @@ def live_gui() -> Generator[tuple[subprocess.Popen, str], None, None]:
|
|||||||
env["SLOP_CREDENTIALS"] = str(cred_file.absolute())
|
env["SLOP_CREDENTIALS"] = str(cred_file.absolute())
|
||||||
if mcp_file.exists():
|
if mcp_file.exists():
|
||||||
env["SLOP_MCP_ENV"] = str(mcp_file.absolute())
|
env["SLOP_MCP_ENV"] = str(mcp_file.absolute())
|
||||||
|
env["SLOP_GLOBAL_PRESETS"] = str((temp_workspace / "presets.toml").absolute())
|
||||||
|
|
||||||
process = subprocess.Popen(
|
process = subprocess.Popen(
|
||||||
["uv", "run", "python", "-u", gui_script, "--enable-test-hooks"],
|
["uv", "run", "python", "-u", gui_script, "--enable-test-hooks"],
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import pytest
|
||||||
|
from pathlib import Path
|
||||||
|
from src.presets import PresetManager
|
||||||
|
from src.models import Preset
|
||||||
|
|
||||||
|
def test_load_all_merged(tmp_path, monkeypatch):
|
||||||
|
"""Tests that load_all correctly merges global and project presets."""
|
||||||
|
global_file = tmp_path / "global_presets.toml"
|
||||||
|
project_root = tmp_path / "project"
|
||||||
|
project_root.mkdir()
|
||||||
|
project_file = project_root / "project_presets.toml"
|
||||||
|
|
||||||
|
# Setup global presets
|
||||||
|
global_file.write_text("""
|
||||||
|
[presets.global_only]
|
||||||
|
system_prompt = "global prompt"
|
||||||
|
temperature = 0.5
|
||||||
|
|
||||||
|
[presets.override_me]
|
||||||
|
system_prompt = "original prompt"
|
||||||
|
""", encoding="utf-8")
|
||||||
|
|
||||||
|
# Setup project presets
|
||||||
|
project_file.write_text("""
|
||||||
|
[presets.project_only]
|
||||||
|
system_prompt = "project prompt"
|
||||||
|
max_output_tokens = 100
|
||||||
|
|
||||||
|
[presets.override_me]
|
||||||
|
system_prompt = "overridden prompt"
|
||||||
|
""", encoding="utf-8")
|
||||||
|
|
||||||
|
monkeypatch.setattr("src.presets.get_global_presets_path", lambda: global_file)
|
||||||
|
monkeypatch.setattr("src.presets.get_project_presets_path", lambda p: project_file)
|
||||||
|
|
||||||
|
pm = PresetManager(project_root=project_root)
|
||||||
|
presets = pm.load_all()
|
||||||
|
|
||||||
|
assert len(presets) == 3
|
||||||
|
assert presets["global_only"].system_prompt == "global prompt"
|
||||||
|
assert presets["global_only"].temperature == 0.5
|
||||||
|
assert presets["project_only"].system_prompt == "project prompt"
|
||||||
|
assert presets["project_only"].max_output_tokens == 100
|
||||||
|
assert presets["override_me"].system_prompt == "overridden prompt"
|
||||||
|
|
||||||
|
def test_save_preset_global(tmp_path, monkeypatch):
|
||||||
|
"""Tests saving a preset to the global scope."""
|
||||||
|
global_file = tmp_path / "global_presets.toml"
|
||||||
|
monkeypatch.setattr("src.presets.get_global_presets_path", lambda: global_file)
|
||||||
|
|
||||||
|
pm = PresetManager()
|
||||||
|
preset = Preset(name="new_global", system_prompt="new global prompt", temperature=0.7)
|
||||||
|
pm.save_preset(preset, scope="global")
|
||||||
|
|
||||||
|
assert global_file.exists()
|
||||||
|
loaded_presets = pm.load_all()
|
||||||
|
assert "new_global" in loaded_presets
|
||||||
|
assert loaded_presets["new_global"].system_prompt == "new global prompt"
|
||||||
|
assert loaded_presets["new_global"].temperature == 0.7
|
||||||
|
|
||||||
|
def test_save_preset_project(tmp_path, monkeypatch):
|
||||||
|
"""Tests saving a preset to the project scope."""
|
||||||
|
project_root = tmp_path / "project"
|
||||||
|
project_root.mkdir()
|
||||||
|
project_file = project_root / "project_presets.toml"
|
||||||
|
global_file = tmp_path / "global_presets.toml"
|
||||||
|
|
||||||
|
monkeypatch.setattr("src.presets.get_global_presets_path", lambda: global_file)
|
||||||
|
monkeypatch.setattr("src.presets.get_project_presets_path", lambda p: project_file)
|
||||||
|
|
||||||
|
pm = PresetManager(project_root=project_root)
|
||||||
|
preset = Preset(name="new_project", system_prompt="new project prompt", max_output_tokens=500)
|
||||||
|
pm.save_preset(preset, scope="project")
|
||||||
|
|
||||||
|
assert project_file.exists()
|
||||||
|
# Global file should NOT have been created/modified
|
||||||
|
assert not global_file.exists()
|
||||||
|
|
||||||
|
loaded_presets = pm.load_all()
|
||||||
|
assert "new_project" in loaded_presets
|
||||||
|
assert loaded_presets["new_project"].system_prompt == "new project prompt"
|
||||||
|
assert loaded_presets["new_project"].max_output_tokens == 500
|
||||||
|
|
||||||
|
def test_save_preset_project_no_root():
|
||||||
|
"""Tests that saving to project scope fails if no project root is provided."""
|
||||||
|
pm = PresetManager(project_root=None)
|
||||||
|
preset = Preset(name="fail", system_prompt="fail")
|
||||||
|
with pytest.raises(ValueError, match="Project scope requested but no project_root provided"):
|
||||||
|
pm.save_preset(preset, scope="project")
|
||||||
|
|
||||||
|
def test_delete_preset(tmp_path, monkeypatch):
|
||||||
|
"""Tests deleting a preset from both scopes."""
|
||||||
|
global_file = tmp_path / "global_presets.toml"
|
||||||
|
project_root = tmp_path / "project"
|
||||||
|
project_root.mkdir()
|
||||||
|
project_file = project_root / "project_presets.toml"
|
||||||
|
|
||||||
|
global_file.write_text("""
|
||||||
|
[presets.global1]
|
||||||
|
system_prompt = "g1"
|
||||||
|
[presets.both]
|
||||||
|
system_prompt = "both_g"
|
||||||
|
""", encoding="utf-8")
|
||||||
|
|
||||||
|
project_file.write_text("""
|
||||||
|
[presets.project1]
|
||||||
|
system_prompt = "p1"
|
||||||
|
[presets.both]
|
||||||
|
system_prompt = "both_p"
|
||||||
|
""", encoding="utf-8")
|
||||||
|
|
||||||
|
monkeypatch.setattr("src.presets.get_global_presets_path", lambda: global_file)
|
||||||
|
monkeypatch.setattr("src.presets.get_project_presets_path", lambda p: project_file)
|
||||||
|
|
||||||
|
pm = PresetManager(project_root=project_root)
|
||||||
|
|
||||||
|
# Delete from project
|
||||||
|
pm.delete_preset("both", scope="project")
|
||||||
|
presets = pm.load_all()
|
||||||
|
assert "project1" in presets
|
||||||
|
# "both" should now show the global version because project override is gone
|
||||||
|
assert presets["both"].system_prompt == "both_g"
|
||||||
|
|
||||||
|
# Delete from global
|
||||||
|
pm.delete_preset("global1", scope="global")
|
||||||
|
presets = pm.load_all()
|
||||||
|
assert "global1" not in presets
|
||||||
|
assert "both" in presets
|
||||||
|
|
||||||
|
# Delete last project preset
|
||||||
|
pm.delete_preset("project1", scope="project")
|
||||||
|
presets = pm.load_all()
|
||||||
|
assert "project1" not in presets
|
||||||
|
assert "both" in presets # still in global
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
from src.presets import PresetManager
|
||||||
|
from src.models import Preset
|
||||||
|
|
||||||
|
class TestPresetManager(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.test_dir = Path(tempfile.mkdtemp())
|
||||||
|
self.project_root = self.test_dir / "project"
|
||||||
|
self.project_root.mkdir()
|
||||||
|
|
||||||
|
# Mocking global path is harder since it's hardcoded in paths.py
|
||||||
|
# But we can at least test project-specific ones and the manager's logic.
|
||||||
|
# For the sake of this test, we will only test what we can without
|
||||||
|
# affecting the real global_presets.toml if possible.
|
||||||
|
|
||||||
|
self.manager = PresetManager(project_root=self.project_root)
|
||||||
|
# Override paths for testing to avoid touching real files
|
||||||
|
self.manager.global_path = self.test_dir / "global_presets.toml"
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
shutil.rmtree(self.test_dir)
|
||||||
|
|
||||||
|
def test_save_and_load_global(self):
|
||||||
|
preset = Preset(name="test_global", system_prompt="You are a global assistant")
|
||||||
|
self.manager.save_preset(preset, scope="global")
|
||||||
|
|
||||||
|
presets = self.manager.load_all()
|
||||||
|
self.assertIn("test_global", presets)
|
||||||
|
self.assertEqual(presets["test_global"].system_prompt, "You are a global assistant")
|
||||||
|
|
||||||
|
def test_save_and_load_project(self):
|
||||||
|
preset = Preset(name="test_project", system_prompt="You are a project assistant")
|
||||||
|
self.manager.save_preset(preset, scope="project")
|
||||||
|
|
||||||
|
presets = self.manager.load_all()
|
||||||
|
self.assertIn("test_project", presets)
|
||||||
|
self.assertEqual(presets["test_project"].system_prompt, "You are a project assistant")
|
||||||
|
|
||||||
|
def test_project_overwrites_global(self):
|
||||||
|
g_preset = Preset(name="shared", system_prompt="Global version")
|
||||||
|
p_preset = Preset(name="shared", system_prompt="Project version")
|
||||||
|
|
||||||
|
self.manager.save_preset(g_preset, scope="global")
|
||||||
|
self.manager.save_preset(p_preset, scope="project")
|
||||||
|
|
||||||
|
presets = self.manager.load_all()
|
||||||
|
self.assertEqual(presets["shared"].system_prompt, "Project version")
|
||||||
|
|
||||||
|
def test_delete_preset(self):
|
||||||
|
preset = Preset(name="to_delete", system_prompt="Delete me")
|
||||||
|
self.manager.save_preset(preset, scope="project")
|
||||||
|
|
||||||
|
presets = self.manager.load_all()
|
||||||
|
self.assertIn("to_delete", presets)
|
||||||
|
|
||||||
|
self.manager.delete_preset("to_delete", scope="project")
|
||||||
|
presets = self.manager.load_all()
|
||||||
|
self.assertNotIn("to_delete", presets)
|
||||||
|
|
||||||
|
def test_dynamic_project_path(self):
|
||||||
|
"""Verifies that project_path updates when project_root changes."""
|
||||||
|
initial_root = self.test_dir / "project1"
|
||||||
|
initial_root.mkdir()
|
||||||
|
manager = PresetManager(project_root=initial_root)
|
||||||
|
self.assertEqual(manager.project_path, initial_root / "project_presets.toml")
|
||||||
|
|
||||||
|
new_root = self.test_dir / "project2"
|
||||||
|
new_root.mkdir()
|
||||||
|
manager.project_root = new_root
|
||||||
|
self.assertEqual(manager.project_path, new_root / "project_presets.toml")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
import pytest
|
||||||
|
import time
|
||||||
|
import tomli_w
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
from src.api_hook_client import ApiHookClient
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session", autouse=True)
|
||||||
|
def test_env_setup():
|
||||||
|
temp_workspace = Path("tests/artifacts/live_gui_workspace")
|
||||||
|
if temp_workspace.exists():
|
||||||
|
try: shutil.rmtree(temp_workspace)
|
||||||
|
except: pass
|
||||||
|
temp_workspace.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
config_path = temp_workspace / "config.toml"
|
||||||
|
manual_slop_path = temp_workspace / "manual_slop.toml"
|
||||||
|
|
||||||
|
# Create minimal project file
|
||||||
|
manual_slop_path.write_text("[project]\nname = 'TestProject'\n", encoding="utf-8")
|
||||||
|
|
||||||
|
# Create local config.toml
|
||||||
|
config_path.write_text(tomli_w.dumps({
|
||||||
|
"projects": {
|
||||||
|
"paths": [str(manual_slop_path.absolute())],
|
||||||
|
"active": str(manual_slop_path.absolute())
|
||||||
|
},
|
||||||
|
"ai": {
|
||||||
|
"provider": "gemini",
|
||||||
|
"model": "gemini-2.5-flash-lite"
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
yield
|
||||||
|
# Cleanup handled by live_gui fixture usually, but we can be explicit
|
||||||
|
if config_path.exists(): config_path.unlink()
|
||||||
|
|
||||||
|
def test_preset_switching(live_gui):
|
||||||
|
client = ApiHookClient()
|
||||||
|
|
||||||
|
# Paths for presets
|
||||||
|
temp_workspace = Path("tests/artifacts/live_gui_workspace")
|
||||||
|
global_presets_path = temp_workspace / "presets.toml"
|
||||||
|
project_presets_path = temp_workspace / "project_presets.toml"
|
||||||
|
manual_slop_path = temp_workspace / "manual_slop.toml"
|
||||||
|
|
||||||
|
# Cleanup before test
|
||||||
|
if global_presets_path.exists(): global_presets_path.unlink()
|
||||||
|
if project_presets_path.exists(): project_presets_path.unlink()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create a global preset
|
||||||
|
global_presets_path.write_text(tomli_w.dumps({
|
||||||
|
"presets": {
|
||||||
|
"TestGlobal": {
|
||||||
|
"system_prompt": "Global Prompt",
|
||||||
|
"temperature": 0.7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
# Create a project preset
|
||||||
|
project_presets_path.write_text(tomli_w.dumps({
|
||||||
|
"presets": {
|
||||||
|
"TestProject": {
|
||||||
|
"system_prompt": "Project Prompt",
|
||||||
|
"temperature": 0.3
|
||||||
|
},
|
||||||
|
"TestGlobal": { # Override
|
||||||
|
"system_prompt": "Overridden Prompt",
|
||||||
|
"temperature": 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
# Switch to the local project to ensure context is correct
|
||||||
|
client.push_event("custom_callback", {
|
||||||
|
"callback": "_switch_project",
|
||||||
|
"args": [str(manual_slop_path.absolute())]
|
||||||
|
})
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
# Trigger reload of presets (just in case)
|
||||||
|
client.push_event("custom_callback", {
|
||||||
|
"callback": "_refresh_from_project",
|
||||||
|
"args": []
|
||||||
|
})
|
||||||
|
time.sleep(2) # Wait for processing
|
||||||
|
|
||||||
|
# Apply Global Preset (should use override from project if available in merged list)
|
||||||
|
client.push_event("custom_callback", {
|
||||||
|
"callback": "_apply_preset",
|
||||||
|
"args": ["TestGlobal", "global"]
|
||||||
|
})
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# Verify state
|
||||||
|
state = client.get_gui_state()
|
||||||
|
assert state["global_preset_name"] == "TestGlobal"
|
||||||
|
assert state["global_system_prompt"] == "Overridden Prompt"
|
||||||
|
assert state["temperature"] == 0.5
|
||||||
|
|
||||||
|
# Apply Project Preset
|
||||||
|
client.push_event("custom_callback", {
|
||||||
|
"callback": "_apply_preset",
|
||||||
|
"args": ["TestProject", "project"]
|
||||||
|
})
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
state = client.get_gui_state()
|
||||||
|
assert state["project_preset_name"] == "TestProject"
|
||||||
|
assert state["project_system_prompt"] == "Project Prompt"
|
||||||
|
assert state["temperature"] == 0.3
|
||||||
|
|
||||||
|
# Select "None"
|
||||||
|
client.push_event("custom_callback", {
|
||||||
|
"callback": "_apply_preset",
|
||||||
|
"args": ["None", "global"]
|
||||||
|
})
|
||||||
|
time.sleep(1)
|
||||||
|
state = client.get_gui_state()
|
||||||
|
assert not state.get("global_preset_name") # Should be None or ""
|
||||||
|
|
||||||
|
# Prompt remains from previous application
|
||||||
|
assert state["global_system_prompt"] == "Overridden Prompt"
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Cleanup
|
||||||
|
if global_presets_path.exists(): global_presets_path.unlink()
|
||||||
|
if project_presets_path.exists(): project_presets_path.unlink()
|
||||||
|
|
||||||
|
def test_preset_manager_modal(live_gui):
|
||||||
|
client = ApiHookClient()
|
||||||
|
temp_workspace = Path("tests/artifacts/live_gui_workspace")
|
||||||
|
global_presets_path = temp_workspace / "presets.toml"
|
||||||
|
project_presets_path = temp_workspace / "project_presets.toml"
|
||||||
|
|
||||||
|
# Open Modal
|
||||||
|
client.set_value("show_preset_manager_modal", True)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# Create New Preset via Modal Logic (triggering the callback directly for reliability in headless)
|
||||||
|
client.push_event("custom_callback", {
|
||||||
|
"callback": "_cb_save_preset",
|
||||||
|
"args": ["ModalPreset", "Modal Content", 0.9, 1.0, 4096, "global"]
|
||||||
|
})
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
# Verify file exists
|
||||||
|
assert global_presets_path.exists()
|
||||||
|
with open(global_presets_path, "rb") as f:
|
||||||
|
import tomllib
|
||||||
|
data = tomllib.load(f)
|
||||||
|
assert "ModalPreset" in data["presets"]
|
||||||
|
assert data["presets"]["ModalPreset"]["temperature"] == 0.9
|
||||||
|
|
||||||
|
# Delete Preset via Modal Logic
|
||||||
|
client.push_event("custom_callback", {
|
||||||
|
"callback": "_cb_delete_preset",
|
||||||
|
"args": ["ModalPreset", "global"]
|
||||||
|
})
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
# Verify file content
|
||||||
|
with open(global_presets_path, "rb") as f:
|
||||||
|
data = tomllib.load(f)
|
||||||
|
assert "ModalPreset" not in data["presets"]
|
||||||
Reference in New Issue
Block a user