Skip to content

feat(automations): highlight {{ }} tokens + flag typos in builder text fields (#3653)#3727

Merged
Yeraze merged 2 commits into
mainfrom
feat/automation-token-hints
Jun 25, 2026
Merged

feat(automations): highlight {{ }} tokens + flag typos in builder text fields (#3653)#3727
Yeraze merged 2 commits into
mainfrom
feat/automation-token-hints

Conversation

@Yeraze

@Yeraze Yeraze commented Jun 25, 2026

Copy link
Copy Markdown
Owner

Second of the two documented post-#3721 follow-ups (AUTOMATION_ENGINE_PLAN.md §11).

Why

{{ }} substitution tokens were indistinguishable from plain text in the builder, and a typo (e.g. {{ trigger.lastestVersion }}) silently rendered blank with no warning — exactly the confusion that surfaced while testing the Startup automation.

Change

Token-bearing text/textarea fields (flagged tokens: true in the catalog: message text + DM-to, notify title + body, condition values, setVar value) now render with TokenTextField — a highlight backdrop layered under a transparent input:

  • {{ trigger.* }} / {{ var.* }} / {{ NOW }} tokens are blue when recognized, red + wavy underline when not.
  • Unrecognized tokens are also listed inline below the field ("Unrecognized token(s): … — check for typos").

Recognition uses tokenHints.ts (validTokenSet) built from the exported TRIGGER_TOKENS + UNIVERSAL_TOKENS for the current trigger plus the known variable names. It's a non-blocking hint (not a hard save gate) so a token the static registry doesn't enumerate can't block a legitimate save.

Tests

tokenHints.test.ts covers validTokenSet (trigger tokens + universals + vars + NOW), unknownTokens (typo + unknown-var detection, empty-token + dedup handling), and tokenize (segment/known flags). Full suite 7518 passed, 0 failures; tsc + build clean.

The highlight overlay is a layered backdrop/transparent-input technique — worth a quick visual sanity check in the dev container (alignment/scroll-sync).

🤖 Generated with Claude Code

…t fields (#3653)

Second of the two post-#3721 follow-ups. Token-bearing text/textarea fields
(catalog `tokens: true`: message text/DM-to, notify title/body, condition
values, setVar value) now use TokenTextField — a highlight backdrop renders
`{{ trigger.* }}` / `{{ var.* }}` tokens blue when recognized and red+wavy when
not, with unrecognized tokens listed inline below the field to catch typos like
`{{ trigger.lastestVersion }}` or a `var.` name with no matching variable.

- tokenHints.ts (pure, tested): validTokenSet(triggerType, vars) from the
  exported TRIGGER_TOKENS + UNIVERSAL_TOKENS + known variables; tokenize() and
  unknownTokens().
- Non-blocking hint (not a hard save gate) to avoid false positives on tokens
  the static registry might not enumerate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBLhGGNh35oMwTL53va1Y5
@github-actions

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown

Claude finished @Yeraze's task —— View job


PR Review: feat(automations): highlight {{ }} tokens + flag typos in builder text fields

  • Gather context and read changed files
  • Review TokenTextField.tsx
  • Review tokenHints.ts and tokenHints.test.ts
  • Review AutomationBuilder.tsx and catalog.ts changes
  • Compile and post feedback

Overall this is a clean, well-scoped implementation. The layered backdrop/transparent-input technique is a well-known approach for this kind of rich-text-without-a-rich-editor use case. A few observations:

TokenTextField.tsx

Zero-width space character (line 42)

{highlighted}{''}

There's a zero-width space (U+200B) in the trailing string literal. This is likely intentional to prevent backdrop collapse, but it's invisible in code review and will confuse future maintainers. A comment explaining this would be helpful, e.g.:

{highlighted}{'\u200B' /* prevent backdrop collapse */}

Scroll sync on single-line <input>
Single-line <input> elements technically do support scrollLeft (for horizontally overflowing text), so onScroll on the input is correct. However, browsers rarely fire a scroll event on <input type="text"> — the scroll typically happens silently. This means horizontal scroll position can desync on very long single-line values. Consider also syncing on onChange for single-line inputs:

onChange={(e) => { onChange(e.target.value); syncScroll(e.currentTarget); }}

key={i} on segment list
Using index as key in segs.map is fine here since the backdrop is never directly interacted with, but it's worth noting for completeness.

aria-hidden="true" on backdrop
Correct — assistive technology shouldn't read the duplicate backdrop content.


tokenHints.ts

TOKEN_RE is a global regex with the g flag (line 11)

const TOKEN_RE = /\{\{\s*([^}]+?)\s*\}\}/g;

Because TOKEN_RE is module-level and uses the g flag, RegExp.lastIndex is stateful. String.matchAll() resets lastIndex before iterating, so the current usage is safe — matchAll creates a fresh iterator each call. This is fine, but worth documenting since future refactors that use .exec() in a loop directly would break subtly.

Empty-token handling in tokenize (line 35)

segs.push({ text: m[0], token: path.length === 0 ? false : ..., known: path.length === 0 || valid.has(path) });

The logic is correct: {{ }} gets token: false, known: true (treated as plain text). This is consistent with the unknownTokens behavior.

Regex captures content up to first }
[^}]+? stops at the first }. So {{ trigger.foo } (missing second }) won't match — which is correct behavior.


