Commit Graph

252 Commits

Author SHA1 Message Date
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
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 1e381aa0ef Cover the array item modal before refactoring it
This round trip - item schema to dialog fields and back to an object - is the
machinery an optional-group modal wants to reuse, and nothing covered it: the
dialog mock in this spec resolved null, so no test ever reached the builder or
the parser.

Six characterisation tests pin what it does today: which fields it builds and
with what labels and types, that it writes the parsed item into the array,
that it edits in place rather than appending, and that a cancelled dialog
changes nothing.

One of them pins behaviour I intend to change and deliberately does not endorse:
an emptied required number becomes 0. Writing it down is the point - the optional
case has to differ, and the difference should be visible as a changed assertion
rather than as a silent shift.

584 frontend tests green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 12:22:00 +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 307552cde1 Fold the simulation parameters into a section that starts closed
Provider and model are what anyone opening the simulation dialog came for. Shown
flat beside them, the five optional parameters turned the common case into a
seven-field form for a choice most runs do not make.

NodeSettingField gains an optional group, and the dialog renders those fields in
a collapsible section, closed until opened. A closed section that holds values
says how many, so one that is doing something never looks like one that is not -
and it counts a temperature of 0, which is a real setting rather than an empty
field.

The field markup moved into one ng-template used by both the plain list and the
sections. It is about a hundred lines of switch; a second copy would have drifted.

The open-state is a signal rather than a mutated Set. The component is OnPush, so
a Set only re-rendered when the change arrived through a template event - true
here by luck, and false the moment anything toggled a section from code. A test
caught it.

577 frontend tests green; the collapsed-by-default assertions fail when the group
is forced open. Initial bundle now 7.28 kB over budget, up from 4.26.

The node editor is untouched: that one is still to be discussed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 12:04:31 +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 bf4ed026b2 Tell the two run buttons apart by who answers the human steps
A green play and a flask said nothing about the one thing that separates them:
whether you answer the flow's chat and decision steps, or an LLM answers them for
you. The flask was actively misleading - "experiment" is the bias experiments'
word, so it pointed at a different feature.

Both buttons now keep the play, and a small badge carries the difference: a
person, or a robot. A gear was the first thought and is the wrong glyph - it is
the settings icon everywhere else in the product, so on a run button it reads as
"configure this run" rather than "a machine runs it".

They also sit next to each other now. Stop and resume used to separate them, so
each had to be understood alone - which is precisely the job a 17px badge cannot
do. Side by side they are one choice, and each is what makes the other legible.

The tooltips stop repeating the button and say who answers: "Run - you answer the
human steps", "Run simulated - an LLM answers the human steps". That is the
sentence the icon can only gesture at.

557 frontend tests green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 09:26:49 +02:00
Lucio Lelii d31d48e1c2 Move the execution tree to the bottom left, and hide it without a subtree
Anchored to the bottom-left corner instead of the top-left: the top is where the
run's own title and toolbar are, so a panel there covered the thing it was meant
to sit beside. It grows upward, which keeps its toggle in one place as the tree
gets longer.

The panel is no longer rendered at all for a run with no container steps. It used
to render as a permanently disabled toggle explaining there was no subtree - fine
in a rail, but a floating panel that exists only to say it has nothing is worse
than no panel.

With the section behind that condition, the guards inside it became unreachable:
a disabled state, an aria-expanded term and a tooltip that could never appear.
Removed, so the markup stops describing a case that cannot happen.

The page's test stub grew three methods so the viewer can actually render in it.
That is what makes these assertions real rather than vacuous - the first attempt
at the absence test passed against a deliberately broken template, because the
patch that was supposed to break it never matched.

557 frontend tests green; both assertions fail when the condition is removed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 09:11:39 +02:00
Lucio Lelii a8101bee63 Keep the execution tree reachable in fullscreen
Fullscreen makes the viewer a fixed layer over the whole page, and the tree
lives in the rail underneath it. Covered, there is no way back from a
container's child run to the parent - which turns fullscreen into a one-way door
on exactly the runs that most need navigating.

The viewer's fullscreen state becomes a model so the page can follow it, and the
page lifts the *same* panel above that layer. The same one on purpose: a second
instance would start with its expansion state reset every time fullscreen is
toggled, and expansion is how you find the iteration you were reading.

It is lifted only when the tree has something to show. Floating an empty toggle
over the view would obstruct the very thing fullscreen was opened to see.

The two-term condition moved out of the template into a named computed. That is
also what made it testable: rendering the panel drags in the whole viewer, whose
service stub this spec does not have, while the condition is the part that
carries the meaning.

557 frontend tests green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 09:05:22 +02:00
Lucio Lelii 1abdb5b591 Copy the inputs of another run of the same flow
Starting a fresh run means retyping values you already typed. The panel now
offers the other runs of this execution's own group - the group is keyed by
source flow, so a run of a different flow is never offered: matching its inputs
by name would be a coincidence, not a copy.

The copy fills the panel's pending edits rather than writing anything. That is
the whole trick: the values arrive with the unsaved-change styling already on
them, the user reviews them, and the single Save sends them in one bulk request.
No new write path, no new error handling.

It is deliberately partial, and says so. A file input holds a temp file on the
server, which the backend copies by reference when it reruns an execution; from
here there is only a path, and copying it would point this run at another run's
upload. Credentials are not copyable at all - the vault decrypts them
server-side. Both are reported in the summary rather than dropped quietly,
because a copy that silently leaves gaps is worse than one that names them.

Worth knowing before reaching for this: a rerun, bias rerun included, already
arrives fully populated - createBiasRerun calls copyReusableInputs, which carries
over every node input, the global descriptors, the authorizations and the
simulation descriptor. This is for the case that copies nothing: a new run
created from the flow.

An input already matching the source is not offered as a change, since it would
join the unsaved count and invite a pointless save.

