refactor(automation): give triggers back to the triggers list

The automation form had grown a lifecycle picker that created and deleted
global triggers behind the user's back. It broke the model the panel is built
on — an automation is what to send, a trigger is when — and left the global
triggers section describing itself as a place to rename things made elsewhere.

Triggers now belong only to the triggers list and the event editor. The
automation form edits a title, filters and outputs, and saves in one request:
the two-request save, its partial-failure recovery and the snapshot it
reconciled against are all gone with it. An automation with no global trigger
offers to make one from its own row, opening the trigger form with the
automation preselected, so the connection is still one click away without the
form pretending to own it.

Other changes in the same pass:

- the form uses the compact modal instead of the wide one. Nothing in it
  justified 1800px, and output fields now pair up two to a row rather than
  stretching across four columns
- a blank automation gets its own header button beside Start from recipe,
  instead of being a footnote under the recipe list
- the settings form seeds itself from the query when it resolves, as the other
  settings panels do. It was showing automations as OFF while they were on
- the filter operator list goes back to the three master offered, which makes
  the note about not_contains unnecessary rather than explanatory
- comments that narrated the change rather than explaining the code

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AfDKsy6PE3Rbyt32Fg4YKf
This commit is contained in:
Claude
2026-09-09 19:36:09 +00:00
parent 3d23d53555
commit a4e83dd9a0
11 changed files with 279 additions and 436 deletions
@@ -118,8 +118,6 @@ describe('deleteTrigger()', () => {
});
it('ignores a trigger that is already gone', async () => {
// a client reconciling several triggers must not be stuck because another client
// removed one of them first: the end state it asked for is the one it gets
const before = getAutomationTriggers();
await expect(deleteTrigger('never-existed')).resolves.toBeUndefined();
expect(getAutomationTriggers()).toEqual(before);
@@ -87,9 +87,8 @@ export async function deleteTrigger(id: string): Promise<void> {
const triggers = getAutomationTriggers();
const index = triggers.findIndex((trigger) => trigger.id === id);
// ignore request if the trigger does not exist, as deleteAutomation does for the same reason:
// the caller asked for it to be gone and it is, and failing here makes a client that is
// reconciling several triggers unable to finish once another client removed one of them
// deleting is idempotent, as it is in deleteAutomation: the state the caller asked for
// already holds, and erroring would only punish a client that raced another one
if (index === -1) {
return;
}