Commit Graph

247 Commits

Author SHA1 Message Date
Lucio Lelii 46f40fe9be Put a loop's delete button in its label, not between its ends
Every connection puts its × at the midpoint of its two ends, which is on
the curve for an ordinary one. A way back is drawn above the nodes, so
that midpoint landed on whatever connection ran between them - it looked
like that connection's ×, still showing after another was selected.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 13:04:54 +02:00
Lucio Lelii 0c129a5f8d Draw a loop's way back above its nodes, clear of how tall they grow
Below the ports, the way back ran through any node taller than a fixed
drop - most of them, once expanded - and its limit label sat hidden
behind the node. It now runs above the tops of the nodes at both ends,
which move only when a node is dragged, and follows them when one is.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 12:58:58 +02:00
Lucio Lelii f2378216ee Show which round a loop step is on, and answer for the round seen
In an execution, a step on its second round or later carries a Round N
badge. Answers to a person's question - decisions, evaluations, evidence,
revealing the reference - send the round of its loop the question was
opened on, fixed when the dialog opened rather than read at submit time,
so an answer to an old round's question is refused by the server instead
of being applied to a draft the person never saw.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 11:41:13 +02:00
Lucio Lelii fbd27c4344 Draw a loop's way back, and let its limit be set on it
A connection from a router back to where a loop starts is picked out
with the same rule the server uses, drawn underneath the nodes in violet
with its iteration limit, and edited in place when selected. The limit
travels on the connection as its loop settings.

An input used to hold one connection, and Rete dropped the old one before
asking. The entry of a loop takes two - from before the loop, and back
from the router - so inputs now accept several and the editor itself
replaces the old one, except when exactly one of the two leads back.

Connections keep their saved id across loads instead of getting a new one
each time, so settings and selections stay attached. Node capabilities
now carry routesExclusively through to the client.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 11:38:39 +02:00
Lucio Lelii f9491e0904 Add package license to lockfile 2026-09-23 12:12:00 +02:00
Lucio Lelii 1270bf7ea7 Improve execution group management 2026-09-23 12:09:57 +02:00
Lucio Lelii def50c229b Merge branch feature/human-evaluation-node into main 2026-09-22 16:08:33 +02:00
Lucio Lelii 3755081e0f Add a live activity panel to the admin area
Answers one question - is it safe to restart the server - so it answers that
first: a single verdict line, then the two lists behind it. A run in flight
outranks a connected user however many there are, because a person loses an
unsaved edit and comes back while a run is simply gone.

Refreshes itself every fifteen seconds, the page being worthless the moment it
is stale. A failed refresh keeps the last known answer rather than emptying the
page: blank would read as "nobody is connected", the one wrong conclusion
somebody could restart the server on.

PresenceService sends a heartbeat a minute, and only while the tab is visible.
Every authenticated request already counts as a sign of life, but the editor is
quiet - someone reading a flow or typing a prompt makes none for minutes, and
they are the person a restart interrupts worst. A backgrounded tab is not
someone using the application, and counting one would turn the panel into a list
of forgotten tabs, which is the failure the server's own session column already
has.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 10:11:39 +02:00
Lucio Lelii 3d4b49e030 Merge branch 'main' into feature/human-evaluation-node 2026-09-21 15:06:52 +02:00
Lucio Lelii fd30de2b1d Point the web interface at its home on the ISTI Gitea
Seven references followed the move: the licence addendum, the NOTICE, the README
attribution, the citation metadata, the package repository field and the two links
the About dialog shows a user - one of which names the backend service, so it moves
to the service's new home too.

The working notes that used to sit in docs/ are gone from the repository and named
in .gitignore, together with .claude. What remains under docs/ is the user guide,
which is written for whoever runs the application rather than for us.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 15:03:11 +02:00
Lucio Lelii 44be453cbb Summarise a plain-value array row by its value
A task script is a list of strings, and every row read back as "Item 1", "Item 2":
the summary only knew how to read object rows, so the very text being configured was
hidden behind its position. A string, number or boolean row now reads as itself, and
falls back to the position only when it is blank.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 12:15:58 +02:00
Lucio Lelii ed4dae1a39 Let the log be read backwards while it is still running
The panel scrolled itself to the newest line on every poll, so a reader
who scrolled up to follow something got yanked back five seconds later -
exactly when a log is worth reading, and exactly the moment it became
unusable.