tokenHints.test.ts

Coverage is good for the happy paths. A few edge cases worth adding:

  • Whitespace variants: {{trigger.text}} (no spaces), {{ trigger.text }} (extra spaces) — the regex has \s* on both sides of the capture group, so extra spaces around the token name should work, but the m[1].trim() on line 34 handles internal padding too.
  • Nested {{ }} (malformed): {{ {{ nested }} }} — the lazy [^}]+? should handle this correctly by stopping at the first }, leaving leftover text, but a test confirming no crash would be reassuring.
  • A trigger type not in TRIGGER_TOKENS (e.g. a future trigger type): validTokenSet('trigger.unknown', []) — the ?? [] fallback on line 16 handles it, but no test covers this path.

AutomationBuilder.tsx

onChange type mismatch (line 72–73)

<TokenTextField multiline value={...} ... onChange={onChange} />

TokenTextField.onChange is typed as (v: string) => void, but the outer onChange for FieldInput is (v: unknown) => void. TypeScript likely accepts this because string is assignable to unknown in the callback's perspective, but it's worth verifying no type widening issues arise downstream.


catalog.ts

The tokens: true annotation is cleanly added to the relevant fields. No issues here.


CSS (AutomationsPage.css)

The layered position: absolute; inset: 0 approach is correct. Two minor observations:

  1. .ae-tokenfield-backdrop uses white-space: pre for single-line. If the user types a very long value in a single-line field, the backdrop overflows. It's hidden (overflow: hidden) so it won't visually escape the box, but the horizontal scroll sync noted above becomes more important in this case.

  2. .ae-tokenfield-input sets border-color: transparent, which removes the visual border from the input itself. The focus ring is then applied via .ae-tokenfield:focus-within .ae-tokenfield-backdrop. This is clever — but it means the border-color: var(--ctp-blue) focus style applies to the backdrop (which has position: absolute) rather than the actual layout element. Visually this works only if the backdrop exactly covers the input's border box, which the inset: 0 ensures. Worth a quick visual sanity-check at different zoom levels.


Summary

Area Status
Core logic (tokenHints.ts) ✅ Correct and clean
UI component (TokenTextField.tsx) ✅ Good; minor scroll-sync and zero-width space doc nits
Tests (tokenHints.test.ts) ✅ Covers main cases; a few edge cases missing
Catalog integration ✅ Clean tokens: true annotation approach
CSS ✅ Solid; layered backdrop technique well-executed
Performance ✅ Correctly useMemo-guarded
Security ✅ No concerns — pure UI, no eval/innerHTML
Accessibility aria-hidden on backdrop, spellCheck={false} on inputs

Nothing blocking here. The zero-width space documentation and the single-line scroll-sync on onChange are the two items most worth addressing before merge.

…#3653)

Per PR review nit — the backdrop's trailing zero-width space was a literal
invisible char in source (a readability/foot-gun, like a stray NUL). Use an
explicit ​ escape + comment instead. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBLhGGNh35oMwTL53va1Y5
@Yeraze

Yeraze commented Jun 25, 2026

Copy link
Copy Markdown
Owner Author

Thanks for the review. Triage:

Addressed (c12d15c1)

  • The backdrop's trailing zero-width space was a literal invisible char in source → replaced with an explicit \u200b escape + comment.

Verified, no change needed

  • onChange type: FieldInput's (v: unknown) => void is contravariantly assignable to TokenTextField's (v: string) => void (a handler taking unknown can be called with a string); tsc is clean.
  • Single-line scroll-sync: already wired — onScroll syncs scrollLeft/scrollTop on both the input and textarea, and the backdrop's overflow: hidden keeps it in the box.
  • Focus ring on the inset:0 backdrop: intentional; it exactly covers the input's border box. Will eyeball at zoom during manual testing.

Non-blocking overall — thanks for the thorough pass.

@github-actions

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown

Claude finished @Yeraze's task —— View job


PR Review: feat(automations): highlight {{ }} tokens + flag typos in builder text fields

  • Gather context
  • Review TokenTextField.tsx
  • Review tokenHints.ts and tokenHints.test.ts
  • Review AutomationBuilder.tsx and catalog.ts changes
  • Review CSS changes
  • Compile and post feedback

This is the second review pass, after c12d15c addressed the zero-width space documentation nit from the first pass. Overall the implementation is clean and the PR is in good shape.

TokenTextField.tsx

\u200b fix is confirmed — the zero-width space is now explicitly escaped with a comment. ✅

