Commit Graph

233 Commits

Author SHA1 Message Date
Lucio Lelii 974589aeee Render the evaluation form, and submit a judgement as one act
The interaction dialog knew three shapes: a chat, a free-text answer and
a decision. A HumanEvaluation node needs a fourth - the target to open,
the steps to follow, and one control per criterion - so it gets its own
component beside the others rather than another branch inside one.

The verdict goes to the server whole. Sent a criterion at a time, a
failure partway through would leave the step holding a judgement nobody
made.

While a node hides its reference verdict, that input is dropped from the
inputs the form shows: hiding it in the component alone would leave it
sitting in the panel above. Asking to see it is a deliberate, recorded
act, and it does not come back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 23:24:43 +02:00
Lucio Lelii b1497ab3c4 Make a project a container of flows, with no run of its own
Running every flow in a project put two different things in one list: the
sort control decided what you saw, while the up/down arrows set the order
the run would actually use. Pressing an arrow therefore rewrote the run
order from whatever the list happened to be sorted by - usually the
alphabetical one - and the row often did not move, so the arrows read as
broken.

Rather than reconcile the two, a project is now just a grouping: the play
button, the "Run project" menu entry and the reorder arrows are gone, with
the client calls behind them. The server keeps its endpoints, so bringing
the feature back is a frontend change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 21: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 9802f12203 Show the view-skill button on ungrouped array rows too
The node draws array rows from two near-identical template blocks - one
inside a group fieldset, one for ungrouped fields - and the button was
added to only the first. An LLM node renders its Skills list through the
second, so the button never appeared where it was actually wanted.

The test that missed this only exercised the component class, so it adds
a rendering test covering both blocks, and clears the stored user between
service tests: left behind, it made a sign-in a no-op transition and the
cache-reset tests failed intermittently.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 16:08:54 +02:00
Lucio Lelii 87f6e2564b Drop cached projects/flows on login or logout, not just app reload
ProjectsService and FlowsService are root singletons that load their
list once and cache it for the app's lifetime. Logout never cleared
that cache, so a second person signing in on the same tab without a
hard reload still saw whoever's projects and flows were loaded before
them - no backend request was even made.

Authorization now emits userChanged whenever the signed-in identity
actually changes (never on the initial hydration from storage), and
both services reset their cache on it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 11:29:58 +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 0a2f7606d9 Merge feature/declared-field-defaults into master 2026-09-18 21:33:01 +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 bb5621f559 Let the simulator and bias judge dialogs supply a credential
The backend now accepts a credentialId for both the interaction simulator
and the bias judge, but nothing in the UI could provide one - a
credential-requiring provider chosen either way would always fail, short of
coincidentally reusing a credential from elsewhere in the same execution.

openLLMDescriptorSettings (the one dialog shared by both the simulator and
the bias judge picker) stays as it is, tests included. A new
openLLMDescriptorSettingsWithCredential wraps it: once a provider and model
are chosen, if LlmProviderService.listCapabilities() says that provider
requiresCredential, a second dialog lists the user's saved credentials for
it (ExecutionVaultCredentialsService.listForProvider). Two separate modals
rather than one field folded into the first, since NodeSettingsDialogService
only ever holds one dialog open at a time - stacking a "create credential"
flow inside this one was not attempted, matching the pre-existing, already
documented gap that there is nowhere in the app to manage credentials
outside of creating them.

simulateExecution() in task-execution-viewer.ts also now surfaces a
rejected simulation's error message instead of failing silently.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 14:51:16 +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 e1708f95b8 Read the real provider catalog instead of hardcoding InternalOllama
Two things in the copilot both used to hinge on the same wrong check,
isInternalProvider(provider) - a plain string comparison - instead of
what the backend's /llm/providers catalog already publishes:

  - providerNeedsCredential decided whether to show the credential picker
    by name, so a credential-free provider under any name other than
    "InternalOllama" - a remote Ollama with no key, say - was told it
    needed a credential anyway. It now reads requiresCredential from the
    catalog.
  - The "Add credential" dialog had no endpoint field at all. It now
    shows one, built the same way llm-descriptor-settings.ts already
    rebuilds its model field when the provider changes: onValuesChange
    recomputes the field list from the current draft, so choosing a
    provider that requiresEndpoint adds the field, and choosing one back
    away removes it, without closing the dialog.