555 frontend tests green; initial bundle unchanged at 2.88 kB over budget.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 22:53:54 +02:00
Lucio Lelii 360ed5bd82 Compare two runs of a flow, picked from the run list
The second half: picking the pair, and showing the join.

Selection is a mode of one group, and its own state. It is deliberately not
folded into selectedExecutionId - that drives which run the main panel shows, so
ticking a box would navigate away from what the user is reading. Confining it to
a group is not tidiness either: runs of different flows share no step ids, so the
join would report every node as replaced. A third pick replaces the older one
rather than refusing the click, which would leave the user hunting for which box
to clear.

The view puts the two values side by side with the changed words marked, shows
only the differing nodes by default, and compares outcomes alongside nodes -
which is where the answer lives on a flow ending in an End node. An identical
value is not diffed at all: running the table over text known to be the same can
only output "all the same".

Two things it says out loud rather than leaving to be inferred: that model output
varies between runs on its own, so a difference is not by itself evidence of
changed behaviour; and that two runs sharing no node are almost certainly runs of
different versions of the flow, rather than a flow that changed entirely.

540 frontend tests green. The initial bundle grew 0.02 kB - the view lands in the
lazy tasks-executor chunk - leaving it 2.88 kB over its budget.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 22:17:25 +02:00
Lucio Lelii 3afaf9a169 Add a word-level diff for the comparison view
Written here rather than pulled in: there is no diff library in the project, the
initial bundle is already over budget, and what is needed is small. Side by side
without it, two paragraphs of model output differing in one clause have to be
read twice to find the clause.

Word-level, not character-level: the values are prose, and a character diff on a
rewritten sentence produces confetti rather than an explanation.

Each word carries its trailing whitespace. As separate tokens the spaces match
between any two texts, so a wholly rewritten sentence came back as alternating
removed-word / kept-space parts - the same confetti, arrived at from the other
direction. A test caught this; the tokenizer changed, not the expectation.

The LCS table is quadratic in tokens, and two long model outputs are exactly what
this exists for, so past a guard it falls back to whole-value replacement rather
than freezing the view it is meant to render.

525 frontend tests green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 22:13:25 +02:00
Lucio Lelii 381e3d3817 Join two runs of a flow node by node
The first half of comparing two executions: a pure function, so the semantics
are testable without a TestBed - the same shape as flow-grouping and
planInputSaves.

It is not the bias comparison. That one scopes itself to the nodes a probe was
activated on and refuses a run that is not an experiment, so for two ordinary
runs its node set is empty and it compares nothing. Generalising it would mean
rewriting its scoping, not relaxing a condition.

The join key is the step id, stable across runs of one flow. Not the node name:
names are not unique and can be edited, and a rename would report every node as
replaced. Two runs sharing no step at all are reported as disjoint - almost
always the flow was edited between them, which makes the join meaningless rather
than merely empty.

resolveStepOutputs is the part that makes this work on a real flow.
context.result holds only the *unconnected* outputs, so on a flow whose nodes
feed one another it is nearly empty, and on one ending in an End node it is
empty outright - the run this was built for has result {} and one outcome. The
value of a connected output is still observable as what arrived at the input on
the other end of the wire, which is how the backend reconstructs node outputs
for a bias report. Outcomes are compared too, for the same reason.

516 frontend tests green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 22:12:11 +02:00
Lucio Lelii b2d744fe42 Make the bias experiment reachable, and say why when it is not
The Bias impact tab is empty because no experiment has ever run - zero rows in
bias_impact_report_entity - and nothing in the UI said how one is started.

The empty state now explains what a report is, says how many nodes carry a probe
that can be activated on this run, and offers the action. When something is in
the way it states that instead: a subflow cannot be a baseline, an unfinished run
cannot be compared against a rerun, a flow with no activatable probe has nothing
to measure.

"Create biased rerun" no longer returns silently when no node qualifies - a
button that does nothing and explains nothing is indistinguishable from a broken
one. It now names the reason.

The per-node measure control is shown on interactive blocks instead of hidden.
ChatInteraction cannot be replayed in isolation, so the isolated experiment is
genuinely unavailable there - but hiding the control made an annotated node look
identical to an unannotated one. It is rendered disabled, and the tooltip points
at the full-flow route that does work. That is a rule of the domain, worth
stating rather than concealing.

Splitting the node scan out of biasRerunCandidates keeps this free: the count is
computed from the flow snapshot, while only the capability check needs the
network.

506 frontend tests green; each new assertion fails when its behaviour is undone.
Initial bundle now 2.86 kB over budget, up from 2.21 kB - reported, not raised.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 22:10:16 +02:00
Lucio Lelii 53ac229790 Refresh the Bias impact tab when a report is produced
The list fetched only when executionId changed. The experiment and compare
dialogs are global hosts rendered over the still-mounted aside, so the sequence
that actually happens - open the tab, run an experiment, read the report in the
dialog, close it - returned the user to a tab still claiming there were no
reports. There was no refresh either: retry() is rendered only in the error
branch.

A small shared signal announces that a report now exists. The dialogs raise it
rather than the viewer, because they are what knows one was actually produced -
a failed experiment produces none - and it keeps the viewer out of a path it has
no part in.

A reload of the same execution keeps an open report open; only a change of
execution closes it, since that is a different subject.

500 frontend tests green. The reload assertion fails when the revision is
ignored again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 17:35:23 +02:00
Lucio Lelii c35bbc0ccf Compare the bias mode against the value the API actually sends
The backend enum is NORMAL | EXPERIMENT. isBiasVariantContext compared against
'BIAS_VARIANT', a value no endpoint has ever emitted, so it was always false and
took a whole feature path down with it:

- "Compare with baseline" is wrapped in @if (isBiasVariant()), so it never
  rendered - the only route to a FULL_FLOW bias report;
