Inspect: scope document.inspect / doc_inspect by ids, kind, limit and offset (#2167)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
vangel76
2026-10-17 13:49:48 +07:00
committed by bflatastic
co-authored by Claude Opus 5.5
parent d55b4f77cf
commit 1eb9bea82a
8 changed files with 223 additions and 18 deletions
+21 -1
View File
@@ -24,7 +24,7 @@ pub fn specs() -> Vec<CommandSpec> {
),
cmd!("file.close", "Close", ["File"], Some("Cmd+W"), "{index?}", has_doc, file_close),
cmd!("document.activate", "Activate Document", [], None, "{index}", always, doc_activate),
cmd!(query "document.inspect", "Inspect Document", [], None, "{depth?, childLimit?} → layer tree (each node: id, name, kind, visible, locked, bounds, opacity when below 1, blend when not Normal, fill, stroke and strokeWidth of objects and compound paths; a compound path's members have compoundMember: true instead of paint, which is the compound's), artboards, selection, history (a sliced tree reports childCount)", has_doc, |s, p| Ok(inspect::document_opts(s, slice_opts(p, "document.inspect")?))),
cmd!(query "document.inspect", "Inspect Document", [], None, "{depth?, childLimit?, ids?: [id…] (list just these nodes, of any level, in this order, under `nodes` in place of `layers`; an unknown id is an error; at most 10000), kind?: panel label such as Layer; Group; Path; Type or Image (exact, ignoring case; list every such node under `nodes`, each with `path`, its ancestor ids with the layer first, searching the subtrees of `ids` when given), limit?: at most this many entries of the list, offset?: entries skipped first (default 0; with `ids`, `kind`, `limit` or `offset` the reply adds `total`, the entries before paging)} → layer tree (each node: id, name, kind, visible, locked, bounds, opacity when below 1, blend when not Normal, fill, stroke and strokeWidth of objects and compound paths; a compound path's members have compoundMember: true instead of paint, which is the compound's), artboards, selection, history (a sliced tree reports childCount); without the scope options the reply is the whole layer tree", has_doc, doc_inspect),
cmd!(query "document.node", "Inspect Object", [], None, "{id, summary?: compact summary, depth?: child levels in the summary (default all), childLimit?: children shown per node (default all; a level that shows fewer reports childCount)} → one object", has_doc, doc_node),
cmd!(query "document.find", "Find Objects", [], None, "{name?: substring of the Layers panel name, kind?: panel label such as Group; Path; Type or Image (exact), text?: substring of type content, limit?: max matches (default 100, 0 counts only)} → {matches: [{id, name, kind, path}], total}", has_doc, doc_find),
cmd!(query "document.json", "Document JSON", [], None, "{} → complete document model", has_doc, |s, _| Ok(serde_json::to_value(&*s.doc()?.doc).unwrap_or(Value::Null))),
@@ -172,6 +172,26 @@ fn doc_node(s: &mut Session, p: &Value) -> Result<Value> {
if summary { Ok(inspect::node_summary_opts(n, opts, &doc.symbol_boxes())) } else { Ok(serde_json::to_value(n).unwrap_or(Value::Null)) }
}
/// Most node ids `document.inspect {ids}` takes.
const MAX_INSPECT_IDS: usize = 10_000;
fn doc_inspect(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "document.inspect";
let opts = slice_opts(p, C)?;
let ids = match p.get("ids") {
None | Some(Value::Null) => None,
Some(Value::Array(a)) if a.len() > MAX_INSPECT_IDS => return Err(bad(C, format!("`ids` takes at most {MAX_INSPECT_IDS} ids"))),
Some(_) => Some(checked_ids_param(s, p, "ids", C)?),
};
let kind = match p.get("kind") {
None | Some(Value::Null) => None,
Some(Value::String(k)) => Some(k.clone()),
Some(_) => return Err(bad(C, "`kind` must be a string")),
};
let scope = inspect::Scope { ids, kind, limit: opt_u64(p, C, "limit")?, offset: opt_u64(p, C, "offset")?.unwrap_or(0) };
Ok(inspect::document_scoped(s, opts, &scope))
}
/// `depth` / `childLimit` for a summary read: absent (or null) means no limit.
fn slice_opts(p: &Value, cmd: &str) -> Result<inspect::SummaryOpts> {
Ok(inspect::SummaryOpts { depth: opt_u64(p, cmd, "depth")?, child_limit: opt_u64(p, cmd, "childLimit")?, compound_member: false })
+104 -2
View File
@@ -131,9 +131,113 @@ pub fn document(s: &Session) -> Value {
/// The same summary with [`SummaryOpts`] applied to the layer tree (artboards and
/// the rest always come whole): `depth: 0` is the skeleton — top layers with counts.
pub fn document_opts(s: &Session, opts: SummaryOpts) -> Value {
document_scoped(s, opts, &Scope::default())
}
/// Which nodes `document.inspect` lists in place of the whole layer tree.
#[derive(Clone, Debug, Default)]
pub struct Scope {
/// Only these nodes (any level, already checked to exist), in the order given.
pub ids: Option<Vec<vectorcraft_doc::NodeId>>,
/// Only nodes of this panel label (case-insensitive), searched in the subtrees of `ids`
/// or else of every layer.
pub kind: Option<String>,
/// At most this many entries after `offset` (`None`: all).
pub limit: Option<u64>,
/// Entries skipped first.
pub offset: u64,
}
impl Scope {
fn scoped(&self) -> bool {
self.ids.is_some() || self.kind.is_some()
}
}
/// A node with the ids of its ancestors (layer first).
type Hit<'a> = (&'a Node, Vec<u64>);
fn ancestors(d: &Document, id: vectorcraft_doc::NodeId) -> Vec<u64> {
let mut out = Vec::new();
let mut cur = d.parent_of(id);
// The bound keeps a malformed tree from looping.
while let Some(p) = cur.filter(|_| out.len() < 10_000) {
out.push(p.0);
cur = d.parent_of(p);
}
out.reverse();
out
}
fn collect_kind<'a>(n: &'a Node, kind: &str, path: &mut Vec<u64>, out: &mut Vec<Hit<'a>>) {
if n.kind_label().eq_ignore_ascii_case(kind) {
out.push((n, path.clone()));
}
if let Some(ch) = n.children() {
path.push(n.id.0);
for c in ch.iter().rev() {
collect_kind(c, kind, path, out);
}
path.pop();
}
}
/// [`document_opts`] with the layer tree replaced by the nodes `scope` picks. Without `ids`
/// or `kind` they are the top layers (`layers`, paged by `limit`/`offset`); with either they
/// are `nodes` (each with its `path` of ancestor ids when picked by `kind`). A paged or scoped
/// reply also reports `total`, the entries before paging.
pub fn document_scoped(s: &Session, opts: SummaryOpts, scope: &Scope) -> Value {
let Some(st) = s.active() else { return Value::Null };
let d = &st.doc;
let symbols = d.symbol_boxes();
let mut hits: Vec<Hit> = Vec::new();
match (&scope.ids, scope.kind.as_deref().filter(|k| !k.is_empty())) {
(None, None) => hits.extend(d.layers.iter().rev().map(|l| (&**l, Vec::new()))),
(Some(ids), None) => hits.extend(ids.iter().filter_map(|&i| d.node(i)).map(|n| (n, Vec::new()))),
(ids, Some(kind)) => {
let mut path = Vec::new();
match ids {
Some(ids) => {
for n in ids.iter().filter_map(|&i| d.node(i)) {
path = ancestors(d, n.id);
collect_kind(n, kind, &mut path, &mut hits);
}
}
None => d.layers.iter().rev().for_each(|l| collect_kind(l, kind, &mut path, &mut hits)),
}
}
}
let total = hits.len();
let skip = usize::try_from(scope.offset).unwrap_or(usize::MAX);
let take = scope.limit.map_or(usize::MAX, |l| usize::try_from(l).unwrap_or(usize::MAX));
let by_kind = scope.kind.as_deref().is_some_and(|k| !k.is_empty());
let nodes: Vec<Value> = hits
.into_iter()
.skip(skip)
.take(take)
.map(|(n, path)| {
let member = d.parent_of(n.id).and_then(|p| d.node(p)).is_some_and(|p| matches!(p.kind, NodeKind::Compound { .. }));
let mut v = node_summary_opts(n, SummaryOpts { compound_member: member, ..opts }, &symbols);
if by_kind {
v["path"] = json!(path);
}
v
})
.collect();
let mut out = document_base(s);
if let Some(o) = out.as_object_mut() {
o.insert(if scope.scoped() { "nodes" } else { "layers" }.into(), Value::Array(nodes));
if scope.scoped() || scope.limit.is_some() || scope.offset > 0 {
o.insert("total".into(), json!(total));
}
}
out
}
/// Everything `document.inspect` reports except the layer tree.
fn document_base(s: &Session) -> Value {
let Some(st) = s.active() else { return Value::Null };
let d = &st.doc;
json!({
"title": st.title(),
"path": st.path,
@@ -142,8 +246,6 @@ pub fn document_opts(s: &Session, opts: SummaryOpts) -> Value {
"units": d.units.label(),
"colorMode": format!("{:?}", d.color_mode),
"artboards": d.artboards.iter().map(|a| json!({"name": a.name, "x": a.rect.x0, "y": a.rect.y0, "width": a.rect.width(), "height": a.rect.height()})).collect::<Vec<_>>(),
// Top of the stack first, like the Layers panel.
"layers": d.layers.iter().rev().map(|l| node_summary_opts(l, opts, &symbols)).collect::<Vec<_>>(),
"currentLayer": st.active_layer.map(|l| l.0),
// The rows highlighted in the Layers panel (`layer.setCurrent`, `layer.highlight`).
"layerRows": st.highlighted_rows().iter().map(|i| i.0).collect::<Vec<_>>(),
+59
View File
@@ -2042,6 +2042,65 @@ fn document_inspect_slices_the_layer_tree() {
assert!(s.execute("document.inspect", &json!({"depth": "deep"})).is_err());
}
/// `document.inspect` scoped by `ids`, `kind`, `limit` and `offset`: each entry is the node as
/// the full tree lists it, bad input is an error, and the unscoped reply keeps its shape.
#[test]
fn document_inspect_scopes_by_ids_kind_and_page() {
let mut s = session();
let a = rect(&mut s, 0.0, 0.0, 10.0, 10.0).0;
let b = rect(&mut s, 20.0, 0.0, 10.0, 10.0).0;
s.execute("select.all", &json!({})).unwrap();
let group = s.execute("object.group", &json!({})).unwrap()["id"].as_u64().unwrap();
let c = rect(&mut s, 40.0, 0.0, 10.0, 10.0).0;
let mut ins = |p: Value| s.execute("document.inspect", &p).unwrap();
let full = ins(json!({}));
assert!(full.get("nodes").is_none() && full.get("total").is_none(), "{full}");
let layer = full["layers"][0].clone();
let layer_id = layer["id"].as_u64().unwrap();
let find = |tree: &Value, id: u64| -> Value {
fn walk(n: &Value, id: u64) -> Option<Value> {
if n["id"] == id {
return Some(n.clone());
}
n["children"].as_array()?.iter().find_map(|c| walk(c, id))
}
walk(tree, id).unwrap()
};
// ids: just those nodes, in the order given, equal to their part of the full tree.
let some = ins(json!({"ids": [c, group]}));
assert!(some.get("layers").is_none());
assert_eq!(some["nodes"], json!([find(&layer, c), find(&layer, group)]));
assert_eq!(some["total"], 2);
assert_eq!(some["artboards"], full["artboards"]);
assert_eq!(ins(json!({"ids": [group], "depth": 0}))["nodes"][0].get("children"), None);
// kind: every match with its ancestor path, top of the stack first; paged.
let paths = ins(json!({"kind": "rectangle"}));
let ids: Vec<u64> = paths["nodes"].as_array().unwrap().iter().map(|n| n["id"].as_u64().unwrap()).collect();
assert_eq!(ids, [c, b, a]);
assert_eq!(paths["nodes"][1]["path"], json!([layer_id, group]));
assert_eq!(paths["total"], 3);
let page = ins(json!({"kind": "Rectangle", "offset": 1, "limit": 1}));
assert_eq!(page["nodes"].as_array().map(Vec::len), Some(1));
assert_eq!(page["nodes"][0]["id"], b);
assert_eq!(page["total"], 3);
assert_eq!(ins(json!({"kind": "Rectangle", "offset": 99}))["nodes"], json!([]));
assert_eq!(ins(json!({"kind": "Rectangle", "ids": [group]}))["total"], 2);
// limit/offset alone page the top layers.
let paged = ins(json!({"limit": 0}));
assert_eq!(paged["layers"], json!([]));
assert_eq!(paged["total"], 1);
for bad in
[json!({"ids": [999_999]}), json!({"ids": "all"}), json!({"ids": [-1]}), json!({"kind": 3}), json!({"limit": -1}), json!({"offset": 1.5})]
{
assert!(s.execute("document.inspect", &bad).is_err(), "{bad}");
}
}
/// `document.find` searches names, kinds and type content across the whole tree and
/// answers ids with ancestor paths plus the total, so capped replies stay explicit.
#[test]
+2
View File
@@ -275,6 +275,8 @@ fn structured_junk() {
("stroke.set", json!({"weight": -5, "dash": [0, 0, 0], "miterLimit": -1})),
("stroke.set", json!({"weight": 1e308, "dash": [-1, -2], "dashOffset": 1e308})),
("stroke.set", json!({"dash": [1e-12]})),
("document.inspect", json!({"ids": [u64::MAX, 0, 1e308], "kind": "", "limit": u64::MAX, "offset": u64::MAX})),
("document.inspect", json!({"ids": null, "kind": {"a": 1}, "limit": -1, "offset": "x", "depth": u64::MAX})),
("transparency.set", json!({"opacity": -50})),
("transparency.set", json!({"opacity": 1e308, "blend": "NoSuchBlend"})),
("perspective.move", json!({"from": [0, 0], "to": [1e308, -1e308], "perpendicular": true, "copy": true, "plane": "ground"})),
+7
View File
@@ -240,6 +240,13 @@ fn headless_end_to_end() {
assert_eq!(skel["artboards"], doc["artboards"]);
assert_eq!(call(&mut s, 42, "inspect_document", json!({"depth": -1}))["isError"], true);
// Scoping reaches the command: one node by id equals its part of the full tree;
// an unknown id is a tool error.
let r = call(&mut s, 43, "doc_inspect", json!({"ids": [id]}));
let one: Value = serde_json::from_str(&text_of(&r)).unwrap();
assert_eq!(one["nodes"], json!([rect]), "{one}");
assert_eq!(call(&mut s, 44, "doc_inspect", json!({"ids": [999_999]}))["isError"], true);
// Screenshot returns image content (base64 PNG) and text.
let shot_path = tmp("shot.png");
let r = call(&mut s, 5, "screenshot", json!({"path": shot_path.to_str().unwrap()}));
+19 -11
View File
@@ -36,6 +36,20 @@ fn num(desc: &str) -> Value {
fn string(desc: &str) -> Value {
json!({"type": "string", "description": desc})
}
/// The arguments `doc_inspect` / `inspect_document` pass on to `document.inspect`.
const INSPECT_KEYS: [&str; 6] = ["depth", "childLimit", "ids", "kind", "limit", "offset"];
/// Their schema.
fn inspect_props() -> Value {
json!({
"depth": {"type": "integer", "minimum": 0, "description": "Child levels of the layer tree to include (default all; 0 = top layers with counts)"},
"childLimit": {"type": "integer", "minimum": 0, "description": "Children shown per node (default all; a level that shows fewer reports childCount)"},
"ids": {"type": "array", "maxItems": 10000, "items": {"type": "integer", "minimum": 0}, "description": "List just these nodes (any level) as `nodes` instead of the whole layer tree; an unknown id is an error"},
"kind": {"type": "string", "description": "List every node of this panel label (Layer, Group, Path, Type, Image, ...; ignores case) as `nodes`, each with its ancestor `path`; searches the subtrees of `ids` when given"},
"limit": {"type": "integer", "minimum": 0, "description": "At most this many layers or nodes (the reply adds `total`)"},
"offset": {"type": "integer", "minimum": 0, "description": "Layers or nodes skipped first (default 0)"}
})
}
fn paint_schema(what: &str) -> Value {
json!({
"description": format!("{what}: \"#rrggbb\", \"none\", [r,g,b] (0..1), {{\"c\",\"m\",\"y\",\"k\"}}, {{\"gray\"}}, or a full paint.setFill params object ({{\"gradient\":…}} / {{\"swatch\":name}})"),
@@ -129,8 +143,8 @@ pub fn tool_definitions() -> Vec<Value> {
tool(
"doc_inspect",
"Inspect document",
"Summary of the active document, including artboards, layer tree, selection and history.",
obj(json!({"depth":{"type":"integer", "minimum":0},"childLimit":{"type":"integer", "minimum":0}}), &[]),
"Summary of the active document, including artboards, layer tree, selection and history. On a large document scope it: depth 0 for the layer skeleton, ids for just those nodes, kind for every node of one kind, limit/offset to page.",
obj(inspect_props(), &[]),
true,
),
tool(
@@ -175,13 +189,7 @@ pub fn tool_definitions() -> Vec<Value> {
"inspect_document",
"Inspect document",
"Summary of the active document: artboards, layer tree (ids, names, kinds, bounds, fill/stroke), selection, undo history, current tool and default paint. On a large document pass depth: 0 for the layer skeleton (truncated levels report childCount), then locate objects with run_command document.find and read them with document.node.",
obj(
json!({
"depth": {"type": "integer", "minimum": 0, "description": "Child levels of the layer tree to include (default all; 0 = top layers with counts)"},
"childLimit": {"type": "integer", "minimum": 0, "description": "Children shown per node (default all; a level that shows fewer reports childCount)"}
}),
&[],
),
obj(inspect_props(), &[]),
true,
),
tool(
@@ -919,9 +927,9 @@ fn dispatch(b: &mut dyn Backend, name: &str, a: &Args) -> Result<ToolResult, Str
j(b.call(method, json!({"command": cmd, "params": params}))?)
}
"doc_inspect" | "inspect_document" => {
// Only the slicing options reach the command, which validates them.
// Only the slicing and scope options reach the command, which validates them.
let slice: Map<String, Value> =
a.iter().filter(|(k, _)| matches!(k.as_str(), "depth" | "childLimit")).map(|(k, v)| (k.clone(), v.clone())).collect();
a.iter().filter(|(k, _)| INSPECT_KEYS.contains(&k.as_str())).map(|(k, v)| (k.clone(), v.clone())).collect();
j(b.call("document.inspect", Value::Object(slice))?)
}
"ui_inspect" | "inspect_ui" => {
+1 -1
View File
@@ -90,7 +90,7 @@ the two can still win that race.
|---|---|---|
| `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 |
| `document.inspect` | `{depth?, childLimit?, ids?, kind?, limit?, offset?}` | layer tree, selection, the key object (`key`, null without one), history, paint defaults; `depth` / `childLimit` slice the layer tree as `document.node {summary: true}` does; `ids`, `kind`, `limit` and `offset` list just some nodes as `nodes` (with `total`) instead of the whole tree, see [mcp.md](mcp.md). 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. While a menu opened over locked objects is open, it has an `Unlock` submenu with one item for each locked object under the pointer, topmost first and at most 20, labeled with the object's name, which runs `layer.setProps {ids: [id], locked: false}`. A locked group gets one item, and the locked objects inside it get none. Hidden objects, objects on hidden, locked or template layers, and in isolation mode objects outside the isolated group get no item |
+10 -3
View File
@@ -189,7 +189,14 @@ node (layers and groups too) reports `opacity` when it is below 1 and `blend` wh
On a large document, drill instead of dumping: `inspect_document {depth: 0}` (or `run_command document.inspect
{depth?, childLimit?}`, same rules) is the skeleton (artboards with top layers and counts), `run_command
document.find {name?, kind?, text?, limit?}` locates nodes across the whole tree, and `document.node` reads the
interesting ones sliced. `document.find` needs at least one filter and all given filters must match; matching
interesting ones sliced. `document.inspect` and the `doc_inspect` / `inspect_document` tools also scope the
reply themselves: `ids: [id…]` lists just those nodes (any level, in the order given, an unknown id is an error,
at most 10000), `kind` lists every node of one panel label (ignoring case; within the subtrees of `ids` when both
are given) with its ancestor `path`, and `limit` / `offset` page the list. A scoped reply carries the nodes as
`nodes` instead of `layers` (each the summary the full tree gives it, sliced by `depth` / `childLimit`) plus
`total`, the entries before paging; artboards, selection and history stay whole. `limit` / `offset` alone page the
top layers (`layers` and `total`). Without these options the reply is the whole layer tree as before.
`document.find` needs at least one filter and all given filters must match; matching
ignores case, `name` is a substring of the Layers panel name, `text` a substring of type content and `kind` the
exact panel label (`Layer`, `Group`, `Clip Group`, `Path`, `Compound Path`, `Type`, `Image`, `Rectangle`, ...).
It answers `{matches: [{id, name, kind, path}], total}` top of the stack first, where `path` is the ancestor
@@ -261,7 +268,7 @@ The older names below remain listed because existing workflows use them.
| `command_list` | Optional `filter` (or `query`), `menu` (a menu path substring, levels joined with ` > `), `enabled_only`, `brief` (no `params` text); returns the command array |
| `command_run` | `id`, optional `params`; runs the command through the existing backend. An unknown key in `params` is an error that names it and the nearest key the command takes (`invalid parameters for `file.new`: unknown parameter `widht`; did you mean `width`?`), so a misspelt key no longer runs the command with its default (see [Parameter keys](#parameter-keys)) |
| `command_batch` | `steps: [{id, params?}]` (at most 256), optional `stop_on_error` (default true), `atomic` and `label`; returns `completed`, `failed`, `results: [{ok, result\|error}]` and `created` (the ids the `draw_path` / `draw_shape` steps made, in order). A step's `id` is a command id, or `draw_path` / `draw_shape` with that tool's arguments as `params` (checked as `tools/call` checks them; the step's result is the tool's, so `"$N.id"` is the new object). Each edit has its own undo step, or with `atomic: true` the batch's edits are one undo step named `label` (else after its first edit; `edit.undoGroup.begin`/`end` underneath); failures set `isError`. A param string `"$N.path"` or `"$last.path"` takes a value from an earlier step's result (see `run` below). Kept step results total at most 8 MiB; past that the batch stops with the reason. This is the MCP tool, not the engine command `command.batch` (that one takes `commands: [{command, params}]` and is what the Actions panel plays); use `command_run` with `command.batch` when replaying a recorded action |
| `doc_inspect` | Optional `depth`, `childLimit`; same summary as `inspect_document` |
| `doc_inspect` | Optional `depth`, `childLimit`, `ids`, `kind`, `limit`, `offset`; same summary as `inspect_document` |
| `render_preview` | Optional `max_side` (1–4096, default 1024) and `artboard` (0-based, default 0); inline PNG of that artboard without editing it. Artboards too large to render within the allocation bound return a tool error |
| `ui_inspect`, `ui_screenshot` | Connected desktop state/window capture; tool errors in headless mode |
@@ -302,7 +309,7 @@ objects' fills or strokes differ (`fillMixed` / `strokeMixed`, drawn as a "?" pr
|---|---|---|
| `list_commands` | `{filter?, query?, menu?, enabledOnly?, brief?}` | The command catalogue: id, label, menu, shortcut, params doc (left out with `brief`), enablement. |
| `run_command` | `{command, params?}` | Runs any command. Use it for everything without a dedicated tool. |
| `inspect_document` | `{depth?, childLimit?}` | Artboards, layer tree (ids, kinds, bounds, paint), selection, history, tool. A type node's `fill`, `stroke` and `strokeWidth` are the paint its characters show (its first run's); fills and strokes of the type object itself come as `objectFill`, `objectStroke` and `objectStrokeWidth`. Paint comes as a label (`"#ff0000"`, `"None"`, `"G1 40% (#ff9999)"` for a swatch at a tint); `paint.proxies` gives it as an object. |
| `inspect_document` | `{depth?, childLimit?, ids?, kind?, limit?, offset?}` | Artboards, layer tree (ids, kinds, bounds, paint), selection, history, tool. A type node's `fill`, `stroke` and `strokeWidth` are the paint its characters show (its first run's); fills and strokes of the type object itself come as `objectFill`, `objectStroke` and `objectStrokeWidth`. Paint comes as a label (`"#ff0000"`, `"None"`, `"G1 40% (#ff9999)"` for a swatch at a tint); `paint.proxies` gives it as an object. |
| `inspect_ui` | `{}` | UI state. Remote mode only. |
| `select_tool` | `{tool}` | `selection`, `directSelection`, `pen`, `rectangle`, `ellipse`, `polygon`, `star`, `lineSegment`, … |
| `pointer_gesture` | `{events:[{kind,x,y,mods?}], tool?, mods?}` | `kind` is one of `down`, `drag`, `up`, `move`, `doubleclick`. Events go through the same path as the mouse. |