Both read the same LlmProviderService the execution viewer already uses,
loaded once and cached in llmProviderCapabilities. It has to be loaded
eagerly now, not only when the dialog opens: providerNeedsCredential is a
computed() signal that gates whether the configuration is valid to save,
so it cannot wait for a promise nothing has awaited yet. Bootstrap now
kicks that load off in the background; the dialog's own lazy load stays
too, in case bootstrap has not resolved by the time it opens.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 12:30:28 +02:00
Lucio Lelii a0fdfcda35 Show an endpoint field on the execution's "Add credential" dialog
The backend catalog (/llm/providers) now publishes a requiresEndpoint
flag per provider. LlmProviderCapability grows the matching field, and
task-execution-viewer.ts's credential-requirement dialog shows an
"Endpoint URL" field only when the provider being satisfied needs one -
every existing provider still gets the same three fields it always had.

VaultSecret, VaultSecretCreateRequest and VaultSecretUpdateRequest grow
an optional endpoint too, so it round-trips through createSecret and back
out of the credential list unchanged for a provider that has none.

The gate in execution-viewer.utils.ts (buildAuthorizationGate) reads the
same flag onto each VaultAuthorizationEntry, generalising the single
resolveRequiresCredential lookup it already had into
resolveProviderCapability, shared between requiresCredential and the new
requiresEndpoint.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 12:30:13 +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 85e6c12372 Complete the user guide in English and render it to PDF
The guide was Italian, 531 lines, and stopped at the features that existed
when it was written: it described the editor and executions and said
nothing about bias annotations, probes, experiments or reports - the part
of the platform that most needs explaining, because none of it is
discoverable by clicking around.

Bias is now six chapters, including the thing the interface does not say
anywhere: the isolated experiment and the full-flow rerun are chosen by
which control you click, not by a toggle. Also added: projects, credentials
and the vault gate, simulated runs, reruns and comparison, the execution
tree, file uploads and the admin area.

Features that exist in the model but that nothing produces - an automated
bias analyser, a vault management page, a project-run history - are
deliberately absent. A guide that promises a screen the user cannot find is
worse than one that omits it.

The PDF is generated rather than committed by hand: no pandoc here, so
render-user-guide.py emits print-styled HTML that Playwright, already a
dependency, turns into the PDF. Its header carries the two commands.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 12:10:09 +02:00
Lucio Lelii 4e1926588b Show the licence, attribution and source offer in the interface
Two obligations the licence creates had nowhere to land. The attribution
term requires the notice to stay visible in the Appropriate Legal Notices
a work displays, and section 13 owes the Corresponding Source to anyone
interacting with the program over a network - neither is discharged by a
file sitting in a repository the user of a deployment never sees.

So the About dialog is not decoration: it is the only place either
obligation is met, which is why the component says so in a comment. A
deployment that drops it stops complying with the licence it ships under,
and the attribution term would be asking of others something this
interface does not do itself.

The login page sits outside this layout, so an unauthenticated visitor
still sees none of it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 12:09:58 +02:00
Lucio Lelii 33150978aa Release under the AGPL with an attribution term
The repository carried no licence at all, which meant nobody could legally
build on it and the question of what a derivative owes back had no answer.

The AGPL rather than the GPL because this is a web application: under the
GPL, running a modified copy as a hosted service obliges the operator to
publish nothing, and a workflow platform is exactly the kind of thing
someone hosts. Section 13 closes that.

