Repository navigation
feat: custom actions - #4576
Draft
Julusian wants to merge 26 commits into
Draft
feat: custom actions#4576Julusian wants to merge 26 commits into
Julusian wants to merge 26 commits into
Conversation
Add the `custom-action:<id>` control type and its model: a name, description, user defined inputs, a list of actions, local variables and an optional result variable. Like the other controls, the model is a zod schema, which is part of `SomeControlModelSchema`. Each custom action will be offered as an action of the virtual `__custom-actions__` connection. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MuPpHgo3vKCpHZieedYM6i
…actions A custom action is stored as a `custom-action:<id>` control holding its inputs, actions and local variables, with collections like triggers and expression variables. Each one is published as an action of the virtual `__custom-actions__` connection, whose options are its inputs. Running one of those actions parses its options in the context of the caller and then runs the actions of the custom action, waiting for them to finish. The run has its own frame (`RunActionExtras.frame`), so that concurrent runs can't interfere with each other: - `$(options:<id>)` are the inputs given to the run, falling back to the default of an input which is missing or invalid - `$(local:x)` are the local variables of the custom action, and setting one only changes it for the run. Computed variables are evaluated for the run - `$(this:*)` are those of the caller, and the caller's abort signal stops it - custom actions calling each other are limited to 16 deep The result of the action can be one of its local variables. The inputs must be fields a user can author (`AuthorableInputFieldSchema`), with an expression for their visibility and an id usable in a variable, for a custom action to be loaded or imported. One which isn't is repaired like an invalid entity: it is replaced by a static text field, which keeps its id and shows the original, rather than losing the whole custom action. Module feedbacks inside a custom action are subscribed with the defaults of its inputs, so can't see the values of a run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MuPpHgo3vKCpHZieedYM6i
Add a Custom Actions page, with collections, where each custom action has its name, description, result variable, notes, a test run, and tabs for its actions, inputs (using the reusable input field definitions editor) and local variables. The editor warns about connection feedbacks which reference the values of a run, as they can't see them. Custom actions are offered in the action picker under a "Custom actions" group, grouped by their collections, and their inputs are suggested as `$(options:x)` variables when editing a custom action. The virtual connections now have a readable label, so a custom action shows as "Custom action: <name>". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MuPpHgo3vKCpHZieedYM6i
An action calling a custom action which was added before an input existed has no value for it. That was parsed as an empty value, rather than the default of the input. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MuPpHgo3vKCpHZieedYM6i
Custom actions and their collections are now a section of a full export, import and reset, like expression variables. They are new optional sections of version 17 of the export format, which is a compatible change, and each custom action in an import is repaired like a stored control. When importing, the actions of every connection provided by Companion itself are now kept, not just the internal ones, so that actions calling a custom action are no longer dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MuPpHgo3vKCpHZieedYM6i
A custom action can no longer contain an action which runs itself: it isn't offered in the action picker of its own editor, and adding one is rejected. One saved before this is skipped when run. Custom actions calling each other are still allowed, limited to 16 deep. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MuPpHgo3vKCpHZieedYM6i
Entities parsed their options with the variables of their control, looked up by control id, in both the entity manager and the internal module. Instead each entity list now has a scope (its parser and special expression manager), which its entities and their children use. The lists of a control use the control's own scope, so nothing changes yet. This lets a list provide a different scope for its entities, such as the inputs and local variables of a run of a custom action, while still belonging to the same control. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MuPpHgo3vKCpHZieedYM6i
Entities in the same control which parse to the same options now share a single subscription with the module. The subscription id is derived from the control, entity type, definition, parsed options and (for older modules) the inversion, so no entity owns it and nothing moves as members come and go. - An entity joins a subscription when it is parsed, moving to another when its options change. The last member leaving deletes the subscription. - Feedback values reported for a subscription are passed to every member, and an entity joining an existing subscription gets its last value immediately. - Executing an action sends the id of its subscription, so the module sees the id it was subscribed with. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MuPpHgo3vKCpHZieedYM6i
An entity can now hold child groups which exist only while Companion is running. Each has its own scope for parsing options, and is never persisted, exported or shown in the ui. They take part in everything which runs entities (subscribing, cleanup, feedback values, special expressions, finding an entity to upgrade, and tracking entities when a connection starts), but not in anything which edits or describes the stored entities. This is for the copy of a custom action held by each action which calls it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MuPpHgo3vKCpHZieedYM6i
…stom action Every action which calls a custom action now holds a copy of the custom action's actions and local variables, in transient child groups which are never persisted. The copy is subscribed in the control holding the action, with: - the `this:*` and `page:*` variables of that control - the inputs given by the action as `$(options:*)`, re-parsed whenever the options of the action or the variables they use change - the local variables of the copy, at their startup values So connection feedbacks and actions in a custom action can now depend on its inputs, and have their values ready before it is run. Identical subscriptions are shared by the merging in the entity manager. The copies are rebuilt when the custom action changes, and are built at startup once every custom action is loaded. A copy which would loop back to a custom action it is inside, or nest more than 16 deep, is not built. Runs still use the actions of the custom action itself; using the copy for a run comes next. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MuPpHgo3vKCpHZieedYM6i
Now that a subscription id covers everything sent to the module, an entity which re-parses to the same options doesn't need sending again. It is only resent when something outside the id has changed: the image size of a feedback, or the definitions of the module. This avoids sending the module unchanged subscriptions every time a variable used by an entity changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MuPpHgo3vKCpHZieedYM6i
Running a custom action now builds a further copy of it for just that run, from the copy held by the calling action: - its inputs are parsed once, when the run starts, so nothing changes underneath the run - its entities see the local variables of the run, and are re-parsed when one is set, so connection feedbacks can depend on them - when it sees the same inputs as the copy held by the caller, it starts with that copy's feedback, isInverted and storeResult values (including those of any custom actions it calls), so it doesn't wait on the module - it is removed when the run ends A loop of custom actions is no longer run until it is 16 deep: the copy which would loop is never built, so that call does nothing. Running from the editor still runs the custom action's own actions with its default inputs. The controls controller and action runner now share the manager of the copies, which is created by the registry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MuPpHgo3vKCpHZieedYM6i
…with a real module Connection feedbacks in a custom action now see the inputs given by each caller, and the local variables of each run, so the editor no longer warns about them. Adds integration tests: a connection feedback, as a local variable of a custom action, gives each caller the answer for its own input; and changing an input part way through a run doesn't affect that run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MuPpHgo3vKCpHZieedYM6i
Each tab now starts with the same short description, with the test run button beside it on the actions tab rather than on a row of its own. The editors in the tabs no longer repeat the name of the tab as a heading, like the tabs of a trigger. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MuPpHgo3vKCpHZieedYM6i
…bles The result of a custom action is one of its local variables, so with none there is nothing to choose. The field is disabled, and its tooltip says to add a local variable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MuPpHgo3vKCpHZieedYM6i
The input field editor looked and behaved like nothing else in the app. It now matches the actions and events of a control: - each input is a row of the entity list, with a drag handle to reorder it, its type as a badge, its label, and buttons to remove and collapse it - an input is added by choosing its type from a "+ Add input" dropdown at the end of the list, so its type is no longer changed after it is added - the optional properties are shown with "More options", rather than in an accordion Moving an input now moves it to an index, rather than up or down one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MuPpHgo3vKCpHZieedYM6i
…ions Each tab of the custom action editor now starts with the same heading as the actions of a control: the name of the list, expand and collapse all, and any buttons, with a description of the list below it. The test run button is in the heading of the actions. Removing an input now asks for confirmation, like removing an action or event. The local variables editor takes a description of what the variables are for, shown above its usual hint. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MuPpHgo3vKCpHZieedYM6i
…m inputs An input of a custom action is now always shown and always allows an expression. "Visible when" and "Allow expressions" were more complexity than they are worth for now, so they are no longer offered or stored. "More options" now sits above the options it shows, rather than below them, so it reads as the heading of that section.
A disabled field shows no tooltip on hover, so with no local variables the result now says below it to add one.
The value format of a colour input is no longer offered, as a number is not what a user would expect to use. A new colour input defaults to white.
The page, custom variable and connection inputs are no longer offered, as they are more than custom actions need for now. A colour input no longer stores that its value is a CSS string, as it always is. `toCompanionInputField` fills that in where the input is used as a field.
A custom action now has outputs, rather than a single result. Each output is one of its local variables, and each action which calls the custom action chooses where it is written: a local variable of its control, a page variable or a custom variable, with the same picker as the result of an action. An output is written every time a run sets its local variable, so the caller can see the progress of a run, such as a counter in a loop shown on a button. Its final value is written once the run is done, which covers outputs computed from other variables. The outputs are edited in a new Outputs tab, and the result field of a custom action is gone.
…ange Subscription ids were derived from the parsed options, so any change to an entity's options was sent to the module as a delete and a new subscription. That calls unsubscribe and subscribe instead of the module seeing an update with previousOptions, and lets it no longer decide whether a change matters. Subscription ids are now random, with a separate index from the key (control, type, definition, parsed options) to the subscription for finding identical entities. Entities whose options changed are placed once a whole run has been processed: - joining an existing subscription for their new options first - otherwise reusing a subscription they emptied, updating it in place, so a sole member or all members changing together keeps the same id - otherwise creating a new subscription Update payloads are now split into batches when sent, as subscriptions are placed after the loop that used to split them. Inversion is no longer part of the key. Companion applies it to the value of each entity, so feedbacks which differ only in inversion share a subscription.
…authored input The inputs of a custom action are now just authored input fields, as the shared schema already checks their id.
Contributor
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #3149
This allows building your own reusable actions. These custom actions contain other actions with their own local variables. There is a form to define the input fields they have, and they have outputs which can be written back to other variables.
The aim with this is to allow for building reusable chunks of logic. I am hoping to follow this with 'custom feedbacks', and 'custom presets'.
These custom actions are their own control type, giving them the basic lifecycle (mostly for editor purposes). Anywhere which uses the custom action takes a copy of the actions/variables inside of the custom action, as the variance in options means running a new instance.
Internally, this effectively adds them as volatile children inside the entity tree.
When the custom action gets executed, we create another copy of the child entities, as we need to freeze the input options, but keep the feedbacks updating. Without this a while loop waiting for a feedback to become true would never resolve. And once executing we don't want to react to changes to the custom action inputs, as that would cause a skew in what it is executing. that feedback could end up stuck waiting for the wrong thing to change.
This has heaped on complexity to handle this, especially to do so without flooding modules with constant creation and deletion of new entities which only live for the duration of the run. The entitymanager is now able to deduplicate these entities within a control, so that it only has to report to modules the ones where the options diverge.
These survive through export+import, probably not very well through a page import.
I suspect placement in the sidebar is incorrect once the feedbacks and presets exist, but its ok for now.