solidworks-mcp-python 1.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. solidworks_mcp/__init__.py +57 -0
  2. solidworks_mcp/adapters/__init__.py +40 -0
  3. solidworks_mcp/adapters/base.py +1882 -0
  4. solidworks_mcp/adapters/circuit_breaker.py +2052 -0
  5. solidworks_mcp/adapters/com_executor.py +252 -0
  6. solidworks_mcp/adapters/complexity_analyzer.py +272 -0
  7. solidworks_mcp/adapters/connection_pool.py +1884 -0
  8. solidworks_mcp/adapters/factory.py +366 -0
  9. solidworks_mcp/adapters/intelligent_router.py +218 -0
  10. solidworks_mcp/adapters/mock_adapter.py +4016 -0
  11. solidworks_mcp/adapters/pywin32_adapter.py +2842 -0
  12. solidworks_mcp/adapters/solidworks/__init__.py +17 -0
  13. solidworks_mcp/adapters/solidworks/bitmap.py +180 -0
  14. solidworks_mcp/adapters/solidworks/com_utils.py +56 -0
  15. solidworks_mcp/adapters/solidworks/dimension_layout.py +753 -0
  16. solidworks_mcp/adapters/solidworks/documentation.py +1476 -0
  17. solidworks_mcp/adapters/solidworks/drawing_standard.py +1993 -0
  18. solidworks_mcp/adapters/solidworks/face_selection.py +392 -0
  19. solidworks_mcp/adapters/solidworks/features.py +3362 -0
  20. solidworks_mcp/adapters/solidworks/io.py +3649 -0
  21. solidworks_mcp/adapters/solidworks/rebuild_diagnostics.py +269 -0
  22. solidworks_mcp/adapters/solidworks/rebuild_inspector.py +146 -0
  23. solidworks_mcp/adapters/solidworks/selection.py +308 -0
  24. solidworks_mcp/adapters/solidworks/sketch.py +2545 -0
  25. solidworks_mcp/adapters/sw_feature_errors.py +66 -0
  26. solidworks_mcp/adapters/sw_type_info.py +317 -0
  27. solidworks_mcp/adapters/vba_adapter.py +318 -0
  28. solidworks_mcp/adapters/vba_macro_executor.py +215 -0
  29. solidworks_mcp/agents/__init__.py +68 -0
  30. solidworks_mcp/agents/design_knowledge/assembly_patterns.md +127 -0
  31. solidworks_mcp/agents/design_knowledge/hinges.md +118 -0
  32. solidworks_mcp/agents/design_knowledge/snap_fits.md +93 -0
  33. solidworks_mcp/agents/design_knowledge/solidworks_basics.md +3 -0
  34. solidworks_mcp/agents/design_knowledge/tolerancing.md +128 -0
  35. solidworks_mcp/agents/harness.py +216 -0
  36. solidworks_mcp/agents/history_db.py +1508 -0
  37. solidworks_mcp/agents/retrieval_index.py +164 -0
  38. solidworks_mcp/agents/schemas.py +165 -0
  39. solidworks_mcp/agents/smoke_test.py +285 -0
  40. solidworks_mcp/agents/soc_exporter.py +650 -0
  41. solidworks_mcp/agents/soc_pickup.py +332 -0
  42. solidworks_mcp/agents/soc_rewind.py +221 -0
  43. solidworks_mcp/agents/vector_rag.py +665 -0
  44. solidworks_mcp/cache/__init__.py +5 -0
  45. solidworks_mcp/cache/response_cache.py +164 -0
  46. solidworks_mcp/config.py +729 -0
  47. solidworks_mcp/exceptions.py +37 -0
  48. solidworks_mcp/security/__init__.py +68 -0
  49. solidworks_mcp/security/auth.py +128 -0
  50. solidworks_mcp/security/cors.py +27 -0
  51. solidworks_mcp/security/rate_limiting.py +90 -0
  52. solidworks_mcp/security/runtime.py +155 -0
  53. solidworks_mcp/server.py +777 -0
  54. solidworks_mcp/server_cli_fixed.py +120 -0
  55. solidworks_mcp/setup/__init__.py +1 -0
  56. solidworks_mcp/setup/cli.py +399 -0
  57. solidworks_mcp/setup/clients.py +157 -0
  58. solidworks_mcp/setup/config_file.py +230 -0
  59. solidworks_mcp/setup/handshake.py +202 -0
  60. solidworks_mcp/setup/launcher.py +102 -0
  61. solidworks_mcp/setup/legacy.py +100 -0
  62. solidworks_mcp/setup/registration.py +193 -0
  63. solidworks_mcp/setup/solidworks.py +49 -0
  64. solidworks_mcp/tools/__init__.py +64 -0
  65. solidworks_mcp/tools/analysis.py +723 -0
  66. solidworks_mcp/tools/automation.py +578 -0
  67. solidworks_mcp/tools/docs_discovery.py +1476 -0
  68. solidworks_mcp/tools/drawing.py +1015 -0
  69. solidworks_mcp/tools/drawing_analysis.py +456 -0
  70. solidworks_mcp/tools/drawing_automation.py +627 -0
  71. solidworks_mcp/tools/engineering_data.py +466 -0
  72. solidworks_mcp/tools/error_classifier.py +232 -0
  73. solidworks_mcp/tools/export.py +833 -0
  74. solidworks_mcp/tools/file_facts.py +40 -0
  75. solidworks_mcp/tools/file_management.py +1473 -0
  76. solidworks_mcp/tools/input_compat.py +17 -0
  77. solidworks_mcp/tools/macro_recording.py +464 -0
  78. solidworks_mcp/tools/modeling.py +2360 -0
  79. solidworks_mcp/tools/sketching.py +1422 -0
  80. solidworks_mcp/tools/template_management.py +367 -0
  81. solidworks_mcp/tools/tool_boundary.py +280 -0
  82. solidworks_mcp/tools/vba_generation.py +1145 -0
  83. solidworks_mcp/ui/__init__.py +9 -0
  84. solidworks_mcp/ui/local_llm.py +725 -0
  85. solidworks_mcp/ui/routers/__init__.py +10 -0
  86. solidworks_mcp/ui/routers/checkpoint.py +24 -0
  87. solidworks_mcp/ui/routers/docs.py +47 -0
  88. solidworks_mcp/ui/routers/llm.py +83 -0
  89. solidworks_mcp/ui/routers/local_model.py +69 -0
  90. solidworks_mcp/ui/routers/model.py +75 -0
  91. solidworks_mcp/ui/routers/preview.py +38 -0
  92. solidworks_mcp/ui/routers/session.py +161 -0
  93. solidworks_mcp/ui/routers/viewer.py +154 -0
  94. solidworks_mcp/ui/schemas.py +502 -0
  95. solidworks_mcp/ui/server.py +298 -0
  96. solidworks_mcp/ui/service.py +481 -0
  97. solidworks_mcp/ui/services/__init__.py +136 -0
  98. solidworks_mcp/ui/services/_utils.py +958 -0
  99. solidworks_mcp/ui/services/checkpoint_service.py +1110 -0
  100. solidworks_mcp/ui/services/docs_service.py +282 -0
  101. solidworks_mcp/ui/services/llm_service.py +956 -0
  102. solidworks_mcp/ui/services/model_service.py +459 -0
  103. solidworks_mcp/ui/services/parameter_repair_service.py +314 -0
  104. solidworks_mcp/ui/services/preview_service.py +489 -0
  105. solidworks_mcp/ui/services/session_service.py +1389 -0
  106. solidworks_mcp/utils/__init__.py +9 -0
  107. solidworks_mcp/utils/feature_tree_classifier.py +342 -0
  108. solidworks_mcp/utils/logging.py +76 -0
  109. solidworks_mcp/utils/validation.py +77 -0
  110. solidworks_mcp/version.py +8 -0
  111. solidworks_mcp_python-1.5.0.dist-info/METADATA +410 -0
  112. solidworks_mcp_python-1.5.0.dist-info/RECORD +115 -0
  113. solidworks_mcp_python-1.5.0.dist-info/WHEEL +4 -0
  114. solidworks_mcp_python-1.5.0.dist-info/entry_points.txt +3 -0
  115. solidworks_mcp_python-1.5.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,489 @@
