Every list parameter of a node now reads as its name and a count, and
opens on demand to its items; with none there is nothing to open, the
add button is the way in, and the "No items" line is gone. An item
added opens the list, so it is there to see.
The list is one shared component, app-node-list-field, rather than two
near-identical blocks in the block node and a third in the execution
view: the same list reads and behaves the same everywhere, and a
container that gains a list parameter takes the component as it is.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A list whose elements are plain values - the tools an LLM node must use
successfully, the steps an evaluator follows - had no fields to build
its element dialog from, and fell back to a box asking for "Item JSON"
with {} in it, a shape nobody could guess. The element is now edited in
a single field of its own type, labelled and explained as the list is;
an empty value is not added and a value already in the list is refused.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A task script is a list of strings, and every row read back as "Item 1", "Item 2":
the summary only knew how to read object rows, so the very text being configured was
hidden behind its position. A string, number or boolean row now reads as itself, and
falls back to the position only when it is blank.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The button was wired to the literal retriever name "Skills" and to a
hand-built /retriever/Skills/definitions URL, so the node knew about one
particular binding. It now reads x-retriever-definition-url off the item
property: any binding that declares where its value can be read gets the
view, and the node knows nothing about which one it is showing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Skills were selectable only by bare id, with no way to see the
instructions a SKILL.md actually carries. Adds a "view content" button
next to each skill row, backed by the already-existing
/retriever/Skills/definitions/{id} endpoint and the previewOnly dialog
mode, so no backend change or new dialog infrastructure was needed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The control was derived from the field being optional, which put it on nearly
every field in every dialog. It now comes from the schema's
x-ui-defaults-when-empty, which the server sets per field: "you may leave this
blank" and "leaving this blank means something specific" are different claims,
and only the second is worth a control.
Two rules still sit above the declaration. A field the current state requires
offers nothing - clearing it would go towards a default that does not exist - and
a checkbox is excluded, because false is a value, not an absence.
Sampling parameters a provider does not apply are no longer offered either. All
five were shown to every provider and the unsupported ones were dropped at run
time, reported in a warning on an execution that had already happened. The filter
is shared by the two editing surfaces so they cannot drift, and it keeps every
knob whenever the answer is not knowable - no provider chosen, one the server does
not list, a capability call that failed - because a knob wrongly hidden cannot be
set at all, which is worse than one that does nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The backend already publishes /retriever/LLM/models/open, which says
whether a provider's model field is a fixed list or free text - Gemini
has used it in production for a while, through generic-node.ts's own
leaf-field editor. Two other renderers never asked that question, and a
provider that cannot list (any hosted one with no listing endpoint) got
stuck there with an empty, unusable select instead:
- container-node.ts's own field editor decided select-versus-text from
retrieverKey alone, synchronously, before the free-text question could
even be asked. toDialogFieldType now takes the answer as an argument,
resolved first through a new fetchRetrieverFreeText.
- Nested objects - an LLMDescriptor inside a container's configuration
or an array item, reached through the buildSchemaObjectDialog both
generic-node.ts and container-node.ts share - had no way to ask at
all. SchemaObjectDialogHooks grows an optional loadFreeText hook; when
it says yes, loadOptions is skipped entirely (the same fetch a
listable retriever would otherwise make and throw away) and the field
is forced to text. One fix in the shared function covers both
components that call it.
Deliberately not "an empty options list means free text" - that heuristic
would be wrong for a retriever whose empty list is a real answer, a
project's global inputs among them, where nothing to choose from is not
the same question as nothing to type. A property with no loadFreeText
hook, or one that answers no, keeps rendering as a select, empty or not,
exactly as before.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Mechanical: three comment lines at the top of every .ts, .html and .css
under src, and nothing else. Split out from the licence commit so the
files that carry an actual change stay readable in the history, and kept
to its own commit because it moves the blame line on 390 files.
The short SPDX form rather than the full GNU notice - it is
machine-readable under REUSE, it satisfies the requirement to keep the
licence notice intact, and it points at LICENSE-ADDENDUM instead of
restating the attribution term in every file.
The template and stylesheet headers do not reach the bundle: Angular
discards template comments and the production build strips CSS ones. The
one in index.html survives, since that file is served as written, which is
no loss.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An upload row taking its file from a global listed as "GLOBAL · false".
The summary takes the first two values a row holds, and the false was a
"several files" box nobody had touched - while the global the row names,
the one thing that tells it apart from the next row, never got a look in.
Two kinds of value are skipped now, both for the same reason: they
identify nothing. A false flag is an absence dressed as a fact, and a
field the row's own choice hides is irrelevant by construction.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The small parameter editor offered two sources for a configurable-as-input
field: a static value, or a workflow input. A global input was reachable only
by typing ${{global.<name>}} by hand into the value box - and on a provider
whose model list is closed there is no value box to type into, which left the
one flow-level way of deciding such a field unreachable from the editor.
There is now a third source that lists the flow's declared globals and writes
the placeholder for you, with a plain sentence when the flow declares none
rather than an empty select.
The modal also never rendered a field's description, so every tip written on
one of these fields was invisible.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every bound was already in the schema and already enforced by the server,
but nothing passed it to the control: a temperature of 5 was typeable and
only failed on save. The settings dialog and the inline node editor now
share one validator, so a bound declared once reads the same wherever a
value can be typed. Numeric properties finally get a numeric control.
Arrow increment and required granularity are kept apart: step says what
the value must be a multiple of - 1 on an integer, nothing on a decimal -
while stepIncrement only moves the spinner. Arrows on a 0-to-1 field used
to jump by 1, reaching only the two ends of the range; they now move by a
tenth without making 0.35 wrong. FieldValueConstraints omits stepIncrement
so the increment cannot reach the validator to try.
An empty optional field now states that it is using the default, with a
reset beside the control that stays in place and greys out rather than
appearing once a value is typed. Going back to unset is the one thing a
filled box cannot express: clearing it by hand looks identical to never
having decided. Generic - it follows from the schema not requiring the
field, on all three editing surfaces, container included.
Also fixes the dialog reading as broken: descriptions were rendered twice,
once as a mat-hint and once below in error red, and the wrapping hint
overflowed the fixed-height subscript area onto the button beside it.
An optional group now sits in the fieldset of the object that owns it, so
a node holding two LLM descriptors cannot show two identical "Model
parameters" controls with nothing to tell them apart.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An annotation honoured by one node type is worse than no annotation, so the
container needs the same round trip the block editor has. It moves as it is -
the characterisation tests written before the extraction still pass unchanged -
with only the two genuinely component-specific parts left as callbacks: how
select options are resolved, and how a dynamic sub-schema is expanded.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Five empty chips for parameters nobody sets on most nodes took more room than
the prompt. The group now shows as a single control saying how many of its
settings are set, and opens the whole object in one dialog.
The write-back reuses the object round trip rather than the array one, so an
optional numeric cleared in the modal removes the key instead of persisting 0 -
otherwise the provider default would be unreachable, which is the bug fixed
yesterday for the inline editor. Clearing everything drops the object entirely,
so a saved flow never carries an empty husk that implies a choice was made.
A temperature of 0 counts towards the badge: it is the repeatable setting, not
an absence, and a collapsed control must never hide a value.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two changes to the core of the form, both needed before anything can render an
optional group as a single control.
collectSchemaLeafFields stops unfolding an object marked x-ui-optional-group and
emits the object itself instead. Until now no code path produced a leaf of type
object at all: `hasChildren && !isArray` always recursed, which is why five
settings most nodes never touch took more room on the card than the prompt.
Emitting it is opt-in, for the same reason arrays are: most callers want editable
scalars and would choke on an object. An unmarked nested object still unfolds
exactly as before, and a test pins that - the change has to be per object, not a
new rule for nested objects in general.
SchemaDisplayItem gains a fourth slot beside field / richContent / array, with
its lookup map in buildOrderedSchemaDisplay. Nothing fills it yet.
588 frontend tests green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A field inside a nested object whose object declares no name of its own took the
raw parent path as its fieldset legend, which the uppercasing CSS then rendered
as "LLMDESCRIPTOR.PARAMETERS".
The expression `?? parentPath(path)` appeared in four places - twice in
generic-node, twice in container-node - plus a fifth in task-step-node and a
sixth as the default in schema-driven-fields. That is how one mistake came to be
wrong in six places at once, so this is one exported helper rather than six
corrected copies.
Independent of the optional-group work it was found during, and worth having on
its own.
579 frontend tests green; the assertion fails when the helper returns the raw
path again.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Numeric fields inside a nested object were already collected, grouped and saved
by the schema-driven form, so most of this is what that form could not yet do.
A cleared numeric field now removes its key instead of saving 0. Number('') is 0,
so an emptied box used to persist a real zero - and on a temperature that is the
worst confusion available, because 0 is a valid and useful setting, which meant
that once a value had been given there was no way back to the provider default.
The container node carried its own copy of the same parsing and the same defect;
both now agree, and its maxIterations can no longer be cleared into a 0 its own
constraint forbids.
minimum and maximum are read from the schema and bound on the input, and the
placeholder says the range and that empty is allowed - otherwise the only way to
learn either is to save and be refused.
The simulation dialog gains the same five fields, which needed NodeSettingField
to grow a number type and NodeSettingsValues to admit numbers. That widening
rippled into three signatures that assumed string | boolean; the Angular compiler
found them, tsc --noEmit did not.
readSimulatorParameters is pure and tested rather than buried in the viewer: it
is where "the user left this empty" has to survive contact with Number('').
571 frontend tests green, and the cleared-field assertion fails when the parsing
is put back. Initial bundle 4.26 kB over budget, up from 2.88 - reported, not
raised.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Implements the frontend side of the control-flow engine's Fase 4-5
backend extension: presentation-only swimlanes for grouping nodes by
actor/responsibility, and a new IOType.JSON for structured input/
output descriptors. Both are additive/optional fields, backward
compatible with existing flows.
Swimlanes:
- Model: FlowLane (id, name, description, order, color), FlowData.lanes,
and an optional laneId on every block/container.
- Lane management (add/rename/reorder/color/delete) in a new "Lanes"
section of the title toolbar, mirroring the existing Global Inputs
editor. Deleting a lane clears laneId on any node that referenced it,
avoiding an immediate NODE_LANE_NOT_FOUND validation error.
- Real visual swimlanes on the rete.js canvas: horizontal color-coded
bands with labels that pan/zoom together with the nodes, backed by a
transform layer kept in sync with the area's live transform.
- Drag-to-reassign: moving a node updates its laneId based on the drop
Y position, but only for genuine pointer drags — programmatic moves
(initial load, clone, server-side node regeneration) are excluded via
the existing programmatic-translation tracking, so loading a flow
never silently reassigns lanes.
- A small lane badge on the node header confirms the current
assignment after a drag.
- The readonly/execution-view diff and patch logic now accounts for
laneId and lanes so live updates are detected correctly.
Also fixes a data-loss bug found while wiring this up: exportGraph()
rebuilt each node and the top-level FlowData as explicit object
literals that never carried laneId/lanes through, and flowFromApi()
never normalized lanes coming back from the backend — both would have
silently dropped the field on save/reload, the same class of bug fixed
earlier for biasAnnotations.
JSON IO type:
- Global input type picker now offers JSON alongside TEXT/FILE.
- Behavioral probe editor: JSON-typed mock outputs default to {} and
use the JSON textarea editor instead of a plain text input.
- portSelectableKinds (generic-node) includes JSON among the concrete
types offered wherever a port's kind is ANY.
Verified with the full suite (253/253, 21 new tests) and a live
end-to-end browser check: created two lanes, dragged a node from the
unassigned area into a lane (confirmed via the node's lane badge),
saved, and reopened the saved flow with the lane assignment intact.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
While verifying the dialog style convergence, found that generic-node's
"Edit <field>" popup and container-node's "Edit Name" popup have the
same bug just fixed in bias-annotations: they render inside the node
card, which rete.js positions with a CSS transform for pan/zoom, so
their fixed-position .llm-modal-backdrop was trapped inside the node's
own box instead of covering the page — confirmed live, the backdrop's
bounding rect matched the node's box (280x533) rather than the
viewport.
This popup is the primary way to edit any short parameter on any node
in the editor, so the bug was hit far more often than the bias one.
Same fix: promote the backdrop to a native <dialog> shown via
showModal(), move the dimming to ::backdrop, and guard showModal()
since jsdom doesn't implement it. Verified live: the backdrop's rect
now spans the full viewport for both generic-node and container-node,
and closing via a backdrop click still works.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Bugs:
- blocks.ts/containers.ts: reset toInit on a failed initial catalog load so
the next call retries instead of leaving the catalog permanently empty.
- task-step-node.ts: align isEmptyDisplayValue with generic-node's
isMissingValue so a field renders consistently between the editor and the
execution view for the same schema.
- flows-call.fake.ts/assistant-call.fake.ts: wrap synchronous throws in
defer() so catchError() on the caller side actually intercepts them.
- utilities/rete-editor.ts: remove a no-op ternary and a leftover debug log.
- stores/flow-editor.ts: save() now returns a handled error instead of
crashing on a non-null assertion when no flow is loaded.
- Removed leftover console.log statements (title-toolbar, flow-item,
editor-sidebar).
Dead code removed: the unused admin-users page, editor-sidebar's unused
createNewBlock(), rete-editor's unused flowChanged output, and the broken,
uncalled ListStateViewHolder.create().
Refactors (duplication called out by the same review):
- New session-guard.ts factory backing authGuard/adminGuard.
- tasks-executor's formatDuration now reuses the shared util.
- New services/shared/http-error.util.ts replacing the duplicated
extractHttpErrorMessage/toHttpError in admin-call.ts and authorization-call.ts.
- New services/bias/bias-error.util.ts unifying the three different ad hoc
error-message extractions across the bias-* dialogs.
- New pages/admin/admin-access.util.ts and utilities/temporary-signal.ts
replacing the duplicated redirectOnAdminAccessDenied and auto-dismiss-toast
patterns.
- New shared ModalShellComponent adopted by the three bias-* dialogs (their
backdrop/header/footer CSS was already byte-identical); new
password-form-validators.ts and a shared password-dialog-chrome.css
collapsing admin-reset-password-dialog and change-password-dialog, which
duplicated their entire validation logic and CSS.
- New services/shared/{catalog-store,empty-node-cache,pending-sync-counter,
deep-clone,flow-node-mapping}.ts: BlocksService/ContainersService and their
*-call.ts mappers were near line-for-line duplicates (which is exactly how
the toInit bug ended up in both).
- New shared/nodes/node-focus-modal-controller.ts: generic-node.ts and
container-node.ts had ~150 identical lines of focus-modal/body-scroll-lock
plumbing.
Confirm-dialog, node-settings-dialog, human-interaction-dialog and
subflow-preview-dialog were deliberately left out of the modal-shell
extraction: each has a meaningfully different structure and no way to verify
visually here, so forcing them into a shared shell was judged higher risk
than the cosmetic-only bias-*/password dialogs, whose CSS was already
byte-identical.
61 test files / 230 tests passing; `ng build` and `ng test` green throughout.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Finishes the bias impact experiments plan (docs/bias-impact-experiments-plan.md
steps 7-12):
- Side-effect policy selector: reuse the .llm-warning visual language for the
external side-effects banner, add a REQUIRE_CONFIRMATION note.
- Full-flow compare ("Compare with baseline"): new bias-compare-dialog
(service + host) triggering compareBiasExecutions and opening the shared
report viewer; inline errors read from errors[].message/detail.
- Persisted reports: new bias-impact-report-list (list + detail in one view)
wired into a new "Bias impact reports" tab in task-execution-viewer;
404/403 on report detail show the same inline message on purpose.
- Canvas: annotation badge on generic-node (count, executable-probe
indicator, severity from the backend catalog); new
BiasComparisonViewStateService driving bias-active / downstream-changed /
routing-change highlighting on task-step-node and custom-connection, fed by
a highlightOnCanvas event from bias-impact-report-viewer wired in all three
places that render it; legend + "back to normal view" action in the canvas
toolbar.
- Fixed a bug where the bias variant context badge only rendered for
simulated executions.
- Fixed "Measure bias impact" to stay visible-but-disabled with an
explanatory tooltip while the baseline hasn't reached a final state,
instead of being hidden outright, per the §12 checklist.
- Added a Retry action to the compare dialog's error state for parity with
the report list.
- Added the end-to-end facade flow test (annotation -> capability -> isolated
experiment -> report) plus coverage for all new components/services.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>