- the run list never labelled a rerun as a bias variant, which is the very thing
  it was changed to do;
- biasInterventionMix always returned null, so BIAS / MITIGATION / MIXED never
  showed.

I introduced this while fixing a real bug - presence of biasExecutionContext was
marking every run a variant - by correcting the condition to the wrong literal.
The fixtures used the same invented value, so the tests passed and the change
looked verified. They are corrected here too: with the old literal restored,
eight assertions now fail.

The two names are kept apart deliberately and both are commented: 'BIAS_VARIANT'
remains the list's own TaskExecutionKind vocabulary, while the API mode is
'EXPERIMENT'. Treating them as interchangeable is what caused this.

The dev fake was also seeding 'BIAS_VARIANT', so development agreed with the bug
and disagreed with the service.

497 frontend tests green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 17:32:38 +02:00
Lucio Lelii 8659b798d5 Pulse a container while its subflow is working
In the parent's execution graph a container's own status is WAITING_FOR_SUBFLOW,
so it took none of the in-progress styling and sat there inert for minutes while
its child ran. Combined with a tree that was not refreshing, a working run was
indistinguishable from a stuck one.

The card now pulses on the same rhythm as a running node, in indigo rather than
yellow because the work is a level down: the node is not computing, it is
waiting on a child, and that child is the thing worth opening. A badge names the
iteration in flight - __containerIterationIndex is new on the graph payload - so
the signal says what is happening and not merely that something is.

An errored container does not pulse: there the error is what matters.

Motion is now suppressed under prefers-reduced-motion, with the pulses settling
on their emphasised frame so the state survives without the animation. That
covers the two pre-existing pulses as well - adding a third blinking element is
the point where honouring the setting stops being optional.

Budgets: task-step-node.css went 122 bytes over, so the badge icon lost its spin
(the border pulse already carries "in progress"). The initial bundle is still
2.21 kB over its 1.75 MB budget, up from being just inside it - reported, not
raised.

497 frontend tests green; the new assertions fail with the class binding removed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 16:35:33 +02:00
Lucio Lelii 408bcb9309 Keep the execution tree's iteration list in step with the run
The tree fetched a container's iterations once, when the step was first
expanded, and kept that list until the root run id changed. Polling refreshed
the run but never that list - and it is the only source both for which children
exist and for what state each one is in.

So a three-iteration run displayed "Iteration 1, RUNNING" from start to finish.
A run that was working looked stuck, and then looked as though it had done one
iteration and produced nothing, while the container was in fact on iteration 3
with two results already accumulated.

The list now refreshes on each poll tick while the run is live, and once more on
the tick that reports it finished - that tick carries the final status and with
it the last child's real state. Nothing is fetched for a step nobody opened, a
refresh already in flight is not duplicated, and a failed refresh leaves what is
on screen alone rather than replacing valid iterations with an error.

493 frontend tests green. Both new assertions fail with the refresh disabled.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 16:28:26 +02:00
Lucio Lelii ef1c2081ec Put the failed-save message in the sticky bar, beside Save
At the top of the panel the message could be scrolled out of sight by the very
fields it was about, while the button that produced it sits at the foot. It now
shares the sticky bar with that button, so it cannot scroll away.

The bar stacks: the message takes a full-width line that wraps, above the row
with the status and the button, so Save stays exactly where it was and the text
is not clipped to one line in a 320px aside.

The pending count stays alongside it. The edits are still there and the save is
retryable, which is worth showing rather than replacing with the failure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 16:14:29 +02:00
Lucio Lelii 96c3679f2b Report a failed global save once, at the top of the panel
The globals go in a single request, so a failure belongs to the batch and not to
any field in it. Repeating the same text on every input said one thing N times
and implied N separate problems.

One message now sits above the fields, and it can say what the per-field version
could not: the request was all-or-nothing, so none of the values were saved. It
clears on the next edit, so a stale notice cannot outlive the state it described.

Node inputs keep their per-input errors: each is its own request, so a failure
there is attributable and partial success is real. File uploads likewise.

489 frontend tests green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 16:11:41 +02:00
Lucio Lelii fee24f174b Save the edited global inputs in one request
The Save bar fired a request per edited input, all at once, and they all mutated
the same execution. That is how two typed values went missing: the bulk global
endpoint replaced the whole set of globals, so the list input's save landed last
and took its neighbours down with it. The endpoint now merges (service-side fix),
but a save should not depend on request ordering to be correct.

Every edited global now goes in a single PUT /executions/{id}/globals, and the
node inputs follow one at a time - there is no bulk endpoint per step, so the
best available is not to have them in flight together. The single-input save
uses the same two requests, a global batch of exactly one, so there is one code
path and one value normalisation instead of a second copy that could drift.

planInputSaves and preparedInputValue are pure and live with the other viewer
utils, which is what made them testable: the component has no spec harness (14
injected services), and the parts worth pinning are which endpoint gets called
and what shape the value takes.

The fake now writes globals to context.globalInputs and the descriptors, where
the viewer actually reads them. The older single-key fakes only ever touched
context.inputs, so a saved global never showed up in development at all.

A failed batch reports the same error on every input in it: it failed as a
batch, and guessing a culprit would be worse than saying so.

487 frontend tests green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 16:07:33 +02:00
Lucio Lelii 9d3414d3cb Fold a list input away, and edit a long value in a box that fits it
The import action moves onto the name line: it was a flex container in a
baseline-aligned row, so it floated above the name and the type chip. It also
loses the data_array glyph for a plain upload arrow, since the dialog it opens
explains itself.

A list input now folds, on the same rule the two groups follow: unfolded while
it still needs attention, folded once it is satisfied. Five long answers filled
the whole aside otherwise. Folded, the row says how many items it is hiding, so
nothing disappears silently.