The additional term is the part that carries the intent - section 7(b)
permits requiring that author attributions be preserved in the Appropriate
Legal Notices a work displays, so the attribution has to survive into a
derivative's interface and not merely into a file nobody opens. It is a
permitted additional requirement, not a further restriction, so downstream
cannot strip it.

Academic citation is a request in CITATION.cff, not a licence condition:
no OSI licence can compel a citation, and pretending otherwise would put an
unenforceable clause in a document that needs to be enforceable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 12:09:49 +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 f1305c16e7 Say that a file is stored where it is chosen, not on Save
Replacing a file uploads it straight away, while every other input waits
for the Save at the foot of the panel. Nothing said so, so a replacement
left the footer reading "All changes saved" with Save greyed out - which
is exactly what an edit that failed to register looks like.

The row now says it, and a test covers the path it describes: Replace
reopens the picker and the chosen file uploads on its own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 14:51:41 +02:00
Lucio Lelii b4302bf7db Mark the node inside a container that carries the error
A container's errors are reported against the container, with the node
that actually carries one in relatedNodeIds - so opening the subflow to
look for it found a canvas with nothing marked, and an error naming a
container the view no longer showed. Entering a subflow also cleared the
marks outright, and the set was only ever computed at the root, where the
errors grouped under a container are not among the flow-level ones.

The marks now follow whatever the current view reports: the container at
the root, the block that carries the error once inside. Focusing one
error from the panel still narrows them, and dropping that focus goes
back to all of them rather than to none.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 14:28:54 +02:00
Lucio Lelii 3af6f14bb7 Send a global file input to the endpoint that accepts files
The upload went to /globals/{key}, which takes JSON, and came back as an
unsupported content type; the array variant additionally named its parts
after the input, which nothing binds on. Both now use the multipart routes
that exist for this, with the part names those routes read.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 12:31:43 +02:00
Lucio Lelii 84f528203c Give a file input a picker that says what it takes and what went wrong
The native file control said "Choose file / No file chosen": nothing about
what the input accepts, nothing about the file once chosen, and on failure
a bare "Failed to upload file" that threw away the server's explanation -
the wrong type, a size, a name it could not build a file from.

Replace it with a drop zone that states the accepted types and size up
front, then gives way to the file itself while it uploads and once it
lands, with Replace to change it. A failed upload now names the file that
failed next to what the server actually said, and the zone invites another
try - which also needed the native control cleared, since picking the same
file twice fires no change event and picking the same file again is
exactly what a retry is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 12:27:52 +02:00
Lucio Lelii f41962993c Show step preview placeholders as expandable values, not a flat string replace
A node's Condition/prompt preview resolved ${{name}} placeholders with a
single-pass string replace, so a runtime value got duplicated wherever the
same placeholder repeated in the source text (e.g. a Conditional's
${{x}} != null && ${{x}}.contains(...) pattern) and long/verbose values were
dumped inline unbounded. Reuse the existing template-placeholder machinery
(already used for HumanDecisionBlock/HumanInteractionBlock text) instead: a
new "template" field type on the settings dialog renders each placeholder as
its own expandable segment.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 13:26:58 +02:00
Lucio Lelii 81f2a890f2 Validate MCP file uploads in execution UI 2026-09-11 14:50:21 +02:00
Lucio Lelii 86e61690bb Open a step's failure instead of squeezing it into a tooltip
A step's error is regularly a provider payload or a stack, and a hover
tooltip could only ever clip it: there was no way to read past the first
few lines, let alone paste it into a bug report.

