Compare commits
10 Commits
b4396697dd
...
0474df5958
| Author | SHA1 | Date | |
|---|---|---|---|
| 0474df5958 | |||
| cf83aeeff3 | |||
| ca7d1b074f | |||
| 038c909ce3 | |||
| 84b6266610 | |||
| c5df29b760 | |||
| 791e1b7a81 | |||
| 573f5ee5d1 | |||
| 1e223b46b0 | |||
| 93a590cdc5 |
@@ -17,7 +17,7 @@ For deep implementation details when planning or implementing tracks, consult `d
|
|||||||
## Primary Use Cases
|
## Primary Use Cases
|
||||||
|
|
||||||
- **Full Control over Vendor APIs:** Exposing detailed API metrics and configuring deep agent capabilities directly within the GUI.
|
- **Full Control over Vendor APIs:** Exposing detailed API metrics and configuring deep agent capabilities directly within the GUI.
|
||||||
- **Context & Memory Management:** Better visualization and management of token usage and context memory. Includes granular per-file flags (**Auto-Aggregate**, **Force Full**) and a dedicated **'Context' role** for manual injections, allowing developers to optimize prompt limits with expert precision.
|
- **Context & Memory Management:** Better visualization and management of token usage and context memory. Includes granular per-file flags (**Auto-Aggregate**, **Force Full**), a dedicated **'Context' role** for manual injections, and **Context Presets** for saving and loading named file/screenshot selections. Allows assigning specific context presets to MMA agent personas for granular cognitive load isolation.
|
||||||
- **Manual "Vibe Coding" Assistant:** Serving as an auxiliary, multi-provider assistant that natively interacts with the codebase via sandboxed PowerShell scripts and MCP-like file tools, emphasizing manual developer oversight and explicit confirmation.
|
- **Manual "Vibe Coding" Assistant:** Serving as an auxiliary, multi-provider assistant that natively interacts with the codebase via sandboxed PowerShell scripts and MCP-like file tools, emphasizing manual developer oversight and explicit confirmation.
|
||||||
|
|
||||||
## Key Features
|
## Key Features
|
||||||
@@ -33,6 +33,7 @@ For deep implementation details when planning or implementing tracks, consult `d
|
|||||||
- **Track Browser:** Real-time visualization of all implementation tracks with status indicators and progress bars. Includes a dedicated **Active Track Summary** featuring a color-coded progress bar, precise ticket status breakdown (Completed, In Progress, Blocked, Todo), and dynamic **ETA estimation** based on historical completion times.
|
- **Track Browser:** Real-time visualization of all implementation tracks with status indicators and progress bars. Includes a dedicated **Active Track Summary** featuring a color-coded progress bar, precise ticket status breakdown (Completed, In Progress, Blocked, Todo), and dynamic **ETA estimation** based on historical completion times.
|
||||||
- **Visual Task DAG:** An interactive, node-based visualizer for the active track's task dependencies using `imgui-node-editor`. Features color-coded state tracking (Ready, Running, Blocked, Done), drag-and-drop dependency creation, and right-click deletion.
|
- **Visual Task DAG:** An interactive, node-based visualizer for the active track's task dependencies using `imgui-node-editor`. Features color-coded state tracking (Ready, Running, Blocked, Done), drag-and-drop dependency creation, and right-click deletion.
|
||||||
- **Strategy Visualization:** Dedicated real-time output streams for Tier 1 (Strategic Planning) and Tier 2/3 (Execution) agents, allowing the user to follow the agent's reasoning chains alongside the task DAG.
|
- **Strategy Visualization:** Dedicated real-time output streams for Tier 1 (Strategic Planning) and Tier 2/3 (Execution) agents, allowing the user to follow the agent's reasoning chains alongside the task DAG.
|
||||||
|
- **Agent-Focused Filtering:** Allows the user to focus the entire GUI (Session Hub, Discussion Hub, Comms) on a specific agent's activities and scoped context.
|
||||||
- **Track-Scoped State Management:** Segregates discussion history and task progress into per-track state files. Supports **Project-Specific Conductor Directories**, defaulting to `./conductor` relative to each project's TOML file. Projects can define their own conductor path override in `manual_slop.toml` (`[conductor].dir`) via the Projects tab for isolated track management. This prevents global context pollution and ensures the Tech Lead session is isolated to the specific track's objective.
|
- **Track-Scoped State Management:** Segregates discussion history and task progress into per-track state files. Supports **Project-Specific Conductor Directories**, defaulting to `./conductor` relative to each project's TOML file. Projects can define their own conductor path override in `manual_slop.toml` (`[conductor].dir`) via the Projects tab for isolated track management. This prevents global context pollution and ensures the Tech Lead session is isolated to the specific track's objective.
|
||||||
**Native DAG Execution Engine:** Employs a Python-based Directed Acyclic Graph (DAG) engine to manage complex task dependencies. Supports automated topological sorting, robust cycle detection, and **transitive blocking propagation** (cascading `blocked` status to downstream dependents to prevent execution stalls).
|
**Native DAG Execution Engine:** Employs a Python-based Directed Acyclic Graph (DAG) engine to manage complex task dependencies. Supports automated topological sorting, robust cycle detection, and **transitive blocking propagation** (cascading `blocked` status to downstream dependents to prevent execution stalls).
|
||||||
|
|
||||||
@@ -54,6 +55,8 @@ For deep implementation details when planning or implementing tracks, consult `d
|
|||||||
- **High-Fidelity Selectable UI:** Most read-only labels and logs across the interface (including discussion history, comms payloads, tool outputs, and telemetry metrics) are now implemented as selectable text fields. This enables standard OS-level text selection and copying (Ctrl+C) while maintaining a high-density, non-editable aesthetic.
|
- **High-Fidelity Selectable UI:** Most read-only labels and logs across the interface (including discussion history, comms payloads, tool outputs, and telemetry metrics) are now implemented as selectable text fields. This enables standard OS-level text selection and copying (Ctrl+C) while maintaining a high-density, non-editable aesthetic.
|
||||||
- **High-Fidelity UI Rendering:** Employs advanced 3x font oversampling and sub-pixel positioning to ensure crisp, high-clarity text rendering across all resolutions, enhancing readability for dense logs and complex code fragments.
|
- **High-Fidelity UI Rendering:** Employs advanced 3x font oversampling and sub-pixel positioning to ensure crisp, high-clarity text rendering across all resolutions, enhancing readability for dense logs and complex code fragments.
|
||||||
- **Enhanced MMA Observability:** Worker streams and ticket previews now support direct text selection, allowing for easy extraction of specific logs or reasoning fragments during parallel execution.
|
- **Enhanced MMA Observability:** Worker streams and ticket previews now support direct text selection, allowing for easy extraction of specific logs or reasoning fragments during parallel execution.
|
||||||
|
- **Transparent Context Visibility:** A dedicated **Session Hub** exposes the exact aggregated markdown and resolved system prompt sent to the AI.
|
||||||
|
- **Injection Timeline:** Discussion history visually indicates the precise moments when files or screenshots were injected into the session context.
|
||||||
- **Detailed History Management:** Rich discussion history with branching, timestamping, and specific git commit linkage per conversation.
|
- **Detailed History Management:** Rich discussion history with branching, timestamping, and specific git commit linkage per conversation.
|
||||||
- **Advanced Log Management:** Optimizes log storage by offloading large data (AI-generated scripts and tool outputs) to unique files within the session directory, using compact `[REF:filename]` pointers in JSON-L logs to minimize token overhead during analysis. Features a dedicated **Log Management panel** for monitoring, whitelisting, and pruning session logs.
|
- **Advanced Log Management:** Optimizes log storage by offloading large data (AI-generated scripts and tool outputs) to unique files within the session directory, using compact `[REF:filename]` pointers in JSON-L logs to minimize token overhead during analysis. Features a dedicated **Log Management panel** for monitoring, whitelisting, and pruning session logs.
|
||||||
- **Full Session Restoration:** Allows users to load and reconstruct entire historical sessions from their log directories. Includes a dedicated, tinted **'Historical Replay' mode** that populates discussion history and provides a read-only view of prior agent activities.
|
- **Full Session Restoration:** Allows users to load and reconstruct entire historical sessions from their log directories. Includes a dedicated, tinted **'Historical Replay' mode** that populates discussion history and provides a read-only view of prior agent activities.
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ This file tracks all major tracks for the project. Each track has its own detail
|
|||||||
*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.*
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -243,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
|
||||||
@@ -2516,6 +2518,11 @@ class AppController:
|
|||||||
# Build discussion history text separately
|
# Build discussion history text separately
|
||||||
history = flat.get("discussion", {}).get("history", [])
|
history = flat.get("discussion", {}).get("history", [])
|
||||||
discussion_text = aggregate.build_discussion_text(history)
|
discussion_text = aggregate.build_discussion_text(history)
|
||||||
|
|
||||||
|
csp = filter(bool, [self.ui_global_system_prompt.strip(), self.ui_project_system_prompt.strip()])
|
||||||
|
self.last_resolved_system_prompt = "\n\n".join(csp)
|
||||||
|
self.last_aggregate_markdown = full_md
|
||||||
|
|
||||||
return full_md, path, file_items, stable_md, discussion_text
|
return full_md, path, file_items, stable_md, discussion_text
|
||||||
|
|
||||||
def _cb_plan_epic(self) -> None:
|
def _cb_plan_epic(self) -> None:
|
||||||
|
|||||||
145
src/gui_2.py
145
src/gui_2.py
@@ -108,6 +108,16 @@ class App:
|
|||||||
self.show_windows.setdefault("Diagnostics", False)
|
self.show_windows.setdefault("Diagnostics", False)
|
||||||
self.controller.start_services(self)
|
self.controller.start_services(self)
|
||||||
self.controller._predefined_callbacks['_render_text_viewer'] = self._render_text_viewer
|
self.controller._predefined_callbacks['_render_text_viewer'] = self._render_text_viewer
|
||||||
|
self.controller._predefined_callbacks['save_context_preset'] = self.save_context_preset
|
||||||
|
self.controller._predefined_callbacks['load_context_preset'] = self.load_context_preset
|
||||||
|
self.controller._predefined_callbacks['set_ui_file_paths'] = lambda p: setattr(self, 'ui_file_paths', p)
|
||||||
|
self.controller._predefined_callbacks['set_ui_screenshot_paths'] = lambda p: setattr(self, 'ui_screenshot_paths', p)
|
||||||
|
def simulate_save_preset(name: str):
|
||||||
|
from src import models
|
||||||
|
self.files = [models.FileItem(path='test.py')]
|
||||||
|
self.screenshots = ['test.png']
|
||||||
|
self.save_context_preset(name)
|
||||||
|
self.controller._predefined_callbacks['simulate_save_preset'] = simulate_save_preset
|
||||||
self.show_preset_manager_window = False
|
self.show_preset_manager_window = False
|
||||||
self.show_tool_preset_manager_window = False
|
self.show_tool_preset_manager_window = False
|
||||||
self.show_persona_editor_window = False
|
self.show_persona_editor_window = False
|
||||||
@@ -119,6 +129,7 @@ class App:
|
|||||||
self._text_viewer_editor: Optional[ced.TextEditor] = None
|
self._text_viewer_editor: Optional[ced.TextEditor] = None
|
||||||
self.ui_active_tool_preset = ""
|
self.ui_active_tool_preset = ""
|
||||||
self.ui_active_bias_profile = ""
|
self.ui_active_bias_profile = ""
|
||||||
|
self.ui_active_context_preset = ""
|
||||||
self.ui_active_persona = ""
|
self.ui_active_persona = ""
|
||||||
self._editing_persona_name = ""
|
self._editing_persona_name = ""
|
||||||
self._editing_persona_description = ""
|
self._editing_persona_description = ""
|
||||||
@@ -130,6 +141,7 @@ class App:
|
|||||||
self._editing_persona_max_tokens = 4096
|
self._editing_persona_max_tokens = 4096
|
||||||
self._editing_persona_tool_preset_id = ""
|
self._editing_persona_tool_preset_id = ""
|
||||||
self._editing_persona_bias_profile_id = ""
|
self._editing_persona_bias_profile_id = ""
|
||||||
|
self._editing_persona_context_preset_id = ""
|
||||||
self._editing_persona_preferred_models_list: list[dict] = []
|
self._editing_persona_preferred_models_list: list[dict] = []
|
||||||
self._editing_persona_scope = "project"
|
self._editing_persona_scope = "project"
|
||||||
self._editing_persona_is_new = True
|
self._editing_persona_is_new = True
|
||||||
@@ -202,6 +214,7 @@ class App:
|
|||||||
self.show_windows.setdefault("Tier 4: QA", False)
|
self.show_windows.setdefault("Tier 4: QA", False)
|
||||||
self.show_windows.setdefault('External Tools', False)
|
self.show_windows.setdefault('External Tools', False)
|
||||||
self.show_windows.setdefault('Shader Editor', False)
|
self.show_windows.setdefault('Shader Editor', False)
|
||||||
|
self.show_windows.setdefault('Session Hub', False)
|
||||||
self.ui_multi_viewport = gui_cfg.get("multi_viewport", False)
|
self.ui_multi_viewport = gui_cfg.get("multi_viewport", False)
|
||||||
self.layout_presets = self.config.get("layout_presets", {})
|
self.layout_presets = self.config.get("layout_presets", {})
|
||||||
self._new_preset_name = ""
|
self._new_preset_name = ""
|
||||||
@@ -221,8 +234,8 @@ class App:
|
|||||||
self.ui_tool_filter_category = "All"
|
self.ui_tool_filter_category = "All"
|
||||||
self.ui_discussion_split_h = 300.0
|
self.ui_discussion_split_h = 300.0
|
||||||
self.shader_uniforms = {'crt': 1.0, 'scanline': 0.5, 'bloom': 0.8}
|
self.shader_uniforms = {'crt': 1.0, 'scanline': 0.5, 'bloom': 0.8}
|
||||||
|
self.shader_uniforms = {'crt': 1.0, 'scanline': 0.5, 'bloom': 0.8}
|
||||||
def _handle_approve_tool(self, user_data=None) -> None:
|
self.ui_new_context_preset_name = ""
|
||||||
"""UI-level wrapper for approving a pending tool execution ask."""
|
"""UI-level wrapper for approving a pending tool execution ask."""
|
||||||
self._handle_approve_ask()
|
self._handle_approve_ask()
|
||||||
|
|
||||||
@@ -280,6 +293,54 @@ class App:
|
|||||||
pass
|
pass
|
||||||
self.controller.shutdown()
|
self.controller.shutdown()
|
||||||
|
|
||||||
|
def save_context_preset(self, name: str) -> None:
|
||||||
|
sys.stderr.write(f"[DEBUG] save_context_preset called with: {name}\n")
|
||||||
|
sys.stderr.flush()
|
||||||
|
if 'context_presets' not in self.controller.project:
|
||||||
|
self.controller.project['context_presets'] = {}
|
||||||
|
self.controller.project['context_presets'][name] = {
|
||||||
|
'files': [f.to_dict() if hasattr(f, 'to_dict') else {'path': str(f)} for f in self.files],
|
||||||
|
'screenshots': list(self.screenshots)
|
||||||
|
}
|
||||||
|
self.controller._save_active_project()
|
||||||
|
sys.stderr.write(f"[DEBUG] save_context_preset finished. Project keys: {list(self.controller.project.keys())}\n")
|
||||||
|
sys.stderr.flush()
|
||||||
|
|
||||||
|
def load_context_preset(self, name: str) -> None:
|
||||||
|
presets = self.controller.project.get('context_presets', {})
|
||||||
|
if name in presets:
|
||||||
|
preset = presets[name]
|
||||||
|
self.files = [models.FileItem.from_dict(f) if isinstance(f, dict) else models.FileItem(path=str(f)) for f in preset.get('files', [])]
|
||||||
|
self.screenshots = list(preset.get('screenshots', []))
|
||||||
|
|
||||||
|
def delete_context_preset(self, name: str) -> None:
|
||||||
|
if 'context_presets' in self.controller.project:
|
||||||
|
self.controller.project['context_presets'].pop(name, None)
|
||||||
|
self.controller._save_active_project()
|
||||||
|
@property
|
||||||
|
def ui_file_paths(self) -> list[str]:
|
||||||
|
return [f.path if hasattr(f, 'path') else str(f) for f in self.files]
|
||||||
|
|
||||||
|
@ui_file_paths.setter
|
||||||
|
def ui_file_paths(self, paths: list[str]) -> None:
|
||||||
|
old_files = {f.path: f for f in self.files if hasattr(f, 'path')}
|
||||||
|
new_files = []
|
||||||
|
now = time.time()
|
||||||
|
for p in paths:
|
||||||
|
if p in old_files:
|
||||||
|
new_files.append(old_files[p])
|
||||||
|
else:
|
||||||
|
new_files.append(models.FileItem(path=p, injected_at=now))
|
||||||
|
self.files = new_files
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ui_screenshot_paths(self) -> list[str]:
|
||||||
|
return self.screenshots
|
||||||
|
|
||||||
|
@ui_screenshot_paths.setter
|
||||||
|
def ui_screenshot_paths(self, paths: list[str]) -> None:
|
||||||
|
self.screenshots = paths
|
||||||
|
|
||||||
def _test_callback_func_write_to_file(self, data: str) -> None:
|
def _test_callback_func_write_to_file(self, data: str) -> None:
|
||||||
"""A dummy function that a custom_callback would execute for testing."""
|
"""A dummy function that a custom_callback would execute for testing."""
|
||||||
# Ensure the directory exists if running from a different cwd
|
# Ensure the directory exists if running from a different cwd
|
||||||
@@ -587,6 +648,9 @@ class App:
|
|||||||
if imgui.begin_tab_item('Paths')[0]:
|
if imgui.begin_tab_item('Paths')[0]:
|
||||||
self._render_paths_panel()
|
self._render_paths_panel()
|
||||||
imgui.end_tab_item()
|
imgui.end_tab_item()
|
||||||
|
if imgui.begin_tab_item('Context Presets')[0]:
|
||||||
|
self._render_context_presets_panel()
|
||||||
|
imgui.end_tab_item()
|
||||||
imgui.end_tab_bar()
|
imgui.end_tab_bar()
|
||||||
imgui.end()
|
imgui.end()
|
||||||
if self.show_windows.get("Files & Media", False):
|
if self.show_windows.get("Files & Media", False):
|
||||||
@@ -794,6 +858,8 @@ class App:
|
|||||||
if self.show_windows.get("Diagnostics", False):
|
if self.show_windows.get("Diagnostics", False):
|
||||||
self._render_diagnostics_panel()
|
self._render_diagnostics_panel()
|
||||||
|
|
||||||
|
self._render_session_hub()
|
||||||
|
|
||||||
self.perf_monitor.end_frame()
|
self.perf_monitor.end_frame()
|
||||||
# ---- Modals / Popups
|
# ---- Modals / Popups
|
||||||
with self._pending_dialog_lock:
|
with self._pending_dialog_lock:
|
||||||
@@ -1420,6 +1486,7 @@ class App:
|
|||||||
if imgui.button("New Persona", imgui.ImVec2(-1, 0)):
|
if imgui.button("New Persona", imgui.ImVec2(-1, 0)):
|
||||||
self._editing_persona_name = ""; self._editing_persona_system_prompt = ""
|
self._editing_persona_name = ""; self._editing_persona_system_prompt = ""
|
||||||
self._editing_persona_tool_preset_id = ""; self._editing_persona_bias_profile_id = ""
|
self._editing_persona_tool_preset_id = ""; self._editing_persona_bias_profile_id = ""
|
||||||
|
self._editing_persona_context_preset_id = ""
|
||||||
self._editing_persona_preferred_models_list = [{"provider": self.current_provider, "model": self.current_model, "temperature": 0.7, "top_p": 1.0, "max_output_tokens": 4096, "history_trunc_limit": 900000}]
|
self._editing_persona_preferred_models_list = [{"provider": self.current_provider, "model": self.current_model, "temperature": 0.7, "top_p": 1.0, "max_output_tokens": 4096, "history_trunc_limit": 900000}]
|
||||||
self._editing_persona_scope = "project"; self._editing_persona_is_new = True
|
self._editing_persona_scope = "project"; self._editing_persona_is_new = True
|
||||||
imgui.separator()
|
imgui.separator()
|
||||||
@@ -1428,6 +1495,7 @@ class App:
|
|||||||
if name and imgui.selectable(f"{name}##p_list", name == self._editing_persona_name and not getattr(self, '_editing_persona_is_new', False))[0]:
|
if name and imgui.selectable(f"{name}##p_list", name == self._editing_persona_name and not getattr(self, '_editing_persona_is_new', False))[0]:
|
||||||
p = personas[name]; self._editing_persona_name = p.name; self._editing_persona_system_prompt = p.system_prompt or ""
|
p = personas[name]; self._editing_persona_name = p.name; self._editing_persona_system_prompt = p.system_prompt or ""
|
||||||
self._editing_persona_tool_preset_id = p.tool_preset or ""; self._editing_persona_bias_profile_id = p.bias_profile or ""
|
self._editing_persona_tool_preset_id = p.tool_preset or ""; self._editing_persona_bias_profile_id = p.bias_profile or ""
|
||||||
|
self._editing_persona_context_preset_id = getattr(p, 'context_preset', '') or ""
|
||||||
import copy; self._editing_persona_preferred_models_list = copy.deepcopy(p.preferred_models) if p.preferred_models else []
|
import copy; self._editing_persona_preferred_models_list = copy.deepcopy(p.preferred_models) if p.preferred_models else []
|
||||||
self._editing_persona_scope = self.controller.persona_manager.get_persona_scope(p.name); self._editing_persona_is_new = False
|
self._editing_persona_scope = self.controller.persona_manager.get_persona_scope(p.name); self._editing_persona_is_new = False
|
||||||
imgui.end_child()
|
imgui.end_child()
|
||||||
@@ -1513,6 +1581,10 @@ class App:
|
|||||||
imgui.table_next_column(); imgui.text("Bias Profile:"); bn = ["None"] + sorted(self.controller.bias_profiles.keys())
|
imgui.table_next_column(); imgui.text("Bias Profile:"); bn = ["None"] + sorted(self.controller.bias_profiles.keys())
|
||||||
b_idx = bn.index(self._editing_persona_bias_profile_id) if getattr(self, '_editing_persona_bias_profile_id', '') in bn else 0
|
b_idx = bn.index(self._editing_persona_bias_profile_id) if getattr(self, '_editing_persona_bias_profile_id', '') in bn else 0
|
||||||
imgui.set_next_item_width(-1); _, b_idx = imgui.combo("##pbp", b_idx, bn); self._editing_persona_bias_profile_id = bn[b_idx] if b_idx > 0 else ""
|
imgui.set_next_item_width(-1); _, b_idx = imgui.combo("##pbp", b_idx, bn); self._editing_persona_bias_profile_id = bn[b_idx] if b_idx > 0 else ""
|
||||||
|
imgui.table_next_row()
|
||||||
|
imgui.table_next_column(); imgui.text("Context Preset:"); cn = ["None"] + sorted(self.controller.project.get("context_presets", {}).keys())
|
||||||
|
c_idx = cn.index(self._editing_persona_context_preset_id) if getattr(self, '_editing_persona_context_preset_id', '') in cn else 0
|
||||||
|
imgui.set_next_item_width(-1); _, c_idx = imgui.combo("##pcp", c_idx, cn); self._editing_persona_context_preset_id = cn[c_idx] if c_idx > 0 else ""
|
||||||
imgui.end_table()
|
imgui.end_table()
|
||||||
|
|
||||||
if imgui.button("Manage Tools & Biases", imgui.ImVec2(-1, 0)): self.show_tool_preset_manager_window = True
|
if imgui.button("Manage Tools & Biases", imgui.ImVec2(-1, 0)): self.show_tool_preset_manager_window = True
|
||||||
@@ -1540,7 +1612,7 @@ class App:
|
|||||||
if imgui.button("Save##pers", imgui.ImVec2(100, 0)):
|
if imgui.button("Save##pers", imgui.ImVec2(100, 0)):
|
||||||
if self._editing_persona_name.strip():
|
if self._editing_persona_name.strip():
|
||||||
try:
|
try:
|
||||||
import copy; persona = models.Persona(name=self._editing_persona_name.strip(), system_prompt=self._editing_persona_system_prompt, tool_preset=self._editing_persona_tool_preset_id or None, bias_profile=self._editing_persona_bias_profile_id or None, preferred_models=copy.deepcopy(self._editing_persona_preferred_models_list))
|
import copy; persona = models.Persona(name=self._editing_persona_name.strip(), system_prompt=self._editing_persona_system_prompt, tool_preset=self._editing_persona_tool_preset_id or None, bias_profile=self._editing_persona_bias_profile_id or None, context_preset=self._editing_persona_context_preset_id or None, preferred_models=copy.deepcopy(self._editing_persona_preferred_models_list))
|
||||||
self.controller._cb_save_persona(persona, getattr(self, '_editing_persona_scope', 'project')); self.ai_status = f"Saved: {persona.name}"
|
self.controller._cb_save_persona(persona, getattr(self, '_editing_persona_scope', 'project')); self.ai_status = f"Saved: {persona.name}"
|
||||||
except Exception as e: self.ai_status = f"Error: {e}"
|
except Exception as e: self.ai_status = f"Error: {e}"
|
||||||
else: self.ai_status = "Name required"
|
else: self.ai_status = "Name required"
|
||||||
@@ -1698,6 +1770,30 @@ class App:
|
|||||||
self.ai_status = "paths reset to defaults"
|
self.ai_status = "paths reset to defaults"
|
||||||
|
|
||||||
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_paths_panel")
|
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_paths_panel")
|
||||||
|
|
||||||
|
def _render_context_presets_panel(self) -> None:
|
||||||
|
imgui.text_colored(C_IN, "Context Presets")
|
||||||
|
imgui.separator()
|
||||||
|
changed, new_name = imgui.input_text("Preset Name##new_ctx", self.ui_new_context_preset_name)
|
||||||
|
if changed: self.ui_new_context_preset_name = new_name
|
||||||
|
imgui.same_line()
|
||||||
|
if imgui.button("Save Current"):
|
||||||
|
if self.ui_new_context_preset_name.strip():
|
||||||
|
self.save_context_preset(self.ui_new_context_preset_name.strip())
|
||||||
|
|
||||||
|
imgui.separator()
|
||||||
|
presets = self.controller.project.get('context_presets', {})
|
||||||
|
for name in sorted(presets.keys()):
|
||||||
|
preset = presets[name]
|
||||||
|
n_files = len(preset.get('files', []))
|
||||||
|
n_shots = len(preset.get('screenshots', []))
|
||||||
|
imgui.text(f"{name} ({n_files} files, {n_shots} shots)")
|
||||||
|
imgui.same_line()
|
||||||
|
if imgui.button(f"Load##{name}"):
|
||||||
|
self.load_context_preset(name)
|
||||||
|
imgui.same_line()
|
||||||
|
if imgui.button(f"Delete##{name}"):
|
||||||
|
self.delete_context_preset(name)
|
||||||
def _render_track_proposal_modal(self) -> None:
|
def _render_track_proposal_modal(self) -> None:
|
||||||
if self._show_track_proposal_modal:
|
if self._show_track_proposal_modal:
|
||||||
imgui.open_popup("Track Proposal")
|
imgui.open_popup("Track Proposal")
|
||||||
@@ -2002,6 +2098,29 @@ class App:
|
|||||||
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_diagnostics_panel")
|
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_diagnostics_panel")
|
||||||
imgui.end()
|
imgui.end()
|
||||||
|
|
||||||
|
def _render_session_hub(self) -> None:
|
||||||
|
if self.show_windows.get('Session Hub', False):
|
||||||
|
exp, opened = imgui.begin('Session Hub', self.show_windows['Session Hub'])
|
||||||
|
self.show_windows['Session Hub'] = bool(opened)
|
||||||
|
if exp:
|
||||||
|
if imgui.begin_tab_bar('session_hub_tabs'):
|
||||||
|
if imgui.begin_tab_item('Aggregate MD')[0]:
|
||||||
|
if imgui.button("Copy"):
|
||||||
|
imgui.set_clipboard_text(self.last_aggregate_markdown)
|
||||||
|
imgui.begin_child("last_agg_md", imgui.ImVec2(0, 0), True)
|
||||||
|
markdown_helper.render(self.last_aggregate_markdown, context_id="session_hub_agg")
|
||||||
|
imgui.end_child()
|
||||||
|
imgui.end_tab_item()
|
||||||
|
if imgui.begin_tab_item('System Prompt')[0]:
|
||||||
|
if imgui.button("Copy"):
|
||||||
|
imgui.set_clipboard_text(self.last_resolved_system_prompt)
|
||||||
|
imgui.begin_child("last_sys_prompt", imgui.ImVec2(0, 0), True)
|
||||||
|
markdown_helper.render(self.last_resolved_system_prompt, context_id="session_hub_sys")
|
||||||
|
imgui.end_child()
|
||||||
|
imgui.end_tab_item()
|
||||||
|
imgui.end_tab_bar()
|
||||||
|
imgui.end()
|
||||||
|
|
||||||
def _render_markdown_test(self) -> None:
|
def _render_markdown_test(self) -> None:
|
||||||
imgui.text("Markdown Test Panel")
|
imgui.text("Markdown Test Panel")
|
||||||
imgui.separator()
|
imgui.separator()
|
||||||
@@ -2312,6 +2431,22 @@ def hello():
|
|||||||
if ts_str:
|
if ts_str:
|
||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
imgui.text_colored(vec4(120, 120, 100), str(ts_str))
|
imgui.text_colored(vec4(120, 120, 100), str(ts_str))
|
||||||
|
# Visual indicator for file injections
|
||||||
|
e_dt = project_manager.parse_ts(ts_str)
|
||||||
|
if e_dt:
|
||||||
|
e_unix = e_dt.timestamp()
|
||||||
|
next_unix = float('inf')
|
||||||
|
if i + 1 < len(self.disc_entries):
|
||||||
|
n_ts = self.disc_entries[i+1].get("ts", "")
|
||||||
|
n_dt = project_manager.parse_ts(n_ts)
|
||||||
|
if n_dt: next_unix = n_dt.timestamp()
|
||||||
|
injected_here = [f for f in self.files if hasattr(f, 'injected_at') and f.injected_at and e_unix <= f.injected_at < next_unix]
|
||||||
|
if injected_here:
|
||||||
|
imgui.same_line()
|
||||||
|
imgui.text_colored(vec4(100, 255, 100), f"[{len(injected_here)}+]")
|
||||||
|
if imgui.is_item_hovered():
|
||||||
|
tooltip = "Files injected at this point:\n" + "\n".join([f.path for f in injected_here])
|
||||||
|
imgui.set_tooltip(tooltip)
|
||||||
if collapsed:
|
if collapsed:
|
||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
if imgui.button("Ins"):
|
if imgui.button("Ins"):
|
||||||
@@ -2405,6 +2540,7 @@ def hello():
|
|||||||
self._editing_persona_system_prompt = persona.system_prompt or ""
|
self._editing_persona_system_prompt = persona.system_prompt or ""
|
||||||
self._editing_persona_tool_preset_id = persona.tool_preset or ""
|
self._editing_persona_tool_preset_id = persona.tool_preset or ""
|
||||||
self._editing_persona_bias_profile_id = persona.bias_profile or ""
|
self._editing_persona_bias_profile_id = persona.bias_profile or ""
|
||||||
|
self._editing_persona_context_preset_id = getattr(persona, 'context_preset', '') or ""
|
||||||
import copy
|
import copy
|
||||||
self._editing_persona_preferred_models_list = copy.deepcopy(persona.preferred_models) if persona.preferred_models else []
|
self._editing_persona_preferred_models_list = copy.deepcopy(persona.preferred_models) if persona.preferred_models else []
|
||||||
self._editing_persona_is_new = False
|
self._editing_persona_is_new = False
|
||||||
@@ -2433,6 +2569,9 @@ def hello():
|
|||||||
if persona.bias_profile:
|
if persona.bias_profile:
|
||||||
self.ui_active_bias_profile = persona.bias_profile
|
self.ui_active_bias_profile = persona.bias_profile
|
||||||
ai_client.set_bias_profile(persona.bias_profile)
|
ai_client.set_bias_profile(persona.bias_profile)
|
||||||
|
if getattr(persona, 'context_preset', None):
|
||||||
|
self.ui_active_context_preset = persona.context_preset
|
||||||
|
self.load_context_preset(persona.context_preset)
|
||||||
imgui.end_combo()
|
imgui.end_combo()
|
||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
if imgui.button("Manage Personas"):
|
if imgui.button("Manage Personas"):
|
||||||
|
|||||||
@@ -357,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
|
||||||
@@ -371,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
|
||||||
@@ -466,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]:
|
||||||
@@ -508,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
|
||||||
@@ -541,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
|
||||||
|
|||||||
@@ -101,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": {
|
||||||
@@ -248,6 +249,27 @@ def flat_config(proj: dict[str, Any], disc_name: Optional[str] = None, track_id:
|
|||||||
"history": history,
|
"history": history,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
# ── context presets ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def save_context_preset(project_dict: dict, preset_name: str, files: list[str], screenshots: list[str]) -> None:
|
||||||
|
"""Save a named context preset (files + screenshots) into the project dict."""
|
||||||
|
if "context_presets" not in project_dict:
|
||||||
|
project_dict["context_presets"] = {}
|
||||||
|
project_dict["context_presets"][preset_name] = {
|
||||||
|
"files": files,
|
||||||
|
"screenshots": screenshots
|
||||||
|
}
|
||||||
|
|
||||||
|
def load_context_preset(project_dict: dict, preset_name: str) -> dict:
|
||||||
|
"""Return the files and screenshots for a named preset."""
|
||||||
|
if "context_presets" not in project_dict or preset_name not in project_dict["context_presets"]:
|
||||||
|
raise KeyError(f"Preset '{preset_name}' not found in project context_presets.")
|
||||||
|
return project_dict["context_presets"][preset_name]
|
||||||
|
|
||||||
|
def delete_context_preset(project_dict: dict, preset_name: str) -> None:
|
||||||
|
"""Remove a named preset if it exists."""
|
||||||
|
if "context_presets" in project_dict:
|
||||||
|
project_dict["context_presets"].pop(preset_name, None)
|
||||||
# ── track state persistence ─────────────────────────────────────────────────
|
# ── track state persistence ─────────────────────────────────────────────────
|
||||||
|
|
||||||
def save_track_state(track_id: str, state: 'TrackState', base_dir: Union[str, Path] = ".") -> None:
|
def save_track_state(track_id: str, state: 'TrackState', base_dir: Union[str, Path] = ".") -> None:
|
||||||
|
|||||||
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"]
|
||||||
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
|
||||||
Reference in New Issue
Block a user