From 5f05dfe669d90f9d36f050966ad32f299c180dbe Mon Sep 17 00:00:00 2001 From: CAD Assistant Date: Mon, 19 Oct 2026 01:17:24 +0800 Subject: [PATCH 1/3] Add sca cell open, --open on cell writes, and sca viewport An agent working a multi-notebook workspace had to leave the CLI to keep working: every switch of the editor's open notebook was a raw curl to /api/workspace/open found by reading the installed server's source, the viewport image was read out of .sca-editor/ by hand, and a write refused while another notebook was open left no in-CLI way out. - sca cell open NOTEBOOK switches the running editor and waits for the notebook's kernel (SCA_EDITOR_OPEN_WAIT or --wait, default 120s), so the following write does not race the boot. Re-opening the notebook that is already open and ready does not restart the kernel. - sca cell put/insert/rm/run take --open to switch and write in one step; a refused write now names sca cell open as the remedy. - /api/cells reports kernel, so a caller can tell a booted notebook from one whose name has flipped while the kernel still boots. - sca viewport [WORKSPACE] [--out PATH] reports the viewport image's path, age and size, and copies it out for an agent's image reader. - The bundled skill's editor-collaboration reference documents all three. --- CHANGELOG.md | 15 ++ .../domains/editor-collaboration.md | 6 +- src/simplecadapi/cli.py | 1 + src/simplecadapi/editor/cli.py | 148 ++++++++++++++- src/simplecadapi/editor/server.py | 4 + test/test_editor.py | 168 ++++++++++++++++++ 6 files changed, 333 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45904ea..9d5f841 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,21 @@ user-facing notes live in [`docs/updates/`](docs/updates/) (English and ## Unreleased +### Added + +- Editor: `sca cell open NOTEBOOK` switches the running editor to another notebook of the + workspace and waits for its kernel, so the following `sca cell` writes go through instead + of refusing. The write commands take `--open` to switch and write in one step + (`--wait`, or `SCA_EDITOR_OPEN_WAIT`, raises the boot wait; default 120s). `/api/cells` + reports `kernel` so a caller can tell a booted notebook from one still opening. +- Editor: `sca viewport [WORKSPACE] [--out PATH]` reports where the viewport image lives + (path, age, size) and copies it out, instead of reading `.sca-editor/viewport.png` by hand. + +### Changed + +- Editor: a refused write now names `sca cell open NOTEBOOK` (or `--open`) as the way out, + and the bundled skill's editor-collaboration reference says so. + ## 2.2.5 — 2026-10-15 ### Fixed diff --git a/docs/skill/references/domains/editor-collaboration.md b/docs/skill/references/domains/editor-collaboration.md index fc32156..841de78 100644 --- a/docs/skill/references/domains/editor-collaboration.md +++ b/docs/skill/references/domains/editor-collaboration.md @@ -63,6 +63,7 @@ echo 'L = 90' | sca cell put 3 # replace the code of cell 3 and run it (-- sca cell insert --after 3 -f new.py # add a cell (--after start|end|CELL, --name fn_name) sca cell rm 4 sca cell run [CELL ...] # run cells (default all) and what they feed +sca cell open other.py # have the editor open another notebook (waits for its kernel) sca new bracket --kind part # a new notebook; the open editor lists it, the person's view stays ``` @@ -74,7 +75,8 @@ sca new bracket --kind part # a new notebook; the open editor lists it, replaces the cell, so `get` first when the person may have changed it. - The commands find the running editor through `/.sca-editor/server.json`. With no editor running, or with another notebook open, `ls` and `get` read the file and the write - commands refuse: then edit the file directly, nothing else holds it. + commands refuse: run `sca cell open NOTEBOOK` (or pass `--open` to switch and write in one + step), or edit the file directly, nothing else holds it. - `@app.function` cells cannot be changed this way; edit them in the file. ## Read what the person is doing @@ -83,7 +85,7 @@ sca new bracket --kind part # a new notebook; the open editor lists it, | --- | --- | | What is selected | the cell headed `# ---- selection ----` in the notebook file | | Everything at once (selection, cell states, errors, parameter values) | `sca snapshot [WORKSPACE]`, or `/.sca-editor/snapshot.json` | -| What the viewport looks like | `/.sca-editor/viewport.png` (a PNG; look at it) | +| What the viewport looks like | `sca viewport [WORKSPACE] [--out PATH]` — the PNG's path, age and size, copied out with `--out` (the file is `/.sca-editor/viewport.png`) | | Which notebook is open | `notebook` in the snapshot | `sca snapshot` only has an answer while the editor runs; it reports the last diff --git a/src/simplecadapi/cli.py b/src/simplecadapi/cli.py index 900b6d8..9b5f564 100644 --- a/src/simplecadapi/cli.py +++ b/src/simplecadapi/cli.py @@ -12,6 +12,7 @@ sca edit [WORKSPACE] # the web editor (needs the 'editor' extra) sca example [NAME] [DIR] # copy a bundled example workspace out sca snapshot [WORKSPACE] # what the editor user has selected / sees + sca viewport [WORKSPACE] # the image of what the editor user sees Shared contract: group handlers return ``(report, exit_code)`` and are followed by the group's own printer (human-readable for init/addon, JSON diff --git a/src/simplecadapi/editor/cli.py b/src/simplecadapi/editor/cli.py index 22acf6e..a199c58 100644 --- a/src/simplecadapi/editor/cli.py +++ b/src/simplecadapi/editor/cli.py @@ -6,7 +6,9 @@ sca edit path/to/model.py # ... starting at one notebook sca edit --port 7180 --no-browser sca snapshot [WORKSPACE] # what the person has selected / is looking at + sca viewport [WORKSPACE] [--out PATH] # the image of what the person sees sca cell ls|get|put|insert|rm|run # edit the cells of the notebook the editor has open + sca cell open NOTEBOOK [--wait S] # have the editor open another notebook sca new NAME [--kind part|assembly] # a new notebook; the editor lists it at once """ @@ -45,6 +47,19 @@ def configure(subparsers: argparse._SubParsersAction) -> None: snapshot.add_argument("workspace", type=Path, nargs="?", default=Path.cwd()) snapshot.set_defaults(handler=_snapshot, printer=_print_snapshot) + viewport = subparsers.add_parser( + "viewport", + help="the image of what the editor user sees, to look at or copy out", + description="Locate the editor's viewport image (the PNG of what the person sees, " + "kept in WORKSPACE/.sca-editor/ while the editor runs), tell its path, age and " + "size, and with --out copy it somewhere an agent's image reader reaches. A stale " + "age means the page has not redrawn since then; there is no image until the page " + "showed a model at least once.", + ) + viewport.add_argument("workspace", type=Path, nargs="?", default=Path.cwd()) + viewport.add_argument("--out", type=Path, default=None, help="copy the PNG to this path") + viewport.set_defaults(handler=_viewport, printer=_print_json) + cell = subparsers.add_parser( "cell", help="read and edit the cells of a notebook through the running editor", @@ -52,7 +67,8 @@ def configure(subparsers: argparse._SubParsersAction) -> None: "shows the change live (no reload) and the editor, the one writer of the file, saves " "it, so nothing is overwritten. Cells are named by number (see `sca cell ls`), by key " "or by id. When the editor is not running, or has another notebook open, `ls` and " - "`get` read the file and the write commands refuse: edit the file directly then, " + "`get` read the file and the write commands refuse: `sca cell open NOTEBOOK` switches " + "(or pass --open to switch and write in one step), or edit the file directly then — " "nothing else holds it.", ) cell_sub = cell.add_subparsers(dest="cell_command", required=True) @@ -61,6 +77,19 @@ def common(parser: argparse.ArgumentParser) -> None: parser.add_argument("--notebook", "-n", type=Path, default=None, help="the notebook (default: the one the editor has open)") parser.add_argument("--workspace", "-w", type=Path, default=None, help="the workspace the editor serves (default: found from the current directory)") + def writable(parser: argparse.ArgumentParser) -> None: + common(parser) + parser.add_argument( + "--open", action="store_true", + help="when the editor has another notebook open, open this one first " + "(the editor switches and the notebook's kernel restarts)", + ) + parser.add_argument( + "--wait", type=float, default=None, + help="seconds to wait for a notebook opened by --open to be ready " + "(default: SCA_EDITOR_OPEN_WAIT or 120)", + ) + ls = cell_sub.add_parser("ls", help="list the cells with their state") common(ls) ls.add_argument("--code", action="store_true", help="include each cell's code") @@ -70,26 +99,37 @@ def common(parser: argparse.ArgumentParser) -> None: get.add_argument("cell") get.set_defaults(handler=_cell_get, printer=_print_json) put = cell_sub.add_parser("put", help="replace a cell's code (the code comes from --file or stdin) and run it") - common(put) + writable(put) put.add_argument("cell") put.add_argument("--file", "-f", type=Path, default=None, help="read the code from this file (default: stdin)") put.add_argument("--no-run", action="store_true", help="save the code without running the cell") put.set_defaults(handler=_cell_put, printer=_print_json) insert = cell_sub.add_parser("insert", help="add a cell (the code comes from --file or stdin)") - common(insert) + writable(insert) insert.add_argument("--after", default="end", help="a cell, or `start` / `end` (default)") insert.add_argument("--name", default=None, help="the cell's function name") insert.add_argument("--file", "-f", type=Path, default=None) insert.add_argument("--no-run", action="store_true") insert.set_defaults(handler=_cell_insert, printer=_print_json) rm = cell_sub.add_parser("rm", help="delete a cell") - common(rm) + writable(rm) rm.add_argument("cell") rm.set_defaults(handler=_cell_rm, printer=_print_json) run = cell_sub.add_parser("run", help="run cells (default: all) and what they feed") - common(run) + writable(run) run.add_argument("cells", nargs="*") run.set_defaults(handler=_cell_run, printer=_print_json) + open_ = cell_sub.add_parser( + "open", + help="have the running editor open another notebook of this workspace", + description="Switch the editor to NOTEBOOK: the page shows it at once and its " + "kernel restarts, so `sca cell` writes go to it. Waits until the kernel is " + "up so a following write does not race the boot.", + ) + open_.add_argument("notebook", type=Path, help="the notebook file (workspace-relative, or absolute inside the workspace)") + open_.add_argument("--workspace", "-w", type=Path, default=None, help="the workspace the editor serves (default: found from the current directory)") + open_.add_argument("--wait", type=float, default=None, help="seconds to wait for the notebook's kernel (default: SCA_EDITOR_OPEN_WAIT or 120)") + open_.set_defaults(handler=_cell_open, printer=_print_json) new = subparsers.add_parser( "new", @@ -145,6 +185,37 @@ def _print_snapshot(report: dict[str, Any]) -> None: print(json.dumps(report, indent=2, ensure_ascii=False)) +def _viewport(args: argparse.Namespace) -> tuple[dict[str, Any], int]: + import time + + root = args.workspace.expanduser().resolve() + if root.is_file(): + root = root.parent + png = root / ".sca-editor" / "viewport.png" + if not png.is_file(): + # The record knows the editor's workspace even when this directory is + # one of its children; the image the person sees lives next to it. + found = _find_editor(root) + if found is not None: + png = found[0] / ".sca-editor" / "viewport.png" + if not png.is_file(): + raise ValueError( + f"{root}: no viewport image; is `sca edit` running with the page showing a model?" + ) + stat = png.stat() + report: dict[str, Any] = { + "path": str(png), + "bytes": stat.st_size, + "age_seconds": round(time.time() - stat.st_mtime, 1), + } + if args.out is not None: + target = args.out.expanduser().resolve() + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(png.read_bytes()) + report["copied_to"] = str(target) + return report, 0 + + # -- sca cell / sca new --------------------------------------------------------- @@ -247,6 +318,63 @@ def _call(url: str, path: str, body: dict | None = None, timeout: float | None = raise TimeoutError(f"the editor did not answer {path} within {seconds:g}s") from error +def _open_wait_seconds(wait: float | None) -> float: + """How long to wait for a switched notebook's kernel; ``--wait`` or + ``SCA_EDITOR_OPEN_WAIT`` seconds (a marimo boot takes a few).""" + import os + + return float(os.environ.get("SCA_EDITOR_OPEN_WAIT", "120")) if wait is None else float(wait) + + +def _wait_open(url: str, relative: str, wait: float | None = None) -> dict[str, Any]: + """Poll until the editor has *relative* open and its kernel is up. + + The switch itself lands in milliseconds; the kernel boot is the slow part, + and a write sent into it reads as "no kernel yet". Waiting here is what + makes `sca cell open` a safe step before a write instead of a race. + """ + import time + + seconds = _open_wait_seconds(wait) + deadline = time.monotonic() + seconds + while True: + report = _call(url, "/api/cells") + if report.get("notebook") == relative and report.get("kernel"): + return {"ok": True, "open": relative} + if time.monotonic() >= deadline: + return { + "ok": False, + "error": f"the editor is still opening {relative} after {seconds:g}s " + "(the switch was asked for; check `sca cell ls`; " + "SCA_EDITOR_OPEN_WAIT or --wait raises the time)", + } + time.sleep(0.25) + + +def _open_notebook(url: str, relative: str, wait: float | None = None) -> dict[str, Any]: + """Ask the editor to open *relative* and wait until it is writable.""" + current = _call(url, "/api/cells") + if current.get("notebook") == relative and current.get("kernel"): + return {"ok": True, "open": relative, "already": True} + report = _call(url, "/api/workspace/open", {"path": relative}) + if not report.get("ok"): + return report + return _wait_open(url, relative, wait) + + +def _cell_open(args: argparse.Namespace) -> tuple[dict[str, Any], int]: + start = (args.workspace or Path.cwd()).expanduser().resolve() + found = _find_editor(start) + if found is None: + raise ValueError("no editor is running on this workspace (start `sca edit`)") + workspace, url = found + notebook = args.notebook.expanduser().resolve() + if not notebook.is_relative_to(workspace): + raise ValueError(f"{args.notebook} is not in the editor's workspace ({workspace})") + report = _open_notebook(url, notebook.relative_to(workspace).as_posix(), wait=args.wait) + return report, 0 if report.get("ok") else 1 + + def _session(args: argparse.Namespace, *, write: bool) -> tuple[str | None, Path | None, str]: """``(editor url or None, notebook path or None, relative notebook name)`` for a cell command.""" start = args.workspace or (args.notebook.parent if args.notebook else Path.cwd()) @@ -278,9 +406,15 @@ def _session(args: argparse.Namespace, *, write: bool) -> tuple[str | None, Path if relative == open_path: return url, notebook, relative if write: + if getattr(args, "open", False): + report = _open_notebook(url, relative, wait=getattr(args, "wait", None)) + if report.get("ok"): + return url, workspace / relative, relative + raise ValueError(report.get("error") or f"could not open {relative} in the editor") raise ValueError( - f"the editor has {open_path} open, not {relative}: edit {relative} directly " - "(the editor does not hold it), or open it in the editor first" + f"the editor has {open_path} open, not {relative}: run `sca cell open {relative}` " + f"(or pass --open to switch and write in one step), or edit {relative} directly " + "— nothing else holds it" ) _warn(f"the editor has {open_path} open; reading {relative} from the file") return None, notebook, relative diff --git a/src/simplecadapi/editor/server.py b/src/simplecadapi/editor/server.py index 1015b14..f5efdd9 100644 --- a/src/simplecadapi/editor/server.py +++ b/src/simplecadapi/editor/server.py @@ -643,6 +643,10 @@ def cells_report(self, *, refresh: bool = True) -> dict: briefs.append(brief) return { "notebook": self.notebook.relative_to(self.workspace).as_posix() if self.notebook else None, + # Agents poll this after a switch: the name flips at once, the + # kernel is what makes a write go through instead of reading + # "no kernel yet". + "kernel": self.kernel is not None, "cells": briefs, } diff --git a/test/test_editor.py b/test/test_editor.py index d320189..12b3380 100644 --- a/test/test_editor.py +++ b/test/test_editor.py @@ -1260,3 +1260,171 @@ def test_a_failing_cell_names_its_upstream_and_the_failing_ancestor(self) -> Non self.assertEqual([row["failing"] for row in upstream], [False, False, True]) self.assertEqual([row["index"] for row in by_index[3]["upstream"]], [1, 2]) self.assertEqual([row["failing"] for row in by_index[3]["upstream"]], [False, False]) + + +class CellOpenAndViewportTest(unittest.TestCase): + """`sca cell open` / `--open`, and `sca viewport`: how an agent moves the + editor between notebooks and reaches what the person sees without reading + the server's source or calling its http api by hand.""" + + def setUp(self) -> None: + tmp = tempfile.TemporaryDirectory() + self.addCleanup(tmp.cleanup) + self.root = Path(tmp.name).resolve() + self.other = create_notebook(self.root, "other", "part") + + @staticmethod + def _fake_call(responses: list[dict]) -> object: + """A `_call` that answers from a list of canned bodies in order.""" + from unittest import mock + + return mock.patch( + "simplecadapi.editor.cli._call", side_effect=[dict(body) for body in responses] + ) + + def test_cell_open_switches_and_waits_for_the_kernel(self) -> None: + import argparse + from unittest import mock + + from simplecadapi.editor import cli + + args = argparse.Namespace(notebook=self.other, workspace=self.root, wait=5) + responses = [ + {"notebook": "gripper.py", "kernel": True}, # the editor has another notebook + {"ok": True, "opening": "other.py"}, # the switch was accepted + {"notebook": "other.py", "kernel": False}, # flipping, kernel still booting + {"notebook": "other.py", "kernel": True}, # ready + ] + with mock.patch("simplecadapi.editor.cli._find_editor", return_value=(self.root, "http://127.0.0.1:1")): + with self._fake_call(responses): + report, code = cli._cell_open(args) + self.assertEqual(code, 0) + self.assertEqual(report, {"ok": True, "open": "other.py"}) + + def test_cell_open_names_a_notebook_outside_the_workspace(self) -> None: + import argparse + import tempfile + from unittest import mock + + from simplecadapi.editor import cli + + with tempfile.TemporaryDirectory() as elsewhere: + args = argparse.Namespace(notebook=Path(elsewhere) / "elsewhere.py", workspace=self.root, wait=5) + with mock.patch("simplecadapi.editor.cli._find_editor", return_value=(self.root, "http://127.0.0.1:1")): + with self.assertRaisesRegex(ValueError, "not in the editor's workspace"): + cli._cell_open(args) + + def test_cell_open_without_an_editor_says_so(self) -> None: + import argparse + + from simplecadapi.editor import cli + + args = argparse.Namespace(notebook=self.other, workspace=self.root, wait=5) + with self.assertRaisesRegex(ValueError, "no editor is running"): + cli._cell_open(args) + + def test_a_write_with_open_switches_notebooks_in_one_step(self) -> None: + import argparse + from unittest import mock + + from simplecadapi.editor import cli + + def session(**kwargs: object) -> tuple: + args = argparse.Namespace( + notebook=self.other, workspace=self.root, open=kwargs.get("open", False), wait=5 + ) + with mock.patch("simplecadapi.editor.cli._find_editor", return_value=(self.root, "http://127.0.0.1:1")): + with self._fake_call(kwargs["responses"]): + return cli._session(args, write=True) + + # Without --open the write refuses, and the refusal names the way out. + with self.assertRaisesRegex(ValueError, r"sca cell open other\.py"): + session( + responses=[ + {"notebook": "gripper.py", "kernel": True}, + ] + ) + # With --open the editor switches first, then the write goes through. + url, notebook, name = session( + open=True, + responses=[ + {"notebook": "gripper.py", "kernel": True}, # refusal would fire here + {"notebook": "gripper.py", "kernel": True}, # the open's already-open check + {"ok": True, "opening": "other.py"}, + {"notebook": "other.py", "kernel": True}, + ] + ) + self.assertEqual(url, "http://127.0.0.1:1") + self.assertEqual(notebook, self.root / "other.py") + self.assertEqual(name, "other.py") + + def test_a_switch_that_never_settles_is_reported_not_swallowed(self) -> None: + import argparse + from unittest import mock + + from simplecadapi.editor import cli + + args = argparse.Namespace(notebook=self.other, workspace=self.root, wait=0.3) + responses = [ + {"notebook": "gripper.py", "kernel": True}, + {"ok": True, "opening": "other.py"}, + *[{"notebook": "gripper.py", "kernel": True}] * 10, # the switch never lands + ] + with mock.patch("simplecadapi.editor.cli._find_editor", return_value=(self.root, "http://127.0.0.1:1")): + with self._fake_call(responses): + report, code = cli._cell_open(args) + self.assertEqual(code, 1) + self.assertIn("still opening other.py", report["error"]) + + def test_the_cells_report_says_whether_the_kernel_is_up(self) -> None: + state = WorkspaceState(self.root) + state.notebook = self.other + self.assertFalse(state.cells_report(refresh=False)["kernel"]) + state.kernel = object() # any live kernel + self.assertTrue(state.cells_report(refresh=False)["kernel"]) + + def test_viewport_tells_the_image_and_copies_it_out(self) -> None: + import argparse + + from simplecadapi.editor import cli + + png = b"\x89PNG\r\n\x1a\nnot really a png body" + directory = self.root / ".sca-editor" + directory.mkdir() + (directory / "viewport.png").write_bytes(png) + out = self.root / "out" / "seen.png" + args = argparse.Namespace(workspace=self.root, out=out) + report, code = cli._viewport(args) + self.assertEqual(code, 0) + self.assertEqual(report["path"], str(directory / "viewport.png")) + self.assertEqual(report["bytes"], len(png)) + self.assertLess(report["age_seconds"], 10) + self.assertEqual(report["copied_to"], str(out)) + self.assertEqual(out.read_bytes(), png) + + def test_viewport_without_an_image_says_what_is_missing(self) -> None: + import argparse + + from simplecadapi.editor import cli + + args = argparse.Namespace(workspace=self.root, out=None) + with self.assertRaisesRegex(ValueError, "no viewport image"): + cli._viewport(args) + + def test_viewport_finds_the_image_from_a_child_directory(self) -> None: + import argparse + from unittest import mock + + from simplecadapi.editor import cli + + png = b"\x89PNG child" + directory = self.root / ".sca-editor" + directory.mkdir() + (directory / "viewport.png").write_bytes(png) + child = self.root / "sub" + child.mkdir() + args = argparse.Namespace(workspace=child, out=None) + with mock.patch("simplecadapi.editor.cli._find_editor", return_value=(self.root, "http://127.0.0.1:1")): + report, code = cli._viewport(args) + self.assertEqual(code, 0) + self.assertEqual(report["path"], str(directory / "viewport.png")) From f1b7cd5e839034ada00ddc2daeef9d48fabf5b4e Mon Sep 17 00:00:00 2001 From: CAD Assistant Date: Mon, 19 Oct 2026 01:20:19 +0800 Subject: [PATCH 2/3] Make cryptography a dependency for cache signing The notebook cache is marimo's LazyLoader, whose verification is on by default: without the cryptography package it degrades to off and every run warns that cache entries are neither signed nor verified. Declaring the dependency keeps verification working everywhere the cache works, not only where an extra happened to be installed. --- CHANGELOG.md | 3 +++ pyproject.toml | 1 + uv.lock | 12 ++++++++++++ 3 files changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d5f841..a63d88f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,9 @@ user-facing notes live in [`docs/updates/`](docs/updates/) (English and ### Changed +- Cache: `cryptography` ships as a dependency, so the notebook cache keeps marimo's + manifest signing and verification on instead of silently degrading to unsigned, + unverified entries (the "LazyLoader verification='on' degraded to 'off'" warning). - Editor: a refused write now names `sca cell open NOTEBOOK` (or `--open`) as the way out, and the bundled skill's editor-collaboration reference says so. diff --git a/pyproject.toml b/pyproject.toml index 54c071b..87b3286 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,7 @@ dependencies = [ "ezdxf>=1.4.4", "marimo>=0.25.0", "plotly>=7.1.0", + "cryptography>=42.0.0", ] [project.optional-dependencies] diff --git a/uv.lock b/uv.lock index 8243691..be49977 100644 --- a/uv.lock +++ b/uv.lock @@ -640,6 +640,7 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/95/1e/49527ac611af559665f71cbb8f92b332b5ec9c6fbc4e88b0f8e92f5e85df/cryptography-45.0.5.tar.gz", hash = "sha256:72e76caa004ab63accdf26023fccd1d087f6d90ec6048ff33ad0445abf7f605a", size = 744903, upload-time = "2025-07-02T13:06:25.941Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/fb/09e28bc0c46d2c547085e60897fea96310574c70fb21cd58a730a45f3403/cryptography-45.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:101ee65078f6dd3e5a028d4f19c07ffa4dd22cce6a20eaa160f8b5219911e7d8", size = 7043092, upload-time = "2025-07-02T13:05:01.514Z" }, { url = "https://files.pythonhosted.org/packages/b1/05/2194432935e29b91fb649f6149c1a4f9e6d3d9fc880919f4ad1bcc22641e/cryptography-45.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3a264aae5f7fbb089dbc01e0242d3b67dffe3e6292e1f5182122bdf58e65215d", size = 4205926, upload-time = "2025-07-02T13:05:04.741Z" }, { url = "https://files.pythonhosted.org/packages/07/8b/9ef5da82350175e32de245646b1884fc01124f53eb31164c77f95a08d682/cryptography-45.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e74d30ec9c7cb2f404af331d5b4099a9b322a8a6b25c4632755c8757345baac5", size = 4429235, upload-time = "2025-07-02T13:05:07.084Z" }, { url = "https://files.pythonhosted.org/packages/7c/e1/c809f398adde1994ee53438912192d92a1d0fc0f2d7582659d9ef4c28b0c/cryptography-45.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3af26738f2db354aafe492fb3869e955b12b2ef2e16908c8b9cb928128d42c57", size = 4209785, upload-time = "2025-07-02T13:05:09.321Z" }, @@ -649,6 +650,9 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c9/d8/0749f7d39f53f8258e5c18a93131919ac465ee1f9dccaf1b3f420235e0b5/cryptography-45.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ad0caded895a00261a5b4aa9af828baede54638754b51955a0ac75576b831b27", size = 4456087, upload-time = "2025-07-02T13:05:16.945Z" }, { url = "https://files.pythonhosted.org/packages/09/d7/92acac187387bf08902b0bf0699816f08553927bdd6ba3654da0010289b4/cryptography-45.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9024beb59aca9d31d36fcdc1604dd9bbeed0a55bface9f1908df19178e2f116e", size = 4332873, upload-time = "2025-07-02T13:05:18.743Z" }, { url = "https://files.pythonhosted.org/packages/03/c2/840e0710da5106a7c3d4153c7215b2736151bba60bf4491bdb421df5056d/cryptography-45.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:91098f02ca81579c85f66df8a588c78f331ca19089763d733e34ad359f474174", size = 4564651, upload-time = "2025-07-02T13:05:21.382Z" }, + { url = "https://files.pythonhosted.org/packages/2e/92/cc723dd6d71e9747a887b94eb3827825c6c24b9e6ce2bb33b847d31d5eaa/cryptography-45.0.5-cp311-abi3-win32.whl", hash = "sha256:926c3ea71a6043921050eaa639137e13dbe7b4ab25800932a8498364fc1abec9", size = 2929050, upload-time = "2025-07-02T13:05:23.39Z" }, + { url = "https://files.pythonhosted.org/packages/1f/10/197da38a5911a48dd5389c043de4aec4b3c94cb836299b01253940788d78/cryptography-45.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:b85980d1e345fe769cfc57c57db2b59cff5464ee0c045d52c0df087e926fbe63", size = 3403224, upload-time = "2025-07-02T13:05:25.202Z" }, + { url = "https://files.pythonhosted.org/packages/fe/2b/160ce8c2765e7a481ce57d55eba1546148583e7b6f85514472b1d151711d/cryptography-45.0.5-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:f3562c2f23c612f2e4a6964a61d942f891d29ee320edb62ff48ffb99f3de9ae8", size = 7017143, upload-time = "2025-07-02T13:05:27.229Z" }, { url = "https://files.pythonhosted.org/packages/c2/e7/2187be2f871c0221a81f55ee3105d3cf3e273c0a0853651d7011eada0d7e/cryptography-45.0.5-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3fcfbefc4a7f332dece7272a88e410f611e79458fab97b5efe14e54fe476f4fd", size = 4197780, upload-time = "2025-07-02T13:05:29.299Z" }, { url = "https://files.pythonhosted.org/packages/b9/cf/84210c447c06104e6be9122661159ad4ce7a8190011669afceeaea150524/cryptography-45.0.5-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:460f8c39ba66af7db0545a8c6f2eabcbc5a5528fc1cf6c3fa9a1e44cec33385e", size = 4420091, upload-time = "2025-07-02T13:05:31.221Z" }, { url = "https://files.pythonhosted.org/packages/3e/6a/cb8b5c8bb82fafffa23aeff8d3a39822593cee6e2f16c5ca5c2ecca344f7/cryptography-45.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9b4cf6318915dccfe218e69bbec417fdd7c7185aa7aab139a2c0beb7468c89f0", size = 4198711, upload-time = "2025-07-02T13:05:33.062Z" }, @@ -658,14 +662,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/05/2b/aaf0adb845d5dabb43480f18f7ca72e94f92c280aa983ddbd0bcd6ecd037/cryptography-45.0.5-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:7760c1c2e1a7084153a0f68fab76e754083b126a47d0117c9ed15e69e2103492", size = 4449759, upload-time = "2025-07-02T13:05:41.398Z" }, { url = "https://files.pythonhosted.org/packages/91/e4/f17e02066de63e0100a3a01b56f8f1016973a1d67551beaf585157a86b3f/cryptography-45.0.5-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6ff8728d8d890b3dda5765276d1bc6fb099252915a2cd3aff960c4c195745dd0", size = 4319991, upload-time = "2025-07-02T13:05:43.64Z" }, { url = "https://files.pythonhosted.org/packages/f2/2e/e2dbd629481b499b14516eed933f3276eb3239f7cee2dcfa4ee6b44d4711/cryptography-45.0.5-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7259038202a47fdecee7e62e0fd0b0738b6daa335354396c6ddebdbe1206af2a", size = 4554189, upload-time = "2025-07-02T13:05:46.045Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ea/a78a0c38f4c8736287b71c2ea3799d173d5ce778c7d6e3c163a95a05ad2a/cryptography-45.0.5-cp37-abi3-win32.whl", hash = "sha256:1e1da5accc0c750056c556a93c3e9cb828970206c68867712ca5805e46dc806f", size = 2911769, upload-time = "2025-07-02T13:05:48.329Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/28ac139109d9005ad3f6b6f8976ffede6706a6478e21c889ce36c840918e/cryptography-45.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:90cb0a7bb35959f37e23303b7eed0a32280510030daba3f7fdfbb65defde6a97", size = 3390016, upload-time = "2025-07-02T13:05:50.811Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8b/34394337abe4566848a2bd49b26bcd4b07fd466afd3e8cce4cb79a390869/cryptography-45.0.5-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:206210d03c1193f4e1ff681d22885181d47efa1ab3018766a7b32a7b3d6e6afd", size = 3575762, upload-time = "2025-07-02T13:05:53.166Z" }, { url = "https://files.pythonhosted.org/packages/8b/5d/a19441c1e89afb0f173ac13178606ca6fab0d3bd3ebc29e9ed1318b507fc/cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c648025b6840fe62e57107e0a25f604db740e728bd67da4f6f060f03017d5097", size = 4140906, upload-time = "2025-07-02T13:05:55.914Z" }, { url = "https://files.pythonhosted.org/packages/4b/db/daceb259982a3c2da4e619f45b5bfdec0e922a23de213b2636e78ef0919b/cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b8fa8b0a35a9982a3c60ec79905ba5bb090fc0b9addcfd3dc2dd04267e45f25e", size = 4374411, upload-time = "2025-07-02T13:05:57.814Z" }, { url = "https://files.pythonhosted.org/packages/6a/35/5d06ad06402fc522c8bf7eab73422d05e789b4e38fe3206a85e3d6966c11/cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:14d96584701a887763384f3c47f0ca7c1cce322aa1c31172680eb596b890ec30", size = 4140942, upload-time = "2025-07-02T13:06:00.137Z" }, { url = "https://files.pythonhosted.org/packages/65/79/020a5413347e44c382ef1f7f7e7a66817cd6273e3e6b5a72d18177b08b2f/cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:57c816dfbd1659a367831baca4b775b2a5b43c003daf52e9d57e1d30bc2e1b0e", size = 4374079, upload-time = "2025-07-02T13:06:02.043Z" }, + { url = "https://files.pythonhosted.org/packages/9b/c5/c0e07d84a9a2a8a0ed4f865e58f37c71af3eab7d5e094ff1b21f3f3af3bc/cryptography-45.0.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b9e38e0a83cd51e07f5a48ff9691cae95a79bea28fe4ded168a8e5c6c77e819d", size = 3321362, upload-time = "2025-07-02T13:06:04.463Z" }, + { url = "https://files.pythonhosted.org/packages/c0/71/9bdbcfd58d6ff5084687fe722c58ac718ebedbc98b9f8f93781354e6d286/cryptography-45.0.5-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8c4a6ff8a30e9e3d38ac0539e9a9e02540ab3f827a3394f8852432f6b0ea152e", size = 3587878, upload-time = "2025-07-02T13:06:06.339Z" }, { url = "https://files.pythonhosted.org/packages/f0/63/83516cfb87f4a8756eaa4203f93b283fda23d210fc14e1e594bd5f20edb6/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bd4c45986472694e5121084c6ebbd112aa919a25e783b87eb95953c9573906d6", size = 4152447, upload-time = "2025-07-02T13:06:08.345Z" }, { url = "https://files.pythonhosted.org/packages/22/11/d2823d2a5a0bd5802b3565437add16f5c8ce1f0778bf3822f89ad2740a38/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:982518cd64c54fcada9d7e5cf28eabd3ee76bd03ab18e08a48cad7e8b6f31b18", size = 4386778, upload-time = "2025-07-02T13:06:10.263Z" }, { url = "https://files.pythonhosted.org/packages/5f/38/6bf177ca6bce4fe14704ab3e93627c5b0ca05242261a2e43ef3168472540/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:12e55281d993a793b0e883066f590c1ae1e802e3acb67f8b442e721e475e6463", size = 4151627, upload-time = "2025-07-02T13:06:13.097Z" }, { url = "https://files.pythonhosted.org/packages/38/6a/69fc67e5266bff68a91bcb81dff8fb0aba4d79a78521a08812048913e16f/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:5aa1e32983d4443e310f726ee4b071ab7569f58eedfdd65e9675484a4eb67bd1", size = 4385593, upload-time = "2025-07-02T13:06:15.689Z" }, + { url = "https://files.pythonhosted.org/packages/f6/34/31a1604c9a9ade0fdab61eb48570e09a796f4d9836121266447b0eaf7feb/cryptography-45.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:e357286c1b76403dd384d938f93c46b2b058ed4dfcdce64a770f0537ed3feb6f", size = 3331106, upload-time = "2025-07-02T13:06:18.058Z" }, ] [[package]] @@ -3231,6 +3241,7 @@ version = "2.2.5" source = { editable = "." } dependencies = [ { name = "cadquery-ocp" }, + { name = "cryptography" }, { name = "ezdxf" }, { name = "jsonschema" }, { name = "marimo" }, @@ -3300,6 +3311,7 @@ requires-dist = [ { name = "black", marker = "extra == 'dev'", specifier = ">=23.0" }, { name = "cadquery-ocp", specifier = ">=7.9.3.1" }, { name = "ccx2paraview", extras = ["vtk"], marker = "extra == 'fem'", specifier = ">=3.2,<4" }, + { name = "cryptography", specifier = ">=42.0.0" }, { name = "ezdxf", specifier = ">=1.4.4" }, { name = "gmsh", marker = "extra == 'fem'", specifier = ">=4.15,<5" }, { name = "gmsh", marker = "extra == 'gmsh'", specifier = ">=4.15,<5" }, From 47fb649f64ea00c3c1662548485fdf6e21b223aa Mon Sep 17 00:00:00 2001 From: NiJingzhe <1448976235@qq.com> Date: Mon, 19 Oct 2026 01:30:37 +0800 Subject: [PATCH 3/3] release: 2.2.6 Co-Authored-By: GLM-5.3-Flash --- CHANGELOG.md | 2 ++ README.md | 22 +++++++++---------- README.zh-CN.md | 18 +++++++-------- docs/skill/SKILL.md | 4 ++-- docs/updates/2.2.6.md | 44 +++++++++++++++++++++++++++++++++++++ docs/updates/2.2.6.zh-CN.md | 39 ++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- uv.lock | 2 +- 8 files changed, 109 insertions(+), 24 deletions(-) create mode 100644 docs/updates/2.2.6.md create mode 100644 docs/updates/2.2.6.zh-CN.md diff --git a/CHANGELOG.md b/CHANGELOG.md index a63d88f..28fd320 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ user-facing notes live in [`docs/updates/`](docs/updates/) (English and ## Unreleased +## 2.2.6 — 2026-10-19 + ### Added - Editor: `sca cell open NOTEBOOK` switches the running editor to another notebook of the diff --git a/README.md b/README.md index 35adeac..ca41bcb 100644 --- a/README.md +++ b/README.md @@ -149,21 +149,20 @@ the full crank cycle sweeps clean in the same sim · --- -## Update Notes (2.2.5) +## Update Notes (2.2.6) > Validate generated definitions, assembly constraints, and manufacturing > geometry before production use. -- **`sca cell` works behind container proxies.** The editor's discovery record is loopback and - proxy-proof, an unreachable record is reported instead of silently ignored, and file - fallbacks say so on stderr. -- **Cell feedback carries the chain.** put/insert/run return console output and every cell - left unhealthy, stale dependents are run, and a failing cell names its upstream. -- **Validate a stage headlessly.** `sca run --through N / --no-product / --timeout`. -- **Quieter output, shorter prints.** Cache and VTK chatter is gone; a printed - `BRepInspection` is a one-line summary. +- **Switch notebooks without leaving the CLI.** `sca cell open NOTEBOOK` moves the editor + and waits for its kernel; the write commands take `--open` to switch and write in one + step, and a refused write names the way out. +- **`sca viewport` answers what the person sees.** The image's path, age and size, copied + out with `--out`, instead of reading `.sca-editor/viewport.png` by hand. +- **Cache entries are signed and verified again.** `cryptography` ships as a dependency, + so marimo's cache verification no longer silently degrades to off. -Full notes for this version: [`docs/updates/2.2.5.md`](docs/updates/2.2.5.md). +Full notes for this version: [`docs/updates/2.2.6.md`](docs/updates/2.2.6.md). Earlier versions: the [changelog](CHANGELOG.md) and [`docs/updates/`](docs/updates/). --- @@ -184,7 +183,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.5`. +Current release: `simplecadapi==2.2.6`. ## What It Provides @@ -699,6 +698,7 @@ and the [STEP reconstruction workflow](docs/skill/references/workflows/step-reco ## Documentation +- 2.2.6 update notes: [`docs/updates/2.2.6.md`](docs/updates/2.2.6.md) - 2.2.5 update notes: [`docs/updates/2.2.5.md`](docs/updates/2.2.5.md) - 2.2.4 update notes: [`docs/updates/2.2.4.md`](docs/updates/2.2.4.md) - 2.2.3 update notes: [`docs/updates/2.2.3.md`](docs/updates/2.2.3.md) diff --git a/README.zh-CN.md b/README.zh-CN.md index 1c76f52..d3c4a00 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -133,18 +133,17 @@ CalculiX 静力 FEM(边界面按保真的 `interface.*` 标签选取,仿真 --- -## 更新日志(2.2.5) +## 更新日志(2.2.6) > 投入生产前请验证生成的定义、装配约束和制造几何。 -- **`sca cell` 在容器代理后面也能用。** 编辑器发现记录钉在回环地址并绕过代理,应答不了的记录会报出来 - 而不是被静默忽略,降级读文件会在 stderr 上说明。 -- **cell 反馈带着整条链。** put/insert/run 返回 console 输出和每个不健康的 cell,stale 依赖会被运行, - 出错 cell 会列出上游。 -- **按阶段做无头校验。** `sca run --through N / --no-product / --timeout`。 -- **更安静的输出、更短的打印。** 缓存与 VTK 噪声消失,打印 `BRepInspection` 是一行摘要。 +- **不离开 CLI 就能切换 notebook。** `sca cell open NOTEBOOK` 切换编辑器并等 kernel 就绪; + 写命令加 `--open` 切换 + 写入一步完成,被拒绝的写入会直接指路。 +- **`sca viewport` 直接回答"用户看到什么"。** 图片的路径、age、大小,`--out` 复制出去, + 不用手读 `.sca-editor/viewport.png`。 +- **缓存条目重新有了签名验证。** `cryptography` 随包安装,marimo 缓存验证不再静默降级。 -本版本的完整说明:[`docs/updates/2.2.5.zh-CN.md`](docs/updates/2.2.5.zh-CN.md)。 +本版本的完整说明:[`docs/updates/2.2.6.zh-CN.md`](docs/updates/2.2.6.zh-CN.md)。 更早的版本:见[更新日志](CHANGELOG.md)和 [`docs/updates/`](docs/updates/)。 --- @@ -162,7 +161,7 @@ CalculiX 静力 FEM(边界面按保真的 `interface.*` 标签选取,仿真 SimpleCADAPI 是一个基于 OCP 的 Python CAD SDK,提供清晰的函数式建模操作和可重放的模型图。它在 OpenCascade 几何内核之上提供精简的公共 API,可用于创建实体、应用特征、添加语义标签、查询拓扑、导出制造文件,以及将记录的模型转换为 FreeCAD 工作流。 -当前版本:`simplecadapi==2.2.5`。 +当前版本:`simplecadapi==2.2.6`。 ## 核心能力 @@ -588,6 +587,7 @@ uv run --extra fem python examples/ap242_gmsh_volume_mesh/run_calculix.py ## 文档 +- 2.2.6 更新说明:[`docs/updates/2.2.6.zh-CN.md`](docs/updates/2.2.6.zh-CN.md) - 2.2.5 更新说明:[`docs/updates/2.2.5.zh-CN.md`](docs/updates/2.2.5.zh-CN.md) - 2.2.4 更新说明:[`docs/updates/2.2.4.zh-CN.md`](docs/updates/2.2.4.zh-CN.md) - 2.2.3 更新说明:[`docs/updates/2.2.3.zh-CN.md`](docs/updates/2.2.3.zh-CN.md) diff --git a/docs/skill/SKILL.md b/docs/skill/SKILL.md index b1798fe..e120c54 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.5 + version: 2.2.6 package-name: simplecadapi - package-version: 2.2.5 + package-version: 2.2.6 --- # SimpleCAD SDK Skill diff --git a/docs/updates/2.2.6.md b/docs/updates/2.2.6.md new file mode 100644 index 0000000..63eb985 --- /dev/null +++ b/docs/updates/2.2.6.md @@ -0,0 +1,44 @@ +# 2.2.6 Update Notes + +From the same field report as 2.2.5, the follow-up: an agent working a +multi-notebook workspace no longer has to leave the CLI to keep working — and the +cache it relies on is verified again. + +## Switch notebooks without leaving the CLI + +An agent editing `l_link.py`, `l_link_shell.py`, and an assembly had exactly one way +to move the editor between them: a raw `curl` to `/api/workspace/open`, an endpoint +found by reading the installed server's source — because a write refuses while +another notebook is open, and no command could switch. Now: + +```bash +sca cell open other.py # the editor switches; waits for the kernel to boot +echo '...' | sca cell put 3 --open # switch and write in one step +``` + +`sca cell open` waits until the switched notebook's kernel is up +(`SCA_EDITOR_OPEN_WAIT` or `--wait`, default 120s), so the following write does not +race the boot. Re-asking for the notebook that is already open and ready does not +restart the kernel. A refused write names the command as the remedy, and +`/api/cells` reports `kernel` so a caller can tell a booted notebook from one whose +name has flipped while the kernel still boots. + +## See the viewport as a command + +```bash +sca viewport --out /tmp/seen.png # path, age in seconds, size; copied out +``` + +The image of what the person sees is now `sca viewport`'s answer instead of a +`.sca-editor/viewport.png` file agents had to know about and read by hand. The age +says how fresh the view is; there is no image until the page showed a model at +least once. + +## Cache entries are signed and verified again + +The notebook cache is marimo's `LazyLoader`, whose verification is on by default: +without the `cryptography` package it degraded to off and every run warned that +cache entries were neither signed nor verified. `cryptography` ships as a +dependency now, so verification stays on everywhere the cache works. + +See the [changelog](../../CHANGELOG.md) for the complete list. diff --git a/docs/updates/2.2.6.zh-CN.md b/docs/updates/2.2.6.zh-CN.md new file mode 100644 index 0000000..a2fad8b --- /dev/null +++ b/docs/updates/2.2.6.zh-CN.md @@ -0,0 +1,39 @@ +# 2.2.6 更新说明 + +和 2.2.5 来自同一份实地报告的续篇:agent 在多 notebook 工作区里干活,不用再绕开 CLI +才能继续——它依赖的缓存也重新有了签名验证。 + +## 不离开 CLI 就能切换 notebook + +一个 agent 要同时编辑 `l_link.py`、`l_link_shell.py` 和一个装配体,把编辑器在它们之间 +切换的办法只有一个:对着 `/api/workspace/open` 发裸 `curl`——这个端点是它读了安装包里 +server 的源码才找到的,因为编辑器开着别的 notebook 时写入会被拒绝,而没有任何命令能切换。 +现在: + +```bash +sca cell open other.py # 编辑器切换过去;等 kernel 启动完成 +echo '...' | sca cell put 3 --open # 切换 + 写入一步完成 +``` + +`sca cell open` 会等到切换后的 notebook kernel 就绪(`SCA_EDITOR_OPEN_WAIT` 或 +`--wait`,默认 120s),后续写入不会和启动赛跑。对已经打开且就绪的 notebook 再次执行 +不会重启 kernel。写入被拒绝时报错会直接给出这条命令,`/api/cells` 也会报告 `kernel`, +让调用方分得清"名字已切换"和"kernel 还在启动"。 + +## 像命令一样拿视口图 + +```bash +sca viewport --out /tmp/seen.png # 路径、age(秒)、大小;复制出来 +``` + +"用户现在看到什么"的图,现在是 `sca viewport` 的输出,而不是一个要 agent 自己知道、 +自己读的 `.sca-editor/viewport.png` 文件。age 说明画面有多新鲜;页面从未显示过模型时 +没有图。 + +## 缓存条目重新有了签名验证 + +notebook 缓存是 marimo 的 `LazyLoader`,默认开着验证:缺 `cryptography` 包时会降级关 +闭,每次运行都警告"cache entries are neither signed nor verified"。现在 `cryptography` +作为依赖随包安装,缓存能工作的地方验证就开着。 + +完整清单见[更新日志](../../CHANGELOG.md)。 diff --git a/pyproject.toml b/pyproject.toml index 87b3286..719f257 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "simplecadapi" -version = "2.2.5" +version = "2.2.6" description = "A simplified OCP-native CAD modeling Python API" readme = "README.md" license = { file = "LICENSE" } diff --git a/uv.lock b/uv.lock index be49977..5634b28 100644 --- a/uv.lock +++ b/uv.lock @@ -3237,7 +3237,7 @@ wheels = [ [[package]] name = "simplecadapi" -version = "2.2.5" +version = "2.2.6" source = { editable = "." } dependencies = [ { name = "cadquery-ocp" },