Commit Graph

59 Commits

Author SHA1 Message Date
Lucio Lelii aa3f54bb1a Show a node's list parameters closed, with how many items they hold
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>
2026-09-25 19:32:03 +02:00
Lucio Lelii f4a526ebbd Edit a list of plain values one value at a time, not as raw JSON
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>
2026-09-25 19:19:05 +02:00
Lucio Lelii 44be453cbb Summarise a plain-value array row by its value
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>
2026-09-21 12:15:58 +02:00
Lucio Lelii 82396e8b8f Drive the view-content button from the schema, not from a skills rule
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>
2026-09-20 20:58:14 +02:00
Lucio Lelii d09ec47be6 Let a skill binding's content be viewed from the node editor
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>
2026-09-20 11:29:49 +02:00
Lucio Lelii fc541c12ef Offer "Use default" only where a field declares one
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>
2026-09-18 11:52:55 +02:00
Lucio Lelii b13c2019eb Let a retriever-backed field fall back to free text in two more places
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>
2026-09-17 12:30:43 +02:00
Lucio Lelii 5787a6fe44 Add the SPDX licence header to every source file
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>
2026-09-16 12:10:18 +02:00
Lucio Lelii a426a2c5e7 Summarise an array row by the branch it is on
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>
2026-09-14 14:55:29 +02:00
Lucio Lelii c298c352b3 Let a configurable field take a global input, and show its own tip
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>
2026-09-08 10:53:01 +02:00
Lucio Lelii a6a811549a Enforce schema bounds in the editors, and say what an empty field does
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>
2026-09-07 11:53:40 +02:00
Lucio Lelii 0c0fac3643 Move the object dialog round trip into the shared schema module
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>
2026-09-04 14:04:49 +02:00
Lucio Lelii c6060540f9 Render an optional group as one button that opens a modal
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>
2026-09-04 12:39:55 +02:00
Lucio Lelii b278f7f9c7 Let the schema-driven form carry a whole object as one field
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>
2026-09-04 12:29:54 +02:00
Lucio Lelii b53a22dcc4 Name a nested fieldset readably instead of by its dotted path
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>
2026-09-04 12:20:50 +02:00
Lucio Lelii 7ac7d08c98 Let the model parameters be set from the editor and the simulation dialog
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>
2026-09-04 11:29:32 +02:00
Lucio Lelii d05a661473 feat: update flow editor enhancements 2026-09-01 15:53:39 +02:00
Lucio Lelii 760202090d feat: edit container subflows from flow editor 2026-07-27 17:07:46 +02:00
Lucio Lelii 318531111d feat(flow-editor): support node capabilities 2026-07-23 10:37:28 +02:00
Lucio Lelii c031eb8535 fix(flow-editor): render dynamic ports and park swimlanes 2026-07-22 17:39:03 +02:00
Lucio Lelii 0f155e2040 feat(flow-editor): add swimlanes and JSON IO type support
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>
2026-07-22 14:54:43 +02:00
Lucio Lelii 46976b1f92 fix(nodes): fix the same backdrop containing-block bug in the shared .llm-modal popup
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>
2026-07-22 09:20:08 +02:00
Lucio Lelii af246dab65 fix: correct real bugs and remove dead code found in a full-project quality review
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>
2026-07-21 14:27:34 +02:00
Lucio Lelii 94ce814a6f feat(bias-impact): complete side-effect policy, compare, report list and canvas highlighting
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>
2026-07-21 11:20:57 +02:00
Lucio Lelii 7bfef72f79 feat: add bias impact experiments and biased reruns 2026-07-21 10:34:22 +02:00
Lucio Lelii 0d60da1874 feat(flow-editor): support bias annotations 2026-07-20 16:22:55 +02:00
Lucio Lelii 0c64850b7d fix: preserve expanded node mode on boolean toggles 2026-05-18 12:49:31 +02:00
Lucio Lelii 4bcb9f5d3e feat(editor): add focus mode for container and generic nodes 2026-05-13 12:18:13 +02:00
Lucio Lelii 9b0e2bbd89 Update nodes: schema-driven fields, node utility, generic/container/task-step nodes 2026-04-24 12:33:21 +02:00
Lucio Lelii 5220cee49b Consolidate schema-driven node logic and align tests with Vitest 2026-04-21 11:57:01 +02:00
Lucio Lelii a377c1d793 Refactor schema-driven node fields and fix validation highlights 2026-04-17 17:42:32 +02:00
Lucio Lelii e6f6cda845 Unify password policy and fix conditional dynamic fields 2026-04-15 12:11:29 +02:00
Lucio Lelii f57f5be073 race condiiton on node schema loading solved 2026-04-10 09:26:31 +02:00
Lucio Lelii c96ca07e0b fix: restore connection rendering + schema-driven node improvements
- Remove OnPush from CustomConnectionComponent (incompatible with
  rete-angular-plugin Custom Element mounting, broke all connection
  rendering since 7028ac0)
- Extract shared node utilities to node-utility.ts (DRY)
- Add schema-driven icon resolution (x-ui-icon) with fallbacks
- Add schema-driven output port styling (x-ui-output-styles)
- Add schemaLoading guard to TaskStepNodeComponent
2026-04-10 09:06:06 +02:00
Lucio Lelii 7028ac0c53 feat: improve auth flow and task execution UX 2026-04-09 14:01:56 +02:00
Lucio Lelii bb1cdd52ac Improve execution handoff and node loading feedback 2026-03-30 17:06:18 +02:00
Lucio Lelii 27f9bc1285 Add dependency flow validation and execution input UX updates 2026-03-27 15:18:03 +01:00
Lucio Lelii 167e301a21 Refine task previews and add node clone actions 2026-03-25 16:53:05 +01:00
Lucio Lelii 3bf669180d look and feel in the longtext parmeter changed 2026-03-24 14:05:21 +01:00
Lucio Lelii 55418d2226 Refine execution viewer and schema-driven node handling 2026-03-24 13:45:32 +01:00
Lucio Lelii 5672ce4336 Make dynamic array item schemas refresh from dialog state 2026-03-22 17:01:31 +01:00
Lucio Lelii 19b0dadacc Improve schema-driven nodes and interactive execution chat 2026-03-19 15:18:09 +01:00
Lucio Lelii 8a8978937f Refine container handling and readonly flow behavior 2026-03-16 15:41:08 +01:00
Lucio Lelii 25e133ac04 Add first-class container nodes and subflow preview 2026-03-14 15:50:55 +01:00
Lucio Lelii 6f3ca99b1e Add schema-driven block IO compatibility and bindable inputs 2026-03-13 16:51:53 +01:00
Lucio Lelii b24b8177a9 Support dynamic array schemas in node editors 2026-03-12 17:22:23 +01:00
Lucio Lelii f9a2504273 Add execution authorization input flow and inline node deletion UI 2026-03-11 16:57:14 +01:00
Lucio Lelii 78d8527a85 Update assistant session UI and fix structural node reconnection 2026-03-11 12:44:31 +01:00
Lucio Lelii d243395c15 Fix dirty state on node parameter edits 2026-03-10 19:24:58 +01:00
Lucio Lelii 514339599b Extend execution graph and schema-driven node rendering 2026-03-10 16:59:35 +01:00