Long values were being typed through a slit ~320px wide. Every text value now
offers a larger box - on the head for a single value, per row for one item of a
list. Both write back through the ordinary pending change, so the panel's single
Save still decides when anything is sent, and the box is read-only when the
panel is.

Both dialogs now resolve the input by key when applying: editableInputs is
rebuilt on every poll, so the object captured when the dialog opened could be a
stale value to rebase an edit onto.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 14:35:32 +02:00
Lucio Lelii df8c122c12 Import the items of a list input from a pasted JSON array
Filling a TEXT[] input meant clicking Add item and typing into one box after
another - painful when the items are five long interview questions that already
exist as an array somewhere. A multi-value input now carries an import icon next
to its type chip, opening a dialog to paste a JSON array whose elements become
the items.

The parser is deliberately strict and specific about what it refuses, because the
whole point is to save typing: a silent misread would be worse than typing the
items by hand. It names the offending element rather than coercing it - an object
would otherwise have arrived as "[object Object]" - and refuses an empty array
instead of quietly wiping the items. Numbers and booleans are converted, since
those are unambiguous as text.

The imported items are emitted as an ordinary pending change, so the panel's
single Save still governs when they are sent, and an import can be reviewed or
abandoned like any other edit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 14:25:44 +02:00
Lucio Lelii d7c92325c4 Collapse each input group behind a badge of what it is still missing
Global and node inputs are now collapsible sections, each headed by a badge: red
with the number still to fill in, green with a tick once none are. The count is
what makes collapsing safe - a folded group can never quietly be the thing
holding the run back.

A group starts open exactly when something in it is missing, and collapsed when
it is complete, so what needs attention is what you see without expanding
anything first. That is only the default: once the user toggles a group their
choice sticks, in both directions, rather than being overridden the next time the
counts change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 14:20:23 +02:00
Lucio Lelii 253e8ae483 Count node inputs too, and name the groups after the domain
The tally said "0 of 3" while ignoring the manual inputs below it, which are just
as required: the backend only reaches READY when every step is ready, so a step
missing its manual input blocks the start exactly as an unsatisfied global does.
It now counts the whole panel.

"Provided" is decided per input and passed down, rather than inferred in the
panel from a globals-only list. A global uses the backend's own
missingGlobalInputKeys; a node input is judged on its stored value, ignoring
unsaved edits - otherwise typing would make an input look satisfied before it was
sent. An empty list, or a list of blanks, does not count as supplied.

"Flow inputs" and "Manual inputs" are now "Global inputs" and "Node inputs",
which is what they are called everywhere else in the codebase and in the API.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 14:17:31 +02:00
Lucio Lelii dcf3af5551 Make filling in execution inputs one form, not a stack of cards
Each input opened with a centred gradient badge whose largest text read "Flow" -
the same word on every global input, so the most visual weight carried the least
information - followed by a "Type:" line and a four-row textarea. A one-word
positionTitle got the same box as a CV, and three short answers filled the panel.

An input is now one line of chrome - name, a small type chip, and a dot saying
whether it is still required - over a field that starts small. Removing an item
from a list is an icon rather than a full-width "Remove" button, which at this
width used to push the field out of the panel.

Saving is one action for the panel. Per-input buttons meant a click and a round
trip each, with nothing to say how much was still unsaved; a sticky bar now
reports the pending count and saves them together. Each one still goes through
the same single-input request, so a failure is still reported against its own
input.

The panel also stops ignoring missingGlobalInputKeys, which the backend has been
sending all along: it now shows "2 of 5 provided" and marks exactly the inputs
that block the start.

The editor is one template instead of four near-identical copies - global and
node, each single and multiple. That was not the goal here, but writing the same
change four times is how those four drifted apart in the first place. First tests
for this component.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 14:13:09 +02:00
Lucio Lelii 0fe5a0109c Read the bias direction from the fields the API actually sends
The direction badge always said "Bias". It derived the direction from
biasExecutionContext.activeBiasProbes, and that field is not on the bias context
at all - probes belong to a step view - so the lookup was always undefined and
every variant fell through to the bias branch. My own change one commit ago.

A persisted snapshot settles what the payload really is: bias and mitigation are
tracked in four separate collections - annotations per node and activated
subflows per container, one pair for each direction. The direction is which of
them are non-empty, so a variant can now correctly read as Bias, Mitigation, or
Bias + mitigation, including when an intervention was switched on for a whole
container subflow rather than per annotation. A variant with nothing recorded
says "unspecified" instead of guessing.

The model was wrong in a second way, with a second victim:
activeAnnotationIdsByNode is not sent either, so the bias highlighting on graph
nodes read an absent field and never lit anything up. It now combines both
directions.

The model is corrected to the real shape and the two absent fields are gone from
it, so nothing can quietly read undefined again. The derivation lives in pure,
tested helpers rather than inline, and the dev fake now splits activations by
direction like the real payload - flattening them hid this very difference in
development.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 11:40:33 +02:00
Lucio Lelii ad8a65fb18 Make a bias variant recognisable, and quieten the run history
Nothing in the run history said whether a run was a bias variant. Worse, the
viewer header claimed "Bias variant" for *every* execution: it tested for the
presence of biasExecutionContext, which the backend sets on all of them,
defaulting to NORMAL. Only the mode distinguishes them. The same faulty test also
offered the bias comparison action on any plain rerun.

Each history row now carries a kind badge - Run, Rerun, Bias, Mitigation, or Bias
+ mitigation - coloured and with a tooltip saying what it means for the result. A
bias variant reads as a variant even when it is also a rerun, because carrying
probes is what changes how its output should be read.

The rows also stop showing raw uuids. A rerun names the run it came from by
number ("from #1") instead of repeating a 36-character id, the execution id is
gone from the row entirely - the viewer header owns it - and the group header no
longer prints the source flow id. "1 runs" reads "1 run".

