diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1604527..989b14f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,25 @@ user-facing notes live in [`docs/updates/`](docs/updates/) (English and
## Unreleased
+## 2.2.2 — 2026-10-14
+
+### Added
+
+- Editor: code completion from the SDK itself. `scad.` (and `scad.ql.`, `scad.std.gear.`, `ql.`)
+ lists the real functions with their signatures and docstrings; accepting one, or typing `(`
+ after a known name, writes the call with every required parameter as `name=` and Tab moves
+ between them; inside a call the parameters not given yet complete as `name=` (required first).
+ The names come from the running package (`GET /api/sdk`), so they cannot drift from the code.
+- Editor: a **new** action in the workspace tab starts a notebook from the SDK's template (a part,
+ or an assembly that uses the first two parts of the workspace), with the name checked as you
+ type. An empty workspace opens on that form, so `sca edit` on an empty directory is usable.
+
+### Fixed
+
+- Editor: the cell editors rebuilt their configuration on every render (a new `onChange` and
+ `basicSetup` each time), which dropped any snippet in progress and could disturb typing; both
+ are stable now.
+
## 2.2.1 — 2026-10-13
The first stable release of the 2.2 line (2.2.0b1 was a pre-release): everything
diff --git a/README.md b/README.md
index ad8d5cd..430b377 100644
--- a/README.md
+++ b/README.md
@@ -149,37 +149,24 @@ the full crank cycle sweeps clean in the same sim ·
---
-## Update Notes (2.2.1)
+## Update Notes (2.2.2)
> Validate generated definitions, assembly constraints, and manufacturing
> geometry before production use.
-- **New editor.** `sca edit` is part of the SDK now (`pip install
- "simplecadapi[editor]"`). It runs a workspace's notebooks in a marimo kernel
- with the SDK's own cell executor, previews the live model straight from the
- solids, writes your viewport picks as selection code, and lets an agent see
- what you see (`sca snapshot`). **Re-Studio is removed**: the reverse-
- engineering studio (the `Re-mode` workspace of the standalone sca-web-editor)
- is gone, and the standalone sca-web-editor is replaced by `sca edit`. STEP
- reconstruction continues through the inspection API and the skill's
- step-reconstruction workflow.
-- **One runtime, one cache.** The editor, `sca run` and
- `simplecadapi.runtime.run_notebook()` share the per-cell cache, so what one
- computed the others restore. Notebook parameters come from `# ---- params
- ----` cells with an optional `# ui:` comment; local modules (shared
- dimensions) get their own view in the editor.
-- **Landing path.** `docs/landing.md` is the script an agent follows to install
- the SDK and its skill and open an example; `sca example u_link` copies the
- bundled workspace and `sca skill where` shows where the skill goes.
-- Earlier: 2.1.3b1 shipped the addon ecosystem (`sca addon ...`, the
- `sca-addon.toml` descriptor, the `.scadpkg` format spec); see its
- [update notes](docs/updates/2.1.3b1.md).
-
-Every example model is a notebook; its export scripts load the product with
-`run_notebook`, capture one `.scadpkg`, and translate AP242 `.step` and
-editable `.FCStd` from that same package. The AP242/Gmsh example under
-`examples/ap242_gmsh_volume_mesh/` exposes each export and FEM stage as a
-separate directly runnable script.
+- **Code completion from the SDK.** In the editor, `scad.` (and `scad.ql.`,
+ `scad.std.gear.`) lists the real functions with their signatures and
+ docstrings. Accepting one, or typing `(` after its name, writes the call with
+ every required parameter as `name=`, and Tab moves between them; inside a call
+ the parameters not given yet complete as `name=`.
+- **New notebooks from the workspace tab.** A **new** action starts a part, or
+ an assembly that uses the first two parts, from the SDK's template; an empty
+ workspace opens on that form.
+- **Fix.** The cell editors no longer rebuild their configuration on every
+ render, which dropped a snippet in progress.
+
+Full notes for this version: [`docs/updates/2.2.2.md`](docs/updates/2.2.2.md).
+Earlier versions: the [changelog](CHANGELOG.md) and [`docs/updates/`](docs/updates/).
---
@@ -199,7 +186,7 @@ in a compact public API for creating solids, applying features, tagging semantic
intent, querying topology, exporting manufacturing files, and translating recorded
models into FreeCAD workflows.
-Current release: `simplecadapi==2.2.1`.
+Current release: `simplecadapi==2.2.2`.
## What It Provides
@@ -714,6 +701,7 @@ and the [STEP reconstruction workflow](docs/skill/references/workflows/step-reco
## Documentation
+- 2.2.2 update notes: [`docs/updates/2.2.2.md`](docs/updates/2.2.2.md)
- 2.2.1 update notes: [`docs/updates/2.2.1.md`](docs/updates/2.2.1.md)
- 2.1.3b1 update notes: [`docs/updates/2.1.3b1.md`](docs/updates/2.1.3b1.md)
- 2.1.2 update notes: [`docs/updates/2.1.2.md`](docs/updates/2.1.2.md)
diff --git a/README.zh-CN.md b/README.zh-CN.md
index 161501c..801891d 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -133,30 +133,19 @@ CalculiX 静力 FEM(边界面按保真的 `interface.*` 标签选取,仿真
---
-## 更新日志(2.2.1)
+## 更新日志(2.2.2)
> 投入生产前请验证生成的定义、装配约束和制造几何。
-- **新编辑器。** `sca edit` 现在是 SDK 的一部分(`pip install
- "simplecadapi[editor]"`)。它用 SDK 自己的 cell 执行器在 marimo 内核里运行工作
- 区的 notebook,直接由实体生成实时预览,把你在视口里的点选写成选择代码,并让
- agent 看到你看到的(`sca snapshot`)。**Re-Studio 已移除**:逆向工程 studio
- (独立 sca-web-editor 的 `Re-mode` 工作区)不再存在,独立的 sca-web-editor 由
- `sca edit` 取代。STEP 重建继续通过检查 API 和 skill 的 step-reconstruction
- 工作流进行。
-- **一个运行时,一份缓存。** 编辑器、`sca run` 和
- `simplecadapi.runtime.run_notebook()` 共用逐 cell 缓存,一方算过的另一方直接
- 复用。notebook 参数来自 `# ---- params ----` cell,可加 `# ui:` 注释指定控件;
- 本地模块(共用尺寸)在编辑器里有独立视图。
-- **落地路径。** `docs/landing.md` 是 agent 安装 SDK 和 skill、打开示例时遵循的
- 脚本;`sca example u_link` 复制内置工作区,`sca skill where` 显示 skill 的安装位置。
-- 之前:2.1.3b1 带来插件生态(`sca addon ...`、`sca-addon.toml` 描述文件、
- `.scadpkg` 格式规范),见[更新说明](docs/updates/2.1.3b1.zh-CN.md)。
-
-每个示例模型都是 notebook;导出脚本用 `run_notebook` 载入产品,捕获一个
-`.scadpkg`,再从同一个产品包转换出 AP242 `.step` 和可编辑 `.FCStd`。
-`examples/ap242_gmsh_volume_mesh/` 下的 AP242/Gmsh 示例把每个导出和 FEM 阶段
-都做成可直接运行的独立脚本。
+- **来自 SDK 的代码补全。** 在编辑器里,输入 `scad.`(以及 `scad.ql.`、`scad.std.gear.`)
+ 会列出真实存在的函数、签名和文档说明。选中一个函数,或在函数名后输入 `(`,会把每个必填
+ 参数写成 `name=`,Tab 在它们之间跳转;在调用括号内,还没写的参数会补全成 `name=`。
+- **在工作区页面创建 notebook。** 新增的 **new** 按 SDK 模板创建一个零件,或一个使用前两个
+ 零件的装配;空工作区会直接打开这个表单。
+- **修复。** cell 编辑器不再每次渲染都重建配置,这会让进行中的占位跳转丢失。
+
+本版本的完整说明:[`docs/updates/2.2.2.zh-CN.md`](docs/updates/2.2.2.zh-CN.md)。
+更早的版本:见[更新日志](CHANGELOG.md)和 [`docs/updates/`](docs/updates/)。
---
@@ -173,7 +162,7 @@ CalculiX 静力 FEM(边界面按保真的 `interface.*` 标签选取,仿真
SimpleCADAPI 是一个基于 OCP 的 Python CAD SDK,提供清晰的函数式建模操作和可重放的模型图。它在 OpenCascade 几何内核之上提供精简的公共 API,可用于创建实体、应用特征、添加语义标签、查询拓扑、导出制造文件,以及将记录的模型转换为 FreeCAD 工作流。
-当前版本:`simplecadapi==2.2.1`。
+当前版本:`simplecadapi==2.2.2`。
## 核心能力
@@ -599,6 +588,7 @@ uv run --extra fem python examples/ap242_gmsh_volume_mesh/run_calculix.py
## 文档
+- 2.2.2 更新说明:[`docs/updates/2.2.2.zh-CN.md`](docs/updates/2.2.2.zh-CN.md)
- 2.2.1 更新说明:[`docs/updates/2.2.1.zh-CN.md`](docs/updates/2.2.1.zh-CN.md)
- 2.1.3b1 更新说明:[`docs/updates/2.1.3b1.zh-CN.md`](docs/updates/2.1.3b1.zh-CN.md)
- 2.1.2 更新说明:[`docs/updates/2.1.2.zh-CN.md`](docs/updates/2.1.2.zh-CN.md)
diff --git a/docs/skill/SKILL.md b/docs/skill/SKILL.md
index 9cd131c..81dc3a8 100644
--- a/docs/skill/SKILL.md
+++ b/docs/skill/SKILL.md
@@ -4,9 +4,9 @@ description: Build, assemble, inspect, reconstruct, and export parametric CAD mo
license: Apache-2.0
metadata:
project: simplecadapi
- version: 2.2.1
+ version: 2.2.2
package-name: simplecadapi
- package-version: 2.2.1
+ package-version: 2.2.2
---
# SimpleCAD SDK Skill
diff --git a/docs/updates/2.2.2.md b/docs/updates/2.2.2.md
new file mode 100644
index 0000000..bea6c78
--- /dev/null
+++ b/docs/updates/2.2.2.md
@@ -0,0 +1,27 @@
+# 2.2.2 Update Notes
+
+Two additions to the code-first editor (`sca edit`), and a fix that made one of them usable.
+
+## Code completion from the SDK
+
+`scad.` (and `scad.ql.`, `scad.std.gear.`, `ql.`, `Q.`) lists the real functions with their
+signatures; the side panel shows every parameter with its default and the docstring.
+
+- Accepting a function, or typing `(` after a known name, writes the call with every
+ **required** parameter as `name=`: `scad.make_box_rsolid(width=, height=, depth=)`.
+ **Tab** moves to the next parameter.
+- Inside a call, the parameters not given yet complete as `name=`, required ones first.
+- The names come from the running package (`GET /api/sdk`), so they cannot drift from the code.
+
+## Create a notebook from the workspace tab
+
+A **new** action in the notebooks header starts a notebook from the SDK's template: a part, or an
+assembly that uses the first two parts of the workspace. The name is checked as you type. An empty
+workspace opens on that form, so `sca edit` on an empty directory is usable.
+
+## Fixed
+
+The cell editors rebuilt their configuration on every render, which dropped a snippet in progress
+and could disturb typing; the editor's `onChange` and options are stable now.
+
+See the [changelog](../../CHANGELOG.md) for the complete list.
diff --git a/docs/updates/2.2.2.zh-CN.md b/docs/updates/2.2.2.zh-CN.md
new file mode 100644
index 0000000..c2592d9
--- /dev/null
+++ b/docs/updates/2.2.2.zh-CN.md
@@ -0,0 +1,25 @@
+# 2.2.2 更新说明
+
+`sca edit` 编辑器的两项新增,以及一个让其中一项能正常使用的修复。
+
+## 来自 SDK 的代码补全
+
+输入 `scad.`(以及 `scad.ql.`、`scad.std.gear.`、`ql.`、`Q.`)会列出真实存在的函数和签名;
+右侧面板显示每个参数、默认值和文档说明。
+
+- 选中一个函数,或在已知函数名后输入 `(`,会把每个**必填**参数写成 `name=`:
+ `scad.make_box_rsolid(width=, height=, depth=)`。**Tab** 跳到下一个参数。
+- 在调用括号内,还没写的参数会补全成 `name=`,必填的排在最前。
+- 名字来自正在运行的包(`GET /api/sdk`),不会和代码不一致。
+
+## 在工作区页面创建 notebook
+
+notebooks 标题处新增 **new**,按 SDK 模板创建 notebook:一个零件,或一个使用工作区前两个零件的装配。
+边输入边检查名字。空工作区会直接打开这个表单,所以对空目录运行 `sca edit` 也能用了。
+
+## 修复
+
+cell 编辑器每次渲染都会重建配置,导致进行中的占位跳转丢失,也可能干扰输入;现在编辑器的
+`onChange` 和选项都是稳定的。
+
+完整列表见[更新日志](../../CHANGELOG.md)。
diff --git a/editor-web/package-lock.json b/editor-web/package-lock.json
index 378d381..64fa773 100644
--- a/editor-web/package-lock.json
+++ b/editor-web/package-lock.json
@@ -8,8 +8,10 @@
"name": "simplecad-editor-web",
"version": "0.0.0",
"dependencies": {
+ "@codemirror/autocomplete": "^6.20.3",
"@codemirror/lang-python": "^6.2.1",
"@codemirror/language": "^6.13.1",
+ "@codemirror/state": "^6.7.6",
"@codemirror/theme-one-dark": "^6.1.3",
"@codemirror/view": "^6.43.14",
"@lezer/highlight": "^1.2.5",
diff --git a/editor-web/package.json b/editor-web/package.json
index ab30b07..2b2afbd 100644
--- a/editor-web/package.json
+++ b/editor-web/package.json
@@ -11,8 +11,10 @@
"typecheck": "tsc --noEmit -p tsconfig.json"
},
"dependencies": {
+ "@codemirror/autocomplete": "^6.20.3",
"@codemirror/lang-python": "^6.2.1",
"@codemirror/language": "^6.13.1",
+ "@codemirror/state": "^6.7.6",
"@codemirror/theme-one-dark": "^6.1.3",
"@codemirror/view": "^6.43.14",
"@lezer/highlight": "^1.2.5",
diff --git a/editor-web/src/App.tsx b/editor-web/src/App.tsx
index 2d481e4..a19d286 100644
--- a/editor-web/src/App.tsx
+++ b/editor-web/src/App.tsx
@@ -9,7 +9,7 @@ import { ModuleView, type ModuleState } from "@/components/ModuleView";
import { NotebookList } from "@/components/NotebookList";
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
-import { fetchPreview, getWorkspace, openNotebook, postViewport, type ModuleRow, type NotebookRow } from "@/lib/api";
+import { fetchPreview, getWorkspace, newNotebook, openNotebook, postViewport, type ModuleRow, type NotebookRow } from "@/lib/api";
import { parsePreview } from "@/lib/preview";
import { EnvelopeSocket, type Envelope } from "@/lib/envelope";
import { useRunLog } from "@/lib/runlog";
@@ -118,12 +118,14 @@ export function App() {
socket.on("hello", (envelope) => {
setNotebook((envelope["notebook"] as string) ?? null);
setCurrentPath((envelope["path"] as string) ?? null);
+ // an empty workspace: the way forward is the workspace tab's "new"
+ if (!envelope["notebook"]) setLeftTab("workspace");
setKernelAlive(Boolean(envelope["kernel"]));
// No notebook: nothing to wait for. Else the model is on its way.
if (hasModel.current) return;
const stage = envelope["notebook"] ? (envelope["kernel"] ? "building the model" : "starting the kernel") : null;
setLoading(stage);
- if (stage) setStatus(`${stage[0].toUpperCase()}${stage.slice(1)}…`);
+ setStatus(stage ? `${stage[0].toUpperCase()}${stage.slice(1)}…` : "No notebook yet: create one in the workspace tab");
setMarimoUrl((envelope["marimo_url"] as string) ?? null);
refreshWorkspace();
});
@@ -312,6 +314,13 @@ export function App() {
});
};
+ const handleCreate = async (name: string, kind: "part" | "assembly") => {
+ setStatus(`creating ${name}…`);
+ await newNotebook(name, kind);
+ // the server opens it: show its cells
+ setLeftTab("notebook");
+ };
+
return (
socketRef.current?.send({ type: "module.open", path })}
+ onCreate={handleCreate}
/>
{moduleView && (
diff --git a/editor-web/src/components/CellEditor.tsx b/editor-web/src/components/CellEditor.tsx
index f1581cb..7b87b23 100644
--- a/editor-web/src/components/CellEditor.tsx
+++ b/editor-web/src/components/CellEditor.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useMemo, useRef, useState } from "react";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import CodeMirror from "@uiw/react-codemirror";
import { python } from "@codemirror/lang-python";
import { EditorView } from "@codemirror/view";
@@ -20,6 +20,7 @@ import { Badge } from "@/components/ui/badge";
import { ScrollArea } from "@/components/ui/scroll-area";
import { ParamControl, type ParamMeta, type ParamValue } from "@/components/ParamControl";
import { cadTheme } from "@/lib/cm-theme";
+import { sdkCompletion } from "@/lib/sdk-complete";
import { cn } from "@/lib/utils";
export type NotebookCell = {
@@ -49,7 +50,9 @@ export type { ParamMeta, ParamValue };
type SocketLike = { send: (message: Record) => void } | null;
const AUTOSAVE_MS = 700;
-const EXTENSIONS = [python(), EditorView.lineWrapping];
+const EXTENSIONS = [python(), EditorView.lineWrapping, sdkCompletion];
+/** a constant: a new object each render makes the editor wrapper rebuild its configuration, which drops a snippet in progress */
+const BASIC_SETUP = { lineNumbers: false, foldGutter: false, highlightActiveLine: false };
function loadCollapsed(notebook: string | null): Set {
try {
@@ -317,6 +320,10 @@ function CellBlock({
onDelete: () => void;
onEdit: (value: string) => void;
}) {
+ // the editor takes this once: a new function each render would rebuild its configuration
+ const onEditRef = useRef(onEdit);
+ onEditRef.current = onEdit;
+ const handleChange = useCallback((value: string) => onEditRef.current(value), []);
const failed = cell.status === "exception" || cell.status === "error" || (cell.errors?.length ?? 0) > 0;
const stale = cell.status === "stale";
const dirty = draft !== undefined && draft !== cell.code;
@@ -411,8 +418,8 @@ function CellBlock({
extensions={EXTENSIONS}
theme={cadTheme}
editable={!cell.readonly}
- basicSetup={{ lineNumbers: false, foldGutter: false, highlightActiveLine: false }}
- onChange={onEdit}
+ basicSetup={BASIC_SETUP}
+ onChange={handleChange}
onKeyDown={(event) => {
if ((event.ctrlKey || event.metaKey) && event.key === "Enter") {
event.preventDefault();
diff --git a/editor-web/src/components/ModuleView.tsx b/editor-web/src/components/ModuleView.tsx
index 4dc7c04..2f12221 100644
--- a/editor-web/src/components/ModuleView.tsx
+++ b/editor-web/src/components/ModuleView.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useRef, useState } from "react";
+import { useCallback, useEffect, useRef, useState } from "react";
import CodeMirror from "@uiw/react-codemirror";
import { python } from "@codemirror/lang-python";
import { EditorView } from "@codemirror/view";
@@ -8,6 +8,7 @@ import { Badge } from "@/components/ui/badge";
import { ScrollArea } from "@/components/ui/scroll-area";
import { ParamControl, type ParamMeta, type ParamValue } from "@/components/ParamControl";
import { cadTheme } from "@/lib/cm-theme";
+import { sdkCompletion } from "@/lib/sdk-complete";
/** A local module of the workspace (shared dimensions, helpers): what the
* server sends as the `module` envelope. */
@@ -21,7 +22,9 @@ export type ModuleState = {
};
const AUTOSAVE_MS = 700;
-const EXTENSIONS = [python(), EditorView.lineWrapping];
+const EXTENSIONS = [python(), EditorView.lineWrapping, sdkCompletion];
+/** a constant: a new object each render makes the editor wrapper rebuild its configuration, which drops a snippet in progress */
+const BASIC_SETUP = { lineNumbers: true, foldGutter: false, highlightActiveLine: false };
/** A module open next to the notebooks: its dataclass fields as controls, its
* source below. Every edit is written to the file at once, and every notebook
@@ -60,6 +63,11 @@ export function ModuleView({
}, AUTOSAVE_MS);
};
+ // the editor takes this once: a new function each render would rebuild its configuration
+ const editRef = useRef(edit);
+ editRef.current = edit;
+ const handleChange = useCallback((value: string) => editRef.current(value), []);
+
const forward = (id: string, value: ParamValue) => {
pending.current.set(id, value);
if (paramTimer.current !== null) return;
@@ -124,8 +132,8 @@ export function ModuleView({
height="auto"
extensions={EXTENSIONS}
theme={cadTheme}
- basicSetup={{ lineNumbers: true, foldGutter: false, highlightActiveLine: false }}
- onChange={edit}
+ basicSetup={BASIC_SETUP}
+ onChange={handleChange}
/>
diff --git a/editor-web/src/components/NotebookList.tsx b/editor-web/src/components/NotebookList.tsx
index 0551015..21e6d05 100644
--- a/editor-web/src/components/NotebookList.tsx
+++ b/editor-web/src/components/NotebookList.tsx
@@ -1,11 +1,18 @@
+import { Plus } from "lucide-react";
+import { useRef, useState } from "react";
+
import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils";
import type { ModuleRow, NotebookRow } from "@/lib/api";
+/** Names the template accepts: the notebook's id and its file name. */
+const NAME = /^[A-Za-z_][A-Za-z0-9_-]*$/;
+
/** The workspace: its SimpleCAD notebooks (assemblies show what they use) and
* the local modules they import. Editing a module changes every notebook
- * that imports it. */
+ * that imports it. A new notebook starts from the SDK's template. */
export function NotebookList({
notebooks,
current,
@@ -13,6 +20,7 @@ export function NotebookList({
openModule,
onOpen,
onOpenModule,
+ onCreate,
}: {
notebooks: NotebookRow[];
/** Workspace-relative path of the open notebook. */
@@ -21,13 +29,27 @@ export function NotebookList({
openModule: string | null;
onOpen: (path: string) => void;
onOpenModule: (path: string) => void;
+ /** Write a new notebook and open it; rejects with the reason it could not. */
+ onCreate: (name: string, kind: "part" | "assembly") => Promise;
}) {
- if (notebooks.length === 0 && modules.length === 0) {
- return