From 672f53d9abfd95840257eab6b2af2129f930c663 Mon Sep 17 00:00:00 2001 From: Lucio Lelii Date: Mon, 28 Sep 2026 15:39:20 +0200 Subject: [PATCH] Offer Use default on a node field only where there is one The editor a node opens for a single field offered Use default on every optional field that was not a checkbox: a storage's connection, its operation, its scope, and any other choice between alternatives, where there is nothing to go back to. The settings dialog already followed the schema's x-ui-defaults-when-empty; this editor now does too, so the button is on the model's sampling knobs and nowhere else. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../nodes/generic-node/generic-node.spec.ts | 29 +++++++++++++++++++ .../shared/nodes/generic-node/generic-node.ts | 10 +++++-- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/app/shared/nodes/generic-node/generic-node.spec.ts b/src/app/shared/nodes/generic-node/generic-node.spec.ts index b8408b9..724ebcb 100644 --- a/src/app/shared/nodes/generic-node/generic-node.spec.ts +++ b/src/app/shared/nodes/generic-node/generic-node.spec.ts @@ -857,6 +857,35 @@ describe('GenericNodeComponent', () => { expect(component.localEditorGlobalName).toBe(''); }); + it('offers Use default only on a field whose schema says it falls back to one', async () => { + // Every optional field used to get it - a choice between a catalog storage and your own + // connection, an operation, a scope - where there is nothing to go back to. + const component = fixture.componentInstance as any; + const choice = { + ...bindableFieldDefinition(), + path: 'source', + label: 'Connection', + enumOptions: ['CATALOG', 'PERSONAL'], + ui: { visibleWhen: [], enabledWhen: [] } + }; + const knob = { + ...bindableFieldDefinition(), + path: 'llmDescriptor.parameters.temperature', + label: 'Temperature', + type: 'number' as const, + ui: { visibleWhen: [], enabledWhen: [], defaultsWhenEmpty: true } + }; + component.editableFieldDefinitions = [choice, knob]; + component.ensureBlockConfiguration()['llmDescriptor'] = { provider: 'InternalOllama' }; + + await component.openParameterEditor('source'); + expect(component.showLocalEditorDefault()).toBe(false); + + component.closeLocalEditor?.(); + await component.openParameterEditor('llmDescriptor.parameters.temperature'); + expect(component.showLocalEditorDefault()).toBe(true); + }); + it('renders the field description, which the modal never used to show', async () => { // The other half of why none of this was discoverable: the modal held no tip at all, so a // @UiDescription explaining that the field takes a placeholder was invisible. diff --git a/src/app/shared/nodes/generic-node/generic-node.ts b/src/app/shared/nodes/generic-node/generic-node.ts index 9afc4c4..7e507e3 100644 --- a/src/app/shared/nodes/generic-node/generic-node.ts +++ b/src/app/shared/nodes/generic-node/generic-node.ts @@ -213,7 +213,7 @@ export class GenericNodeComponent implements OnDestroy { localEditorPattern: string | null = null; /** The field's own description. The modal never rendered one, so every tip was invisible. */ localEditorTip: string | null = null; - /** Empty means unset on this field, and the modal says so. Every optional field, not just numbers. */ + /** Empty hands the field to a default, and the modal says so - only where the schema says it does. */ localEditorDefaultsWhenEmpty = false; localEditorDefaultValue: string | null = null; localEditorMin: number | null = null; @@ -449,8 +449,12 @@ export class GenericNodeComponent implements OnDestroy { this.localEditorMinLength = definition.ui.minLength ?? null; this.localEditorPattern = definition.ui.pattern ?? null; this.localEditorTip = definition.ui.tip ?? null; - // A boolean has no empty state: false is a value, not an absence. - this.localEditorDefaultsWhenEmpty = definition.type !== 'boolean' && !this.isPathRequired(definition.path); + // Only where the server says emptying the field hands it to a default - the model's sampling + // knobs. An optional field is not one: most have no default to go back to, and a choice + // between alternatives (a source, a scope, an operation) is picked, not reset. A boolean has + // no empty state at all: false is a value, not an absence. + this.localEditorDefaultsWhenEmpty = definition.ui.defaultsWhenEmpty === true + && definition.type !== 'boolean' && !this.isPathRequired(definition.path); this.localEditorDefaultValue = this.schemaDeclaredDefault(definition.path); this.localEditorMin = definition.ui.minimum ?? null; this.localEditorMax = definition.ui.maximum ?? null;