Make project-scope sym-lib-table visible to symbol-discovery tools

search_symbols, list_symbol_libraries, list_library_symbols, and
get_symbol_info previously only consulted the global sym-lib-table. A
library registered with project scope (an entry in
<project>/sym-lib-table) was therefore invisible — even right after
open_project succeeded — making add_schematic_component the only tool
that could see it.

Fix has two parts:

1. Wrap project_commands.open_project and project_commands.create_project
   in handlers that rebuild SymbolLibraryCommands.library_manager against
   the project directory. After open_project, project-scope libraries are
   automatically visible to subsequent search/list/info calls.

2. Add an optional projectPath parameter to the four discovery tools
   (accepts a project directory, .kicad_pro, .kicad_pcb, or .kicad_sch
   path). Stateless callers can resolve project libraries without first
   calling open_project. SymbolLibraryCommands._derive_project_path also
   walks up from schematicPath/boardPath to find the directory that owns
   the project, mirroring the logic in _handle_add_schematic_component.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Michael Holm
2026-05-02 20:35:15 +02:00
parent d3c01e20bd
commit 890746c6ac
4 changed files with 158 additions and 11 deletions

View File

@@ -515,9 +515,57 @@ class SymbolLibraryCommands:
"""Initialize with optional library manager"""
self.library_manager = library_manager or SymbolLibraryManager()
@staticmethod
def _derive_project_path(params: Dict) -> Optional[Path]:
"""Derive a project directory from caller-supplied params.
Accepts an explicit project directory or .kicad_pro file via projectPath,
or any related file path (schematicPath/boardPath) — in which case the
nearest ancestor containing sym-lib-table or a .kicad_pro is used.
"""
for key in ("projectPath", "project_path"):
value = params.get(key)
if value:
p = Path(value).expanduser()
if p.suffix == ".kicad_pro" or p.is_file():
p = p.parent
return p
for key in ("schematicPath", "boardPath"):
value = params.get(key)
if value:
start = Path(value).expanduser().parent
for ancestor in [start, *start.parents]:
if (ancestor / "sym-lib-table").exists() or list(
ancestor.glob("*.kicad_pro")
):
return ancestor
return start
return None
def use_project(self, project_path: Optional[Path]) -> None:
"""Switch the underlying manager to load project-scope libraries.
Callers (e.g. open_project / create_project) use this to make
`<project>/sym-lib-table` visible to subsequent search/list/info calls
without requiring every caller to pass projectPath.
"""
if project_path is None:
return
if self.library_manager.project_path == project_path:
return
logger.info(f"Rebuilding SymbolLibraryManager for project: {project_path}")
self.library_manager = SymbolLibraryManager(project_path=project_path)
def _ensure_manager_for(self, params: Dict) -> None:
"""Rebuild the library manager if the caller's project differs."""
self.use_project(self._derive_project_path(params))
def list_symbol_libraries(self, params: Dict) -> Dict:
"""List all available symbol libraries"""
try:
self._ensure_manager_for(params)
libraries = self.library_manager.list_libraries()
return {"success": True, "libraries": libraries, "count": len(libraries)}
except Exception as e:
@@ -535,6 +583,8 @@ class SymbolLibraryCommands:
if not query:
return {"success": False, "message": "Missing query parameter"}
self._ensure_manager_for(params)
limit = params.get("limit", 20)
library_filter = params.get("library")
@@ -557,6 +607,8 @@ class SymbolLibraryCommands:
if not library:
return {"success": False, "message": "Missing library parameter"}
self._ensure_manager_for(params)
# Check if library exists in sym-lib-table
if library not in self.library_manager.libraries:
available_libs = list(self.library_manager.libraries.keys())
@@ -593,6 +645,8 @@ class SymbolLibraryCommands:
if not symbol_spec:
return {"success": False, "message": "Missing symbol parameter"}
self._ensure_manager_for(params)
result = self.library_manager.find_symbol(symbol_spec)
if result:

View File

@@ -305,8 +305,8 @@ class KiCADInterface:
# Command routing dictionary
self.command_routes = {
# Project commands
"create_project": self.project_commands.create_project,
"open_project": self.project_commands.open_project,
"create_project": self._handle_create_project,
"open_project": self._handle_open_project,
"save_project": self.project_commands.save_project,
"snapshot_project": self._handle_snapshot_project,
"get_project_info": self.project_commands.get_project_info,
@@ -677,6 +677,59 @@ class KiCADInterface:
logger.error(f"Error loading schematic: {str(e)}")
return {"success": False, "message": str(e)}
def _project_path_from_filename(self, filename: Optional[str]) -> Optional[Path]:
"""Resolve a project directory from a filename param.
Accepts a .kicad_pro file, a .kicad_pcb file, or a directory.
"""
if not filename:
return None
try:
p = Path(filename).expanduser()
except Exception:
return None
if p.is_file() or p.suffix in (".kicad_pro", ".kicad_pcb", ".kicad_sch"):
return p.parent
return p
def _refresh_symbol_library_for_project(self, project_path: Optional[Path]) -> None:
"""Rebuild SymbolLibraryCommands' manager so project-scope sym-lib-table
is visible to subsequent search/list/info calls. No-op if unchanged."""
if project_path is None:
return
self._current_project_path = project_path
try:
self.symbol_library_commands.use_project(project_path)
except Exception as e:
logger.warning(f"Failed to refresh symbol library for project {project_path}: {e}")
def _handle_open_project(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Wrap project_commands.open_project so project-scope symbol libraries
become visible to subsequent search_symbols / list_symbol_libraries /
get_symbol_info calls."""
result = self.project_commands.open_project(params)
if result.get("success"):
project_info = result.get("project") or {}
project_path = self._project_path_from_filename(
project_info.get("path") or project_info.get("boardPath") or params.get("filename")
)
self._refresh_symbol_library_for_project(project_path)
return result
def _handle_create_project(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Wrap project_commands.create_project for the same reason as open_project."""
result = self.project_commands.create_project(params)
if result.get("success"):
project_info = result.get("project") or {}
project_path = self._project_path_from_filename(
project_info.get("path")
or project_info.get("boardPath")
or params.get("path")
or params.get("filename")
)
self._refresh_symbol_library_for_project(project_path)
return result
def _handle_place_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Place a component on the PCB, with project-local fp-lib-table support.
If boardPath is given and differs from the currently loaded board, the