The badge now teases the failure - "Error executing" plus its first
line, clamped - and opens the whole text in a dialog where it keeps its
own line breaks, scrolls, stays selectable, and copies in one click.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 16:49:40 +02:00
Lucio Lelii 0aec372d73 Connect the rerun button to the handler that was waiting for it
The list emitted executionRerunRequested and the executor had a handler
for it, but the template never bound the two: pressing rerun on a
finished run emitted into nothing, not even an HTTP call. The button
stayed enabled because it only looks at the status.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 16:30:33 +02:00
Lucio Lelii 497eba6e1b Let the admin console scroll instead of clipping at the viewport
The admin shell was a fixed-height panel whose only scroller was the
content column, and both wrappers above it hide their overflow. Below
900px the sidebar and the content stack into one column, the two rows
no longer fit, and nothing could scroll to reach what fell off the
bottom.

Make the shell itself the single scroller, so header, sidebar and
content scroll together, and give the host the flex sizing that its
height was already assuming.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 12:52:20 +02:00
Lucio Lelii 71aacef9af Note how to build the GUI image
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 12:25:07 +02:00
Lucio Lelii d634343df5 Pre-fill no temperature for an LLM assessment
The assessment dialog opened with temperature 0, for a judgement that reads the same twice. But on
the JSON path the provider already forces a low baseline of its own, and a 0 typed in here overrode
it; the field that actually makes an assessment repeatable is the seed, which sits next to it. Every
sampling box now starts empty, meaning "the provider decides", and the request no longer carries
defaults for the shared picker to merge.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 12:23:32 +02:00
Lucio Lelii 8a57d74fb9 Say when a flow has no global input to take a field from
"From global inputs" was offered on every bindable field, so on a flow that declares none it led to
an empty picker and a form that could not be saved - a dead end with nothing explaining it. The
choice stays visible, because it is what tells you the possibility exists, but it is disabled and
says "(none declared)", and the value field it replaces is hidden while it is selected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 12:23:25 +02:00
Lucio Lelii eef5dfb3ed Stop offering a default on a field the state has made required
A schema-driven dialog read only the schema's own `required` list, so a field made required by
`x-ui-required-when` - an MCP server's `url` once the catalog is off, its `name` - was drawn with
"Use default" and could be saved empty. The inline editor already accounted for the conditional
rule; this brings the dialog to the same answer, and the same rule now gates both the button and
saving.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 12:23:19 +02:00
Lucio Lelii 074c8fb763 Give the model parameters the same treatment a node's own panel gives them
The picker for a simulator or a judge and a node's parameter panel are the
same dialog component, and the dialog has always known how to offer "Use
default" on an optional field - showUseDefault, the "Using the default" hint,
the reset. It reads one flag, defaultsWhenEmpty, which the node panels set on
every optional field and this hand-written list never set at all. So a
temperature typed here by mistake had no way back to unset: clearing the box
by hand looks the same as never having decided.

Set it on all five, and brought the rest of each field in line with what the
schema-driven panel produces for the same object: an arrow step a decimal can
actually move by, an integer step on the integers, and the tips ModelParameters
itself declares, so the same explanation appears in both places.

Temperature's max was 2 here and is 1.0 on the server, which has a comment
explaining why - the dialog was offering a value the run would be rejected
for.

Still hand-written rather than derived from the published schema: this dialog
picks a model for a run, not a node's configuration, and reaching for a block
type's schema to render five known fields would buy a network call and a way
to fail.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 15:15:10 +02:00
Lucio Lelii 984af08f36 Show every LLM assessment as a history, and drop one that arrives too late
The report carried a single `judge`, so asking a second model overwrote the
first - there was no way to compare two opinions, and reopening a report
after a re-evaluation only ever showed the newest one. The viewer now renders
`judgements`, newest first, each collapsible: the current one open and
labelled so, the earlier ones a click away with their own verdicts, narrative
and errors. A report saved with the old single field still reads, as a
history of one.

