Automations and webhooks for the AT Protocol airglow.run

Record form builder makes optional fields look required #26

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

Problem #

  • The optional reply object renders expanded, with required stars on its children.
  • The deprecated entities field is shown.
  • Blobs are entered as raw JSON.

Code: RecordFormBuilder.tsx:735-770.

Seen in the session #

In https://www.youtube.com/watch?v=5MnT-ZgbBIc:

  • 1:27: "those indicate to me that I need something... I'm not trying to do a reply."

Fix direction #

Collapse optional objects behind an "Add" toggle and only star their children once enabled. Hide deprecated fields.

Details: docs/brainstorms/2026-10-17-graze-conference-automation-feedback.md (U10).

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

loop:queued cid=bafyreigy6jn43w3pbwcccx4y6z3b2uzot6eonttur3km5nbmzv3fobkjga

Working on this, attempt 1.

loop:claim run=contribute-20261026-153330-3my32g6j6ps22 branch=loop/3my32g6j6ps22 attempt=1 issue=26

fix(form): collapse optional objects and hide deprecated fields in the record builder

The record form builder used to open every nested object, so an optional object like a post's reply showed up expanded, with required stars on root, parent and their uri/cid. That made it look mandatory. Optional objects now start collapsed behind a "+ Add

#

#

  • [x]
  • [x]
  • [x]

#




loop:pushed run=contribute-20261026-153330-3my32g6j6ps22 sha=4c9080ed630098b288141d7605f3d012c1f5eff0

Verdict: awaiting-owner. The branch does the two things the issue's fix direction asks for, and it does them correctly. Optional objects (a post's reply) now start collapsed behind "+ Add


loop:awaiting-owner run=review-20261026-154526-3my32g6j6ps22 sha=4c9080ed630098b288141d7605f3d012c1f5eff0 verdict=awaiting-owner

Blobs are out of scope for this issue. Merge the branch as it is: collapsing optional objects and hiding deprecated fields is all #26 needs. Blob fields get their own follow-up issue.

The edge case you noted, a deprecated field disappearing once its value is cleared, is fine as is.

Verdict: changes-requested. The branch does what #26 asks. Optional objects such as a post's reply now start collapsed behind "+ Add reply", with no stars on their children until they are added, and deprecated optional fields such as entities are hidden. The owner put blobs out of scope and accepted the edge case where a deprecated field disappears once cleared. One gap is left in the new control: focus is lost each time it is used. When a keyboard user presses "+ Add reply", that button is removed and focus drops to <body>. "Remove reply" does the same. The next Tab then starts again from the top of a long automation form.

