Automations and webhooks for the AT Protocol airglow.run

fix(form): derive the last lexicon-specific input examples from the trigger record #66

Closed tyb.there-hugo.dev
Closed opened by tyb.there-hugo.dev

Problem #

Some inputs in the automation form still show examples that name fields of one specific lexicon (post, like or account). With other triggers, the example points at a field the record does not have. #18 and #58 fixed the record.subject cases. These were missed because both searched for record.subject only.

Evidence (main e8136b4) #

  • app/islands/action-editors/bsky-post.tsx:72: Embed URL placeholder https://example.com or {{event.commit.record.uri}}
  • app/islands/action-editors/kipclip-annotation.tsx:64: Page URL placeholder e.g. https://example.com or {{event.commit.record.url}}
  • app/islands/AutomationForm.tsx:2496: loop "Array path" placeholder e.g. event.commit.record.facets[]
  • app/islands/RecordFormBuilder.tsx:786: boolean placeholder input e.g. {{event.commit.record.active}}
  • app/islands/action-editors/patch-record.tsx:77 with lib/actions/patch-record.ts:49-54: the Base Record URI example is the trigger's first at-uri, which the action rejects unless its collection equals targetCollection.
  • Found with git grep -n 'event\.commit\.record\.' -- app ':!*.test.*'.

Expected #

Each example names a field the selected trigger record has, and is a value the step accepts, or the input shows a generic or no example.

Acceptance criteria #

Out of scope #

The notification title's "New like" wording, webhook sample bodies, and the explanatory hint in schedule-config-editor.tsx:112. These are prose that illustrates syntax, not field examples.

Queued: the loop will work on this version of the issue.

loop:queued cid=bafyreicb37drnnhlhmgi27qbtqudubempvnk6dki2zcaqfwactyajnkmhy

Working on this, attempt 1.

loop:claim run=contribute-20261030-100657-66 branch=loop/66 attempt=1 issue=66

fix(form): derive the last lexicon-specific input examples from the trigger record

The bsky-post Embed URL, kipclip Page URL, loop Array path, record-builder boolean placeholder and patch-record Base Record URI inputs no longer show examples that name post, like or account fields. Each one now takes its example from the selected trigger lexicon, or shows a generic example or none. Before, with other triggers, the example pointed at a field the record does not have, or (for patch-record) at a URI the action rejects.

What changed #

  • bsky-post / kipclip-annotation: the editors now take the examples prop and show e.g. https://example.com or {{examples.url}}, the same as semble-save and margin-bookmark. Without a uri field they show e.g. https://example.com.
  • Loop Array path: the example is the first entry of collectArrayPaths(triggerRecordSchema), through the new loopPathExample. With no array it shows no example. collectArrayPaths and its walkers moved unchanged from AutomationForm.tsx to step-drafts/field-examples.ts so they can be unit tested.
  • RecordFormBuilder boolean placeholder input: shows {{placeholder}} and now has an accessible name (<field> placeholder). Before, it had no label at all.
  • patch-record Base Record URI: TriggerExamples now carries the trigger collection. The form fills it in when the lexicon is a valid NSID. The new patchBaseUriExample returns {{event.uri}} only when targetCollection (trimmed) equals that collection. Otherwise there is no example: lexicons do not say which collection an at-uri field points at, so no trigger field can be shown to pass the action's collection check.

Verified #

  • pnpm run check: passes. pnpm run test: 183 files and 2803 tests pass.
  • New and updated tests, each covering the case with a fitting field and the case without:
    • field-examples.test.ts: collectArrayPaths, loopPathExample, patchBaseUriExample, and triggerExamples with a collection.
    • New bsky-post.test.tsx and kipclip-annotation.test.tsx: the URL placeholder with and without examples.url.
    • patch-record.test.tsx: same collection, typed target, another collection, unknown trigger collection. This replaces the old tests that expected the trigger's at-uri.
    • RecordFormBuilder.test.tsx: the boolean placeholder input's example and its name.
    • AutomationForm.test.tsx: the loop path example with and without an array, and patch-record's example through the whole form for a matching and a different target.
  • git grep -n 'e\.g\. {{event\.commit\.record\.\|or {{event\.commit\.record\.' -- app ':!*.test.*' finds nothing. The remaining examples are built from the schema at runtime.
  • Only placeholder text and one aria-label changed. No focus or keyboard behaviour changed.

Acceptance criteria #