onScroll on <input> (line 62) — as noted in the first review, browsers rarely fire a scroll event on <input type="text">. The current code is correct in that it syncs when a scroll does fire, but for long single-line values a resize or programmatic value change can shift the scroll position without firing scroll. The Yeraze response to the first review noted this was "already wired" — that's true, but the wiring only triggers on a scroll event, not on onChange. This is a minor UX rough edge, not a bug, since the scenario requires a very long single-line value.

No type attribute on <input> (line 56) — the default is type="text", which is correct here, but being explicit is a best practice. Minor nit.

key={i} on segment spans (lines 35-36) — acceptable here since the backdrop is not interacted with and keys are only needed for reconciliation stability, not for correctness.


tokenHints.ts

TOKEN_RE module-level global regex with g flag (line 11)matchAll() is safe with a global regex, but the comment from the first review stands: worth documenting since direct .exec() re-use would silently break. As-is the code is correct.

m[1].trim() double-trims (line 34) — the regex already has \s* on both sides of ([^}]+?), so m[1] will already have leading/trailing whitespace stripped at the regex level. The .trim() call is redundant but harmless — it just adds a very small overhead per token match.

TRIGGER_TOKENS[triggerType] ?? [] fallback (line 16) — correctly handles unknown/future trigger types. ✅

text.matchAll(TOKEN_RE) called twice — once in tokenize and once in unknownTokens when rendering TokenTextField. Both are useMemo-guarded in the component, so this doesn't iterate twice on every keystroke. ✅


tokenHints.test.ts

Coverage is good for the primary use cases. A few gaps worth noting (non-blocking):

  1. No test for unknown trigger type: validTokenSet('trigger.doesNotExist', []) should return a set with just NOW + universals. The ?? [] fallback handles it, but there's no test confirming this.

  2. Whitespace variants not tested: {{trigger.text}} (no spaces) should match — the regex \s* handles it but no test confirms.

  3. tokenize with no tokens: a plain-text input with no {{ }} is not explicitly tested (though it works — it returns a single plain segment).

  4. tokenize text reconstruction: the test on line 39 verifies join('') equals the input — this is a nice roundtrip test. ✅


AutomationBuilder.tsx

onChange type (line 73): FieldInput.onChange is typed (v: unknown) => void, and TokenTextField.onChange is (v: string) => void. Passing a (v: unknown) => void handler where (v: string) => void is expected is structurally compatible (you can always call a function that accepts unknown with a string), and tsc accepts this. ✅

default case in FieldInput switch (line 167-171): The tokens flag on the default case handles kind: 'text' fields. This is correct — the default arm covers the 'text' kind since it's not an explicitly handled case. Worth adding a comment or an explicit case 'text': for clarity, but not a bug.


catalog.ts

The tokens: true annotations are applied consistently to:

  • condition.numericvalue
  • condition.stringvalue
  • condition.variablevalue
  • action.sendMessagetext, to
  • action.notifytitle, body
  • flow.setVarvalue

This looks complete and well-scoped. The condition value fields being token-aware is particularly useful (e.g., comparing against {{ var.threshold }}). ✅


AutomationsPage.css

white-space: pre on single-line backdrop (line 84) — correct for preserving exact text metrics. The overflow: hidden clip prevents visual overflow. ✅

Focus ring on backdrop (line 92)ae-tokenfield:focus-within .ae-tokenfield-backdrop { border-color: var(--ctp-blue) } applies the focus border to the position: absolute backdrop overlay. Since the backdrop uses inset: 0 and shares the same border-box metrics as the input, this works visually, but the focus indicator is technically on the aria-hidden element rather than the actual focusable input. This is a minor a11y technicality — screen readers still focus the real input; the border is just cosmetic.

color-mix(in srgb, ...) usage (lines 94, 98)color-mix() has excellent support in all modern browsers but is not supported in IE11 or older Safari (pre-16.4). Given MeshMonitor targets modern browsers this is fine, but worth being aware of.


Summary

Area Status
Core logic (tokenHints.ts) ✅ Correct; minor double-trim nit
UI component (TokenTextField.tsx) ✅ Good; single-line scroll-sync is a known minor limitation
Tests (tokenHints.test.ts) ✅ Main paths covered; a few edge cases missing (non-blocking)
Catalog integration tokens: true applied to all correct fields
CSS ✅ Solid technique; color-mix() is modern-browser-only (acceptable)
Accessibility aria-hidden on backdrop; focus ring on backdrop is cosmetic-only
TypeScript tsc clean; type widening is safe
Security ✅ No concerns — pure UI, no eval/innerHTML

Nothing blocking. The implementation is solid. The remaining items (single-line scroll sync on onChange, missing edge-case tests) are all non-blocking nits.

@Yeraze Yeraze merged commit dead9ec into main Jun 25, 2026
20 checks passed
@Yeraze Yeraze deleted the feat/automation-token-hints branch June 25, 2026 14:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant