# Control protocol > **Last updated:** 2026-10-13 · **Change:** synthetic input budgets (#1709); bounded transport waits (#1726). `vectorcraft --control ` listens on `127.0.0.1:` (loopback only). The port must be a number from 1 to 65535 (`--control=` works too), given once; otherwise the app prints an error and exits before it starts. One JSON request per line: `{"id": 1, "method": "ui.inspect", "params": {}}` → `{"id": 1, "ok": true, "result": {...}}` or `{"id":1,"ok":false,"error":"..."}`. Set `VECTORCRAFT_CONFIG_DIR` to a dedicated directory for an isolated native test profile. Preferences, logs, recovery and user libraries then use that root, without importing the legacy DrawCraft profile. An unset override preserves the usual platform paths and legacy migration; an explicitly empty override disables those paths. `VECTORCRAFT_NO_PREFS` continues to disable preference reads and writes. **Only requests are read.** Every line must be a JSON object with a string `method` (`id` and `params` are optional; blank lines are skipped). Anything else gets one error reply (`{"ok": false, "error": "… closing the connection"}`) and the server **closes the connection**, so nothing sent after it on that connection runs. That covers text that isn't JSON, a JSON array or number, an object without `method`, invalid UTF-8, and a line longer than 4 MiB. An HTTP request (for example a web page's cross-origin `fetch` to `127.0.0.1:`) therefore can't smuggle a command in its body: its request line is rejected first. At most 16 connections are served at once; further ones get an error line and are closed. Clients that get an error reply should reconnect. The port has no authentication, so only enable it while you use it. Idle clients have 5 minutes to start their next line; once the first byte arrives, the complete line must arrive within an absolute 30 seconds (trickled bytes do not renew it). A reply has 60 seconds to be written. Expiration closes the connection and returns its slot (an idle connection is closed without a line, so nothing waits where the next reply would be; a line that stalls gets the usual error line first); persistent clients can send further requests within these bounds, or reconnect after idle expiration. These transport budgets are separate from the existing 60-second command-processing wait. Transport: `apps/vectorcraft/src/control_server.rs`. ## Synthetic input budgets `ui.drag` accepts at most **256** `steps`; `ui.click` and `ui.drag` accept at most **16** `count`. They must be non-negative JSON integers. Omitted values keep their defaults (8 steps, one click); explicit zero still acts as one, as before. Oversized or malformed values return an error before any move, press or release is queued. All synthetic methods (`ui.key`, `ui.text`, `ui.move`, `ui.click`, `ui.drag`, `ui.wheel`) share a **1,024-event** pending-queue limit. The whole request must fit; rejection preserves existing input and does not append a partial gesture. Clients can retry once earlier input has drained. At most **64 control requests** run per frame; later requests remain on the channel and another frame is scheduled. These limits cover synthetic input admission, separately from socket transport and command-processing timeouts. A request the app hasn't answered after **60 s** gets `{"ok": false, "error": "timeout: …"}`. If the app had not started it (it was busy, or its window wasn't being drawn), the request is withdrawn and never runs, and the error says "will not run": retrying is safe. If the app was already running it, the error says "still running": it may yet complete, so check the document before retrying. ## Confining file access ```sh vectorcraft --control 7979 --automation-read-root /work/project --automation-write-root /work/project/out ``` `--automation-read-root ` and `--automation-write-root ` (or `VECTORCRAFT_AUTOMATION_READ_ROOT` / `VECTORCRAFT_AUTOMATION_WRITE_ROOT`; `--flag=` works too) confine what the control channel reads and writes, with the same flags as `vectorcraft-cli mcp` and the other Craft apps. Every path a request reads (open, place, relink, library and profile loads, plug-ins…) must lie inside the read root, every path it writes (save, export, package, captures, library saves…) inside the write root; anything else is an ordinary error reply. Paths are made absolute against the app's working directory, links are followed (for a file that doesn't exist yet, its folder's), and the comparison is by whole folder names (`/work2` is not inside `/work`), ignoring case on Windows; `..` below a folder that doesn't exist, a link that leads nowhere and Windows device names are refused. A root left out grants none of its access. The roots must be existing folders, or the app doesn't start; without `--control` they are ignored. See `docs/mcp.md` › Confining file access for the details. Each root flag may be given once; a command-line root overrides its environment default. Missing or empty values, another option used as a value, and duplicate flags stop the desktop before it starts any services, even without `--control`. For a folder name starting with `-`, use `--flag=-folder` or the relative path `./-folder`. What is confined: the requests themselves, the frames that carry input they inject (`ui.click`, `ui.key`, `ui.text`…, so a menu item an agent clicks is confined too), the background saves and exports they start, and window captures. The person at the keyboard is not: files opened from the command line, Finder or a file dialog, and everything done with the mouse and keyboard, work as usual. The preferences that move folders the app reads or writes on its own (Additional Fonts Folder, Additional Plug-ins Folder, Data Recovery's folder, the Templates folder) can only be pointed inside the roots by automation. The app's own folders (preferences, Data Recovery, the library folders, VectorCraft's Fonts folder) are not confined, since no agent names a path there. A path is checked, then opened: another program that swaps in a link between the two can still win that race. | Method | Params | | |---|---|---| | `engine.execute` | `{command, params}` | run any engine or UI command (see `engine.commands`). A key of `params` that the command's params description doesn't name is an error naming it and the nearest key the command takes, before anything runs (`unknown parameter `widht`; did you mean `width`?`); see [Parameter keys](mcp.md#parameter-keys) | | `engine.commands` | | every command with label, shortcut, params doc, enablement | | `document.inspect` | `{depth?, childLimit?}` | layer tree, selection, the key object (`key`, null without one), history, paint defaults; the options slice the layer tree as `document.node {summary: true}` does. Each node reports `opacity` below 1 and `blend` when it isn't Normal; objects and compound paths report `fill`, `stroke` and `strokeWidth`, a compound path's members `compoundMember: true` instead | | `ui.inspect` | | tool, UI state, `screenMode` (0 normal, 1 full screen with menu bar, 2 full screen, 3 Presentation Mode), `taskBar` (`pinned`, and `rect` `[x, y, w, h]` while the Contextual Task Bar shows), `freeTransformWidget` (`[x, y, w, h]` while the Free Transform widget shows), view, canvas rect, window size, perf, background saves and exports still running, `nativeMenuBar` (true when the menus are in the macOS menu bar rather than the window; `ui.menu.list` lists the in-window menus either way), `fileDialog` (`"open"`, `"place"`, `"save"` or `"folder"` while a native file dialog is open, `null` otherwise; see below) | | `ui.menu.list` / `ui.menu.invoke` | `{command, params}` | the full menu tree / invoke an item | | `ui.contextMenu.list` | | the canvas context menu for the current selection, flattened like `ui.menu.list` (`path` holds its submenus). `ui.click {x, y, button: "right"}` on the canvas opens it, after selecting the object there unless it is already selected | | `ui.tool.select` / `ui.tool.list` | `{tool}` | | | `ui.pointer` | `{events:[{kind: down|drag|up|move|doubleclick, x, y, space?: "doc"|"screen", mods?}]}` | drive the active tool exactly like the mouse: `mods.cmd` held at a press with any tool but a selection tool drags with the selection tool chosen last (Direct Selection with the Pen until one is), and the release gives the tool back as it was; the reply lists `selection`, `key` (the key object, null without one) and `tool` | | `ui.key` / `ui.text` | `{key, shift?, alt?, cmd?}` / `{text}` | synthetic keyboard input | | `ui.move` | `{x, y}` | screen-point pointer move | | `ui.click` / `ui.drag` | `{x, y, button?, count?, shift?, alt?, ctrl?, cmd?}` / `{x, y, toX, toY, steps?, count?, button?, shift?, alt?, ctrl?, cmd?}` | egui pointer input; see synthetic input budgets above | | `ui.wheel` | `{x, y, dy?, dx?, unit?: "line"\|"point", shift?, alt?, cmd?}` | a mouse wheel turn over screen point (x, y): `dy` notches up (+) or down, `dx` sideways. Over the canvas the wheel scrolls and Cmd- or Alt-wheel (Option on the Mac) zooms about the pointer; with the `zoomWithMouseWheel` preference the wheel and Alt-wheel zoom about the pointer, Shift-wheel scrolls up and down and Cmd-wheel (Ctrl on Windows and Linux) sideways. Over a focused numeric field (click it first) each notch steps its value as Up/Down do (Shift: ten, Cmd/Ctrl: a tenth) and the panel stays put; over anything else in a panel the wheel scrolls it | | `ui.set` | `{brightness?, panel?, rulers?, outline?, grid?, smartGuides?, boundingBox?, controlBar?}` | | | `ui.dialog.set` / `.confirm` / `.cancel` | `{field, value}` | fill and submit the open dialog | | `ui.screenshot` | `{path?, data?}` | capture the window (PNG), written to `path`; `data: true` sends it back as `pngBase64` too. Needs a presented frame: with the screen locked or the window minimized/covered it fails after ~8 s with an explanatory error | | `ui.render` | `{path?, scale?, artboard?, data?}` | render an artboard headlessly (PNG; `artboard`: its 0-based index, default the first): written to `path`, else (or with `data: true` as well) sent back as `pngBase64` | | `ui.resize` / `ui.focus` | | | | `app.open` / `app.save` / `app.export` / `app.quit` | `{path}` / `{path?}` / `{path?, format?, artboard?, range?, scale?, …}` | `app.open` reads every format `document.open` reads (see `document.formats`) and returns its result (`{index, title, format, warnings, …}`; `warnings` say what didn't come in as it was, such as an EPS shown as its preview image and why its PostScript couldn't be read), or null when a dialog asks first or a library loads. `app.export` encodes through the engine's `document.export` (same options; the document keeps its path) and writes `path` through the host; without `path` it returns `{dataBase64, format, bytes}`, as headless mode does. `app.quit`, `file.close` and `file.closeAll` first open a `saveChanges` dialog for each modified document (they return `{"pending": "saveChanges"}`): `ui.dialog.confirm` saves, `ui.dialog.set {field: "discard", value: true}` then confirm discards, `ui.dialog.cancel` cancels the whole close or quit | | `app.export` with `useArtboards` | `{path, useArtboards: true, range? \| artboards?, …}` | Export As: PNG, JPEG and WebP write one file per chosen artboard (default all; SVG does so for a `range`), `-.`, and return `{path, files: [path…]}`; a PDF keeps them as pages of `path`. `useArtboards: false` covers the bounds of the visible art. The menu's Export As… is `file.exportAs`: the `exportAs` dialog (`format`, `useArtboards`, `all`, `range`), then a save dialog and the format's options: `pngOptions` / `jpgOptions` / `webpOptions`, `svgOptions`, or `savePdf` for a PDF with Use Artboards | | `ui.dialog.*` on `gradientStop` | `{field: "color" \| "opacity" \| "location", value}` | double-clicking a stop on the Gradient tool's annotator opens its popover (fields `index`, `x`, `y`, `tab`); set `color` (hex), `opacity` or `location` (percentages) and `ui.dialog.confirm` to apply them to the selected stop. `gradient.selectStop {index}` picks the stop the annotator, the panels and Delete/←/→ (`ui.key`) act on | | `engine.execute` `ui.savePdfDialog` | `{path?, preset?, range?, …document.exportPdf options}` | opens the Save PDF dialog (`file.export.pdf` with no params opens it too). Its fields are the `document.exportPdf` options: set `compatibility` with `ui.dialog.set {field: "compatibility", value: "1.5"}`, a whole section with `{field: "compression", value: {…}}`, the artboards with `__allArtboards: false` and `range`, the visible section with `__section` (General, Compression, Marks and Bleeds, Output, Advanced, Security, Summary). `ui.dialog.confirm` writes `path` (else asks for one), returns `{path, bytes, warnings}` and opens the file when `viewAfterSaving` is set | | `file.save` / `file.saveAs` / `file.saveCopy` / `file.saveAsTemplate` (via `engine.execute`) | `{path?, format?, options?, svg?}` | Save writes the document's own file in its own format (a document opened from or saved as SVG/PDF saves as that again; the result's `warnings` say what the format loses). With `path` the file is written at once (Save As or a Copy to an SVG path without SVG options first opens SVG Options, below). Without one (never saved, a converted older file, Save As, a Copy, a Template) the app shows a save panel with one file type per save format (`vectorcraft`, `template`, `pdf`, `svg`, `svgz`), then the format's options: SVG opens the `svgOptions` dialog (below; returns `{"pending": "svgOptions", "path"}`), PDF the `savePdf` dialog (its `__save` field names the save command); `ui.dialog.confirm` finishes the save. On the web a `saveOptions` dialog (fields `path`, `format`) first names the file and picks the format. `file.formatOptions {format?}` reads the options a save would use. `file.revert` opens a `confirm` dialog (`{"pending": "confirm"}`); `ui.dialog.confirm` reloads the saved file in the same tab | | SVG Options (`engine.execute` of `file.export.svg`, `file.saveAs`, `file.saveCopy`) | `{}` / `{path?, svg?: {…}}` | `file.export.svg` without params, and Save As or Save a Copy to an `.svg`/`.svgz` path without SVG options, open the `svgOptions` dialog (`ui.dialog.set` its fields: `styling`, `outlineText`, `images`, `objectIds`, `decimals`, `minify`, `responsive`, `fewerTspans`, `metadata`, `preserveEditing`, `useArtboards`, `allArtboards`, `range`, `showCode`, `profile` (`svg11`/`tiny12`), `encoding` (`utf8`/`utf16`/`latin1`), `embedFonts`; `ui.dialog.confirm` exports or saves and remembers the choices). With `svg: {…}` (the `document.formats` SVG options) they run directly. `app.save {path?, svg?}` takes the same options; a document saved as SVG saves as SVG again with the same options | | `engine.execute` `file.place` | `{paths?}` / `{path \| name+dataBase64, …file.place params}` | with no file, picks files and opens the `place` dialog; with `paths`, opens it for them (fields `files`, `link`, `template`, `replace`; `__replace` says whether Replace applies, `__info` describes each file). `ui.dialog.confirm` places one file centred in the view (or in place of the selected object) and loads the place cursor with several (`file.place.queue`). With a file, it runs `file.place` centred in the view. Files dropped on the window with no document open open. On the web, files dropped on the canvas are placed at the drop point (linked; Shift embeds) and files dropped off it (the tab bar) open, as in Illustrator; desktop drops carry no pointer position, so documents (native, SVG, PDF, `.ai`, EPS, DXF, EMF/WMF) open as tabs of their own and pictures and text are placed in the middle of the view. Swatch libraries and color books (`.vcswatches`, `.gpl`, `.ase`, `.acb`), graphic style libraries (`.vcstyles`), Libraries panel files (`.vclibrary`), presets (`.vcflattener`, `.vcpdfpresets`, `.vcprintpresets`, `.vcperspective`), plug-ins (`.wasm`) and shortcut sets (`.kys`, in Import Shortcut Set) open wherever they are dropped, as File › Open opens them. With the cursor loaded (`ui.inspect` → `tool: "place"`, `toolOptions: {count, current, name, names}`), `ui.pointer` clicks and drags place files and `ui.key` ←/→/↑/↓ and Escape cycle and discard them | | `app.export` raster options | `{path?, format: png \| jpg \| webp, ppi?, background?, antiAlias?, interlaced?, quality?}` | `ppi` sets the pixel size (72 = one pixel per point) and is stored in the file; `background` is `transparent`, `white`, `black` or `"#rrggbb"`; `antiAlias` is `none`, `art` or `type` (text snapped to pixels); `interlaced` writes an Adam7 PNG. The PNG Options dialog (`pngOptions`; `jpgOptions` / `webpOptions` for the other raster formats) has the same fields: fill them with `ui.dialog.set` and press OK with `ui.dialog.confirm` | | `app.export` JPEG options | `{path?, format: jpg, quality?: 0–100, colorModel?: rgb \| cmyk \| gray, method?: baseline \| optimized \| progressive, scans?: 3–5, embedIcc?, imageMap?: none \| client \| server}` | `cmyk` writes ink amounts in the working CMYK space (CMYK colours keep their inks; RGB ones are separated with the colour settings); `embedIcc` (default true) embeds sRGB, the working CMYK profile or a grey profile, generated by the CMS; `imageMap` also writes `.html` (client-side ``) or `.map` (NCSA) beside the image for the objects with a URL and an Image Map shape (`attributes.set`), returned in `linked`. The `jpgOptions` dialog has these fields too (`quality` is shown 0–10; a CMYK document defaults to `cmyk`) | | `app.export` PNG-8 and GIF | `{path?, format: png8 \| gif, colors?: 2–256, reduction?: perceptual \| selective \| adaptive \| web \| blackWhite \| gray, dither?: none \| diffusion \| pattern \| noise, ditherAmount?: 0–100, transparency?, matte?, interlaced?, …raster options}` | a palette of at most `colors` entries (art with that many colours or fewer keeps them exactly); pixels under half opacity become one transparent entry unless `transparency` is false, partly transparent edges are blended over `matte` (`none` keeps their colour). `png8` writes an indexed `.png` (1–8 bits per pixel); Export for Screens rows take both. The `png8Options` / `gifOptions` dialogs have these fields | | `app.export` Text and WebP | `{path?, format: txt, encoding?: utf8 \| utf16, lineEndings?: lf \| crlf, selectionOnly?}` / `{format: webp, lossless?, quality?}` | Text writes the stories in stacking order (back to front; a thread once; hidden objects and template layers left out), each line ended by `lineEndings`; `utf16` is little-endian with a byte order mark. Export As → Text opens `txtOptions` (fields `path`, `encoding`, `lineEndings`, `selectionOnly`; OK writes the file). WebP is always lossless: `lossless: false` adds a warning | | `engine.execute` `document.exportForOffice` | `{}` / `{path?, ppi?: 72 \| 150 \| 300, transparent?, artboard?}` | File → Save for Office Documents…: without params opens `saveForOffice` (fields `ppi`, `transparent`, `artboard`; OK picks the file); with params writes the artboard as a PNG (`path`, else a picked file) → `{path}`. Headless it returns `{dataBase64, width, height}` | | Native file dialogs | any command with no `path` that needs one (`file.open`, `file.saveAs`, `file.place`, exports…) | on macOS (a sheet on the window) and Linux the dialog doesn't hold the window: the call returns `"cancelled"` at once, `ui.inspect` reports `fileDialog`, and when the person at the computer picks a file the command runs again on the document it was asked for. While it is open another dialog is refused: that call returns `"cancelled"` too, and the status bar (`ui.inspect` → `ui.status`) says `"A file dialog is open: choose there first"`; on macOS `app.quit`, `file.close` and `file.closeAll` return that message and the menu bar does nothing (#592, #867). On Windows the dialog is shown in line: the call returns once it closes. Pass `path` to skip the dialog | | Background Save / Export | `app.save`, `app.export {path}`, `file.save`, `file.saveAs`, `file.saveCopy` | with the `backgroundSave` / `backgroundExport` preferences on (the default) the desktop app writes the file from a snapshot on a worker thread: the call returns `{path, background: true, status}` at once, `ui.inspect` lists the jobs still running in `background` (labels such as `"Saving a.vectorcraft"`), and the document counts as saved (as of the snapshot) once its file is written. Quit and Close wait for them. With the preferences off (`prefs.set`), saves return once written | | `engine.execute` `file.exportForScreens` | `{}` / `{…document.exportForScreens params}` | without params opens `exportForScreens` on the settings the document last exported with (`document.exportSettings`). Fields: `tab` (`artboards` \| `assets`), `select` (`all` \| `range` \| `full`), `range` ("1-3, 5"), `boards` (a bool per artboard; checking them sets `select` and `range`), `includeBleed`, `folder` (desktop; default the Desktop, else home), `openLocation`, `subfolders`, `prefix`, `preset` (`""` \| `mobile` \| `density`), `formats` (rows `{format, scale: "2x" \| "100w" \| "100h" \| "72ppi", suffix, quality?, folder?}`), `settings` (`{png \| png8 \| jpg \| webp \| gif \| svg \| pdf: {…options}}`, the gear's Format Settings; `__settings` names the format shown). `ui.dialog.confirm` exports (a bad range or no folder keeps it open); on the web OK reads "Download" and the files download (several as one `.zip`). With params it runs `document.exportForScreens`: no `folder` returns the files (or with `zip: true` one store-only zip) as `dataBase64`; with `openLocation` the first file is shown in the file manager | | Export for Screens' Assets tab, Asset Export panel | `ui.dialog.set` `tab: "assets"`, `assets: [asset id…]`; `window.panel {panel: "assetExport"}` | on the Assets tab of `exportForScreens` the `assets` field lists the checked asset ids (all of them when it opens); `ui.dialog.confirm` runs `document.exportForScreens {assets}` (nothing checked keeps it open) and OK reads "Export Asset". The Asset Export panel acts through `assets.*` and shares the format rows with the dialog (`assets.settings.set`) | | `engine.execute` `file.exportSelection` | `{multiple?}` | File › Export Selection…: collects the selected objects as assets (`assets.add`; one per object unless `multiple` is false) and opens `exportForScreens` on its Assets tab with just them checked → `{assets}`. `document.exportSelection` still exports the selection in one go, without the dialog | | CSS Properties panel | `window.panel {panel: "cssProperties"}`; `engine.execute` `css.copy` / `css.exportFile` | the panel shows `css.selection` of the selection (after Generate CSS, while nothing is selected, `css.generate`); its menu holds the options the commands take (`units`, `position`, `dimensions`, `unnamed`, `rasterize`). `css.copy {scope?, …options}` copies the CSS to the clipboard → `{css, rules}`; `css.exportFile {path?, scope?, …options}` writes `css.export` to `path` (else a picked file; the web downloads it) with the pictures of rasterized art next to it → `{path, rules, images}` | | `ui.dialog.*` on `importPdf` | `{field: "page" \| "allPages" \| "range" \| "cropTo" \| "password", value}` | opening (`app.open`, `file.open`) a PDF or `.ai` with several pages or a password, or placing one (`file.place` without `page`), opens the Import PDF dialog (`mode`: `open` or `place`; `pageCount`). While `__locked` is true, set `password` and `ui.dialog.confirm` to unlock it (a one-page file then opens or is placed). Then set `allPages: false` and `range` ("2-3, 5") to open some pages, or `page` to place one, and `cropTo` (`bounding`, `art`, `crop`, `trim`, `bleed`, `media`); `ui.dialog.confirm` opens or places them (placing keeps the other `file.place` params, such as `link`, `template` and `at`, in `__place`). `document.open {pages, cropTo, password}` and `file.place {page, crop, password}` do the same without the dialog | Swatch editors: `engine.execute` with `ui.swatchOptions {name}` opens the `swatchOptions` dialog for a colour swatch (fields `name`, `spot`, `global`, `mode`: `gray`/`rgb`/`hsb`/`lab`/`cmyk`/`web`, `color`: `"#rrggbb"` or a colour object, `preview`), which previews on the canvas while open; `ui.dialog.confirm` applies it with `swatch.edit` as one undo step and `ui.dialog.cancel` rolls the preview back. For a gradient swatch the dialog has `name` only (it shows the gradient) and OK renames it; pattern swatches open pattern editing instead. Dropping a gradient (or colour) on a swatch of its kind in the Swatches panel with Alt held replaces it (`swatch.edit {name, paint}`). Confirmations: deleting swatches from the Swatches panel opens a `confirm` dialog (fields `message`, `detail`); `ui.dialog.confirm` runs the command it asks about (here `swatch.delete`) and `ui.dialog.cancel` drops it. New swatches and colour groups: `ui.newSwatch {spot?, group?}` opens the `newSwatch` dialog prefilled from the active fill or stroke (fields as in `swatchOptions` plus `group`; a gradient or pattern only takes `name`) and `ui.newColorGroup {swatches?}` opens `newColorGroup` (fields `name`, `fromArtwork`, `toGlobal`, `includeTints`, `swatches`); `ui.dialog.confirm` runs `swatch.new` / `swatch.newGroup`. The Color Picker is a dialog too: `engine.execute {command: "ui.colorPicker", params: {stroke?, color?}}` opens it for the fill (or stroke) proxy; `ui.dialog.set {field: "hex", value: "00FF00"}` (or `color`, `channel`, `webOnly`, `swatches`) then `ui.dialog.confirm` applies the colour through `paint.setFill` / `paint.setStroke`. Graphic styles: `ui.graphicStyleOptions {name}` opens the `graphicStyleOptions` dialog (field `name`) for a style and `ui.dialog.confirm` renames it with `graphicStyle.rename` (a name another style has returns an error and keeps the dialog open); without `name` it names a new style made from the selection (`graphicStyle.new`). `ui.mergeGraphicStyles {names}` opens the same dialog to name the style `ui.dialog.confirm` merges from them (`graphicStyle.merge`). Deleting styles from the panel asks first with a `confirm` dialog (`graphicStyle.delete`). Symbols: `ui.symbolOptions` opens the `symbolOptions` dialog (field `name`) that the Symbols panel's New Symbol asks first; `ui.dialog.confirm` makes the symbol from the selection with that name (`symbol.new`). Gradient panel: double-clicking a stop on the panel's slider opens the same `gradientStop` popover, with a `screen` field (`[x, y]`, screen points) in place of `x`/`y`. The panel's stop eyedropper selects the Eyedropper tool with the tool option `stop` (the tool to return to; `tool.setOption {key: "stop", value: "gradient"}`): its next click on art samples the colour there into the selected stop (`paint.sampleColor {color, stop}`) and switches back. Dragging a swatch, a Fill/Stroke proxy or the panel's gradient thumbnail onto art runs `paint.setFill` / `paint.setStroke` (the active proxy) with the paint's params and the object's `ids`; a colour dropped on the panel's ramp adds or recolours a stop. Freeform gradients: double-clicking a point with the Gradient tool opens the same `gradientStop` popover on the selected point (`paint.freeform.selectPoint`); its fields are `color` (hex), `opacity` and `spread` (percentages), applied by `ui.dialog.confirm` through `paint.freeform.setPoint`. `ui.key` Escape ends the line Lines mode is drawing. Tool options: double-clicking a tool button runs `tool.options {tool}`. For `gradient` it opens the Gradient panel; for `eyedropper` it opens Eyedropper Options, an `eyedropperOptions` dialog (fields `sampleSize` 1/3/5, `pickUp` and `apply`, the attribute trees of `eyedropper.setOptions`) whose `ui.dialog.confirm` runs `eyedropper.setOptions` (what `appearance.copyFrom` copies). For `hand` it fits the artboard in the window (`view.fitArtboard`) and for `zoom` it shows 100% (`view.actualSize`). For `rotate`, `scale`, `reflect` and `shear` it opens the same dialog as Object › Transform (dialog kind = the tool id), or fails with `nothing selected`. For `selection`, `directSelection` and `groupSelection` it opens the Move dialog (kind `move`), with the same failure. `ui.key` Enter with one of those seven tools opens its dialog too (nothing happens without a selection), with the transform tools' `origin` at their reference point. Gradient tool handles snap to anchors, edges and smart guides; Shift constrains them to 45° steps from the `constrainAngle` preference. Effect dialogs: `engine.execute {command: "effect.dialog", params: {effect, index?, item?}}` opens the `effect` dialog (fields: the effect's parameters, `preview`). With `index` it edits that applied effect of `item` (null: the object's effects) prefilled with its values, and `ui.dialog.confirm` runs `effect.setParams`; otherwise confirm runs `effect.apply`. Choosing an effect that the list already has returns `{"pending": "effectExists"}` and opens the `effectExists` question: `ui.dialog.confirm` opens the applied effect's dialog, `ui.dialog.set {field: "discard", value: true}` then confirm opens a fresh one that adds another, `ui.dialog.cancel` drops it. The Transform effect's dialog (`effect.dialog {effect: "distort.transform"}`) has Transform Each's controls: sliders for `scaleH`, `scaleV` (%) and `moveH`, `moveV` (pt), an angle dial for `rotate`, then `copies`, `reflectX`, `reflectY`, `reference` (0–8, the 9-point grid) and `random` (each object varies its own way, the same on every redraw). Plug-in dialogs: `engine.execute {command: "plugin.dialog", params: {id}}` (Object › Plug-ins) runs an object filter plug-in at once when it takes no parameters, else opens the `plugin` dialog: one field per declared parameter (named after it), `__plugin` (the plug-in id) and `preview`. It previews `plugin.run` on the canvas while open; `ui.dialog.confirm` keeps the result as one undo step and `ui.dialog.cancel` rolls it back. Live-effect plug-ins open the `effect` dialog (`effect.dialog {effect: "plugin."}`). `ui.installPlugin {path?}` installs a `.wasm` (without a path it asks for one); see [plugins.md](plugins.md). Overprint Black: the Edit → Edit Colors → Overprint Black… menu item opens a `command` parameter dialog for `edit.colors.overprintBlack` (fields `remove`, `percentage`, `fill`, `stroke`, `includeCmyBlacks`, `includeSpotBlacks`); `ui.dialog.set` then `ui.dialog.confirm` runs it on the selection. Color Guide: `color.harmony {color, rule, steps?, variation?, amount?}` returns the harmony group (base first) and its variation grid; the panel draws the same grid. `ui.colorGuideOptions` opens the `colorGuideOptions` dialog (fields `steps` 1–20, `amount` 0–100) and `ui.dialog.confirm` sets the panel's options, which `ui.inspect` reports as `ui.color_guide` (`variation`, `steps`, `amount`). Save Colors as Swatches runs `swatch.new {colors: [...]}` (one swatch per colour, one undo step). Color Guide Limit to Library: `ui.colorGuideLimit {library}` (a swatch library id or name, `"document"` for the document's swatches, `""` for none) limits the panel's colours; `ui.inspect` reports it as `ui.color_guide_limit`, and `color.harmony {..., limitTo}` answers the limited guide (every colour snaps to the library's nearest colour, ΔE 2000). Edit or Apply Colors runs `ui.recolorDialog {colors, library?}` with the harmony colours: on the selected art it recolours as usual; with nothing selected the colours themselves are the rows and `ui.dialog.confirm` saves them as a new colour group (`swatch.newGroup`, named by the `groupName` field). Color Themes panel (`window.panel {panel: "colorThemes"}`): its Create tab makes a theme on a harmony wheel and its My Themes tab lists the saved themes; every action runs a command (`colorTheme.save`, `colorTheme.delete`, `colorTheme.addToSwatches`, `swatch.newGroup` for the theme being made), and `colorTheme.list` reads the library. Edit Colors dialogs: `ui.colorBalanceDialog` opens Adjust Colors (`colorBalance`: fields `mode` `gray`/`rgb`/`cmyk`/ `global`, the channels `r` `g` `b` / `c` `m` `y` `k` / `gray` / `tint` (global mode) from −100 to 100, `convert`, `fill`, `stroke`, `preview`) and `ui.saturateDialog` opens Saturate (`saturate`: `intensity` −100..100, `preview`). Both preview on the canvas while open; `ui.dialog.confirm` keeps the result as one undo step (`edit.colors.adjustBalance` / `edit.colors.saturate`) and `ui.dialog.cancel` rolls it back. Global mode answers with an error until tints of global and spot colours exist; the dialog then stays open. Recolor Artwork: `ui.recolorDialog {colors?, library?, group?}` opens the `recolor` dialog on the selected art (`colors`: a count for an n-colour job, or colours to assign as the new colours; `library`: Limit to Library, a library or `"document"`, `""` for the first library; `group`: a colour group, Edit or Apply Color Group). Fields: `rows` (`[{from: [keys], to: key, exclude?}]` as `recolor.reduce` returns them), `colors` (null for Auto), `method`, `preserveWhite`, `preserveBlack`, `preserveGrays`, `limitTo`, `groupName`, `tab` (`assign` or `edit`), `rule` (harmony id), `linked`, `preview`. Setting `colors`, a preserve flag or `limitTo` reduces the rows again (also on `ui.dialog.confirm`). The dialog previews `recolor.apply` on the canvas; confirm keeps it as one undo step (with a group, the group is rewritten in the same step; with no art selected only the group changes). In the Swatches panel, double-clicking a colour group's folder, or the options button while a group is selected, opens it. Library panel: `engine.execute {command: "window.swatchLibrary", params: {library}}` opens the read-only library panel on a swatch library (`ui.inspect` shows it as `library_panel: {kind, id}`; `library: null` closes it). Clicking a swatch there runs `swatch.library.add {library, names: [name], apply}` (the active proxy, or the other one with Alt), so one undo step adds and applies it; Shift/Cmd-clicks select swatches and colour groups for Add to Swatches. `window.swatchLibrary.other {path?}` loads a library file (or another document's swatches) and opens it there; opening a `.vcswatches`, `.gpl` or `.ase` file with `app.open` does the same. A file with the same extension and bytes as a User Defined library opens as that library. `ui.saveSwatchLibrary {names?}` opens the `saveSwatchLibrary` dialog (fields `name`, `format`: `vcswatches`/`gpl`/`ase`/`css`, `user`: save to the user library folder, `selectedOnly` with `names`); `ui.dialog.confirm` runs `swatch.library.save` (to a file it asks for a path and suggests `name` with the format's extension as the file name; the web downloads the file under that name). Graphic style libraries open in the same panel: `window.graphicStyleLibrary {library}` (`library_panel: {kind: "graphicStyles", id}`; `library: null` closes it). Clicking a style there runs `graphicStyle.addFromLibrary {library, name, apply: true, add}` (`add` with Alt), adding and applying it in one undo step; Shift/Cmd-clicks select styles for Add to Graphic Styles. `window.graphicStyleLibrary.other {path?}` loads a `.vcstyles` file (or another document's graphic styles) and opens it there; opening a `.vcstyles` file with `app.open` does the same. The User Defined libraries are `window.userGraphicStyleLibrary1`…`10` in Window → Graphic Style Libraries. `ui.saveGraphicStyleLibrary {names?}` opens the `saveGraphicStyleLibrary` dialog (fields `name`, `user`: save to the user library folder, `selectedOnly` with `names`); `ui.dialog.confirm` runs `graphicStyle.saveLibrary` (to a file it asks for a path and suggests `name` with `.vcstyles` as the file name; the web downloads the file under that name). Tile Edge Color: Object → Pattern → Tile Edge Color… (`ui.tileEdgeColor`) opens the `tileEdgeColor` dialog (field `color`: `#rrggbb` or a preset name such as "Light Blue"); `ui.dialog.confirm` sets the preference `patternTileEdgeColor` (`prefs.set`), the colour pattern editing mode draws the tile edge and the swatch bounds in. Window title bar: on Windows and Linux the window has no OS decorations and the app bar is the title bar. Its caption buttons (Minimize, Maximize/Restore, Close) sit at the bar's right end, 46 pt each; they are window chrome, not commands, so drive them with `ui.click` if needed. Close runs `app.quit` (the same `saveChanges` questions for modified documents), and empty bar space and the 5 pt window edges move and resize the window. Preferences › User Interface › System Title Bar (`systemTitleBar`, read when the app starts) gives the window its OS decorations back instead: the OS title then shows the active document (`*name — VectorCraft` with unsaved changes) and the app bar drops its brand mark. On macOS the window buttons are centred on the app bar; the web build is unchanged. Fill/Stroke chips and panel shortcuts: the Control bar's and Properties' Fill and Stroke chips bring their proxy forward (`paint.toggleActive {fill}`) and open a popover with the Swatches panel (Shift-click: the Color panel's mixer); a swatch clicked there runs `paint.setFill` / `paint.setStroke`. The Properties panel has them with nothing selected too (they set up the next object), with the Control bar's Stroke link (the Stroke panel as a popover) and weight spinner. Panel keys (Brushes F5, Color F6, Layers F7, Color Guide Shift+F3, Graphic Styles Shift+F5, Appearance Shift+F6, Align Shift+F7, Transform Shift+F8, Info Cmd+F8, Gradient Cmd+F9, Pathfinder Cmd+Shift+F9, Stroke Cmd+F10, Transparency Cmd+Shift+F10, Attributes Cmd+F11, Symbols Cmd+Shift+F11, Character Cmd+T, Paragraph Cmd+Alt+T, Tabs Cmd+Shift+T, OpenType Cmd+Alt+Shift+T; Cmd is Ctrl on Windows and Linux) run `window.panel {panel}`, can be changed in Edit › Keyboard Shortcuts (entry `panel:`, every panel) and pressed with `ui.key`; `ui.menu.list` shows them on the Window menu's items. `window.panel` takes a panel id in any case or the panel's display label (`"Layers"`, `"Color Guide"`). Collapsing the dock: `window.collapseDock {collapsed?}` (the » at the top of the dock; omitted toggles) hides the Properties | Layers | Libraries group and puts its three panels as icons at the top of the icon column, under a « that expands them again. While collapsed, those icons and `window.panel {panel: "properties"|"layers"|"libraries"}` pop the panel out next to the column like the other icon panels (`ui.dock_collapsed`, `ui.open_panel` in `ui.inspect`); expanding with one popped out shows its tab. The state is saved with the preferences and in user workspaces; the built-in workspaces expand the dock. Floating tool groups: dragging or clicking the tear-off bar down a tool group's flyout (or releasing the long press that opened it over the bar) floats the group as a strip of tool buttons, moved by the same bar and put back in the toolbar by the × at its top; `window.floatTools {tool, floating?}` does the same for the group of the current layout holding `tool` (omitted toggles; a tool alone in its slot is an error). While a group floats, the presses that open its flyout raise the strip instead. The strips (`ui.floating_flyouts` in `ui.inspect`: the group's tools and the strip's top-left corner) are saved with the preferences and in user workspaces; the built-in workspaces float none. Edit Toolbar: the ••• button at the bottom of the toolbar (or `window.editToolbar {open?}`) opens the All Tools drawer beside the toolbar; Escape or a click outside closes it. While it is open, `ui.drag` a tool from it onto the toolbar to add it (onto a button's top or bottom edge: its own slot before or after; onto its middle: into its group), from the toolbar into it to remove it, or within the toolbar to move it. `toolbar.addTool {tool, slot?, place?}`, `toolbar.removeTool {tool, group?}` and `toolbar.reset` do the same for the current Basic or Advanced toolbar and return its slots; the edits (`ui.toolbar_edits`) are saved with the preferences. The drawer lists the tools one per row (icon, name, shortcut), each flyout group framed together under its heading; its ☰ menu has Reset and the Basic and Advanced toolbars. Its Show section (`toolbar.show {control?, on?}`, `control` one of `fillStroke`, `coloring`, `drawMode`, `screenMode`) shows or hides the controls under the toolbar's tools, saved with the preferences (`ui.toolbar_show`). The toolbar names its groups only with Preferences › User Interface › Show Tool Group Labels, off as it ships (#1736). The Contextual Task Bar: its handle (the grip at its left end) drags it anywhere on the canvas, which always holds it whole, also when the window shrinks. Unpinned, the bar keeps that offset as it follows the selection, until another document becomes active. Its More Options (…) menu has Hide Bar (`window.taskBar`); Pin Bar Position (`window.taskBar.pin {pinned?}`, omitted toggles, checked while pinned), which holds the bar where it is across selection changes (the handle still moves it) and, turned off, lets it follow the selection again from there; Reset Bar Position (`window.taskBar.reset`), which unpins it and puts it back under the selection; and Show Properties Panel. As in Illustrator, neither the position nor the pin is saved: each launch starts with the bar under the selection. The Free Transform widget: while the Free Transform tool (E) is active, a small box at the canvas's top left holds Constrain over the tool's three modes, Free Transform, Perspective Distort and Free Distort. The buttons set the tool's options, as `tool.setOption {key: "mode", value: "free"|"perspective"|"distort"}` and `{key: "constrain", value}` do: Constrain acts as Shift held (proportional scaling, moves and rotations by 45°) and does nothing in the distort modes, where it is greyed. The modifier keys still work while dragging (Cmd on a corner distorts it freely, Cmd+Alt+Shift in perspective, Cmd on a side shears). Ruler guides are layer objects: `guide.add` puts a guide on the current layer (or `layer`), where it shows, hides, locks and is deleted with its layer and is listed in the Layers panel (a click on its row selects it, a drag onto another layer's rows moves it there); `guide.setLayer {index?, indexes?, layer}` moves guides to another layer, and `guide.list` gives each guide's `layer`, `shown` and `editable`. Guides on a hidden or locked layer leave the selection and can't be picked, moved or deleted, and no guide goes onto a locked layer; Duplicate copies a layer's guides, Merge Selected and Flatten Artwork keep them on the merged layer (Flatten discards a hidden layer's). Guides in files saved before guides had layers go on the top visible, unlocked layer when the document opens. Floating panels: dragging a dock tab, a panel icon or a popped-out panel's title out of the dock floats that panel inside the window, and the strip right of the dock's tabs floats the whole Properties | Layers | Libraries group. A floating group moves by its title bar (or the strip right of its tabs); a tab dragged out of it floats on its own; dropped on another group's title bar or tabs it stacks with that group, and dropped on the dock (lit up while the pointer is over it) or closed with its × its panels go back to the tabbed group or the icon column. Dropped on the upper or lower half of another group, floating or docked (a line shows where), a group joins it in a set right above or below it: the groups stack top to bottom, each showing one of its panels, and move together by the top group's title bar, whose × docks them all; a group dragged by its tab strip (a lone panel's tab or the strip right of the tabs) leaves the set. A double-click on a tab collapses a group to its tabs, and another expands it. Dropped on the left edge of the icon column or of a dock column, a set docks as a new dock column there: its groups stack in it as in a floating set, take groups dropped on their edges and tabs, and leave it dragged by their tab strip; the strip along the column's top floats the whole column again, and its × puts its panels back where they live. `window.panel.float {panel, x?, y?, onto?, below?, above?, group?, collapsed?}` floats a panel (with `group`, its whole group) with its top-left corner at `x`, `y` (window points; default cascaded), or stacks it as a tab with the floating group holding `onto`, or puts its group in a set right `below` or `above` the group holding that panel; `collapsed` collapses or expands its group; `window.panel.dock {panel, group?, column?}` puts it back, or with `column` (`true`, or the place: 1 next to the icon column, counting left) docks its group and set as a dock column; `window.panel.float` on a docked panel floats its column. `panel` takes the `window.panel` ids and labels, and `"tools"` for the Tools panel, which floats by its title bar and docks on the window's left edge. `window.panel` on a floating panel shows its tab (expanding its group). `ui.floating_panels` in `ui.inspect` lists the groups (panel ids, the index of the tab shown, the top-left corner, `column`: the set it is stacked in, top to bottom in list order, `collapsed`, and `docked`: the dock column it stands in) and `ui.toolbar_pos` the Tools panel's corner (null: docked); both are saved with the preferences and in user workspaces (the built-in workspaces dock everything), and a corner saved on a bigger window is clamped into this one when drawn. Floating panels stay inside the app window (no separate OS windows). Flatten Transparency: `ui.flattenTransparencyDialog` opens the `flattenTransparency` dialog for the selection (fields `preset`: a preset name, setting it loads that preset's options; the option keys of `object.flattenTransparency`: `balance` 0–100, `lineArtPpi` and `gradientPpi` 1–2400, `textToOutlines`, `strokesToOutlines`, `clipComplexRegions`, `antiAlias`, `preserveAlpha`, `preserveOverprints`; `preview`, off at first). With `preview` on it flattens on the canvas while open; `ui.dialog.confirm` keeps the result as one undo step and `ui.dialog.cancel` rolls it back. Out-of-range values answer with an error and the dialog stays open. Flattener presets: `ui.flattenerPresetsDialog {selected?}` opens Edit → Transparency Flattener Presets (dialog `flattenerPresets`). Fields: `selected` (a preset name), then the selected preset's `name` and option keys. On a saved preset, `ui.dialog.set` of an option saves it and a new `name` renames it (both through `flattener.presets.save`); built-in presets don't change. New, Delete, Import… and Export… are `flattener.presets.save {preset}`, `flattener.presets.delete`, `flattener.presets.import` and `flattener.presets.export`; `ui.dialog.confirm` closes. Opening a `.vcflattener` file with `app.open` imports its presets. In the Flatten Transparency dialog, Save Preset… keeps the dialog's options as a saved preset. PDF presets: `ui.pdfPresetsDialog {selected?}` opens Edit → PDF Presets (dialog `pdfPresets`, field `selected`: a preset name). Built-in presets are read-only. New… and Edit… open the preset editor, which `ui.pdfPresetDialog {name? (a saved preset to edit) | preset? (the preset a new one starts from)}` opens too: dialog `pdfPreset`, the Save PDF dialog's option fields plus `name` and `description`; `ui.dialog.confirm` runs `pdf.preset.save` and returns to PDF Presets (a new preset can't take a name in use). Delete, Import… and Export… are `pdf.preset.delete`, `pdf.preset.import` and `pdf.preset.export`; `ui.dialog.confirm` closes. Opening a `.vcpdfpresets` file with `app.open` imports its presets. In the Save PDF dialog, Save Preset… shows a name field (`__savePresetAs`): while it shows, `ui.dialog.confirm` saves the dialog's settings as that preset and selects it instead of writing the PDF. Flattener Preview: `window.panel {panel: "flattenerPreview"}` shows the panel; `ui.flattenerPreview {highlight?, overprints?, preset?, options?, showOptions?}` sets it (a preset loads its options, `options` adjust them), shows it and refreshes its snapshot of the document, answering as `flattener.preview` does. `ui.inspect` reports the settings as `ui.flattener_preview`. The panel keeps its snapshot until Refresh (it notes when the document or the settings changed since); its preview greys the art and paints the highlighted objects or areas red. Expand: Object → Expand… (`ui.expandDialog`, or `object.expand` without params from the menus, palette and shortcuts) opens the `expand` dialog for the selection. Fields: `object`, `fill`, `stroke` (all on; `__object`, `__fill`, `__stroke` say which the selection has something for, as `object.expand.info` does, and the others are disabled), `gradient` (`objects` or `mesh`) and `steps` (1–1000, default 255). `ui.dialog.confirm` runs `object.expand` with them as one undo step. Attributes panel: `window.panel {panel: "attributes"}` (Cmd+F11) shows it; its controls run `attributes.set`, `path.reverse {reversed}` and `path.setFillRule {rule}`, and its Browser button runs the UI command `attributes.openUrl {url?}`, which opens the URL (default: the selection's) in the web browser and answers `{url}`. Spot Colors: the Swatches panel menu's Spot Colors… (`ui.spotColors`) opens the `spotColors` dialog (field `useLab`: true shows and separates spot colours defined in Lab from their Lab values, false from their working-CMYK equivalents); `ui.dialog.confirm` runs `swatch.spotOptions {useLab}` as one undo step. Scale options: `ui.menuDialog {command}` opens the dialog a command's menu item opens (`object.scale` → `scale`, `object.transformEach` → `transformEach`, also Move, Rotate, Reflect, Shear and the Path dialogs). The Scale dialog (`scale`) and Object → Transform → Transform Each… (dialog `transformEach`: `scaleH`, `scaleV` %, `moveH`, `moveV` pt, `rotate` °, `reflectX`, `reflectY`, `random`, `reference` 0–8, `copy`, `preview`) have the fields `corners` (Scale Corners) and `strokes` (Scale Strokes & Effects). They start from the preferences `scaleCorners` and `scaleStrokes`, and `ui.dialog.confirm` keeps them as the preferences (`prefs.set`) before running `object.scale` / `object.transformEach` with them. The Transform panel's checkboxes and flyout item and the Properties panel's checkbox set the same preferences through `prefs.set`. Use Preview Bounds: the Align panel flyout's Use Preview Bounds item shows and toggles the `usePreviewBounds` preference (`prefs.set`); while it is on, the bounding box and the Transform panel, Properties and Control bar fields measure visual bounds. Align To: the Align panel's Align To choice is saved with the preferences, and `ui.inspect` reports it as `ui.align_to` (`"selection"` or `"artboard"`; Align to Key Object stores no choice). The align buttons of the Align panel, the Control bar and the Properties panel run `object.align` with `to: "artboard"` when one object is selected, else with `"key"` while the selection has a key object, else with that choice. The Object → Align items send the same `to` while every selected object is selected whole. With direct-selected anchor points or segments they send no `to`, and the points align to their own bounds. `object.align` from `engine.execute` or `ui.menu.invoke` without `to` keeps the engine's default: the key object while there is one, else the selection. Width Point Edit: double-clicking a width point with the Width tool, or `ui.widthPointEdit {id, index}`, opens the `widthPoint` dialog (fields `id`, `index`, `t`, `side1` and `side2`: the left and right widths in points, `linked`, `adjustAdjoining`). `ui.dialog.confirm` runs `stroke.widthPoint.set` with them; `ui.dialog.set {field: "discard", value: true}` then confirm (the Delete button) removes the point with `stroke.widthPoint.remove`. Corners: double-clicking a Live Corners widget with the Selection or Direct Selection tool, or `ui.corners {id?, corners?}`, opens the `corners` dialog for a path's corners (the Direct-Selected ones, else every corner; fields `id`, `corners`: anchor indices of the path with its corners uncut, a rectangle's 0–3 clockwise from the top-left, `kind`: round, invertedRound or chamfer, `radius` in points; `kind` or `radius` is absent while the corners differ). `ui.dialog.confirm` runs `object.setLiveShape` with them. Perspective plane options: double-clicking a plane widget of the perspective grid, or `ui.perspectivePlane {plane}`, opens the `perspectivePlane` dialog (fields `plane`: left, right or ground; `location`: points along the plane's normal; `objects`: none, move or copy). `ui.dialog.confirm` runs `perspective.plane.move` with them. Units: dialog distance fields (Move's `dx`/`dy`, shape sizes, Offset Path's `offset`, Split Into Grid's gutters and totals, Artboard Options' sizes, effects' distances, `transformEach`'s `moveH`/`moveV`, New Document's `width`/`height`, the length preferences in `preferences`) show in the General unit (the active document's units, or `unitsGeneral` without a document). Through `ui.dialog.set` a number is in points and a string may carry its unit (`"10 mm"`; a bare number in a string is points). The `preferences` dialog's `unitsGeneral` starts as the active document's units, and OK sets them. `newDocument` has `units` (a unit label; print presets start in `unitsGeneral`, screen presets in Pixels). Document Setup: File → Document Setup… (`file.documentSetup`, Cmd+Alt+P) opens the `documentSetup` dialog. Its fields are what `document.setup` reports (`units`, `bleed` [top, bottom, left, right] in pt, `outlineImages`, `highlightSubstitutedFonts`, `highlightSubstitutedGlyphs`, `gridSize`, `gridColors`, `simulatePaper`, `flattenerPreset`, `discardWhiteOverprint`, `language`, `quotes`, `typographersQuotes`, `superscript`, `subscript`, `smallCapsSize`, `exportText`, `backgroundContents`) plus `tab` (`General`, `Transparency` or `Type`) and `bleedLinked`. `ui.dialog.confirm` runs `document.setup` with them as one undo step; a bad value answers with an error and the dialog stays open. New Document: File → New… (`file.newDialog`, Cmd+N) opens the `newDocument` dialog. Its fields are `file.new`'s params (`preset`, `name`, `width` and `height` in pt, `units`, `artboards`, `artboardLayout`, `bleed`, `backgroundContents`, `colorMode`, `rasterEffectsPpi`, `previewMode`) plus `category` (the tab: Recent, Saved, Mobile, Web, Print, Film & Video, Art & Illustration, Branding, Social), `advanced` (Advanced Options open), `bleedLinked` and `presetName` (Save Preset's name). Setting `preset` to a name from `file.newPresets` fills the other fields from that preset; changing a field afterwards makes the settings custom (`preset` becomes ""). More Settings is the `newDocumentMore` dialog over the same fields (its Profile is `category`). `ui.dialog.confirm` runs `file.new` with the fields (with `previewMode: "pixel"` the app turns Pixel Preview on); on an error the dialog stays open. Swatch Conflict: through the app, when the pasted objects' global or spot swatches conflict with the document's (see `clipboard.conflicts`) and the params have no `swatchConflict`, every Paste command opens the `swatchConflict` dialog instead and answers `{dialog: "swatchConflict"}`. Fields: `conflicts` (`[{name, document, clipboard, spot}]`), `index` (the conflict asked about), `choice` (`merge` or `add`), `applyToAll` and `choices` (the answers so far). `ui.dialog.confirm` records the answer and asks about the next conflict (with `applyToAll`, answers the rest the same); the last one runs the paste with `swatchConflict`. `ui.dialog.cancel` pastes nothing. Paste placement: through the app, `edit.paste` and `edit.pasteWithoutFormatting` without `center`, `dx` or `dy` paste at the centre of the view. The Paste menu items are enabled while the system clipboard holds something to paste (SVG, PDF, text or a bitmap on Windows; text or a bitmap on macOS; text on Linux, whose paste keys still take a bitmap; SVG on the web, whose paste keys also take pictures and files), even with nothing copied in the app (looked at up to four times a second; `ui.menu.list` shows it). The Layers panel menu (≡ on the dock's tab strip while Layers shows) lists the layer commands (New Layer…, Duplicate and Delete Selection, Options for Selection…, clipping mask, isolation, Locate Object, Merge Selected, Flatten Artwork, Collect in New Layer, Release to Layers, Reverse Order, Template, Hide/Outline/Lock Others or Show/Preview/Unlock All Layers), Paste Remembers Layers (`layer.pasteRemembersLayers`, checked when on) and Panel Options…. Layer Options is the `layerOptions` dialog (`ui.layerOptions {ids?}`, `ui.newLayer {sublayer?}`), Panel Options the `layersPanelOptions` dialog (`ui.layersPanelOptions`), and `ui.layersExpand {ids?, open?}` opens or closes rows; see the Layers panel section of `docs/mcp.md` for the row commands (`layer.setCurrent`, `layer.highlight`, `layer.move`…). File Info: File → File Info… (`file.info` from the menu or Cmd+Alt+Shift+I; `ui.fileInfoDialog` for agents) opens the `fileInfo` dialog. Its fields are what `file.info` reports (`title`, `author`, `authorTitle`, `description`, `keywords` (a list: the chips), `rating`, `copyrightStatus`, `copyrightNotice`, `copyrightUrl`; `created` and `modified` are shown read-only) plus `__keyword`, keywords typed but not added yet (comma-separated; OK adds them). `ui.dialog.confirm` runs `file.info` with them as one undo step; a bad value answers with an error and the dialog stays open. Document Raster Effects Settings: Effect → Document Raster Effects Settings… (`ui.rasterEffectsSettingsDialog` for agents) opens the `rasterEffectsSettings` dialog. Its fields are what `document.rasterEffectsSettings` reports (`resolution`, `colorModel`, `background`, `antiAlias`, `clippingMask`, `addAround` in pt, `preserveSpotColors`); `ui.dialog.confirm` runs it with them as one undo step; a bad value keeps the dialog open. Object → Rasterize… opens a `command` dialog for `object.rasterize` whose fields (`ppi`, `colorModel`, `background`, `antiAlias`, `clippingMask`, `addAround`) start from these settings. Missing linked files: `app.open` of a document whose linked images' files can't be found (`document.open`'s `missingLinks`) opens the `missingLinks` dialog for the first (fields `missing`: `[{name, path, ids}]`, `modified`, `applyToAll`, `path`). `ui.dialog.set {field: "path", value}` then `ui.dialog.confirm` replaces it with that file (`links.relink`; without `path` a file is picked), and with `applyToAll` the others are looked for by name in the same folder; `ui.dialog.set {field: "discard", value: true}` then confirm ignores it (the images keep their preview), with `applyToAll` the rest too. The next missing file is asked about after each answer; `ui.dialog.cancel` stops asking. Then, with the preference `updateLinks: "askWhenModified"`, modified linked files are offered for update in a `confirm` dialog whose `ui.dialog.confirm` runs `links.update`. The same happens when the app's window comes back to the front and a linked file of the active document changed meanwhile (asked once per change of a file); with `"automatically"` the files are read again at once, with `"manually"` the Links panel shows them as modified. Find Linked Files: in the `missingLinks` dialog, `ui.dialog.set {field: "searchFolder", value}` then `ui.dialog.confirm` is Find in Folder…: the dialog becomes `findLinks`, which looks in that folder and its subfolders for every file the dialog still asks about (`links.findFiles`). Fields: `document` (the document's uid), `missing` (`[{name, path, ids}]`), `modified`, `back`, `state` (empty, `searching`, `done`, `stopped` or `failed`), `search` (the search's `id`), `folder`, `found` (`links.findFiles`'s `links`, with the files found), `searched`, `skipped`, `unreadable`, `stopped`, `error`, `picks` (the file chosen for each row of `missing`, or null; with each search result, a row not chosen in the dialog gets its only same file, else its only file found), `touched` (the rows whose file was chosen in the dialog; search results leave their pick as it is), `copy` (Copy into the document's folder, shown for a saved document) and `searchFolder`. `ui.dialog.set {field: "searchFolder", value}` then `ui.dialog.confirm` searches that folder and keeps the dialog open. The fields change as frames draw the dialog; `links.findFiles {}` reports the search directly. `ui.dialog.confirm` without `searchFolder` is Relink: each row with a file runs `links.relink {ids, path, copy}`, one undo step each (`copy` for images only: a placed document is relinked where its file is), and it returns `{relinked, failed}`; the status line names the first file that failed. With `back` (opened from `missingLinks`), the rows left without a file and those that failed go back to that dialog, then the modified files are offered for update. `ui.dialog.cancel` closes it and stops its search. `ui.findLinksInFolder {folder?, ids?}` (Find in Folder… in the Links panel menu) asks for a folder (or takes `folder`), then opens the dialog for the files of `ids`, else of the selected linked images and placed documents, else the missing ones; with nothing to look for, it fails with "No linked files to look for" and asks for no folder. On the web, `ui.findLinksInFolder` is disabled, `links.findFiles` can't start a search and the dialog doesn't open. Missing fonts: once the missing linked file questions are answered, or at once when there are none, `app.open` of a document whose type uses fonts that aren't available (`text.missingFonts`) opens the `missingFonts` dialog, one document at a time, and so does `links.editOriginal` of a placed document. When another dialog takes its place (Import PDF for a file opened next, for example), the dialog opens again once that one closes. Fields: `document` (the document's uid), `fonts` (`[{family, style, status, resolved}]`), `fontsNextToDocument` (the `Fonts` folder next to the document, or empty), `state` (empty, `searching`, `done`, `stopped` or `failed`), `search` (the search's `id`), `folder`, `found` (`text.findFontFiles`'s `fonts`, with the files found), `searched`, `skipped`, `unreadable`, `stopped`, `error` and `chosen` (the files to add; a font's first file is chosen when the search finds it). Its buttons are Find Fonts and Close, with Search Fonts Folder, Find in Folder… and, once files are found, Add Fonts in the dialog. `ui.dialog.set {field: "discard", value: true}` then `ui.dialog.confirm` is Find Fonts: the dialog closes and Type › Find Font opens (dialog `findFont`) with the first missing font selected. `ui.dialog.set {field: "searchFolder", value}` then `ui.dialog.confirm` searches that folder (`text.findFontFiles`), keeps the dialog open and clears `searchFolder`, also when the search doesn't start. The fields change as frames draw the dialog; `text.findFontFiles {}` reports the search directly. `ui.dialog.confirm` without `discard` or `searchFolder` is Add Fonts: it copies the files in `chosen` (`text.addFontFiles`) and closes the dialog. `ui.dialog.cancel` (Close) closes it and stops its search. After Find Fonts or Close, the dialog doesn't open again for that open. `ui.missingFontsDialog {folder?}` opens the dialog for the active document, searching `folder` when given. `ui.findFontsInFolder {folder?}` (Find in Folder… in Type › Find Font…, shown when the document misses fonts) asks for a folder, then opens the dialog searching it. The web shows a status line instead of the dialog, and both commands are disabled there. Text Import Options: placing a `.txt` file through the app (`file.place` with a file and no `text` options, the Place dialog, a drop) opens the `textImport` dialog (fields `platform`: `windows`/`mac`, `characterSet`: `unicode`/`ansi`, `removeLineReturns`, `removeParagraphReturns`, `replaceSpaces`, `spaces`). `ui.dialog.confirm` places the file as area type with those options (`file.place {text: {…}}`). System clipboard formats: through the desktop app, `edit.copy` and `edit.cut` publish what `clipboard.flavours` lists to the system clipboard, and every Paste command first loads what another app copied (`clipboard.importSvg`, `importPdf`, `importEps`, `importText` or `importImage`, centred in the view; the desktop app reads PDF and EPS from the Windows clipboard and, with SVG, from the macOS pasteboard, and an `.eps` file copied in a file manager anywhere); an import error answers the Paste with that error and pastes nothing. Cmd+V also pastes a bitmap or PDF with no text beside it (on the key's release, as egui sends no Paste event for it). Screenshots paste on every platform: Windows reads `PNG`, else `CF_DIBV5`/`CF_DIB`, macOS the pasteboard's PNG or TIFF, Linux `image/png`, and the web the picture the browser's paste event carries (#457). File menu details: `file.openRecent1`…`file.openRecent30` open the recent files the `recentFilesCount` preference lists (0 hides them all; `file.recentFiles` returns the listed paths), `file.reveal` shows the document's file in the system file manager (desktop only; answers `{path}`), and `file.newFromTemplate` without params opens a file picker in the Templates folder (preference `templatesFolder`). Older native files open as " [Converted]" (preference `appendConverted`), so their Save asks for a new `.vectorcraft` name. Native files reopen at the view they were saved with. DXF Options: Export As… with the DXF format (`file.exportAs {format: "dxf"}`, then the save dialog), or `ui.dxfOptionsDialog {path?, …document.exportDxf options}` for agents, opens the `dxfOptions` dialog. Its fields are `document.export` params: `version`, `unit` (a unit name such as `Millimeters`), `scale`, `scaleLineweights`, `colors` (`8`, `16`, `256`, `true`), `rasterFormat` (`png`, `jpeg`), `preserve` (`appearance`, `editability`), `selectedOnly`, `alterPaths`, `outlineText`, plus `path` and the artboard choice. `ui.dialog.confirm` checks them (a bad value, or Export Selected Art Only with nothing selected, keeps the dialog open), remembers them for the next DXF Options and writes the file (asking for a path when there is none). Export As also lists DWG, greyed out: setting its `format` to `dwg` shows, and on confirm answers, the hint to export DXF instead. DXF Import Options: opening a DXF drawing through the app (`app.open`, `file.open`, a drop) or placing one (`file.place` with a `.dxf` file and no `dxf` options, which answers `{"dialog": "dxfImport"}`) opens the `dxfImport` dialog (`mode`: `open` or `place`; `layouts`, `units` and `version` describe the drawing). Its fields are the `document.open` `dxf` options: `layout` (one of `layouts`), `fit`, `unit` (a unit name such as `Millimeters`) and `scale` (1 unit = `scale` drawing units; ignored with `fit`), `scaleLineweights`, `center`, `mergeLayers`. `ui.dialog.confirm` opens or places the drawing (placing keeps the other `file.place` params in `__place`) and remembers `fit`, `scaleLineweights`, `center` and `mergeLayers` for the next drawing; an error (an unknown layout, art too large at that scale) keeps the dialog open. Placement Options: the Links panel's flyout (or `ui.placementOptionsDialog {ids?}` for agents) opens the `placementOptions` dialog for the selected images (fields `ids`, `preserve`: `transforms`/`bounds`/`fileDimensions`/ `fit`/`fill`, `align`: `topLeft` … `bottomRight`, `clip`). `ui.dialog.confirm` runs `links.placementOptions` with them as one undo step. The Links panel (`window.panel {panel: "links"}`) shows `links.list` and the first selected image's `links.info`; `links.editOriginal` and `links.reveal` (Edit › Edit Original, Show in Folder) hand the linked file to the system and return `{path}`. Package: File → Package… (`ui.packageDialog`) opens the `package` dialog for a saved document (a document never saved gets a `confirm` dialog whose OK runs `file.saveAs`). Fields: `folder` (the location), `name` (the package folder), `copyLinks`, `linksFolder`, `relink`, `copyFonts`, `report`. `ui.dialog.confirm` saves unsaved changes, runs `file.package` and then opens a `confirm` dialog whose OK runs `file.showPackage {folder}`; the web downloads the zip instead. Document Info's flyout picks a category and Save… runs `docInfo.save {path?, selectionOnly?}`. Slices: through the app, `object.slice.options` and `object.slice.divide` with no params open their dialogs on the selected slices. `sliceOptions` (fields `kind`: `image`/`noImage`/`htmlText`, `name`, `url`, `target`, `message`, `alt`, `text`, `background`: `""`/`matte`/`#rrggbb`, `hAlign`, `vAlign`; `__html` says whether HTML Text applies) runs `object.slice.options` on `ui.dialog.confirm`; `divideSlices` (fields `divideRows`, `rows`, `rowMode`: `count`/`size`, `rowHeight`, and `divideColumns`, `columns`, `columnMode`, `columnWidth`) runs `object.slice.divide`. Read slice options with `slice.list`. The Slice tools: `ui.pointer` drags with the `slice` tool make a user slice (Shift square, Alt from the centre; one undo step). With `sliceSelection` a click selects a slice (Shift toggles), a drag moves the selected slices, a drag on a selected user slice's handle resizes it, a double click opens `sliceOptions` and `ui.key` Delete deletes the selected slices. Hidden or locked slices (`view.slices.hide`, `view.slices.lock`) can't be picked. EPS Options: Export As… with the EPS format (`file.exportAs {format: "eps"}`, then the save dialog), or `ui.epsOptionsDialog {path?, …document.exportEps options}` for agents, opens the `epsOptions` dialog. Its fields are `document.export` params: `previewFormat` (`none`, `tiffBw`, `tiffColor`), `transparentPreview`, `overprints` (`preserve`, `discard`), `flattenerPreset` (a built-in or saved preset's name), `includeLinkedFiles`, `thumbnails`, `cmykPostScript`, `compatibleGradients`, `level` (`2`, `3`), plus `path` and the artboard choice (`useArtboards`, `range`). `ui.dialog.confirm` checks them (a bad value or an unknown preset keeps the dialog open), remembers them for the next EPS Options and writes the file(s), asking for a path when there is none. Data Recovery: at launch, copies left behind by a crash open the `recovery` dialog (never copies another running instance keeps, so instances started side by side for screenshots don't get it). Fields: `copies` (`[{file, title, saved}]`, from `file.recovery.list`) and `discard`. `ui.dialog.confirm` restores them all (`file.recovery.restore` each, opening " [Recovered]" documents), `discard: true` then confirm deletes them, `ui.dialog.cancel` keeps them for the next launch. While copies are written in the background `ui.inspect` lists them under `background` ("Saving recovery data for "). Native save options: File → Save As to a native or `.ai` file opens the `saveOptions` dialog after the save panel (heading "VectorCraft Options" or "PDF-compatible .ai Options"). Fields: `separateArtboards`, `all` (true: every artboard; false: `range`, such as "1-3, 5"), `includeLinked`, `embedProfiles`, `pdfCompatible`, `compress`, and for native files `version` and `preview`. A bad range keeps the dialog open; `ui.dialog.confirm` writes the file and the artboards' files. EMF and WMF: Export As… lists both formats (`file.exportAs {format: "emf"}` or `"wmf"`); they have no options dialog, so `ui.dialog.confirm` asks for the path and writes the file (one per artboard with Use Artboards). Open and Place read `.emf` and `.wmf`; a paste of `image/emf` from a system clipboard service runs `clipboard.importEmf`. TIFF, BMP and Targa Options: Export As… with the TIFF, BMP or Targa format (`file.exportAs {format: "tiff"}`, then the save dialog) opens `tiffOptions`, `bmpOptions` or `tgaOptions`: the raster rows of PNG Options (`ppi`, `background`, `antiAlias`), then TIFF's `colorModel` (`rgb`, `cmyk`, `gray`; a CMYK document starts on `cmyk`), `byteOrder` (`little`, `big`), `lzw` and `embedIcc`; BMP's `colorModel` (`rgb`, `gray`), `fileFormat` (`windows`, `os2`), `depth` (1, 4, 8, 16, 24, 32), `reduction` and `dither` (4 and 8 bits), `rle` and `flipRows`; Targa's `depth` (16, 24, 32). The BMP dialog keeps its choices writable together (OS/2 falls back to 24 bits and turns RLE and flipped rows off; RLE turns flipped rows off). `ui.dialog.set` the fields and `ui.dialog.confirm` writes the file(s) with `document.export`. Save for Web: File → Export → Save for Web (Legacy)… (`file.saveForWeb`, Ctrl/Cmd+Alt+Shift+S) opens the `saveForWeb` dialog on the remembered settings (`webExport.settings`). Its fields are the `document.exportForWeb` settings (`format`, `colors`, `reduction`, `dither`, `quality`, `colorTable`, `width`, `percent`, `output`…; set `preset` to load a preset) plus `__view` (`original`, `optimized`, `2up`, `4up`), `__zoom` (`fit` or a percentage), `__kbps` and `__color` (the colour table's selected colour). `ui.dialog.confirm` remembers the settings and saves (a save panel for the image, or the page with HTML output; the web downloads the files); with `discard: true` it is Done: it remembers the settings and closes. `file.saveForWeb` with settings saves without the dialog, and `file.saveForWeb.browser` writes the HTML page to a temporary folder and opens it in the default browser (desktop). PSD Options: Export As… with the PSD format (`file.exportAs {format: "psd"}`, then the save dialog) opens `psdOptions`: the raster rows of PNG Options (`ppi`, `background`, `antiAlias`), then `colorModel` (`rgb`, `cmyk`, `gray`; a CMYK document starts on `cmyk`), `layers` (true: Write Layers; false: Flat Image, whose background list starts at White), `maxEditability` and `hiddenLayers` (with layers) and `embedIcc`. `ui.dialog.set` the fields and `ui.dialog.confirm` writes the file(s) with `document.export`. Print: File → Print… (`file.print` with no params, Ctrl/Cmd+P) opens the `print` dialog on the document's print settings. Its fields are the `print.setup` settings (`copies`, `artboards`, `range`, `media`, `orientation`, `placement`, `scaling`, `marks`, `bleed`, `output`, `graphics`, `color`, `margin`…; a section is an object, as `{field: "marks", value: {"trim": true}}`) plus `printer` (a name from `print.printers`, `""` the system's default) and `toFile` (save the job as a PDF), and the UI-only `__section` (General, Marks and Bleed, Output, Graphics, Color Management, Advanced, Summary) and `__sheet` (the page the preview shows, 0-based). `ui.dialog.confirm` (Print) keeps the settings with the document (`print.setup`, one undo step) and prints them: to the printer through the system's print queue (the web: the browser's print dialog), else, with `toFile` or no printing available, a save panel for the PDF. With `discard: true` it is Done: it only keeps the settings. Settings that can't print (a missing artboard, a range of no tiles) answer with an error and keep the dialog open. `file.print {settings?, printer?, toFile?, path?}` prints without the dialog; `print.printers` lists the printers and `print.printerSetup {printer?}` opens their system settings (Setup…). Desktop submission returns `{background: true, printed: false, printer, status}` while the spooler runs on a worker. `ui.inspect` lists it under `background`; completion or refusal appears in `ui.status`. Quit waits for submissions. PDF/PostScript export and the browser's print dialog retain their existing command results. Agents still get `{printers: [{name, default}], service, setup}` from `print.printers`; discovery failure yields an empty list. Print replies preserve `pages`, `bytes`, `format` and `warnings` alongside the pending fields above. Validation/start failures use the existing control reply `{ok: false, error: "…"}`; later spooler failures appear in `ui.inspect` → `ui.status`, with no second reply or structured backend error object. Headless MCP keeps its engine-only `file.print` result (`path` or `dataBase64`, plus the same print metadata); no spooler fields are added there. Print presets: in the `print` dialog, `preset` names the print preset whose settings were loaded last; setting it (`ui.dialog.set {field: "preset", value: "Posters"}`) loads that preset's settings, and the list reads `[Custom]` once they differ. Save Preset… shows a name field (`__savePresetAs`): while it shows, `ui.dialog.confirm` saves the dialog's settings as that preset (`print.presets.save`) and selects it instead of printing. `ui.printPresetsDialog {selected?}` opens Edit → Print Presets (dialog `printPresets`, field `selected`); `[Default]` is protected. New… and Edit… open the preset editor, which `ui.printPresetDialog {name? (a saved preset to edit) | preset? (the preset a new one starts from)}` opens too: dialog `printPreset`, the Print dialog's settings fields plus `name`; `ui.dialog.confirm` runs `print.presets.save` and returns to Print Presets (a new preset can't take a name in use). Delete, Import… and Export… are `print.presets.delete`, `print.presets.import` and `print.presets.export`; `ui.dialog.confirm` closes. Opening a `.vcprintpresets` file with `app.open` imports its presets. Modifiers on synthetic input: `ui.key`, `ui.click`, `ui.drag` and `ui.wheel` take `shift`, `alt`, `ctrl` and `cmd` (Command on macOS, Ctrl elsewhere), and the app holds them for the frames the input spans, as if the keys were down: a key's press, its `text` and its release; a click's press and release; a drag from the press through every move to the release. Every handler sees them (Shift+arrow nudges by the big increment, Alt+arrow nudges a copy, Shift-clicking a Layers selection square adds to the selection); the keyboard's own modifiers apply again afterwards. Cutting tools: Mirror & Cut (`mirrorCut`), Line Cut (`lineCut`) and Rectangle Cut (`rectCut`) cut the selected paths and compound paths. `ui.tool.select` one, then drag with `ui.pointer`: the cut previews live and the release keeps it as one undo step (`ui.key` Escape cancels). They run `path.mirrorCut`, `path.lineCut` and `path.rectCut`, which an agent can call directly. Mirror & Cut's tool options (`tool.setOption`, shown in the Control bar) are `axis` (`free` | `vertical` | `horizontal`; a constrained axis follows the pointer and a click places it) and `keep` (`left` | `right` | `top` | `bottom`); Alt on release keeps the other side. Puppet Warp (`puppetWarp`): with art selected, the tool shows pins at once (automatic ones at the centre and the end of each limb until pins are placed). `ui.pointer` clicks on the art add pins (each new pin is selected; Shift-click adds a pin to the selection or takes it out), dragging a pin moves every selected pin and warps the art, and Alt-dragging near (not on) a selected pin turns the art around it. A click off the mesh adds nothing: the cursor shows not-allowed there and the canvas says so. `ui.key` Delete or Backspace removes the selected pins. Adding, deleting and each drag are one undo step each, running `object.puppetWarp {rest: true, …}`; the pins live in the document, so Undo/Redo take them back with the art, they stay across tool switches while the selection is unchanged, and another selection starts afresh. `object.puppetWarp.pins` reads them. Its tool options (`tool.setOption`, shown in the Control bar) are `expand` (Expand Mesh, points, 0 allowed), `showMesh` (on by default) and `selectAllPins` (`true` selects every pin, `false` none); `ui.inspect` → `toolOptions` lists the `pins` the tool last showed and the `selected` ones. Colour adjustment effects (Effect → Color Adjustments) open the `effect` dialog like the other effects (`effect.dialog {effect: "adjust.hueSaturation"}`): its fields are the effect's parameters (sliders for the amounts, `channel` for Curves and Levels, `points` for Curves, `color` for Shift to Color) and they preview live; set them with `ui.dialog.set` and `ui.dialog.confirm` to apply (Curves also has a graph: drag its points). Object → Vector Halftone… (`engine.execute {command: "ui.menuDialog", params: {command: "object.vectorHalftone"}}`) opens the `vectorHalftone` dialog (fields `shape`, `frequency`, `angle`, `mode`, `color`, `invert`, `clip`, `keepOriginal`, `preview`): it previews live and `ui.dialog.confirm` keeps the halftone as one undo step. Tool options last: `tool.setOption {key, value}` or `{values: {…}}` (and `tool` to set another tool's) sets them, and the options a tool keeps (Liquify brush and tool options, Mirror & Cut, Puppet Warp, Symbolism, drawing tools) stay when you switch tools or documents and are saved with the preferences. The Liquify tools share their Global Brush Dimensions (`width`, `height`, `angle`, `intensity`). Perspective grid: View → Perspective Grid → Define Grid… (`ui.perspectiveGridDialog`) opens the `perspectiveGrid` dialog prefilled from the grid. Its fields are those of `perspective.grid.define` (`kind`, `units`, `scale`, `gridline`, `angle`, `distance`, `horizonHeight`, `thirdVp`, `leftColor`, `rightColor`, `groundColor`, `opacity`; lengths in `units`, so setting `units` converts them). `ui.dialog.confirm` runs `perspective.grid.define`; a refused value keeps the dialog open. Object → Envelope Distort's dialogs open through their menu items or `ui.menuDialog {command}`. Make with Warp… (`object.envelope.makeWithWarp`) opens `envelopeWarp` (fields `style`, `horizontal`, `bend`, `h`, `v`, `preview`, and `reset`); while an envelope is selected the menu shows Reset with Warp… instead, and the same item, shortcut or `object.envelope.resetWithWarp` opens it with `reset: true` and the envelope's values. Make with Mesh… / Reset with Mesh… open `envelopeMesh` (`rows`, `cols`, `reset`, and `maintainShape` when resetting). Envelope Options… (`object.envelope.options`) opens `envelopeOptions` (`antiAlias`, `preserveShape`, `fidelity`, `distortAppearance`, `distortLinearGradients`, `distortPatternFills`) with the selected envelope's values, or with nothing selected the defaults for new envelopes. They preview live while an envelope is involved, and `ui.dialog.confirm` keeps the result as one undo step. Liquify Tool Options: double-clicking a Liquify tool (`tool.options {tool: "twirl"}`) opens a `liquifyOptions` dialog. Its fields are `tool`, the Global Brush Dimensions `width`, `height` (pt), `angle`, `intensity` (%) and `usePressure`, then the tool's options `detail`, `simplify` and `simplifyOn` (Warp, Twirl, Pucker, Bloat), `rate` (Twirl), `complexity`, `affectAnchors`, `affectIn` and `affectOut` (Scallop, Crystallize, Wrinkle), `horizontal` and `vertical` (Wrinkle, %), and `showBrush`. `ui.dialog.confirm` runs `tool.setOption {tool, values}`. `ui.pointer` events take `pressure` (0..1, default 1): it is the Liquify intensity while Use Pressure Pen is on. Freehand Tool Options: double-clicking the Pencil, Paintbrush, Smooth, Blob Brush or Eraser tool (`tool.options {tool: "pencil"}`) opens a `freehandOptions` dialog. Its fields are `tool` and the options the tool keeps: `fidelity` (pt; Pencil, Paintbrush, Smooth), `fill` (Pencil, Paintbrush), `closeWithin` and `editWithin` (screen pixels, 0 turns it off; Pencil, Paintbrush) and `size` (pt; Blob Brush, Eraser). `ui.dialog.confirm` runs `tool.setOption {tool, values}`. `ui.pointer` events also take `holdMs` (0..60000): the pointer then holds still that long, button down, before the next event. Twirl, Pucker and Bloat keep applying while held (a repeat of the last point every 0.1 s), exactly as for the same time held with the mouse. Liquify leaves type, symbols, images, graphs, meshes, envelopes, repeats and blends as they are and says so in the status bar. Perspective grid presets: in the `perspectiveGrid` dialog, `name` is the preset whose fields are loaded (setting the fields of a preset, as the Preset menu does, keeps its name; other changes clear it, which reads [Custom]). Save Preset… sets `__mode: "save"` and `__from: "define"` with `name` the new preset's name: `ui.dialog.confirm` then saves the fields as that preset (`perspective.presets.save`) and returns to Define Grid on it. View → Perspective Grid → Save Grid as Preset… (`ui.savePerspectivePreset`) opens the same dialog in `save` mode; OK saves and closes. Edit → Perspective Grid Presets… (`ui.perspectivePresetsDialog {selected?}`) opens dialog `perspectiveGridPresets` (field `selected`): New… and Edit… open the preset editor (`perspectiveGrid` with `__mode: "edit"`, `__original` the preset edited), whose OK runs `perspective.presets.save` and comes back; Delete, Import… and Export… are `perspective.presets.delete`, `import` and `export`. View → Perspective Grid → One/Two/Three Point Perspective list that type's built-in views and the saved presets (`ui.perspectiveUserPreset.`, hidden while empty). Opening a `.vcperspective` file with `app.open` imports its presets. Blend Options (Object › Blend › Blend Options…, `ui.blendOptions`, the Blend tool's double-click, Alt-click and toolbar button) opens the `blendOptions` dialog on the selected blend's options: `spacing` (`smooth` | `steps` | `distance`), `steps`, `distance` (pt), `orientation` (`page` | `path`) and `preview`; it previews live and `ui.dialog.confirm` keeps the change as one undo step. With no blend selected (`__target: "defaults"`) OK sets what new blends start with. The Blend tool (`ui.tool.select {tool: "blend"}`): `ui.pointer` down on an object, then on another, blends them; down on an anchor point blends from that point; each further object clicked joins the blend. A selected blend shows its spine. With Direct Selection (`directSelection`) a `ui.pointer` drag from a spine point moves it (`object.blend.spine.moveAnchor`, one undo step) and, once a point is clicked, from one of its handles reshapes the curve, and a drag on a curved spine segment bends it (`object.blend.spine.reshapeSegment`); the Anchor Point tool (`anchorPoint`) bends any segment of a selected blend's spine, straight ones too. Direct and Group Selection pick a blend's key objects. With the Pen tool a click on a selected blend's spine adds a point (on a point no key sits on, deletes it). Perspective grid view options: View → Perspective Grid lists Show/Hide Grid, Show/Hide Rulers, Snap to Grid (checked), Lock/Unlock Grid and Lock Station Point (checked); `ui.menu.list` reports the current labels. They run `perspective.grid.show`, `rulers`, `snap`, `lock` and `lockStation`. Perspective grid widgets: the Plane Switching Widget is fixed in a corner of the canvas (`ui.pointer` with `space: "screen"` reaches it at the same pixels whatever the zoom); a press on it with any tool picks the plane while the grid shows. `ui.key` with `key` `"1"`…`"4"` picks the left, horizontal, right or no plane. Double-clicking the Perspective Grid tool (`tool.options {tool: "perspectiveGrid"}`) opens `perspectiveGridOptions` (fields `show`, `position`: `topLeft`, `topRight`, `bottomLeft`, `bottomRight`); `ui.dialog.confirm` runs `perspective.widget.options`. Envelopes on the canvas: `pointer_gesture` with the Mesh tool (`mesh`) or Direct Selection on a selected mesh envelope drags its points (a press on a point focuses it, and a press on one of the focused point's handle ends drags that handle); a Mesh tool click inside a mesh envelope adds a row and a column. While an envelope or its content is selected the Control bar shows its controls (Edit Envelope / Edit Contents, warp style, orientation, bend and distortions or mesh rows and columns, Reset, Envelope Options); each runs an `object.envelope.*` command. ## Shared panel docking Panel arrangements support tab grouping and reordering, horizontal and vertical splits, and movable, resizable floating groups inside the application window. Drag a panel tab, or use its context menu. Floating groups remain within the application viewport. The shared layout begins with the existing workspace on the first docking operation. Workspace saves retain its groups, active tabs, split sizes, floating geometry, and the positions of hidden panels. Resetting a built-in workspace restores its default dock. - `window.panel.move {panel, anchor, zone?, before?}` moves a panel into the target group. `zone` is `tab` (default), `center`, `left`, `right`, `top`, or `bottom`; `before` reorders tabs by panel ID. The destination must already be visible. - `window.panel.float {panel, x?, y?, width?, height?}` detaches a panel. - `window.panel.dock {panel, anchor?, zone?, before?}` returns a panel to its previous dock placement when possible, or to the supplied destination. - `window.panel.activate {panel}` reveals and selects a panel, restoring its saved position when possible. `window.panel.close {panel}` hides it. Panel IDs are stable across themes. Invalid IDs, non-finite or invalid geometry, and invalid destinations return an error without changing the arrangement. `window.panel.layout {action}` also accepts the shared, externally tagged action schema. For example, `{"action":{"MoveFloating":{"panel":"properties","rect":[44,66,360,450]}}}` updates a floating group's position and size; `{"action":{"ResizeSplit":{"path":[],"size":{"Ratio":0.35}}}}` resizes the root divider (`false`/`true` path entries select first/second children). `Move` with `placement: {"Tab":{"before":"swatches"}}` reorders tabs, and `SetStackOpen {panel,open}` / `ResizeStack {panel,height}` control accordion entries. The UI dispatches these same commands. Unknown panel IDs, invalid geometry, and stale split paths return an error without changing the workspace. ## Synthetic input hardening checklist - [x] Validate counts and reserve the whole gesture before queue mutation. - [x] Bound pending events across all synthetic methods, preserving rejected requests' prior queue. - [x] Yield after a finite control-request batch per frame; retain later requests and schedule another frame. ## Transport hardening checklist - [x] Bound idle, partial-line and reply-write waits without renewing a partial-line deadline per byte. - [x] Release connection slots on worker exit, including timeout and failed worker creation. - [x] Preserve correlated replies and persistent connections within the transport budgets. ## Revision history | Date | Change | Summary | |---|---|---| | 2026-10-13 | minor | Documented count, pending-event and per-frame request budgets (#1709). | | 2026-10-13 | minor | Documented transport deadlines and connection-slot recovery (#1726). |