The viewer header keeps only what changes how a result should be read: Simulated,
the bias variant, Subflow. Execution id, simulator descriptor, experiment id,
baseline and probe internals moved behind a Details toggle, closed by default.
Two Italian strings in that block are now English, like the rest of the app.

Cards are flat here too, matching the flows list: no gradients, no lift on hover,
a left accent for the selected run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 11:34:36 +02:00
Lucio Lelii f086064e61 Make the outcomes section collapsible
An outcome payload is often a generated document - the local example is a full
rejection email - which pushed the execution graph down the page. The section now
collapses.

It starts open, because this is the flow's answer and it was invisible until a
moment ago. Collapsed, the header keeps the outcome codes, so the conclusion is
never hidden entirely: only the payload folds away.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 11:29:35 +02:00
Lucio Lelii 323115fd40 Disable the execution tree toggle when there is no subtree
The tree renders nothing for a run without container steps, so the panel offered
to open onto an empty box. The toggle is now inert in that case, greyed, with a
tooltip saying why, and the chevron - which promised something to unfold - is
gone.

The host decides "is there anything to show" with the tree's own rule rather than
a second guess at it: the container-step test moved out of the component into an
exported function taking the container-type predicate, so both call one
definition. Guessing separately is how a panel ends up claiming content the tree
will not draw.

An existing test caught the behaviour change, which is the right outcome: it is
rewritten to cover both halves - inert with no run selected, and toggling once a
run actually has a container step.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 11:25:15 +02:00
Lucio Lelii 0dcdc6d135 Show a run's outcomes, where an End node puts the flow's answer
A flow result is built only from *unconnected* outputs, so wiring the last block
into an End node moves its value out of the result and into the outcome payload -
which nothing in the UI read. The run then looked like it produced nothing at all.
There is already an execution in a local database whose outcome payload is a full
generated rejection email that was invisible for exactly this reason.

The run view now has an Outcomes section above the graph, listing each End the run
passed through: its code, its label, the step it came from, and its payload. An
End reached with no value says so, rather than showing an empty box - the two
cases mean different things.

A text payload renders as text rather than through the JSON tree. The tree would
have kept the line breaks but wraps strings in quotes, and the common case here is
a generated document, not a data structure.

This is the smallest of the options for the underlying gap: the data was already
persisted and already in the API payload, so nothing in the engine changed. The
gap itself remains - End cannot be attached as a pure ordering dependency
(canDependOnOtherNodes is false), so a single-exit flow still has to choose
between a labelled end and a value in `result`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 11:21:33 +02:00
Lucio Lelii 4fa449658e Hide flow finalization behind a feature flag
Finalizing a flow makes it permanently read-only and cannot be undone, and it is
not part of the current workflow, so the controls that create that state are now
hidden: the Finalized toggle in the title toolbar and the Finalized entry in the
flows list filter. Gated by FLOW_FINALIZATION_ENABLED, matching SWIMLANES_ENABLED,
so nothing is deleted and re-enabling is one line.

Only the controls are gated. The badge on a flow row, the disabled delete and the
read-only editor stay, because rows finalized before the switch still exist and
hiding the explanation of why such a flow cannot be edited would make the app
inexplicable. For the same reason the toggle reappears for a flow that is already
finalized, so its state is never invisible in the place that owns it.

A persisted FINALIZED list filter now falls back to showing everything: a filter
whose control is hidden would otherwise keep narrowing the list with nothing on
screen to clear it.

The backend is untouched - finalized still gates editing and deletion there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 10:58:15 +02:00
Lucio Lelii 4c973b3922 Make the blocks and containers palettes scroll again
Rewriting those two stylesheets from scratch dropped the :host rule they had, so
the host had no height for .blocks-list-items to be constrained by. Its
overflow-y never engaged and the palette simply ran off the bottom of the sidebar
once there were more entries than fitted. Restored, with a note saying what it is
load-bearing for.

Two related sizing fixes while here. The flows list capped itself at
calc(100vh - 380px), a number tuned for the tall header that the collapsible
filter panel replaced; height now comes from the flex column, which cannot go
stale the next time the header changes. And the executions history was missing
min-height: 0, so its flex item refused to shrink below its content and never
scrolled either.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 10:55:05 +02:00
Lucio Lelii 5476324d8c Quieten the blocks and containers palettes, description on demand
Each card carried an avatar, a title, a subtitle, the full description as body
copy and a chip that repeated the subtitle - five elements to say what is really
a name and a kind. In a narrow sidebar that reads as a wall of text you have to
scroll past to find the block you want to drag.

A card is now one line plus a kind icon, with the description behind the info
button. It expands in place rather than in a tooltip or a modal: these
descriptions run to several hundred characters, which a tooltip cannot render
readably, and a modal is heavy for a hint you read while reaching for a drag.
Only one description is open at a time, so the list cannot unfold into the same
wall of text by another route.

Dragging is untouched and now says so: the whole card shows a grab cursor and a
delayed tooltip naming the gesture, and a test pins the draggable attribute so a
future restyle cannot quietly break the palette's only real job.

The chip removal also drops MatChipsModule from both components.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 10:52:12 +02:00
Lucio Lelii 6d1efc4f69 Quieten the flow list: one action menu, flatter cards, subtler filter row
Three things the sidebar was getting wrong.

The flow card was doing too much: two gradients, a 24px shadow, a 14px radius and
a blue tint, repeated a dozen times down a narrow list. A list of flows should
read as a list, not as a stack of raised panels, so the card is now flat with a
hairline border, and depth is spent only on the one card that is open - a left
accent rather than a wash of colour.

Every action moved into a single overflow menu next to the name, which also
settles the overflow for good: four icon buttons never fit a 320px sidebar, and
less still beside the reorder arrows. The visibility lock became what it always
was - an indicator, not a permanently disabled button - and moved up next to the
name, so the footer row disappears entirely and each row is two lines.