Following now stops as soon as the reader leaves the end, and resumes on
its own when they come back to it, so the common case needs no button.
The button is there to say which of the two is happening and to override
it: a log that stops moving on its own otherwise looks like a log that
stopped.

The threshold lives in the utils with the rule, not in the component: not
zero, because a list that grew by a line between the scroll and the
handler would otherwise read as the reader having walked away.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 11:55:51 +02:00
Lucio Lelii 469099dffc Read an event's details as an object, under a title that fits
The first cut put the event's own sentence in the heading and its details
in a textarea of raw JSON. A pruning warning runs to a full paragraph, so
the title wrapped five lines and read as an error page, and finding one
number in the body meant counting braces.

The event now opens in its own dialog: a two-word name from the event
type, the node and time beneath it, the sentence as prose, and the
details as the JSON tree the app already had. The name is derived, not
tabulated - a type nobody has seen yet still reads as words - with the
initialisms that would otherwise shout or look like typos spelled out.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 11:24:17 +02:00
Lucio Lelii 8c3d9c6ed3 Let an event's own numbers be opened from the log
The log showed each event's sentence and threw away everything behind it.
A pruning warning saying "6332 characters over budget" could not tell you
what the total actually was, a tool call could not say which tool: those
numbers reached the browser and were dropped, leaving the API as the only
way to read them.

Rows that carry details now offer an eye, which opens them read-only.
Rows that carry none offer nothing - a button opening an empty box is
worse than no button - so the decision of what is worth opening lives in
the view model, where it can be tested, rather than in the template.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 11:07:38 +02:00
Lucio Lelii 974589aeee Render the evaluation form, and submit a judgement as one act
The interaction dialog knew three shapes: a chat, a free-text answer and
a decision. A HumanEvaluation node needs a fourth - the target to open,
the steps to follow, and one control per criterion - so it gets its own
component beside the others rather than another branch inside one.

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

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 21:15:58 +02:00
Lucio Lelii 82396e8b8f Drive the view-content button from the schema, not from a skills rule
The button was wired to the literal retriever name "Skills" and to a
hand-built /retriever/Skills/definitions URL, so the node knew about one
particular binding. It now reads x-retriever-definition-url off the item
property: any binding that declares where its value can be read gets the
view, and the node knows nothing about which one it is showing.

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

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

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

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

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 11:29:58 +02:00
Lucio Lelii d09ec47be6 Let a skill binding's content be viewed from the node editor
Skills were selectable only by bare id, with no way to see the
instructions a SKILL.md actually carries. Adds a "view content" button
next to each skill row, backed by the already-existing
/retriever/Skills/definitions/{id} endpoint and the previewOnly dialog
mode, so no backend change or new dialog infrastructure was needed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 11:29:49 +02:00
Lucio Lelii 0a2f7606d9 Merge feature/declared-field-defaults into master 2026-09-18 21:33:01 +02:00
Lucio Lelii fc541c12ef Offer "Use default" only where a field declares one
The control was derived from the field being optional, which put it on nearly
every field in every dialog. It now comes from the schema's
x-ui-defaults-when-empty, which the server sets per field: "you may leave this
blank" and "leaving this blank means something specific" are different claims,
and only the second is worth a control.

Two rules still sit above the declaration. A field the current state requires
offers nothing - clearing it would go towards a default that does not exist - and
a checkbox is excluded, because false is a value, not an absence.