Decisions you can reverse #

  • bsky-post's Embed URL placeholder now starts with e.g. (e.g. https://example.com), as the issue asks. Before, it read https://example.com or ....
  • For patch-record with a different collection, it shows no example rather than trying to match at-uri fields to the target. Lexicons carry no target-collection hint to match on.
  • The boolean placeholder input shows {{placeholder}} rather than a field taken from the trigger lexicon.
  • Not changed, because the issue does not list them: the follow editor still pre-fills Subject DID with {{event.commit.record.subject}}, and its hint still says "like {{event.did}} or {{event.commit.record.subject}}". The git grep above does not match these. With a trigger that has no subject, the pre-filled value does not fit, so they may deserve their own issue.

Follow-up, filed as a new issue on merge if the review agrees:

fix(form): show the placeholder input when "Placeholder..." is picked for a boolean field

In the record builder, a boolean field's select has a "Placeholder..." option, but picking it never shows the text input for the placeholder. BooleanField in app/islands/RecordFormBuilder.tsx maps __placeholder__ to onValueChange(""), and it only shows the input when isPlaceholder (value !== "" && value !== "true" && value !== "false") is true. An empty value is never a placeholder, so the input stays hidden and the select drops back to "Select..." on the next render. The input only appears when a loaded template already has a placeholder in that field.

How I know: a happy-dom render of RecordFormBuilder with { live: { type: "boolean" } }. I set #field-live to __placeholder__ and dispatched change. After that, input[aria-label="live placeholder"] was still missing.

Where to start: keep a local "placeholder mode" state in BooleanField, set when the option is picked, so the input renders while the value is still empty. Then move focus to that input so the keyboard user lands on it.

Review: https://tangled.org/exosphere.site/airglow/compare/main...loop/66

loop:pushed run=contribute-20261030-100657-66 sha=e63e3549b302067ad4ef8c4ac98742dd1eafa49e

changes-requested. The branch meets all six acceptance criteria as written, and pnpm run check and pnpm run test both pass. One gap is left, and it shows up on the most common trigger. The loop's Array path example is now the first array in lexicon order. For app.bsky.feed.post that array is entities, a field the lexicon itself marks "DEPRECATED: replaced by app.bsky.richtext.facet." So a post trigger now shows e.g. event.commit.record.entities[], a field that current posts do not carry. Before this branch it showed facets[], which was right for posts. This goes against the issue's goal that each example "names a field the selected trigger record has".

Changes #

  1. In app/islands/step-drafts/field-examples.ts, make loopPathExample skip arrays whose schema node is marked deprecated. Treat a node as deprecated when its description matches /^\s*deprecated\b/i. buildRecordSchema keeps the description on array nodes, and /api/lexicons/<nsid>?schema=record returns it. If every array is deprecated, the input shows no example. Leave collectArrayPaths as it is, because it also feeds the datalist of suggestions.
    • What I tried in this clone, then reverted: inside loopPathExample, filter out the top-level properties whose description marks them deprecated, then call collectArrayPaths on what is left.
    • Through the form: in AutomationForm.test.tsx's POST_SCHEMA, I added entities: { type: "array", description: "DEPRECATED: replaced by app.bsky.richtext.facet.", items: { type: "string" } } before langs.
      • With that filter, the "gives the loop's Array path the trigger record's first array" test still gets e.g. event.commit.record.langs[].
      • Without the filter, which is the branch as it stands, the same test gets e.g. event.commit.record.entities[].
    • Through the server's schema builder: I ran a post lexicon in its real property order (text, entities, facets, reply, langs, tags, createdAt) through buildRecordSchema, then loopPathExample. The branch returns event.commit.record.entities[], and with the filter it returns event.commit.record.facets[].
    • Please add both cases as tests: a deprecated array listed first is skipped, and a schema whose only arrays are deprecated gives undefined.

