[FE feat] Mustache support#4465
Open
ardaerzin wants to merge 75 commits into
Open
Conversation
Remove normalizeCompact() wraps from buildEvaluatorExecutionInputs in
runnable/utils.ts so testcase values, upstream output, and ground_truth
arrive at the backend as native JSON (object/array/number/...) rather
than stringified text. Required for mustache nested access (e.g.
{{geo.region}}) to work over object-typed variables.
The completion-path was already preserving native types post-#4394
(loadableController.selectors.row().data is native). Added a clarifying
comment to toDisplayString in execution/selectors.ts to prevent future
misuse for transport — it is a UI-display helper only.
13 new tests in build-evaluator-execution-inputs.test.ts pin the contract:
schema-driven and legacy paths, object/array/primitive preservation,
gap-04 invariant (JSON-shaped strings stay strings).
Move Mahmoud's V2 view-mode vocabulary from the design-mockups POC (web/apps/design-mockups/src/components/proposed/) into @agenta/entity-ui under a new ./view-types subpath. Exports: - ViewType (6-way: text / markdown / chat / form / json / yaml) - FieldKind (4-way bucketing for view-options decision) - NestedKind (precise nested kind for form widgets) - isChatMessagesArray, detectFieldKind, detectNestedKind - getViewOptions, getDefaultViewForValue - ViewTypeSelect (the "View as ▾" dropdown component) - FormView (recursive form rendering for objects/arrays) - Pure formatters: valueToDisplay, coerceTextEdit, parseJsonEdit, parseYamlEdit The chip vocabulary stays distinct from FieldKind. For type chips, consumers use TypeChip + inferLogicalType from @agenta/ui + @agenta/shared (granular: string / number / boolean / null / json-object / json-array). V2's FieldKind is INTERNAL to view-options decision logic only. Tests live in agenta-entities/tests/unit (stopgap until entity-ui gets its own vitest runner from #4393): 22 view-types tests + 23 formatters tests pinning the contract.
New component in @agenta/playground-ui/playground-inputs-body that renders a list of per-variable bordered cards composed from @agenta/entity-ui/view-types primitives. Replaces the per-variable SharedEditor rendering with type chips + per-variable "View as ▾" dropdown (text / markdown / chat / form / json / yaml). All edits write NATIVE values via onValueChange — never stringifies on the way out (RFC: "native JSON stays native until template rendering"). Components: - PlaygroundInputsBody: top-level orchestrator. Props: rowId, inputs, unreferencedColumns, editable, onValueChange, onAddDraftColumn, onViewModeChange. - VariableCard: single card with header (name + TypeChip via inferLogicalType + ViewTypeSelect + optional [draft] badge) and body switched by mode (SharedEditor / FormView / ChatMessageList / JSON / YAML code editor / InputNumber / Switch per type). - UnreferencedColumnsFooter: collapsed-by-default footer rendered once below all cards. "N unused testcase columns hidden..." - viewModeAtoms: atom family keyed by (rowId, varName) — session scoped per-variable view-mode state. Wiring into existing playground (SingleLayout / ComparisonLayout) lives in Step 6 — this commit ships the presentational component.
…body
The new V2-aligned input UX renders one card per variable that the
prompt references — including draft variables (referenced but not yet
on the testcase). Testcase columns the prompt does NOT reference are
collapsed under an "N unused testcase columns" footer.
This commit adds the pure split helper + the atom layer that feeds it:
- splitInputsVisibility({referencedKeys, testcaseData}) → {inputs,
unreferencedColumns}. Pure function in execution/visibility.ts.
inputs[i].isDraft = true when name is referenced but missing from
testcaseData.
- referencedVariableKeysAtomFamily(downstreamKey): schema-referenced
variable keys only (template input ports + downstream evaluator
expected columns). Excludes testcase-only extras.
- rowVariableKeysAtomFamily refactored to call referencedVariableKeys
+ add testcase-extras on top. Same external contract — connected
testset still merges expected_output and friends.
- playgroundInputsAtomFamily({testcaseId, downstreamKey}): wraps the
pure split with the live atom sources. System fields stripped from
testcase data before splitting so __id__ and friends don't bleed
into the unused-columns footer.
- executionItemController.selectors.inputsVisibility(...) +
.referencedVariableKeys(...) expose the new atoms on the controller
surface — matches PlaygroundInputsBody's props shape directly.
13 new tests pin the contract: referenced+testcase intersection
(native preservation), draft annotation (referenced - testcase),
null/undefined value handling, unreferenced collection, edge cases.
Small antd Select for choosing how a prompt template renders (Mustache / Jinja2 / [Curly] / [F-string]). Wraps a vendored buildTemplateFormatOptions(currentFormat) helper that matches the WP-B3 web-handoff contract: - New / mustache / jinja2 prompts → ["mustache", "jinja2"] - Prompts on curly → ["mustache", "jinja2", "curly"] - Prompts on fstring → ["mustache", "jinja2", "fstring"] - Default for new prompts = "mustache" - Legacy formats never offered to other prompts - Unknown formats appended defensively, never coerced VENDORING NOTE: The buildTemplateFormatOptions helper is vendored from #4393 (still OPEN). When #4393 lands, the canonical version ships at agenta-entity-ui/src/DrillInView/SchemaControls/ templateFormatOptions.ts — this branch's copy gets deleted then and TemplateFormatPicker re-imports from there. See the file header for the full vendoring note. Differences vs #4393's version (labels, option shape, nullable input) are documented inline. TemplateFormatPicker itself is genuinely additive — #4393 only ships the options helper + wires it into PromptSchemaControl (drawer). The playground needs its own picker component. 15 new tests pin the options contract: new prompts, mustache/jinja2 stored, curly/fstring legacy appending, hint tags, unknown defensive fallback, never-coerce idempotency.
Missed the package.json hunk in the TemplateFormatPicker commit (cace2ec). Adds the "./template-format" subpath so consumers can import {TemplateFormatPicker} from "@agenta/entity-ui/template-format".
…flagged)
Adds an opt-in path through SingleLayout's flat (non-grouped) branch
that renders PlaygroundInputsBodyHost in place of the per-variable
VariableControlAdapter loop. Off by default — existing UX is preserved
on merge; OSS (or a dev toggle) flips useNewPlaygroundInputsBodyAtom
to true to surface the new V2-aligned cards.
New files:
- agenta-playground-ui/src/state/featureFlags.ts — defines
useNewPlaygroundInputsBodyAtom (boolean, default false). Plain
session atom; promote to atomWithStorage if it becomes a user pref.
- agenta-playground-ui/.../PlaygroundInputsBody/PlaygroundInputsBodyHost.tsx
atom-aware wrapper that reads inputsVisibility({testcaseId, downstreamKey})
and writes via setTestcaseCellValue. Drafts route through the same
setter (it creates the column on first set).
Modified:
- agenta-playground-ui/src/state/index.ts — exports the flag atom.
- PlaygroundInputsBody/index.tsx — re-exports the Host.
- SingleLayout.tsx — non-grouped branch checks the flag and renders
Host instead of variableIds.map(renderVariable) when enabled.
Deferred (explicit follow-ups documented in approved design doc):
- ComparisonLayout — second adapter consumer, same swap pattern.
- Grouped evaluator layout — keeps VariableControlAdapter per design
doc ("the adapter stays for evaluator-playground / chain-step").
- TemplateFormatPicker placement into an OSS prompt-config surface —
needs design-team sign-off on placement per design doc Open Q2.
- Default-flip the feature flag — small follow-up commit after the
user verifies the new UX in dev.
…o fe-feat/mustache-support # Conflicts: # api/uv.lock # services/uv.lock
Now that WP-B3 (#4393) is merged in, drop the vendored duplicates and align on the canonical exports: - Delete agenta-entity-ui/src/template-format/templateFormatOptions.ts (vendored copy). Re-point TemplateFormatPicker to import buildTemplateFormatOptions + TemplateFormat from #4393's canonical location: src/DrillInView/SchemaControls/templateFormatOptions.ts. - Adopt #4393's labels ("Prompt Syntax: Mustache" / "Jinja2" / "Curly" / "F-string") via TEMPLATE_FORMAT_LABELS — drawer + playground now share the same vocabulary. - Drop my hint chip system; #4393's option shape is {label, value}. - Drop my vendored template-format-options.test.ts — superseded by #4393's agenta-entity-ui/tests/unit/templateFormatOptions.test.ts. - Migrate the other two stopgap tests (view-types + formatters) from agenta-entities/tests/unit/ to agenta-entity-ui/tests/unit/ now that #4393 ships a vitest runner in entity-ui (vitest.config.ts). Relative imports tightened to ../../src/view-types/. Remaining stopgap in agenta-entities/tests/unit/: - playground-inputs-visibility.test.ts — tests @agenta/playground code, which still has no vitest runner. Keep there. - build-evaluator-execution-inputs.test.ts — tests @agenta/entities code, naturally lives in entities. Stays. Test totals after reconciliation: - @agenta/entity-ui — 58 tests (4 files: view-types, formatters, my + #4393's chatPromptsMustache, #4393's templateFormatOptions) - @agenta/entities — 353 tests
…orts up) ESLint import/order rule requires `@agenta/*` workspace imports to come before relative imports. The pre-existing `@agenta/playground-ui/adapters` import was already in the wrong place; my Step 6 additions (PlaygroundInputsBodyHost, useNewPlaygroundInputsBodyAtom) sat next to it. Move all three to the right block — no behavior change.
Flip useNewPlaygroundInputsBodyAtom default to true. The V2-aligned PlaygroundInputsBody is now the default rendering for SingleLayout's flat (non-grouped) path: bordered card per variable, granular type chips, "View as ▾" dropdown with Chat / Form / Text / Markdown / JSON / YAML, native-JSON edits. VariableControlAdapter is still used for: - the grouped evaluator layout (useGroupedLayout === true) — field ports nested under envelope sections, follow-up swap deferred. - ComparisonLayout — multi-variant side-by-side view, same pattern as SingleLayout but the swap is the next ticket per the design doc. Once ComparisonLayout is also swapped, the flag + conditional in SingleLayout can be removed entirely.
The prompt-editor token validator at templateVariable.ts:130 treated
any `{{/...}}` as a JSON Pointer and required the first segment to be a
known envelope slot. That rejected mustache section close tags like
`{{/languages}}` (paired with `{{#languages}}`) as "Unknown envelope
slot."
Fix: short-circuit single-segment identifier-shaped paths
(`/^/[a-zA-Z_][\w.]*$/`) as valid. They can't be multi-segment JSON
Pointers, and in mustache they're section close tags. Multi-segment
paths (`/inputs/foo`) still get the envelope-slot check. Numeric-led
paths (`/123abc`) fall through and are rejected.
Trade-off: legacy curly users writing `{{/input}}` (singular, typo of
`{{/inputs}}`) lose the typo-detection hint at the editor. The runtime
remains the source of truth — mustache renderer surfaces a clear
error for unmatched close tags, and curly's `/input` lookup returns
no value. Accepted because mustache is the new default and section
close tags are common-path syntax.
Also corrects A5 in the test plan: `{{$.geo.region}}` is by-design
rejected by the validator (JSONPath must root at an envelope slot).
Switched the suggested syntax to `{{$.inputs.geo.region}}` with a
note about the runtime-spread vs static-validator gap.
14 tests in template-variable-validation.test.ts pin: plain names,
dotted access, JSONPath envelope rooting, JSON Pointer envelope
rooting, mustache section close acceptance, numeric-led rejection.
3-row JSON array (Vanuatu / Kiribati / Switzerland) with 9 columns
covering every type the playground UX has to handle:
- string : country, correct_answer (plain {{name}} subst.)
- number : population_thousands (NUMBER chip, native transport)
- boolean : is_island_nation (BOOLEAN chip, Switch widget)
- object : geo (2-level nested — the headline mustache test)
- array : languages (ARRAY chip, section iteration)
- string-as-JSON : metadata (gap-04: stays STRING, not parsed)
- messages : chat-shaped role-tagged array (MESSAGES chip, Chat view)
- unused-column : notes (drives the unreferenced-columns footer)
Uploads via the testset UI in bare-array form (matches the
/simple/testsets/upload endpoint, NOT the {name, csvdata} wrapper).
Referenced by test-plan.md (same folder) §1 (upload) and the
scenarios in §3.
The RFC's canonical mustache JSONPath examples use a testcase top-level
column as the root (e.g. `{{$.profile.name}}` against a `profile` column
that's spread into the render context). The editor's validator at
templateVariable.ts:108 was rejecting these as "Unknown envelope slot",
because it required the root segment to be one of {inputs, outputs,
parameters, testcase, trace, revision}.
That contradicts the RFC. Fix: relax the JSONPath check — accept any
root that ISN'T a near-miss typo of an envelope slot. Typo detection
stays as an actionable hint (e.g. `$.input.country` → suggests
`inputs`), but legit testcase columns (`$.geo.region`, `$.profile.name`,
`$.country`) now pass through.
The bare root `{{$}}` (whole context as compact JSON) is also now
accepted; it was previously rejected for having no segments. RFC docs
this as canonical syntax for serializing the whole context.
JSON Pointer rule unchanged. Per RFC, JSON Pointer is legacy-curly
only; mustache uses `$.` JSONPath.
Reverts the A5 test-plan workaround — `{{$.geo.region}}` is now what
the user should type, matching the RFC examples.
Updated `template-variable-validation.test.ts` from 14 → 19 tests:
adds the testcase-column rooting cases, the bare-`$` case, and the
typo-hint mention of the testcase-column escape.
… testcase-spread roots
Two related fixes to keep input-port discovery aligned with the RFC's
spread semantics:
1. extractTemplateVariables now skips mustache block markers — they're
structural syntax, not variables, and shouldn't produce phantom
input ports:
- `{{#name}}` / `{{^name}}` — section opens / inverted sections
- `{{/name}}` — section closes
- `{{!comment}}` — comments
- `{{> partial}}` — partials (also rejected at render time)
- `{{.}}` — the implicit iterator
Filter applied for all formats (mustache uses these natively; curly
doesn't but the filter is defensive against paste-from-mustache).
2. parseTemplateExpression's JSONPath branch now routes non-envelope
first segments through the testcase-spread path: `{{$.geo.region}}`
parses as {envelope: "inputs", key: "geo", subPath: "region"},
equivalent to {{$.inputs.geo.region}}. Matches RFC: "testcase
top-level keys are spread into the render context, so `{{$.profile.
name}}` resolves against the spread `profile`."
Same shape already in place for plain dot-notation (`profile.name`);
this change brings JSONPath in line.
Test updates:
- port-helpers.test.ts: `$.invalid.x` no longer rejected — replaced
with `$.input.x` (still rejected as envelope-slot typo). Added a
positive test for the testcase-spread case.
- New extract-template-variables.test.ts: 15 tests pinning block-
marker filtering across mustache / curly / fstring.
Full @agenta/entities suite: 385 tests pass.
playgroundInputsAtomFamily was treating the testcaseMolecule entity
as if it were the row data dict. `testcaseMolecule.data(id)` returns
`{id, data: {...row...}, flags, tags, meta}` — the row columns live
at `entity.data`, not at the entity's top level.
Symptom: after loading a testset, every referenced variable rendered
with the `draft` badge ("Not on testcase yet"), and the unreferenced-
columns footer counted the entity's own fields (`flags`, `tags`, ...)
instead of testcase columns. Native data was on the molecule but my
atom couldn't see it because `"country" in entity` was always false.
Fix: switch to `testcaseMolecule.data(testcaseId)` (the canonical
accessor pattern used by `testcaseDataAtomFamily`) and access
`entity.data` for the row columns.
…path
Step 1 only killed `normalizeCompact` in the evaluator path. The
completion + chat playground request body went through TWO MORE
stringification points in executionItems.ts:
1. `resolveVariableValues` returned `Record<string, string>` via
`stringifyValue`, which `JSON.stringify`'d every object/array.
That dict gets merged into `data.inputs` by `transformToRequestBody`
— so the backend received `geo: '{"region":"..."}'` (string) instead
of `geo: {region: "..."}` (object).
2. `buildCompletionInputRow` wrapped each value as `{value: String(v)}`,
turning `{region: "..."}` into `"[object Object]"` for completion
prompts.
Symptom user hit: `{{$.geo.region}}` raised "Unreplaced variables in
mustache template" because the JSONPath resolver can't navigate into a
string. mystache's `{{geo.region}}` silently returned empty (mystache
is permissive on missing keys) so plain dotted access LOOKED like it
worked while actually rendering blank.
Fix:
- `resolveVariableValues` now returns `Record<string, unknown>`,
passes values through native.
- `buildCompletionInputRow` drops the `String(value)` wrap; the
request-body's `extractInputValues` already handles native object
values without coercion.
- `variableValues` type signatures widened to `Record<string, unknown>`
in both ResolveVariableValues callers and BuildRequestBody params.
`transformToRequestBody` already typed `variableValues?: Record<string,
unknown>` so no API change there — this just fixes the producers.
After this, `{{geo.region}}` and `{{$.geo.region}}` both resolve to
the same value at the backend.
Mirror SingleLayout's feature-flagged swap. When `useNewPlaygroundInputsBodyAtom` is on, the comparison view renders a single shared `PlaygroundInputsBodyHost` (V2 bordered cards + type chips + "View as ▾" dropdown) instead of the per-variable `VariableControlAdapter` loop. Row-level controls (open focus drawer + delete row) move out of each variable's header cluster into a small toolbar above the shared inputs body — one cluster instead of N — since the new cards have their own header design and the actions are testcase-row scoped, not per-variable. The existing per-variable loop stays intact behind the flag for any consumer not opted in; the public Props surface is unchanged. Closes the second of the deferred follow-ups from the mustache-support design (ComparisonLayout swap). The feature flag can be removed once the grouped evaluator layout swap is also done.
Migrate the `useGroupedLayout === true` branch in SingleLayout off the per-variable `VariableControlAdapter` loop and onto `PlaygroundInputsBodyHost`. The host now takes an optional `sections` prop that partitions referenced variables into named left-border blocks, mirroring the legacy `<SectionBlock>` accent — `inputs` envelope + the extracted field ports share one block, `outputs` envelope sits in its own block. `VariableCard` gains optional `helpText`, surfaced as a small Info tooltip next to the variable name. The host enriches each visibility input with `helpText` from the input-port schema map, so evaluator envelope variables (`inputs`/`outputs`) keep the guidance tooltip the legacy `VariableHeader` used to render. The grouped + flat layouts now both flow through one component; SingleLayout's legacy SectionBlock + per-variable path is kept behind the feature flag for any consumer not opted in. Closes the grouped-evaluator deferred follow-up from the mustache-support design.
…ader
Add the prompt template_format picker to the PlaygroundConfigSection
prompt section header, alongside the existing Refine + Configure-model
controls. The picker lives where the user already looks for prompt-
level settings — top right of the section — instead of being buried
in the action bar at the bottom of the messages list.
Wires through `updatePromptRootField("template_format", next)`, which
already handles both canonical (root-level `parameters.template_format`)
and legacy (`parameters.prompt.template_format`) shapes. Reads from
`promptModelInfo.promptValue.template_format` / `templateFormat` so
both naming variants are respected.
Note: `PromptSchemaControl` still renders its own inline Select in the
action bar (kept from #4393's BE+SDK work) so any consumer using
PromptSchemaControl directly outside PlaygroundConfigSection keeps a
picker. In the playground, both pickers stay in sync via the existing
value→localTemplateFormat sync effect. Removing the action-bar picker
inside PlaygroundConfigSection is a follow-up consolidation if the
header placement is approved.
Closes the picker-placement deferred follow-up from the
mustache-support design.
Previously the variable extractor skipped every `{{...}}` token whose
inner expression started with `#`, `^`, `/`, `!`, `>`, or was exactly
`.` — meaning `{{#languages}}...{{/languages}}` produced no port at
all, so the playground had no input card for `languages`.
`#name` (section opener) and `^name` (inverted section opener) ARE
variables — `name` still needs a value at render time (the iterable to
loop, or the truthiness to test). Strip the prefix and surface the
base name as a port. `&name` (unescaped variable) gets the same
treatment. Closers / comments / partials / the implicit `.` stay
skipped — they're structural only.
Tests updated to pin the new semantics:
{{#languages}}{{.}}{{/languages}} → ["languages"]
{{^empty}}none{{/empty}} → ["empty"]
{{&html}} → ["html"]
{{/languages}} → []
{{! comment }} → []
{{> partial}} → []
{{.}} → []
When a referenced variable is still a draft (no value on the testcase
yet), the inputs body had no way to know its shape — it fell through
to `inferLogicalType(undefined) → "null"` and defaulted to a Text
input with a `null` chip. So a `geo` port referenced via
`{{geo.region}}` / `{{geo.coordinates.lat}}` opened as a single-line
text input even though the prompt clearly authored it as an object.
Plumb the declared port type (from `inputPortSchemaMap`) through:
PlaygroundInputsBodyHost reads `inputPortSchemaMap[name].type` and
adds `expectedType` to each visibility entry
PlaygroundInputsBody uses new `getViewOptionsForExpectedType` /
`getDefaultViewForExpectedType` helpers that
fall back to expectedType when value is empty
VariableCard uses expectedType to derive the TypeChip
variant (`object` / `array` / `boolean` /
`number` / `string`) when the value is empty
A draft `geo` port now opens as Form with an `object` chip; a draft
`languages` opens with the right shape too. Once the user types a
value, the runtime value takes over (chat-shaped arrays still detect
as Chat, etc.) — `expectedType` is a draft-time hint only.
Tests added in entity-ui covering the empty-value fallback for every
expectedType, the runtime-value-wins case (real string beats
`expectedType: "object"`), and the chat-shaped array override.
When a referenced variable is still a draft (no value on the testcase
yet) AND the port schema describes its expected shape, render the
Form / JSON / YAML view pre-populated with that empty skeleton so the
user sees the fields the prompt is actually asking for.
`geo` referenced via `{{geo.region}}` / `{{geo.subregion}}` /
`{{geo.coordinates.lat}}` / `{{geo.coordinates.lng}}` now opens as a
Form with all four sub-fields visible — instead of an empty form
where the user has to know which keys to add.
How it works:
new helper buildEmptyShapeFromSchema reads `_pathHints` (the
original nested sub-paths
preserved by buildSubPathSchema)
to reconstruct nesting that the
flat `properties` would lose.
Returns null for primitives.
Host injects `expectedSchema` (`inputPortSchemaMap[name].schema`)
onto each variable.
VariableCard computes a `seedShape` once per render — only when the
value is empty AND the schema yields a shape. Passes it
into CardBody.
CardBody uses `seedShape ?? value` for Form / JSON / YAML modes
(where structure is meaningful). Text / Markdown / Chat
modes use the raw value — seeding wouldn't help there.
Render-only: `onChange` only fires on real user edits, so the testcase
value stays untouched until the user actually types into a field. Once
they do, the full shape is persisted (untouched fields persist as `""`,
which mustache renders identically to `undefined`).
Unit tests cover the helper end-to-end:
- primitive schemas → null
- flat properties → flat empty object
- nested properties → recursive empty object
- `_pathHints` preferred over flat properties (the playground case)
- empty `_pathHints` falls through to properties
- non-string `_pathHints` entries are skipped (defensive)
The "View as ▾" dropdown previously rendered with a custom antd
Dropdown trigger ("View as Text ▾"), a "SELECT HOW TO VIEW" section
header, and a "default" hint pill on the selected row. That was
visually inconsistent with the other small dropdowns in the playground
(the prompt config view-mode picker is a borderless `antd Select` with
just option labels).
Replace the custom Dropdown with a plain borderless `antd Select` —
same visual treatment as the surrounding playground UI. The trigger
now just shows the current mode (e.g. "Text", "Form"); the menu is
just the options list with the selected one highlighted. Same
function, less chrome.
Props are backward compatible — `value`, `options`, `onChange`,
`disabled` all unchanged. Added optional `variant` (default
`"borderless"`) and `className` for surfaces that need an outlined
chip or layout overrides.
…orts
A name referenced ONLY as a mustache section opener — like
`{{#languages}}{{.}}{{/languages}}` — was previously bucketed as a
plain `string` port. The iteration intent (`#name` says "iterate over
this if it's an array") is the strongest signal we have about its
shape; the new behaviour surfaces it as an `array` port with the
matching TypeChip and view-mode defaults.
How:
new helper extractMustacheSectionOpeners(input, fmt)
→ Set<string> of names that appeared as `{{#name}}` /
`{{^name}}` in the source template. Empty for non-
mustache formats.
new helper extractSectionOpenersFromConfig(agConfig)
→ mirrors extractVariablesFromConfig — collects section
openers across every prompt-like entry's messages.
groupTemplateVariables now accepts an optional
`{sectionOpeners?: Set<string>}` hint. Type inference priority:
1. Sub-paths present → "object" (strongest signal)
2. In sectionOpeners AND no sub-paths → "array" (iteration intent)
3. Otherwise → "string"
workflow molecule callers (`buildEvaluatorFieldPortsFromTemplate` +
the two non-evaluator branches in `inputPortsAtomFamily`) compute
the opener set per content/config and pass it through. Array ports
emit `{schema: {type: "array"}}` so the empty-shape seed produces
`[]` and the new playground inputs body can offer a sensible JSON
skeleton on drafts.
Defaults adjusted:
array drafts default to JSON view (not Form). FormView has no
add-item affordance for an empty `[]`, so JSON's editable buffer is
the more useful entry point. Form stays in the dropdown — it works
well once items exist.
Tests added: section-opener extraction (mustache only), grouping with
the hint (priority: object > array > string), array default view, the
sub-pathed-AND-section case (object wins).
`getViewOptionsForExpectedType` now special-cases `expectedType: "array"` to put JSON first in the dropdown. FormView has no "add item" affordance for an empty `[]` — the user would see "(empty object)" with no path forward. JSON's editable buffer is the natural entry point: the user types `["en", "fr"]` and they're done. Form is still in the options list (`[json default, form, yaml]`) so arrays with items can use it for per-index editing. Tests pin both cases: empty array defaults to JSON, real array (non-empty) defaults to Form (value-driven path takes over once a value exists).
Previous styling commit (83693af) over-corrected — moving to a plain antd Select dropped the "View as {mode}" prefix on the trigger, but the trigger label was the part that was working. The original beef was the LIST styling (the "SELECT HOW TO VIEW" group header and the "default" hint pills), not the trigger. Revert to the Dropdown + Button trigger reading "View as X ▾", but keep the menu items flat — no group header, no hint pills. Best of both: the trigger still discloses the dropdown's purpose, the menu matches the rest of the playground's lightweight dropdowns.
…down `FormView`'s `formOuter` has `paddingRight: 20px` so the form's leaf cards (the input rectangles) sit inside that padding, away from the variable card's right border. But the labelRow holding each per-field `View as ▾` button also lived inside that padding, so the per-field dropdown sat 20px to the LEFT of the card-level dropdown above — a visible misalignment when scanning down the inputs. Stretch the labelRow past `formOuter.paddingRight` with a negative right margin so the per-field dropdown right-aligns with the card-level one. The field BODY (leafCard, nested rail, nested form fields) stays inside the padding, so the input rectangles keep their breathing room from the card border. Same fix applies to deeper nesting: `nestedRail` adds left padding only, so a nested field's labelRow has the same right edge as a top-level one — both extend 20px to the right via the same margin, both land at the card content's right edge.
Children labels were stealing visual weight from their parent variable name — 14px / weight 600 / near-black / sans-serif inside a nested form, versus the parent card's name at 12px / weight 500 / blue / mono. `region` was screaming louder than `geo`. Unify nested field labels to the same vocabulary the parent uses: mono, 12px, weight 500, brand blue (#1677FF). The indent + 2px rail already communicates nesting; the label itself doesn't need to shout. Also swap the antd `<Tag>` kind chip for the shared `TypeChip` component used in the parent card, mapping the nested 6-way kind to the chip vocabulary (`object` → `json-object`, `array` → `json-array`). Nested fields now show the SAME chip family as the parent — `object` chip with brand-blue tone instead of antd's gold. Result: visual hierarchy reads top-down — parent name + chip set the bar, children echo with the same look at the same weight.
…abels Previous fix (89f73b3) used a negative margin-right on the label row to right-align the per-field `View as ▾` button with the card-level dropdown above — but that left the field BODY (leaf cards, nested rails, input rectangles) still indented inside `formOuter.paddingRight: 20`. So the buttons aligned but the input rectangles below them did NOT. Simpler fix: drop the right padding entirely. Now every form-rendered element — labels, View-as buttons, leaf input cards, nested rails — shares the same right edge as the card content. One consistent vertical line down the right side of every variable card. Removed the labelRow `marginRight: -20` compensation it was paired with — no longer needed once `formOuter.paddingRight` is 0.
Follow-up on `3731ac9d49` per Arda's DM with Kaosiso (2026-06-01): > Kaosiso: "I see here that legacy app should still include Curly. > However when we select Mustache or jinja2 we no longer > see the Curly option. Is that the expected behavior?" > Arda: "untill their config change, was my read on this" > Arda: "does curly disappear before committing?" > Kaosiso: "Yes" > Arda: "gotcha, shouldn't have happened" Refined spec: the curly escape hatch should stay visible WHILE the draft change is uncommitted, and drop once the user has actually persisted the change. My first cut used `useRef` captured at mount, which covered the draft-change case correctly — but it ALSO left curly visible indefinitely after the commit, past the point Arda expects. Fix: track the previous `entityId` and re-capture the original format whenever the id changes. A commit produces a new revision with a new entityId; the ref resets to the just-persisted format, dropping the legacy escape hatch as intended. Draft changes within the same revision (which don't change entityId) leave the ref alone — that's the escape-hatch window. Verified `@agenta/entity-ui` tsc + prettier clean, 13/13 `templateFormatOptions.test.ts` still pass (the helper itself is unchanged; the semantic refinement is in the caller).
…opdown
Kaosiso's screenshot of the prompt-template System message dropdown on
2026-06-01 showed:
[Markdown] <- highlighted/selected
[Text]
Arda's directive: should be [Text, Markdown, ...] always. Audit and
fix every place where the order could flip.
Root cause: `@agenta/ui/drill-in/utils/getViewOptions.ts` had a "long
string" heuristic — when the string was >100 chars OR contained a
newline, the function returned `[Markdown, Text, ...jsonYaml]` instead
of the regular `[Text, Markdown, ...jsonYaml]`. The screenshot's
system message hit the heuristic (multi-line, definitely >100 chars).
Theory of the original heuristic: "long multi-line text is more likely
markdown content, so default to markdown view". Doesn't pan out — the
DEFAULT MODE for the chat-message editor is hardcoded to `"text"` in
`ChatMessageList.tsx:92`, so the option-order flip doesn't change the
initial view, only confuses the user when they open the dropdown
(option positions change based on what they typed).
Fix: drop the conditional. `getViewOptions` for any string returns
`[Text, Markdown, JSON, YAML]` regardless of length. The order is now
content-independent everywhere.
Audit of other text/markdown option lists across the packages:
- @agenta/ui drill-in/utils/getViewOptions.ts: FIXED (this commit)
- @agenta/ui drill-in/core/DrillInRootToolbar.tsx: already correct
- @agenta/entity-ui view-types/viewTypes.ts (getViewOptions and
getViewOptionsForExpectedType): already correct
- @agenta/entity-ui view-types/ViewTypeSelect.tsx (labels map only,
no ordering): N/A
Consumers of the fixed helper:
- `ChatMessageList.tsx` (the screenshot's dropdown)
- `JsonObjectField.tsx` (drill-in JSON object editor)
No tests touched — @agenta/ui has no vitest config; the helper is
small and the fix is straightforward. The behaviour is pinned by the
@agenta/entity-ui view-types tests for the parallel helper.
Inventory #4 marked fixed.
Follow-up on `1e6dfa92ee`. Per Arda: "no more 'long' string logic in
the playground variables / inputs". Tighten the docstring to spell
out that the rule applies to ALL content-based view-mode heuristics,
not just the option-order flip the previous comment described.
Audit of the remaining length-based logic across the inputs/variable
path turned up nothing else to remove:
* `@agenta/entity-ui` `getViewOptions` and `getDefaultViewForValue`
in `view-types/viewTypes.ts`: no length checks. Always Text-first
for strings, default derived from the first option.
* `@agenta/ui drill-in/core/DrillInRootToolbar.tsx`: fixed
`[Text, Markdown, JSON, YAML]` constant. No conditional.
* `disableLongText` / `LongTextNode` in `@agenta/ui/Editor/plugins/
code/`: unrelated — those control truncation of long string
literals INSIDE the JSON / YAML editor, not view-mode selection
for inputs/variables. Kept as-is.
* `ChatMessageList.tsx` `useState<ChatViewMode>("text")`: hardcoded
Text default. No length bias.
No code change in this commit beyond comments; the heuristic itself
was removed in `1e6dfa92ee`.
…`{{|}}`
Mahmoud (Slack #release-v100, 2026-06-01):
> Variables keep closing automatically after the first letter.
Kaosiso (later in the same thread):
> When using Mustache prompt syntax, typing inside a variable
> placeholder causes the cursor to unexpectedly jump outside the
> curly braces after entering the first character.
Arda's repro after my earlier #7 regex fix was deployed:
> typing `{{` creates the token, autocloses in the editor `{{|}}`
> where `|` is the cursor position. then typing a single letter `a`
> results in `{{a}} |` a variable named "a" is created and cursor
> jumps out of the token automatically. this doesn't match the
> behavior we had before this branch.
Root cause: `TokenPlugin.$transformNode` always assumes the user
just CLOSED the token manually — every FULL_TOKEN_REGEX match
inserts a space after the new TokenNode and moves the cursor
to it (the `no-afterToken` branch) OR moves the cursor to the
afterNode (the `afterToken` branch). Both branches treat the
match as "user finished authoring this token, here's where to keep
typing past it".
That worked fine before `AutoCloseTokenBracesPlugin` existed —
typing `{{name}}` character-by-character, the FULL match only
fires when the user types the closing `}}`, so jumping the cursor
out matches their last action. Once auto-close turned `{{`
straight into `{{|}}` with the cursor between the brace pairs,
the very first character typed inside (`{{a}}`) ALSO triggers the
match — but now the user is mid-authoring, not done, so the
cursor-jump-out reads as "the editor stole my cursor".
Fix: capture the cursor offset BEFORE the transform. If it was
strictly inside the matched braces (between `{{` and `}}`, not
at the closing edge), the user is typing inside an auto-closed
token — place the cursor inside the new TokenNode at the same
relative offset. Existing manual-close behaviour (space + jump
out) is preserved for the case where the cursor was at the
closing edge or beyond. Empty `{{}}` token case keeps its
legacy "cursor inside at offset 2" treatment.
Traced through four scenarios:
- Auto-close + type `a` (`{{a}}`, cursor=3 before transform):
cursor stays inside TokenNode at offset 3 (between `a` and `}`).
- Manual `{{a}}` (cursor=5 before transform): manual-close path,
space inserted, cursor past it.
- Empty auto-closed `{{}}` (cursor=2): empty-token branch, cursor
at TokenNode offset 2 (legacy).
- Token mid-text `hello {{a}}world` (cursor=9, between `a` and
`}}`): cursor stays inside TokenNode at offset 3.
Inventory #2 and #8 both mark fixed — they were the same root
cause described from two angles.
After Arda's note that tracing works on main but not on our branch (suspected interaction between PR #4469 and our merge), did a full static-analysis pass on the FE tracing pipeline. Findings: * All tracing-related FE files are BYTE-IDENTICAL to main — `runnableSetup.ts` (references builder), the two tracing API endpoint files, `executionItems.ts` references-passing block, `store.ts` sourceRef setter. * All commits from PR #4491 (`fix/broken-tracing-and-workflow-events`, the fix JP shipped after PR #4469 broke tracing) are reachable from branch HEAD — `b1b5b899df`, `83495a340a`, `601c9de468`, `69379a9f04`, `301f74f65f`, `92be0a87e2`. * Our branch's actual diff vs main does NOT touch the trace pipeline, references building, sourceRef construction, or tracing endpoints. The diff is mustache token plugin, playground inputs body, visibility rule, template format picker, schema-aware seeding, view-type ordering, chat single- testcase gate, native-vs-stringified input transport. Recorded three plausible hypotheses in the inventory: 1. Premise re-check — "main works" may need re-verification with the same repro Mahmoud used. 2. Indirect effect from native-input transport — the only semantically meaningful FE change that touches the request body. Backend trace-save might choke on nested-object inputs even though trace_id was already returned. 3. Environmental — staging state, build cache, env var. Not code. Recommended next steps documented in inventory: re-verify main → capture wire-format diff → hand off to JP if backend-side.
…rk locally
Confirmed via:
* Local HEAD == origin HEAD (`396ecd5eda`).
* `origin/main` (`d7c60c14e6`, frozen since 2026-06-01 10:21Z, i.e.
6 minutes BEFORE Mahmoud's tracing report) is an ancestor of our
HEAD.
* `origin/feat/add-mustache-rendering` (`a12751604a`, Arda's main-
merge at 15:08Z) is an ancestor of our HEAD via merge
`8dd4da1870`.
* Static analysis earlier (commit `396ecd5eda`) confirmed all FE
tracing files (`runnableSetup.ts`, `tracing/api/index.ts`,
`trace/api/api.ts`, executionItems `payloadRefs` block) are
byte-identical to main.
* Arda re-tested locally on the synced branch and traces work.
Net read: Mahmoud's #1 was a transient state at QA time, almost
certainly the staging deploy catching FE post-#4469 / backend
pre-#4491 (or the inverse) before JP's tracing fix had fully
propagated. The code on our branch IS the post-fix state; staging
behaves like local once redeployed.
All nine inventory items now marked fixed/resolved.
…yped scalar
Follow-up on `e26ca33a47` after Arda asked whether the merge handles
deep nesting and string→object transitions. My earlier statement was
WRONG: deep nesting (`{{country.x.y}}`) IS already reconstructed via
`buildEmptyShapeFromSchema`'s `_pathHints` branch and the existing
`buildShapeFromPathHints` helper. The test at
`view-types.test.ts:302-321` proves it.
The actual edge case my merge missed is shape CONFLICT: user typed
`x: "foo"` when the prompt was `{{country.x}}`, then changed the
prompt to `{{country.x.y}}` — the schema now expects `x: {y: ""}`
but the user's value carries `x: "foo"`. My merge kept the scalar
(no data loss) but left the user with no way to see/edit `y`.
This commit adds detection + a per-card banner:
* `VariableCard.seedShape` useMemo now also computes
`shapeConflicts: ShapeConflict[]` — top-level keys where the
schema expects an object/array but the value carries a scalar.
* When `shapeConflicts.length > 0` and the card is editable, an
antd `Alert` (warning) renders above the form: "The prompt now
expects nested fields at `x`. Adopting the new shape will discard
your current scalar value." with a single "Use prompt shape"
button.
* The button overwrites each conflicting key with the schema's
expected nested skeleton, preserving every other key. The
user's scalar is discarded for now (loss documented in
comments) — slot-picker follow-up: when the new schema has a
single sub-key we could drop the scalar there automatically;
when multiple we'd surface a small picker so the user chooses.
Punted because the simpler path ships the core UX without
blocking on a richer migration flow.
* Opposite-direction conflicts (schema says scalar but value is
object) are NOT flagged — the user's data is structurally
richer than the schema knows, and overwriting would lose
information. Left as-is.
`shapeConflicts` is only surfaced when the card is editable —
read-only surfaces don't need the migration affordance.
Verified `@agenta/playground-ui` tsc + prettier clean.
Follow-up to `3731ac9d49`/`909ba25129` (the picker escape hatch).
The original-format ref means a user who switched off curly mid-
session can still revert by re-picking curly from the dropdown.
But once they commit, the next revision starts on the new format
and curly disappears from the picker for good. Surfacing that
boundary explicitly was Arda's ask.
Adds a compact inline `Alert` (info-style) above the prompt
section's action bar. Visible ONLY when:
1. The original (server-persisted) format for this revision is
`curly` or `fstring` (the two legacy formats hidden from the
picker for new prompts).
2. The user's local pick differs from the original — i.e. they
have an uncommitted switch in progress.
3. The card is editable (`!disabled`).
Wording: "Switching from `curly` is permanent — once you commit,
you won't be able to switch back. Discard the draft to revert."
The banner disappears as soon as the user either reverts the
picker to `curly` (no longer differs from original) or commits
(new revision → ref re-syncs → conditions stop matching). So
it's only present in the actionable window where the warning
matters.
Verified `@agenta/entity-ui` tsc + prettier clean.
Follow-up on `fa88012d0b`. Placing the banner ABOVE the action bar caused a layout shift each time it toggled visibility — picking mustache made the banner appear and pushed every button (Message, Tool, Output type, Format) downward; reverting to curly removed the banner and the buttons jumped back up. That's mid-interaction target-loss, exactly the wart Arda flagged on touching down on a moving picker. Move the banner to render BELOW the action bar instead. The buttons stay anchored in place; the banner appears/disappears at the bottom of the prompt section. Same conditions, same wording — only the DOM order changed.
…el "unused" behaviour
Arda flagged on 2026-06-01: after deleting `{{obj.b}}` from the prompt
(keeping `{{obj.a}}` and `{{obj.c}}`), the `obj` variable card still
renders `b` as an active input field. The user's value carries
`{a, b: {y}, c}` from when they typed b.y earlier, and my previous
merge (`e26ca33a47` / `f59b702a13`) iterated VALUE keys with schema
ones spread on top — so anything the user had typed survived, even
when the schema no longer declared it.
This mirrors the top-level behaviour of "removed prompt variable's
testcase column moves to the unused-columns footer" — but does it at
one level deeper, inside the form view of an object variable.
Three changes inside `VariableCard.seedShape`:
1. Flip the iteration: walk the SCHEMA's keys (not the value's).
Schema-declared keys render; schema-missing keys don't.
2. Track `unreferencedNested` — value-only keys (in value, not in
schema). These are the prompt-removed sub-paths.
3. New `handleValueChange` interceptor: when the user edits the
rendered shape via FormView, restore the `unreferencedNested`
keys from the original value before writing back. The testcase
data KEEPS the dropped sub-keys silently, so re-adding
`{{obj.b}}` to the prompt restores them without data loss.
Concrete scenarios after this fix:
- Schema {a, c}, value {a, b: {y}, c} → form shows {a, c}.
Edits write `{a: <new>, c: <new>, b: <kept silently>}`.
- Schema {a, c}, value {a, c} → form shows {a, c}.
No interceptor work, unreferencedNested empty.
- Schema {a, b}, value {a} → form shows {a, b: ""}.
Mahmoud's earlier missing-sub-field case still works.
- Schema {a}, value {a, b: {y}, c} → form shows {a}. Both
`b` and `c` preserved silently.
- Schema {x: {y: ""}}, value {x: "scalar"} → form shows {x: ""}
(string field). `shapeConflicts` banner offers "Use prompt
shape" to convert. Unchanged from `f59b702a13`.
`shapeConflicts` is computed the same way — when schema expects
nested but value carries a scalar at a key the schema declares.
Value-only keys (the new `unreferencedNested`) are NOT counted as
conflicts; they're filtered out cleanly.
Verified `@agenta/playground-ui` tsc + prettier clean.
… overlaps text Arda screenshot 2026-06-01: the shape-conflict banner showed "Adopting the new shape will discard your current scalar value" with the "Use prompt shape" button visually wedged in the middle of the text. Antd `Alert`'s `action` prop renders the action alongside the message in a fixed right-side slot — when the message wraps to multiple lines, the action stays anchored at the top-right and the text reads as if it wraps under or around the button. Hard to parse. Embed the button INSIDE the message slot with a `flex flex-wrap` container. The button either sits inline at the end of the text (when short) or wraps cleanly to its own line (when long), with no overlap. The action prop is dropped — message is the sole content slot now. Also drops the dead "shapeConflicts.length === 1 ? X : X" ternary that emitted the same string in both branches (originally was meant to differ by singular/plural; the plural variant landed only on the trailing "value(s)" word so the ternary on the prefix was a no-op). Verified `@agenta/playground-ui` tsc + prettier clean.
…n chrome
Arda screenshot 2026-06-01 spotted two issues on the banner:
1. The code chip for the conflicting key name (`a` / `b` / etc.)
used `bg-[#fff7e6]` — the SAME pale-yellow as the warning
Alert's background. The chip blended in completely, reading as
plain inline text rather than a code reference.
2. The "Use prompt shape" button used `type="link"` which renders
without button chrome — looked like a third paragraph of warning
text, not an interactive action.
Fixes:
* Code chip: white background with a thin amber border
(`border-[#FFD591]`) and amber text (`text-[#874D00]`) so it
contrasts cleanly against the cream-yellow Alert background
while still reading as amber-themed in the warning context.
* Button: drop `type="link"` → default antd Button (white bg,
grey border, the standard "secondary" look). `size="small"`
keeps it compact; `shrink-0` prevents text wrap inside the
button itself.
Verified `@agenta/playground-ui` tsc + prettier clean.
…appear
Regression spotted by Arda 2026-06-01: prompt had `{{obj.a.y}}`, user
renamed it to `{{obj.a.t}}`, and the inputs card still rendered `obj.a.y`
in form view (with a stale empty input slot for the old key).
The previous schema-strict seed-shape logic only iterated the SCHEMA's
keys at the TOP level. When it found a top-level key in both schema
and value, it copied the value verbatim — never descending. For
`{obj: {a: {y}}}` vs schema `{obj: {a: {t}}}` that meant `merged.obj =
valueRec.obj` → `{a: {y}}` survived intact, and FormView faithfully
rendered the stale `y`.
Fix — recursion at every depth:
* `buildSchemaStrictShape(skel, val, pathPrefix)` — recursive helper.
At each level, walks the schema's keys (not the value's), descends
into nested objects, and reports structural conflicts with dotted
paths (`obj.a` when the user has a scalar where the schema now
expects an object).
* `mergeEditWithStash(edit, original, skel)` — write-back inverse.
Walks `original` alongside `edit` against the same schema skeleton
and preserves any value-only keys at every depth. The user only
sees / edits schema keys, but the underlying testcase value keeps
the stash so re-adding `{{obj.<path>}}` to the prompt later
restores the data without loss.
* `setAtPath(obj, dottedPath, value)` — used by the "Use prompt
shape" affordance to overwrite nested conflicts (`obj.a`, not just
top-level `a`).
* Removed the separate `unreferencedNested` tracking from the memo
— recovery is inline in `mergeEditWithStash` now, so a single
recursive walk handles both the render filter and the stash
preservation. Banner chips now display dotted paths for nested
conflicts.
Behavior matrix (all verified mentally against the new code):
| Scenario | Form render | Underlying value after edit |
|-------------------------------------------|--------------------|------------------------------|
| rename `obj.a.y` → `obj.a.t` | shows only `t` | both `y` (stashed) + `t` |
| remove `{{obj.b}}` from prompt | `obj.b` hidden | `b` value preserved |
| add `{{obj.a.z}}` (new sub-key) | `z` appears empty | edits merge into `obj.a` |
| conflict: scalar at `obj.a`, schema wants | banner shows | adoption overwrites with |
| `{obj.a.t}` | `obj.a` chip | expected nested shape |
Verified `@agenta/playground-ui` tsc + prettier clean.
…disclosure
The previous commit makes the form view drop nested keys the prompt no
longer references (e.g. `obj.a.y` after a rename to `obj.a.t`) while
silently preserving them in the underlying testcase value. The user
asked for symmetry with the top-level `UnreferencedColumnsFooter` so
that the stash is discoverable instead of invisible.
New `StashedPathsFooter` inside `VariableCard`:
* Collapsed-by-default disclosure rendered below the body, inside the
card's border. Summary matches the top-level wording:
"N unused nested keys hidden because the prompt does not reference
them." Click to expand → flat list of `<variableName>.<dotted-path>`
entries with a compact preview of each stashed value (primitives
raw, objects JSON.stringify'd and truncated at 80 chars).
* Re-mounts when the stashed-path set changes (`key={paths.join("|")}`
from parent). Same anti-leak as the top-level footer — adding or
removing a stash entry collapses the disclosure rather than letting
a freshly-stashed value silently appear inside an already-expanded
view.
* Read-only references. The user "restores" a stashed key by
re-adding `{{variableName.<path>}}` to the prompt — the value comes
back automatically through `mergeEditWithStash`.
`buildSchemaStrictShape` was extended to collect a flat list of
`StashedPath { path, value }` entries as it walks. Stash entries from
deeper recursion are pushed BEFORE the current level's, so the list
groups related branches naturally (e.g. `obj.a.y`, `obj.a.z`, then
`obj.b`).
No change to write-back semantics — `mergeEditWithStash` already
preserved stashed keys at every depth. This commit just makes the
stash visible.
Verified `@agenta/playground-ui` tsc + prettier clean.
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.
Summary
Mustache support
Playground inputs
Others
Testing
Verified locally
Added or updated tests
QA follow-up
Checklist
Contributor Resources