Sampling parameters a provider does not apply are no longer offered either. All
five were shown to every provider and the unsupported ones were dropped at run
time, reported in a warning on an execution that had already happened. The filter
is shared by the two editing surfaces so they cannot drift, and it keeps every
knob whenever the answer is not knowable - no provider chosen, one the server does
not list, a capability call that failed - because a knob wrongly hidden cannot be
set at all, which is worse than one that does nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 11:52:55 +02:00
Lucio Lelii bb5621f559 Let the simulator and bias judge dialogs supply a credential
The backend now accepts a credentialId for both the interaction simulator
and the bias judge, but nothing in the UI could provide one - a
credential-requiring provider chosen either way would always fail, short of
coincidentally reusing a credential from elsewhere in the same execution.

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

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

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 14:51:16 +02:00
Lucio Lelii b13c2019eb Let a retriever-backed field fall back to free text in two more places
The backend already publishes /retriever/LLM/models/open, which says
whether a provider's model field is a fixed list or free text - Gemini
has used it in production for a while, through generic-node.ts's own
leaf-field editor. Two other renderers never asked that question, and a
provider that cannot list (any hosted one with no listing endpoint) got
stuck there with an empty, unusable select instead:

  - container-node.ts's own field editor decided select-versus-text from
    retrieverKey alone, synchronously, before the free-text question could
    even be asked. toDialogFieldType now takes the answer as an argument,
    resolved first through a new fetchRetrieverFreeText.
  - Nested objects - an LLMDescriptor inside a container's configuration
    or an array item, reached through the buildSchemaObjectDialog both
    generic-node.ts and container-node.ts share - had no way to ask at
    all. SchemaObjectDialogHooks grows an optional loadFreeText hook; when
    it says yes, loadOptions is skipped entirely (the same fetch a
    listable retriever would otherwise make and throw away) and the field
    is forced to text. One fix in the shared function covers both
    components that call it.

Deliberately not "an empty options list means free text" - that heuristic
would be wrong for a retriever whose empty list is a real answer, a
project's global inputs among them, where nothing to choose from is not
the same question as nothing to type. A property with no loadFreeText
hook, or one that answers no, keeps rendering as a select, empty or not,
exactly as before.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 12:30:43 +02:00
Lucio Lelii e1708f95b8 Read the real provider catalog instead of hardcoding InternalOllama
Two things in the copilot both used to hinge on the same wrong check,
isInternalProvider(provider) - a plain string comparison - instead of
what the backend's /llm/providers catalog already publishes:

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

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

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

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

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

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 12:30:13 +02:00
Lucio Lelii 5787a6fe44 Add the SPDX licence header to every source file
Mechanical: three comment lines at the top of every .ts, .html and .css
under src, and nothing else. Split out from the licence commit so the
files that carry an actual change stay readable in the history, and kept
to its own commit because it moves the blame line on 390 files.

The short SPDX form rather than the full GNU notice - it is
machine-readable under REUSE, it satisfies the requirement to keep the
licence notice intact, and it points at LICENSE-ADDENDUM instead of
restating the attribution term in every file.

The template and stylesheet headers do not reach the bundle: Angular
discards template comments and the production build strips CSS ones. The
one in index.html survives, since that file is served as written, which is
no loss.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 12:10:18 +02:00
Lucio Lelii 85e6c12372 Complete the user guide in English and render it to PDF
The guide was Italian, 531 lines, and stopped at the features that existed
when it was written: it described the editor and executions and said
nothing about bias annotations, probes, experiments or reports - the part
of the platform that most needs explaining, because none of it is
discoverable by clicking around.

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 12:09:49 +02:00
Lucio Lelii a426a2c5e7 Summarise an array row by the branch it is on
An upload row taking its file from a global listed as "GLOBAL · false".
The summary takes the first two values a row holds, and the false was a
"several files" box nobody had touched - while the global the row names,
the one thing that tells it apart from the next row, never got a look in.

Two kinds of value are skipped now, both for the same reason: they
identify nothing. A false flag is an absence dressed as a fact, and a
field the row's own choice hides is irrelevant by construction.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 11:41:24 +02:00
Lucio Lelii c298c352b3 Let a configurable field take a global input, and show its own tip
The small parameter editor offered two sources for a configurable-as-input
field: a static value, or a workflow input. A global input was reachable only
by typing ${{global.<name>}} by hand into the value box - and on a provider
whose model list is closed there is no value box to type into, which left the
one flow-level way of deciding such a field unreachable from the editor.

There is now a third source that lists the flow's declared globals and writes
the placeholder for you, with a plain sentence when the flow declares none
rather than an empty select.

The modal also never rendered a field's description, so every tip written on
one of these fields was invisible.

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 10:50:54 +02:00