Also: closing the dialog while an assessment was running left `judging` stuck
true forever, so reopening any report showed a disabled button stuck on
"Evaluating...". And opening a different report while one was still running
let the late answer land on it, silently replacing the report on screen with
someone else's assessment. Both dialogs now carry a token that advances
whenever what they're showing changes; a response that arrives after its
token is stale gets discarded instead of applied. The job itself is
unaffected - it keeps running server-side and its verdicts land on the report
regardless, which is what makes reopening it later still show them.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 11:42:06 +02:00
Lucio Lelii 03ac4901ed Put the provider/model picker above the dialog that opens it
The picker (node-settings-dialog) shared --z-modal with every other dialog
guest, so opening it from "Evaluate impact with LLM" - itself a modal - tied
on z-index with the report behind it and lost on DOM order: it rendered, and
its backdrop even blocked clicks, but neither was visible. It read as a
button that did nothing.

Named the layer this actually is - --z-dialog-over-modal, the same one the
confirmation dialog already needed and had defined ad hoc as --z-confirm -
and moved the picker onto it.

While chasing this, closed a real silence next to it: with no LLM provider
published at all, the picker answered null and the caller treated that like
a dismissal, so the button did nothing for a second, unrelated reason. It now
throws with a message, and Simulate surfaces it as a notification instead of
swallowing it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 11:41:41 +02:00
Lucio Lelii 68f4b6db85 Open a bias report in its own window, and make the reports list a list
A report is a wide, two-column document. It rendered inside the run's side
panel - a few hundred pixels at most - where the two-column diff collapsed
into a ribbon and the summary ran off the row's right edge with a horizontal
scrollbar to prove it. It now opens in a dialog of its own, at the same width
the comparison already uses, wired next to the other dialog hosts in the app
shell. The list behind it goes back to being a list: one row read top to
bottom (kind, changed/unchanged, date; a two-line summary; annotation count
and node id), and it no longer owns the fetching or the LLM-assessment state
that the detail view needs - the dialog host does, the same way the compare
dialog already did.

Separately, the empty Bias impact tab offered "Run a biased rerun" on a run
that already is one - asking to make a variant of a variant. On a run that is
itself a comparable variant, the tab now offers "Compare with baseline"
instead, wired to the same dialog the toolbar button opens.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 11:41:24 +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 f5386cdb7e Say which subject a bias intervention moved, and let an LLM assess it
The report now reads the per-subject sections the service produces: a row per
iterated subject with what its labelled numbers did (Score 7 -> 4), the two
texts behind it a click away, and the changed ones listed first. The band at
the top leads with what the run actually did - whether the final decision
changed, how many subjects moved, the largest delta - because the counts of
changed nodes that used to open the report were the least actionable thing in
it. A container's iterations are listed with the inner node that changed,
which is what a per-subject iterator run needs and the accumulated list could
never show. Reports produced before any of this exists still render, from
their raw outputs.

"Evaluate impact with LLM" sits next to the report it is about, in all three
places one is mounted, and opens the provider and model picker the interaction
simulator uses - extracted so both call the same dialog rather than two of
their own, with temperature 0 offered by default because a verdict that reads
differently every time it is asked for is worse than none. The assessment runs
as a job, polled like the isolated experiment, and is stored on the report, so
reopening it later shows the same verdicts and the model that produced them.
It is labelled an assessment throughout, and a pair the model could not answer
for is marked without hiding that pair's own figures.

A rerun of a simulated run now opens the Simulate dialog on the simulator it
inherited, with the inherited sampling out where it can be seen - a seed
carried over is the reason the two runs are comparable, and behind a closed
section nobody would find it. Before it is started, a run says which simulator
the run it repeats used; afterwards, both the bias report and the run-to-run
comparison say so when the two sides were not answered the same way, since
that difference is not the intervention's doing and nothing said it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 10:50:54 +02:00
Lucio Lelii d11e2f5a9a Show a compared output field by field, not as two JSON trees
"Compare with baseline" rendered each side as a JSON tree, side by side in a
760px modal. Two things made it unreadable. The keys are <node id>.<output
name>, so every row opened with forty characters of uuid before the one word
saying what the value is; and the tree sized each row to its longest value,
which pushed a paragraph of model output out of the visible box and left the
row looking like an empty key.

