Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,52 @@

## [Unreleased]

## [0.2.133] - 2026-10-02

### Fixed (P134 — material that never took, macros that could not fail)

Seen in a real session: the part was built correctly and was visible. But the tree still
said 材质 <未指定> after `set_material` "succeeded", every mass came out at water density, and
the agent then spent about ten `run_macro` calls on macros that all "completed" without
doing anything.

**`set_material` reported success when nothing was applied.**
- **Why:** `SetMaterialPropertyName2` returns nothing, and SolidWorks silently ignores a name
it does not know. A Chinese install has 「合金钢」, not "Alloy Steel".
- **Fix:** the tool now applies each candidate — the given name, then its zh/en alias (Alloy
Steel ↔ 合金钢, Plain Carbon Steel ↔ 普通碳钢, 6061 Alloy ↔ 6061 合金, …; "steel" /
"钢" / "aluminum" / "铝" map to a standard grade) — in both database spellings, and
verifies each one by reading the density back.
- **Result:** if nothing takes, the tool raises an error naming what it tried. If the
density is unchanged on a part that already has a material, it returns `verified:false`
with a note instead of claiming success.

**`run_macro` bound nothing.** The tool description promised that `swApp` and `Part` are
bound, but nothing assigned them, so every macro touching SolidWorks died on its first
statement. The entry `Sub` now binds `swApp` / `Part` / `swModel` (only when the macro uses
them).

**`run_macro` reported success after a runtime error.** An error inside the entry `Sub`
reached the runner with `Err` already clear, so a macro that stopped on its first broken
line came back "脚本执行完成".
- **Fix:** the entry `Sub` is instrumented. Before each statement it records the line number
and text (no marker after a ` _` continuation or between `Select Case` and its first
`Case`), and it sets a completion flag on its last line and on every `Exit Sub`.
- **Result:** a macro that did not reach the end is reported as failed, with the line it
stopped on.

**Connection note:** it is now attached to the first result only (it was repeated on every
result), and it distinguishes "SolidWorks was not running" from "a background instance
without a window was shown" from "a second instance was started".

### Tests
- **`tests/vba-macro-writer.test.mjs`:** binding, completion flag and line reporting, no
marker where VBScript forbids a statement, Functions-only scripts.
- **`sidecar/tests/test_p133_visibility.py`:** `set_material` falls back to the Chinese
name and verifies; a material that never takes is an error; an ambiguous case is not
claimed; the background-instance note.
- **Totals:** 201 JS + 67 Python tests.

## [0.2.132] - 2026-10-02

### Fixed (P133 — tools built the part in a SolidWorks the user could not see)
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@
</p>

<p align="center">
<img src="https://img.shields.io/badge/version-0.2.132-blue" alt="version" />
<img src="https://img.shields.io/badge/version-0.2.133-blue" alt="version" />
<img src="https://img.shields.io/badge/electron-28-47848F?logo=electron" alt="electron" />
<img src="https://img.shields.io/badge/react-18-61DAFB?logo=react" alt="react" />
<img src="https://img.shields.io/badge/typescript-5.3-3178C6?logo=typescript" alt="typescript" />
<img src="https://img.shields.io/badge/python-3.9%2B-3776AB?logo=python&logoColor=white" alt="python" />
<img src="https://img.shields.io/badge/tests-197_JS_%2B_63_Python-brightgreen" alt="tests" />
<img src="https://img.shields.io/badge/tests-201_JS_%2B_67_Python-brightgreen" alt="tests" />
<img src="https://img.shields.io/badge/license-Apache_2.0-orange" alt="license" />
</p>

Expand Down Expand Up @@ -86,7 +86,7 @@ Millwright:
- **Agentic tool loop.** Observe → reason → act. The model chains multiple tool calls, reads structured JSON back from each one, and recovers from errors instead of failing silently.
- **Visual understanding.** Reorient, rotate, screenshot, and analyze the model — via a multimodal main model or a dedicated vision model.
- **Resident execution engine.** A persistent Python sidecar holds one COM connection open across an entire multi-step task.
- **Developer-friendly.** 197 TypeScript/Node tests plus a Python suite (`pytest sidecar/tests`) for the sidecar, a typed IPC boundary, and a `SKIP_SW_CONNECT` mode for UI-only development without SolidWorks installed.
- **Developer-friendly.** 201 TypeScript/Node tests plus a Python suite (`pytest sidecar/tests`) for the sidecar, a typed IPC boundary, and a `SKIP_SW_CONNECT` mode for UI-only development without SolidWorks installed.