What I checked:

  • pnpm run check passes. pnpm run test passes: 167 files, 2611 tests. The branch is behind main (based on 68784c1), but git merge-tree against main 841cfb7 shows it merges cleanly.
  • I read every changed line in RecordFormBuilder.tsx. initState now only creates state for required objects and skips optional deprecated fields. serializeNode treats an undefined object as absent, so the record leaves it out. Saved templates load back through deserializeObject, so a saved reply opens expanded and a missing one opens collapsed. Array items still initialize their nested required objects. Union variants and nested objects go through the new PropertyFields, so they collapse the same way. In patch mode, ticking a field initializes it as if required. No change falls outside the issue.
  • Real use: I wrote a throwaway happy-dom test (since deleted) that builds a schema with the real buildRecordSchema from an app.bsky.feed.post-shaped lexicon (text, deprecated entities, reply → #replyRef → strongRefs, langs, tags, createdAt). Filling text with {{event.commit.record.text}} emits only {"text": ..., "createdAt": "{{now}}"}. entities is not rendered, reply is collapsed and only text/createdAt are starred. Adding reply shows the starred root/parent uri/cid, and removing it drops it from the output again.
  • Keyboard: in the same test I focused "+ Add reply" and activated it. document.activeElement afterwards was BODY. Same for "Remove reply". The accessible names ("+ Add reply", "Remove reply") are fine and tell these controls apart from the ones beside them.

Changes:

  1. In ObjectField (app/islands/RecordFormBuilder.tsx), keep focus in place when the object is added or removed. After "+ Add <name>", move focus into the expanded fieldset: its "Remove <name>" button, or the first field. After "Remove <name>", move focus to the "+ Add <name>" button that replaces it. For example, set a pending-focus flag in the click handler and apply it in an effect or ref callback after the re-render.
  2. Add a test to the "RecordFormBuilder optional and deprecated fields" suite. Focus "+ Add reply", click it, and assert document.activeElement is inside the reply fieldset rather than document.body. Then do the same for "Remove reply" landing on "+ Add reply".

loop:verdict run=review-20261026-214904-3my32g6j6ps22 sha=4c9080ed630098b288141d7605f3d012c1f5eff0 verdict=changes-requested

Fix round 1: the review asked for changes.

loop:fix run=contribute-20261026-215156-3my32g6j6ps22 round=1 issue=26

fix(form): collapse optional objects and hide deprecated fields in the record builder

The record form builder used to open every nested object, so an optional object like a post's reply showed up expanded, with required stars on root, parent and their uri/cid. That made it look mandatory. Optional objects now start collapsed behind a "+ Add <name>" button, and their children only show up (with their stars) once the object is added. "Remove <name>" collapses it again and drops it from the record. Keyboard focus moves with these controls: adding an object puts focus on its "Remove <name>" button, and removing it puts focus back on "+ Add <name>". Optional fields whose lexicon description starts with "DEPRECATED" (such as entities on app.bsky.feed.post) are now hidden.

Verification #

  • New tests in app/islands/RecordFormBuilder.test.tsx (an app.bsky.feed.post-shaped schema):
    • reply renders collapsed with no stars and is not written.
    • Adding it shows the starred children, filling them writes reply, and removing it drops it again.
    • A saved template with reply opens expanded.
    • entities is hidden, but a saved template that sets it still shows it and keeps its value.
    • Patch mode doesn't list entities, and ticking reply there opens it expanded.
    • Focus stays in place: after "+ Add reply" is clicked, document.activeElement is the "Remove reply" button inside the reply fieldset. After "Remove reply" is clicked, it is "+ Add reply". Without the fix, this test fails.
  • pnpm run check passes.
  • pnpm run test passes: 2612 tests.
  • I didn't try it in a browser: there is no dev server or PDS in this environment.

Acceptance criteria #

This round #

The review found that keyboard focus dropped to <body> when "+ Add <name>" or "Remove <name>" was used. ObjectField now records which control should get focus in the click handler and focuses it in an effect after the re-render. A test for this is added to the "RecordFormBuilder optional and deprecated fields" suite.

Decisions you can reverse #

  • What counts as deprecated: lexicons have no deprecation flag, so a field counts as deprecated when its description starts with "deprecated" (case-insensitive). Deprecated fields are still shown when they're required, when a saved template already gives them a value, or when they're already ticked in patch mode. That keeps existing templates editable.
  • Optional objects are no longer pre-filled: an untouched optional object now has no state, so defaults inside it (such as {{now}} for a datetime) aren't written until the user adds it. Before, an optional object holding only a datetime was written to the record with nothing but {{now}} in it.
  • Where focus lands after adding: focus goes to the "Remove <name>" button, the first control in the expanded fieldset, rather than to the first field inside it. Tabbing forward from there reaches the fields.
  • Patch mode: ticking a top-level object opens it expanded, since ticking it already means "I want this". Optional objects nested inside it start collapsed like everywhere else.

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

loop:pushed run=contribute-20261026-215156-3my32g6j6ps22 sha=2bd1e5ea1b69eab80a8083199de7aacd8a05aa74

Verdict: merge. The branch does what #26 asks: optional objects now start collapsed behind a "+ Add <name>" button, their children's required stars show only once the object is added, and deprecated fields are hidden. The owner's comment puts blobs out of scope and approves the edge case where a deprecated field disappears once its value is cleared. pnpm run check and pnpm run test both pass.

What I checked and how:

  • The diff against the merge base (68784c1) touches only app/islands/RecordFormBuilder.tsx, its .css.ts and its test file. Nothing is out of scope, and there is no migration.
  • I read every changed line: initState now takes required, so only required objects get state and optional deprecated fields get no default. PropertyFields now renders the record, nested objects and union variants. ObjectField has the collapsed/expanded states and moves focus. Patch mode hides deprecated fields that aren't ticked, and ticking a field initialises it as required. Restore still works: a saved template with reply deserializes to state and opens expanded. Without it, the object is undefined and stays collapsed. A required object with no state still renders, through value ?? {}.
  • pnpm run check: passes (format, lint, types). pnpm run test: 167 files, 2612 tests, all pass.
  • Real-use check: I wrote a throwaway test, since deleted. It ran a real-shaped app.bsky.feed.post lexicon through buildRecordSchema, with mocked external refs (strongRef, embed.images, embed.defs), and rendered the builder:
    • On first render, entities is hidden, reply shows only "+ Add reply", and only text and createdAt have stars. The template emitted is {"createdAt": "{{now}}"}.
    • After "+ Add reply", focus is on "Remove reply", and root/parent/uri/cid show with stars. Filling them writes reply with $type: app.bsky.feed.post#replyRef, and each strongRef gets $type: com.atproto.repo.strongRef. All required fields are present.
    • After "Remove reply", reply leaves the record and focus goes back to "+ Add reply".
    • In the images embed variant, the nested optional aspectRatio also starts collapsed.
  • UI controls: both are native <button type="button">, so they work from the keyboard. Focus lands on the button that replaces the one you used. The accessible names "+ Add reply" and "Remove reply" include the field name, so they can be told apart from the controls next to them.
  • There is no followup.md. The owner said blobs will get their own issue.

loop:verdict run=review-20261026-220408-3my32g6j6ps22 sha=2bd1e5ea1b69eab80a8083199de7aacd8a05aa74 verdict=merge

Merged main into the branch: 9730ba3ade9ad5dca580cb505502e87a04e179f4.

loop:carry from=2bd1e5ea1b69eab80a8083199de7aacd8a05aa74 sha=9730ba3ade9ad5dca580cb505502e87a04e179f4

Merged onto main as 3671815928ee19f5453c5bdf0d61d9789c364eb8.

loop:merged sha=3671815928ee19f5453c5bdf0d61d9789c364eb8 head=9730ba3ade9ad5dca580cb505502e87a04e179f4

Follow-up: optional blob fields are tracked in #59. Direction: collapse them behind "+ Add ", as this issue does for optional objects.

Sign up or Sign in to add to the discussion.