Outputs are now flattened into named fields and paired by key, with the
output name in the heading and the node id kept as a small chip. Each changed
field shows the two sides with the words that moved highlighted, reusing the
word diff the run comparison already uses; identical fields collapse to a
single column, and a filter hides them by default. With several repetitions
one variant is compared at a time instead of all of them stacked.

The tree's min-width: max-content is gone, which also fixes the same
disappearing values in the execution viewer, the other place it is used.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 10:50:31 +02:00
Lucio Lelii a8cad0d3cd 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>
2026-09-07 14:31:20 +02:00
Lucio Lelii 3ba14244c3 Stop the HumanInteractive config type claiming fields the server drops
The type declared llmDescriptor, inputAsList and outputAsList. None of the
three exists on the Java class, which has only actionDescription, and
BlockConfiguration carries @JsonIgnoreProperties(ignoreUnknown = true), so
everything the fakes were sending was discarded in silence. They are
leftovers from when simulation was configured on the node, before it moved
to the execution - the descriptor a human task's executor uses is the
simulator's, passed in when the run is launched.

Worse than dead: the fake block-type schema declared simulateWith as a
*required* property, so in dev mode the editor rendered a field the real
server has no idea about. That schema also carried an LLMDescriptor
definition nothing referenced once simulateWith was gone.

A type that lies costs more than the fields it saves. This one sent me
planning work for a block that has no LLM.

Also pins what the node editor does with a nested field bound to an input,
which needed no change to support llmDescriptor.model: a blank value at a
dotted path plus a matching port reads as provided by that input, a set
value does not, and neither does a blank one before the server has created
the port.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 14:24:13 +02:00
Lucio Lelii 9e42ca154a Fold the assistant's panels away, and name the default it will use
Quick prompts and the assistant model panel both start collapsed now: the
prompt box is what the copilot is opened for, and everything else is a
detour until asked for. A saved session still reopens with whatever was
last expanded, the same as the other collapsible sections.

The assistant model panel gains the toggle it was already written for -
modelPickerOpen and toggleModelPicker existed, were persisted in the
session snapshot, and were never wired to the template, so the panel could
not be folded at all.

Ticking Use default configuration used to leave the panel empty: it said a
default applied without saying which. The backend has been publishing
defaultProvider, defaultModel and defaultPhaseModels alongside the
retriever URLs all along, so nothing new was needed there - only the
showing. The effective selection also appears in the section header, so
the collapsed panel still answers what the run will use, and the per-phase
lines list only the phases actually overridden rather than three rows
repeating the main model.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 12:30:03 +02:00
Lucio Lelii 37bf2cb853 Drop the Provider credentials panel from the assistant
A key gets added by picking a provider that needs one, so a second place
to manage the vault was redundant. The panel is gone, and with it the
rotate and enable/disable paths that only it could reach - the assistant
answers a provider's requirement now, it does not administer the vault.
That leaves creation as the only credential operation in the app; a
management screen, if one is wanted, belongs somewhere it can be found.

The picker's Add credential no longer appears only when no compatible key
exists: it was the panel that covered adding a second one, so limiting it
to the empty case would have made that unreachable. It also carries the
saving state the panel used to show.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 12:23:40 +02:00
Lucio Lelii ffbd8c192b Open Add credential as a modal, and close the empty credential select
Add credential set a flag whose form rendered only inside the credentials
panel, further down the aside. Pressed from the credential picker beside
the model - where you actually notice a key is missing - it scrolled
nothing into view and looked like it did nothing at all. Both entry points
now open the shared settings dialog, which is hosted at the app root and
so works wherever it is asked for.

The dialog carries the rules the inline forms carried by hand: a label and
a key are required to create one, while rotating an existing key leaves it
optional because empty there means "keep the current one", and the
provider is read-only because it is what makes a credential compatible -
rotating must not move it. The current provider is offered even when the
provider list never loaded, so the select cannot be a dead end.