The filter toggle looked exactly like a project group: same bordered, rounded,
grey box. It is a control, not a section, so it is borderless and muted now, with
a rule under the open panel to separate it from the groups below.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 08:20:01 +02:00
Lucio Lelii 3d93a3f99a Keep the flow actions inside the sidebar, collapse the list filters
The flow card carried four Material icon buttons at their default 40px, with
flex-shrink disabled, in a 320px sidebar - and a grouped row adds reorder arrows
on top. The icons pushed past the edge. They are compact now, the date can
shrink, and the footer wraps rather than overflowing.

Search, visibility, sorting and the project selector together filled most of a
short viewport, so the controls were all you could see and the list itself was
pushed off screen. Everything except the search now sits in a panel that starts
collapsed, in both the flows list and the executions history. The search box
stays out of it: it is the control people reach for first. The collapsed header
carries a count of the active filters, so a hidden filter can never silently be
holding items back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 08:15:30 +02:00
Lucio Lelii 3eba5815ee Show projects with no flows, so a new one is visible
Groups were built only from flows, so a project with none did not render at all.
Creating your first project therefore looked like the opposite of what happened:
the new project was nowhere to be seen, while every pre-existing flow suddenly
appeared inside a group - the "No project" bucket, which the list only starts
showing once grouping switches on.

Every project now gets a group, empty ones included, with an empty state that
says how to put a flow in it. Empty groups are still dropped while a search or a
filter is narrowing the list, where they would be noise rather than reassurance.
The ungrouped bucket, in turn, only appears when something is actually in it.

Also stops the dev fake deriving new project ids from the current count: after a
delete that reissued a freed id, and any flow still pointing at it reappeared
inside the new project.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 08:02:47 +02:00
Lucio Lelii 0b706ea95c Group the flows list by project, with shared context and project runs
Projects group the flow list in place: no new page or tab, the existing sidebar
list gains collapsible per-project sections plus a project filter. Everything is
behind PROJECTS_ENABLED in shared/feature-flags, and with no projects the list
renders exactly as it did before - flows-list.spec guards that.

FlowsList stays the single owner of the load, filter, sort and list state; the
grouping is extracted into a pure flow-grouping.ts and a presentational
flows-group component. A wrapper rendering N flows lists would have re-registered
the same list state and re-triggered the load N times. The group chrome mirrors
tasks-executions-list, which already implements collapsible groups, so the two
sidebars read as one product.

toFlowCreateRequest deliberately still carries no project. It builds the
full-replace PUT the editor issues on every save, so a project sent there would
be silently dropped each time; membership changes only through
assignFlowToProject. flow-mapper.spec guards it.

Deleting a project destroys its flows, so it gets its own dialog rather than a
wider ConfirmDialogService: it names the count, lists the flows, says that
finalized flows go too - which the flow list otherwise forbids - and requires the
project name to be typed. Widening the shared confirm service for one destructive
caller would have rippled through every other call site.

Moving a flow between projects is a menu item, not drag-and-drop: the sidebar is
320px with its own scroll, and the whole card is already a click target, so a
drag gesture would fight the open-flow gesture. Flow order inside a project uses
up/down arrows for the same reason.

Shared context is edited in a dialog modelled on the Global Inputs panel, which
is the mental model users already have for ${{global.x}}; the title toolbar shows
the inherited values read-only, because that is where prompts are written.

"Run project" creates and starts a run - creating alone would look like nothing
happened. Runs come back BLOCKED or STOPPED when a step needs inputs or failed,
and the UI says so instead of claiming progress. Project runs surface in /tasks
as sibling groups labelled with a project chip, derived in tasks-executor: no
second nesting level and no backend change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 07:53:42 +02:00
Lucio Lelii abf21b1c24 Add fullscreen mode, wider zoom-out, and draggable execution graph positions
- Lower rete editor min zoom (0.35 -> 0.1) so wide flows fit on screen.
- Remove the Export action from the read-only subflow preview dialog,
  since it's only ever opened from the execution view.
- Add a fullscreen toggle to the flow editor and task execution viewer,
  placed next to the title; disables the "flows" sidebar tab and hides
  the right assistant/validation panel while the flow editor is
  fullscreen, and collapses the left sidebar if it was open.
- Allow dragging nodes in the read-only execution graph and persist
  custom positions client-side in sessionStorage, keyed by execution id,
  restored on reload/refresh.
2026-09-02 20:00:34 +02:00
Lucio Lelii 3906bf8bd5 Fix create-with-AI minimize/resume regression from cancellable-call stub
hasCancellableCall() was hardcoded to false (from an earlier commit that
gutted the real async call tracking), which silently blocked
minimizeCreateWithAi() from ever minimizing an in-progress AI flow creation
into the floating banner, and blocked closeCreateWithAi() from prompting to
cancel a running request.

restoreSessionForFlow() also never resumed polling for a call that was still
QUEUED/RUNNING when its snapshot was saved (e.g. after minimizing and
reopening, or a page reload), so a resumed session lost live progress
updates and could get stuck showing a stale phase.

Restores real hasCancellableCall()/cancelActiveCall() backed by the
submitMessage/getCall/cancelCall flow, resumes polling on restore for an
active call, and refreshes the session from the backend otherwise. Adds
getSession() across the assistant call service stack to support this.
2026-09-02 13:39:55 +02:00
Lucio Lelii 38b01b2bee Migrate flow assistant chat to the async session call flow
Replace the direct draft/refine/fix/explain HTTP calls (which no longer exist
on the backend) with submitMessage + polling on getCall, wiring the
currentCall/progress-phase UI that was already scaffolded but never
connected. Intent (draft vs refine vs fix vs explain) is now inferred
server-side instead of guessed client-side. Updates the fake service and
specs to match.
2026-09-02 13:32:21 +02:00
Lucio Lelii 3b48e71b82 fix(blocks): follow the renamed block type catalog endpoint
The backend replaced GET /blocks/types/catalog with
GET /blocks/types/configurations/catalog. Same payload, same contract.

