MCP: panel names from the dock's own tables, object-read errors kept apart, docs (review of #328)
normalize_panel walks DockTab::ALL and ICON_PANELS instead of repeating the
dock tab ids. vectorcraft://object/{id} turns only a missing object into
-32602; no document or an unreachable app stay backend errors, as before.
docs/mcp.md documents document.node's summary flag and the panel names
open_panel takes; control-protocol.md the window.panel names. The ROADMAP
states the measured speed-up (a leaf read on 2,000 objects: ~20 ms to ~0.06 ms).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
cea2ee6a54
commit
87d1acd589
|
|
@ -89,7 +89,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist
|
|||
- Actions panel (record/playback, persisted), generic parameter dialogs for every "…" command.
|
||||
- JSON-lines control channel with real egui pointer and keyboard injection.
|
||||
- MCP server with 25 tools (drawing, text, effects, Pathfinder, transforms, graphs, text wrap, export, screenshots, any command), attached to the running app or headless. Tool descriptions and replies agree with the engine: format lists come from its format table, `save_file` says it keeps the document's own format, `inspect_document` reports the paint type's characters show, and selection exports report the encoder's warnings.
|
||||
- MCP protocol: five prompts (`prompts/list` / `prompts/get`: poster, icon set, recolour, trace-and-style, export set), `completion/complete` for prompt arguments and template variables read from the live catalogues (formats, effect ids, command ids, trace presets, swatch and object ids), four resource templates (`vectorcraft://object/{id}`, `//command/{id}`, `//effect/{id}`, `//swatch/{name}`) so an agent reads one thing instead of the whole document, and `logging/setLevel` with `notifications/message`. `window.panel` ids are case-insensitive (display labels work too), `vectorcraft://object/{id}` reads through `document.node {summary: true}` (identical reply bytes with one lookup instead of a full-tree serialize per read — ~437x faster leaf reads on a 2,000-object document, measured release-vs-release), and the CLI sweeps the protocol surface over stdio against the compiled binary. `docs/mcp.md` has a Protocol section covering the capability table and what is deliberately absent (subscriptions, progress, cancellation, sampling, elicitation, Streamable HTTP — all of which need the transport to interleave reads).
|
||||
- MCP protocol: five prompts (`prompts/list` / `prompts/get`: poster, icon set, recolour, trace-and-style, export set), `completion/complete` for prompt arguments and template variables read from the live catalogues (formats, effect ids, command ids, trace presets, swatch and object ids), four resource templates (`vectorcraft://object/{id}`, `//command/{id}`, `//effect/{id}`, `//swatch/{name}`) so an agent reads one thing instead of the whole document, and `logging/setLevel` with `notifications/message`. `window.panel` ids are case-insensitive (display labels work too), `vectorcraft://object/{id}` reads through `document.node {summary: true}` (the same reply bytes from one lookup instead of summarizing the whole document: a leaf read on a 2,000-object document drops from ~20 ms to ~0.06 ms), and the CLI sweeps the protocol surface over stdio against the compiled binary. `docs/mcp.md` has a Protocol section covering the capability table and what is deliberately absent (subscriptions, progress, cancellation, sampling, elicitation, Streamable HTTP — all of which need the transport to interleave reads).
|
||||
- Headless CLI (`vectorcraft-cli run`, `convert`, `info`, `bench`, `perf`, `mcp`).
|
||||
- Actions panel that records and plays back commands.
|
||||
- **UI:** Illustrator 2026 layout restyled to measured values:
|
||||
|
|
|
|||
|
|
@ -118,8 +118,6 @@ pub fn read(b: &mut dyn Backend, uri: &str) -> Result<Value, ReadError> {
|
|||
return Err(ReadError::Invalid(format!("{} needs a value for `{}`", template.uri, variable_name(template.uri))));
|
||||
}
|
||||
let found = match template.name {
|
||||
// One direct `document.node` lookup through `engine.execute` (which reaches both
|
||||
// backends), instead of pulling the whole layer tree out of `document.inspect`.
|
||||
"object" => read_object(b, &var).map_err(ReadError::Backend)?,
|
||||
// The command catalogue is a control method; the rest are engine commands.
|
||||
"command" => find_in(b.call("engine.commands", json!({})).map_err(ReadError::Backend)?, None, "id", &var),
|
||||
|
|
@ -174,21 +172,19 @@ fn decode(s: &str) -> String {
|
|||
}
|
||||
|
||||
/// The layer or object with this id in the active document, as its compact summary
|
||||
/// (bounds, paint labels, subtree — the `document.inspect` shape for one node).
|
||||
/// (bounds, paint labels, subtree: the `document.inspect` shape for one node).
|
||||
///
|
||||
/// One `document.node {summary: true}` lookup through `engine.execute` (which reaches
|
||||
/// both backends), instead of pulling the whole layer tree out of `document.inspect`
|
||||
/// and scanning it: the reply matches the old read field for field, without the
|
||||
/// O(document) serialize-and-summarize on every call.
|
||||
///
|
||||
/// A non-numeric id, or one that names nothing, is "not found" (the caller turns it into
|
||||
/// `-32602`); backend failures are swallowed the same way, matching how [`crate::prompts`]
|
||||
/// treats an unreachable catalogue (empty rather than an error).
|
||||
/// One `document.node {summary: true}` lookup instead of summarizing the whole document
|
||||
/// and searching it. A non-numeric id, or one that names nothing, is "not found" (the
|
||||
/// caller turns it into `-32602`); other failures (no document, an unreachable app) stay
|
||||
/// backend errors.
|
||||
fn read_object(b: &mut dyn Backend, id: &str) -> Result<Option<Value>, String> {
|
||||
let Ok(num) = id.parse::<u64>() else { return Ok(None) };
|
||||
match b.call("engine.execute", json!({"command": "document.node", "params": {"id": num, "summary": true}})) {
|
||||
let Ok(id) = id.parse::<u64>() else { return Ok(None) };
|
||||
match b.call("engine.execute", json!({"command": "document.node", "params": {"id": id, "summary": true}})) {
|
||||
Ok(v) => Ok(Some(v)),
|
||||
Err(_) => Ok(None),
|
||||
// `EngineError::NoNode`, as both backends word it.
|
||||
Err(e) if e.starts_with("no such object") => Ok(None),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -246,6 +242,12 @@ mod tests {
|
|||
assert!(read(b.as_mut(), "vectorcraft://object/").is_err());
|
||||
assert!(read(b.as_mut(), "vectorcraft://object/99999").is_err());
|
||||
assert!(read(b.as_mut(), DOC_URI).is_ok());
|
||||
// An id that names nothing is a bad value; without a document the backend says why.
|
||||
for uri in ["vectorcraft://object/99999", "vectorcraft://object/abc"] {
|
||||
assert!(matches!(read(b.as_mut(), uri), Err(ReadError::Invalid(_))), "{uri}");
|
||||
}
|
||||
let mut empty = Box::new(crate::Headless::new());
|
||||
assert!(matches!(read(empty.as_mut(), "vectorcraft://object/1"), Err(ReadError::Backend(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -736,26 +736,14 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
|||
),
|
||||
];
|
||||
|
||||
/// Canonical panel id for `window.panel`: the dock tabs (`properties`, `layers`, `libraries`)
|
||||
/// and every `ICON_PANELS` id, matched case-insensitively. Each icon panel's display label
|
||||
/// is accepted too, so `"Layers"` and `"Swatches"` work the way agents write them.
|
||||
/// Canonical panel id for `window.panel`: a dock tab's or an icon panel's id or display label,
|
||||
/// matched case-insensitively (`"Layers"`, `"swatches"`, `"Color Guide"`).
|
||||
fn normalize_panel(input: &str) -> Option<&'static str> {
|
||||
let name = input.trim();
|
||||
if name.eq_ignore_ascii_case("properties") {
|
||||
return Some("properties");
|
||||
}
|
||||
if name.eq_ignore_ascii_case("layers") {
|
||||
return Some("layers");
|
||||
}
|
||||
if name.eq_ignore_ascii_case("libraries") {
|
||||
return Some("libraries");
|
||||
}
|
||||
for &(id, label, _) in ICON_PANELS.iter() {
|
||||
if name.eq_ignore_ascii_case(id) || name.eq_ignore_ascii_case(label) {
|
||||
return Some(id);
|
||||
}
|
||||
}
|
||||
None
|
||||
let tabs = DockTab::ALL.into_iter().map(|t| (t.info().0, t.info().1));
|
||||
tabs.chain(ICON_PANELS.iter().map(|&(id, label, _)| (id, label)))
|
||||
.find(|(id, label)| name.eq_ignore_ascii_case(id) || name.eq_ignore_ascii_case(label))
|
||||
.map(|(id, _)| id)
|
||||
}
|
||||
|
||||
/// Handle a UI command. `None` = not a UI command (the engine handles it).
|
||||
|
|
|
|||
|
|
@ -179,6 +179,7 @@ forward (`paint.toggleActive {fill}`) and open a popover with the Swatches panel
|
|||
a swatch clicked there runs `paint.setFill` / `paint.setStroke`. Panel keys (Color F6, Color Guide Shift+F3,
|
||||
Appearance Shift+F6, Graphic Styles Shift+F5, Stroke Cmd+F10, Gradient Cmd+F9, Transparency Cmd+Shift+F10) run
|
||||
`window.panel {panel}` and can be 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 «
|
||||
|
|
|
|||
|
|
@ -75,13 +75,18 @@ is thousands of lines an agent pays for again on every change.
|
|||
|
||||
| `uriTemplate` | Reads |
|
||||
|---|---|
|
||||
| `vectorcraft://object/{id}` | One layer or object with its children, bounds and paint |
|
||||
| `vectorcraft://object/{id}` | One layer or object with its children, bounds and paint: `document.node {id, summary: true}`, the node as `document.inspect` lists it |
|
||||
| `vectorcraft://command/{id}` | One command: label, menu path, shortcut, parameter description, enablement |
|
||||
| `vectorcraft://effect/{id}` | One live effect with its parameters and defaults |
|
||||
| `vectorcraft://swatch/{name}` | One swatch, colour, gradient or pattern swatch (percent-encode spaces) |
|
||||
|
||||
An unknown URI is `-32002`; a template with no value, or a value that names nothing, is `-32602`.
|
||||
|
||||
The same reads work as query commands through `run_command`: `document.node {id}` returns the object's full model
|
||||
JSON (geometry, appearance, every attribute) and `document.node {id, summary: true}` the compact summary
|
||||
`document.inspect` gives for it (id, name, kind, bounds, paint labels, children), without summarizing the whole
|
||||
document.
|
||||
|
||||
### Completions
|
||||
|
||||
`completion/complete` answers for a prompt argument (`ref/prompt`) or a template variable
|
||||
|
|
@ -157,7 +162,7 @@ objects' fills or strokes differ (`fillMixed` / `strokeMixed`, drawn as a "?" pr
|
|||
| `press_key` | `{key, mods?}` | Remote: a real key event. Headless: runs the command or tool bound to that shortcut, or sends the key to the busy tool (digits too: `5` while dragging with the Perspective Selection tool). |
|
||||
| `type_text` | `{text}` | Remote only. |
|
||||
| `invoke_menu` | `{command, params?}` | Invokes a menu item by command id. Includes UI commands such as `view.*` and `window.*` in remote mode. |
|
||||
| `open_panel` | `{panel}` | Remote only. |
|
||||
| `open_panel` | `{panel}` | Remote only. `panel` is a panel id (`layers`, `swatches`, `colorGuide`, …, as `window.panel` takes) or its display label (`"Color Guide"`), in any case. |
|
||||
| `screenshot` | `{path?, scale?, artboard?, window?}` | Returns MCP image content (`image/png`, base64) plus a text block. Renders the artboard; `window:true` captures the app window (remote only). |
|
||||
| `open_file` | `{path}` | Opens any readable file as a new active document: `.vectorcraft`/`.drawcraft`, `.vctemplate`, `.svg`/`.svgz`, `.pdf`/`.ai`, `.ait`, `.eps`, `.dxf`, `.emf`, `.wmf`, PNG, JPEG, GIF, WebP, TIFF, BMP (an image opens as a document of its pixel size). Templates (`.vctemplate`, `.ait`) open as a new untitled document. PDF, `.ai` and SVG files saved with Preserve Editing reopen as the document they carry. `run_command document.formats` lists the formats. |
|
||||
| `save_file` | `{path?}` | Runs `document.save`: the document's own file in its own format (native `.vectorcraft` unless it was opened from or saved as SVG, PDF or a restorable `.ai`; then `warnings` say what that format loses). A path's extension picks the format (`.vectorcraft`, `.vctemplate`, `.pdf`, `.svg`, `.svgz`, `.ai`: a PDF carrying the native document, which reopens editable). |
|
||||
|
|
|
|||
Loading…
Reference in New Issue