Let the placeholder flag alone decide, without requiring a textarea

buildTemplatedRichContentParts asked for x-ui-widget: textarea as well as
the flag, so a one-line field could not have its ${{...}} marked even when
its value really is interpolated. Whether a value is interpolated is a
property of the value, not of the control used to type it.

A no-op for everything that exists today: every field carrying the flag
gets it from LongText, whose widget defaults to textarea. It is what lets
LLMDescriptor.model declare the same thing without becoming a textarea.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lucio Lelii 2026-09-07 14:31:20 +02:00
parent 3ba14244c3
commit a8cad0d3cd
2 changed files with 58 additions and 1 deletions

View File

@ -5,6 +5,7 @@ import {
import { parentGroupLabel } from './node-utility';
import {
buildOrderedSchemaDisplay,
buildTemplatedRichContentParts,
buildSchemaEditableFieldDefinitions,
buildSchemaFieldViewModel,
buildSchemaRetrieverContext,
@ -668,3 +669,55 @@ describe('optional groups', () => {
});
});
describe('buildTemplatedRichContentParts', () => {
/** Splitting a value on its ${{...}} placeholders so the editor can mark them. */
const splitParts = (content: string) =>
content.split(/(\$\{\{[^}]*\}\})/).filter((part) => part.length > 0)
.map((part) => ({ text: part, isDynamicInput: part.startsWith('${{') }));
function schemaWith(field: Record<string, unknown>) {
return { type: 'object', properties: { subject: field } };
}
it('splits a one-line field that says it takes placeholders', () => {
// It used to require x-ui-widget: textarea as well, so a field drawn as anything else could
// not declare that its value is interpolated - which is what LLMDescriptor.model needs.
const schema = schemaWith({ type: 'string', 'x-ui-accept-variable-as-placeholder': true });
const parts = buildTemplatedRichContentParts(
{ subject: 'model is ${{global.modelName}}' }, 'subject', schema, splitParts);
expect(parts).toEqual([
{ text: 'model is ', isDynamicInput: false },
{ text: '${{global.modelName}}', isDynamicInput: true }
]);
});
it('still splits a textarea, which is every field that declared it until now', () => {
const schema = schemaWith({
type: 'string',
'x-ui-widget': 'textarea',
'x-ui-accept-variable-as-placeholder': true
});
const parts = buildTemplatedRichContentParts(
{ subject: 'Summarise ${{text}}' }, 'subject', schema, splitParts);
expect(parts.some((part) => part.isDynamicInput)).toBe(true);
});
it('leaves a field that never claimed to take placeholders as one flat part', () => {
const schema = schemaWith({ type: 'string' });
const parts = buildTemplatedRichContentParts(
{ subject: 'literally ${{not a placeholder}}' }, 'subject', schema, splitParts);
expect(parts).toEqual([{ text: 'literally ${{not a placeholder}}', isDynamicInput: false }]);
});
it('has nothing to split when the value is empty', () => {
const schema = schemaWith({ type: 'string', 'x-ui-accept-variable-as-placeholder': true });
expect(buildTemplatedRichContentParts({ subject: ' ' }, 'subject', schema, splitParts)).toEqual([]);
});
});

View File

@ -707,7 +707,11 @@ export function buildTemplatedRichContentParts(
if (!content) return [];
const ui = getSchemaPathUiMeta(root, path);
if (ui.widget === 'textarea' && ui.acceptVariableAsPlaceholder) {
// The flag alone decides: whether a value is interpolated is a property of the value, not of the
// control used to type it. Requiring a textarea too meant a one-line field could not say it
// takes ${{...}} - and every field that says so today is a textarea anyway, so nothing that
// already worked changes.
if (ui.acceptVariableAsPlaceholder) {
return splitParts(content);
}