[FE feat] Mustache support#4465
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.
Three coordinated changes to match the approved mockup:
1. ViewTypeSelect trigger drops the "View as" prefix
- Reads just the current mode name ("String ↕", "Form ↕")
- Two-way caret icon (CaretUpDown) instead of single down-caret
- Menu list unchanged — already flat + clean from the earlier fix
2. Rename "Text" mode label to "String"
- Underlying ViewType value stays as `"text"` — no API change
- Visible everywhere the mode label surfaces: top-level dropdown
trigger, dropdown menu items, nested FormView leaf dropdowns
- "String" is the broader term — fits strings AND primitives
(numbers/booleans/nulls) rendered as their text form
3. Revert nested field labels to dark bold sans-serif
- Children labels are a DISTINCT concept (a property of the value)
vs. the parent variable name (the input identifier the prompt
references), so they shouldn't share the same style
- Parent stays blue-mono-medium (the variable name)
- Nested goes dark-bold-sans-serif at the same size (12px) — bold
enough to read as "field name" without outweighing the parent
4. Unified action cluster on every VariableCard header
- Copy-value button (always shown)
- Database "Synced from {name}" indicator when the row is connected
to a testset
- Same cluster per card — uniform across the row, no per-card
variation
- Host reads `loadableController.selectors.connectedSource` and
passes the source name through `PlaygroundInputsBody` →
`VariableCard`. Per-card display, gated by the global loadable
state.
Two things were breaking the encapsulation cue: 1. The card border was `#e5e7eb` (too light) and missing an explicit `border-solid` — depending on which reset/theme was active, it could render invisibly. Bumped to `#d4d4d8` + explicit `border-solid` so the card boundary is clearly visible. 2. The inner SharedEditor / InputNumber were carrying their OWN prominent borders (`editorType="border"` for the editor, default antd InputNumber border). That inner border dominated the visual hierarchy — the header (label + chip + controls) looked like it was floating above a separate input box rather than sitting INSIDE the same card. Switched top-level primitive editors to borderless: - TextLeafEditor: editorType="border" → "borderless" - CodeLeafEditor: editorType="border" → "borderless" - InputNumber: variant="borderless" + `!px-0` Now the card's own border is the only visible boundary; the header and the value render as one connected block inside it. Nested FormView leaf cards keep their own borders unchanged — that's the right treatment for sub-elements within a parent form structure.
Last commit (52967ae) made the editors borderless at idle but the SharedEditor's borderless variant has hover/focus borders baked into `SharedEditorImpl` (lines 403, 416) that re-appear when the user moves the mouse over the card or focuses an input. That's the "disgusting" hover border in the screenshot — a nested border showing up inside the encapsulating card. Two changes: 1. Top-level text/markdown mode → replaced SharedEditor with antd `Input.TextArea` (variant="borderless"). The textarea has no built-in hover/focus border, no shadow, no padding. It melts into the variable card so the header + value read as one block — same as moving labels INTO the textarea, which was the design intent. We lose the rich-text affordances (tokens, markdown rendering) that SharedEditor provides for prompts, but variable VALUES are plain text — the rich features were never useful here. 2. JSON/YAML mode → kept SharedEditor (needed for codeOnly + line numbers) but flipped `state` to `"filled"` and added explicit `!border-transparent hover:!border-transparent focus:!border- transparent focus-within:!border-transparent` overrides on the wrapper. That beats the `isEditorFocused && "!border-[#BDC7D1]"` rule in SharedEditorImpl (which is `!important` and can't be overridden without matching specificity). The card border is now the ONLY visible boundary at every state — idle, hover, focused, typing. Header and value live inside the same enclosed block, as the design called for.
`js-yaml.dump([])` and `dump({})` only have a flow-style YAML
representation, which renders identically to JSON literals. The user
explicitly picked YAML mode for `languages` (an empty array draft)
and saw `[]` — same as JSON. Looks broken / wrong.
For empty containers, return an empty buffer in YAML mode instead of
the flow-style literal. The editor's placeholder ("Enter YAML") then
guides the user to type fresh YAML. As soon as the value has any
content, `js-yaml.dump` produces proper block-style output (lists
with `- `, mappings with `key: value`) — which is what users expect
when they pick YAML.
Strings that parse to empty containers get the same treatment.
Test coverage:
- valueToDisplay([], "yaml") === ""
- valueToDisplay({}, "yaml") === ""
- valueToDisplay("[]", "yaml") === ""
- valueToDisplay(["en", "fr"], "yaml") still produces "- en\n- fr\n"
- valueToDisplay({foo: "bar"}, "yaml") still produces "foo: bar\n"
The array case had JSON pushed to the FIRST slot (because empty
arrays default to JSON for UX reasons), which broke the consistent
list ordering — every other kind had JSON/YAML at the bottom and the
kind-specific modes at the top.
Decouple list order from default mode:
- `getViewOptionsForExpectedType` now follows ONE rule for every
kind: kind-specific modes first (String / Markdown / Form),
then JSON, then YAML at the bottom.
- `getDefaultViewForExpectedType` no longer reads `options[0]`. It
encodes the default per expectedType directly (object → form,
array → json, primitives → text), so arrays can still default to
JSON without yanking JSON out of its list slot.
Result, for any draft:
string → [String, Markdown, JSON, YAML]
object → [Form, JSON, YAML]
array → [Form, JSON, YAML] ← consistent with object now
boolean → [String, JSON, YAML]
Array default mode unchanged (still JSON for empty arrays, Form once
the value has items).
The token typeahead opened on `{{` and worked for flat (`{{co...`)
and JSONPath (`{{$.in...`) modes, but didn't fire when the user typed
`{{#` (mustache section opener) — the `#` query didn't match any
field name and the dropdown stayed closed. So authoring an iteration
block (`{{#languages}}...{{/languages}}`) required typing the name
from memory.
Extend `parsePathContext` to recognize `#` and `^` prefixes as two
new modes:
`{{#` → mode: "section"
`{{#la` → mode: "section", current: "la"
`{{#user.` → mode: "section", prefix: ["user"], current: ""
`{{^empty` → mode: "inverted-section", current: "empty"
Section modes share the same suggestion sources as flat mode (port
schemas + testcase columns + observed sub-keys) — the section name
is a field on the input context, so the candidate set is identical.
The differing piece is the inserted token text: `{{#${name}}}` for
section, `{{^${name}}}` for inverted, handled in `push()` via the
mode switch.
Mustache `#name` works for arrays (iteration), objects (context
switch), AND truthy primitives, so we don't filter the candidates
by type — any field is a valid section target. Auto-pairing with a
`{{/name}}` close tag is a separate UX improvement (current tokens
are single nodes; pairing needs a second insert).
Exported `parsePathContext` so the new modes can be unit-tested in a
future commit (no vitest in @agenta/ui yet).
…display) QA flagged this twice — Mahmoud's May 21st video on #4394 ("if I move this to JSON, why is it now saying it's an object") and the 2026-05-27 chat editor screenshot ("This bug from QA has not been resolved. We still mistake stringified JSONS to Objects!"). Root cause: `valueToDisplay` had a `typeof value === "string"` branch in JSON and YAML modes that tried `JSON.parse(value)` first and, on success, dumped the parsed object. So a STRING column whose content happened to be JSON-shaped (the `metadata` cell `'{"source":...}'`) got rendered as a multi-line OBJECT in JSON mode and a YAML mapping in YAML mode. At display time the user couldn't tell strings from objects; type was silently changing. Drop the string special-case in both modes. Always call `JSON.stringify(value, null, 2)` (json) or `yamlDump(value, ...)` (yaml) directly. View modes are now pure REPRESENTATION transforms — they never change the value's type. Renders, after the fix: string "Vanuatu" json → "Vanuatu" (JSON literal, quoted) yaml → Vanuatu (YAML plain scalar) string '{"a":1}' json → "{\"a\":1}" (escaped, STILL a string) yaml → '{"a":1}' (YAML scalar, quoted) object {a: 1} json → {\n "a": 1\n} (multi-line) yaml → a: 1 (block style) number 42 json → 42 (literal) boolean true json → true (literal) Also fixes the secondary bug Mahmoud's video called out — that JSON mode for a plain string `Vanuatu` rendered raw unquoted text, which the JSON code editor flagged as a syntax error. Now it's a valid JSON string literal. Tests updated to pin the new behavior: - JSON: strings are JSON-literals, JSON-shaped strings escape (don't parse) - YAML: strings are YAML scalars, JSON-shaped strings stay strings - `"[]"` as a string survives the empty-container suppression (only actual empty arrays / objects get suppressed) The fix lands in the shared formatter — playground AND testset drawer (both consume `valueToDisplay` via FormView) get the gap-04 invariant back at display time.
…ield)
The `messages` variable rendered in chat mode showed message content but
clicks didn't focus / typing didn't register — read-only despite
`disabled={!editable}` evaluating to false.
Root cause: my CardBody passed only `messages`, `onChange`, `disabled`
to `ChatMessageList`. The proven-working consumer (`MessagesField` in
the drill-in drawer) passes `enableTokens` + `templateFormat` +
`allowFileUpload={false}`. Without `enableTokens` / `templateFormat`,
the underlying Lexical editor mounts its plugin stack in a stripped-
down state where the content surface effectively reads as static
text.
Mirror MessagesField's prop set:
enableTokens (so `{{var}}` tokens are recognized inside chat
message content — also makes the editor mount
the editable plugin path)
templateFormat (paired with enableTokens — curly for now;
could thread the prompt's actual template_format
later)
allowFileUpload (false — variable values don't take inline
attachments; attachments live in their own
testcase column shape)
No tests touched — ChatMessageList interactions need a DOM environment
and aren't in scope of the formatter / view-types suites.
Sorry for the wrong-direction debug pass on chat editability — the
disabled state came from a different prop entirely.
`UnreferencedColumnsFooter` renders the "N unused testcase columns
hidden..." block. Its `editable` prop defaults to `false` (defensive
— some display surfaces want unreferenced columns read-only), and the
Host wasn't passing it. So when the user expanded the footer, every
card inside rendered with `editable={false}` → `disabled={true}` on
every editor inside, including ChatMessageList.
That matches the user's screenshot: `messages` was sitting under the
"5 unused testcase columns hidden" footer (the prompt only references
`country` / `geo` / `metadata`), so it was an UNREFERENCED column —
and unreferenced columns were read-only.
Fix: pass `unreferencedEditable={editable}` from the Host. Once the
user expands the footer, the cards behave the same as referenced
ones — same editor, same interactions. The footer's existence is just
a visibility-rule signal ("the prompt doesn't reference these"), not
a permission boundary.
Two FE behaviors were firing for ALL `{{...}}` formats — curly, jinja2,
AND mustache — even though section / inverted-section / comment / partial
syntax is mustache-only. So if a user authored a curly prompt with
`{{#items}}` (mustache syntax pasted in error), the FE silently
"helped" them by extracting `items` as a port and offering section-
opener typeahead suggestions, while the BE would still error at render
time. Mismatch.
Fixes:
1. `extractTemplateVariables` — split per format:
- mustache: strip `#`/`^`/`&` prefixes; skip `/`/`!`/`>`/`.`
- curly / jinja2: skip any token whose first char is one of those
mustache markers (`#^&/!>.`). They're not valid identifiers in
these formats, so no phantom port — the user sees the broken
token in the editor and gets clear feedback to fix it.
2. `TokenMenuPlugin` + `parsePathContext` — section / inverted-section
modes are now mustache-only. The plugin reads templateFormat from
TokenBehaviorConfig (already threaded through the lexical extension)
and passes it to parsePathContext. In non-mustache the typeahead
stays in flat mode for `{{#...`, the `#` becomes part of the literal
query, no port name matches, no suggestions surface.
Tests updated:
- curly: `{{#items}}` / `{{^empty}}` / `{{!c}}` / etc. → []
- curly: dotted access `{{user.name}}` still extracts
- curly: mixed `{{name}} {{#items}}` → ["name"] (legit survives)
- mustache tests unchanged
`extractMustacheSectionOpeners` was already format-gated correctly (no
change needed).
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
…o fe-feat/mustache-support
Railway Preview Environment
|
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
web/packages/agenta-ui/src/Editor/plugins/token/TokenTypeaheadPlugin.tsx (1)
264-285:⚠️ Potential issue | 🟠 Major | ⚡ Quick winNormalize seen section tokens before feeding them back into
push().This loop now serves flat + section modes, but it still mines raw token bodies like
#languagesand^empty. In section mode that gets prefixed a second time, so selecting a previously-seen section opener inserts{{##languages}}/{{^^empty}}. It also lets section openers leak into plain flat suggestions. Strip the leading control character before extracting segments, and skip#/^tokens whenmode === "flat".Suggested fix
- const flatPrefix = prefix.length > 0 ? `${prefix.join(".")}.` : "" - for (const token of dynamicallyReadingTokens) { - if (!token || token.startsWith("$.") || token === "$") continue - if (flatPrefix && !token.startsWith(flatPrefix)) continue - const rest = flatPrefix ? token.slice(flatPrefix.length) : token + const flatPrefix = prefix.length > 0 ? `${prefix.join(".")}.` : "" + for (const token of dynamicallyReadingTokens) { + if (!token || token.startsWith("$.") || token === "$") continue + const normalizedToken = + token.startsWith("#") || token.startsWith("^") ? token.slice(1) : token + if (mode === "flat" && normalizedToken !== token) continue + if (flatPrefix && !normalizedToken.startsWith(flatPrefix)) continue + const rest = flatPrefix ? normalizedToken.slice(flatPrefix.length) : normalizedToken const nextSeg = rest.split(/[.[\]'"]/).filter(Boolean)[0] if (!nextSeg) continue push(nextSeg, {hint: "seen"}) }web/packages/agenta-entities/src/runnable/portHelpers.ts (1)
304-338:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winScope
sectionOpenersto the grouped variable id.
groupsare keyed by${envelope}.${key}, but the array inference only checkssectionOpeners?.has(key). If the template contains bothinputs.languagesandoutputs.languages, a section opener for one will coerce the other group to"array"too. Please key the opener signal with the same${envelope}.${key}identity used bygroups.web/packages/agenta-entities/src/runnable/utils.ts (1)
1-10:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winFix Prettier formatting failure.
The CI pipeline reports a Prettier formatting check failure. Run
pnpm lint-fixin the web folder to auto-fix formatting issues. As per coding guidelines: "Runpnpm lint-fixin the web folder after making changes to the frontend".
🧹 Nitpick comments (2)
web/packages/agenta-entity-ui/src/view-types/ViewTypeSelect.tsx (1)
61-63: ⚡ Quick winReplace inline style objects with Tailwind utility classes.
This component currently uses CSS-in-JS (
style={...}andstylesobject), which diverges from the frontend styling standard in this repo.Proposed refactor
- <Button type="text" size="small" disabled={disabled} style={styles.trigger}> - <span style={styles.triggerValue}>{VIEW_LABELS[value]}</span> - <CaretUpDown size={12} style={styles.triggerCaret} /> + <Button + type="text" + size="small" + disabled={disabled} + className="inline-flex h-6 items-center gap-1 rounded px-2 text-xs" + > + <span className="font-semibold text-[`#051729`]">{VIEW_LABELS[value]}</span> + <CaretUpDown size={12} className="mt-px opacity-65" /> </Button> </Dropdown> ) } - -const styles = { - trigger: { - display: "inline-flex", - alignItems: "center", - gap: 4, - padding: "0 8px", - height: 24, - borderRadius: 4, - fontSize: 12, - }, - triggerValue: {color: "`#051729`", fontWeight: 600}, - triggerCaret: {marginTop: 1, opacity: 0.65}, -}As per coding guidelines, "Always prefer Tailwind utility classes over CSS-in-JS for styling".
Also applies to: 69-81
web/packages/agenta-playground-ui/src/components/PlaygroundInputsBody/VariableCard.tsx (1)
169-181: ⚡ Quick winUse
async/awaitfor the clipboard write.This is a simple one-shot async path, so the
.then()chain just diverges from the repo convention and makes the handler harder to read than a localtry/catch.Suggested fix
- const handleCopy = useCallback(() => { + const handleCopy = useCallback(async () => { const text = value === undefined || value === null ? "" : typeof value === "string" ? value : typeof value === "number" || typeof value === "boolean" ? String(value) : JSON.stringify(value, null, 2) - navigator.clipboard.writeText(text).then( - () => message.success({content: "Copied", duration: 1.5}), - () => message.error({content: "Copy failed", duration: 2}), - ) + try { + await navigator.clipboard.writeText(text) + message.success({content: "Copied", duration: 1.5}) + } catch { + message.error({content: "Copy failed", duration: 2}) + } }, [value])As per coding guidelines, "Always use async/await for promises instead of
.then()chains".
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 8160798c-e634-4949-a170-bdbe6ad38b47
📒 Files selected for processing (41)
docs/design/prompt-runtime-unification/wp-f-playground-mustache/test-plan.mddocs/design/prompt-runtime-unification/wp-f-playground-mustache/testset.jsonweb/packages/agenta-entities/src/runnable/index.tsweb/packages/agenta-entities/src/runnable/portHelpers.tsweb/packages/agenta-entities/src/runnable/utils.tsweb/packages/agenta-entities/src/workflow/state/molecule.tsweb/packages/agenta-entities/tests/unit/build-evaluator-execution-inputs.test.tsweb/packages/agenta-entities/tests/unit/extract-template-variables.test.tsweb/packages/agenta-entities/tests/unit/playground-inputs-visibility.test.tsweb/packages/agenta-entities/tests/unit/port-helpers.test.tsweb/packages/agenta-entities/tests/unit/template-variable-validation.test.tsweb/packages/agenta-entity-ui/package.jsonweb/packages/agenta-entity-ui/src/DrillInView/components/PlaygroundConfigSection.tsxweb/packages/agenta-entity-ui/src/template-format/TemplateFormatPicker.tsxweb/packages/agenta-entity-ui/src/template-format/index.tsweb/packages/agenta-entity-ui/src/view-types/FormView.tsxweb/packages/agenta-entity-ui/src/view-types/ViewTypeSelect.tsxweb/packages/agenta-entity-ui/src/view-types/formatters.tsweb/packages/agenta-entity-ui/src/view-types/index.tsweb/packages/agenta-entity-ui/src/view-types/viewTypes.tsweb/packages/agenta-entity-ui/tests/unit/playground-inputs-formatters.test.tsweb/packages/agenta-entity-ui/tests/unit/view-types.test.tsweb/packages/agenta-playground-ui/package.jsonweb/packages/agenta-playground-ui/src/components/ExecutionItems/assets/ExecutionRow/ComparisonLayout.tsxweb/packages/agenta-playground-ui/src/components/ExecutionItems/assets/ExecutionRow/SingleLayout.tsxweb/packages/agenta-playground-ui/src/components/PlaygroundInputsBody/PlaygroundInputsBody.tsxweb/packages/agenta-playground-ui/src/components/PlaygroundInputsBody/PlaygroundInputsBodyHost.tsxweb/packages/agenta-playground-ui/src/components/PlaygroundInputsBody/UnreferencedColumnsFooter.tsxweb/packages/agenta-playground-ui/src/components/PlaygroundInputsBody/VariableCard.tsxweb/packages/agenta-playground-ui/src/components/PlaygroundInputsBody/index.tsxweb/packages/agenta-playground-ui/src/components/PlaygroundInputsBody/viewModeAtoms.tsweb/packages/agenta-playground-ui/src/state/featureFlags.tsweb/packages/agenta-playground-ui/src/state/index.tsweb/packages/agenta-playground/src/state/controllers/executionItemController.tsweb/packages/agenta-playground/src/state/execution/executionItems.tsweb/packages/agenta-playground/src/state/execution/index.tsweb/packages/agenta-playground/src/state/execution/selectors.tsweb/packages/agenta-playground/src/state/execution/visibility.tsweb/packages/agenta-shared/src/utils/templateVariable.tsweb/packages/agenta-ui/src/Editor/plugins/token/TokenTypeaheadPlugin.tsxweb/packages/agenta-ui/src/Editor/plugins/token/extensions/tokenBehavior.tsx
Address six CodeRabbit findings on PR #4465. Each in one focused diff: 1. **Prettier formatting** — `pnpm run format-fix` over the web/ tree. `extract-template-variables.test.ts` had a long line outside Prettier's wrap width and an unrelated EE env file was also stale. Both fixed. 2. **Buffer resync on external value changes** (VariableCard.tsx) `TextLeafEditor` and `CodeLeafEditor` seeded their local `buffer` from props once and never resynced. Testcase refetches / row updates left the visible text stale while `value` had already moved on. Added `useEffect(() => setBuffer(initial), [initial])` to both. 3. **Reject `$.` JSONPath** (agenta-shared/utils/templateVariable.ts) `hasEmptySegment` only catches duplicated separators (`..`, `//`), not a lone trailing one — so `$.` (root + trailing dot, no field) slipped through as valid. Only the bare `$` form is allowed; `$.` now returns `{valid: false, reason: "JSONPath root has no field after `$`."}` so the editor shows the invalid-token treatment. 4. **Envelope-scoped sectionOpeners lookup** (portHelpers.ts) `groupTemplateVariables` checked `sectionOpeners.has(key)` but the groups themselves are keyed by `${envelope}.${key}`. A section opener `{{#languages}}` would coerce BOTH `inputs.languages` AND `outputs.languages` (if both appeared in one prompt) to `array`. Resolve each opener through `parseTemplateExpression` first and key the lookup set by `${envelope}.${key}` — same identity the groups use. 5. **Cross-package import via path alias** (template-variable-validation.test.ts) Switched `"../../../agenta-shared/src/utils/templateVariable"` to `"@agenta/shared/utils"` (which re-exports `validateTemplateVariable` from the templateVariable module). Matches repo guideline: no relative paths for cross-package imports. 6. **Typeahead seen-token prefix handling** (TokenTypeaheadPlugin.tsx) Mustache section openers live in the editor as tokens with bodies like `#languages` / `^empty`. The previously-seen fallback loop was reading them verbatim: - In SECTION / inverted-section mode, `push()` re-prefixes, producing `{{##languages}}` / `{{^^empty}}`. - In FLAT mode, the prefixed token would leak into the flat suggestion list. Strip the leading `#` / `^` before prefix-matching + segment extraction (so section-mode selections produce clean tokens), and skip prefixed tokens entirely when `mode === "flat"`. Tests added: - validation: `$.` rejected with the "no field" reason - extract-template-variables.test.ts kept its existing coverage; the new behavior is exercised by existing curly / mustache tests after the Prettier-driven formatting tweak Full test suite: 455 / 455 entities, 79 / 79 entity-ui. tsc + lint clean across @agenta/entities, @agenta/entity-ui, @agenta/playground-ui, @agenta/shared, @agenta/ui. CodeRabbit comment 3 (`templateFormat` threading to ChatMessageList) is its own commit — it touches more files and needs the Host to read the active prompt format from playground state.
…nputs CodeRabbit finding (#3 of 6) — `ChatMessageList` was hardcoded to `templateFormat="curly"` inside `VariableCard`. So when the prompt was authored in mustache and a variable's value was chat-shaped, the inner per-message editors would tokenize `{{...}}` as curly — mustache sections (`{{#name}}`) and close tags (`{{/name}}`) inside a chat message would render in the wrong color, autocomplete would offer the wrong mode, etc. Thread the active format through the chain: SingleLayout / ComparisonLayout └─ read `parameters.prompt.template_format` (legacy nested) or `parameters.template_format` (canonical root) from `workflowMolecule.selectors.data(entityId)`. Fall back to `"curly"` when no value is stored. └─ pass `templateFormat={promptTemplateFormat}` to PlaygroundInputsBodyHost └─ forwards to PlaygroundInputsBody └─ forwards to VariableCard └─ forwards to CardBody └─ uses it on ChatMessageList in chat mode The Host doesn't resolve `templateFormat` itself because comparison layouts may render multiple variants with different declared formats — picking one is the caller's call. SingleLayout uses the row's single `entityId`; ComparisonLayout uses the structural root node (first depth-0 node) as the canonical primary. All hops default the prop to `"curly"` so consumers that don't pass it preserve today's behavior.
Restores the beautified Outputs / Inputs rendering for spans whose ag.data.outputs or ag.data.inputs arrives as a JSON string (langchain AgentExecutor returnValues, ChatOpenAI LLMResult generations, n8n traces). Background: the May 22 refactor (97cb574, merged via #4394) made tryParseJsonValue strict so the testcase / playground editors stop auto-parsing strings into objects. extractPreview from #4410 was written against the old behavior and silently broke. With the new strict helper, every rule's isPlainObject check failed on string inputs and the cell fell through to raw stringified text. Adds a private tryParseJsonString helper inside extractPreview.ts that unstringifies container-shaped JSON before rule matching. Kept private so it cannot leak back into surfaces that intentionally moved to "strings stay strings" (cell renderers outside the dispatcher, testcase drawer, playground inputs in #4465).
Summary
Mustache support
Playground inputs
Others
Testing
Verified locally
Added or updated tests
QA follow-up
Checklist
Contributor Resources