* plugins: install effect plug-in bundles (wasm + ScriptUI panels) in one go
* plugins: address review - keep listing errors, refuse replacing an installed plug-in
* plugins: report unreadable bundle entries when their metadata cannot be read
* plugins: surface partial install errors in the menu bar, cover the install flow with tests
* plugins: surface partial install errors for async dialog answers too
* plugins: show result errors as compact status text
* plugins: harden installs - confined automation, no panel overwrite, size cap, cleanup errors
* plugins: bound install reads from one handle, detect unreadable panels, clarify install docs
* plugins: drop an unneeded mut in the bounded read
* plugins: saturate the bounded read limit
* plugins: install into the session's Plug-ins folder, refuse hidden single installs and case-variant panels
- Install .wasm files into Session::plugins_dir() (the folder the Plug-in Library
from #2524 uses), not a path rebuilt from the settings folder; the host tests
now point both at their temp folder so they never touch the real settings.
- A single install of a hidden file (".x.wasm") was skipped and reported as a
success that installed nothing: it is now an error.
- Installed names must be bare file names (and not Windows device names on
Windows), as file.installScript requires, so every installed panel is listed
and can be uninstalled.
- A panel whose name differs from an installed one only in case replaced it on
Windows and macOS: compare ignoring case and check the file on disk too.
- The loaded plug-in's source is the installed copy, not the file it came from.
- Document the `bundle` menu parameter, so checked execution accepts it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* plugins: drop needless to_vec in the bundle install test (clippy)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* plugins: keep the loader test's byte vectors, drop to_vec in the install test (clippy)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: STAR Web Studio <hello.starwebstudio@gmail.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Plug-ins: API 2 (PluginHost, padding, layer_span)
Split out of the OpenFX work so it can be reviewed on its own. Additive; the WebAssembly ABI
stays at 1.
- point3 / layer / text / hidden parameters, integer sliders, parameter groups.
- EffectPlugin::padding (transparent pixels around the buffer, at most 4096 per side).
- EffectPlugin::render_v2 with a PluginHost: the input at other times, other layers, parameters
at other times, frame rate, per-instance key, the layer's source range.
- EffectEnv::layer_span from the renderer; MAX_PLUGINS 1024.
- plugin_error: a plug-in effect's last render error, for the UI.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KxacwTpUwqyfpcLgKW8yjS
* Plug-ins: external effect helpers (crates/helper-host)
Native plug-in formats run in separate helper programs, never in EffectCraft's process
(docs/adr/0001-external-effect-helpers.md). New safe-Rust crate effectcraft-helper-host (L3,
native only, unsafe forbidden): it finds helper programs, starts each as a child process and
registers its effects through plug-in API 2, so the UI and command surface need nothing new.
- Protocol v1 (docs/external-helpers.md): JSON lines over stdin / stdout (helper replies are
prefixed so plug-in chatter is ignored); pixels go through a frame exchange file (/dev/shm on
Linux), never through the pipe. Render-time callbacks for the input at other times, other layers
and parameters at other times, with nested renders through the same helper.
- A helper that exits, hangs past its timeout or breaks the protocol is stopped. The render fails
with a message, the frame is left as it was, and the helper restarts for the next render. An
effect that stops its helper 3 times is turned off (helper_blocklist.txt).
- Opt-in: nothing starts helpers unless the user asks (Effect > Scan for Effect Helpers), or opts
in to starting them at launch with EFFECTCRAFT_HELPERS=1, an environment variable, so writing
to the settings folder alone can't run code. effect.helpers.scan / load are refused for
confined agent sessions (confine.rs), like file.executeFile.
- Discovery: helpers/ next to the app (Contents/Helpers on macOS), helpers/ in the settings
folder, EFFECTCRAFT_HELPER_PATH.
- Commands effect.helpers.list / scan / load; Effect menu entries (translated); the last render
error of an effect shows as a warning sign after its name in Effect Controls.
- serve(): the helper side for Rust helpers. tests/it/main.rs runs the whole protocol (render,
padding, callbacks, nesting, threads, crash, restart, block list, hang) against a fake helper,
with nothing to install.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KxacwTpUwqyfpcLgKW8yjS
* Effect helpers: address review (start failures, frame file mode, nextest listing)
- A helper that can't start (not installed any more, restarting too often, no frame exchange
file) is reported but no longer charged to the effect being rendered: it is Failure::Refused,
not Failure::Broken, so it never counts toward turning that effect off. New check in
tests/it: once the helper is out of restarts, renders of an effect fail without blocking it
(fails without the fix: the effect was turned off after 3 such renders).
- The frame exchange file is created owner-only (0o600) on Unix: it holds the user's frames and
/dev/shm is shared. Asserted in the frames unit test.
- tests/it (harness = false) speaks the libtest listing protocol cargo-nextest uses:
`--list --format terse` prints `helper_protocol: test` (nothing with `--ignored`), and
`helper_protocol --exact` runs the checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KxacwTpUwqyfpcLgKW8yjS
* Effect helpers: bound writes to a helper, drain stderr, keep callbacks in step
- Writes to a helper go through a writer thread with the request timeout: a
helper that stops reading its stdin (hung with a large callback answer or
render request pending) no longer blocks the render thread forever while
holding the helper.
- stderr is drained as bytes (lossy, in 64 KiB pieces) to the end: output that
isn't UTF-8 stopped the reader, closing the pipe under the helper (EPIPE /
SIGPIPE on its next write); lines were unbounded.
- Callback images that can't be written are answered with `null` instead of
failing the render while the helper waits (which left the protocol out of
step); a frame that can't be written before the request is sent is not
charged to the effect.
- Mid-render calls use the render's own process: no respawn or heartbeat ping
while the helper is waiting for an answer.
- Waiting for a busy helper is bounded (load timeout) instead of forever;
status() no longer blocks on a running render.
- Reap the child when start-up fails after spawning; cap the block list read.
- Scripts start helpers only with Allow Scripts to Write Files and Access
Network on (they run native code), as for File.execute().
- helper-host declares the workspace's no-panic crate lints; the desktop
main() keeps its doc comment.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Effect helpers: rustfmt the fake provider, build the test session in its initializer (clippy)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Plug-in Library: offer WebAssembly API 1 releases only; a helper already stopped mid-render isn't charged again
- WasmPlugin loads only modules at the WebAssembly ABI version, which stays 1 while
PLUGIN_API_VERSION is 2: share it as effects::plugin::WASM_API_VERSION and have the
library's compatibility check use it, so it never installs an API 2 release that
can't load.
- with_running: a process that is already gone was stopped by what broke it (a nested
render, charged there), so this render fails without charging its own effect.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* frames: plug-in effects count as reading other frames (plug-in API 2)
With main's narrowed frame invalidation (#2795), an edit inside a precomp under an
effect that reads its layer at other times must reach the layer's whole span.
Plug-in API 2 gives every plug-in effect the input at other times, so any effect
outside ec. counts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
* G8: plug-in library
Community Plug-in Library for WebAssembly effect plug-ins (#376): one
reviewed catalog (index.json), install / update / remove from Settings >
Plug-in Library (also Effect > Plug-in Library...), library.* commands,
start-up loading of installed packages with SHA-256 checks. Downloads go
through the system curl (https, GitHub hosts), as trained models do.
Preview before installing (library.preview): the release is downloaded,
checked like an install, and rendered in the sandbox on a built-in sample
frame at default settings (2 s at 12 fps) with a Before/After split, plus
the effect's parameters, "Open on GitHub" and an optional author video
link (catalog field preview.video). Authors need to do nothing for it.
Hardening: a module that fails to load leaves the installed copy alone,
Update All updates every package in one job, minApp also applies to an
explicit version, curl ignores ~/.curlrc and URL globbing, unique temp
files, percent-decoded file URLs. Plug-in effect slots grow from 64 to
256 with a clearer overflow error.
Docs: plugins.md "Plug-in Library", publishing-plugins.md (author guide
from zero to listed), gaps.md; examples/plugin-catalog is the starting
content of the catalog repository (schema, validation workflow).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* contributors: credit iseemhlyfta
* G8: plug-in library: doc comments on the library's functions
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* G8: plug-in catalog check: require one object with schema 1 and a packages array
An empty file or several JSON values passed jq empty, and with
SKIP_SCHEMA=1 nothing else checked the catalog's shape.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* G8: plug-in library: one preview texture set; catalog schema requires effects
The preview kept a texture set per package version previewed (about
6 MB each) for the whole session; it now keeps one, replaced when
another package is previewed. Releases in the catalog schema must list
at least one effect id, as the author guide says.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* G8: plug-in catalog schema: effect ids follow the manifest's id rules
Letters, digits, '.', '_' and '-', and not the reserved 'ec.' prefix,
so library.missing can match them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* G8: refuse Plug-in Library jobs while automation file access is confined
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* G8: split the Plug-in Library page into toolbar, card, links and actions
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* G8: show Plug-in Library toasts without a Roto job; cache preview frame checks
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* G8: log Plug-in Library packages that fail to load at start-up
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* G8: check an update before replacing a loaded package; Update All past failures
- An update to a package that is already loaded is now compiled and its manifest checked
(through the previewer, without registering it) before the new file is written and the old
version's folder deleted. Before, a release with the right SHA-256 that didn't load replaced
the working version and failed at the next start.
- Package ids that differ only in case, or end in a dot, would share one folder on Windows and
macOS: the catalog keeps the first of them, and ids can't end in a dot (schema too).
- Update All: a package that can't be picked or installed no longer stops the others; the
result names what was updated and what wasn't.
- Downloads: at most 5 redirects, and one that ends off GitHub's hosts is refused, as the module
docs say.
- A job that panics no longer leaves the library busy; poisoned status locks are used, not
dropped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* G8: rustfmt the update test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Edit ▸ Keyboard Shortcuts now lists the scripts File ▸ Scripts lists, under
File ▸ Scripts, and a key given to one runs it as its menu entry does. The
key is bound to the script's file name (file.runScript {"name": ...}) and is
saved with the preset. File ▸ Scripts and the macOS menu show it.
The shortcut table lists the Scripts folder again when a script is installed
or uninstalled, when the editor opens and when shortcuts.list runs.
shortcuts.set and shortcuts.reset look a script up in File ▸ Scripts' list
as it is now, and a script it doesn't list is an error instead of a key for
Run Script File....
A subfolder of a Scripts folder can be a link to another folder (an extension's own checkout).
`file.uninstallScript {"name": "Motion Kit/Animate.jsx"}` then deleted Animate.jsx in that
folder, outside the settings folder. Uninstall now refuses `Folder/File.jsx` names and removes
only scripts at the top of the Scripts folders, where Install puts them; listing and running
through a linked folder still work.
Regression test: uninstall_never_deletes_through_a_linked_subfolder (failed before the fix).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
File ▸ Scripts gets a Recent Script Files submenu with the ten scripts run
last, newest first: script files run with Run Script File… or a command, and
the installed and bundled scripts of File ▸ Scripts. ScriptUI panels are not
listed. The list is kept in the settings. Each entry runs its script again
through `file.runScript {"recent": i}`, and Cmd+Alt+Shift+D runs the most
recent one; the key can be changed in Edit ▸ Keyboard Shortcuts. An entry
records whether it ran a file or an installed script, so a file named like a
bundled script runs again as that file.
An extension that ships several panels no longer adds one Window menu entry per panel:
`Scripts/ScriptUI Panels/Motion Kit/Animate.jsx` shows as Window > Motion Kit > Animate.jsx,
and a subfolder of `Scripts/` is a submenu of File > Scripts. Top-level scripts are unchanged.
- Script names may be `Folder/File.jsx`: exactly one folder, each part a bare name (no `..`,
`\`, NUL or empty part), never `ScriptUI Panels/...` under `Scripts/`.
- window.scriptPanel, file.runScript, file.uninstallScript, file.scripts.list and
scriptui_panels.json take such names; a panel's tab is titled after its file.
- ConfigStore::list_dirs (DirConfig, MemoryConfig) lists the subfolders.
- menus::dynamic_nodes groups a folder's entries into a MenuNode::Submenu; the egui and native
menus draw it with their submenu code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Scripts in Scripts/Startup and Scripts/Shutdown in the settings folder
were never run. The desktop and web apps now run the Startup scripts at
launch, before a project opens, and the desktop app runs the Shutdown
scripts as it quits, after the project is closed. Each folder runs in
alphabetical order with case ignored, and files larger than 16 MiB are
not run. A Startup script that fails is reported in a toast and in the
log, a Shutdown script that fails is reported in the log, and the next
one still runs.
A Startup script that waits in a modal dialog goes on when the dialog
closes, while the next scripts run. A Shutdown script's dialogs close
as if canceled. With the settings folder on disk, each script runs as
its file, so #include, $.fileName and $.evalFile resolve against the
script's own folder.
The new commands file.runStartupScripts and file.runShutdownScripts
return the scripts that ran, those still waiting in a dialog, and those
that failed. Like script.run, both are refused in confined automation
sessions, in batches and in script steps, which now read one list of
script commands (RUNS_SCRIPTS). The list also holds window.scriptPanel
and window.restoreScriptPanels, which run ScriptUI panel scripts and
were not refused on main either. The apps still call
window.restoreScriptPanels at launch. plugins.md, scripting-parity.md,
gaps.md, target-app-parity.md and agents.md describe the folders. Each script
still runs in its own global scope, so a Startup script's variables are
not available to later scripts.
Problem: ScriptUI controls never sent mouse events to scripts.
addEventListener("mousedown" | "mouseup" | "mousemove" | "mouseover" |
"mouseout" | "click") listeners never ran, so custom-drawn controls
(onDraw + mouse, as DuScriptUI builds them) could not react to the pointer.
docs/plugins.md listed this as missing.
Fix: scriptui.js builds a ScriptUI MouseEvent (clientX/clientY relative to
the target, button, detail = click count, screenX/Y, modifier keys,
getModifierState, stopPropagation, preventDefault) and delivers it in the
capture, target and bubble phases. addEventListener takes the capture flag.
The parent walk is bounded, so a script-made parent cycle cannot hang it.
Each published control lists the mouse events a listener wants
(Widget::mouse, inherited from containers). The new scriptui.mouse
command dispatches one event with clamped, finite values and rejects
unknown types and disabled controls. The egui ScriptUI view hit-tests the
pointer over script windows and docked panels, skipping covered layers and
windows blocked by a modal dialog. It sends mouseover/mouseout,
mousemove, mousedown/mouseup with click counts, and a click carrying the
pointer details in place of the plain click, so a checkbox still toggles
once. Handlers run under the existing script limits; a throwing listener
reports an error and the window keeps working.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: ExtendScript's preprocessor directives weren't understood, so a
script (or ScriptUI panel, or library such as DuAEF) failed to parse at
its first `#include`; `#target` / `#targetengine` / `#strict` lines broke
parsing too.
Fix: before a script runs, `include::expand` inlines `#include "f"` and
`//@include "f"` in place of the line, resolved against the including
file's folder, then the folders of the latest `#includepath "a;b"`.
Each file is inlined once (which also breaks cycles), nesting is limited
to 32, files are capped at 16 MiB (64 MiB in all), and reads follow the
scripts' file gate (the script's own folder, the project's folder, or
anywhere with Allow Scripts to Write Files). `#target`, `#targetengine`,
`#strict`, `#script` and `#engine` are dropped. Errors in the expanded
code are mapped back to the included file and its line. Scripts without
directives run unchanged. File resolution, the gate and the limits live
in `scriptfile.rs` for `$.evalFile` (#760) to share.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Ease Presets panel (#254) is modelled on a third-party panel, so,
as with third-party tools in After Effects, it isn't core any more: it
is `extensions/scriptui-panels/Ease Presets.jsx`, a ScriptUI panel
bundled as an optional extension (Window ▸ Ease Presets.jsx), written
only against the public scripting API. Clicking a preset eases every
pair of neighbouring selected keyframes in one undo step (per
dimension, along the motion path for spatial properties, the keys'
other sides as they play); the fields edit the working curve, which the
panel draws; From Keys, Save, Rename and Delete as before. The twelve
built-in curves are unchanged.
Scripting API fixes it needed (After Effects parity):
- `app.settings` lasts between runs (`script_settings.json` in the
settings folder; `script.settings.get` / `script.settings.save`).
- Per-key methods (`setTemporalEaseAtKey`, `setInterpolationTypeAtKey`,
`removeKey`, the continuous / auto-Bezier / roving setters) no longer
change the user's key selection (they selected the key they edited).
- `keyInTemporalEase` / `keyOutTemporalEase` report linear and
auto-Bezier sides as they play, `keyIn/OutSpatialTangent` the
tangents the motion path plays.
- `selectedProperties` includes the properties of selected keyframes.
- `isInterpolationTypeValid` is false for Linear / Bezier on values that
only hold.
Removed from core: Window ▸ Ease Presets, the `keys.easePreset.*`
commands, `engine::ease_presets` and the `ease_presets.json` store,
`keyframe::EaseCurve` (nothing else used it) and the egui panel. User
presets saved by the v0.6.0 panel move into the panel's app.settings
section the first time settings load (the old file is left as is).
Docs: plugins.md (bundled extensions), parity.md, gaps.md G8, agents.md.
Test: app_settings_last_between_runs,
per_key_methods_keep_the_key_selection_and_report_eases_as_they_play,
spatial_tangents_are_reported_as_the_path_plays (each fails before),
the_ease_presets_panel_eases_every_selected_pair_in_one_undo_step,
a_typed_curve_scales_to_each_dimension_and_follows_motion_paths,
user_presets_save_rename_delete_and_persist,
presets_saved_by_the_old_core_panel_carry_over (script, through the
panel's controls), ease_presets_panel_eases_the_selected_keys_from_its_list
(UI: the panel docks and a click on a preset eases the keys).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EffectCraft's extension points compared with After Effects' (public
user guide and Scripting Guide) are now in docs/plugins.md, which
becomes "Plug-ins and extensions": scripts, ScriptUI panels and effect
plug-ins, with a short guide and an example panel for writing your own.
ScriptUI panels already came from `Scripts/ScriptUI Panels/` in the
settings folder, were listed at the bottom of the Window menu and
docked as tabs. What was missing: After Effects reopens the panels
that were open when it quit. Now the open panels are remembered in
`scriptui_panels.json` in the settings folder (a panel is added when
it opens and dropped when its tab or its script closes it; quitting
leaves the list), and `window.restoreScriptPanels` opens them again;
the desktop and web apps run it at start-up. A panel whose script is
gone or fails is reported and forgotten, and the others still open.
Where a panel was docked isn't kept, as workspace layouts aren't saved
between launches.
Gaps recorded in plugins.md and gaps.md G8: the Startup and Shutdown
script folders, keyboard shortcuts for scripts, ExtendScript's
`#include` / `#target` directives and `.jsxbin`.
Test: open_panels_open_again_at_the_next_launch (fails before: no
restore command), a_panel_dropped_in_the_folder_is_listed_and_opens_as_a_panel
(script), panels_open_at_quit_dock_again_at_launch (UI: the restored
panel docks with its controls; closing the tab forgets it).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ui-egui: scriptui_view draws script windows at engine layout bounds; PanelKind::ScriptPanel; File ▸ Scripts and Window menu list scripts/panels (egui + native menus)
- examples/plugins/posterize-bands (WebAssembly, API v1); host plug-in tests; desktop loads <config>/Plug-ins
- MCP: script_ui and history tools; docs/plugins.md, agents.md, parity, architecture, ATTRIBUTION
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>