1
+ """Preview and feature-highlighting service for the Prefab CAD assistant dashboard.
2
+
3
+ Exports PNG screenshots and 3D geometry (GLB/STL) from the active SolidWorks model and
4
+ persists preview state in the session.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import time
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from loguru import logger
15
+
16
+ from ...adapters import create_adapter
17
+ from ...agents.history_db import (
18
+ get_design_session,
19
+ insert_model_state_snapshot,
20
+ insert_tool_call_record,
21
+ list_model_state_snapshots,
22
+ )
23
+ from ...config import load_config
24
+ from ._utils import (
25
+ DEFAULT_API_ORIGIN,
26
+ DEFAULT_PREVIEW_ORIENTATION,
27
+ ensure_preview_dir,
28
+ merge_metadata,
29
+ parse_json_blob,
30
+ sanitize_preview_viewer_url,
31
+ )
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # Internal helpers
35
+ # ---------------------------------------------------------------------------
36
+
37
+
38
+ def _public_preview_url(
39
+ preview_path: Path,
40
+ *,
41
+ api_origin: str = DEFAULT_API_ORIGIN,
42
+ ) -> str:
43
+ """Build a cache-busting public URL for a preview image.
44
+
45
+ Args:
46
+ preview_path: Filesystem path to the preview image.
47
+ api_origin: Base URL of the running FastAPI server.
48
+
49
+ Returns:
50
+ Public URL string with a ``ts`` query parameter.
51
+ """
52
+ timestamp = (
53
+ int(preview_path.stat().st_mtime) if preview_path.exists() else int(time.time())
54
+ )
55
+ return f"{api_origin}/previews/{preview_path.name}?ts={timestamp}"
56
+
57
+
58
+ async def _reopen_target_model_for_preview(
59
+ adapter: Any, model_path: str, *, context: str
60
+ ) -> None:
61
+ """Reopen the persisted target model before preview export."""
62
+ candidate_path = Path(str(model_path))
63
+ if not candidate_path.exists():
64
+ raise RuntimeError(
65
+ f"Target model path for {context} does not exist: {candidate_path}"
66
+ )
67
+
68
+ logger.info(
69
+ "[ui.refresh_preview] reopening target model for {} {}",
70
+ context,
71
+ str(candidate_path.resolve()),
72
+ )
73
+ reopen_result = await adapter.open_model(str(candidate_path.resolve()))
74
+ if not getattr(reopen_result, "is_success", False):
75
+ raise RuntimeError(
76
+ reopen_result.error
77
+ or f"Failed to reopen target model for {context}: {candidate_path.resolve()}"
78
+ )
79
+
80
+
81
+ # ---------------------------------------------------------------------------
82
+ # Public API
83
+ # ---------------------------------------------------------------------------
84
+
85
+
86
+ async def refresh_preview(
87
+ session_id: str,
88
+ *,
89
+ orientation: str = DEFAULT_PREVIEW_ORIENTATION,
90
+ db_path: Path | None = None,
91
+ preview_dir: Path | None = None,
92
+ api_origin: str = DEFAULT_API_ORIGIN,
93
+ adapter_override: Any | None = None,
94
+ active_model_path_override: str | None = None,
95
+ reopen_active_model: bool = True,
96
+ ) -> dict[str, Any]:
97
+ """Export the current SolidWorks viewport to a PNG preview and GLB/STL for the 3D viewer.
98
+
99
+ Args:
100
+ session_id: Dashboard session identifier.
101
+ orientation: View orientation for the PNG export
102
+ (``"front"``, ``"top"``, ``"right"``, ``"isometric"``, ``"current"``).
103
+ db_path: Optional SQLite path override.
104
+ preview_dir: Override for the preview output directory.
105
+ api_origin: Base URL of the running FastAPI server.
106
+ adapter_override: Pre-connected adapter (avoids creating a second connection when
107
+ called from ``connect_target_model``).
108
+ active_model_path_override: Override for the model path to reopen.
109
+ reopen_active_model: When ``True``, reopen the persisted active model path before
110
+ exporting (default).
111
+
112
+ Returns:
113
+ Full dashboard state payload.
114
+ """
115
+ from .session_service import ( # noqa: PLC0415
116
+ build_dashboard_state,
117
+ ensure_dashboard_session,
118
+ )
119
+
120
+ ensure_dashboard_session(session_id, db_path=db_path)
121
+ logger.info(
122
+ "[ui.refresh_preview] session_id={} orientation={}",
123
+ session_id,
124
+ orientation,
125
+ )
126
+ session_row = get_design_session(session_id, db_path=db_path) or {}
127
+ metadata = parse_json_blob(session_row.get("metadata_json"))
128
+ preview_viewer_url = sanitize_preview_viewer_url(
129
+ metadata.get("preview_viewer_url"),
130
+ session_id=session_id,
131
+ api_origin=api_origin,
132
+ )
133
+ resolved_preview_dir = ensure_preview_dir(preview_dir)
134
+ preview_path = resolved_preview_dir / f"{session_id}.png"
135
+ active_model_path = active_model_path_override or metadata.get("active_model_path")
136
+ adapter = adapter_override
137
+ owns_adapter = adapter_override is None
138
+
139
+ try:
140
+ if not active_model_path:
141
+ raise RuntimeError(
142
+ "No attached model path found for preview refresh. Attach a target model first."
143
+ )
144
+
145
+ if adapter is None:
146
+ config = load_config()
147
+ adapter = await create_adapter(config)
148
+ if owns_adapter:
149
+ await adapter.connect()
150
+ if reopen_active_model and hasattr(adapter, "open_model"):
151
+ await _reopen_target_model_for_preview(
152
+ adapter, str(active_model_path), context="preview"
153
+ )
154
+
155
+ # --- Step 1: Export 3D geometry for the Three.js viewer (GLB preferred) ---
156
+ glb_path = resolved_preview_dir / f"{session_id}.glb"
157
+ stl_path = resolved_preview_dir / f"{session_id}.stl"
158
+ viewer_ts = int(time.time())
159
+ viewer_format = "none"
160
+ try:
161
+ glb_result = await adapter.export_file(str(glb_path.resolve()), "glb")
162
+ if (
163
+ glb_result.is_success
164
+ and glb_path.exists()
165
+ and glb_path.stat().st_size > 0
166
+ ):
167
+ viewer_format = "glb"
168
+ viewer_ts = int(glb_path.stat().st_mtime)
169
+ logger.info(
170
+ "[ui.refresh_preview] GLB export succeeded path={}",
171
+ str(glb_path.resolve()),
172
+ )
173
+ except Exception as _glb_exc:
174
+ logger.warning("[ui.refresh_preview] GLB export failed: {}", str(_glb_exc))
175
+
176
+ if viewer_format == "none":
177
+ try:
178
+ stl_result = await adapter.export_file(str(stl_path.resolve()), "stl")
179
+ if (
180
+ stl_result.is_success
181
+ and stl_path.exists()
182
+ and stl_path.stat().st_size > 0
183
+ ):
184
+ viewer_format = "stl"
185
+ viewer_ts = int(stl_path.stat().st_mtime)
186
+ logger.info(
187
+ "[ui.refresh_preview] STL export succeeded path={}",
188
+ str(stl_path.resolve()),
189
+ )
190
+ except Exception:
191
+ logger.debug("[ui.refresh_preview] STL export skipped (adapter error)")
192
+
193
+ preview_viewer_url = (
194
+ f"{api_origin}/api/ui/viewer/{session_id}?t={viewer_ts}&fmt={viewer_format}"
195
+ )
196
+
197
+ # --- Step 2: Export PNG screenshot (best-effort) ---
198
+ png_payload = {
199
+ "file_path": str(preview_path.resolve()),
200
+ "format_type": "png",
201
+ "width": 1280,
202
+ "height": 720,
203
+ "view_orientation": orientation,
204
+ }
205
+ png_ok = False
206
+ png_error: str = ""
207
+ snapshot_id: int | None = None
208
+ try:
209
+ result = await adapter.export_image(png_payload)
210
+ if result.is_success and preview_path.exists():
211
+ png_ok = True
212
+ logger.info(
213
+ "[ui.refresh_preview] PNG export succeeded file_path={}",
214
+ str(preview_path.resolve()),
215
+ )
216
+ snapshot_id = insert_model_state_snapshot(
217
+ session_id=session_id,
218
+ screenshot_path=str(preview_path.resolve()),
219
+ state_fingerprint=f"preview-{preview_path.stat().st_mtime_ns}",
220
+ db_path=db_path,
221
+ )
222
+ insert_tool_call_record(
223
+ session_id=session_id,
224
+ tool_name="export_image",
225
+ input_json=json.dumps(png_payload, ensure_ascii=True),
226
+ output_json=json.dumps(result.data or {}, ensure_ascii=True),
227
+ success=True,
228
+ db_path=db_path,
229
+ )
230
+ else:
231
+ png_error = result.error or "export_image returned failure"
232
+ logger.warning("[ui.refresh_preview] PNG export failed: {}", png_error)
233
+ except Exception as png_exc:
234
+ png_error = str(png_exc)
235
+ logger.warning("[ui.refresh_preview] PNG export exception: {}", png_exc)
236
+
237
+ if owns_adapter:
238
+ await adapter.disconnect()
239
+
240
+ # --- Step 3: Export per-orientation PNG thumbnails ---
241
+ VIEW_ORIENTATIONS = ["isometric", "front", "top", "right"]
242
+ preview_view_urls: dict[str, str] = {}
243
+ try:
244
+ config2 = load_config()
245
+ adapter2 = await create_adapter(config2)
246
+ await adapter2.connect()
247
+ if hasattr(adapter2, "open_model"):
248
+ await _reopen_target_model_for_preview(
249
+ adapter2,
250
+ str(active_model_path),
251
+ context="orientation previews",
252
+ )
253
+ _sel_name = str(
254
+ metadata.get("selected_feature_selector_name")
255
+ or metadata.get("selected_feature_name")
256
+ or ""
257
+ ).strip()
258
+ if _sel_name and hasattr(adapter2, "select_feature"):
259
+ try:
260
+ await adapter2.select_feature(_sel_name)
261
+ logger.info(
262
+ "[ui.refresh_preview] re-selected '{}' before view screenshots",
263
+ _sel_name,
264
+ )
265
+ except Exception as _sel_exc:
266
+ logger.debug(
267
+ "[ui.refresh_preview] re-select '{}' failed (non-fatal): {}",
268
+ _sel_name,
269
+ _sel_exc,
270
+ )
271
+ for view_name in VIEW_ORIENTATIONS:
272
+ view_path = resolved_preview_dir / f"{session_id}-{view_name}.png"
273
+ try:
274
+ if _sel_name and hasattr(adapter2, "select_feature"):
275
+ await adapter2.select_feature(_sel_name)
276
+ view_result = await adapter2.export_image(
277
+ {
278
+ "file_path": str(view_path.resolve()),
279
+ "format_type": "png",
280
+ "width": 640,
281
+ "height": 480,
282
+ "view_orientation": view_name,
283
+ }
284
+ )
285
+ if view_result.is_success and view_path.exists():
286
+ ts = int(view_path.stat().st_mtime)
287
+ preview_view_urls[view_name] = (
288
+ f"{api_origin}/previews/{view_path.name}?ts={ts}"
289
+ )
290
+ logger.info(
291
+ "[ui.refresh_preview] view PNG {} exported", view_name
292
+ )
293
+ else:
294
+ logger.warning(
295
+ "[ui.refresh_preview] view PNG {} failed: {}",
296
+ view_name,
297
+ view_result.error or "no detail",
298
+ )
299
+ except Exception as _ve:
300
+ logger.warning(
301
+ "[ui.refresh_preview] view PNG {} exception: {}",
302
+ view_name,
303
+ str(_ve),
304
+ )
305
+ await adapter2.disconnect()
306
+ except Exception as _views_exc:
307
+ logger.warning(
308
+ "[ui.refresh_preview] multi-view export failed: {}", str(_views_exc)
309
+ )
310
+
311
+ # Preserve existing view URLs when a refresh attempt returns no images.
312
+ existing_view_urls = metadata.get("preview_view_urls")
313
+ if isinstance(existing_view_urls, dict):
314
+ if not preview_view_urls:
315
+ preview_view_urls = dict(existing_view_urls)
316
+ else:
317
+ merged_view_urls = dict(existing_view_urls)
318
+ merged_view_urls.update(preview_view_urls)
319
+ preview_view_urls = merged_view_urls
320
+
321
+ viewer_label = (
322
+ f"3D viewer ({viewer_format.upper()})"
323
+ if viewer_format != "none"
324
+ else "3D viewer (no model)"
325
+ )
326
+ png_label = "PNG" if png_ok else f"no PNG ({png_error})"
327
+ status_msg = f"Preview refreshed ({viewer_label}, {png_label})."
328
+
329
+ merge_metadata(
330
+ session_id,
331
+ db_path=db_path,
332
+ preview_orientation=orientation,
333
+ latest_message=status_msg,
334
+ preview_status=status_msg,
335
+ latest_snapshot_id=(str(snapshot_id) if snapshot_id is not None else ""),
336
+ preview_viewer_url=preview_viewer_url,
337
+ preview_stl_ready=(viewer_format != "none"),
338
+ preview_png_ready=png_ok,
339
+ preview_view_urls=preview_view_urls,
340
+ latest_error_text="",
341
+ remediation_hint="",
342
+ )
343
+ except Exception as exc:
344
+ logger.exception("[ui.refresh_preview] failed: {}", exc)
345
+ insert_tool_call_record(
346
+ session_id=session_id,
347
+ tool_name="export_image",
348
+ input_json=json.dumps({"orientation": orientation}, ensure_ascii=True),
349
+ output_json=json.dumps({"error": str(exc)}, ensure_ascii=True),
350
+ success=False,
351
+ db_path=db_path,
352
+ )
353
+ merge_metadata(
354
+ session_id,
355
+ db_path=db_path,
356
+ latest_message=f"Preview refresh failed: {exc}",
357
+ preview_status=f"Preview refresh failed: {exc}",
358
+ preview_orientation=orientation,
359
+ latest_error_text=str(exc),
360
+ remediation_hint="Open a model in SolidWorks and retry preview refresh.",
361
+ )
362
+
363
+ return build_dashboard_state(session_id, db_path=db_path, api_origin=api_origin)
364
+
365
+
366
+ async def highlight_feature(
367
+ session_id: str,
368
+ feature_name: str,
369
+ *,
370
+ db_path: Path | None = None,
371
+ api_origin: str = DEFAULT_API_ORIGIN,
372
+ ) -> dict[str, Any]:
373
+ """Select and highlight a named feature in the active SolidWorks model.
374
+
375
+ Args:
376
+ session_id: Dashboard session identifier.
377
+ feature_name: Name of the feature to select (must match the feature-tree entry).
378
+ db_path: Optional SQLite path override.
379
+ api_origin: Base URL of the running FastAPI server.
380
+
381
+ Returns:
382
+ Full dashboard state payload.
383
+ """
384
+ from .session_service import ( # noqa: PLC0415
385
+ build_dashboard_state,
386
+ ensure_dashboard_session,
387
+ )
388
+
389
+ ensure_dashboard_session(session_id, db_path=db_path)
390
+ session_row = get_design_session(session_id, db_path=db_path) or {}
391
+ metadata = parse_json_blob(session_row.get("metadata_json"))
392
+ active_model_path = metadata.get("active_model_path")
393
+ resolved_name = (feature_name or "").strip()
394
+ if not resolved_name:
395
+ merge_metadata(
396
+ session_id,
397
+ db_path=db_path,
398
+ latest_error_text="No feature name provided for selection.",
399
+ remediation_hint="Pass a non-empty feature_name.",
400
+ )
401
+ return build_dashboard_state(session_id, db_path=db_path, api_origin=api_origin)
402
+
403
+ try:
404
+ known_feature_names: set[str] = set()
405
+ for snapshot in list_model_state_snapshots(session_id, db_path=db_path):
406
+ raw_tree = snapshot.get("feature_tree_json")
407
+ if not raw_tree:
408
+ continue
409
+ try:
410
+ parsed_tree = json.loads(raw_tree)
411
+ except Exception:
412
+ continue
413
+ if isinstance(parsed_tree, list):
414
+ known_feature_names.update(
415
+ str(item.get("name") or "").strip()
416
+ for item in parsed_tree
417
+ if str(item.get("name") or "").strip()
418
+ )
419
+ if known_feature_names:
420
+ break
421
+
422
+ config = load_config()
423
+ adapter = await create_adapter(config)
424
+ await adapter.connect()
425
+ if active_model_path and hasattr(adapter, "open_model"):
426
+ candidate = Path(str(active_model_path))
427
+ if candidate.exists():
428
+ await adapter.open_model(str(candidate.resolve()))
429
+ selected = False
430
+ entity_type = ""
431
+ selected_name = resolved_name
432
+ if hasattr(adapter, "select_feature"):
433
+ result = await adapter.select_feature(resolved_name)
434
+ if result.is_success and isinstance(result.data, dict):
435
+ selected = bool(result.data.get("selected"))
436
+ entity_type = str(result.data.get("entity_type") or "")
437
+ selected_name = str(result.data.get("selected_name") or resolved_name)
438
+ await adapter.disconnect()
439
+ tracked_only = (not selected) and (resolved_name in known_feature_names)
440
+ merge_metadata(
441
+ session_id,
442
+ db_path=db_path,
443
+ selected_feature_name=resolved_name,
444
+ selected_feature_selector_name=selected_name,
445
+ latest_message=(
446
+ f"Selected '{resolved_name}' ({entity_type}) in SolidWorks."
447
+ if selected
448
+ else (
449
+ f"Tracking '{resolved_name}' from the feature tree. "
450
+ "SolidWorks did not expose a direct selectable handle for that row."
451
+ if tracked_only
452
+ else f"Could not select feature '{resolved_name}' — name may not match the feature tree."
453
+ )
454
+ ),
455
+ latest_error_text=(
456
+ ""
457
+ if (selected or tracked_only)
458
+ else f"SelectByID2 returned False for '{resolved_name}'."
459
+ ),
460
+ remediation_hint=(
461
+ ""
462
+ if (selected or tracked_only)
463
+ else "Check that the feature name exactly matches the SolidWorks feature tree entry."
464
+ ),
465
+ )
466
+ insert_tool_call_record(
467
+ session_id=session_id,
468
+ tool_name="ui.highlight_feature",
469
+ input_json=json.dumps({"feature_name": resolved_name}, ensure_ascii=True),
470
+ output_json=json.dumps(
471
+ {
472
+ "selected": selected,
473
+ "tracked_only": tracked_only,
474
+ "entity_type": entity_type,
475
+ },
476
+ ensure_ascii=True,
477
+ ),
478
+ success=(selected or tracked_only),
479
+ db_path=db_path,
480
+ )
481
+ except Exception as exc:
482
+ logger.exception("[ui.highlight_feature] failed: {}", exc)
483
+ merge_metadata(
484
+ session_id,
485
+ db_path=db_path,
486
+ latest_error_text=str(exc),
487
+ remediation_hint="Ensure SolidWorks is open with the target model loaded.",
488
+ )
489
+ return build_dashboard_state(session_id, db_path=db_path, api_origin=api_origin)