Files
effectcraft/docs/control-protocol.md
T
bflatasticandClaude Opus 5.5 89e075d1b1 Shapes: Tools bar Tool Creates Shape / Mask and Fill / Stroke Options (#227)
The Tools bar had Fill and Stroke swatches and a Stroke Width, kept in
the interface only, so agents couldn't set them and new shapes could
only be a solid fill with a solid stroke. As in After Effects:

- With a shape layer selected and a shape tool or the Pen active, Tool
  Creates Shape / Tool Creates Mask choose whether drawing adds shapes
  to the layer or masks on it (Fill and Stroke hide while drawing
  masks).
- Clicking the word "Fill" or "Stroke" opens Fill Options / Stroke
  Options: None, Solid Color, Linear Gradient or Radial Gradient, the
  blend mode and the opacity. The swatches show the kind; Alt-click
  steps through the kinds.
- New shapes and Pen paths are painted with them: a Gradient Fill or
  Gradient Stroke across the shape for the gradient kinds.

The options are engine state (EditorState.shape_tool) set with the new
shape.toolOptions command; shape.newShape and shape.newPath use them
unless given paint parameters (which now include fillType, fillBlend,
fillOpacity and their stroke twins; layer.newShape takes them too). A
Pen path without paint parameters now takes the Tools bar's stroke
(none by default) instead of a fixed 2 px white one. layer.addMask also
draws rounded rectangle, polygon and star masks, so every shape tool
makes a mask on a selected footage or solid layer (rounded rectangles,
polygons and stars made a new shape layer).

Test: shape_tool_options_paint_new_shapes,
masks_of_every_shape_tool_kind,
tool_creates_mask_draws_a_mask_on_the_selected_shape_layer,
fill_options_paint_the_next_shape.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 13:03:38 +07:00

9.6 KiB

Control protocol

The desktop app exposes a control channel for agents, tests and the MCP bridge:

effectcraft --control 9877        # or EFFECTCRAFT_CONTROL_PORT=9877
  • Transport: TCP on 127.0.0.1 only, one JSON object per line in each direction. Connections are persistent; requests on one connection are answered in order.
  • Request: {"id": 1, "method": "engine.execute", "params": {...}} (id is echoed back, params defaults to {}).
  • Reply: {"id": 1, "ok": true, "result": ...} or {"id": 1, "ok": false, "error": "message"}.
  • Requests run on the UI thread between frames. Input methods (ui.click, ui.key and similar) reply after the synthetic input has been processed. Methods that need an element that isn't drawn yet are retried for a few frames. A request times out after 60 s.
  • Only requests are read: every line must be a JSON object with a string method (blank lines are skipped). Anything else (text that isn't JSON, a JSON array or number, an object without method, invalid UTF-8, a line longer than 4 MiB) gets one error reply ending in "closing the connection", and the server closes the connection, so nothing sent after it runs. An HTTP request (for example a web page's cross-origin fetch to 127.0.0.1:<port>) 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. The port has no authentication, so only enable it while you use it.

Quick test:

printf '{"id":1,"method":"engine.execute","params":{"command":"comp.new","params":{"name":"A"}}}\n' | nc 127.0.0.1 9877

Engine

Method Params Result
engine.execute {command, params?} The command's result. Every engine command (effectcraft-cli commands) runs exactly as it does headless, so it never opens a dialog: comp.new {} creates a default comp. UI-only ids (tool.*, view.*, window.*, playback.*, timeline.*, app.*) fall through to the menu dispatcher. Unknown top-level params are rejected, and the error lists the accepted keys.
ui.menu.invoke {id, params?} Like a menu click: commands with empty params may open their dialog (New Composition, Solid Settings, file pickers).
UI commands (via either) timeline.column {column, visible?} (av, keys, label, num, comment, switches, modes, parent, in, out, duration, stretch), timeline.sourceName {value?}, timeline.search {query}, timeline.reveal.<kind> / timeline.revealAdd.<kind> (the property shortcuts and Shift+shortcut: position, scale, rotation, opacity, anchor, effects, masks, feather, levels, animated, and the double-press sets maskPath, maskOpacity, timeRemap, expressions, material, paint, selected, missingEffects, waveform), timeline.keyAt.<anchor|position|scale|rotation|opacity> (Alt+Shift+A/P/S/R/T), timeline.twirlSelected (Ctrl/Cmd+), timeline.collapseAll, flowchart.options {layers?, effects?, solids?, direction?: lr|tb, comp?}, flowchart.graph {comp?}→{nodes, edges}, window.workspace {name}` (all After Effects workspaces).
engine.commands {filter?, enabledOnly?} [{id, label, menu, shortcut, params, enabled, why}]
ui.menu.list {} The menu tree with enablement.
render.frame {comp?, time?, max_side?, path?, base64?} Renders a comp frame through the session, independent of the viewer zoom or resolution. comp is an id or name (default: the active comp), time is in comp seconds (default: the CTI), max_side caps the longest side (0 = full size). Writes a PNG to path (default: a temp file) and returns {comp, time, width, height, path}. With base64: true it returns {…, png: "<base64>"} and writes no file.

Useful query commands for engine.execute (they aren't journaled or undoable): project.summary, comp.info {comp?}, layer.tree {layer, comp?, depth?, time?} (every node has a path), prop.get {layer, path, comp?, time?}, editor.state, command.list {filter?, enabledOnly?, schemas?}, command.describe {command} (params doc and JSON Schema), app.capabilities (version, counts, export formats, parity summary), jobs.list (Progress panel jobs, e.g. the background footage.check after File ▸ Open).

Every engine command is reachable through engine.execute, and every interactive widget of every panel registers an element id; both are enforced by crates/ui-egui/tests/ui_agent_audit.rs.

UI state

Method Params Result
ui.inspect {} Window size, pixels per point, fps, serialized UiState, playback, time, active comp, selection, open dialog, last render ms.
ui.elements {prefix?} [{id, label, rect: [x, y, w, h]}]: the widgets drawn in the last frame, in points.
ui.set {tool?, workspace?, theme?, focused?, viewer?, timeline?, menuBar?, home?} viewer: {zoom (null = fit), res: Full|Half|Third|Quarter|Auto, pan: [x,y], grid, rulers, safeMargins, transparencyGrid}; timeline: {pps (null = fit), start, graphEditor, showModes, openLayers: [ids], openGroups: [uids]}.
ui.panel.show / ui.panel.close {panel} Panels: Project, Composition, Timeline, EffectControls, EffectsPresets, Info, Preview, Character, Paragraph, Align and others.
ui.playback {action: play|stop|toggle|status} {playing, time, audio, levelsDb, peaksDb} (audio: an audio preview is running; L/R meter levels and held peaks in dBFS)
ui.resize {width, height} Resizes the window (points).
ui.focus {} Activates the window and takes keyboard focus. Agents normally avoid this, because the app is started without stealing focus.
app.quit {force?} Closes the app. A modified project shows the Save / Don't Save / Cancel prompt first (automation ids dialog.unsaved.save, dialog.unsaved.dontSave, dialog.unsaved.cancel) unless force: true.

Input

Points are logical window coordinates. A target is {id} (the element's centre; add fx/fy between 0 and 1 to pick a spot inside it) or {x, y}. modifiers is {shift, alt, command, ctrl}, where command is ⌘ on macOS and Ctrl elsewhere.

Method Params
ui.click {id | x,y, button?: left|right|middle, count?, modifiers?}
ui.move {id | x,y}
ui.drag {from: target, to: target, steps? (12), modifiers?}: press, move, release
ui.scroll {id | x,y, dx, dy}
ui.key {key}, for example Space, F9, Cmd+Z, Cmd+Shift+Z, PageDown, Escape
ui.type {text}: text input into the focused field

Locating things

Method Params Result
ui.viewer.locate {x, y} (comp pixels) {x, y} window point
ui.viewer.hit {id | x,y} (window point) {x, y} comp pixel
ui.timeline.locate {layer (id), time?} {x, y} window point on the layer bar

Screenshots

Method Params Result
ui.screenshot {path?, panel?, id?} {path, width, height}: a PNG of the window, or cropped to one panel or element.

Element ids are stable, for example tools.Selection, panel.Timeline, panel.tab.EffectControls, panel.tab.Timeline.<comp> (one Timeline tab per open comp; panel.tab.Timeline is the shown one), panel.tab.Viewer(<n>) and viewers.<n> (View ▸ New Viewer's other Composition viewers; viewers.0 is the Composition panel while another viewer is active), header.workspace.Animation, project.item.<id> (a double-click opens the item, Enter renames it), viewer.comp, viewer.handle.<layer>.<i>, timeline.layer.<id>.bar, timeline.layer.<id>.nestedMarker.<i> (a precomp's comp markers), timeline.layer.<id>.twirl, timeline.layer.<id>.row (click selects, Enter or a double-click renames, drag reorders), timeline.prop.<uid>.stopwatch, timeline.key.<uid>.<frame>, timeline.cti, effectControls.prop.<uid>.value (an angle's revolutions are timeline.prop.<uid>.revolutions, effectControls.prop.<uid>.revolutions and properties.prop.<uid>.revolutions) and effects.item.<name>. The Composition viewer adds viewer.magnification, viewer.resolution, viewer.roi, viewer.grid, viewer.channel, viewer.exposure, viewer.snapshot, viewer.showSnapshot, viewer.fastPreviews (their popup entries are viewer.<menu>Item.<n>), viewer.ruler.top|left|origin, viewer.mask.<uid>.vertex.<i>, viewer.shapePath.<uid>.vertex.<i>, viewer.motionPath.<layer>.<key>[.in|.out], viewer.puppetPin.<uid>[.rotate|.scale], viewer.freeTransform.handle.<i> and viewer.regionOfInterest; the Graph Editor adds timeline.graph.transformBox[.<i>], timeline.graph.snap and timeline.graph.reference. Use ui.elements to see what is on screen.

Viewer state that agents drive headless too: view.snapping, view.channel {channel, colorized?}, view.exposure {stops | delta}, view.resetExposure, view.takeSnapshot, view.showSnapshot, view.fastPreviewMode {mode}, view.setRegionOfInterest {rect}, view.addGuide, view.moveGuide, view.removeGuide; editing: shape.newPath (Pen on shape layers), shape.newShape (the shape tools: a new group in the selected shape layer, else a new shape layer), shape.toolOptions (the Tools bar's Tool Creates Shape / Mask and Fill and Stroke Options), mask.insertVertex, mask.convertVertex, mask.deleteVertices, path.freeTransform (masks and shape paths by uid), keys.setSpatialTangents (motion-path handles) and keys.transform (Graph Editor transform box, timeline Alt-drag scaling).

The MCP server's bridge mode (effectcraft-cli mcp --bridge 9877) is a thin client of this protocol; see agents.md.