The container catalog endpoint is unchanged and is left alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 11:20:48 +02:00
Lucio Lelii c7cefaca96 feat(execution): gate execution start on vault credentials
An execution cannot start until every requiredAuthorizations entry is
satisfied. A key of the form LLMProvider::<provider>::authorization is
answered with a vault secret id chosen from a picker filtered by that
provider, or from a credential created on the spot; anything else keeps
the literal-value panel it had.

A provider key never falls through to the literal-value panel, whatever
the provider catalog says or fails to say, so the API key itself can no
longer be pasted as the authorization value. The catalog is therefore no
longer part of the gate: an outstanding requirement blocks the start on
its own, and a failed catalog read is a notice with a retry.

The authorizations PUT answers with the recomputed execution, so its
response replaces the execution in the store rather than triggering a
list refresh, and the backend's 400 message reaches the user. A settled
requirement stays on screen with the credential label and a change
action, and a banner above the graph names the missing providers when
the context aside is collapsed.

The gate itself is now pure - buildAuthorizationGate and
isExecutionStartable in execution-viewer.utils.ts - and covered by
tests, since the component cannot be instantiated under the test
environment.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 23:23:14 +02:00
Lucio Lelii 4a40a8aa2a feat(assistant): select provider, model and vault credential
Lets the assistant panel pick the provider and model for a session, and
choose or create the vault credential an external provider needs. Status
messages for the credential endpoints come from the shared
CREDENTIAL_ERROR_MESSAGES map, so the assistant and the vault report
400/401/409 the same way.

Also moves the remaining Italian UI strings to English, matching the
rest of the application.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 23:23:14 +02:00
Lucio Lelii 7e9b483882 feat(vault): add vault and LLM provider services
Adds the services the credential flows need, following the existing
call/fake pattern: a base, an HTTP implementation and a fake for the
vault, the LLM provider catalog and the per-provider credential listing,
registered in all three environments.

The two fakes share an in-memory vault store that reproduces the
server-side rules of the credential listing (owner, active, provider,
soft delete), so a credential created from one panel shows up in the
other exactly as it does against the real backend.

Also teaches extractHttpErrorMessage the RFC 7807 `detail` field, which
is what the execution endpoints answer with.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 23:23:14 +02:00
Lucio Lelii 8b34f49056 fix(assistant): unlock configuration after failed request 2026-09-01 16:32:11 +02:00
Lucio Lelii d05a661473 feat: update flow editor enhancements 2026-09-01 15:53:39 +02:00
Lucio Lelii cf6b25bd1d feat(assistant): add provider and model selection 2026-09-01 15:53:28 +02:00
Lucio Lelii b6474def51 fix(validation): scope errors to active subflows 2026-08-03 21:52:53 +02:00
Lucio Lelii 4d92cbc51c feat(execution): refine tree and subflow layouts 2026-08-03 17:21:52 +02:00
Lucio Lelii 14f78b79ce fix(editor): render container subflows and long port labels 2026-08-03 17:21:28 +02:00
Lucio Lelii 15b411c992 fix: reset refine panel state when opening a flow 2026-07-28 08:01:49 +02:00
Lucio Lelii 31e2700c5a feat: refine subflow editing navigation and zoom 2026-07-27 17:48:51 +02:00
Lucio Lelii 760202090d feat: edit container subflows from flow editor 2026-07-27 17:07:46 +02:00
Lucio Lelii 1e5a6e78ca feat(task-execution): resolve ${{...}} placeholders in human interaction text
question/actionDescription arrive with literal ${{name}} / ${{global.name}}
placeholders since no block type ever persists a "resolved" prompt. Resolves
them client-side in a single pass (not sequential replaces, to avoid
re-resolving a value that itself contains ${{...}}), then renders each
value as its own expandable card or collapsed accordion instead of
concatenating long/array values inline into the surrounding text.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 21:07:54 +02:00
Lucio Lelii b1e1a08b13 feat(task-execution): show container iteration history in a tree
Adds GET /executions/{id}/node/{stepId}/iterations to the task
executions API and a recursive execution-tree component that lets
users navigate the full iteration history of looping containers
(not just the currently active one), rendered in the left rail
alongside the run list.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 12:46:08 +02:00
Lucio Lelii 8faee0fbaf feat: support interactive container subflows 2026-07-24 07:22:38 +02:00
Lucio Lelii 4886bf828a feat(task-execution): support human decision contracts 2026-07-23 11:30:32 +02:00
Lucio Lelii a6fef8c207 style(task-execution): highlight clickable bias badge 2026-07-23 11:22:02 +02:00
Lucio Lelii c67163faaa feat(task-execution): show bias annotation details 2026-07-23 11:21:34 +02:00
Lucio Lelii f633493bd6 fix(task-execution): resolve bias badge from catalog 2026-07-23 11:09:58 +02:00
Lucio Lelii cc1f773c2d fix(task-execution): render execution graph topology 2026-07-23 10:48:06 +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 9f54896545 feat(bias-impact): extend bias annotation support to containers
Mirrors the existing block-side bias impact feature onto containers,
reusing the same descriptor, form and error handling as the backend
integration doc specifies, rather than duplicating them:

- FlowContainer now carries biasAnnotations, matching FlowBlock.
- ContainersService/ContainersCallService gained
  retrieveBiasCapabilities/retrieveBiasCapabilitiesForInstance,
  backed by GET/POST /containers/types/{type}/bias-capabilities.
