Commit Graph

169 Commits

Author SHA1 Message Date
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