## Cross-version compatibility

Expand Down Expand Up @@ -248,7 +248,7 @@ Contributions welcome — see [CONTRIBUTING.md](docs/CONTRIBUTING.md). We especi

- [x] **v0.1** — MVP: Electron shell, LLM adapters, COM bridge, first tool set
- [x] **v0.2** — Python sidecar, agentic tool loop, dual-engine fallback, vision feedback, confirmation cards, Apache-2.0 open source
- [x] **v0.2.4 → v0.2.132** — Extensive hardening against real SolidWorks installs ← *current*: the sketch → feature → cut → visual-verification loop now runs end to end on real hardware
- [x] **v0.2.4 → v0.2.133** — Extensive hardening against real SolidWorks installs ← *current*: the sketch → feature → cut → visual-verification loop now runs end to end on real hardware
- [ ] **v0.3** — Streaming tool calls, sketching on model faces (not just reference planes), hole wizard, sheet metal, drawing annotations, remaining `#VERIFY` parameters confirmed
- [ ] **v1.0** — MCP server, multi-CAD support

Expand Down
8 changes: 4 additions & 4 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@
</p>

<p align="center">
<img src="https://img.shields.io/badge/version-0.2.132-blue" alt="version" />
<img src="https://img.shields.io/badge/version-0.2.133-blue" alt="version" />
<img src="https://img.shields.io/badge/electron-28-47848F?logo=electron" alt="electron" />
<img src="https://img.shields.io/badge/react-18-61DAFB?logo=react" alt="react" />
<img src="https://img.shields.io/badge/typescript-5.3-3178C6?logo=typescript" alt="typescript" />
<img src="https://img.shields.io/badge/python-3.9%2B-3776AB?logo=python&logoColor=white" alt="python" />
<img src="https://img.shields.io/badge/tests-197_JS_%2B_63_Python-brightgreen" alt="tests" />
<img src="https://img.shields.io/badge/tests-201_JS_%2B_67_Python-brightgreen" alt="tests" />
<img src="https://img.shields.io/badge/license-Apache_2.0-orange" alt="license" />
</p>

Expand Down Expand Up @@ -85,7 +85,7 @@ Millwright:
- **Agent 工具循环。** 观察 → 推理 → 执行。模型串联多次工具调用,读取每次返回的结构化 JSON,出错能自愈而不是静默失败。
- **视觉理解。** 可翻转、旋转、截屏,再做分析——既支持多模态主模型,也支持独立视觉模型。
- **常驻执行引擎。** 常驻 Python 边车在一整个多步任务中复用同一条 COM 连接。
- **开发者友好。** 197 个 TS/Node 单元测试,另有独立的 Python 测试套件(`pytest sidecar/tests`),类型化 IPC 边界,`SKIP_SW_CONNECT` 纯 UI 开发模式(无需 SolidWorks)。
- **开发者友好。** 201 个 TS/Node 单元测试,另有独立的 Python 测试套件(`pytest sidecar/tests`),类型化 IPC 边界,`SKIP_SW_CONNECT` 纯 UI 开发模式(无需 SolidWorks)。

## 跨版本兼容

Expand Down Expand Up @@ -247,7 +247,7 @@ SolidWorks

