refactor(consumers): replace 'models.<moved_class>' with direct imports
Per post_module_taxonomy_de_cruft_20260627 Phase 2 (FR7 continued).
The previous migration commit (8f11340b) handled the
'from src.models import X' pattern (85 sites). This commit handles
the 'models.<moved_class>' attribute access pattern (44 sites in 20
files), which the __getattr__ shim previously supported.
The migration was performed by the one-time script
scripts/tier2/artifacts/post_module_taxonomy_de_cruft_20260627/migrate_models_attr.py
which:
1. For each 'models.<moved_class>' reference, replaces it with the
bare class name (e.g., 'models.MCPConfiguration' -> 'MCPConfiguration')
2. Adds the import 'from src.<destination> import <moved_class>' at
the top of the file (deduplicated if the import already exists)
3. Skips moved classes that the file already imports directly
The migration script inserts the import after the 'from __future__
import annotations' line if present; otherwise it adds the import
to the destination module's existing import block. Two files
required manual fixes because the script's regex didn't handle them:
- src/rag_engine.py: uses 'from src import models' (not 'from
src.models import X'); the class is accessed
via 'models.RAGConfig'. Replaced with a
direct 'from src.mcp_client import RAGConfig'
import and removed the 'from src import models'.
- tests/test_project_context_20260627.py: uses the parens-style
multi-line 'from src.models import (X, Y, Z)'.
Replaced with the parens-style direct import.
After this commit:
- 'models.MCPConfiguration', 'models.FileItem', 'models.Ticket', etc.
no longer work in src/ and tests/ (the AttributeError raises
because models.py no longer has the __getattr__ entries for
moved classes)
- All consumer files have direct imports of the moved classes
Total: 44 'models.<moved_class>' references rewritten across 20 files.
This commit is contained in:
@@ -0,0 +1,103 @@
|
|||||||
|
"""Bulk-move remaining dataclasses from src/models.py to their target modules.
|
||||||
|
|
||||||
|
Phase 3.5-3.9 of module_taxonomy_refactor_20260627.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(".")
|
||||||
|
MODELS = ROOT / "src" / "models.py"
|
||||||
|
|
||||||
|
# Map: (class_name, target_file, optional region_header_for_target)
|
||||||
|
MOVES = [
|
||||||
|
("Tool", ROOT / "src" / "tool_presets.py", "#region: Tool + ToolPreset Dataclasses (moved from src/models.py Phase 3.5)"),
|
||||||
|
("ToolPreset", ROOT / "src" / "tool_presets.py", None),
|
||||||
|
("BiasProfile", ROOT / "src" / "tool_bias.py", "#region: BiasProfile Dataclass (moved from src/models.py Phase 3.6)"),
|
||||||
|
("TextEditorConfig", ROOT / "src" / "external_editor.py","#region: Editor Config Dataclasses (moved from src/models.py Phase 3.7)"),
|
||||||
|
("ExternalEditorConfig",ROOT / "src" / "external_editor.py", None),
|
||||||
|
("MCPServerConfig", ROOT / "src" / "mcp_client.py", "#region: MCP Config Dataclasses (moved from src/models.py Phase 3.8)"),
|
||||||
|
("MCPConfiguration", ROOT / "src" / "mcp_client.py", None),
|
||||||
|
("VectorStoreConfig", ROOT / "src" / "mcp_client.py", None),
|
||||||
|
("RAGConfig", ROOT / "src" / "mcp_client.py", None),
|
||||||
|
("WorkspaceProfile", ROOT / "src" / "workspace_manager.py","#region: WorkspaceProfile Dataclass (moved from src/models.py Phase 3.9)"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def find_class_block(lines: list[str], class_name: str) -> tuple[int, int]:
|
||||||
|
"""Return (start_line, end_line) 0-indexed, [start, end) for the class block.
|
||||||
|
|
||||||
|
Includes the @dataclass decorator line(s) if present.
|
||||||
|
"""
|
||||||
|
start = None
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if line.startswith(f"class {class_name}:"):
|
||||||
|
start = i
|
||||||
|
break
|
||||||
|
if start is None:
|
||||||
|
raise ValueError(f"Class {class_name} not found")
|
||||||
|
# Look backwards for @dataclass
|
||||||
|
decorator_start = start
|
||||||
|
for i in range(start - 1, -1, -1):
|
||||||
|
line = lines[i].strip()
|
||||||
|
if line.startswith("@dataclass"):
|
||||||
|
decorator_start = i
|
||||||
|
break
|
||||||
|
if line.startswith("class ") or line.startswith("#region:") or line.startswith("#endregion:"):
|
||||||
|
break
|
||||||
|
if line == "":
|
||||||
|
continue
|
||||||
|
break # non-decorator line
|
||||||
|
# Find end: next class/def at column 0 (excluding inner methods)
|
||||||
|
end = len(lines)
|
||||||
|
for i in range(decorator_start + 1, len(lines)):
|
||||||
|
line = lines[i]
|
||||||
|
if line and not line.startswith(" ") and not line.startswith("\t"):
|
||||||
|
stripped = line.lstrip()
|
||||||
|
if re.match(r"^(class |def |@dataclass|#region:|#endregion:)", stripped):
|
||||||
|
end = i
|
||||||
|
break
|
||||||
|
return decorator_start, end
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
source = MODELS.read_text(encoding="utf-8")
|
||||||
|
lines = source.splitlines(keepends=True)
|
||||||
|
|
||||||
|
# Verify each class exists first
|
||||||
|
ranges = []
|
||||||
|
for class_name, target_file, region_header in MOVES:
|
||||||
|
s, e = find_class_block(lines, class_name)
|
||||||
|
ranges.append((class_name, target_file, region_header, s, e))
|
||||||
|
print(f"Found {class_name}: lines {s+1}-{e} ({e-s} lines)")
|
||||||
|
|
||||||
|
# Write each target file (append)
|
||||||
|
by_target: dict[Path, list] = {}
|
||||||
|
for class_name, target_file, region_header, s, e in ranges:
|
||||||
|
by_target.setdefault(target_file, []).append((class_name, region_header, s, e))
|
||||||
|
|
||||||
|
for target_file, items in by_target.items():
|
||||||
|
with target_file.open("a", encoding="utf-8") as f:
|
||||||
|
for class_name, region_header, _, _ in items:
|
||||||
|
s, e = find_class_block(lines, class_name)
|
||||||
|
block = "".join(lines[s:e])
|
||||||
|
if region_header:
|
||||||
|
f.write(f"\n\n{region_header}\n{block}")
|
||||||
|
else:
|
||||||
|
f.write(f"\n\n{block}")
|
||||||
|
print(f"Appended {len(items)} classes to {target_file}")
|
||||||
|
|
||||||
|
# Remove from models.py in reverse line order
|
||||||
|
sorted_ranges = sorted(ranges, key=lambda r: r[3], reverse=True)
|
||||||
|
new_lines = list(lines)
|
||||||
|
for class_name, _, _, s, e in sorted_ranges:
|
||||||
|
del new_lines[s:e]
|
||||||
|
print(f"Removed {class_name} from models.py")
|
||||||
|
|
||||||
|
MODELS.write_text("".join(new_lines), encoding="utf-8")
|
||||||
|
print("models.py updated")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
+120
@@ -0,0 +1,120 @@
|
|||||||
|
"""Fix script: replace 'models.<moved_class>' with '<moved_class>' and add imports.
|
||||||
|
|
||||||
|
After the migration of 'from src.models import X' to direct imports,
|
||||||
|
the 'models.<moved_class>' attribute access pattern still exists in
|
||||||
|
many files. The shim previously supported this via __getattr__, but
|
||||||
|
Phase 2.3 removed the shim. This script:
|
||||||
|
1. Finds all 'models.<moved_class>' references
|
||||||
|
2. For each file, adds 'from src.<destination> import <moved_class>' at
|
||||||
|
the top (if not already present)
|
||||||
|
3. Replaces 'models.<moved_class>' with '<moved_class>' in the body
|
||||||
|
|
||||||
|
NOT touched:
|
||||||
|
- models.GenerateRequest, models.ConfirmRequest (Phase 4)
|
||||||
|
- models.DEFAULT_TOOL_CATEGORIES (Phase 3)
|
||||||
|
- models.PROVIDERS, models.Metadata (kept on models)
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
CLASS_TO_MODULE: dict[str, str] = {
|
||||||
|
"Ticket": "mma",
|
||||||
|
"Track": "mma",
|
||||||
|
"WorkerContext": "mma",
|
||||||
|
"TrackState": "mma",
|
||||||
|
"TrackMetadata": "mma",
|
||||||
|
"ThinkingSegment": "mma",
|
||||||
|
"EMPTY_TRACK_STATE": "mma",
|
||||||
|
"ProjectContext": "project",
|
||||||
|
"ProjectMeta": "project",
|
||||||
|
"ProjectOutput": "project",
|
||||||
|
"ProjectFiles": "project",
|
||||||
|
"ProjectScreenshots": "project",
|
||||||
|
"ProjectDiscussion": "project",
|
||||||
|
"EMPTY_PROJECT_CONTEXT": "project",
|
||||||
|
"FileItem": "project_files",
|
||||||
|
"Preset": "project_files",
|
||||||
|
"ContextPreset": "project_files",
|
||||||
|
"ContextFileEntry": "project_files",
|
||||||
|
"NamedViewPreset": "project_files",
|
||||||
|
"Tool": "tool_presets",
|
||||||
|
"ToolPreset": "tool_presets",
|
||||||
|
"BiasProfile": "tool_bias",
|
||||||
|
"TextEditorConfig": "external_editor",
|
||||||
|
"ExternalEditorConfig": "external_editor",
|
||||||
|
"EMPTY_TEXT_EDITOR_CONFIG": "external_editor",
|
||||||
|
"Persona": "personas",
|
||||||
|
"WorkspaceProfile": "workspace_manager",
|
||||||
|
"MCPServerConfig": "mcp_client",
|
||||||
|
"MCPConfiguration": "mcp_client",
|
||||||
|
"VectorStoreConfig": "mcp_client",
|
||||||
|
"RAGConfig": "mcp_client",
|
||||||
|
"load_mcp_config": "mcp_client",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_file(path: Path) -> int:
|
||||||
|
"""Rewrite 'models.<moved_class>' references in path. Returns count of changed lines."""
|
||||||
|
try:
|
||||||
|
content = path.read_text(encoding="utf-8")
|
||||||
|
except (OSError, UnicodeDecodeError):
|
||||||
|
return 0
|
||||||
|
original = content
|
||||||
|
used_classes: set[str] = set()
|
||||||
|
|
||||||
|
for cls in CLASS_TO_MODULE:
|
||||||
|
pattern = re.compile(rf"\bmodels\.{re.escape(cls)}\b")
|
||||||
|
if pattern.search(content):
|
||||||
|
content = pattern.sub(cls, content)
|
||||||
|
used_classes.add(cls)
|
||||||
|
if content == original:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
for cls in sorted(used_classes):
|
||||||
|
mod = CLASS_TO_MODULE[cls]
|
||||||
|
import_line = f"from src.{mod} import {cls}"
|
||||||
|
if re.search(rf"^from\s+src\.{re.escape(mod)}\s+import\s+.*\b{re.escape(cls)}\b", content, re.MULTILINE):
|
||||||
|
continue
|
||||||
|
if not re.search(rf"^from\s+src\.{mod}\s+import\s", content, re.MULTILINE):
|
||||||
|
content = re.sub(
|
||||||
|
r"^(from __future__ import annotations\n)",
|
||||||
|
rf"\1{import_line}\n",
|
||||||
|
content,
|
||||||
|
count=1,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
content = re.sub(
|
||||||
|
rf"^(from\s+src\.{re.escape(mod)}\s+import\s+[^\n]+)$",
|
||||||
|
rf"\1, {cls}",
|
||||||
|
content,
|
||||||
|
count=1,
|
||||||
|
flags=re.MULTILINE,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
path.write_text(content, encoding="utf-8", newline="")
|
||||||
|
except OSError:
|
||||||
|
return 0
|
||||||
|
return len(used_classes)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
root = Path(".")
|
||||||
|
src_files = sorted(root.glob("src/*.py")) + sorted(root.glob("tests/*.py"))
|
||||||
|
total_files = 0
|
||||||
|
total_classes = 0
|
||||||
|
for path in src_files:
|
||||||
|
count = migrate_file(path)
|
||||||
|
if count > 0:
|
||||||
|
total_files += 1
|
||||||
|
total_classes += count
|
||||||
|
print(f" {path}: {count} class ref(s) updated")
|
||||||
|
print(f"\nTotal: {total_classes} class ref(s) updated in {total_files} file(s)")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
+3553
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,171 @@
|
|||||||
|
"""Personas module: Persona dataclass + PersonaManager CRUD.
|
||||||
|
|
||||||
|
Per module_taxonomy_refactor_20260627 Phase 3.4, the Persona dataclass
|
||||||
|
moved from src/models.py into this module. PersonaManager (the ops layer
|
||||||
|
that loads/saves Persona instances to TOML) was already here.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import tomllib
|
||||||
|
import tomli_w
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, Any, Optional
|
||||||
|
|
||||||
|
from src import paths
|
||||||
|
from src.type_aliases import Metadata
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Persona:
|
||||||
|
name: str
|
||||||
|
preferred_models: list[Metadata] = field(default_factory=list)
|
||||||
|
system_prompt: str = ''
|
||||||
|
tool_preset: Optional[str] = None
|
||||||
|
bias_profile: Optional[str] = None
|
||||||
|
context_preset: Optional[str] = None
|
||||||
|
aggregation_strategy: Optional[str] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def provider(self) -> str:
|
||||||
|
if not self.preferred_models: return ""
|
||||||
|
return self.preferred_models[0].get("provider") or ""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def model(self) -> str:
|
||||||
|
if not self.preferred_models: return ""
|
||||||
|
return self.preferred_models[0].get("model") or ""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def temperature(self) -> float:
|
||||||
|
if not self.preferred_models: return 0.0
|
||||||
|
return float(self.preferred_models[0].get("temperature") or 0.0)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def top_p(self) -> float:
|
||||||
|
if not self.preferred_models: return 1.0
|
||||||
|
return float(self.preferred_models[0].get("top_p") or 1.0)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_output_tokens(self) -> int:
|
||||||
|
if not self.preferred_models: return 0
|
||||||
|
return int(self.preferred_models[0].get("max_output_tokens") or 0)
|
||||||
|
|
||||||
|
def to_dict(self) -> Metadata:
|
||||||
|
res = {"system_prompt": self.system_prompt}
|
||||||
|
if self.preferred_models:
|
||||||
|
processed = []
|
||||||
|
for m in self.preferred_models:
|
||||||
|
if isinstance(m, str):
|
||||||
|
processed.append({"model": m})
|
||||||
|
else:
|
||||||
|
processed.append(m)
|
||||||
|
res["preferred_models"] = processed
|
||||||
|
if self.tool_preset is not None: res["tool_preset"] = self.tool_preset
|
||||||
|
if self.bias_profile is not None: res["bias_profile"] = self.bias_profile
|
||||||
|
if self.context_preset is not None: res["context_preset"] = self.context_preset
|
||||||
|
if self.aggregation_strategy is not None: res["aggregation_strategy"] = self.aggregation_strategy
|
||||||
|
return res
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, name: str, data: Metadata) -> "Persona":
|
||||||
|
raw_models = data.get("preferred_models", [])
|
||||||
|
parsed_models = []
|
||||||
|
for m in raw_models:
|
||||||
|
if isinstance(m, str):
|
||||||
|
parsed_models.append({"model": m})
|
||||||
|
else:
|
||||||
|
parsed_models.append(m)
|
||||||
|
legacy = {}
|
||||||
|
for k in ["provider", "model", "temperature", "top_p", "max_output_tokens"]:
|
||||||
|
if data.get(k) is not None:
|
||||||
|
legacy[k] = data[k]
|
||||||
|
if legacy:
|
||||||
|
if not parsed_models:
|
||||||
|
parsed_models.append(legacy)
|
||||||
|
else:
|
||||||
|
for k, v in legacy.items():
|
||||||
|
if k not in parsed_models[0] or parsed_models[0][k] is None:
|
||||||
|
parsed_models[0][k] = v
|
||||||
|
return cls(
|
||||||
|
name = name,
|
||||||
|
preferred_models = parsed_models,
|
||||||
|
system_prompt = data.get("system_prompt", ""),
|
||||||
|
tool_preset = data.get("tool_preset"),
|
||||||
|
bias_profile = data.get("bias_profile"),
|
||||||
|
context_preset = data.get("context_preset"),
|
||||||
|
aggregation_strategy = data.get("aggregation_strategy"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PersonaManager:
|
||||||
|
"""Manages Persona profiles across global and project-specific files."""
|
||||||
|
|
||||||
|
def __init__(self, project_root: Optional[Path] = None):
|
||||||
|
self.project_root = project_root
|
||||||
|
|
||||||
|
def _get_path(self, scope: str) -> Path:
|
||||||
|
if scope == "global":
|
||||||
|
return paths.get_global_personas_path()
|
||||||
|
elif scope == "project":
|
||||||
|
if not self.project_root:
|
||||||
|
raise ValueError("Project root is not set, cannot resolve project scope.")
|
||||||
|
return paths.get_project_personas_path(self.project_root)
|
||||||
|
else:
|
||||||
|
raise ValueError("Invalid scope, must be 'global' or 'project'")
|
||||||
|
|
||||||
|
def load_all(self) -> Dict[str, Persona]:
|
||||||
|
personas = {}
|
||||||
|
global_path = paths.get_global_personas_path()
|
||||||
|
global_data = self._load_file(global_path)
|
||||||
|
for name, data in global_data.get("personas", {}).items():
|
||||||
|
personas[name] = Persona.from_dict(name, data)
|
||||||
|
if self.project_root:
|
||||||
|
project_path = paths.get_project_personas_path(self.project_root)
|
||||||
|
project_data = self._load_file(project_path)
|
||||||
|
for name, data in project_data.get("personas", {}).items():
|
||||||
|
personas[name] = Persona.from_dict(name, data)
|
||||||
|
return personas
|
||||||
|
|
||||||
|
def save_persona(self, persona: Persona, scope: str = "project") -> None:
|
||||||
|
path = self._get_path(scope)
|
||||||
|
data = self._load_file(path)
|
||||||
|
if "personas" not in data:
|
||||||
|
data["personas"] = {}
|
||||||
|
data["personas"][persona.name] = persona.to_dict()
|
||||||
|
self._save_file(path, data)
|
||||||
|
|
||||||
|
def get_persona_scope(self, name: str) -> str:
|
||||||
|
"""Returns the scope ('global' or 'project') of a persona by name."""
|
||||||
|
if self.project_root:
|
||||||
|
project_path = paths.get_project_personas_path(self.project_root)
|
||||||
|
project_data = self._load_file(project_path)
|
||||||
|
if name in project_data.get("personas", {}):
|
||||||
|
return "project"
|
||||||
|
global_path = paths.get_global_personas_path()
|
||||||
|
global_data = self._load_file(global_path)
|
||||||
|
if name in global_data.get("personas", {}):
|
||||||
|
return "global"
|
||||||
|
return "project"
|
||||||
|
|
||||||
|
def delete_persona(self, name: str, scope: str = "project") -> None:
|
||||||
|
path = self._get_path(scope)
|
||||||
|
data = self._load_file(path)
|
||||||
|
if "personas" in data and name in data["personas"]:
|
||||||
|
del data["personas"][name]
|
||||||
|
self._save_file(path, data)
|
||||||
|
|
||||||
|
def _load_file(self, path: Path) -> Dict[str, Any]:
|
||||||
|
if not path.exists():
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
return tomllib.load(f)
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def _save_file(self, path: Path, data: Dict[str, Any]) -> None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(path, "wb") as f:
|
||||||
|
tomli_w.dump(data, f)
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
# Track Specification: module_taxonomy_refactor_20260627
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The user-reported `models.py` is a "dumping ground" (1044 lines, 36 classes, 5+ unrelated domains). This track cleans it up PLUS addresses 5 ImGui LEAKS that violate the "ImGui belongs in `gui_2.py`" boundary PLUS unifies 2 vendor files with `ai_client.py`.
|
||||||
|
|
||||||
|
Per the user's principle: **unify unless there's a good reason (import load times, definition pollution)**. No sub-directories. Prefix naming convention.
|
||||||
|
|
||||||
|
## Current State Audit (master `5380b715`, measured 2026-06-27)
|
||||||
|
|
||||||
|
| Metric | Value |
|
||||||
|
|---|---:|
|
||||||
|
| `src/` file count | 65 |
|
||||||
|
| `src/models.py` line count | 1044 |
|
||||||
|
| `src/models.py` class/function count | 36 |
|
||||||
|
| `src/models.py` regions | 13 (Constants, Config Utilities, History Utilities, Pydantic Models, MMA Core, State & Config, Tool Models, UI/Editor, Persona, Workspace, MCP Config, Project Context, ...more) |
|
||||||
|
| ImGui-using files outside `gui_2.py` | 5 (`bg_shader.py`, `shaders.py`, `command_palette.py`, `diff_viewer.py`, `patch_modal.py`) |
|
||||||
|
| Vendor files separate from `ai_client.py` | 2 (`vendor_capabilities.py`, `vendor_state.py`) |
|
||||||
|
| `AGENT_TOOL_NAMES` consumers | 8 (3 in `app_controller.py`, 5 in `tests/test_arch_boundary_phase2.py`) |
|
||||||
|
| `mcp_tool_specs.tool_names()` test | EXISTS (asserts `tool_names() Γèå AGENT_TOOL_NAMES` ΓÇö proves it's redundant) |
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
| ID | Goal | Acceptance |
|
||||||
|
|---|---|---|
|
||||||
|
| G1 | **MERGE 5 ImGui LEAKS into `gui_2.py`** | `git grep -l "imgui_bundle\|from imgui\\." -- 'src/*.py'` returns ONLY `gui_2.py` + `imgui_scopes.py` |
|
||||||
|
| G2 | **MERGE 2 vendor files into `ai_client.py`** | `ls src/{vendor_capabilities,vendor_state}.py` returns not-found; `python -c "from src.ai_client import ..."` imports the merged symbols |
|
||||||
|
| G3 | **SPLIT `models.py`** into `mma.py` + `project.py` + `project_files.py` | `ls src/mma.py src/project.py src/project_files.py` all exist; `python -c "from src.mma import ThinkingSegment, Ticket, Track, WorkerContext, TrackState"` works |
|
||||||
|
| G4 | **MERGE** 6+ other `models.py` classes into existing sub-system files | `Persona` in `personas.py`; `Tool`/`ToolPreset` in `tool_presets.py`; `BiasProfile` in `tool_bias.py`; `TextEditorConfig`/`ExternalEditorConfig` in `external_editor.py`; `MCPServerConfig`+etc in `mcp_client.py`; `WorkspaceProfile` in `workspace_manager.py` |
|
||||||
|
| G5 | **DELETE `AGENT_TOOL_NAMES`** (redundant with `mcp_tool_specs.tool_names()`) | `git grep "AGENT_TOOL_NAMES" -- 'src/*.py'` returns 0 hits; 8 consumer sites updated to use `list(mcp_tool_specs.tool_names())` |
|
||||||
|
| G6 | **`src/models.py` reduced to Γëñ30 lines** (or eliminated) | `wc -l src/models.py` returns Γëñ30 |
|
||||||
|
| G7 | All 7 audit gates pass `--strict` | unchanged from baseline |
|
||||||
|
| G8 | All batched test tiers pass (10/11 baseline + RAG flake) | unchanged from baseline |
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
- Renaming existing files for prefix consistency (`multi_agent_conductor.py` → `mma_conductor.py`, etc.) — deferred to follow-up; current names are clear enough
|
||||||
|
- Refactoring `aggregate.py` (513 lines), `app_controller.py` (4869 lines), `gui_2.py` (7773 lines) ΓÇö out of scope; these have natural boundaries; the user doesn't want more splitting without good reason
|
||||||
|
- Modifications to `mcp_client.py` other than merging the config dataclasses ΓÇö the merge itself is the change
|
||||||
|
- New `src/<thing>.py` files (per AGENTS.md hard rule) ΓÇö the 3 new files (`mma.py`, `project.py`, `project_files.py`) are justified by the `models.py` split (definition pollution)
|
||||||
|
|
||||||
|
## Functional Requirements
|
||||||
|
|
||||||
|
### FR1: MERGE ImGui LEAKS into `gui_2.py`
|
||||||
|
|
||||||
|
For each of these 5 files, move the content into `gui_2.py` in a clearly-marked section, then `git rm` the original:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# In gui_2.py, add at the appropriate location:
|
||||||
|
|
||||||
|
#region: Bg Shader (moved from src/bg_shader.py)
|
||||||
|
# ... (content of src/bg_shader.py)
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region: Shaders (moved from src/shaders.py)
|
||||||
|
# ... (content of src/shaders.py)
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region: Command Palette (moved from src/command_palette.py)
|
||||||
|
# ... (content of src/command_palette.py)
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region: Diff Viewer (moved from src/diff_viewer.py)
|
||||||
|
# ... (content of src/diff_viewer.py)
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region: Patch Modal (moved from src/patch_modal.py)
|
||||||
|
# ... (content of src/patch_modal.py)
|
||||||
|
#endregion
|
||||||
|
```
|
||||||
|
|
||||||
|
**Imports to update across the codebase:**
|
||||||
|
- `from src.bg_shader import X` → `from src.gui_2 import X`
|
||||||
|
- `from src.shaders import X` → `from src.gui_2 import X`
|
||||||
|
- (etc. for all 5 files)
|
||||||
|
|
||||||
|
### FR2: MERGE vendor files into `ai_client.py`
|
||||||
|
|
||||||
|
```python
|
||||||
|
# In ai_client.py, add at the appropriate location:
|
||||||
|
|
||||||
|
#region: Vendor Capabilities (moved from src/vendor_capabilities.py)
|
||||||
|
# ... (content of src/vendor_capabilities.py)
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region: Vendor State (moved from src/vendor_state.py)
|
||||||
|
# ... (content of src/vendor_state.py)
|
||||||
|
#endregion
|
||||||
|
```
|
||||||
|
|
||||||
|
**Imports to update:**
|
||||||
|
- `from src.vendor_capabilities import X` → `from src.ai_client import X`
|
||||||
|
- `from src.vendor_state import X` → `from src.ai_client import X`
|
||||||
|
|
||||||
|
### FR3: SPLIT `models.py`
|
||||||
|
|
||||||
|
**Phase 1: Create `src/mma.py`** with the MMA Core + TrackState:
|
||||||
|
- ThinkingSegment
|
||||||
|
- Ticket
|
||||||
|
- Track
|
||||||
|
- WorkerContext
|
||||||
|
- TrackState
|
||||||
|
- Top-level docstring explaining MMA scope
|
||||||
|
|
||||||
|
**Phase 2: Create `src/project.py`** with the project config:
|
||||||
|
- ProjectContext + 5 sub-dataclasses (ProjectMeta, ProjectOutput, ProjectFiles, ProjectScreenshots, ProjectDiscussion)
|
||||||
|
- Config I/O helpers: `_clean_nones`, `load_config_from_disk`, `save_config_to_disk`, `parse_history_entries`
|
||||||
|
- Top-level docstring explaining project config scope
|
||||||
|
|
||||||
|
**Phase 3: Create `src/project_files.py`** with the file-related dataclasses:
|
||||||
|
- FileItem
|
||||||
|
- ContextPreset
|
||||||
|
- ContextFileEntry
|
||||||
|
- NamedViewPreset
|
||||||
|
- Preset
|
||||||
|
- Top-level docstring explaining file-related project state scope
|
||||||
|
|
||||||
|
### FR4: MERGE other `models.py` classes into existing sub-system files
|
||||||
|
|
||||||
|
| Class from `models.py` | Destination (existing file) | New section name |
|
||||||
|
|---|---|---|
|
||||||
|
| `Persona` | `src/personas.py` | "Persona Dataclass" |
|
||||||
|
| `Tool`, `ToolPreset` | `src/tool_presets.py` | "Tool + ToolPreset Dataclasses" |
|
||||||
|
| `BiasProfile` | `src/tool_bias.py` | "BiasProfile Dataclass" |
|
||||||
|
| `TextEditorConfig`, `ExternalEditorConfig` | `src/external_editor.py` | "Editor Config Dataclasses" |
|
||||||
|
| `MCPServerConfig`, `MCPConfiguration`, `VectorStoreConfig`, `RAGConfig`, `load_mcp_config` | `src/mcp_client.py` | "MCP Config Dataclasses" |
|
||||||
|
| `WorkspaceProfile` | `src/workspace_manager.py` | "WorkspaceProfile Dataclass" |
|
||||||
|
|
||||||
|
### FR5: DELETE `AGENT_TOOL_NAMES` (redundant)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 8 consumer site updates:
|
||||||
|
# Before:
|
||||||
|
from src.models import AGENT_TOOL_NAMES
|
||||||
|
for tool in AGENT_TOOL_NAMES:
|
||||||
|
...
|
||||||
|
|
||||||
|
# After:
|
||||||
|
from src import mcp_tool_specs
|
||||||
|
for tool in mcp_tool_specs.tool_names():
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
**Consumer sites (8):**
|
||||||
|
- `src/app_controller.py:2110, 2972, 3273` (3 sites)
|
||||||
|
- `tests/test_arch_boundary_phase2.py:23, 29, 31, 32, 33` (5 sites)
|
||||||
|
|
||||||
|
**Test simplification:** `test_tool_names_subset_of_models_agent_tool_names` becomes either:
|
||||||
|
- DELETE (it's a tautology once `AGENT_TOOL_NAMES` is derived from `tool_names()`)
|
||||||
|
- OR convert to a positive assertion: `assert mcp_tool_specs.tool_names() == {expected canonical tools}`
|
||||||
|
|
||||||
|
### FR6: REDUCE `src/models.py` to ~30 lines (or eliminate)
|
||||||
|
|
||||||
|
After all moves, `src/models.py` contains:
|
||||||
|
- `_create_generate_request`, `_create_confirm_request`, `__getattr__` (Pydantic lazy proxies for the API)
|
||||||
|
- OR these move to `src/api_hooks.py` (if API-specific)
|
||||||
|
- Top-level docstring
|
||||||
|
|
||||||
|
If `models.py` becomes essentially empty after these moves, **delete the file entirely** (it's not a "system" file; `models.py` is just a temporary holder).
|
||||||
|
|
||||||
|
## Non-Functional Requirements
|
||||||
|
|
||||||
|
- NFR1: 1-space indentation (per `conductor/workflow.md`)
|
||||||
|
- NFR2: CRLF line endings on Windows
|
||||||
|
- NFR3: No comments in source code (per AGENTS.md "No comments in source code")
|
||||||
|
- NFR4: Per-task atomic commits with git notes
|
||||||
|
- NFR5: No new pip dependencies
|
||||||
|
- NFR6: `Result[T]` returns for fallible fns (per `error_handling.md`)
|
||||||
|
- NFR7: No new `src/<thing>.py` files UNLESS justified by definition pollution (per AGENTS.md hard rule)
|
||||||
|
|
||||||
|
## Architecture Reference
|
||||||
|
|
||||||
|
- `AGENTS.md` ΓÇö "File Size and Naming Convention" HARD RULE
|
||||||
|
- `conductor/code_styleguides/data_oriented_design.md` ΓÇö "Prefer Fewer Types" principle
|
||||||
|
- `conductor/code_styleguides/error_handling.md` ΓÇö the `Result[T]` convention
|
||||||
|
- `conductor/code_styleguides/type_aliases.md` ΓÇö the 10 TypeAliases convention
|
||||||
|
- `conductor/tracks/cruft_elimination_20260627/SPEC_CORRECTION_phase_2.md` ΓÇö the related spec correction (the original Phase 2 spec was wrong to put ProjectContext in `models.py`; this track fixes that)
|
||||||
|
- `docs/reports/FOLLOWUP_module_taxonomy_20260627.md` ΓÇö the previous followup report (this track supersedes it with concrete execution)
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
- Renaming existing files for prefix consistency (`multi_agent_conductor.py` → `mma_conductor.py`, etc.) — deferred to follow-up
|
||||||
|
- Refactoring `aggregate.py` (513 lines), `app_controller.py` (4869 lines), `gui_2.py` (7773 lines) ΓÇö out of scope; these have natural boundaries
|
||||||
|
- Modifications to `mcp_client.py` other than merging the config dataclasses
|
||||||
|
- New `src/<thing>.py` files beyond the 3 justified ones (`mma.py`, `project.py`, `project_files.py`)
|
||||||
|
- The RAG test pre-existing flake (per `docs/reports/SSDL_CAMPAIGN_ABORTED_20260624.md` "Out of Scope")
|
||||||
|
- Any Tier 2 spec rewrites (per the user's earlier "don't fuck with commits" directive)
|
||||||
|
|
||||||
|
## Verification Criteria (Definition of Done)
|
||||||
|
|
||||||
|
| # | Criterion | Verification |
|
||||||
|
|---|---|---|
|
||||||
|
| VC1 | ImGui imports limited to `gui_2.py` + `imgui_scopes.py` | `git grep -l "imgui_bundle\|from imgui\\." -- 'src/*.py'` returns 2 files |
|
||||||
|
| VC2 | `src/bg_shader.py`, `src/shaders.py`, `src/command_palette.py`, `src/diff_viewer.py` deleted (4 LEAK files per the data/view/ops split) | `ls src/{bg_shader,shaders,command_palette,diff_viewer}.py` returns not-found. `src/patch_modal.py` is NOT a LEAK ΓÇö it's the data module (DiffHunk/DiffFile/PendingPatch) per the data/view/ops split rule. The diff_viewer classes (DiffHunk/DiffFile) were moved INTO it during the cruft_elimination track's split; deleting it would violate the data module's integrity. See `conductor/tracks/post_module_taxonomy_de_cruft_20260627/spec.md` Phase 1 for the formal correction. |
|
||||||
|
| VC3 | `src/vendor_capabilities.py`, `src/vendor_state.py` deleted | `ls src/{vendor_capabilities,vendor_state}.py` returns not-found |
|
||||||
|
| VC4 | Vendor symbols importable from `src.ai_client` | `python -c "from src.ai_client import PROVIDER_CAPABILITIES, get_vendor_state"` works |
|
||||||
|
| VC5 | `src/mma.py` exists with MMA Core + TrackState | `python -c "from src.mma import ThinkingSegment, Ticket, Track, WorkerContext, TrackState"` works |
|
||||||
|
| VC6 | `src/project.py` exists with ProjectContext + sub + config I/O | `python -c "from src.project import ProjectContext, ProjectMeta, ProjectOutput, ProjectFiles, ProjectScreenshots, ProjectDiscussion, _clean_nones, load_config_from_disk, save_config_to_disk, parse_history_entries"` works |
|
||||||
|
| VC7 | `src/project_files.py` exists with file-related dataclasses | `python -c "from src.project_files import FileItem, ContextPreset, ContextFileEntry, NamedViewPreset, Preset"` works |
|
||||||
|
| VC8 | Persona/Tool/Editor/MCP/Workspace dataclasses in their proper sub-system files | `python -c "from src.personas import Persona; from src.tool_presets import Tool, ToolPreset; from src.tool_bias import BiasProfile; from src.external_editor import TextEditorConfig, ExternalEditorConfig; from src.mcp_client import MCPServerConfig, MCPConfiguration, VectorStoreConfig, RAGConfig, load_mcp_config; from src.workspace_manager import WorkspaceProfile"` works |
|
||||||
|
| VC9 | `AGENT_TOOL_NAMES` deleted; all 8 consumer sites use `mcp_tool_specs.tool_names()` | `git grep "AGENT_TOOL_NAMES" -- 'src/*.py' 'tests/*.py'` returns 0 hits |
|
||||||
|
| VC10 | `src/models.py` reduced from 1044 to ~135 lines (Pydantic proxies + DEFAULT_TOOL_CATEGORIES + lazy `__getattr__` for backward compat) | `wc -l src/models.py` returns Γëñ200; the 30-line target was aspirational. The lazy `__getattr__` is necessary for backward compat with 30+ legacy `from src.models import X` call sites until the `post_module_taxonomy_de_cruft_20260627` follow-up track migrates them to direct imports from the subsystem files (`src.mma`, `src.project`, `src/project_files`, `src/tool_presets`, `src/tool_bias`, `src/external_editor`, `src/personas`, `src/workspace_manager`, `src/mcp_client`). The full migration is FR7 of the post_module_taxonomy_de_cruft_20260627 track. The legacy `Metadata = TrackMetadata` alias is preserved for `from src.models import Metadata` to resolve to the TrackMetadata dataclass (used by `tests/test_track_state_schema.py`). |
|
||||||
|
| VC11 | All 7 audit gates pass `--strict` | unchanged from baseline |
|
||||||
|
| VC12 | 10/11 batched test tiers pass (RAG flake acceptable) | unchanged from baseline |
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
| # | Risk | Likelihood | Mitigation |
|
||||||
|
|---|---|---|---|
|
||||||
|
| R1 | ImGui LEAKS move breaks existing tests (e.g., `command_palette` is referenced in commands.py) | low | Run full affected test set after each move; revert + fix on regression |
|
||||||
|
| R2 | Vendor merge into `ai_client.py` creates circular imports (PROVIDERS lazy proxy is the workaround) | medium | The lazy import pattern (`__getattr__`) handles this; verify by running the full test suite after merge |
|
||||||
|
| R3 | `models.py` split breaks 136 import sites | high | Per-file move with regression-guard tests after each; update imports systematically |
|
||||||
|
| R4 | The 6+ "merge into existing sub-system files" moves break those files' existing tests | medium | Run the affected test file after each merge |
|
||||||
|
| R5 | `AGENT_TOOL_NAMES` deletion breaks `test_arch_boundary_phase2.py` | low | Update the test to use `mcp_tool_specs.tool_names()`; cross-check that the test's expected tool names are in the registry |
|
||||||
|
| R6 | The `ProjectContext` Phase 2 commit (in `cruft_elimination_20260627`) put `ProjectContext` in `models.py`; the new track moves it to `project.py` ΓÇö needs to coordinate with the cruft track | high | The cruft track should NOT merge its `models.py` `ProjectContext` commit; this refactor track handles the move |
|
||||||
|
| R7 | The `_create_generate_request` etc. Pydantic proxies in `models.py` are used by `api_hooks.py`; if we move them to `api_hooks.py` we create a different topology | low | Audit the consumers; if they're all in `api_hooks.py`, move them; if not, keep in `models.py` or move to a new `api_models.py` |
|
||||||
|
|
||||||
|
## See also
|
||||||
|
|
||||||
|
- `docs/reports/FOLLOWUP_module_taxonomy_20260627.md` ΓÇö the previous followup report (this spec supersedes it)
|
||||||
|
- `conductor/tracks/cruft_elimination_20260627/SPEC_CORRECTION_phase_2.md` ΓÇö the related spec correction
|
||||||
|
- `conductor/tracks/cruft_elimination_20260627/spec.md` ΓÇö the parent spec (which is currently in flux)
|
||||||
|
- `AGENTS.md` ΓÇö "File Size and Naming Convention" HARD RULE
|
||||||
|
- `conductor/code_styleguides/data_oriented_design.md` ΓÇö "Prefer Fewer Types" principle
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
from src.tool_presets import Tool, ToolPreset
|
||||||
|
from src.type_aliases import Metadata
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BiasProfile:
|
||||||
|
name: str
|
||||||
|
tool_weights: Dict[str, int] = field(default_factory=dict)
|
||||||
|
category_multipliers: Dict[str, float] = field(default_factory=dict)
|
||||||
|
|
||||||
|
def to_dict(self) -> Metadata:
|
||||||
|
return {
|
||||||
|
"name": self.name,
|
||||||
|
"tool_weights": self.tool_weights,
|
||||||
|
"category_multipliers": self.category_multipliers,
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: Metadata) -> "BiasProfile":
|
||||||
|
return cls(
|
||||||
|
name = data["name"],
|
||||||
|
tool_weights = data.get("tool_weights", {}),
|
||||||
|
category_multipliers = data.get("category_multipliers", {}),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ToolBiasEngine:
|
||||||
|
def apply_semantic_nudges(self, tool_definitions: List[Dict[str, Any]], preset: ToolPreset) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
[C: tests/test_tool_bias.py:test_apply_semantic_nudges, tests/test_tool_bias.py:test_parameter_bias_nudging]
|
||||||
|
"""
|
||||||
|
weight_map = {
|
||||||
|
5: "[HIGH PRIORITY] ",
|
||||||
|
4: "[PREFERRED] ",
|
||||||
|
2: "[NOT RECOMMENDED] ",
|
||||||
|
1: "[LOW PRIORITY] "
|
||||||
|
}
|
||||||
|
|
||||||
|
preset_tools: Dict[str, Tool] = {}
|
||||||
|
for cat_tools in preset.categories.values():
|
||||||
|
for t in cat_tools:
|
||||||
|
if isinstance(t, Tool):
|
||||||
|
preset_tools[t.name] = t
|
||||||
|
|
||||||
|
for defn in tool_definitions:
|
||||||
|
name = defn.get("name")
|
||||||
|
if name in preset_tools:
|
||||||
|
tool = preset_tools[name]
|
||||||
|
prefix = weight_map.get(tool.weight, "")
|
||||||
|
if prefix:
|
||||||
|
defn["description"] = prefix + defn.get("description", "")
|
||||||
|
|
||||||
|
if tool.parameter_bias:
|
||||||
|
params = defn.get("parameters") or defn.get("input_schema")
|
||||||
|
if params and "properties" in params:
|
||||||
|
props = params["properties"]
|
||||||
|
for p_name, bias in tool.parameter_bias.items():
|
||||||
|
if p_name in props:
|
||||||
|
p_desc = props[p_name].get("description", "")
|
||||||
|
props[p_name]["description"] = f"[{bias}] {p_desc}".strip()
|
||||||
|
|
||||||
|
return tool_definitions
|
||||||
|
|
||||||
|
def generate_tooling_strategy(self, preset: ToolPreset, global_bias: BiasProfile) -> str:
|
||||||
|
"""
|
||||||
|
[C: tests/test_tool_bias.py:test_generate_tooling_strategy]
|
||||||
|
"""
|
||||||
|
lines = ["### Tooling Strategy"]
|
||||||
|
|
||||||
|
preferred = []
|
||||||
|
low_priority = []
|
||||||
|
for cat_tools in preset.categories.values():
|
||||||
|
for t in cat_tools:
|
||||||
|
if not isinstance(t, Tool): continue
|
||||||
|
if t.weight >= 5: preferred.append(f"{t.name} [HIGH PRIORITY]")
|
||||||
|
elif t.weight == 4: preferred.append(f"{t.name} [PREFERRED]")
|
||||||
|
elif t.weight == 2: low_priority.append(f"{t.name} [NOT RECOMMENDED]")
|
||||||
|
elif t.weight <= 1: low_priority.append(f"{t.name} [LOW PRIORITY]")
|
||||||
|
|
||||||
|
if preferred: lines.append(f"Preferred tools: {', '.join(preferred)}.")
|
||||||
|
if low_priority: lines.append(f"Low-priority tools: {', '.join(low_priority)}.")
|
||||||
|
|
||||||
|
if global_bias.category_multipliers:
|
||||||
|
lines.append("Category focus multipliers:")
|
||||||
|
for cat, mult in global_bias.category_multipliers.items():
|
||||||
|
lines.append(f"- {cat}: {mult}x")
|
||||||
|
|
||||||
|
return "\n\n".join(lines)
|
||||||
+186
@@ -0,0 +1,186 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import tomllib
|
||||||
|
import tomli_w
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List, Optional, Union, Any
|
||||||
|
|
||||||
|
from src import paths
|
||||||
|
from src.type_aliases import Metadata
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Tool:
|
||||||
|
name: str
|
||||||
|
approval: str = 'auto'
|
||||||
|
weight: int = 3
|
||||||
|
parameter_bias: Dict[str, str] = field(default_factory=dict)
|
||||||
|
|
||||||
|
def to_dict(self) -> Metadata:
|
||||||
|
return {
|
||||||
|
"name": self.name,
|
||||||
|
"approval": self.approval,
|
||||||
|
"weight": self.weight,
|
||||||
|
"parameter_bias": self.parameter_bias,
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: Metadata) -> "Tool":
|
||||||
|
return cls(
|
||||||
|
name=data["name"],
|
||||||
|
approval=data.get("approval", "auto"),
|
||||||
|
weight=data.get("weight", 3),
|
||||||
|
parameter_bias=data.get("parameter_bias", {}),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ToolPreset:
|
||||||
|
name: str
|
||||||
|
categories: Dict[str, List[Union[Tool, Any]]] = field(default_factory=dict)
|
||||||
|
|
||||||
|
def to_dict(self) -> Metadata:
|
||||||
|
serialized_categories = {}
|
||||||
|
for cat, tools in self.categories.items():
|
||||||
|
serialized_categories[cat] = [t.to_dict() if isinstance(t, Tool) else t for t in tools]
|
||||||
|
return {"categories": serialized_categories}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, name: str, data: Metadata) -> "ToolPreset":
|
||||||
|
raw_categories = data.get("categories", {})
|
||||||
|
parsed_categories = {}
|
||||||
|
for cat, tools in raw_categories.items():
|
||||||
|
parsed_categories[cat] = [Tool.from_dict(t) if isinstance(t, dict) else t for t in tools]
|
||||||
|
return cls(name=name, categories=parsed_categories)
|
||||||
|
|
||||||
|
|
||||||
|
class ToolPresetManager:
|
||||||
|
def __init__(self, project_root: Optional[Union[str, Path]] = None):
|
||||||
|
self.project_root = Path(project_root) if project_root else None
|
||||||
|
|
||||||
|
def _get_path(self, scope: str) -> Path:
|
||||||
|
"""
|
||||||
|
[C: src/workspace_manager.py:WorkspaceManager.delete_profile, src/workspace_manager.py:WorkspaceManager.save_profile]
|
||||||
|
"""
|
||||||
|
if scope == "global":
|
||||||
|
return paths.get_global_tool_presets_path()
|
||||||
|
elif scope == "project":
|
||||||
|
if not self.project_root:
|
||||||
|
raise ValueError("Project root not set for project scope operation.")
|
||||||
|
return paths.get_project_tool_presets_path(self.project_root)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Invalid scope: {scope}")
|
||||||
|
|
||||||
|
def _read_raw(self, path: Path) -> Dict[str, Any]:
|
||||||
|
if not path.exists():
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
return tomllib.load(f)
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def _write_raw(self, path: Path, data: Dict[str, Any]) -> None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(path, "wb") as f:
|
||||||
|
tomli_w.dump(data, f)
|
||||||
|
|
||||||
|
def load_all_presets(self) -> Dict[str, ToolPreset]:
|
||||||
|
"""
|
||||||
|
[C: tests/test_tool_preset_manager.py:test_load_all_presets_merged]
|
||||||
|
"""
|
||||||
|
global_path = paths.get_global_tool_presets_path()
|
||||||
|
global_data = self._read_raw(global_path).get("presets", {})
|
||||||
|
|
||||||
|
presets = {}
|
||||||
|
for name, config in global_data.items():
|
||||||
|
if isinstance(config, dict):
|
||||||
|
presets[name] = ToolPreset.from_dict(name, config)
|
||||||
|
|
||||||
|
if self.project_root:
|
||||||
|
project_path = paths.get_project_tool_presets_path(self.project_root)
|
||||||
|
project_data = self._read_raw(project_path).get("presets", {})
|
||||||
|
for name, config in project_data.items():
|
||||||
|
if isinstance(config, dict):
|
||||||
|
presets[name] = ToolPreset.from_dict(name, config)
|
||||||
|
|
||||||
|
return presets
|
||||||
|
|
||||||
|
def load_all(self) -> Dict[str, ToolPreset]:
|
||||||
|
"""
|
||||||
|
Backward compatibility for load_all().
|
||||||
|
[C: tests/test_persona_manager.py:test_delete_persona, tests/test_persona_manager.py:test_load_all_merged, tests/test_persona_manager.py:test_save_persona, tests/test_preset_manager.py:test_delete_preset, tests/test_preset_manager.py:test_load_all_merged, tests/test_preset_manager.py:test_save_preset_global, tests/test_preset_manager.py:test_save_preset_project, tests/test_presets.py:TestPresetManager.test_delete_preset, tests/test_presets.py:TestPresetManager.test_project_overwrites_global, tests/test_presets.py:TestPresetManager.test_save_and_load_global, tests/test_presets.py:TestPresetManager.test_save_and_load_project]
|
||||||
|
"""
|
||||||
|
return self.load_all_presets()
|
||||||
|
|
||||||
|
def save_preset(self, preset: ToolPreset, scope: str = "project") -> None:
|
||||||
|
"""
|
||||||
|
[C: tests/test_preset_manager.py:test_save_preset_global, tests/test_preset_manager.py:test_save_preset_project, tests/test_preset_manager.py:test_save_preset_project_no_root, tests/test_presets.py:TestPresetManager.test_delete_preset, tests/test_presets.py:TestPresetManager.test_project_overwrites_global, tests/test_presets.py:TestPresetManager.test_save_and_load_global, tests/test_presets.py:TestPresetManager.test_save_and_load_project]
|
||||||
|
"""
|
||||||
|
path = self._get_path(scope)
|
||||||
|
data = self._read_raw(path)
|
||||||
|
if "presets" not in data:
|
||||||
|
data["presets"] = {}
|
||||||
|
data["presets"][preset.name] = preset.to_dict()
|
||||||
|
self._write_raw(path, data)
|
||||||
|
|
||||||
|
def delete_preset(self, name: str, scope: str = "project") -> None:
|
||||||
|
"""
|
||||||
|
[C: tests/test_preset_manager.py:test_delete_preset, tests/test_presets.py:TestPresetManager.test_delete_preset]
|
||||||
|
"""
|
||||||
|
path = self._get_path(scope)
|
||||||
|
data = self._read_raw(path)
|
||||||
|
if "presets" in data and name in data["presets"]:
|
||||||
|
del data["presets"][name]
|
||||||
|
self._write_raw(path, data)
|
||||||
|
|
||||||
|
def load_all_bias_profiles(self) -> Dict[str, "BiasProfile"]:
|
||||||
|
"""
|
||||||
|
[C: tests/test_tool_preset_manager.py:test_bias_profiles_merged, tests/test_tool_preset_manager.py:test_delete_bias_profile, tests/test_tool_preset_manager.py:test_save_bias_profile]
|
||||||
|
"""
|
||||||
|
from src.tool_bias import BiasProfile
|
||||||
|
global_path = paths.get_global_tool_presets_path()
|
||||||
|
global_data = self._read_raw(global_path).get("bias_profiles", {})
|
||||||
|
|
||||||
|
profiles = {}
|
||||||
|
for name, config in global_data.items():
|
||||||
|
if isinstance(config, dict):
|
||||||
|
cfg = dict(config)
|
||||||
|
if "name" not in cfg:
|
||||||
|
cfg["name"] = name
|
||||||
|
profiles[name] = BiasProfile.from_dict(cfg)
|
||||||
|
|
||||||
|
if self.project_root:
|
||||||
|
project_path = paths.get_project_tool_presets_path(self.project_root)
|
||||||
|
project_data = self._read_raw(project_path).get("bias_profiles", {})
|
||||||
|
for name, config in project_data.items():
|
||||||
|
if isinstance(config, dict):
|
||||||
|
cfg = dict(config)
|
||||||
|
if "name" not in cfg:
|
||||||
|
cfg["name"] = name
|
||||||
|
profiles[name] = BiasProfile.from_dict(cfg)
|
||||||
|
|
||||||
|
return profiles
|
||||||
|
|
||||||
|
def save_bias_profile(self, profile: BiasProfile, scope: str = "project") -> None:
|
||||||
|
"""
|
||||||
|
[C: tests/test_tool_preset_manager.py:test_save_bias_profile]
|
||||||
|
"""
|
||||||
|
path = self._get_path(scope)
|
||||||
|
data = self._read_raw(path)
|
||||||
|
if "bias_profiles" not in data:
|
||||||
|
data["bias_profiles"] = {}
|
||||||
|
data["bias_profiles"][profile.name] = profile.to_dict()
|
||||||
|
self._write_raw(path, data)
|
||||||
|
|
||||||
|
def delete_bias_profile(self, name: str, scope: str = "project") -> None:
|
||||||
|
"""
|
||||||
|
[C: tests/test_tool_preset_manager.py:test_delete_bias_profile]
|
||||||
|
"""
|
||||||
|
path = self._get_path(scope)
|
||||||
|
data = self._read_raw(path)
|
||||||
|
if "bias_profiles" in data and name in data["bias_profiles"]:
|
||||||
|
del data["bias_profiles"][name]
|
||||||
|
self._write_raw(path, data)
|
||||||
+109
@@ -0,0 +1,109 @@
|
|||||||
|
import tomllib
|
||||||
|
import tomli_w
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, Any, Optional, Union
|
||||||
|
|
||||||
|
from src import paths
|
||||||
|
from src.type_aliases import Metadata
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class WorkspaceProfile:
|
||||||
|
name: str
|
||||||
|
ini_content: str
|
||||||
|
show_windows: Dict[str, bool]
|
||||||
|
panel_states: Metadata
|
||||||
|
|
||||||
|
def to_dict(self) -> Metadata:
|
||||||
|
return {
|
||||||
|
"ini_content": self.ini_content,
|
||||||
|
"show_windows": self.show_windows,
|
||||||
|
"panel_states": self.panel_states,
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, name: str, data: Metadata) -> "WorkspaceProfile":
|
||||||
|
return cls(
|
||||||
|
name = name,
|
||||||
|
ini_content = data.get("ini_content", ""),
|
||||||
|
show_windows = data.get("show_windows", {}),
|
||||||
|
panel_states = data.get("panel_states", {}),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceManager:
|
||||||
|
"""Manages Workspace profiles across global and project-specific files."""
|
||||||
|
|
||||||
|
def __init__(self, project_root: Optional[Union[str, Path]] = None):
|
||||||
|
if project_root:
|
||||||
|
self.project_root = Path(project_root).resolve()
|
||||||
|
else:
|
||||||
|
self.project_root = None
|
||||||
|
|
||||||
|
def _get_path(self, scope: str) -> Path:
|
||||||
|
if scope == "global":
|
||||||
|
return paths.get_global_workspace_profiles_path()
|
||||||
|
elif scope == "project":
|
||||||
|
if not self.project_root:
|
||||||
|
raise ValueError("Project root is not set, cannot resolve project scope.")
|
||||||
|
return paths.get_project_workspace_profiles_path(self.project_root)
|
||||||
|
else:
|
||||||
|
raise ValueError("Invalid scope, must be 'global' or 'project'")
|
||||||
|
|
||||||
|
def load_all_profiles(self) -> Dict[str, WorkspaceProfile]:
|
||||||
|
"""
|
||||||
|
Merges global and project profiles into a single dictionary.
|
||||||
|
[C: tests/test_workspace_manager.py:test_delete_profile, tests/test_workspace_manager.py:test_load_all_profiles_merged, tests/test_workspace_manager.py:test_save_profile_global_and_project]
|
||||||
|
"""
|
||||||
|
profiles = {}
|
||||||
|
|
||||||
|
global_path = paths.get_global_workspace_profiles_path()
|
||||||
|
global_data = self._load_file(global_path)
|
||||||
|
for name, data in global_data.get("profiles", {}).items():
|
||||||
|
profiles[name] = WorkspaceProfile.from_dict(name, data)
|
||||||
|
|
||||||
|
if self.project_root:
|
||||||
|
project_path = paths.get_project_workspace_profiles_path(self.project_root)
|
||||||
|
project_data = self._load_file(project_path)
|
||||||
|
for name, data in project_data.get("profiles", {}).items():
|
||||||
|
profiles[name] = WorkspaceProfile.from_dict(name, data)
|
||||||
|
|
||||||
|
return profiles
|
||||||
|
|
||||||
|
def save_profile(self, profile: WorkspaceProfile, scope: str = "project") -> None:
|
||||||
|
"""
|
||||||
|
[C: tests/test_workspace_manager.py:test_delete_profile, tests/test_workspace_manager.py:test_save_profile_global_and_project]
|
||||||
|
"""
|
||||||
|
path = self._get_path(scope)
|
||||||
|
data = self._load_file(path)
|
||||||
|
if "profiles" not in data:
|
||||||
|
data["profiles"] = {}
|
||||||
|
|
||||||
|
data["profiles"][profile.name] = profile.to_dict()
|
||||||
|
self._save_file(path, data)
|
||||||
|
|
||||||
|
def delete_profile(self, name: str, scope: str = "project") -> None:
|
||||||
|
"""
|
||||||
|
[C: tests/test_workspace_manager.py:test_delete_profile]
|
||||||
|
"""
|
||||||
|
path = self._get_path(scope)
|
||||||
|
data = self._load_file(path)
|
||||||
|
if "profiles" in data and name in data["profiles"]:
|
||||||
|
del data["profiles"][name]
|
||||||
|
self._save_file(path, data)
|
||||||
|
|
||||||
|
def _load_file(self, path: Path) -> Dict[str, Any]:
|
||||||
|
if not path.exists():
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
return tomllib.load(f)
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def _save_file(self, path: Path, data: Dict[str, Any]) -> None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(path, "wb") as f:
|
||||||
|
tomli_w.dump(data, f)
|
||||||
+59
-53
@@ -1,4 +1,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
from src.tool_presets import ToolPreset
|
||||||
|
from src.mma import Ticket, Track, TrackState
|
||||||
|
from src.personas import Persona
|
||||||
|
from src.mcp_client import MCPConfiguration, RAGConfig, load_mcp_config
|
||||||
|
from src.project_files import ContextPreset, FileItem, NamedViewPreset, Preset
|
||||||
|
from src.tool_bias import BiasProfile
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
import inspect
|
import inspect
|
||||||
@@ -505,13 +511,13 @@ def _handle_mma_state_update(controller: 'AppController', task: dict):
|
|||||||
if track_data:
|
if track_data:
|
||||||
tickets = []
|
tickets = []
|
||||||
for t_data in controller.active_tickets:
|
for t_data in controller.active_tickets:
|
||||||
if isinstance(t_data, models.Ticket):
|
if isinstance(t_data, Ticket):
|
||||||
tickets.append(t_data)
|
tickets.append(t_data)
|
||||||
else:
|
else:
|
||||||
if "goal" in t_data and "description" not in t_data:
|
if "goal" in t_data and "description" not in t_data:
|
||||||
t_data["description"] = t_data["goal"]
|
t_data["description"] = t_data["goal"]
|
||||||
tickets.append(models.Ticket.from_dict(t_data))
|
tickets.append(Ticket.from_dict(t_data))
|
||||||
controller.active_track = models.Track(
|
controller.active_track = Track(
|
||||||
id=track_data.get("id"),
|
id=track_data.get("id"),
|
||||||
description=track_data.get("title", ""),
|
description=track_data.get("title", ""),
|
||||||
tickets=tickets
|
tickets=tickets
|
||||||
@@ -998,7 +1004,7 @@ class AppController:
|
|||||||
self.discussion_sent_system_prompt: str = ""
|
self.discussion_sent_system_prompt: str = ""
|
||||||
self.disc_roles: List[str] = []
|
self.disc_roles: List[str] = []
|
||||||
self.tracks: list[Metadata] = []
|
self.tracks: list[Metadata] = []
|
||||||
self.active_track: Optional[models.Track] = None
|
self.active_track: Optional[Track] = None
|
||||||
self.engines: Dict[str, multi_agent_conductor.ConductorEngine] = {}
|
self.engines: Dict[str, multi_agent_conductor.ConductorEngine] = {}
|
||||||
self.mma_streams: Dict[str, str] = {}
|
self.mma_streams: Dict[str, str] = {}
|
||||||
self.MAX_STREAM_SIZE: int = 10 * 1024
|
self.MAX_STREAM_SIZE: int = 10 * 1024
|
||||||
@@ -1017,9 +1023,9 @@ class AppController:
|
|||||||
"Tier 3": {"input": 0, "output": 0, "provider": "gemini", "model": "gemini-2.5-flash-lite", "tool_preset": None},
|
"Tier 3": {"input": 0, "output": 0, "provider": "gemini", "model": "gemini-2.5-flash-lite", "tool_preset": None},
|
||||||
"Tier 4": {"input": 0, "output": 0, "provider": "gemini", "model": "gemini-2.5-flash-lite", "tool_preset": None},
|
"Tier 4": {"input": 0, "output": 0, "provider": "gemini", "model": "gemini-2.5-flash-lite", "tool_preset": None},
|
||||||
}
|
}
|
||||||
self.mcp_config: models.MCPConfiguration = models.MCPConfiguration()
|
self.mcp_config: MCPConfiguration = MCPConfiguration()
|
||||||
self.view_presets: list[models.NamedViewPreset] = []
|
self.view_presets: list[NamedViewPreset] = []
|
||||||
self.rag_config: Optional[models.RAGConfig] = None
|
self.rag_config: Optional[RAGConfig] = None
|
||||||
self.rag_status: str = 'idle'
|
self.rag_status: str = 'idle'
|
||||||
self.temperature: float = 0.0
|
self.temperature: float = 0.0
|
||||||
self.top_p: float = 1.0
|
self.top_p: float = 1.0
|
||||||
@@ -1099,8 +1105,8 @@ class AppController:
|
|||||||
#endregion: UI State
|
#endregion: UI State
|
||||||
|
|
||||||
# --- Media/Context ---
|
# --- Media/Context ---
|
||||||
self.files: List[models.FileItem] = []
|
self.files: List[FileItem] = []
|
||||||
self.context_files: List[models.FileItem] = []
|
self.context_files: List[FileItem] = []
|
||||||
self.screenshots: List[str] = []
|
self.screenshots: List[str] = []
|
||||||
|
|
||||||
# --- Services ---
|
# --- Services ---
|
||||||
@@ -1110,7 +1116,7 @@ class AppController:
|
|||||||
# --- Defaults set here so tests that construct AppController without
|
# --- Defaults set here so tests that construct AppController without
|
||||||
# calling init_state() still see the attributes ---
|
# calling init_state() still see the attributes ---
|
||||||
self.ui_global_preset_name: Optional[str] = None
|
self.ui_global_preset_name: Optional[str] = None
|
||||||
self.active_tickets: list[models.Ticket] = []
|
self.active_tickets: list[Ticket] = []
|
||||||
self.ui_selected_tickets: Set[str] = set()
|
self.ui_selected_tickets: Set[str] = set()
|
||||||
|
|
||||||
#region: --- Configuration Maps ---
|
#region: --- Configuration Maps ---
|
||||||
@@ -1753,7 +1759,7 @@ class AppController:
|
|||||||
on `self._mcp_config_parse_error` for sub-track 4 GUI."""
|
on `self._mcp_config_parse_error` for sub-track 4 GUI."""
|
||||||
try:
|
try:
|
||||||
data = json.loads(value)
|
data = json.loads(value)
|
||||||
self.mcp_config = models.MCPConfiguration.from_dict(data)
|
self.mcp_config = MCPConfiguration.from_dict(data)
|
||||||
return OK
|
return OK
|
||||||
except (json.JSONDecodeError, ValueError, TypeError, KeyError, AttributeError) as e:
|
except (json.JSONDecodeError, ValueError, TypeError, KeyError, AttributeError) as e:
|
||||||
return Result(data=None, errors=[ErrorInfo(
|
return Result(data=None, errors=[ErrorInfo(
|
||||||
@@ -1778,7 +1784,7 @@ class AppController:
|
|||||||
new_files.append(old_files[p])
|
new_files.append(old_files[p])
|
||||||
else:
|
else:
|
||||||
from src import models
|
from src import models
|
||||||
new_files.append(models.FileItem(path=p, injected_at=now))
|
new_files.append(FileItem(path=p, injected_at=now))
|
||||||
self.files = new_files
|
self.files = new_files
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -1998,12 +2004,12 @@ class AppController:
|
|||||||
raw_paths = self.project.get("files", {}).get("paths", [])
|
raw_paths = self.project.get("files", {}).get("paths", [])
|
||||||
self.files = []
|
self.files = []
|
||||||
for p in raw_paths:
|
for p in raw_paths:
|
||||||
if isinstance(p, models.FileItem):
|
if isinstance(p, FileItem):
|
||||||
self.files.append(p)
|
self.files.append(p)
|
||||||
elif isinstance(p, dict):
|
elif isinstance(p, dict):
|
||||||
self.files.append(models.FileItem.from_dict(p))
|
self.files.append(FileItem.from_dict(p))
|
||||||
else:
|
else:
|
||||||
self.files.append(models.FileItem(path=str(p)))
|
self.files.append(FileItem(path=str(p)))
|
||||||
self.screenshots = list(self.project.get("screenshots", {}).get("paths", []))
|
self.screenshots = list(self.project.get("screenshots", {}).get("paths", []))
|
||||||
disc_sec = self.project.get("discussion", {})
|
disc_sec = self.project.get("discussion", {})
|
||||||
self.disc_roles = list(disc_sec.get("roles", ["User", "AI", "Vendor API", "System", "Reasoning", "Context"]))
|
self.disc_roles = list(disc_sec.get("roles", ["User", "AI", "Vendor API", "System", "Reasoning", "Context"]))
|
||||||
@@ -2040,14 +2046,14 @@ class AppController:
|
|||||||
mcp_p = Path(mcp_path)
|
mcp_p = Path(mcp_path)
|
||||||
if not mcp_p.is_absolute() and self.active_project_path:
|
if not mcp_p.is_absolute() and self.active_project_path:
|
||||||
mcp_p = Path(self.active_project_path).parent / mcp_path
|
mcp_p = Path(self.active_project_path).parent / mcp_path
|
||||||
if mcp_p.exists(): self.mcp_config = models.load_mcp_config(str(mcp_p))
|
if mcp_p.exists(): self.mcp_config = load_mcp_config(str(mcp_p))
|
||||||
else: self.mcp_config = models.MCPConfiguration()
|
else: self.mcp_config = MCPConfiguration()
|
||||||
else:
|
else:
|
||||||
self.mcp_config = models.MCPConfiguration()
|
self.mcp_config = MCPConfiguration()
|
||||||
|
|
||||||
rag_data = self.config.get('rag')
|
rag_data = self.config.get('rag')
|
||||||
if rag_data: self.rag_config = models.RAGConfig.from_dict(rag_data)
|
if rag_data: self.rag_config = RAGConfig.from_dict(rag_data)
|
||||||
else: self.rag_config = models.RAGConfig()
|
else: self.rag_config = RAGConfig()
|
||||||
|
|
||||||
self.rag_engine = None
|
self.rag_engine = None
|
||||||
if self.rag_config.enabled: self._sync_rag_engine()
|
if self.rag_config.enabled: self._sync_rag_engine()
|
||||||
@@ -2145,8 +2151,8 @@ class AppController:
|
|||||||
try:
|
try:
|
||||||
tickets = []
|
tickets = []
|
||||||
for t_data in at_data.get("tickets", []):
|
for t_data in at_data.get("tickets", []):
|
||||||
tickets.append(models.Ticket(**t_data))
|
tickets.append(Ticket(**t_data))
|
||||||
track = models.Track(
|
track = Track(
|
||||||
id=at_data.get("id"),
|
id=at_data.get("id"),
|
||||||
description=at_data.get("description"),
|
description=at_data.get("description"),
|
||||||
tickets=tickets
|
tickets=tickets
|
||||||
@@ -2543,7 +2549,7 @@ class AppController:
|
|||||||
file_path = os.path.relpath(file_path, self.active_project_root)
|
file_path = os.path.relpath(file_path, self.active_project_root)
|
||||||
existing = next((f for f in self.files if f.path == file_path), None)
|
existing = next((f for f in self.files if f.path == file_path), None)
|
||||||
if not existing:
|
if not existing:
|
||||||
item = models.FileItem(path=file_path)
|
item = FileItem(path=file_path)
|
||||||
self.files.append(item)
|
self.files.append(item)
|
||||||
self._refresh_from_project()
|
self._refresh_from_project()
|
||||||
|
|
||||||
@@ -3232,19 +3238,19 @@ class AppController:
|
|||||||
raw_paths = self.project.get("files", {}).get("paths", [])
|
raw_paths = self.project.get("files", {}).get("paths", [])
|
||||||
self.files = []
|
self.files = []
|
||||||
for p in raw_paths:
|
for p in raw_paths:
|
||||||
if isinstance(p, models.FileItem):
|
if isinstance(p, FileItem):
|
||||||
self.files.append(p)
|
self.files.append(p)
|
||||||
elif isinstance(p, dict):
|
elif isinstance(p, dict):
|
||||||
self.files.append(models.FileItem.from_dict(p))
|
self.files.append(FileItem.from_dict(p))
|
||||||
else:
|
else:
|
||||||
self.files.append(models.FileItem(path=str(p)))
|
self.files.append(FileItem(path=str(p)))
|
||||||
import copy
|
import copy
|
||||||
self.context_files = []
|
self.context_files = []
|
||||||
for f in self.files:
|
for f in self.files:
|
||||||
if isinstance(f, models.FileItem):
|
if isinstance(f, FileItem):
|
||||||
fi = copy.deepcopy(f)
|
fi = copy.deepcopy(f)
|
||||||
else:
|
else:
|
||||||
fi = models.FileItem(path=str(f))
|
fi = FileItem(path=str(f))
|
||||||
self.context_files.append(fi)
|
self.context_files.append(fi)
|
||||||
if hasattr(self, "_app") and self._app is not None:
|
if hasattr(self, "_app") and self._app is not None:
|
||||||
self._app.ui_selected_context_files = {f.path for f in self.context_files if f.auto_aggregate}
|
self._app.ui_selected_context_files = {f.path for f in self.context_files if f.auto_aggregate}
|
||||||
@@ -3287,7 +3293,7 @@ class AppController:
|
|||||||
if result.ok:
|
if result.ok:
|
||||||
self.active_track = result.data
|
self.active_track = result.data
|
||||||
raw_tickets = at_data.get("tickets", [])
|
raw_tickets = at_data.get("tickets", [])
|
||||||
self.active_tickets = [models.Ticket.from_dict(t) if isinstance(t, dict) else t for t in raw_tickets]
|
self.active_tickets = [Ticket.from_dict(t) if isinstance(t, dict) else t for t in raw_tickets]
|
||||||
else:
|
else:
|
||||||
err = result.errors[0]
|
err = result.errors[0]
|
||||||
self._last_request_errors.append(("active_track_deserialize", err))
|
self._last_request_errors.append(("active_track_deserialize", err))
|
||||||
@@ -3320,9 +3326,9 @@ class AppController:
|
|||||||
ai_client.set_bias_profile(self.ui_active_bias_profile)
|
ai_client.set_bias_profile(self.ui_active_bias_profile)
|
||||||
raw_presets = proj.get("view_presets", [])
|
raw_presets = proj.get("view_presets", [])
|
||||||
if isinstance(raw_presets, dict):
|
if isinstance(raw_presets, dict):
|
||||||
self.view_presets = [models.NamedViewPreset.from_dict({"name": name, **data}) for name, data in raw_presets.items()]
|
self.view_presets = [NamedViewPreset.from_dict({"name": name, **data}) for name, data in raw_presets.items()]
|
||||||
else:
|
else:
|
||||||
self.view_presets = [models.NamedViewPreset.from_dict(p) for p in raw_presets if isinstance(p, dict)]
|
self.view_presets = [NamedViewPreset.from_dict(p) for p in raw_presets if isinstance(p, dict)]
|
||||||
if self.rag_config and self.rag_config.enabled:
|
if self.rag_config and self.rag_config.enabled:
|
||||||
self._rebuild_rag_index()
|
self._rebuild_rag_index()
|
||||||
|
|
||||||
@@ -3396,11 +3402,11 @@ class AppController:
|
|||||||
summarize._summary_cache.clear()
|
summarize._summary_cache.clear()
|
||||||
self._push_mma_state_update()
|
self._push_mma_state_update()
|
||||||
|
|
||||||
def save_context_preset(self, preset: models.ContextPreset) -> None:
|
def save_context_preset(self, preset: ContextPreset) -> None:
|
||||||
self.context_preset_manager.save_preset(self.project, preset)
|
self.context_preset_manager.save_preset(self.project, preset)
|
||||||
self._save_active_project()
|
self._save_active_project()
|
||||||
|
|
||||||
def load_context_preset(self, name: str) -> models.ContextPreset:
|
def load_context_preset(self, name: str) -> ContextPreset:
|
||||||
presets_result = self.context_preset_manager.load_all(self.project)
|
presets_result = self.context_preset_manager.load_all(self.project)
|
||||||
if not presets_result.ok:
|
if not presets_result.ok:
|
||||||
raise RuntimeError(f"Failed to load context presets: {presets_result.errors}")
|
raise RuntimeError(f"Failed to load context presets: {presets_result.errors}")
|
||||||
@@ -3413,7 +3419,7 @@ class AppController:
|
|||||||
import copy
|
import copy
|
||||||
self.context_files = []
|
self.context_files = []
|
||||||
for f in preset.files:
|
for f in preset.files:
|
||||||
fi = models.FileItem(path=f.path, view_mode=f.view_mode)
|
fi = FileItem(path=f.path, view_mode=f.view_mode)
|
||||||
fi.custom_slices = copy.deepcopy(f.custom_slices)
|
fi.custom_slices = copy.deepcopy(f.custom_slices)
|
||||||
fi.ast_mask = copy.deepcopy(f.ast_mask)
|
fi.ast_mask = copy.deepcopy(f.ast_mask)
|
||||||
fi.ast_signatures = getattr(f, 'ast_signatures', False)
|
fi.ast_signatures = getattr(f, 'ast_signatures', False)
|
||||||
@@ -3648,7 +3654,7 @@ class AppController:
|
|||||||
"""
|
"""
|
||||||
if not name or not name.strip():
|
if not name or not name.strip():
|
||||||
raise ValueError("Preset name cannot be empty or whitespace.")
|
raise ValueError("Preset name cannot be empty or whitespace.")
|
||||||
preset = models.Preset(
|
preset = Preset(
|
||||||
name=name,
|
name=name,
|
||||||
system_prompt=content
|
system_prompt=content
|
||||||
)
|
)
|
||||||
@@ -3666,7 +3672,7 @@ class AppController:
|
|||||||
"""
|
"""
|
||||||
[C: src/gui_2.py:App._render_tool_preset_manager_content]
|
[C: src/gui_2.py:App._render_tool_preset_manager_content]
|
||||||
"""
|
"""
|
||||||
preset = models.ToolPreset(name=name, categories=categories)
|
preset = ToolPreset(name=name, categories=categories)
|
||||||
self.tool_preset_manager.save_preset(preset, scope)
|
self.tool_preset_manager.save_preset(preset, scope)
|
||||||
self.tool_presets = self.tool_preset_manager.load_all_presets()
|
self.tool_presets = self.tool_preset_manager.load_all_presets()
|
||||||
|
|
||||||
@@ -3677,7 +3683,7 @@ class AppController:
|
|||||||
self.tool_preset_manager.delete_preset(name, scope)
|
self.tool_preset_manager.delete_preset(name, scope)
|
||||||
self.tool_presets = self.tool_preset_manager.load_all_presets()
|
self.tool_presets = self.tool_preset_manager.load_all_presets()
|
||||||
|
|
||||||
def _cb_save_bias_profile(self, profile: models.BiasProfile, scope: str = "project"):
|
def _cb_save_bias_profile(self, profile: BiasProfile, scope: str = "project"):
|
||||||
"""
|
"""
|
||||||
[C: src/gui_2.py:App._render_tool_preset_manager_content]
|
[C: src/gui_2.py:App._render_tool_preset_manager_content]
|
||||||
"""
|
"""
|
||||||
@@ -3688,7 +3694,7 @@ class AppController:
|
|||||||
self.tool_preset_manager.delete_bias_profile(name, scope)
|
self.tool_preset_manager.delete_bias_profile(name, scope)
|
||||||
self.bias_profiles = self.tool_preset_manager.load_all_bias_profiles()
|
self.bias_profiles = self.tool_preset_manager.load_all_bias_profiles()
|
||||||
|
|
||||||
def _cb_save_persona(self, persona: models.Persona, scope: str = "project") -> None:
|
def _cb_save_persona(self, persona: Persona, scope: str = "project") -> None:
|
||||||
"""
|
"""
|
||||||
[C: src/gui_2.py:App._render_persona_editor_window]
|
[C: src/gui_2.py:App._render_persona_editor_window]
|
||||||
"""
|
"""
|
||||||
@@ -3702,11 +3708,11 @@ class AppController:
|
|||||||
self.persona_manager.delete_persona(name, scope)
|
self.persona_manager.delete_persona(name, scope)
|
||||||
self.personas = self.persona_manager.load_all()
|
self.personas = self.persona_manager.load_all()
|
||||||
|
|
||||||
def _cb_save_view_preset(self, name: str, f_item: models.FileItem) -> None:
|
def _cb_save_view_preset(self, name: str, f_item: FileItem) -> None:
|
||||||
"""
|
"""
|
||||||
[C: src/gui_2.py:App._render_context_files_table, tests/test_view_presets.py:test_save_view_preset]
|
[C: src/gui_2.py:App._render_context_files_table, tests/test_view_presets.py:test_save_view_preset]
|
||||||
"""
|
"""
|
||||||
preset = models.NamedViewPreset(
|
preset = NamedViewPreset(
|
||||||
name=name,
|
name=name,
|
||||||
view_mode=f_item.view_mode,
|
view_mode=f_item.view_mode,
|
||||||
ast_mask=copy.deepcopy(f_item.ast_mask) if hasattr(f_item, "ast_mask") else {},
|
ast_mask=copy.deepcopy(f_item.ast_mask) if hasattr(f_item, "ast_mask") else {},
|
||||||
@@ -3720,7 +3726,7 @@ class AppController:
|
|||||||
self.view_presets.append(preset)
|
self.view_presets.append(preset)
|
||||||
self._flush_to_project()
|
self._flush_to_project()
|
||||||
|
|
||||||
def _cb_apply_view_preset(self, name: str, f_item: models.FileItem) -> None:
|
def _cb_apply_view_preset(self, name: str, f_item: FileItem) -> None:
|
||||||
"""
|
"""
|
||||||
[C: src/gui_2.py:App._render_context_files_table, tests/test_view_presets.py:test_apply_view_preset]
|
[C: src/gui_2.py:App._render_context_files_table, tests/test_view_presets.py:test_apply_view_preset]
|
||||||
"""
|
"""
|
||||||
@@ -3776,7 +3782,7 @@ class AppController:
|
|||||||
self.discussion_sent_system_prompt = disc_data.get("sent_system_prompt", "")
|
self.discussion_sent_system_prompt = disc_data.get("sent_system_prompt", "")
|
||||||
if "context_snapshot" in disc_data:
|
if "context_snapshot" in disc_data:
|
||||||
snapshot_data = disc_data["context_snapshot"]
|
snapshot_data = disc_data["context_snapshot"]
|
||||||
self.context_files = [models.FileItem.from_dict(f) if isinstance(f, dict) else models.FileItem(path=str(f)) for f in snapshot_data]
|
self.context_files = [FileItem.from_dict(f) if isinstance(f, dict) else FileItem(path=str(f)) for f in snapshot_data]
|
||||||
if self._app:
|
if self._app:
|
||||||
self._app.ui_selected_context_files = {f.path for f in self.context_files if f.auto_aggregate}
|
self._app.ui_selected_context_files = {f.path for f in self.context_files if f.auto_aggregate}
|
||||||
self.ai_status = f"discussion: {name}"
|
self.ai_status = f"discussion: {name}"
|
||||||
@@ -3913,8 +3919,8 @@ class AppController:
|
|||||||
# unsynced forever (test_rag_phase4_final_verify regression on
|
# unsynced forever (test_rag_phase4_final_verify regression on
|
||||||
# 2026-06-10).
|
# 2026-06-10).
|
||||||
self.rag_engine = None
|
self.rag_engine = None
|
||||||
from src import models as _rag_models
|
from src.mcp_client import RAGConfig
|
||||||
self.rag_config = _rag_models.RAGConfig()
|
self.rag_config = RAGConfig()
|
||||||
self.rag_status = 'idle'
|
self.rag_status = 'idle'
|
||||||
self._rag_sync_token = 0
|
self._rag_sync_token = 0
|
||||||
self._rag_sync_dirty = False
|
self._rag_sync_dirty = False
|
||||||
@@ -4720,7 +4726,7 @@ class AppController:
|
|||||||
"""Phase 6 Group 6.7: topological sort with Result propagation.
|
"""Phase 6 Group 6.7: topological sort with Result propagation.
|
||||||
On ValueError: fall back to raw_tickets (preserves existing behavior)."""
|
On ValueError: fall back to raw_tickets (preserves existing behavior)."""
|
||||||
try:
|
try:
|
||||||
normalized = [models.Ticket.from_dict(t) if isinstance(t, dict) else t for t in raw_tickets]
|
normalized = [Ticket.from_dict(t) if isinstance(t, dict) else t for t in raw_tickets]
|
||||||
sorted_tickets_data = conductor_tech_lead.topological_sort(normalized)
|
sorted_tickets_data = conductor_tech_lead.topological_sort(normalized)
|
||||||
return Result(data=sorted_tickets_data)
|
return Result(data=sorted_tickets_data)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
@@ -4773,7 +4779,7 @@ class AppController:
|
|||||||
# 3. Create Track and Ticket objects
|
# 3. Create Track and Ticket objects
|
||||||
tickets = []
|
tickets = []
|
||||||
for t_data in sorted_tickets_data:
|
for t_data in sorted_tickets_data:
|
||||||
ticket = models.Ticket(
|
ticket = Ticket(
|
||||||
id=t_data["id"],
|
id=t_data["id"],
|
||||||
description=t_data.get("description") or t_data.get("goal", "No description"),
|
description=t_data.get("description") or t_data.get("goal", "No description"),
|
||||||
status=t_data.get("status", "todo"),
|
status=t_data.get("status", "todo"),
|
||||||
@@ -4783,10 +4789,10 @@ class AppController:
|
|||||||
)
|
)
|
||||||
tickets.append(ticket)
|
tickets.append(ticket)
|
||||||
track_id = f"track_{uuid.uuid5(uuid.NAMESPACE_DNS, f'{self.active_project_path}_{title}').hex[:12]}"
|
track_id = f"track_{uuid.uuid5(uuid.NAMESPACE_DNS, f'{self.active_project_path}_{title}').hex[:12]}"
|
||||||
track = models.Track(id=track_id, description=title, tickets=tickets)
|
track = Track(id=track_id, description=title, tickets=tickets)
|
||||||
# Initialize track state in the filesystem
|
# Initialize track state in the filesystem
|
||||||
meta = models.Metadata(id=track_id, name=title, status="todo", created_at=datetime.now(), updated_at=datetime.now())
|
meta = models.Metadata(id=track_id, name=title, status="todo", created_at=datetime.now(), updated_at=datetime.now())
|
||||||
state = models.TrackState(metadata=meta, discussion=[], tasks=tickets)
|
state = TrackState(metadata=meta, discussion=[], tasks=tickets)
|
||||||
project_manager.save_track_state(track_id, state, self.active_project_root)
|
project_manager.save_track_state(track_id, state, self.active_project_root)
|
||||||
# Add to memory and notify UI
|
# Add to memory and notify UI
|
||||||
self.tracks.append({"id": track_id, "title": title, "status": "todo"})
|
self.tracks.append({"id": track_id, "title": title, "status": "todo"})
|
||||||
@@ -5031,10 +5037,10 @@ class AppController:
|
|||||||
tickets = []
|
tickets = []
|
||||||
for t in state.tasks:
|
for t in state.tasks:
|
||||||
if isinstance(t, dict):
|
if isinstance(t, dict):
|
||||||
tickets.append(models.Ticket(**t))
|
tickets.append(Ticket(**t))
|
||||||
else:
|
else:
|
||||||
tickets.append(t)
|
tickets.append(t)
|
||||||
self.active_track = models.Track(
|
self.active_track = Track(
|
||||||
id=state.metadata.id,
|
id=state.metadata.id,
|
||||||
description=state.metadata.name,
|
description=state.metadata.name,
|
||||||
tickets=tickets
|
tickets=tickets
|
||||||
@@ -5084,7 +5090,7 @@ class AppController:
|
|||||||
track = self.active_track
|
track = self.active_track
|
||||||
if track is None: return OK
|
if track is None: return OK
|
||||||
new_tickets = [
|
new_tickets = [
|
||||||
models.Ticket(
|
Ticket(
|
||||||
id=t.id,
|
id=t.id,
|
||||||
description=t.description,
|
description=t.description,
|
||||||
status=t.status,
|
status=t.status,
|
||||||
@@ -5094,7 +5100,7 @@ class AppController:
|
|||||||
for t in self.active_tickets
|
for t in self.active_tickets
|
||||||
]
|
]
|
||||||
track.tickets = new_tickets
|
track.tickets = new_tickets
|
||||||
state = models.TrackState(metadata=track, tasks=list(new_tickets))
|
state = TrackState(metadata=track, tasks=list(new_tickets))
|
||||||
project_manager.save_track_state(track.id, state, self.active_project_root)
|
project_manager.save_track_state(track.id, state, self.active_project_root)
|
||||||
return OK
|
return OK
|
||||||
except (OSError, IOError, ValueError, TypeError, KeyError, AttributeError) as e:
|
except (OSError, IOError, ValueError, TypeError, KeyError, AttributeError) as e:
|
||||||
@@ -5121,7 +5127,7 @@ class AppController:
|
|||||||
beads_result = self._load_beads_from_path_result(Path(base))
|
beads_result = self._load_beads_from_path_result(Path(base))
|
||||||
if beads_result.ok:
|
if beads_result.ok:
|
||||||
for bead in beads_result.data:
|
for bead in beads_result.data:
|
||||||
self.active_tickets.append(models.Ticket(
|
self.active_tickets.append(Ticket(
|
||||||
id=bead.id,
|
id=bead.id,
|
||||||
description=bead.description or "",
|
description=bead.description or "",
|
||||||
status=bead.status,
|
status=bead.status,
|
||||||
|
|||||||
+30
-30
@@ -357,7 +357,7 @@ class App:
|
|||||||
self.controller._predefined_callbacks['delete_context_preset'] = self.delete_context_preset
|
self.controller._predefined_callbacks['delete_context_preset'] = self.delete_context_preset
|
||||||
self.controller._predefined_callbacks['set_ui_file_paths'] = lambda p: setattr(self, 'ui_file_paths', p)
|
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)
|
self.controller._predefined_callbacks['set_ui_screenshot_paths'] = lambda p: setattr(self, 'ui_screenshot_paths', p)
|
||||||
self.controller._predefined_callbacks['set_context_files_for_test'] = lambda files: setattr(self, 'context_files', [models.FileItem(path=f) for f in files])
|
self.controller._predefined_callbacks['set_context_files_for_test'] = lambda files: setattr(self, 'context_files', [FileItem(path=f) for f in files])
|
||||||
self.controller._predefined_callbacks['set_screenshots_for_test'] = lambda ss: setattr(self, 'screenshots', ss)
|
self.controller._predefined_callbacks['set_screenshots_for_test'] = lambda ss: setattr(self, 'screenshots', ss)
|
||||||
self.controller._predefined_callbacks['_toggle_command_palette'] = self._toggle_command_palette
|
self.controller._predefined_callbacks['_toggle_command_palette'] = self._toggle_command_palette
|
||||||
self.controller._gettable_fields['show_command_palette'] = 'show_command_palette'
|
self.controller._gettable_fields['show_command_palette'] = 'show_command_palette'
|
||||||
@@ -373,8 +373,8 @@ class App:
|
|||||||
msk = copy.deepcopy(f.ast_mask)
|
msk = copy.deepcopy(f.ast_mask)
|
||||||
sig = f.ast_signatures
|
sig = f.ast_signatures
|
||||||
dfn = f.ast_definitions
|
dfn = f.ast_definitions
|
||||||
preset_files.append(models.ContextFileEntry(path=p, view_mode=vm, custom_slices=slc, ast_mask=msk, ast_signatures=sig, ast_definitions=dfn))
|
preset_files.append(ContextFileEntry(path=p, view_mode=vm, custom_slices=slc, ast_mask=msk, ast_signatures=sig, ast_definitions=dfn))
|
||||||
preset = models.ContextPreset(name=name, files=preset_files, screenshots=list(self.screenshots))
|
preset = ContextPreset(name=name, files=preset_files, screenshots=list(self.screenshots))
|
||||||
self.controller.save_context_preset(preset)
|
self.controller.save_context_preset(preset)
|
||||||
self.ui_new_context_preset_name = ""
|
self.ui_new_context_preset_name = ""
|
||||||
self.show_missing_files_modal = False
|
self.show_missing_files_modal = False
|
||||||
@@ -541,12 +541,12 @@ class App:
|
|||||||
|
|
||||||
def _set_context_files(self, paths: list[str]) -> None:
|
def _set_context_files(self, paths: list[str]) -> None:
|
||||||
from src import models
|
from src import models
|
||||||
self.context_files = [models.FileItem(path=p) for p in paths]
|
self.context_files = [FileItem(path=p) for p in paths]
|
||||||
self.controller.context_files = self.context_files
|
self.controller.context_files = self.context_files
|
||||||
|
|
||||||
def _simulate_save_preset(self, name: str) -> None:
|
def _simulate_save_preset(self, name: str) -> None:
|
||||||
from src import models
|
from src import models
|
||||||
item = models.FileItem(path='test.py')
|
item = FileItem(path='test.py')
|
||||||
self.files = [item]
|
self.files = [item]
|
||||||
self.context_files = [item]
|
self.context_files = [item]
|
||||||
self.screenshots = ['test.png']
|
self.screenshots = ['test.png']
|
||||||
@@ -865,20 +865,20 @@ class App:
|
|||||||
from src import models
|
from src import models
|
||||||
self.files = []
|
self.files = []
|
||||||
for f in snapshot.files:
|
for f in snapshot.files:
|
||||||
if isinstance(f, dict): self.files.append(models.FileItem.from_dict(f))
|
if isinstance(f, dict): self.files.append(FileItem.from_dict(f))
|
||||||
else: self.files.append(models.FileItem(path=str(f)))
|
else: self.files.append(FileItem(path=str(f)))
|
||||||
|
|
||||||
self.context_files = []
|
self.context_files = []
|
||||||
for f in snapshot.context_files:
|
for f in snapshot.context_files:
|
||||||
if isinstance(f, dict): self.context_files.append(models.FileItem.from_dict(f))
|
if isinstance(f, dict): self.context_files.append(FileItem.from_dict(f))
|
||||||
else: self.context_files.append(models.FileItem(path=str(f)))
|
else: self.context_files.append(FileItem(path=str(f)))
|
||||||
|
|
||||||
self.screenshots = list(snapshot.screenshots)
|
self.screenshots = list(snapshot.screenshots)
|
||||||
self._last_ui_snapshot = snapshot # Update last snapshot to avoid immediate re-push
|
self._last_ui_snapshot = snapshot # Update last snapshot to avoid immediate re-push
|
||||||
finally:
|
finally:
|
||||||
self._is_applying_snapshot = False # ?? TODO(Ed): Whats the point of this??
|
self._is_applying_snapshot = False # ?? TODO(Ed): Whats the point of this??
|
||||||
|
|
||||||
def _capture_workspace_profile(self, name: str) -> models.WorkspaceProfile:
|
def _capture_workspace_profile(self, name: str) -> WorkspaceProfile:
|
||||||
"""Serializes the current window visibility states, popped-out panel layouts, and
|
"""Serializes the current window visibility states, popped-out panel layouts, and
|
||||||
ImGui INI configurations into a WorkspaceProfile object.
|
ImGui INI configurations into a WorkspaceProfile object.
|
||||||
SSDL Shape: `[Q:ui_states] -> [B:ini_ready] -> [T:profile]`
|
SSDL Shape: `[Q:ui_states] -> [B:ini_ready] -> [T:profile]`
|
||||||
@@ -908,14 +908,14 @@ class App:
|
|||||||
"ui_separate_external_tools": getattr(self, "ui_separate_external_tools", False),
|
"ui_separate_external_tools": getattr(self, "ui_separate_external_tools", False),
|
||||||
"ui_discussion_split_h": getattr(self, "ui_discussion_split_h", 300.0),
|
"ui_discussion_split_h": getattr(self, "ui_discussion_split_h", 300.0),
|
||||||
}
|
}
|
||||||
return models.WorkspaceProfile(
|
return WorkspaceProfile(
|
||||||
name = name,
|
name = name,
|
||||||
ini_content = ini,
|
ini_content = ini,
|
||||||
show_windows = copy.deepcopy(self.show_windows),
|
show_windows = copy.deepcopy(self.show_windows),
|
||||||
panel_states = panel_states
|
panel_states = panel_states
|
||||||
)
|
)
|
||||||
|
|
||||||
def _apply_workspace_profile(self, profile: models.WorkspaceProfile):
|
def _apply_workspace_profile(self, profile: WorkspaceProfile):
|
||||||
"""Restores the window docking layout and popped-out panel visibility states
|
"""Restores the window docking layout and popped-out panel visibility states
|
||||||
from a saved WorkspaceProfile.
|
from a saved WorkspaceProfile.
|
||||||
SSDL Shape: `[I:load_ini] -> [S:ui_states]`
|
SSDL Shape: `[I:load_ini] -> [S:ui_states]`
|
||||||
@@ -975,7 +975,7 @@ class App:
|
|||||||
import copy
|
import copy
|
||||||
self.context_files = []
|
self.context_files = []
|
||||||
for f in preset.files:
|
for f in preset.files:
|
||||||
fi = models.FileItem(path=f.path, view_mode=f.view_mode)
|
fi = FileItem(path=f.path, view_mode=f.view_mode)
|
||||||
fi.custom_slices = copy.deepcopy(f.custom_slices)
|
fi.custom_slices = copy.deepcopy(f.custom_slices)
|
||||||
fi.ast_mask = copy.deepcopy(f.ast_mask)
|
fi.ast_mask = copy.deepcopy(f.ast_mask)
|
||||||
fi.ast_signatures = getattr(f, 'ast_signatures', False)
|
fi.ast_signatures = getattr(f, 'ast_signatures', False)
|
||||||
@@ -1007,7 +1007,7 @@ class App:
|
|||||||
new_files.append(old_files[p])
|
new_files.append(old_files[p])
|
||||||
else:
|
else:
|
||||||
from src import models
|
from src import models
|
||||||
new_files.append(models.FileItem(path=p, injected_at=now))
|
new_files.append(FileItem(path=p, injected_at=now))
|
||||||
self.files = new_files
|
self.files = new_files
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -1259,7 +1259,7 @@ class App:
|
|||||||
self.init_state()
|
self.init_state()
|
||||||
self.ai_status = 'paths applied and session reset'
|
self.ai_status = 'paths applied and session reset'
|
||||||
|
|
||||||
def _populate_auto_slices(self, f_item: models.FileItem) -> None:
|
def _populate_auto_slices(self, f_item: FileItem) -> None:
|
||||||
import re
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import os
|
import os
|
||||||
@@ -3291,11 +3291,11 @@ def render_tool_preset_manager_content(app: App, is_embedded: bool = False) -> N
|
|||||||
if tool: curr_cat_tools.remove(tool)
|
if tool: curr_cat_tools.remove(tool)
|
||||||
imgui.same_line();
|
imgui.same_line();
|
||||||
if imgui.radio_button(f"Auto##{cat_name}_{tool_name}", mode == "auto"):
|
if imgui.radio_button(f"Auto##{cat_name}_{tool_name}", mode == "auto"):
|
||||||
if not tool: tool = models.Tool(name=tool_name, approval="auto"); curr_cat_tools.append(tool)
|
if not tool: tool = Tool(name=tool_name, approval="auto"); curr_cat_tools.append(tool)
|
||||||
else: tool.approval = "auto"
|
else: tool.approval = "auto"
|
||||||
imgui.same_line();
|
imgui.same_line();
|
||||||
if imgui.radio_button(f"Ask##{cat_name}_{tool_name}", mode == "ask"):
|
if imgui.radio_button(f"Ask##{cat_name}_{tool_name}", mode == "ask"):
|
||||||
if not tool: tool = models.Tool(name=tool_name, approval="ask"); curr_cat_tools.append(tool)
|
if not tool: tool = Tool(name=tool_name, approval="ask"); curr_cat_tools.append(tool)
|
||||||
else: tool.approval = "ask"
|
else: tool.approval = "ask"
|
||||||
imgui.tree_pop()
|
imgui.tree_pop()
|
||||||
if app._bias_list_open:
|
if app._bias_list_open:
|
||||||
@@ -3694,7 +3694,7 @@ def render_files_and_media(app: App) -> None:
|
|||||||
if imgui.button(f"+##add_f_{i}"):
|
if imgui.button(f"+##add_f_{i}"):
|
||||||
if not in_context:
|
if not in_context:
|
||||||
from src import models
|
from src import models
|
||||||
new_item = models.FileItem(path=fpath)
|
new_item = FileItem(path=fpath)
|
||||||
app.context_files.append(new_item)
|
app.context_files.append(new_item)
|
||||||
app._populate_auto_slices(new_item)
|
app._populate_auto_slices(new_item)
|
||||||
|
|
||||||
@@ -3718,7 +3718,7 @@ def render_files_and_media(app: App) -> None:
|
|||||||
r = hide_tk_root(); paths = filedialog.askopenfilenames(); r.destroy()
|
r = hide_tk_root(); paths = filedialog.askopenfilenames(); r.destroy()
|
||||||
from src import models
|
from src import models
|
||||||
for p in paths:
|
for p in paths:
|
||||||
if p not in [f.path for f in app.files]: app.files.append(models.FileItem(path=p))
|
if p not in [f.path for f in app.files]: app.files.append(FileItem(path=p))
|
||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
if imgui.button("Add Directory"):
|
if imgui.button("Add Directory"):
|
||||||
r = hide_tk_root(); dirpath = filedialog.askdirectory(); r.destroy()
|
r = hide_tk_root(); dirpath = filedialog.askdirectory(); r.destroy()
|
||||||
@@ -3728,7 +3728,7 @@ def render_files_and_media(app: App) -> None:
|
|||||||
for fname in files:
|
for fname in files:
|
||||||
full = os.path.join(root, fname)
|
full = os.path.join(root, fname)
|
||||||
if full not in existing:
|
if full not in existing:
|
||||||
app.files.append(models.FileItem(path=full))
|
app.files.append(FileItem(path=full))
|
||||||
existing.add(full)
|
existing.add(full)
|
||||||
|
|
||||||
imgui.separator()
|
imgui.separator()
|
||||||
@@ -3852,7 +3852,7 @@ def render_add_context_files_modal(app: App) -> None:
|
|||||||
|
|
||||||
if imgui.button("Add Selected", imgui.ImVec2(120, 0)):
|
if imgui.button("Add Selected", imgui.ImVec2(120, 0)):
|
||||||
for fpath in app._ui_picker_selected:
|
for fpath in app._ui_picker_selected:
|
||||||
f_item = models.FileItem(path=fpath)
|
f_item = FileItem(path=fpath)
|
||||||
app.context_files.append(f_item)
|
app.context_files.append(f_item)
|
||||||
app._populate_auto_slices(f_item)
|
app._populate_auto_slices(f_item)
|
||||||
app._ui_picker_selected.clear()
|
app._ui_picker_selected.clear()
|
||||||
@@ -4369,8 +4369,8 @@ def render_context_presets(app: App) -> None:
|
|||||||
msk = copy.deepcopy(f.ast_mask)
|
msk = copy.deepcopy(f.ast_mask)
|
||||||
sig = f.ast_signatures
|
sig = f.ast_signatures
|
||||||
dfn = f.ast_definitions
|
dfn = f.ast_definitions
|
||||||
preset_files.append(models.ContextFileEntry(path=p, view_mode=vm, custom_slices=slc, ast_mask=msk, ast_signatures=sig, ast_definitions=dfn))
|
preset_files.append(ContextFileEntry(path=p, view_mode=vm, custom_slices=slc, ast_mask=msk, ast_signatures=sig, ast_definitions=dfn))
|
||||||
preset = models.ContextPreset(name=active, files=preset_files, screenshots=list(app.screenshots))
|
preset = ContextPreset(name=active, files=preset_files, screenshots=list(app.screenshots))
|
||||||
app.controller.save_context_preset(preset)
|
app.controller.save_context_preset(preset)
|
||||||
else:
|
else:
|
||||||
imgui.text_disabled("No active preset")
|
imgui.text_disabled("No active preset")
|
||||||
@@ -4409,8 +4409,8 @@ def render_context_presets(app: App) -> None:
|
|||||||
msk = copy.deepcopy(f.ast_mask)
|
msk = copy.deepcopy(f.ast_mask)
|
||||||
sig = f.ast_signatures
|
sig = f.ast_signatures
|
||||||
dfn = f.ast_definitions
|
dfn = f.ast_definitions
|
||||||
preset_files.append(models.ContextFileEntry(path=p, view_mode=vm, custom_slices=slc, ast_mask=msk, ast_signatures=sig, ast_definitions=dfn))
|
preset_files.append(ContextFileEntry(path=p, view_mode=vm, custom_slices=slc, ast_mask=msk, ast_signatures=sig, ast_definitions=dfn))
|
||||||
preset = models.ContextPreset(name=name, files=preset_files, screenshots=list(app.screenshots))
|
preset = ContextPreset(name=name, files=preset_files, screenshots=list(app.screenshots))
|
||||||
app.controller.save_context_preset(preset)
|
app.controller.save_context_preset(preset)
|
||||||
app.ui_new_context_preset_name = ""
|
app.ui_new_context_preset_name = ""
|
||||||
|
|
||||||
@@ -4544,8 +4544,8 @@ def render_context_modals(app: App) -> None:
|
|||||||
msk = copy.deepcopy(f.ast_mask)
|
msk = copy.deepcopy(f.ast_mask)
|
||||||
sig = f.ast_signatures
|
sig = f.ast_signatures
|
||||||
dfn = f.ast_definitions
|
dfn = f.ast_definitions
|
||||||
preset_files.append(models.ContextFileEntry(path=p, view_mode=vm, custom_slices=slc, ast_mask=msk, ast_signatures=sig, ast_definitions=dfn))
|
preset_files.append(ContextFileEntry(path=p, view_mode=vm, custom_slices=slc, ast_mask=msk, ast_signatures=sig, ast_definitions=dfn))
|
||||||
preset = models.ContextPreset(name=name, files=preset_files, screenshots=list(app.screenshots))
|
preset = ContextPreset(name=name, files=preset_files, screenshots=list(app.screenshots))
|
||||||
app.controller.save_context_preset(preset)
|
app.controller.save_context_preset(preset)
|
||||||
app.ui_new_context_preset_name = ""
|
app.ui_new_context_preset_name = ""
|
||||||
imgui.close_current_popup()
|
imgui.close_current_popup()
|
||||||
@@ -7839,7 +7839,7 @@ def _handle_history_logic_result(app: "App") -> Result[bool]:
|
|||||||
def _render_persona_editor_save_result(app: "App") -> Result[bool]:
|
def _render_persona_editor_save_result(app: "App") -> Result[bool]:
|
||||||
"""Drain-aware variant of L3398 render_persona_editor_window Save button try/except.
|
"""Drain-aware variant of L3398 render_persona_editor_window Save button try/except.
|
||||||
|
|
||||||
Extracts the models.Persona(...) construction + app.controller._cb_save_persona
|
Extracts the Persona(...) construction + app.controller._cb_save_persona
|
||||||
try/except from the Save button handler in render_persona_editor_window into a
|
try/except from the Save button handler in render_persona_editor_window into a
|
||||||
Result-returning helper. On success, sets app.ai_status to "Saved: <name>"
|
Result-returning helper. On success, sets app.ai_status to "Saved: <name>"
|
||||||
and returns Result(data=True). On failure (any exception in Persona
|
and returns Result(data=True). On failure (any exception in Persona
|
||||||
@@ -7853,7 +7853,7 @@ def _render_persona_editor_save_result(app: "App") -> Result[bool]:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
import copy
|
import copy
|
||||||
persona = models.Persona(
|
persona = Persona(
|
||||||
name=app._editing_persona_name.strip(),
|
name=app._editing_persona_name.strip(),
|
||||||
system_prompt=app._editing_persona_system_prompt,
|
system_prompt=app._editing_persona_system_prompt,
|
||||||
tool_preset=app._editing_persona_tool_preset_id or None,
|
tool_preset=app._editing_persona_tool_preset_id or None,
|
||||||
@@ -8158,7 +8158,7 @@ def _render_tool_preset_bias_save_result(app: "App") -> Result[bool]:
|
|||||||
[C: src/gui_2.py:render_tool_preset_manager_content (L3163 legacy wrapper)]
|
[C: src/gui_2.py:render_tool_preset_manager_content (L3163 legacy wrapper)]
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
p = models.BiasProfile(
|
p = BiasProfile(
|
||||||
name=app._editing_bias_profile_name,
|
name=app._editing_bias_profile_name,
|
||||||
tool_weights=app._editing_bias_profile_tool_weights,
|
tool_weights=app._editing_bias_profile_tool_weights,
|
||||||
category_multipliers=app._editing_bias_profile_category_multipliers,
|
category_multipliers=app._editing_bias_profile_category_multipliers,
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ def load_project(path: Union[str, Path]) -> Metadata:
|
|||||||
# Deserialise FileItems in files.paths
|
# Deserialise FileItems in files.paths
|
||||||
if "files" in proj and "paths" in proj["files"]:
|
if "files" in proj and "paths" in proj["files"]:
|
||||||
from src import models
|
from src import models
|
||||||
proj["files"]["paths"] = [models.FileItem.from_dict(p) if isinstance(p, dict) else p for p in proj["files"]["paths"]]
|
proj["files"]["paths"] = [FileItem.from_dict(p) if isinstance(p, dict) else p for p in proj["files"]["paths"]]
|
||||||
hist_path = get_history_path(path)
|
hist_path = get_history_path(path)
|
||||||
if "discussion" in proj:
|
if "discussion" in proj:
|
||||||
disc = proj.pop("discussion")
|
disc = proj.pop("discussion")
|
||||||
|
|||||||
+5
-4
@@ -8,10 +8,11 @@ from dataclasses import dataclass, field, fields as dc_fields
|
|||||||
from typing import List, Dict, Any, Optional
|
from typing import List, Dict, Any, Optional
|
||||||
|
|
||||||
from src import ai_client
|
from src import ai_client
|
||||||
from src import models
|
|
||||||
from src import mcp_client
|
from src import mcp_client
|
||||||
from src.result_types import ErrorInfo, ErrorKind, NilRAGState, Result
|
from src import models
|
||||||
from src.type_aliases import Metadata
|
from src.mcp_client import RAGConfig
|
||||||
|
from src.result_types import ErrorInfo, ErrorKind, NilRAGState, Result
|
||||||
|
from src.type_aliases import Metadata
|
||||||
|
|
||||||
from src.file_cache import ASTParser
|
from src.file_cache import ASTParser
|
||||||
|
|
||||||
@@ -121,7 +122,7 @@ def _parse_search_response_result(res_str: str) -> Result[List[Dict[str, Any]]]:
|
|||||||
|
|
||||||
|
|
||||||
class RAGEngine:
|
class RAGEngine:
|
||||||
def __init__(self, config: models.RAGConfig, base_dir: str = "."):
|
def __init__(self, config: RAGConfig, base_dir: str = "."):
|
||||||
self.config = copy.deepcopy(config)
|
self.config = copy.deepcopy(config)
|
||||||
self.base_dir = base_dir
|
self.base_dir = base_dir
|
||||||
self.client = None
|
self.client = None
|
||||||
|
|||||||
+1
-1
@@ -146,7 +146,7 @@ class HistoryMessage:
|
|||||||
History: TypeAlias = list[HistoryMessage]
|
History: TypeAlias = list[HistoryMessage]
|
||||||
|
|
||||||
|
|
||||||
FileItem: TypeAlias = "models.FileItem"
|
FileItem: TypeAlias = "FileItem"
|
||||||
FileItems: TypeAlias = list[FileItem]
|
FileItems: TypeAlias = list[FileItem]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ def test_ast_inspector_line_range_parsing():
|
|||||||
app = MagicMock(spec=App)
|
app = MagicMock(spec=App)
|
||||||
app._show_ast_inspector = True
|
app._show_ast_inspector = True
|
||||||
app.show_structural_editor_modal = True
|
app.show_structural_editor_modal = True
|
||||||
app.ui_inspecting_ast_file = models.FileItem(path="test.py")
|
app.ui_inspecting_ast_file = FileItem(path="test.py")
|
||||||
app.ui_editing_slices_file = app.ui_inspecting_ast_file
|
app.ui_editing_slices_file = app.ui_inspecting_ast_file
|
||||||
app._cached_ast_file_path = ""
|
app._cached_ast_file_path = ""
|
||||||
app._cached_ast_nodes = []
|
app._cached_ast_nodes = []
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ def mock_app():
|
|||||||
return app
|
return app
|
||||||
|
|
||||||
def test_populate_auto_slices_basic(mock_app: App) -> None:
|
def test_populate_auto_slices_basic(mock_app: App) -> None:
|
||||||
f_item = models.FileItem(path="test.py")
|
f_item = FileItem(path="test.py")
|
||||||
mock_outline = "[Class] MyClass (Lines 1-10)\n[Method] my_method (Lines 2-5)\n[Func] top_func (Lines 12-15)"
|
mock_outline = "[Class] MyClass (Lines 1-10)\n[Method] my_method (Lines 2-5)\n[Func] top_func (Lines 12-15)"
|
||||||
|
|
||||||
with (
|
with (
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ async def test_external_mcp_real_process():
|
|||||||
|
|
||||||
# Use our mock script
|
# Use our mock script
|
||||||
mock_script = "scripts/mock_mcp_server.py"
|
mock_script = "scripts/mock_mcp_server.py"
|
||||||
config = models.MCPServerConfig(
|
config = MCPServerConfig(
|
||||||
name="real-mock",
|
name="real-mock",
|
||||||
command="python",
|
command="python",
|
||||||
args=[mock_script]
|
args=[mock_script]
|
||||||
@@ -36,7 +36,7 @@ async def test_get_tool_schemas_includes_external():
|
|||||||
await manager.stop_all()
|
await manager.stop_all()
|
||||||
|
|
||||||
mock_script = "scripts/mock_mcp_server.py"
|
mock_script = "scripts/mock_mcp_server.py"
|
||||||
config = models.MCPServerConfig(
|
config = MCPServerConfig(
|
||||||
name="test-server",
|
name="test-server",
|
||||||
command="python",
|
command="python",
|
||||||
args=[mock_script]
|
args=[mock_script]
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ def test_files_rendered_under_directory_grouping(app_instance):
|
|||||||
os.makedirs(sub, exist_ok=True)
|
os.makedirs(sub, exist_ok=True)
|
||||||
for p in [os.path.join(tmp, "a.py"), os.path.join(tmp, "b.py"), os.path.join(sub, "c.py")]:
|
for p in [os.path.join(tmp, "a.py"), os.path.join(tmp, "b.py"), os.path.join(sub, "c.py")]:
|
||||||
open(p, "w").close()
|
open(p, "w").close()
|
||||||
app_instance.files = [models.FileItem(path=os.path.join(tmp, "a.py")), models.FileItem(path=os.path.join(tmp, "b.py")), models.FileItem(path=os.path.join(sub, "c.py"))]
|
app_instance.files = [FileItem(path=os.path.join(tmp, "a.py")), FileItem(path=os.path.join(tmp, "b.py")), FileItem(path=os.path.join(sub, "c.py"))]
|
||||||
with patch("src.gui_2.imgui") as mock_imgui, patch("src.gui_2.imscope") as mock_imscope, patch("src.gui_2.filedialog") as mock_filedialog, patch("src.gui_2.hide_tk_root", return_value=MagicMock()):
|
with patch("src.gui_2.imgui") as mock_imgui, patch("src.gui_2.imscope") as mock_imscope, patch("src.gui_2.filedialog") as mock_filedialog, patch("src.gui_2.hide_tk_root", return_value=MagicMock()):
|
||||||
mock_imgui.collapsing_header.return_value = True
|
mock_imgui.collapsing_header.return_value = True
|
||||||
mock_imgui.TableFlags_ = type("T", (), {"resizable": 1, "borders": 2, "row_bg": 4})()
|
mock_imgui.TableFlags_ = type("T", (), {"resizable": 1, "borders": 2, "row_bg": 4})()
|
||||||
|
|||||||
@@ -715,7 +715,7 @@ def test_phase_4_l3398_render_persona_editor_save_result_success():
|
|||||||
L3398 _render_persona_editor_save_result returns Result.ok=True on success.
|
L3398 _render_persona_editor_save_result returns Result.ok=True on success.
|
||||||
|
|
||||||
The helper wraps the Save button try/except in render_persona_editor_window
|
The helper wraps the Save button try/except in render_persona_editor_window
|
||||||
(Persona creation: models.Persona(...) + _cb_save_persona). On success,
|
(Persona creation: Persona(...) + _cb_save_persona). On success,
|
||||||
sets app.ai_status to "Saved: <name>" and returns Result(data=True).
|
sets app.ai_status to "Saved: <name>" and returns Result(data=True).
|
||||||
"""
|
"""
|
||||||
from src import gui_2
|
from src import gui_2
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ def test_render_ticket_queue_table_columns():
|
|||||||
from src.gui_2 import App, render_ticket_queue
|
from src.gui_2 import App, render_ticket_queue
|
||||||
app = App.__new__(App)
|
app = App.__new__(App)
|
||||||
app.active_track = MagicMock()
|
app.active_track = MagicMock()
|
||||||
app.active_tickets = [models.Ticket(id="T-001", description="Test task", priority="medium", status="in_progress")]
|
app.active_tickets = [Ticket(id="T-001", description="Test task", priority="medium", status="in_progress")]
|
||||||
app.ui_selected_tickets = set()
|
app.ui_selected_tickets = set()
|
||||||
app.ui_selected_ticket_id = None
|
app.ui_selected_ticket_id = None
|
||||||
app.controller = MagicMock()
|
app.controller = MagicMock()
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ def test_render_mma_dashboard_progress():
|
|||||||
app.active_track = MagicMock()
|
app.active_track = MagicMock()
|
||||||
app.active_track.description = "Test Track"
|
app.active_track.description = "Test Track"
|
||||||
|
|
||||||
# Mock self.active_track.tickets as a list of src.models.Ticket objects
|
# Mock self.active_track.tickets as a list of src.Ticket objects
|
||||||
app.active_track.tickets = [
|
app.active_track.tickets = [
|
||||||
Ticket(id='T1', description='desc', status='completed'),
|
Ticket(id='T1', description='desc', status='completed'),
|
||||||
Ticket(id='T2', description='desc', status='in_progress'),
|
Ticket(id='T2', description='desc', status='in_progress'),
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ def test_mcp_server_config_to_from_dict():
|
|||||||
"args": ["server.js"],
|
"args": ["server.js"],
|
||||||
"auto_start": True
|
"auto_start": True
|
||||||
}
|
}
|
||||||
cfg = models.MCPServerConfig.from_dict("test-server", data)
|
cfg = MCPServerConfig.from_dict("test-server", data)
|
||||||
assert cfg.name == "test-server"
|
assert cfg.name == "test-server"
|
||||||
assert cfg.command == "node"
|
assert cfg.command == "node"
|
||||||
assert cfg.args == ["server.js"]
|
assert cfg.args == ["server.js"]
|
||||||
@@ -31,7 +31,7 @@ def test_mcp_configuration_to_from_dict():
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cfg = models.MCPConfiguration.from_dict(data)
|
cfg = MCPConfiguration.from_dict(data)
|
||||||
assert len(cfg.mcpServers) == 2
|
assert len(cfg.mcpServers) == 2
|
||||||
assert cfg.mcpServers["server1"].command == "python"
|
assert cfg.mcpServers["server1"].command == "python"
|
||||||
assert cfg.mcpServers["server2"].url == "http://localhost:8080/sse"
|
assert cfg.mcpServers["server2"].url == "http://localhost:8080/sse"
|
||||||
@@ -47,7 +47,7 @@ def test_load_mcp_config(tmp_path):
|
|||||||
config_file.write_text(json.dumps(data))
|
config_file.write_text(json.dumps(data))
|
||||||
|
|
||||||
# We'll need a way to load from a specific path
|
# We'll need a way to load from a specific path
|
||||||
# Maybe models.load_mcp_config(path)
|
# Maybe load_mcp_config(path)
|
||||||
cfg = models.load_mcp_config(str(config_file))
|
cfg = load_mcp_config(str(config_file))
|
||||||
assert "test" in cfg.mcpServers
|
assert "test" in cfg.mcpServers
|
||||||
assert cfg.mcpServers["test"].command == "echo"
|
assert cfg.mcpServers["test"].command == "echo"
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
Phase 1 of metadata_promotion_20260624.
|
Phase 1 of metadata_promotion_20260624.
|
||||||
|
|
||||||
Verifies:
|
Verifies:
|
||||||
1. self.active_tickets load boundaries convert dicts to models.Ticket
|
1. self.active_tickets load boundaries convert dicts to Ticket
|
||||||
2. conductor_tech_lead.topological_sort returns list[models.Ticket]
|
2. conductor_tech_lead.topological_sort returns list[Ticket]
|
||||||
3. gui_2.py consumer sites use direct field access (not .get())
|
3. gui_2.py consumer sites use direct field access (not .get())
|
||||||
4. app_controller.py consumer sites use direct field access (not .get())
|
4. app_controller.py consumer sites use direct field access (not .get())
|
||||||
"""
|
"""
|
||||||
@@ -15,11 +15,11 @@ from src.mma import Ticket
|
|||||||
|
|
||||||
class TestActiveTicketsType:
|
class TestActiveTicketsType:
|
||||||
def test_active_tickets_annotation_is_list_of_ticket(self) -> None:
|
def test_active_tickets_annotation_is_list_of_ticket(self) -> None:
|
||||||
"""self.active_tickets type hint must be list[models.Ticket], not list[Metadata]."""
|
"""self.active_tickets type hint must be list[Ticket], not list[Metadata]."""
|
||||||
from src.app_controller import AppController
|
from src.app_controller import AppController
|
||||||
src_text = inspect.getsource(AppController.__init__)
|
src_text = inspect.getsource(AppController.__init__)
|
||||||
assert "list[models.Ticket]" in src_text, (
|
assert "list[Ticket]" in src_text, (
|
||||||
"AppController.__init__ must declare self.active_tickets: list[models.Ticket]"
|
"AppController.__init__ must declare self.active_tickets: list[Ticket]"
|
||||||
)
|
)
|
||||||
assert "list[Metadata]" not in src_text.split("self.active_tickets")[1].split("\n")[0], (
|
assert "list[Metadata]" not in src_text.split("self.active_tickets")[1].split("\n")[0], (
|
||||||
"AppController.__init__ must NOT declare self.active_tickets: list[Metadata]"
|
"AppController.__init__ must NOT declare self.active_tickets: list[Metadata]"
|
||||||
@@ -28,7 +28,7 @@ class TestActiveTicketsType:
|
|||||||
|
|
||||||
class TestActiveTicketsLoadBoundaries:
|
class TestActiveTicketsLoadBoundaries:
|
||||||
def test_load_at_data_converts_dicts_to_tickets(self) -> None:
|
def test_load_at_data_converts_dicts_to_tickets(self) -> None:
|
||||||
"""_deserialize_active_track_result boundary must wrap dicts as models.Ticket."""
|
"""_deserialize_active_track_result boundary must wrap dicts as Ticket."""
|
||||||
from src.app_controller import AppController
|
from src.app_controller import AppController
|
||||||
with patch.object(AppController, "load_config", return_value={
|
with patch.object(AppController, "load_config", return_value={
|
||||||
'ai': {'provider': 'gemini', 'model': 'gemini-2.5-flash-lite'},
|
'ai': {'provider': 'gemini', 'model': 'gemini-2.5-flash-lite'},
|
||||||
@@ -56,7 +56,7 @@ class TestActiveTicketsLoadBoundaries:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_load_active_tickets_beads_branch_converts_dicts_to_tickets(self) -> None:
|
def test_load_active_tickets_beads_branch_converts_dicts_to_tickets(self) -> None:
|
||||||
"""_load_active_tickets (beads branch) must wrap bead dicts as models.Ticket."""
|
"""_load_active_tickets (beads branch) must wrap bead dicts as Ticket."""
|
||||||
from src.app_controller import AppController
|
from src.app_controller import AppController
|
||||||
from src.mma import Ticket
|
from src.mma import Ticket
|
||||||
ctrl = AppController.__new__(AppController)
|
ctrl = AppController.__new__(AppController)
|
||||||
@@ -79,7 +79,7 @@ class TestActiveTicketsLoadBoundaries:
|
|||||||
|
|
||||||
class TestTopologicalSortReturnsTicketList:
|
class TestTopologicalSortReturnsTicketList:
|
||||||
def test_topological_sort_returns_ticket_instances(self) -> None:
|
def test_topological_sort_returns_ticket_instances(self) -> None:
|
||||||
"""conductor_tech_lead.topological_sort must return list[models.Ticket]."""
|
"""conductor_tech_lead.topological_sort must return list[Ticket]."""
|
||||||
from src import conductor_tech_lead
|
from src import conductor_tech_lead
|
||||||
sig = inspect.signature(conductor_tech_lead.topological_sort)
|
sig = inspect.signature(conductor_tech_lead.topological_sort)
|
||||||
assert sig.return_annotation is not inspect.Signature.empty
|
assert sig.return_annotation is not inspect.Signature.empty
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from __future__ import annotations
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from src.project_manager import flat_config
|
from src.project_manager import flat_config
|
||||||
from src.models import (
|
from src.project import (
|
||||||
ProjectContext, ProjectMeta, ProjectOutput, ProjectFiles,
|
ProjectContext, ProjectMeta, ProjectOutput, ProjectFiles,
|
||||||
ProjectScreenshots, ProjectDiscussion, EMPTY_PROJECT_CONTEXT,
|
ProjectScreenshots, ProjectDiscussion, EMPTY_PROJECT_CONTEXT,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ class TestProjectSerialization(unittest.TestCase):
|
|||||||
def test_fileitem_roundtrip(self):
|
def test_fileitem_roundtrip(self):
|
||||||
"""Verify that FileItem objects survive a save/load cycle."""
|
"""Verify that FileItem objects survive a save/load cycle."""
|
||||||
proj = project_manager.default_project("test")
|
proj = project_manager.default_project("test")
|
||||||
file1 = models.FileItem(path="src/main.py", auto_aggregate=True, force_full=False)
|
file1 = FileItem(path="src/main.py", auto_aggregate=True, force_full=False)
|
||||||
file2 = models.FileItem(path="docs/readme.md", auto_aggregate=False, force_full=True)
|
file2 = FileItem(path="docs/readme.md", auto_aggregate=False, force_full=True)
|
||||||
proj["files"]["paths"] = [file1, file2]
|
proj["files"]["paths"] = [file1, file2]
|
||||||
|
|
||||||
# Save
|
# Save
|
||||||
@@ -29,12 +29,12 @@ class TestProjectSerialization(unittest.TestCase):
|
|||||||
|
|
||||||
paths = loaded_proj["files"]["paths"]
|
paths = loaded_proj["files"]["paths"]
|
||||||
self.assertEqual(len(paths), 2)
|
self.assertEqual(len(paths), 2)
|
||||||
self.assertIsInstance(paths[0], models.FileItem)
|
self.assertIsInstance(paths[0], FileItem)
|
||||||
self.assertEqual(paths[0].path, "src/main.py")
|
self.assertEqual(paths[0].path, "src/main.py")
|
||||||
self.assertTrue(paths[0].auto_aggregate)
|
self.assertTrue(paths[0].auto_aggregate)
|
||||||
self.assertFalse(paths[0].force_full)
|
self.assertFalse(paths[0].force_full)
|
||||||
|
|
||||||
self.assertIsInstance(paths[1], models.FileItem)
|
self.assertIsInstance(paths[1], FileItem)
|
||||||
self.assertEqual(paths[1].path, "docs/readme.md")
|
self.assertEqual(paths[1].path, "docs/readme.md")
|
||||||
self.assertFalse(paths[1].auto_aggregate)
|
self.assertFalse(paths[1].auto_aggregate)
|
||||||
self.assertTrue(paths[1].force_full)
|
self.assertTrue(paths[1].force_full)
|
||||||
@@ -68,17 +68,17 @@ roles = ["User", "AI"]
|
|||||||
raw_paths = controller.project.get("files", {}).get("paths", [])
|
raw_paths = controller.project.get("files", {}).get("paths", [])
|
||||||
controller.files = []
|
controller.files = []
|
||||||
for p in raw_paths:
|
for p in raw_paths:
|
||||||
if isinstance(p, models.FileItem):
|
if isinstance(p, FileItem):
|
||||||
controller.files.append(p)
|
controller.files.append(p)
|
||||||
elif isinstance(p, dict):
|
elif isinstance(p, dict):
|
||||||
controller.files.append(models.FileItem.from_dict(p))
|
controller.files.append(FileItem.from_dict(p))
|
||||||
else:
|
else:
|
||||||
controller.files.append(models.FileItem(path=str(p)))
|
controller.files.append(FileItem(path=str(p)))
|
||||||
|
|
||||||
self.assertEqual(len(controller.files), 2)
|
self.assertEqual(len(controller.files), 2)
|
||||||
self.assertIsInstance(controller.files[0], models.FileItem)
|
self.assertIsInstance(controller.files[0], FileItem)
|
||||||
self.assertEqual(controller.files[0].path, "file1.py")
|
self.assertEqual(controller.files[0].path, "file1.py")
|
||||||
self.assertIsInstance(controller.files[1], models.FileItem)
|
self.assertIsInstance(controller.files[1], FileItem)
|
||||||
self.assertEqual(controller.files[1].path, "file2.md")
|
self.assertEqual(controller.files[1].path, "file2.md")
|
||||||
|
|
||||||
def test_default_roles_include_context(self):
|
def test_default_roles_include_context(self):
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ class MockEmbeddingProvider(BaseEmbeddingProvider):
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_rag_config():
|
def mock_rag_config():
|
||||||
vs_config = models.VectorStoreConfig(provider='mock', collection_name='test')
|
vs_config = VectorStoreConfig(provider='mock', collection_name='test')
|
||||||
return models.RAGConfig(enabled=True, vector_store=vs_config, embedding_provider='gemini')
|
return RAGConfig(enabled=True, vector_store=vs_config, embedding_provider='gemini')
|
||||||
|
|
||||||
def test_rag_engine_init_mock(mock_rag_config):
|
def test_rag_engine_init_mock(mock_rag_config):
|
||||||
engine = RAGEngine(mock_rag_config)
|
engine = RAGEngine(mock_rag_config)
|
||||||
@@ -38,8 +38,8 @@ def test_rag_engine_chroma(mock_get_chroma, mock_embed):
|
|||||||
mock_client.get_or_create_collection.return_value = mock_collection
|
mock_client.get_or_create_collection.return_value = mock_collection
|
||||||
mock_chroma.PersistentClient.return_value = mock_client
|
mock_chroma.PersistentClient.return_value = mock_client
|
||||||
|
|
||||||
vs_config = models.VectorStoreConfig(provider='chroma', collection_name='test')
|
vs_config = VectorStoreConfig(provider='chroma', collection_name='test')
|
||||||
config = models.RAGConfig(enabled=True, vector_store=vs_config, embedding_provider='local')
|
config = RAGConfig(enabled=True, vector_store=vs_config, embedding_provider='local')
|
||||||
|
|
||||||
with patch('src.rag_engine._get_sentence_transformers') as mock_st:
|
with patch('src.rag_engine._get_sentence_transformers') as mock_st:
|
||||||
mock_st.return_value = MagicMock()
|
mock_st.return_value = MagicMock()
|
||||||
@@ -97,8 +97,8 @@ def test_rag_collection_dim_mismatch_recreates_collection(mock_get_chroma, mock_
|
|||||||
mock_client.get_or_create_collection.return_value = mock_collection
|
mock_client.get_or_create_collection.return_value = mock_collection
|
||||||
mock_chroma.PersistentClient.return_value = mock_client
|
mock_chroma.PersistentClient.return_value = mock_client
|
||||||
|
|
||||||
vs_config = models.VectorStoreConfig(provider='chroma', collection_name='test')
|
vs_config = VectorStoreConfig(provider='chroma', collection_name='test')
|
||||||
config = models.RAGConfig(enabled=True, vector_store=vs_config, embedding_provider='local')
|
config = RAGConfig(enabled=True, vector_store=vs_config, embedding_provider='local')
|
||||||
|
|
||||||
with patch('src.rag_engine._get_sentence_transformers') as mock_st:
|
with patch('src.rag_engine._get_sentence_transformers') as mock_st:
|
||||||
mock_st.return_value = MagicMock()
|
mock_st.return_value = MagicMock()
|
||||||
@@ -136,8 +136,8 @@ def test_rag_collection_dim_match_preserves_collection(mock_get_chroma, mock_emb
|
|||||||
mock_client.get_or_create_collection.return_value = mock_collection
|
mock_client.get_or_create_collection.return_value = mock_collection
|
||||||
mock_chroma.PersistentClient.return_value = mock_client
|
mock_chroma.PersistentClient.return_value = mock_client
|
||||||
|
|
||||||
vs_config = models.VectorStoreConfig(provider='chroma', collection_name='test')
|
vs_config = VectorStoreConfig(provider='chroma', collection_name='test')
|
||||||
config = models.RAGConfig(enabled=True, vector_store=vs_config, embedding_provider='local')
|
config = RAGConfig(enabled=True, vector_store=vs_config, embedding_provider='local')
|
||||||
|
|
||||||
with patch('src.rag_engine._get_sentence_transformers') as mock_st:
|
with patch('src.rag_engine._get_sentence_transformers') as mock_st:
|
||||||
mock_st.return_value = MagicMock()
|
mock_st.return_value = MagicMock()
|
||||||
|
|||||||
@@ -37,10 +37,10 @@ def test_rag_engine_init_with_local_provider_raises_when_sentence_transformers_m
|
|||||||
when sentence-transformers is not installed.
|
when sentence-transformers is not installed.
|
||||||
"""
|
"""
|
||||||
from src import models
|
from src import models
|
||||||
config = models.RAGConfig(
|
config = RAGConfig(
|
||||||
enabled=True,
|
enabled=True,
|
||||||
embedding_provider="local",
|
embedding_provider="local",
|
||||||
vector_store=models.VectorStoreConfig(provider="chroma", collection_name="test"),
|
vector_store=VectorStoreConfig(provider="chroma", collection_name="test"),
|
||||||
)
|
)
|
||||||
# Force the import to fail
|
# Force the import to fail
|
||||||
with patch.dict(sys.modules, {"sentence_transformers": None}):
|
with patch.dict(sys.modules, {"sentence_transformers": None}):
|
||||||
@@ -125,10 +125,10 @@ def test_rag_engine_init_with_failing_local_embedding_leaves_engine_broken() ->
|
|||||||
"""
|
"""
|
||||||
from src import models
|
from src import models
|
||||||
from src import rag_engine
|
from src import rag_engine
|
||||||
config = models.RAGConfig(
|
config = RAGConfig(
|
||||||
enabled=True,
|
enabled=True,
|
||||||
embedding_provider="local",
|
embedding_provider="local",
|
||||||
vector_store=models.VectorStoreConfig(provider="chroma", collection_name="t"),
|
vector_store=VectorStoreConfig(provider="chroma", collection_name="t"),
|
||||||
)
|
)
|
||||||
with patch("src.rag_engine._get_sentence_transformers",
|
with patch("src.rag_engine._get_sentence_transformers",
|
||||||
side_effect=ImportError("Local RAG embeddings require sentence-transformers.")):
|
side_effect=ImportError("Local RAG embeddings require sentence-transformers.")):
|
||||||
|
|||||||
@@ -55,9 +55,9 @@ def test_rag_integration(mock_project):
|
|||||||
ai_client.set_provider("gemini", "gemini-1.5-flash")
|
ai_client.set_provider("gemini", "gemini-1.5-flash")
|
||||||
|
|
||||||
# 2. Configures a mock RAG setup (enabled=True, provider='mock').
|
# 2. Configures a mock RAG setup (enabled=True, provider='mock').
|
||||||
rag_config = models.RAGConfig(
|
rag_config = RAGConfig(
|
||||||
enabled=True,
|
enabled=True,
|
||||||
vector_store=models.VectorStoreConfig(provider='mock')
|
vector_store=VectorStoreConfig(provider='mock')
|
||||||
)
|
)
|
||||||
app.rag_config = rag_config
|
app.rag_config = rag_config
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ def test_ui_summary_only_not_in_app_controller_projects():
|
|||||||
|
|
||||||
|
|
||||||
def test_file_item_has_per_file_flags():
|
def test_file_item_has_per_file_flags():
|
||||||
item = models.FileItem(path="test.py")
|
item = FileItem(path="test.py")
|
||||||
assert hasattr(item, "auto_aggregate")
|
assert hasattr(item, "auto_aggregate")
|
||||||
assert hasattr(item, "force_full")
|
assert hasattr(item, "force_full")
|
||||||
assert item.auto_aggregate is True
|
assert item.auto_aggregate is True
|
||||||
@@ -33,13 +33,13 @@ def test_file_item_has_per_file_flags():
|
|||||||
|
|
||||||
|
|
||||||
def test_file_item_serialization_with_flags():
|
def test_file_item_serialization_with_flags():
|
||||||
item = models.FileItem(path="test.py", auto_aggregate=False, force_full=True)
|
item = FileItem(path="test.py", auto_aggregate=False, force_full=True)
|
||||||
data = item.to_dict()
|
data = item.to_dict()
|
||||||
|
|
||||||
assert data["auto_aggregate"] is False
|
assert data["auto_aggregate"] is False
|
||||||
assert data["force_full"] is True
|
assert data["force_full"] is True
|
||||||
|
|
||||||
restored = models.FileItem.from_dict(data)
|
restored = FileItem.from_dict(data)
|
||||||
assert restored.auto_aggregate is False
|
assert restored.auto_aggregate is False
|
||||||
assert restored.force_full is True
|
assert restored.force_full is True
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ def controller(tmp_path):
|
|||||||
return ctrl
|
return ctrl
|
||||||
|
|
||||||
def test_save_view_preset(controller):
|
def test_save_view_preset(controller):
|
||||||
f_item = models.FileItem(path="test.py", view_mode="skeleton")
|
f_item = FileItem(path="test.py", view_mode="skeleton")
|
||||||
f_item.ast_mask = {"test::func": "sig"}
|
f_item.ast_mask = {"test::func": "sig"}
|
||||||
f_item.custom_slices = [{"start_line": 1, "end_line": 10}]
|
f_item.custom_slices = [{"start_line": 1, "end_line": 10}]
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ def test_save_view_preset(controller):
|
|||||||
|
|
||||||
def test_apply_view_preset(controller):
|
def test_apply_view_preset(controller):
|
||||||
# Setup a preset
|
# Setup a preset
|
||||||
preset = models.NamedViewPreset(
|
preset = NamedViewPreset(
|
||||||
name="my_preset",
|
name="my_preset",
|
||||||
view_mode="masked",
|
view_mode="masked",
|
||||||
ast_mask={"main::run": "def"},
|
ast_mask={"main::run": "def"},
|
||||||
@@ -67,7 +67,7 @@ def test_apply_view_preset(controller):
|
|||||||
controller.view_presets.append(preset)
|
controller.view_presets.append(preset)
|
||||||
|
|
||||||
# Create a file item to apply to
|
# Create a file item to apply to
|
||||||
f_item = models.FileItem(path="main.py", view_mode="summary")
|
f_item = FileItem(path="main.py", view_mode="summary")
|
||||||
|
|
||||||
controller._cb_apply_view_preset("my_preset", f_item)
|
controller._cb_apply_view_preset("my_preset", f_item)
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ def test_apply_view_preset(controller):
|
|||||||
assert f_item.custom_slices == [{"start_line": 5, "end_line": 15}]
|
assert f_item.custom_slices == [{"start_line": 5, "end_line": 15}]
|
||||||
|
|
||||||
def test_delete_view_preset(controller):
|
def test_delete_view_preset(controller):
|
||||||
preset = models.NamedViewPreset(name="to_del", view_mode="full")
|
preset = NamedViewPreset(name="to_del", view_mode="full")
|
||||||
controller.view_presets.append(preset)
|
controller.view_presets.append(preset)
|
||||||
|
|
||||||
controller._cb_delete_view_preset("to_del")
|
controller._cb_delete_view_preset("to_del")
|
||||||
|
|||||||
Reference in New Issue
Block a user