What I checked #

  • Embed URL and Page URL: bsky-post and kipclip now take examples and show e.g. https://example.com, plus or {{examples.url}} when the record has a URL field. AutomationForm.tsx:2729 passes examples to every EditorBlock.
  • Record builder boolean: the boolean placeholder input now shows {{placeholder}} and has the accessible name <field> placeholder.
  • patch-record Base Record URI: the example is {{event.uri}} only when the trimmed targetCollection equals the trigger collection. The form only passes that collection when the lexicon is a valid NSID. event.uri is a built-in placeholder (lib/actions/template.ts:315). It also passes the parsed.collection !== targetCollection check in lib/actions/patch-record.ts. For any other collection there is no example, which the issue allows.
  • The issue's grep: git grep -n 'e\.g\. {{event\.commit\.record\.\|or {{event\.commit\.record\.' -- app ':!*.test.*' finds nothing.
  • Searching for the rest of the bug class: I ran the broader event\.commit\.record\. grep and went through every placeholder= in app/. What is left:
    • prose hints (AutomationForm.tsx:3105, schedule-config-editor.tsx:112);
    • pre-filled defaults rather than examples (follow subject, bsky-list-add DEFAULT_RECORD_SUBJECT), which pr.md already flags as possible issues of their own;
    • collect-links' "Allowed paths" example (embed.external.uri, facets[].features[].uri). Those paths are relative to the action's source, which its own example sets to {{post.record}}, not to the trigger record, so it is not this bug.
  • Tests: the new and changed tests cover each changed placeholder, both with a fitting field and without one. The collectArrayPaths code moved unchanged.
  • Keyboard and focus: no focus or keyboard behaviour changed.

Follow-up: file #

The follow-up is real. In RecordFormBuilder.tsx, BooleanField maps __placeholder__ to onValueChange(""). It only shows the input when isPlaceholder is true, and isPlaceholder requires value !== "". So choosing "Placeholder..." never shows the input. This bug was there before the branch and is a separate one: this issue only changes the example text inside that input. It breaks a control people use, so it is worth an issue.

loop:verdict run=review-20261030-101806-66 sha=e63e3549b302067ad4ef8c4ac98742dd1eafa49e verdict=changes-requested

Fix round 1: the review asked for changes.

loop:fix run=contribute-20261030-102208-66 round=1 issue=66

fix(form): derive the last lexicon-specific input examples from the trigger record

The bsky-post Embed URL, kipclip Page URL, loop Array path, record-builder boolean placeholder and patch-record Base Record URI inputs no longer show examples that name post, like or account fields. Each one now takes its example from the selected trigger lexicon, or shows a generic example or none. Before, with other triggers, the example pointed at a field the record does not have, or (for patch-record) at a URI the action rejects.

What changed #

  • bsky-post / kipclip-annotation: the editors now take the examples prop and show e.g. https://example.com or {{examples.url}}, the same as semble-save and margin-bookmark. Without a uri field they show e.g. https://example.com.
  • Loop Array path: the example is the first entry of collectArrayPaths(triggerRecordSchema), through the new loopPathExample. It skips arrays whose lexicon description starts with "deprecated" (matched by /^\s*deprecated\b/i), so a post trigger shows facets[] rather than the deprecated entities[]. With no current array it shows no example. collectArrayPaths and its walkers moved unchanged from AutomationForm.tsx to step-drafts/field-examples.ts so they can be unit tested.
  • RecordFormBuilder boolean placeholder input: shows {{placeholder}} and now has an accessible name (<field> placeholder). Before, it had no label at all.
  • patch-record Base Record URI: TriggerExamples now carries the trigger collection. The form fills it in when the lexicon is a valid NSID. The new patchBaseUriExample returns {{event.uri}} only when targetCollection (trimmed) equals that collection. Otherwise there is no example: lexicons do not say which collection an at-uri field points at, so no trigger field can be shown to pass the action's collection check.

Verified #

  • pnpm run check: passes. pnpm run test: 2806 tests pass.
  • New and updated tests, each covering the case with a fitting field and the case without:
    • field-examples.test.ts: collectArrayPaths, loopPathExample (including a deprecated array listed first, one inside an object, and a record whose only arrays are deprecated, which gives undefined), patchBaseUriExample, and triggerExamples with a collection.
    • New bsky-post.test.tsx and kipclip-annotation.test.tsx: the URL placeholder with and without examples.url.
    • patch-record.test.tsx: same collection, typed target, another collection, unknown trigger collection. This replaces the old tests that expected the trigger's at-uri.
    • RecordFormBuilder.test.tsx: the boolean placeholder input's example and its name.
    • AutomationForm.test.tsx: the loop path example with and without an array (the post schema now lists the deprecated entities array before langs, and the example is still langs[]), and patch-record's example through the whole form for a matching and a different target.
  • git grep -n 'e\.g\. {{event\.commit\.record\.\|or {{event\.commit\.record\.' -- app ':!*.test.*' finds nothing. The remaining examples are built from the schema at runtime.
  • Only placeholder text and one aria-label changed. No focus or keyboard behaviour changed.

Acceptance criteria #

Decisions you can reverse #

  • The loop example skips deprecated fields through nested objects too, not only at the top level, and drops a deprecated object along with the arrays inside it. The datalist of suggestions (collectArrayPaths) still lists deprecated arrays.
  • bsky-post's Embed URL placeholder now starts with e.g. (e.g. https://example.com), as the issue asks. Before, it read https://example.com or ....
  • For patch-record with a different collection, it shows no example rather than trying to match at-uri fields to the target. Lexicons carry no target-collection hint to match on.
  • The boolean placeholder input shows {{placeholder}} rather than a field taken from the trigger lexicon.
  • Not changed, because the issue does not list them: the follow editor still pre-fills Subject DID with {{event.commit.record.subject}}, and its hint still says "like {{event.did}} or {{event.commit.record.subject}}". The git grep above does not match these. With a trigger that has no subject, the pre-filled value does not fit, so they may deserve their own issue.

This round #

Review fix: the loop's Array path example skips arrays the lexicon marks deprecated, so app.bsky.feed.post shows event.commit.record.facets[] instead of entities[]. Added the two requested tests (deprecated array first is skipped, only deprecated arrays give undefined) and the deprecated entities array in the form test's post schema.

Follow-up, filed as a new issue on merge if the review agrees:

fix(form): show the placeholder input when "Placeholder..." is picked for a boolean field

In the record builder, a boolean field's select has a "Placeholder..." option, but picking it never shows the text input for the placeholder. BooleanField in app/islands/RecordFormBuilder.tsx maps __placeholder__ to onValueChange(""), and it only shows the input when isPlaceholder (value !== "" && value !== "true" && value !== "false") is true. An empty value is never a placeholder, so the input stays hidden and the select drops back to "Select..." on the next render. The input only appears when a loaded template already has a placeholder in that field.

How I know: a happy-dom render of RecordFormBuilder with { live: { type: "boolean" } }. I set #field-live to __placeholder__ and dispatched change. After that, input[aria-label="live placeholder"] was still missing.

Where to start: keep a local "placeholder mode" state in BooleanField, set when the option is picked, so the input renders while the value is still empty. Then move focus to that input so the keyboard user lands on it.

Review: https://tangled.org/exosphere.site/airglow/compare/main...loop/66

loop:pushed run=contribute-20261030-102208-66 sha=81a0f2454d2b97f2e8aa8c39f22205b6b70153bf

merge. The one change round 1 asked for is done. loopPathExample now skips arrays whose description matches /^\s*deprecated\b/i, so a post trigger shows e.g. event.commit.record.facets[] again instead of entities[]. Both requested tests were added, and the form test's post schema now has the deprecated entities array. All six acceptance criteria still hold, and pnpm run check and pnpm run test (2806 tests) both pass on 81a0f24.

What I checked and how:

  • Round 1's point: I read the e63e354..81a0f24 diff. withoutDeprecated filters only a copy of the schema, used by loopPathExample. collectArrayPaths, which also feeds the loop path suggestions in arrayPathSuggestionsForScope, is unchanged, as round 1 asked. Nothing from round 1's checks was reverted. ObjectNode.properties is required in schema-types.ts, so the recursion cannot hit undefined.
  • The most common real input: in a throwaway vitest file (since deleted), I ran an app.bsky.feed.post lexicon in its real property order (text, entities, facets, reply, langs, tags, createdAt, with #entity itself marked deprecated) through the server's buildRecordSchema, then loopPathExample. It gives event.commit.record.facets[]. A profile lexicon, which has no array, gives undefined. patchBaseUriExample gives {{event.uri}} for target app.bsky.feed.post and nothing for app.bsky.feed.like.
  • The other criteria, re-checked on this head: bsky-post and kipclip show e.g. https://example.com, adding or {{examples.url}} only when the trigger has a URL field. The boolean placeholder input shows {{placeholder}} and has the accessible name <field> placeholder, which tells it apart from the select labelled <field>. patch-record's example is {{event.uri}} only when the trimmed targetCollection equals the trigger NSID. The issue's grep finds nothing. In the broader event\.commit\.record\. grep, every hit is a prose hint, a pre-filled default, or a path built at runtime, the same as round 1 found.
  • Main moved: the branch is based on 49d36ac, and main is now at d6a11d6. git merge-tree shows a clean merge.

Follow-up: file. I re-checked the claim against RecordFormBuilder.tsx on this head. BooleanField maps __placeholder__ to onValueChange(""), but it only shows the input when isPlaceholder is true, and isPlaceholder requires value !== "". So choosing "Placeholder..." never shows the input. This bug was there before the branch, and this issue does not fix it: it only changes the example text inside that input. It breaks a control people use, so it is worth its own issue.

loop:verdict run=review-20261030-102941-66 sha=81a0f2454d2b97f2e8aa8c39f22205b6b70153bf verdict=merge followup=1

Merged main into the branch: 36c7a1db50d6b585044c94acfaada55a8e07d94b.

loop:carry from=81a0f2454d2b97f2e8aa8c39f22205b6b70153bf sha=36c7a1db50d6b585044c94acfaada55a8e07d94b

Merged onto main as cdc51e52a4884054cacbfef3706264995a39e45b.

loop:merged sha=cdc51e52a4884054cacbfef3706264995a39e45b head=36c7a1db50d6b585044c94acfaada55a8e07d94b

Filed the follow-up as #86, a new issue, not marked ready, from branch loop/66 (cdc51e52a488): fix(form): show the placeholder input when "Placeholder..." is picked for a boolean field

loop:filed issue=86

Sign up or Sign in to add to the discussion.