From 747245a75fb8befee3349953d373caa0c27acb9c Mon Sep 17 00:00:00 2001 From: buyun14 <112449589+buyun14@users.noreply.github.com> Date: Sun, 20 Sep 2026 19:21:49 +0800 Subject: [PATCH] docs(automation): document the native entry points and their limits 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. --- docs/automation/README.md | 4 ++ docs/automation/native.md | 107 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 docs/automation/native.md diff --git a/docs/automation/README.md b/docs/automation/README.md index 1dcf4a90e..8293aec55 100644 --- a/docs/automation/README.md +++ b/docs/automation/README.md @@ -17,6 +17,10 @@ The server provides four tools: - `ocs_execute` performs one operation, an atomic record update, or a sequential batch against the real editor. - `ocs_capture` returns a bounded PNG of the drawing viewport or complete window. +Native builds also accept a second, headless entry point (`OpenCADStudio --serve`, one +JSON request per line) for scripts and CI; the two native channels, their startup dialogs +and their limits are described in [native.md](native.md). + The normal flow is to call `ocs_sessions`, pass its returned `session_id` as `ocs_session_id` to the other tools, read the chosen session with `ocs_read`, then pass the returned document state into `ocs_execute`. For example, an undo request has this shape: ```json diff --git a/docs/automation/native.md b/docs/automation/native.md new file mode 100644 index 000000000..3d3c9f9a7 --- /dev/null +++ b/docs/automation/native.md @@ -0,0 +1,107 @@ +# Driving the native builds + +Every native build exposes the same control core through two entry points: the MCP +server described in the [README](README.md) (`OpenCADStudio --mcp`), and a headless +JSON-line channel meant for scripts and CI (`OpenCADStudio --serve`). The operations, +request envelope and replies are the same; this page covers what is specific to the +native side. The browser entry is described in [web.md](web.md). + +## Choosing a channel + +| | `--mcp` | `--serve` | +|---|---|---| +| Transport | MCP over stdio | one JSON request per line on stdin, one reply per line on stdout | +| Editor | starts the desktop editor and exposes the live document | no window; works on its own document | +| Needs a display | yes — on a headless machine `xvfb-run` works, on software rendering the viewport may show the GPU warning dialog | no | +| `measure`, `ocs_capture`, `set_properties`, interactive steps | available | `measure` and `capture` are not; the remaining operations are | +| Good for | an MCP client driving a real editor | batch jobs, CI, agents without a display | + +`OpenCADStudio --export IN OUT` converts a drawing in one shot, without any session. It +accepts `.dwg` and `.dxf` targets only. + +## Requirements + +The process needs a writable `HOME` (or `XDG_CONFIG_HOME`) — without one, `ocs_sessions` +answers `No user configuration directory` and nothing else works. + +## Startup dialogs + +A fresh profile opens two dialogs before any drawing exists, and both of them block +document creation: + +| Step | `ocs_sessions` reports | +|---|---| +| right after launch | `modal: "AssocPrompt"` | +| after `close_modal` | `modal: "DonationPrompt"` | +| after a second `close_modal` | `modal: null` | + +While a dialog is open, `{"op":"new"}` answers `ok` but no tab is created, so there is +no `document_id` to work with. Close the dialogs first: + +```python +while (info := sessions())["modal"]: + execute({"op": "action", "name": "close_modal", "document_id": info["document_id"]}) +``` + +The donation prompt appears once per application version per profile, so an automated +profile meets it once per version rather than on every run. See +[web.md](web.md) for the same pattern on the browser side. + +## Targeting a document + +Reads and mutations target the active tab. `new` creates a document but does not move +the active tab, so pass the `document_id` from the reply explicitly — without it, reads +answer empty and it looks like nothing was created. `activate` switches the active tab. +A client connection to `--mcp` starts a new editor instance, so session state does not +carry over between connections. + +## Feeding a whole command line + +`run` takes one complete command line, including the answers to every prompt, and then +terminates the command as if Enter were pressed. A prompt cannot be continued on a +following line — each line is parsed as a fresh command line, so a command left waiting +is replaced by the next one (send `{"op":"cancel"}` if you want to drop it explicitly). + +`waiting_input` means input is still required — not that nothing happened. `SPLINE` and +`ARC`, for example, keep accepting further input even after the entity exists, and +`zoom_extents` reports it while the view has already changed. Send `{"op":"cancel"}` to +close the command when the line is done with it. + +Two reply fields describe what happened to the line: + +| Field | Meaning | +|---|---| +| `unconsumed` | Tokens that no prompt ever asked for, in order. `CIRCLE 0,0 5 9` reports `["9"]`, because the radius step ends the command; a fully consumed line reports `[]`. | +| `blocked_by` | What *this line* left open: `"command"`, `"text_editor"`, `"mtext_editor"`, `"modal:"`, or `null`. Set whenever `status` is `waiting_input`. | + +`TEXT 0,0 5 0 hello world` therefore creates one `Text` entity that holds +`hello world`, reports `completed` with `added: 1`, and leaves nothing unconsumed. The +content step runs in the in-place editor, which the feeder commits and then closes so +that the command does not stay armed for a second line. + +## Interactive steps + +For commands whose answers cannot be written on one line, use `start` and then `input`. +`start` replies with `state.command`, whose `accepts`, `options`, `prompt` and +`input_example` say what the current step wants: + +| Step | Request | Note | +|---|---|---| +| a point | `{"op":"input","kind":"point","point":[x,y,0],"space":"wcs"}` | the point is an array; separate `x`/`y` fields are rejected | +| a typed value (height, rotation) | `{"op":"input","kind":"text","text":"5"}` | digits go through `text`, not `token`; `{"op":"input","kind":"enter"}` accepts the default | +| a keyword (`J`, `ST`, `C`) | `{"op":"input","kind":"token","text":"C"}` | the payload field is named `text` | +| the text of `TEXT`/`MTEXT` | `{"op":"action","name":"text_input","value":"hello"}` then `{"op":"action","name":"text_commit"}` | the command has already ended at that point and the editor owns the input | + +## Native-only operations + +| Operation | Device | +|---|---| +| `measure` | `ocs_read` with `op: "measure"` and `parameters.handles` returns kernel-computed length, area, bounds and mass properties. | +| `ocs_capture` | Returns the viewport or the whole window. Either an inline base64 image, or a PNG written under the temporary directory with the reply carrying its `path`. | +| `set_properties` | Mutates record properties. It requires `collection`, and colours are the serialised enum — `{"Index": 1}` or `"ByLayer"`; a colour name such as `"Red"` is rejected with `invalid_value`. | + +## Known gaps + +On the `--serve` path, `MTEXT`, `HATCH` and `POLYGON` report `completed` with +`added: 0`: they need interactive surfaces (a pattern choice, a boundary selection) that +a single command line has no tokens to fill. `TEXT` no longer belongs to that list.