Compare commits
3 Commits
7924d65438
...
23943443e3
| Author | SHA1 | Date | |
|---|---|---|---|
| 23943443e3 | |||
| 6f1fea85f0 | |||
| d237d3b94d |
@@ -60,7 +60,7 @@ For deep implementation details when planning or implementing tracks, consult `d
|
||||
- **Dedicated Diagnostics Hub:** Consolidates real-time telemetry (FPS, CPU, Frame Time) and transient system warnings into a standalone **Diagnostics panel**, providing deep visibility into application health without polluting the discussion history.
|
||||
- **Improved MMA Observability:** Enhances sub-agent logging by injecting precise ticket IDs and descriptive roles into communication metadata, enabling granular filtering and tracking of parallel worker activities within the Comms History.
|
||||
- **In-Depth Toolset Access:** MCP-like file exploration, URL fetching, search, and dynamic context aggregation embedded within a multi-viewport Dear PyGui/ImGui interface.
|
||||
- **Integrated Workspace:** A consolidated Hub-based layout (Context, AI Settings, Discussion, Operations) designed for expert multi-monitor workflows.
|
||||
- **Integrated Workspace:** A consolidated Hub-based layout (Context, AI Settings, Discussion, Operations) designed for expert multi-monitor workflows. Features **GUI-Based Path Configuration** within the Context Hub, allowing users to view and edit system paths (conductor, logs, scripts) with real-time resolution source tracking (default, env, or config).
|
||||
- **Session Analysis:** Ability to load and visualize historical session logs with a dedicated tinted "Prior Session" viewing mode.
|
||||
- **Structured Log Taxonomy:** Automated session-based log organization into configurable directories (defaulting to `logs/sessions/`). Includes a dedicated GUI panel for monitoring and manual whitelisting. Features an intelligent heuristic-based pruner that automatically cleans up insignificant logs older than 24 hours while preserving valuable sessions.
|
||||
- **Clean Project Root:** Enforces a "Cruft-Free Root" policy by organizing core implementation into a `src/` directory and redirecting all temporary test data, configurations, and AI-generated artifacts to `tests/artifacts/`.
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
- **ai_style_formatter.py:** Custom Python formatter specifically designed to enforce 1-space indentation and ultra-compact whitespace to minimize token consumption.
|
||||
|
||||
- **src/paths.py:** Centralized module for path resolution. Supports project-specific conductor directory overrides via project TOML (`[conductor].dir`), enabling isolated track management per project. All paths are resolved to absolute objects. Path configuration (logs, conductor, scripts) can also be configured via `config.toml` or environment variables, eliminating hardcoded filesystem dependencies.
|
||||
- **src/paths.py:** Centralized module for path resolution. Supports project-specific conductor directory overrides via project TOML (`[conductor].dir`), enabling isolated track management per project. All paths are resolved to absolute objects. Provides **Path Resolution Metadata**, exposing the source of each resolved path (default, environment variable, or configuration file) for high-fidelity GUI display. Path configuration (logs, conductor, scripts) can also 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.
|
||||
|
||||
|
||||
+2
-2
@@ -105,11 +105,11 @@ This file tracks all major tracks for the project. Each track has its own detail
|
||||
|
||||
### Path Configuration
|
||||
|
||||
1. [ ] **Track: Project-Specific Conductor Directory**
|
||||
1. [x] **Track: Project-Specific Conductor Directory**
|
||||
*Link: [./tracks/project_conductor_dir_20260308/](./tracks/project_conductor_dir_20260308/)*
|
||||
*Goal: Make conductor directory per-project. Each project TOML can specify custom conductor dir for isolated track/state management.*
|
||||
|
||||
2. [ ] **Track: GUI Path Configuration in Context Hub**
|
||||
2. [x] **Track: GUI Path Configuration in Context Hub**
|
||||
*Link: [./tracks/gui_path_config_20260308/](./tracks/gui_path_config_20260308/)*
|
||||
*Goal: Add path configuration UI to Context Hub. Allow users to view and edit configurable paths directly from the GUI.*
|
||||
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
# Implementation Plan: External MCP Server Support
|
||||
|
||||
## Phase 1: Configuration & Data Modeling
|
||||
## Phase 1: Configuration & Data Modeling [checkpoint: 4ba1bd9]
|
||||
- [x] Task: Define the schema for external MCP server configuration. [1c863f0]
|
||||
- [x] Update `src/models.py` to include `MCPServerConfig` and `MCPConfiguration` classes.
|
||||
- [x] Implement logic to load `mcp_config.json` from global and project-specific paths.
|
||||
- [x] Task: Integrate configuration loading into `AppController`. [c09e0f5]
|
||||
- [x] Ensure the MCP config path is correctly resolved from `config.toml` and `manual_slop.toml`.
|
||||
- [x] Task: Write unit tests for configuration loading and validation. [c09e0f5]
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 1: Configuration & Data Modeling' (Protocol in workflow.md)
|
||||
- [x] Task: Conductor - User Manual Verification 'Phase 1: Configuration & Data Modeling' [4ba1bd9]
|
||||
|
||||
## Phase 2: MCP Client Extension
|
||||
- [ ] Task: Implement `ExternalMCPManager` in `src/mcp_client.py`.
|
||||
- [ ] Add support for managing multiple MCP server sessions.
|
||||
- [ ] Implement the `StdioMCPClient` for local subprocess communication.
|
||||
- [ ] Implement the `RemoteMCPClient` for SSE/WebSocket communication.
|
||||
- [ ] Task: Update Tool Discovery.
|
||||
- [ ] Implement `list_external_tools()` to aggregate tools from all active external servers.
|
||||
- [ ] Task: Update Tool Dispatch.
|
||||
- [ ] Modify `mcp_client.dispatch()` and `mcp_client.async_dispatch()` to route tool calls to either native tools or the appropriate external server.
|
||||
- [ ] Task: Write integration tests for stdio and remote MCP client communication (using mock servers).
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 2: MCP Client Extension' (Protocol in workflow.md)
|
||||
## Phase 2: MCP Client Extension [checkpoint: 828fadf]
|
||||
- [x] Task: Implement `ExternalMCPManager` in `src/mcp_client.py`. [828fadf]
|
||||
- [x] Add support for managing multiple MCP server sessions.
|
||||
- [x] Implement the `StdioMCPClient` for local subprocess communication.
|
||||
- [x] Implement the `RemoteMCPClient` for SSE/WebSocket communication (stub).
|
||||
- [x] Task: Update Tool Discovery. [828fadf]
|
||||
- [x] Implement `list_external_tools()` to aggregate tools from all active external servers.
|
||||
- [x] Task: Update Tool Dispatch. [828fadf]
|
||||
- [x] Modify `mcp_client.dispatch()` and `mcp_client.async_dispatch()` to route tool calls to either native tools or the appropriate external server.
|
||||
- [x] Task: Write integration tests for stdio and remote MCP client communication (using mock servers). [828fadf]
|
||||
- [x] Task: Conductor - User Manual Verification 'Phase 2: MCP Client Extension' [828fadf]
|
||||
|
||||
## Phase 3: GUI Integration & Lifecycle
|
||||
- [ ] Task: Update the **Operations** panel in `src/gui_2.py`.
|
||||
- [ ] Create a new "External Tools" section.
|
||||
- [ ] List discovered tools from active external servers.
|
||||
- [ ] Add a "Refresh External MCPs" button to reload configuration and rediscover tools.
|
||||
- [ ] Task: Implement Lifecycle Management.
|
||||
- [ ] Add the "Auto-start on Project Load" logic to start servers when a project is initialized.
|
||||
- [ ] Add status indicators (e.g., color-coded dots) for each external server in the GUI.
|
||||
- [ ] Task: Write visual regression tests or simulation scripts to verify the updated Operations panel.
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 3: GUI Integration & Lifecycle' (Protocol in workflow.md)
|
||||
## Phase 3: GUI Integration & Lifecycle [checkpoint: 3b2588a]
|
||||
- [x] Task: Update the **Operations** panel in `src/gui_2.py`. [3b2588a]
|
||||
- [x] Create a new "External Tools" section.
|
||||
- [x] List discovered tools from active external servers.
|
||||
- [x] Add a "Refresh External MCPs" button to reload configuration and rediscover tools.
|
||||
- [x] Task: Implement Lifecycle Management. [3b2588a]
|
||||
- [x] Add the "Auto-start on Project Load" logic to start servers when a project is initialized.
|
||||
- [x] Add status indicators (e.g., color-coded dots) for each external server in the GUI.
|
||||
- [x] Task: Write visual regression tests or simulation scripts to verify the updated Operations panel. [3b2588a]
|
||||
- [x] Task: Conductor - User Manual Verification 'Phase 3: GUI Integration & Lifecycle' [3b2588a]
|
||||
|
||||
## Phase 4: Agent Integration & HITL
|
||||
- [ ] Task: Update AI tool declarations.
|
||||
- [ ] Ensure `ai_client.py` includes external tools in the tool definitions sent to Gemini/Anthropic.
|
||||
- [ ] Task: Verify HITL Approval Flow.
|
||||
- [ ] Ensure that calling an external tool correctly triggers the `ConfirmDialog` modal.
|
||||
- [ ] Verify that approved external tool results are correctly returned to the AI.
|
||||
- [ ] Task: Perform a final end-to-end verification with a real external MCP server.
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 4: Agent Integration & HITL' (Protocol in workflow.md)
|
||||
## Phase 4: Agent Integration & HITL [checkpoint: f4c5a0b]
|
||||
- [x] Task: Update AI tool declarations. [f4c5a0b]
|
||||
- [x] Ensure `ai_client.py` includes external tools in the tool definitions sent to Gemini/Anthropic.
|
||||
- [x] Task: Verify HITL Approval Flow. [f4c5a0b]
|
||||
- [x] Ensure that calling an external tool correctly triggers the `ConfirmDialog` modal.
|
||||
- [x] Verify that approved external tool results are correctly returned to the AI.
|
||||
- [x] Task: Perform a final end-to-end verification with a real external MCP server. [f4c5a0b]
|
||||
- [x] Task: Conductor - User Manual Verification 'Phase 4: Agent Integration & HITL' [f4c5a0b]
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
## Phase 1: Path Info Display
|
||||
Focus: Show current path resolution in GUI
|
||||
|
||||
- [ ] Task 1.1: Add path info functions to paths.py
|
||||
- [x] Task 1.1: Add path info functions to paths.py [d237d3b]
|
||||
- WHERE: src/paths.py
|
||||
- WHAT: Add functions to get path resolution source (default/env/config)
|
||||
- HOW: Return tuple of (resolved_path, source)
|
||||
- SAFETY: New functions, no modifications
|
||||
|
||||
- [ ] Task 1.2: Create path display helper
|
||||
- [x] Task 1.2: Create path display helper [d237d3b]
|
||||
- WHERE: src/paths.py
|
||||
- WHAT: Function to get all paths with resolution info
|
||||
- HOW: Returns dict of path_name -> (resolved, source)
|
||||
@@ -18,25 +18,25 @@ Focus: Show current path resolution in GUI
|
||||
## Phase 2: Context Hub Panel
|
||||
Focus: Add Path Configuration panel to GUI
|
||||
|
||||
- [ ] Task 2.1: Add Paths tab to Context Hub
|
||||
- [x] Task 2.1: Add Paths tab to Context Hub [d237d3b]
|
||||
- WHERE: src/gui_2.py (Context Hub section)
|
||||
- WHAT: New tab/section for path configuration
|
||||
- HOW: Add ImGui tab item, follow existing panel patterns
|
||||
- SAFETY: New panel, no modifications to existing
|
||||
|
||||
- [ ] Task 2.2: Display current paths
|
||||
- [x] Task 2.2: Display current paths [d237d3b]
|
||||
- WHERE: src/gui_2.py (new paths panel)
|
||||
- WHAT: Show resolved paths and their sources
|
||||
- HOW: Call paths.py functions, display in read-only text
|
||||
- SAFETY: New code
|
||||
|
||||
- [ ] Task 2.3: Add path text inputs
|
||||
- [x] Task 2.3: Add path text inputs [d237d3b]
|
||||
- WHERE: src/gui_2.py (paths panel)
|
||||
- WHAT: Editable text inputs for each path
|
||||
- HOW: ImGui input_text for conductor_dir, logs_dir, scripts_dir
|
||||
- SAFETY: New code
|
||||
|
||||
- [ ] Task 2.4: Add browse buttons
|
||||
- [x] Task 2.4: Add browse buttons [d237d3b]
|
||||
- WHERE: src/gui_2.py (paths panel)
|
||||
- WHAT: File dialog buttons to browse for directories
|
||||
- HOW: Use existing file dialog patterns in gui_2.py
|
||||
@@ -45,19 +45,19 @@ Focus: Add Path Configuration panel to GUI
|
||||
## Phase 3: Persistence
|
||||
Focus: Save path changes to config.toml
|
||||
|
||||
- [ ] Task 3.1: Add config write function
|
||||
- [x] Task 3.1: Add config write function [d237d3b]
|
||||
- WHERE: src/gui_2.py or new utility
|
||||
- WHAT: Write [paths] section to config.toml
|
||||
- HOW: Read existing config, update paths section, write back
|
||||
- SAFETY: Backup before write, handle errors
|
||||
|
||||
- [ ] Task 3.2: Add Apply button
|
||||
- [x] Task 3.2: Add Apply button [d237d3b]
|
||||
- WHERE: src/gui_2.py (paths panel)
|
||||
- WHAT: Button to save changes
|
||||
- HOW: Call config write function, show success/error message
|
||||
- SAFETY: Confirmation dialog
|
||||
|
||||
- [ ] Task 3.3: Add Reset button
|
||||
- [x] Task 3.3: Add Reset button [d237d3b]
|
||||
- WHERE: src/gui_2.py (paths panel)
|
||||
- WHAT: Reset paths to defaults
|
||||
- HOW: Clear custom values, show confirmation
|
||||
@@ -66,13 +66,13 @@ Focus: Save path changes to config.toml
|
||||
## Phase 4: UX Polish
|
||||
Focus: Improve user experience
|
||||
|
||||
- [ ] Task 4.1: Add restart warning
|
||||
- [x] Task 4.1: Add restart warning [d237d3b]
|
||||
- WHERE: src/gui_2.py (paths panel)
|
||||
- WHAT: Show warning that changes require restart
|
||||
- HOW: Text label after Apply
|
||||
- SAFETY: New code
|
||||
|
||||
- [ ] Task 4.2: Add tooltips
|
||||
- [x] Task 4.2: Add tooltips [d237d3b]
|
||||
- WHERE: src/gui_2.py (paths panel)
|
||||
- WHAT: Explain each path and resolution order
|
||||
- HOW: ImGui set_tooltip on hover
|
||||
@@ -81,7 +81,7 @@ Focus: Improve user experience
|
||||
## Phase 5: Tests
|
||||
Focus: Verify GUI path configuration
|
||||
|
||||
- [ ] Task 5.1: Test path display
|
||||
- [x] Task 5.1: Test path display [d237d3b]
|
||||
- WHERE: tests/test_gui_paths.py (new file)
|
||||
- WHAT: Verify paths panel shows correct values
|
||||
- HOW: Mock paths.py, verify display
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
## Phase 1: Extend paths.py
|
||||
Focus: Add project-specific path resolution
|
||||
|
||||
- [ ] Task 1.1: Add project-aware conductor path functions
|
||||
- [x] Task 1.1: Add project-aware conductor path functions [48e2ed8]
|
||||
- WHERE: src/paths.py
|
||||
- WHAT: Add optional project_path parameter to get_conductor_dir, get_tracks_dir, get_track_state_dir
|
||||
- HOW: If project_path provided, resolve relative to project root; otherwise use global
|
||||
- SAFETY: Maintain backward compatibility with no-arg calls
|
||||
|
||||
- [ ] Task 1.2: Add project conductor path resolution
|
||||
- [x] Task 1.2: Add project conductor path resolution [48e2ed8]
|
||||
- WHERE: src/paths.py
|
||||
- WHAT: New function `_resolve_project_conductor_dir(project_path)` that reads from project TOML
|
||||
- HOW: Load project TOML, check `[conductor].dir` key
|
||||
@@ -18,18 +18,18 @@ Focus: Add project-specific path resolution
|
||||
## Phase 2: Update project_manager.py
|
||||
Focus: Use project-specific paths for track operations
|
||||
|
||||
- [ ] Task 2.1: Update save_track_state to use project conductor dir
|
||||
- [x] Task 2.1: Update save_track_state to use project conductor dir [3999e9c]
|
||||
- WHERE: src/project_manager.py (around line 240)
|
||||
- WHAT: Pass project base_dir to paths.get_track_state_dir()
|
||||
- HOW: Get base_dir from project_path, call paths with project_path param
|
||||
- SAFETY: Maintain existing function signature compatibility
|
||||
|
||||
- [ ] Task 2.2: Update load_track_state to use project conductor dir
|
||||
- [x] Task 2.2: Update load_track_state to use project conductor dir [3999e9c]
|
||||
- WHERE: src/project_manager.py (around line 252)
|
||||
- WHAT: Load track state from project-specific directory
|
||||
- HOW: Same as above
|
||||
|
||||
- [ ] Task 2.3: Update get_all_tracks to use project conductor dir
|
||||
- [x] Task 2.3: Update get_all_tracks to use project conductor dir [3999e9c]
|
||||
- WHERE: src/project_manager.py (around line 297)
|
||||
- WHAT: List tracks from project-specific directory
|
||||
- HOW: Accept optional project_path param
|
||||
@@ -37,7 +37,7 @@ Focus: Use project-specific paths for track operations
|
||||
## Phase 3: Update app_controller.py
|
||||
Focus: Pass project path to track operations
|
||||
|
||||
- [ ] Task 3.1: Update track creation to use project conductor dir
|
||||
- [x] Task 3.1: Update track creation to use project conductor dir [3999e9c]
|
||||
- WHERE: src/app_controller.py (around line 1907, 1937)
|
||||
- WHAT: Pass active_project_path to track path functions
|
||||
- HOW: Get active_project_path, pass to paths.get_tracks_dir()
|
||||
@@ -46,13 +46,13 @@ Focus: Pass project path to track operations
|
||||
## Phase 4: Tests
|
||||
Focus: Verify project-specific behavior
|
||||
|
||||
- [ ] Task 4.1: Write test for project-specific conductor dir
|
||||
- [x] Task 4.1: Write test for project-specific conductor dir [48e2ed8]
|
||||
- WHERE: tests/test_project_paths.py (new file)
|
||||
- WHAT: Create mock project with custom conductor dir, verify tracks saved there
|
||||
- HOW: Mock project_manager, verify path resolution
|
||||
- SAFETY: New test file
|
||||
|
||||
- [ ] Task 4.2: Test backward compatibility
|
||||
- [x] Task 4.2: Test backward compatibility [3999e9c]
|
||||
- WHERE: tests/test_project_paths.py
|
||||
- WHAT: Verify global paths still work without project_path
|
||||
- HOW: Call functions without project_path, verify defaults
|
||||
|
||||
@@ -851,6 +851,10 @@ class AppController:
|
||||
self.ui_separate_tier3 = False
|
||||
self.ui_separate_tier4 = False
|
||||
self.config = models.load_config()
|
||||
path_info = paths.get_full_path_info()
|
||||
self.ui_conductor_dir = str(path_info['conductor_dir']['path'])
|
||||
self.ui_logs_dir = str(path_info['logs_dir']['path'])
|
||||
self.ui_scripts_dir = str(path_info['scripts_dir']['path'])
|
||||
theme.load_from_config(self.config)
|
||||
ai_cfg = self.config.get("ai", {})
|
||||
self._current_provider = ai_cfg.get("provider", "gemini")
|
||||
|
||||
+65
-1
@@ -6,6 +6,7 @@ import math
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
import shutil
|
||||
import copy
|
||||
from pathlib import Path
|
||||
from tkinter import filedialog, Tk
|
||||
@@ -451,7 +452,14 @@ class App:
|
||||
exp, opened = imgui.begin("Context Hub", self.show_windows["Context Hub"])
|
||||
self.show_windows["Context Hub"] = bool(opened)
|
||||
if exp:
|
||||
self._render_projects_panel()
|
||||
if imgui.begin_tab_bar('context_hub_tabs'):
|
||||
if imgui.begin_tab_item('Projects')[0]:
|
||||
self._render_projects_panel()
|
||||
imgui.end_tab_item()
|
||||
if imgui.begin_tab_item('Paths')[0]:
|
||||
self._render_paths_panel()
|
||||
imgui.end_tab_item()
|
||||
imgui.end_tab_bar()
|
||||
imgui.end()
|
||||
if self.show_windows.get("Files & Media", False):
|
||||
exp, opened = imgui.begin("Files & Media", self.show_windows["Files & Media"])
|
||||
@@ -1462,6 +1470,62 @@ class App:
|
||||
ch, self.ui_auto_scroll_comms = imgui.checkbox("Auto-scroll Comms History", self.ui_auto_scroll_comms)
|
||||
ch, self.ui_auto_scroll_tool_calls = imgui.checkbox("Auto-scroll Tool History", self.ui_auto_scroll_tool_calls)
|
||||
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_projects_panel")
|
||||
|
||||
def _save_paths(self):
|
||||
self.config["paths"] = {
|
||||
"conductor_dir": self.ui_conductor_dir,
|
||||
"logs_dir": self.ui_logs_dir,
|
||||
"scripts_dir": self.ui_scripts_dir
|
||||
}
|
||||
cfg_path = paths.get_config_path()
|
||||
if cfg_path.exists():
|
||||
shutil.copy(cfg_path, str(cfg_path) + ".bak")
|
||||
models.save_config(self.config)
|
||||
paths.reset_resolved()
|
||||
self.ai_status = "paths saved - restart required"
|
||||
|
||||
def _render_paths_panel(self) -> None:
|
||||
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_paths_panel")
|
||||
path_info = paths.get_full_path_info()
|
||||
|
||||
imgui.text_colored(C_IN, "System Path Configuration")
|
||||
imgui.separator()
|
||||
|
||||
if self.ai_status == "paths saved - restart required":
|
||||
imgui.text_colored(vec4(255, 50, 50), "Restart required for path changes to take effect.")
|
||||
imgui.separator()
|
||||
|
||||
def render_path_field(label: str, attr: str, key: str, tooltip: str):
|
||||
info = path_info.get(key, {'source': 'unknown'})
|
||||
imgui.text(label)
|
||||
if imgui.is_item_hovered(): imgui.set_tooltip(tooltip)
|
||||
imgui.same_line()
|
||||
imgui.text_disabled(f"(Source: {info['source']})")
|
||||
|
||||
val = getattr(self, attr)
|
||||
changed, new_val = imgui.input_text(f"##{key}", val)
|
||||
if imgui.is_item_hovered(): imgui.set_tooltip(tooltip)
|
||||
if changed: setattr(self, attr, new_val)
|
||||
imgui.same_line()
|
||||
if imgui.button(f"Browse##{key}"):
|
||||
r = hide_tk_root()
|
||||
d = filedialog.askdirectory(title=f"Select {label}")
|
||||
r.destroy()
|
||||
if d: setattr(self, attr, d)
|
||||
|
||||
render_path_field("Conductor Directory", "ui_conductor_dir", "conductor_dir", "Base directory for implementation tracks and project state.")
|
||||
render_path_field("Logs Directory", "ui_logs_dir", "logs_dir", "Directory where session JSON-L logs and artifacts are stored.")
|
||||
render_path_field("Scripts Directory", "ui_scripts_dir", "scripts_dir", "Directory for AI-generated PowerShell scripts.")
|
||||
|
||||
imgui.separator()
|
||||
if imgui.button("Apply", imgui.ImVec2(120, 0)):
|
||||
self._save_paths()
|
||||
imgui.same_line()
|
||||
if imgui.button("Reset", imgui.ImVec2(120, 0)):
|
||||
self.init_state()
|
||||
self.ai_status = "paths reset to defaults"
|
||||
|
||||
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_paths_panel")
|
||||
def _render_track_proposal_modal(self) -> None:
|
||||
if self._show_track_proposal_modal:
|
||||
imgui.open_popup("Track Proposal")
|
||||
|
||||
+51
-6
@@ -45,7 +45,7 @@ See Also:
|
||||
from pathlib import Path
|
||||
import os
|
||||
import tomllib
|
||||
from typing import Optional
|
||||
from typing import Optional, Any
|
||||
|
||||
_RESOLVED: dict[str, Path] = {}
|
||||
|
||||
@@ -105,20 +105,42 @@ def _get_project_conductor_dir_from_toml(project_root: Path) -> Optional[Path]:
|
||||
if c_dir:
|
||||
p = Path(c_dir)
|
||||
if not p.is_absolute(): p = project_root / p
|
||||
return p
|
||||
return p.resolve()
|
||||
except: pass
|
||||
return None
|
||||
|
||||
def get_conductor_dir(project_path: Optional[str] = None) -> Path:
|
||||
if project_path:
|
||||
project_root = Path(project_path)
|
||||
project_root = Path(project_path).resolve()
|
||||
p = _get_project_conductor_dir_from_toml(project_root)
|
||||
if p: return p
|
||||
return project_root / 'conductor'
|
||||
|
||||
if "conductor_dir" not in _RESOLVED:
|
||||
_RESOLVED["conductor_dir"] = _resolve_path("SLOP_CONDUCTOR_DIR", "conductor_dir", "conductor")
|
||||
return _RESOLVED["conductor_dir"]
|
||||
# Check env and config
|
||||
root_dir = Path(__file__).resolve().parent.parent
|
||||
env_val = os.environ.get("SLOP_CONDUCTOR_DIR")
|
||||
if env_val:
|
||||
p = Path(env_val)
|
||||
if not p.is_absolute(): p = root_dir / p
|
||||
_RESOLVED["conductor_dir"] = p.resolve()
|
||||
else:
|
||||
try:
|
||||
with open(get_config_path(), "rb") as f:
|
||||
cfg = tomllib.load(f)
|
||||
if "paths" in cfg and "conductor_dir" in cfg["paths"]:
|
||||
p = Path(cfg["paths"]["conductor_dir"])
|
||||
if not p.is_absolute(): p = root_dir / p
|
||||
_RESOLVED["conductor_dir"] = p.resolve()
|
||||
except: pass
|
||||
|
||||
if "conductor_dir" in _RESOLVED:
|
||||
return _RESOLVED["conductor_dir"]
|
||||
|
||||
if project_path:
|
||||
return (Path(project_path).resolve() / "conductor").resolve()
|
||||
|
||||
root_dir = Path(__file__).resolve().parent.parent
|
||||
return (root_dir / "conductor").resolve()
|
||||
|
||||
def get_logs_dir() -> Path:
|
||||
if "logs_dir" not in _RESOLVED:
|
||||
@@ -139,6 +161,29 @@ def get_track_state_dir(track_id: str, project_path: Optional[str] = None) -> Pa
|
||||
def get_archive_dir(project_path: Optional[str] = None) -> Path:
|
||||
return get_conductor_dir(project_path) / "archive"
|
||||
|
||||
def _resolve_path_info(env_var: str, config_key: str, default: str) -> dict[str, Any]:
|
||||
if env_var in os.environ:
|
||||
return {'path': Path(os.environ[env_var]).resolve(), 'source': f'env:{env_var}'}
|
||||
try:
|
||||
with open(get_config_path(), 'rb') as f:
|
||||
cfg = tomllib.load(f)
|
||||
if 'paths' in cfg and config_key in cfg['paths']:
|
||||
p = Path(cfg['paths'][config_key])
|
||||
if not p.is_absolute():
|
||||
p = (Path(__file__).resolve().parent.parent / p).resolve()
|
||||
return {'path': p, 'source': 'config.toml'}
|
||||
except: pass
|
||||
root_dir = Path(__file__).resolve().parent.parent
|
||||
p = (root_dir / default).resolve()
|
||||
return {'path': p, 'source': 'default'}
|
||||
|
||||
def get_full_path_info() -> dict[str, dict[str, Any]]:
|
||||
return {
|
||||
'conductor_dir': _resolve_path_info('SLOP_CONDUCTOR_DIR', 'conductor_dir', 'conductor'),
|
||||
'logs_dir': _resolve_path_info('SLOP_LOGS_DIR', 'logs_dir', 'logs/sessions'),
|
||||
'scripts_dir': _resolve_path_info('SLOP_SCRIPTS_DIR', 'scripts_dir', 'scripts/generated')
|
||||
}
|
||||
|
||||
def reset_resolved() -> None:
|
||||
"""For testing only - clear cached resolutions."""
|
||||
_RESOLVED.clear()
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
from src import paths
|
||||
|
||||
# We mock App to avoid the heavy initialization logic
|
||||
class MockApp:
|
||||
def __init__(self):
|
||||
self.ui_conductor_dir = '/mock/conductor'
|
||||
self.ui_logs_dir = '/mock/logs'
|
||||
self.ui_scripts_dir = '/mock/scripts'
|
||||
self.config = {"paths": {}}
|
||||
self.ai_status = ""
|
||||
|
||||
from src.gui_2 import App
|
||||
_save_paths = App._save_paths
|
||||
|
||||
def test_save_paths():
|
||||
mock_app = MockApp()
|
||||
|
||||
with patch('src.models.save_config') as mock_save, \
|
||||
patch('shutil.copy') as mock_copy, \
|
||||
patch('src.paths.get_config_path') as mock_get_cfg, \
|
||||
patch('src.paths.reset_resolved') as mock_reset:
|
||||
|
||||
mock_get_cfg.return_value = MagicMock()
|
||||
mock_get_cfg.return_value.exists.return_value = True
|
||||
|
||||
mock_app.ui_conductor_dir = '/new/conductor'
|
||||
mock_app._save_paths()
|
||||
|
||||
# Verify config update
|
||||
assert mock_app.config['paths']['conductor_dir'] == '/new/conductor'
|
||||
mock_save.assert_called_once()
|
||||
mock_copy.assert_called_once()
|
||||
assert 'restart required' in mock_app.ai_status
|
||||
mock_reset.assert_called_once()
|
||||
+52
-19
@@ -1,25 +1,17 @@
|
||||
import os
|
||||
import pytest
|
||||
import tomllib
|
||||
import json
|
||||
import tomli_w
|
||||
from pathlib import Path
|
||||
from src import paths
|
||||
from src import project_manager
|
||||
|
||||
def test_get_conductor_dir_default():
|
||||
paths.reset_resolved()
|
||||
# Should return default "conductor" relative to root
|
||||
# Should return absolute path to "conductor" in project root
|
||||
expected = Path(__file__).resolve().parent.parent / "conductor"
|
||||
assert paths.get_conductor_dir() == expected
|
||||
|
||||
def test_get_conductor_dir_project_specific_no_toml(tmp_path):
|
||||
paths.reset_resolved()
|
||||
project_root = tmp_path / "my_project"
|
||||
project_root.mkdir()
|
||||
|
||||
# Should default to project_root / "conductor" if no manual_slop.toml
|
||||
res = paths.get_conductor_dir(project_path=str(project_root))
|
||||
assert res == project_root / "conductor"
|
||||
|
||||
def test_get_conductor_dir_project_specific_with_toml(tmp_path):
|
||||
paths.reset_resolved()
|
||||
project_root = tmp_path / "my_project"
|
||||
@@ -38,18 +30,59 @@ def test_get_conductor_dir_project_specific_with_toml(tmp_path):
|
||||
res = paths.get_conductor_dir(project_path=str(project_root))
|
||||
assert res == project_root / "custom_tracks"
|
||||
|
||||
def test_get_tracks_dir_project_specific(tmp_path):
|
||||
def test_get_all_tracks_project_specific(tmp_path):
|
||||
paths.reset_resolved()
|
||||
project_root = tmp_path / "my_project"
|
||||
project_root.mkdir()
|
||||
|
||||
res = paths.get_tracks_dir(project_path=str(project_root))
|
||||
assert res == project_root / "conductor" / "tracks"
|
||||
# Custom conductor dir
|
||||
custom_dir = project_root / "my_conductor"
|
||||
custom_dir.mkdir()
|
||||
tracks_dir = custom_dir / "tracks"
|
||||
tracks_dir.mkdir()
|
||||
|
||||
# Create a dummy track
|
||||
track_dir = tracks_dir / "test_track_20260312"
|
||||
track_dir.mkdir()
|
||||
with open(track_dir / "metadata.json", "w") as f:
|
||||
json.dump({"id": "test_track", "title": "Test Track"}, f)
|
||||
|
||||
# Setup manual_slop.toml
|
||||
toml_path = project_root / "manual_slop.toml"
|
||||
config = {"conductor": {"dir": "my_conductor"}}
|
||||
with open(toml_path, "wb") as f:
|
||||
f.write(tomli_w.dumps(config).encode())
|
||||
|
||||
# project_manager.get_all_tracks(base_dir) should now find it
|
||||
tracks = project_manager.get_all_tracks(str(project_root))
|
||||
assert len(tracks) == 1
|
||||
assert tracks[0]["title"] == "Test Track"
|
||||
|
||||
def test_get_track_state_dir_project_specific(tmp_path):
|
||||
def test_get_all_tracks_global_fallback(tmp_path):
|
||||
paths.reset_resolved()
|
||||
project_root = tmp_path / "my_project"
|
||||
project_root.mkdir()
|
||||
|
||||
res = paths.get_track_state_dir("track-123", project_path=str(project_root))
|
||||
assert res == project_root / "conductor" / "tracks" / "track-123"
|
||||
# Create a directory without manual_slop.toml
|
||||
empty_dir = tmp_path / "empty_project"
|
||||
empty_dir.mkdir()
|
||||
|
||||
# Setup a fake global conductor
|
||||
global_conductor = tmp_path / "global_conductor"
|
||||
global_conductor.mkdir()
|
||||
global_tracks = global_conductor / "tracks"
|
||||
global_tracks.mkdir()
|
||||
|
||||
track_dir = global_tracks / "global_track"
|
||||
track_dir.mkdir()
|
||||
with open(track_dir / "metadata.json", "w") as f:
|
||||
json.dump({"id": "global_track", "title": "Global Track"}, f)
|
||||
|
||||
# Override global conductor dir via env var
|
||||
os.environ["SLOP_CONDUCTOR_DIR"] = str(global_conductor)
|
||||
try:
|
||||
paths.reset_resolved()
|
||||
# Pass project_path pointing to a dir without TOML
|
||||
tracks = project_manager.get_all_tracks(str(empty_dir))
|
||||
# paths.get_conductor_dir(str(empty_dir)) should fall back to global
|
||||
assert any(t["id"] == "global_track" for t in tracks)
|
||||
finally:
|
||||
del os.environ["SLOP_CONDUCTOR_DIR"]
|
||||
|
||||
Reference in New Issue
Block a user