- [x] **v0.1** — MVP:Electron 骨架、LLM 适配器、COM 桥接、首批工具
- [x] **v0.2** — Python 边车、agent 工具循环、双引擎降级、视觉反馈、确认卡片,Apache-2.0 开源
- [x] **v0.2.4 → v0.2.132** — 大量真机加固 ← *当前*:草图 → 特征 → 切除 → 视觉核验的完整闭环已在真机上端到端跑通
- [x] **v0.2.4 → v0.2.133** — 大量真机加固 ← *当前*:草图 → 特征 → 切除 → 视觉核验的完整闭环已在真机上端到端跑通
- [ ] **v0.3** — 流式工具调用、在模型面上画草图(而非仅基准面)、孔向导、钣金、工程图标注、剩余 `# VERIFY` 参数完成核验
- [ ] **v1.0** — MCP server、多 CAD 支持

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "millwright",
"version": "0.2.132",
"version": "0.2.133",
"description": "Open-source AI automation for SolidWorks — talk to your CAD.",
"keywords": [
"solidworks",
Expand Down
6 changes: 5 additions & 1 deletion sidecar/sw_agent/bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,11 @@ def ensure_visible(app, pids_before: set[int] | None) -> str | None:
after = _sw_pids()
second = bool(pids_before) and after is not None and bool(after - pids_before)
if not second:
return ("SolidWorks 之前没有可见窗口,已启动/显示 SolidWorks;之后的建模都在这个窗口里进行。")
if pids_before: # attached to a SolidWorks already running in the background
return ("连接到的是一个在后台运行、没有窗口的 SolidWorks(可能是之前残留的进程),已把它显示出来,"
"之后的建模都在这个窗口里。如果你还开着另一个 SolidWorks 窗口,请以这个新显示的为准,"
"或关掉多余的那个。")
return "SolidWorks 之前没有运行,已为你启动并显示,之后的建模都在这个窗口里。"
note = ("你已打开的 SolidWorks 无法被连接,系统因此新启动了一个 SolidWorks 实例(现已显示出来),"
"模型会建在这个新窗口里,而不是你原来的窗口。通常原因是两者权限级别不同——其中一个是"
"「以管理员身份运行」的。")
Expand Down
3 changes: 2 additions & 1 deletion sidecar/sw_agent/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,9 +207,10 @@ def work():
except Exception: # noqa: BLE001
pass
# P133: we are driving a SolidWorks the user did not open (see
# bridge.ensure_visible) — say so on every result until the session reconnects
# bridge.ensure_visible) — say so ONCE, on the first result after connecting
if getattr(ctx, "connect_note", None):
data["_connection"] = ctx.connect_note
ctx.connect_note = None
if mutating:
adv = _advisory(ctx)
if adv:
Expand Down
87 changes: 84 additions & 3 deletions sidecar/sw_agent/tools/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,10 +249,91 @@ def save_as(ctx: Context, path: str):
category="document",
)
def set_material(ctx: Context, material: str, database: str = ""):
"""P133: SetMaterialPropertyName2 returns nothing, and an unknown name is silently
ignored — a Chinese SolidWorks has 「合金钢」, not "Alloy Steel". The tool used to report
success while the tree still said 材质 <未指定> and every mass came out at water
density. Now each candidate (the given name, then its zh/en alias) is applied and
VERIFIED by reading the density back; nothing taking is an error, not a success."""
part = ctx.require(DOC_PART, "part")
db = database or "SOLIDWORKS Materials"
part.SetMaterialPropertyName2("", db, material)
return {"material": material, "database": db}
dbs = [database] if database else ["SOLIDWORKS Materials", "solidworks materials"]
names = _material_candidates(material)
before = _density(ctx)
tried = []
for db in dbs:
for name in names:
tried.append(f"{db}/{name}")
try:
part.SetMaterialPropertyName2("", db, name)
except Exception: # noqa: BLE001 — try the next spelling
continue
try:
ctx.model.ForceRebuild3(False)
except Exception: # noqa: BLE001
pass
after = _density(ctx)
if after is None: # cannot read back on this install — report, do not claim
return {"material": name, "database": db, "verified": False}
if _changed(before, after):
return {"material": name, "database": db, "verified": True,
"density_kg_m3": round(after, 3)}
if before is not None and not _changed(_NO_MATERIAL_DENSITY, before):
continue # still the no-material density: the name did not take
# the part already had a material of this density — re-applying the same one,
# or a name SolidWorks ignored; the density cannot tell which
return {"material": name, "database": db, "verified": False,
"density_kg_m3": round(after, 3),
"note": "density unchanged — either this material was already applied, or "
"the name was not recognised; check the material in the feature tree"}
raise SWError(
f"material '{material}' was not applied — SolidWorks did not recognise the name "
f"(tried {', '.join(tried)}; density still {before} kg/m³). Use the exact name from "
"SolidWorks' material dialog — a Chinese install names them e.g. 合金钢 / 普通碳钢 / 6061 合金.")


# P133: English ↔ Chinese names of SolidWorks' standard materials (+ generic words)
_MATERIAL_ALIASES: list[tuple[str, ...]] = [
("Alloy Steel", "合金钢"),
("Plain Carbon Steel", "普通碳钢"),
("Cast Carbon Steel", "铸造碳钢"),
("Cast Alloy Steel", "铸造合金钢"),
("1060 Alloy", "1060 合金"),
("6061 Alloy", "6061 合金"),
("Plain Carbon Steel", "普通碳钢", "steel", "钢"),
("6061 Alloy", "6061 合金", "aluminum", "aluminium", "铝", "铝合金"),
]