A credential select with nothing in it is now disabled and says "No
credentials available" instead of opening onto an empty list. That replaces
the viewer's separate amber note, which said the same thing a second time
in a second place.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 12:19:50 +02:00
Lucio Lelii 8f8d2cb9f7 Let the copilot take a typed model too
Making Gemini's model list empty left the assistant's own picker with an
empty select and no way out: it has its own provider and model controls,
not the schema-driven field machinery, so the rule that an unlistable
catalogue has to be typed never reached it. Worse, it called the empty
list "No models are available for the selected provider" - wrong twice,
since the models exist and the message hid the fix.

It now asks the same /open endpoint, and the four model controls - the
main one and the three per-phase overrides, which were disabled outright
while the list was empty - take a typed name. The question is asked
alongside the list rather than derived from it: an open provider is
exactly the one whose list comes back empty, so "nothing to show" and
"nothing to offer" must not collapse into one answer. Closed on failure,
which leaves a select the user can see is broken.

The URL derivation moved to a shared helper. Both callers suffix the path
while keeping the query string, and the provider rides in that query - a
second copy of that detail is where the two would have drifted apart.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 12:02:00 +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 5464ad952f Stop the AI creation button coming back after the modal is closed
The floating "AI flow creation" button was never tied to a running generation: it
appeared whenever a snapshot existed under the create-modal key. Two paths put
one there without anything being in flight - the assistant persists on destroy
unconditionally, and closing the modal only cleared the snapshot inside the
branch that had just cancelled a call. So opening the modal once and closing it
was enough to make the button return on every later load, and it survived
reloads because the store persists to localStorage.

Closing now discards the conversation whichever path it takes. Minimize is what
keeps one alive, and reloading during a running generation still restores it -
which is what the button is for.

Both clears are needed: the component one also stops its own ngOnDestroy from
writing the snapshot straight back, and the store one still applies when the
assistant is already gone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 18:02:53 +02:00
Lucio Lelii eb6618044d Import a flow from a JSON file
"From JSON" in the New menu picks a file and creates the flow from it. The name
is de-duplicated the way "Empty flow" already does it, so importing the same
file twice gives two flows you can tell apart, and the result opens in the editor
- which is where the import's real failure mode shows: a flow can save and still
not be executable here.

The type check before the request is the part that matters. A typeName this
server does not know makes the backend validator dereference a null and answer
500; an unknown configuration id fails inside Jackson with a raw 400. Both are
unreadable, so the file is refused up front with the offending names. The check
honours the two type names the server has renamed, or it would reject files the
server would have accepted.

A bare graph is accepted as well as an envelope: the backend hands whole graphs
around in that shape, so a JSON copied from the container import or out of the
database still works.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 16:50:51 +02:00
Lucio Lelii c41ad1092c Export a flow as a JSON file from the actions menu
A flow could be cloned or nested in a container, but never left the installation
it lived in - no copy for a ticket, for another machine, or for outside a
database that run_service.sh recreates from scratch on every restart.

The file is an envelope, not a bare graph: the name and description survive, and
there is somewhere to put a format version. What the server decides for itself
stays behind - id, author, published, finalized, projectId, status - so an
import can never be a way to mint a public or finalized flow. Node ids inside the
graph are kept: each flow stores its own, and rewriting them would mean remapping
every connection.

Export re-reads the flow rather than trusting the cached row, and unlike opening
it does not fall back to that row on failure: a file that looks complete and is
not would be worse than an error.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 16:48:01 +02:00
Lucio Lelii 43bcc38b9e Honour optional groups in the container node too
Same annotation, same control, same write-back as in a block: one button that
opens the whole object, a badge saying how many settings are set, and a group
that disappears when everything is cleared.

The one container-specific piece is how a select resolves its options, which the
shared round trip takes as a callback.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 14:08:17 +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