Commit Graph

106 Commits

Author SHA1 Message Date
Lucio Lelii 3af6f14bb7 Send a global file input to the endpoint that accepts files
The upload went to /globals/{key}, which takes JSON, and came back as an
unsupported content type; the array variant additionally named its parts
after the input, which nothing binds on. Both now use the multipart routes
that exist for this, with the part names those routes read.

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

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 12:23:32 +02:00
Lucio Lelii 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 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 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 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 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 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 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 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 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 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 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 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 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 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 14f78b79ce fix(editor): render container subflows and long port labels 2026-08-03 17:21:28 +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 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 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 af246dab65 fix: correct real bugs and remove dead code found in a full-project quality review
Bugs:
- blocks.ts/containers.ts: reset toInit on a failed initial catalog load so
  the next call retries instead of leaving the catalog permanently empty.
- task-step-node.ts: align isEmptyDisplayValue with generic-node's
  isMissingValue so a field renders consistently between the editor and the
  execution view for the same schema.
- flows-call.fake.ts/assistant-call.fake.ts: wrap synchronous throws in
  defer() so catchError() on the caller side actually intercepts them.
- utilities/rete-editor.ts: remove a no-op ternary and a leftover debug log.
- stores/flow-editor.ts: save() now returns a handled error instead of
  crashing on a non-null assertion when no flow is loaded.
- Removed leftover console.log statements (title-toolbar, flow-item,
  editor-sidebar).

Dead code removed: the unused admin-users page, editor-sidebar's unused
createNewBlock(), rete-editor's unused flowChanged output, and the broken,
uncalled ListStateViewHolder.create().

Refactors (duplication called out by the same review):
- New session-guard.ts factory backing authGuard/adminGuard.
- tasks-executor's formatDuration now reuses the shared util.
- New services/shared/http-error.util.ts replacing the duplicated
  extractHttpErrorMessage/toHttpError in admin-call.ts and authorization-call.ts.
- New services/bias/bias-error.util.ts unifying the three different ad hoc
  error-message extractions across the bias-* dialogs.
- New pages/admin/admin-access.util.ts and utilities/temporary-signal.ts
  replacing the duplicated redirectOnAdminAccessDenied and auto-dismiss-toast
  patterns.
- New shared ModalShellComponent adopted by the three bias-* dialogs (their
  backdrop/header/footer CSS was already byte-identical); new
  password-form-validators.ts and a shared password-dialog-chrome.css
  collapsing admin-reset-password-dialog and change-password-dialog, which
  duplicated their entire validation logic and CSS.
- New services/shared/{catalog-store,empty-node-cache,pending-sync-counter,
  deep-clone,flow-node-mapping}.ts: BlocksService/ContainersService and their
  *-call.ts mappers were near line-for-line duplicates (which is exactly how
  the toInit bug ended up in both).
- New shared/nodes/node-focus-modal-controller.ts: generic-node.ts and
  container-node.ts had ~150 identical lines of focus-modal/body-scroll-lock
  plumbing.

Confirm-dialog, node-settings-dialog, human-interaction-dialog and
subflow-preview-dialog were deliberately left out of the modal-shell
extraction: each has a meaningfully different structure and no way to verify
visually here, so forcing them into a shared shell was judged higher risk
than the cosmetic-only bias-*/password dialogs, whose CSS was already
byte-identical.

61 test files / 230 tests passing; `ng build` and `ng test` green throughout.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 14:27:34 +02:00
Lucio Lelii 94ce814a6f feat(bias-impact): complete side-effect policy, compare, report list and canvas highlighting
Finishes the bias impact experiments plan (docs/bias-impact-experiments-plan.md
steps 7-12):

- Side-effect policy selector: reuse the .llm-warning visual language for the
  external side-effects banner, add a REQUIRE_CONFIRMATION note.
- Full-flow compare ("Compare with baseline"): new bias-compare-dialog
  (service + host) triggering compareBiasExecutions and opening the shared
  report viewer; inline errors read from errors[].message/detail.
- Persisted reports: new bias-impact-report-list (list + detail in one view)
  wired into a new "Bias impact reports" tab in task-execution-viewer;
  404/403 on report detail show the same inline message on purpose.
- Canvas: annotation badge on generic-node (count, executable-probe
  indicator, severity from the backend catalog); new
  BiasComparisonViewStateService driving bias-active / downstream-changed /
  routing-change highlighting on task-step-node and custom-connection, fed by
  a highlightOnCanvas event from bias-impact-report-viewer wired in all three
  places that render it; legend + "back to normal view" action in the canvas
  toolbar.
- Fixed a bug where the bias variant context badge only rendered for
  simulated executions.
- Fixed "Measure bias impact" to stay visible-but-disabled with an
  explanatory tooltip while the baseline hasn't reached a final state,
  instead of being hidden outright, per the §12 checklist.
- Added a Retry action to the compare dialog's error state for parity with
  the report list.
- Added the end-to-end facade flow test (annotation -> capability -> isolated
  experiment -> report) plus coverage for all new components/services.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 11:20:57 +02:00
Lucio Lelii 7bfef72f79 feat: add bias impact experiments and biased reruns 2026-07-21 10:34:22 +02:00
Lucio Lelii 0d60da1874 feat(flow-editor): support bias annotations 2026-07-20 16:22:55 +02:00
Lucio Lelii 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
Lucio Lelii df26fdc666 Assistant: gestione cancel, status e refactor chiamate (AssistantService, AssistantCall) 2026-05-18 12:35:21 +02:00
Lucio Lelii 175059de79 feat(assistant): add AI flow creation modal and robust session mapping 2026-05-13 12:17:35 +02:00
Lucio Lelii c87596fea1 Strengthen frontend spec coverage 2026-04-21 12:39:33 +02:00