3 Commits

11 changed files with 263 additions and 81 deletions
+1 -1
View File
@@ -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. - **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. - **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. - **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. - **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. - **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/`. - **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/`.
+1 -1
View File
@@ -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. - **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. - **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
View File
@@ -105,11 +105,11 @@ This file tracks all major tracks for the project. Each track has its own detail
### Path Configuration ### 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/)* *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.* *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/)* *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.* *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 # 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] Task: Define the schema for external MCP server configuration. [1c863f0]
- [x] Update `src/models.py` to include `MCPServerConfig` and `MCPConfiguration` classes. - [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] Implement logic to load `mcp_config.json` from global and project-specific paths.
- [x] Task: Integrate configuration loading into `AppController`. [c09e0f5] - [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] 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] - [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 ## Phase 2: MCP Client Extension [checkpoint: 828fadf]
- [ ] Task: Implement `ExternalMCPManager` in `src/mcp_client.py`. - [x] Task: Implement `ExternalMCPManager` in `src/mcp_client.py`. [828fadf]
- [ ] Add support for managing multiple MCP server sessions. - [x] Add support for managing multiple MCP server sessions.
- [ ] Implement the `StdioMCPClient` for local subprocess communication. - [x] Implement the `StdioMCPClient` for local subprocess communication.
- [ ] Implement the `RemoteMCPClient` for SSE/WebSocket communication. - [x] Implement the `RemoteMCPClient` for SSE/WebSocket communication (stub).
- [ ] Task: Update Tool Discovery. - [x] Task: Update Tool Discovery. [828fadf]
- [ ] Implement `list_external_tools()` to aggregate tools from all active external servers. - [x] Implement `list_external_tools()` to aggregate tools from all active external servers.
- [ ] Task: Update Tool Dispatch. - [x] Task: Update Tool Dispatch. [828fadf]
- [ ] Modify `mcp_client.dispatch()` and `mcp_client.async_dispatch()` to route tool calls to either native tools or the appropriate external server. - [x] 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). - [x] Task: Write integration tests for stdio and remote MCP client communication (using mock servers). [828fadf]
- [ ] Task: Conductor - User Manual Verification 'Phase 2: MCP Client Extension' (Protocol in workflow.md) - [x] Task: Conductor - User Manual Verification 'Phase 2: MCP Client Extension' [828fadf]
## Phase 3: GUI Integration & Lifecycle ## Phase 3: GUI Integration & Lifecycle [checkpoint: 3b2588a]
- [ ] Task: Update the **Operations** panel in `src/gui_2.py`. - [x] Task: Update the **Operations** panel in `src/gui_2.py`. [3b2588a]
- [ ] Create a new "External Tools" section. - [x] Create a new "External Tools" section.
- [ ] List discovered tools from active external servers. - [x] List discovered tools from active external servers.
- [ ] Add a "Refresh External MCPs" button to reload configuration and rediscover tools. - [x] Add a "Refresh External MCPs" button to reload configuration and rediscover tools.
- [ ] Task: Implement Lifecycle Management. - [x] Task: Implement Lifecycle Management. [3b2588a]
- [ ] Add the "Auto-start on Project Load" logic to start servers when a project is initialized. - [x] 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. - [x] 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. - [x] Task: Write visual regression tests or simulation scripts to verify the updated Operations panel. [3b2588a]
- [ ] Task: Conductor - User Manual Verification 'Phase 3: GUI Integration & Lifecycle' (Protocol in workflow.md) - [x] Task: Conductor - User Manual Verification 'Phase 3: GUI Integration & Lifecycle' [3b2588a]
## Phase 4: Agent Integration & HITL ## Phase 4: Agent Integration & HITL [checkpoint: f4c5a0b]
- [ ] Task: Update AI tool declarations. - [x] Task: Update AI tool declarations. [f4c5a0b]
- [ ] Ensure `ai_client.py` includes external tools in the tool definitions sent to Gemini/Anthropic. - [x] Ensure `ai_client.py` includes external tools in the tool definitions sent to Gemini/Anthropic.
- [ ] Task: Verify HITL Approval Flow. - [x] Task: Verify HITL Approval Flow. [f4c5a0b]
- [ ] Ensure that calling an external tool correctly triggers the `ConfirmDialog` modal. - [x] Ensure that calling an external tool correctly triggers the `ConfirmDialog` modal.
- [ ] Verify that approved external tool results are correctly returned to the AI. - [x] 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. - [x] Task: Perform a final end-to-end verification with a real external MCP server. [f4c5a0b]
- [ ] Task: Conductor - User Manual Verification 'Phase 4: Agent Integration & HITL' (Protocol in workflow.md) - [x] Task: Conductor - User Manual Verification 'Phase 4: Agent Integration & HITL' [f4c5a0b]
@@ -3,13 +3,13 @@
## Phase 1: Path Info Display ## Phase 1: Path Info Display
Focus: Show current path resolution in GUI 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 - WHERE: src/paths.py
- WHAT: Add functions to get path resolution source (default/env/config) - WHAT: Add functions to get path resolution source (default/env/config)
- HOW: Return tuple of (resolved_path, source) - HOW: Return tuple of (resolved_path, source)
- SAFETY: New functions, no modifications - 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 - WHERE: src/paths.py
- WHAT: Function to get all paths with resolution info - WHAT: Function to get all paths with resolution info
- HOW: Returns dict of path_name -> (resolved, source) - HOW: Returns dict of path_name -> (resolved, source)
@@ -18,25 +18,25 @@ Focus: Show current path resolution in GUI
## Phase 2: Context Hub Panel ## Phase 2: Context Hub Panel
Focus: Add Path Configuration panel to GUI 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) - WHERE: src/gui_2.py (Context Hub section)
- WHAT: New tab/section for path configuration - WHAT: New tab/section for path configuration
- HOW: Add ImGui tab item, follow existing panel patterns - HOW: Add ImGui tab item, follow existing panel patterns
- SAFETY: New panel, no modifications to existing - 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) - WHERE: src/gui_2.py (new paths panel)
- WHAT: Show resolved paths and their sources - WHAT: Show resolved paths and their sources
- HOW: Call paths.py functions, display in read-only text - HOW: Call paths.py functions, display in read-only text
- SAFETY: New code - 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) - WHERE: src/gui_2.py (paths panel)
- WHAT: Editable text inputs for each path - WHAT: Editable text inputs for each path
- HOW: ImGui input_text for conductor_dir, logs_dir, scripts_dir - HOW: ImGui input_text for conductor_dir, logs_dir, scripts_dir
- SAFETY: New code - SAFETY: New code
- [ ] Task 2.4: Add browse buttons - [x] Task 2.4: Add browse buttons [d237d3b]
- WHERE: src/gui_2.py (paths panel) - WHERE: src/gui_2.py (paths panel)
- WHAT: File dialog buttons to browse for directories - WHAT: File dialog buttons to browse for directories
- HOW: Use existing file dialog patterns in gui_2.py - HOW: Use existing file dialog patterns in gui_2.py
@@ -45,19 +45,19 @@ Focus: Add Path Configuration panel to GUI
## Phase 3: Persistence ## Phase 3: Persistence
Focus: Save path changes to config.toml 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 - WHERE: src/gui_2.py or new utility
- WHAT: Write [paths] section to config.toml - WHAT: Write [paths] section to config.toml
- HOW: Read existing config, update paths section, write back - HOW: Read existing config, update paths section, write back
- SAFETY: Backup before write, handle errors - 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) - WHERE: src/gui_2.py (paths panel)
- WHAT: Button to save changes - WHAT: Button to save changes
- HOW: Call config write function, show success/error message - HOW: Call config write function, show success/error message
- SAFETY: Confirmation dialog - SAFETY: Confirmation dialog
- [ ] Task 3.3: Add Reset button - [x] Task 3.3: Add Reset button [d237d3b]
- WHERE: src/gui_2.py (paths panel) - WHERE: src/gui_2.py (paths panel)
- WHAT: Reset paths to defaults - WHAT: Reset paths to defaults
- HOW: Clear custom values, show confirmation - HOW: Clear custom values, show confirmation
@@ -66,13 +66,13 @@ Focus: Save path changes to config.toml
## Phase 4: UX Polish ## Phase 4: UX Polish
Focus: Improve user experience 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) - WHERE: src/gui_2.py (paths panel)
- WHAT: Show warning that changes require restart - WHAT: Show warning that changes require restart
- HOW: Text label after Apply - HOW: Text label after Apply
- SAFETY: New code - SAFETY: New code
- [ ] Task 4.2: Add tooltips - [x] Task 4.2: Add tooltips [d237d3b]
- WHERE: src/gui_2.py (paths panel) - WHERE: src/gui_2.py (paths panel)
- WHAT: Explain each path and resolution order - WHAT: Explain each path and resolution order
- HOW: ImGui set_tooltip on hover - HOW: ImGui set_tooltip on hover
@@ -81,7 +81,7 @@ Focus: Improve user experience
## Phase 5: Tests ## Phase 5: Tests
Focus: Verify GUI path configuration 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) - WHERE: tests/test_gui_paths.py (new file)
- WHAT: Verify paths panel shows correct values - WHAT: Verify paths panel shows correct values
- HOW: Mock paths.py, verify display - HOW: Mock paths.py, verify display
@@ -3,13 +3,13 @@
## Phase 1: Extend paths.py ## Phase 1: Extend paths.py
Focus: Add project-specific path resolution 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 - WHERE: src/paths.py
- WHAT: Add optional project_path parameter to get_conductor_dir, get_tracks_dir, get_track_state_dir - 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 - HOW: If project_path provided, resolve relative to project root; otherwise use global
- SAFETY: Maintain backward compatibility with no-arg calls - 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 - WHERE: src/paths.py
- WHAT: New function `_resolve_project_conductor_dir(project_path)` that reads from project TOML - WHAT: New function `_resolve_project_conductor_dir(project_path)` that reads from project TOML
- HOW: Load project TOML, check `[conductor].dir` key - HOW: Load project TOML, check `[conductor].dir` key
@@ -18,18 +18,18 @@ Focus: Add project-specific path resolution
## Phase 2: Update project_manager.py ## Phase 2: Update project_manager.py
Focus: Use project-specific paths for track operations 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) - WHERE: src/project_manager.py (around line 240)
- WHAT: Pass project base_dir to paths.get_track_state_dir() - WHAT: Pass project base_dir to paths.get_track_state_dir()
- HOW: Get base_dir from project_path, call paths with project_path param - HOW: Get base_dir from project_path, call paths with project_path param
- SAFETY: Maintain existing function signature compatibility - 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) - WHERE: src/project_manager.py (around line 252)
- WHAT: Load track state from project-specific directory - WHAT: Load track state from project-specific directory
- HOW: Same as above - 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) - WHERE: src/project_manager.py (around line 297)
- WHAT: List tracks from project-specific directory - WHAT: List tracks from project-specific directory
- HOW: Accept optional project_path param - HOW: Accept optional project_path param
@@ -37,7 +37,7 @@ Focus: Use project-specific paths for track operations
## Phase 3: Update app_controller.py ## Phase 3: Update app_controller.py
Focus: Pass project path to track operations 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) - WHERE: src/app_controller.py (around line 1907, 1937)
- WHAT: Pass active_project_path to track path functions - WHAT: Pass active_project_path to track path functions
- HOW: Get active_project_path, pass to paths.get_tracks_dir() - 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 ## Phase 4: Tests
Focus: Verify project-specific behavior 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) - WHERE: tests/test_project_paths.py (new file)
- WHAT: Create mock project with custom conductor dir, verify tracks saved there - WHAT: Create mock project with custom conductor dir, verify tracks saved there
- HOW: Mock project_manager, verify path resolution - HOW: Mock project_manager, verify path resolution
- SAFETY: New test file - SAFETY: New test file
- [ ] Task 4.2: Test backward compatibility - [x] Task 4.2: Test backward compatibility [3999e9c]
- WHERE: tests/test_project_paths.py - WHERE: tests/test_project_paths.py
- WHAT: Verify global paths still work without project_path - WHAT: Verify global paths still work without project_path
- HOW: Call functions without project_path, verify defaults - HOW: Call functions without project_path, verify defaults
+4
View File
@@ -851,6 +851,10 @@ class AppController:
self.ui_separate_tier3 = False self.ui_separate_tier3 = False
self.ui_separate_tier4 = False self.ui_separate_tier4 = False
self.config = models.load_config() 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) theme.load_from_config(self.config)
ai_cfg = self.config.get("ai", {}) ai_cfg = self.config.get("ai", {})
self._current_provider = ai_cfg.get("provider", "gemini") self._current_provider = ai_cfg.get("provider", "gemini")
+65 -1
View File
@@ -6,6 +6,7 @@ import math
import json import json
import sys import sys
import os import os
import shutil
import copy import copy
from pathlib import Path from pathlib import Path
from tkinter import filedialog, Tk from tkinter import filedialog, Tk
@@ -451,7 +452,14 @@ class App:
exp, opened = imgui.begin("Context Hub", self.show_windows["Context Hub"]) exp, opened = imgui.begin("Context Hub", self.show_windows["Context Hub"])
self.show_windows["Context Hub"] = bool(opened) self.show_windows["Context Hub"] = bool(opened)
if exp: 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() imgui.end()
if self.show_windows.get("Files & Media", False): if self.show_windows.get("Files & Media", False):
exp, opened = imgui.begin("Files & Media", self.show_windows["Files & Media"]) 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_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) 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") 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: 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")
+51 -6
View File
@@ -45,7 +45,7 @@ See Also:
from pathlib import Path from pathlib import Path
import os import os
import tomllib import tomllib
from typing import Optional from typing import Optional, Any
_RESOLVED: dict[str, Path] = {} _RESOLVED: dict[str, Path] = {}
@@ -105,20 +105,42 @@ def _get_project_conductor_dir_from_toml(project_root: Path) -> Optional[Path]:
if c_dir: if c_dir:
p = Path(c_dir) p = Path(c_dir)
if not p.is_absolute(): p = project_root / p if not p.is_absolute(): p = project_root / p
return p return p.resolve()
except: pass except: pass
return None return None
def get_conductor_dir(project_path: Optional[str] = None) -> Path: def get_conductor_dir(project_path: Optional[str] = None) -> Path:
if project_path: if project_path:
project_root = Path(project_path) project_root = Path(project_path).resolve()
p = _get_project_conductor_dir_from_toml(project_root) p = _get_project_conductor_dir_from_toml(project_root)
if p: return p if p: return p
return project_root / 'conductor'
if "conductor_dir" not in _RESOLVED: if "conductor_dir" not in _RESOLVED:
_RESOLVED["conductor_dir"] = _resolve_path("SLOP_CONDUCTOR_DIR", "conductor_dir", "conductor") # Check env and config
return _RESOLVED["conductor_dir"] 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: def get_logs_dir() -> Path:
if "logs_dir" not in _RESOLVED: 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: def get_archive_dir(project_path: Optional[str] = None) -> Path:
return get_conductor_dir(project_path) / "archive" 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: def reset_resolved() -> None:
"""For testing only - clear cached resolutions.""" """For testing only - clear cached resolutions."""
_RESOLVED.clear() _RESOLVED.clear()
+36
View File
@@ -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
View File
@@ -1,25 +1,17 @@
import os import os
import pytest import pytest
import tomllib import json
import tomli_w import tomli_w
from pathlib import Path from pathlib import Path
from src import paths from src import paths
from src import project_manager
def test_get_conductor_dir_default(): def test_get_conductor_dir_default():
paths.reset_resolved() 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" expected = Path(__file__).resolve().parent.parent / "conductor"
assert paths.get_conductor_dir() == expected 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): def test_get_conductor_dir_project_specific_with_toml(tmp_path):
paths.reset_resolved() paths.reset_resolved()
project_root = tmp_path / "my_project" 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)) res = paths.get_conductor_dir(project_path=str(project_root))
assert res == project_root / "custom_tracks" 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() paths.reset_resolved()
project_root = tmp_path / "my_project" project_root = tmp_path / "my_project"
project_root.mkdir() project_root.mkdir()
res = paths.get_tracks_dir(project_path=str(project_root)) # Custom conductor dir
assert res == project_root / "conductor" / "tracks" 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() 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)) # Create a directory without manual_slop.toml
assert res == project_root / "conductor" / "tracks" / "track-123" 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"]