_NO_MATERIAL_DENSITY = 1000.0 # SolidWorks' density for a part with no material assigned


def _material_candidates(material: str) -> list[str]:
m = material.strip()
low = m.lower()
out = [m]
for group in _MATERIAL_ALIASES:
if low in (g.lower() for g in group):
out += [g for g in group[:2] if g not in out]
return out


def _density(ctx) -> float | None:
"""kg/m³ from IMassProperty, or None when it cannot be read."""
try:
ext = ctx.model.Extension
for maker in ("CreateMassProperty", "CreateMassProperty2"):
try:
mp = sw_get(ext, maker)
except Exception: # noqa: BLE001
continue
if mp is not None:
return float(sw_get(mp, "Density"))
except Exception: # noqa: BLE001
pass
return None


def _changed(before: float | None, after: float) -> bool:
ref = _NO_MATERIAL_DENSITY if before is None else before
return abs(after - ref) > 1e-6


@tool("rebuild_model", "Force a full model rebuild, i.e. Ctrl+Q (= Rebuild; ForceRebuild3)", params={}, category="document")
Expand Down
73 changes: 72 additions & 1 deletion sidecar/tests/test_p133_visibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,75 @@ def test_hidden_only_instance_is_shown(monkeypatch):
app = _App(False)
note = bridge.ensure_visible(app, set()) # SolidWorks was not running before
assert app.Visible is True
assert "新启动" not in note
assert "没有运行" in note


def test_hidden_background_instance_is_shown(monkeypatch):
monkeypatch.setattr(bridge, "_sw_pids", lambda: {100})
app = _App(False)
note = bridge.ensure_visible(app, {100}) # same process, it just had no window
assert app.Visible is True
assert "后台" in note


# ---- set_material: verified by density, zh/en aliases ----

_DENSITY = {"合金钢": 7700.0, "普通碳钢": 7800.0, "6061 合金": 2700.0}


class _MatPart:
"""A Chinese SolidWorks: only Chinese names exist; unknown names are silently ignored."""

def __init__(self, material=None):
self.material = material
self.calls = []

def SetMaterialPropertyName2(self, cfg, db, name):
self.calls.append(name)
if name in _DENSITY:
self.material = name

def ForceRebuild3(self, _):
return True


def _mat_ctx(part):
class Mp:
_oleobj_ = object()

@property
def Density(self):
return _DENSITY.get(part.material, 1000.0)

class Ext:
_oleobj_ = object()
CreateMassProperty = Mp()

class Ctx:
model = part
def require(self, *_):
return part
part.Extension = Ext()
return Ctx()


def test_set_material_falls_back_to_the_chinese_name_and_verifies():
from sw_agent.tools import document
part = _MatPart()
out = document.set_material(_mat_ctx(part), "Alloy Steel")
assert part.material == "合金钢"
assert out["material"] == "合金钢" and out["verified"] is True and out["density_kg_m3"] == 7700.0


def test_set_material_that_never_takes_is_an_error():
import pytest
from sw_agent.bridge import SWError
from sw_agent.tools import document
with pytest.raises(SWError, match="not applied"):
document.set_material(_mat_ctx(_MatPart()), "Unobtainium")


def test_set_material_unknown_name_on_a_part_with_material_is_not_claimed():
from sw_agent.tools import document
out = document.set_material(_mat_ctx(_MatPart("普通碳钢")), "Unobtainium")
assert out["verified"] is False and "unchanged" in out["note"]
5 changes: 4 additions & 1 deletion src/main/agent/agent-loop-sidecar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,10 @@ const VIRTUAL_TOOLS = [
+ 'accepts, pick faces and edges by meaning, and report the REAL error instead of '
+ 'silently doing nothing. A static check runs first and REFUSES code that passes '
+ 'millimetres to metre-based APIs or opens with On Error Resume Next. '
+ 'Write the body only — swApp and Part are already bound, no Sub main() wrapper.',
+ 'Write the body only — no Sub main() wrapper. swApp (the SolidWorks application), '
+ 'Part and swModel (both = the active document) are bound for you. A runtime error '
+ 'stops the macro and is reported with the line it stopped on; a macro has no other '
+ 'way to return data than WScript.Echo, which comes back as the tool output.',
parameters: {
type: 'object',
properties: {
Expand Down
Loading
Loading