Color converts as {'kind': ByLayer|None|ByBlock|Index|Rgb, 'value': ...}.
Leader.override_color is now writable; neither DWG nor DXF stores it, which
is recorded as a fourth persistence blocker with a canary assertion.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Adds a hand-written flat Dimension model to the adapter (manifest
manual_kinds hook) and a synthetic-property section to the host coverage
policy. The host derives actual_measurement and the base definition point on
scripted create/edit. staged_python_dimension_lifecycle_over_real_ipc passes
the full gate for every subtype including DWG and DXF reopen. Python ints are
now accepted for coordinates and angles.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Maps vertices, flags, justification, normal, scale and style with host
validation and MLineStyle binding. The host recomputes vertex direction,
miter and element parameters on scripted create/edit, so scripts only supply
positions. staged_python_mline_lifecycle_over_real_ipc passes the full gate
including DWG and DXF reopen.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Refs #1349. The automation README describes `--mcp` and never mentions
`--serve`, even though every native build accepts it and it is the only channel
that works without a display. It also leaves out the two startup dialogs, the
`document_id` routing, and what `waiting_input` actually means.
`docs/automation/native.md` covers, all measured on a Linux headless machine
with `OpenCADStudio 2026.37+165`:
- choosing between `--mcp` and `--serve`, and what each needs (display,
no-display, `--export` for one-shot conversion);
- the writable `HOME`/`XDG_CONFIG_HOME` requirement, without which
`ocs_sessions` answers `No user configuration directory`;
- the startup dialogs: `AssocPrompt` then `DonationPrompt`, `new` answering
`ok` with no document until `close_modal` clears them, and that the donation
prompt appears once per application version per profile;
- targeting a document: reads and writes follow the active tab, so
`document_id` has to be passed explicitly after `new`;
- feeding a whole line: `waiting_input` means more input is required (not that
nothing happened), `cancel` closes a command, and the `unconsumed` /
`blocked_by` fields added in #1392;
- the interactive `start`/`input` shapes, including that a typed value goes
through `kind: "text"` while a keyword uses `kind: "token"` with its payload
in a field named `text`, and that the `TEXT` content step belongs to the
in-place editor (`text_input` / `text_commit`);
- what only native builds have: `measure`, `ocs_capture` (inline base64, or a
PNG path), and `set_properties` with `collection` and colours as
`{"Index": n}` / `"ByLayer"`;
- the remaining gap: `MTEXT`, `HATCH` and `POLYGON` report `completed` with
`added: 0` on the `--serve` path (#1390).
The README gains one line pointing to the page, placed after the tool list
rather than at the end, so it does not overlap #1394.
Docs only; no code changes.
`run_command_line` feeds the tokens after the command verb to the interactive
command and then terminates it as if Enter were pressed. Two gaps left a
headless caller unable to tell what actually happened.
In-place text editor takes over, remaining tokens were dropped
----------------------------------------------------------------
The `TEXT` content step hands over to the in-place editor, at which point the
command has already ended (`active_cmd` is `None`). The feed loop stopped
there, so the rest of the line — including the text itself — vanished:
`TEXT 0,0 5 0 hello` reported `completed` with `added: 0` and created nothing.
The tail is now typed into that editor and committed, and the commit is done
synchronously so its result is observable: `text_inline_commit()` returning
false leaves the tokens in `unconsumed` instead of reporting a successful line.
Dispatch the commit as a queued message instead and a failed commit would look
exactly like a good one — the text dropped, `added: 0`, `unconsumed: []`.
Only an editor this line opens is taken over, and after a successful commit the
line is closed with an escape: `TEXT` re-arms for the next line
(`open_next_line`), which would otherwise let the *next* batch line be eaten as
the next text position.
Leftover tokens are dropped
---------------------------
Any token no prompt ever asked for is recorded in `CommandLine::unconsumed` and
surfaced by the automation `run` response, so over-typed or misplaced input is
visible instead of silently ignored: `CIRCLE 0,0 5 9` reports
`"unconsumed": ["9"]` while a fully consumed line reports an empty list.
Pending surfaces are reported instead of `completed`
----------------------------------------------------
`run` now carries `blocked_by` — `"command"`, `"text_editor"`,
`"mtext_editor"`, `"modal:<Kind>"`, or `null` — and answers `waiting_input`
when something is still open rather than claiming `completed`.
`blocked_by` is attributed to the line that opened the surface: a condition and
its snapshot are taken before the command runs, so an editor left open by an
earlier line, another tab, or a startup dialog does not turn a finished command
into `waiting_input`. Without that attribution a caller polling for `completed`
would wait forever on someone else's editor. `docs/automation` documented
`waiting_input` as "more input is required", which is exactly what a pending
surface is; the two new fields are documented there as well.
Tests
-----
* `batch_text_line_creates_text_and_consumes_every_token` — the regression case
creates exactly one `Text` entity and leaves nothing unconsumed.
* `multi_word_text_keeps_the_spaces` — asserts the committed string, so an
off-by-one in the consumed index or a wrong join cannot pass.
* `run_reports_leftover_tokens_instead_of_dropping_them`.
* `text_without_content_reports_the_open_editor`.
* `a_stray_editor_is_not_hijacked_by_the_next_line` — the next line neither
types into the stray editor nor inherits its blocker (`blocked_by: null`,
`status: completed`).
* `a_command_still_waiting_is_named_as_the_blocker`.
Verified over the `--serve` protocol: `TEXT 0,0 5 0 hello world` → `completed`,
`added: 1`, and the entity really holds `hello world`; `TEXT 40,0 5 0` →
`waiting_input` + `blocked_by: "text_editor"`; the following `CIRCLE 0,0 5 9` →
`completed`, `blocked_by: null`, `unconsumed: ["9"]`; `LINE` → `waiting_input`
+ `blocked_by: "command"`.
Full lib suite: 1497 passed, 2 failed — both failures
(`io::plot_style::…named_tables_and_missing_files_get_a_reason`,
`scene::parametric_constraints::…arc_grips_drive_center_start_and_end_but_not_midpoint`)
reproduce on the unmodified tree; run with `LC_ALL=C` since several xref tests
otherwise fail on a non-English locale.
Known remaining gap, unchanged here: `MTEXT` / `HATCH` still answer `completed`
with `added: 0` in this path (they need interactive surfaces the feeder has no
tokens to fill). Reported separately in #1390.
Adds Leader validation and annotation/dimension-style reference checks to
ocs_plugin_api, maps 15 writable and 5 read-only fields in the adapter, and
adds staged_python_leader_lifecycle_over_real_ipc covering C/R/E/D/U/I/V/P.
The W gate stays open: acadrust's DXF writer omits the annotation handle and
DXF reads drop annotation_offset; DWG R2010+ does not store text size or the
hookline flag. Each gap is pinned by a canary assertion.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Describe the two JavaScript functions the web build exposes
(ocs_control_submit / ocs_control_take), how to poll them, the queue and reply
limits, opening a drawing from bytes with data_base64, and closing the startup
dialog before new. Everything is taken from src/app/control/mod.rs,
src/app/startup.rs and docs/automation/web_save_smoke.cjs.
Before and after captures of DIMLINEAR at the second-point and placement
stages and DIMANGULAR at arc placement, taken from the upstream base and
this branch with the same automation steps.
Refresh associations after complete SpaceMouse camera updates and recognize imported paper-space owners without Layout objects. Use kernel containment, intersection, perpendicular and spline tangent solving; pin cadkernel 7ce27f2 and add regressions.
Found while building an out-of-tree scripting plugin from scratch against
this doc and the actual ocs_plugin_api/host source. Two categories:
Fixed outright (docs/examples didn't match the real code):
- The worked CadModule example returned Vec<RibbonGroup> from
ribbon_groups(); the real trait requires &[RibbonGroup] - the example
wouldn't compile. Fixed with the OnceLock pattern the host's own
benchmark code already uses.
- Reconciled a contradiction between "process isolation removes the need
for a shared Rust ABI" and the exact-rustc_version requirement described
a few paragraphs earlier in the same doc.
- The roadmap's "Next" list still had external automation as unstarted;
--mcp/--serve are real and operational (src/cli.rs, src/mcp.rs).
- Added the missing plugin.toml to docs/plugin-template-v2 (its own
DESIGN.md already claimed one could be installed).
- Documented that plugin-template's CI floats rust-toolchain@stable, which
doesn't guarantee matching the host's exact rustc - added acadrust_source
to both templates' plugin.toml plus the release.yml step that fills it
from Cargo.lock's resolved acadrust source, alongside the existing
rustc_version fill.
Flagging for your call, not fixed here (found the behavior, not sure it's
the intended one, prefer to ask than guess at a fix):
- acadrust_compatible()'s ABI gate only runs at all if plugin.toml declares
acadrust_source - an undeclared field is silently treated as compatible,
unlike rustc_version (undeclared = incompatible). The same shortcut
appears in two places: ExternalPlugin::acadrust_compatible
(src/plugin/external.rs) and the inline check inside fetch_release_info
(src/plugin/marketplace.rs). If this is deliberate (e.g. back-compat with
manifests from before the field existed), no action needed beyond a
comment; if not, both call sites need the same fix.
- The command line still uppercases typed/pasted input even with the `>`
literal-spaces prefix (Message::CommandInput, src/app/update/mod.rs) -
`>` only stops a space from auto-submitting the line, it doesn't skip
`s.to_uppercase()`. A plugin command needing a case-sensitive typed
argument has no escape today short of automation.
- fetch_release_info (src/plugin/marketplace.rs) returns every release
under a linked repo with no filter on the manifest's own id field (which
it already parses to build ReleaseInfo) - a repo hosting more than one
plugin under tag-namespaced releases surfaces all of them mixed
together in one plugin's version list.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Builds from main reported only the release version (2026.36) even when
HEAD was hundreds of commits past the tag. build.rs now also emits
OCS_TAG_DISTANCE, OCS_COMMIT_DATE, OCS_BUILD_METADATA and
OCS_FULL_VERSION: a clean checkout of the release tag stays "2026.36",
anything else becomes "2026.36+194.gef189d77", with ".dirty" appended
for uncommitted changes and only the hash when no tag can be counted
from (shallow clone, tarball). OCS_APP_VERSION is unchanged so the
update check, donation gate, User-Agents and window title keep working.
The About dialog gains Build / Commit date / Profile cards, Copy Info,
the REPORT issue template and recovery reports carry the full version,
and `-V`/`--version` print it instead of Cargo's 2026.36.0. Release
workflows check out full history so release binaries see the tag.
Also fixes the build being marked dirty when there is no repository:
diff-index exits 128 there, not 1.
Co-Authored-By: Claude Opus 5 <[email protected]>