- bias-annotations and behavioral-probe-editor now accept FlowNode
  instead of FlowBlock only; the probe editor dispatches capability
  lookups to BlocksService or ContainersService based on nodeFamily,
  so container annotations reuse the exact same shared components
  and UI instead of a second, parallel implementation.
- container-node gained the bias badge, the bias-annotations panel,
  and preserves annotations across recreateContainer() the same way
  generic-node already does across block regeneration.
- task-execution-viewer's bias-rerun candidate list now includes
  containers with executable probes, not just blocks.

Also fixes a real data-loss bug found while wiring this up:
rete-editor's exportGraph explicitly stripped bias annotations from
containers on every flow save, silently discarding anything entered
through the new panel.

Verified with the full suite (238/238, 6 new tests) and a live
end-to-end browser check: an annotation added to a container only
offered INPUT_TRANSFORMATION/OUTPUT_TRANSFORMATION (per the fake
container capabilities), and survived a save + reopen of the saved
flow, proving the exportGraph fix actually round-trips the data.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 09:59:46 +02:00
Lucio Lelii 761535acdc fix(generic-node): align delete-confirm colors with the shared danger palette
generic-node's "Delete node?" popup used its own slightly different
red shades (border #fca5a5, gradient background, text #881337)
instead of the #7f1d1d/#fecaca/#fff1f2 combination that its own
warning tooltip and container-node's identical delete-confirm popup
already use consistently. Aligned it to that shared combination.

Investigated the header color difference between generic-node (blue)
and container-node (teal) that an earlier audit flagged as an
inconsistency: task-step-node.css shows the header gradient is a
deliberate per-node-type identity color (blue = automatic, orange =
human-interaction, per its .llm-node--human .llm-header rule), so
container-node's teal is consistent with that scheme, not a bug — left
unchanged. The port badge colors (input/output/dependency) were
already identical between generic-node and container-node.

Verified with the full suite (232/232) and a live browser check of
the delete-confirm popup.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 09:25:03 +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 803e70e6e5 refactor(dialogs): converge confirm/node-settings/human-interaction/subflow-preview onto shared tokens
Each of these four dialogs hardcoded its own border-radius, border
color, shadow and z-index instead of sharing the tokens introduced
for modal-shell. Two of the mismatches were more than cosmetic:

- confirm-dialog sat at z-index 10000, below node-settings-dialog
  (10010) and below any modal-shell-based dialog (10021). Both
  bias-rerun-dialog and bias-impact-experiment-dialog call
  ConfirmDialogService while staying open themselves, so a confirm
  triggered from either would render completely hidden behind them
  and be unclickable, silently stalling the flow. confirm-dialog is
  now the top of the z-index ladder (--z-confirm: 10031) since it is
  the one dialog that can legitimately nest inside any other.
- human-interaction-dialog used a 12px corner radius while every
  other dialog uses 16px; aligned it to --radius-modal.

subflow-preview-dialog keeps its larger radius, gradient background
and node-card border color, since it deliberately mirrors the canvas
aesthetic of the subflow it previews rather than a form dialog — only
its z-index was aligned to the shared ladder.

Verified with the full suite (232/232) and a live browser check of
the token-consuming modal-shell dialogs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 09:02:35 +02:00
Lucio Lelii fcb8c0957a refactor(styles): introduce shared design tokens, migrate modal-shell onto them
The dialog/modal CSS across the app hardcodes the same handful of
values (border-radius, box-shadow, surface/border/muted-text colors,
z-index) independently in each component, with several components
already drifting to slightly different values for the same intent.

Add a small set of :root custom properties in styles.css (radius,
shadow, surface/border/text colors, spacing scale, modal z-index
ladder) and migrate modal-shell.css — the shared shell already used
by three dialogs — onto them first, as the lowest-risk starting
point. Values are unchanged, only their source moved to tokens, so
this is a no-op visually: verified via the existing test suite
(232/232) and a live browser check confirming computed values are
unchanged and the editor/dashboard render identically.

Follow-up: retrofit confirm-dialog, node-settings-dialog,
human-interaction-dialog and subflow-preview-dialog onto the same
tokens, then reconcile node CSS (generic-node/container-node).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 15:07:18 +02:00
Lucio Lelii 897f57808e fix(bias-annotations): backdrop no longer trapped inside node canvas transform
The bias annotation editor rendered its dimming backdrop as a plain
position:fixed div. Since it sits inside a node card that rete.js
positions with a CSS transform for pan/zoom, that transform becomes
the containing block for fixed descendants, so the backdrop was
confined to the node's own box instead of covering the page.

Switch to a native <dialog> shown via showModal(), which promotes it
to the browser's top layer regardless of ancestor transforms, and use
::backdrop for the dimming overlay. showModal() is guarded because
jsdom does not implement it, so unit tests keep working.

Confirmed both by unit tests and a live browser check: the backdrop's
bounding rect now spans the full viewport instead of the node's box.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 14:56:43 +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 909c4e6724 test(tasks-executor): update grouped executions mock 2026-07-20 16:33:05 +02:00
Lucio Lelii 0d60da1874 feat(flow-editor): support bias annotations 2026-07-20 16:22:55 +02:00
Lucio Lelii a258064eb5 feat: add execution groups and schema-driven flow fields 2026-07-20 16:22:50 +02:00
Lucio Lelii 4c4b57781f feat: Add subflow export functionality with source name tracking
- Implement export button in subflow preview dialog
- Add sourceName parameter to track flow origin
- Add deriveSourceName() utility to normalize source names
- Improve dialog layout with flexbox for action buttons
- Fix confirm dialog z-index (10000)
- Add exporting state signal for export button UI
2026-05-20 17:13:18 +02:00
Lucio Lelii 3dbb140131 fix: ensure LoopContainer required fields are set via schema-driven normalization 2026-05-20 15:54:33 +02:00