Commit Graph
14522 Commits
Author SHA1 Message Date
0bbd559ac8 feat: instrument AI fill/fix, evals, agents and the debugger (#10853)
* feat: track AI fill, AI fix, evals, reusable agents and debugger usage

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore: pin ee ref to the feature_usage registry commit

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore: update ee-repo-ref to c3b6f62ea579a3583d4b474e9885c77104cfc87e

This commit updates the EE repository reference after PR #745 was merged in windmill-ee-private.

Previous ee-repo-ref: 77992910929188a854eadc06ee45971877b6f954

New ee-repo-ref: c3b6f62ea579a3583d4b474e9885c77104cfc87e

Automated by sync-ee-ref workflow.

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2026-08-28 16:31:13 +02:00
hugocasaandClaude Opus 5 8f349c032a fix: nested template literals in step inputs, and unresolvable $args tags (#10856)
* fix(frontend): keep nested template literals intact in template inputs

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: fail a flow step with an unresolvable $args tag instead of hanging

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): surface input expression errors when running a step test

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): treat an escaped \${ as literal text when escaping backticks

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: accept the string "null" as a tag component, reject only JSON null

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: leave a same_worker step's inert tag alone, log an unresolved flow tag

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): escape every backtick when the template walk desynchronizes

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: leave a dedicated runnable's inert step tag alone

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: reroute a step only when its own tag is what failed to resolve

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor: name the inert-tag guard step_is_pulled_by_tag

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: reject a tag only when it interpolates to nothing at all

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): validate the template walk instead of trusting a balanced stack

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: describe what an unresolvable tag actually interpolates to

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(frontend): decide template escaping with a real parser, not a hand-rolled scan

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor: name is_flow_step on push now that it is load-bearing

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): heal an expression escaped before nested templates were handled

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: reroute a step whose tag reads args that failed to evaluate

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: never hand a job that failed before running to a dedicated runner

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: reroute only a step whose args failed, leave other tags untouched

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: drop the post-preprocessor tag fallback, leaving tag resolution untouched

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor: leave interpolate_args exactly as it was

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test: use a generic example in the template literal tests

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): show an expression escaped by the old rule as it was authored

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): surface input expression errors from every step-run entry point

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: state what is_dedicated_worker actually reads

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): heal only text whose backticks were all escaped by the old rule

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): match the old rule textually so an authored backslash still heals

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): heal only expressions the old rule broke, never ones that parse

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 16:27:54 +02:00
9213319a74 docs(api): document large completed job result placeholder (#10866)
* docs(api): document large completed job result placeholder

* style(api): use spaces for the large-result description indentation

Co-authored-by: Diego Imbert <70353967+diegoimbert@users.noreply.github.com>

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Diego Imbert <70353967+diegoimbert@users.noreply.github.com>
2026-08-28 08:45:00 +02:00
hugocasaandClaude Opus 5 320f400512 feat: enable Anthropic prompt caching on Vertex AI agent steps (#10876)
* feat: enable Anthropic prompt caching on Vertex AI agent steps

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Mfioobw3Wp8qAmPE82aM9

* fix: add an escape hatch for Vertex projects with prompt caching disabled

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Mfioobw3Wp8qAmPE82aM9

* docs: state that the caching flag spans every Anthropic platform

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Mfioobw3Wp8qAmPE82aM9

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 08:44:08 +02:00
AlexRV12andClaude Opus 5 fb82f36e6d fix: pre-fill the test panel JSON args editor and align its placeholder (#10871)
* fix: pre-fill the test panel JSON args editor and align its placeholder

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: reseed the JSON args editor when the preprocessor tab is selected

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: seed schema defaults and own-property args in the JSON payload

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: follow the schema in an untouched JSON payload, ignore same-tab clicks

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: latch JSON editor ownership from Monaco, drop the late remounts

The pristine check read the bound `code` value, which trails the buffer by
SimpleEditor's 200ms debounce — a reseed arriving in that window overwrote text
already typed. Latch ownership from Monaco's own change event instead, via a new
undebounced `input` event guarded so `setCode`'s `setValue` does not read as an
edit.

Both `.then(() => argsRender++)` bumps are gone: the arg views now remount at the
tab transition only, and follow the schema in through `initialCode` when
inference resolves, so a remount can no longer land on an in-progress payload.

`FlowPreviewContent.selectInput` overwrote the editor on select but not on
deselect, leaving the abandoned input's payload over reverted args.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 18:51:08 +02:00
Ruben Fiszelandrubenfiszel 90b40fffc3 chore(main): release 1.798.1 (#10870)
* chore(main): release 1.798.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
v1.798.1
2026-08-27 12:00:45 +02:00
hugocasaandClaude Opus 5 c2279db8a9 fix: allow job tokens to read the automate_username_creation setting (#10869)
* fix: let a job token read the automate_username_creation setting

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test: use an ungated global setting as the confinement control

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 11:37:37 +02:00
Ruben Fiszelandrubenfiszel 2302e58c24 chore(main): release 1.798.0 (#10868)
* chore(main): release 1.798.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
v1.798.0
2026-08-27 11:07:15 +02:00
29133398f9 feat: a wizard for importing a hub project, and finishing what the import cannot (#10729)
* feat(frontend): guided setup wizard for data tables

On Cloud a data table cannot use the Windmill instance database, so a new
workspace hit a dead end: an alert telling the user to go find a PostgreSQL
resource somewhere else. Setting one up meant three disconnected places, and the
connection could only be tested after the config had already been saved.

Adds a three-step wizard (choose a database -> set it up -> name it) reached from
the data tables settings page:

- Supabase: signs in via the existing supabase_wizard OAuth client and creates
  the project from inside Windmill. Because db_pass is an input to project
  creation, Windmill sets the password and the user never visits a dashboard.
- Your own database: picks an existing postgresql resource, or adds one with a
  connection string through the form that already supports it.
- Windmill database: hands back to the inline row editor, since instance
  databases are provisioned by a superadmin.

Verifying access is no longer a step the user takes: Continue runs the check and
passing it is what advances the wizard, so a database that cannot create tables
never reaches the workspace config.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore: pin ee-repo-ref to the Supabase provisioning endpoints

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): do not claim the database is ready when its check failed

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: address review findings on the data table wizard

- The Supabase create branch advanced on `provisioning === 4` without consulting
  the check it had just run, so a role that cannot create tables could reach
  Finish. It now blocks and offers Try again.
- Retrying no longer mints a fresh secret variable + resource each time: the
  credentials are only re-created when the password actually changed.
- The generated password is captured before the create call rather than after,
  since a throw there can still leave a project behind.
- On a failed provision the project list is refreshed, so the just-created
  project can be picked up from the other tab instead of provisioning a second.
- Finish refuses a name that already belongs to another data table, which
  previously repointed it at the new database.
- Secrets go to the acting user's namespace instead of a literal `u/admin/`.
- The progress list no longer ticks "Created on Supabase" before the request is
  sent, and does not claim the database is ready when its check failed.
- The wizard's resume state is cleared when it closes, so reopening after an
  abandoned OAuth round trip is not stuck on step 2.
- The OAuth callback shares the session-storage key rather than repeating it.
- SupabaseConnect uses the shared provisioning helpers instead of a fork.
- Restores the doc comment displaced onto TestDataTableResourceQuery.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): simplify Alert layout and balance its vertical padding

The body was rendered by two near-duplicate branches, each wrapping the text in an
extra div only to hang a margin on it, and the margins disagreed: the collapsible
branch spaced above with mt-2, the static one below with mb-2. Since isCollapsed
defaults to true, every non-collapsible alert took the static branch, so titled
alerts read as 24px of space below the text against 16px above -- visibly
off-centre -- with the title and body flush against each other.

Collapse both branches into one and drop the margins; the container's own padding
now sets top and bottom equally, with a small gap under the title row.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): only offer Supabase when its OAuth client is configured

The wizard offered the Supabase card unconditionally, so on an instance whose
superadmin never configured a supabase_wizard client -- or whose backend is built
without the oauth2 feature, which compiles the whole /api/oauth router out -- the
card dead-ended at a 404. Gate it on listOauthConnects, the same check
ApiConnectForm already makes, fetched on open so configuring the client mid-session
does not require a reload.

Also drop the Supabase project ref from the existing-project cards: it is an opaque
identifier that means nothing outside Supabase's own dashboard URLs. Show the region
instead, plus a status word when the project is not healthy, since a paused project
is the one case where the connection check fails for a reason unrelated to the
password.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): run the Supabase OAuth leg in a popup

A full-page redirect unmounts the wizard, so anything the user does on Supabase's
side -- signing in, confirming an email, browsing their dashboard -- leaves them
with nothing pointing back at Windmill, and the wizard had to park its state in
sessionStorage to survive the trip.

Open the connect endpoint in a popup instead. The modal stays on screen throughout
and the callback hands the token back through postMessage rather than navigating.
The parked-state path stays as the fallback for browsers that block the popup.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): scope the connection check to the choice that produced it

A failed check stayed on screen when the user switched Supabase mode or picked a
different provider, so a fresh tab opened showing an error about a database it had
nothing to do with. Clear the report and the error on both switches; re-clicking the
tab already selected leaves an error the user is reading in place.

Also polish the Supabase step: project cards get the provider-card treatment (icon,
p-3, flex column) instead of a hand-rolled variant whose block layout left more
padding above the name than below; form labels settle on text-emphasis; and the
signup link sits under the primary button for anyone who does not have an account
yet.

Drop the "free" badge and the "Free on Supabase" line -- every option in the wizard
is free, so neither told the user anything -- and say what the Supabase card
actually does now that connecting an existing project is the default.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(frontend): one setup checklist and one Supabase step for every host

The data table wizard, the instance database modal and the resource drawer each had
their own version of the same two interactions, and they had already begun to drift:
the wizard's Supabase resource shape was rebuilt by hand in the drawer, and the
instance checks rendered with no notion of a step being in flight.

SetupChecklist replaces LoggedWizardResult, whose only consumer was the instance
modal. It adds the running state that component lacked, so a list driven by an
endpoint that reports nothing until it returns still shows where it is. Both the
instance checks and the Supabase provisioning stages render through it.

SupabaseProjectStep owns picking or creating a project, and useSupabaseOauth owns
the popup leg. Each host keeps only what is genuinely its own: the wizard saves a
variable and resource then verifies the connection, the resource drawer fills in its
own form. Both trigger authorization themselves, so a host can offer it a screen
earlier than the step does.

The lists load behind a spinner because which mode to open on depends on whether the
account has projects; deciding that after rendering flipped the toggle under the user.

Adds a kitchen_sink playground for the checklist so the animation and every failure
position can be exercised without a backend, a superadmin, or a Supabase account.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): tidy the resource drawer around the Supabase entry point

Connect Supabase was a hand-styled anchor carrying Supabase's brand hex values
rather than a Button, and it sat in a row whose other controls had settled on
unifiedSize md. Making it a Button meant SupabaseIcon had to satisfy IconType, so it
now takes `size` (deriving height/width from it) alongside the string props its other
callers pass.

The manual resource form spaced every field 32px apart and WhitelistIp added another
16px of its own, which read as a gap rather than a rhythm. One gap of 16px, with the
form itself given a little more separation from the description above it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): stop Supabase resources coming up modified when first opened

Resource forms fill in every unset property from the schema as soon as they render,
so a postgresql resource saved without region, root_certificate_pem and use_iam_auth
was dirty -- and had saved a draft -- the first time anyone looked at it. Write them
with the rest of the value.

SupabaseConnect also rebuilt the resource shape by hand instead of using the shared
helper, which is how the pooler host format ended up in two places.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(backend): record where a data table came from and whether setup finished

edit_datatable_config replaces the whole datatables map and DataTable does not deny
unknown fields, so anything the request omits is dropped without a word. origin and
setup_incomplete would have been erased by any unrelated save;
preserve_unmanaged_datatable_fields carries them -- and migrations_enabled, which had
the same problem inline -- forward for entries that already exist, following renames.

setup_incomplete is what lets a row be recorded before the resource it points at
exists, so the wizard can write nothing until the user finishes. There is deliberately
no intermediate state: the setup runs entirely in the browser, so nothing server-side
could advance one.

datatable_health probes every data table at once for the settings page and skips the
incomplete ones, whose resource_path resolves to nothing yet. set_datatable_setup
patches a single entry instead of resending the map. test_datatable_connection_value
checks a connection the caller has not saved anywhere, which the wizard needs before
it has written a resource.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): make destructive default and subtle buttons read red

Both variants were neutral until the pointer arrived, then filled solid red: nothing
marked the button as destructive until you were already on it. They now carry red text
at rest, with a faded red border on default and a light red wash on hover, which is
what the legacy red border style in the same file had always done.

Three call sites passed color="red" alongside a design-system variant. getStyleClass
returns before colour is read for accent, accent-secondary, default and subtle, so the
delete-migration control, its modal confirm and the import-database button had all been
rendering neutral. They pass destructive now.

The dropdown variant strips the button's own border, and matched border-border-light
literally -- a class the destructive style no longer contains.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(frontend): rebuild data table setup around a read-only row

The wizard gathers intent over two steps, reviews it on a third and writes nothing
until Finish, so a billable Supabase project is created only once the user has seen
what will happen. runSetup is also the retry: every step probes for its own result
before doing anything, so running it again on a half-finished data table resumes
instead of duplicating. Its steps are keyed rather than dispatched on their titles,
where rewording one changed what it did.

The settings row stops being an editable form with a dirty/save cycle. It carries the
name, where the database came from, a health dot and two actions; everything rare
moved into the gear panel, which also offers Finish setup for a data table whose
wizard never completed. Manage is ExploreAssetButton, the control the ducklake list
already uses, and the row and panel both link out to the underlying resource.

supabaseResourceValue no longer assembles the pooler host from the region.
aws-0-<region>.pooler.supabase.com is wrong for any project Supabase allocated
elsewhere, so the host, user and port come from the pooler config endpoint.

Two data tables sharing one database also share _wm_migrations, which is probed
unqualified, so the review step warns when the database being connected is already
behind another data table.

SupabaseConnect is deleted. The resource drawer uses the shared project step
restricted to existing projects: creating one is a billed action and belongs in the
wizard, which has somewhere to report what it did. The kitchen_sink checklist
playground goes with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): fall back to a direct Supabase connection when the pooler cannot be read

Reading a project's Supavisor config needs the database_pooling_config_read scope, which
an instance's Supabase OAuth app may never have been granted. No retry recovers from
that, and the wizard treated it as fatal: the user was left with an error and no way to
finish connecting a project that was otherwise fine.

resolveSupabaseConnection replaces the bare pooler read everywhere it happened. Asking
for session pooling and failing now yields a direct connection plus the reason, which
supabaseResourceValue already knew how to write. Nothing about the fallback is silent --
direct is IPv6-only, which is the whole reason session pooling is the default -- so the
wizard warns on its review step and the resource drawer says so in its toast.

The row is recorded before credentials are saved, so an origin claiming session pooling
has to be corrected once a direct host is what gets written; the run patches it through
set_datatable_setup rather than leaving the panel to report a mode nothing uses.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(frontend): open the database behind a data table, and say when it cannot write

Every database in the list now opens the surface that owns its credentials. A postgres
one opens its resource in the editor drawer; a Windmill instance one opens the instance
modal, which is where its setup checks, password rotation and drop already lived. Both
are reachable from the row and from the panel's provenance list, and the provider icon
moved inside the button so the whole thing is one target.

CustomInstanceDbWizardModal targeted #content unconditionally, which put it underneath
the panel drawer that now opens it. It takes a target, and the panel portals it to the
body.

The status column gains a third state. The probe reports privileges but nothing gated
the dot on them, so a data table whose role cannot create tables showed as Connected and
only failed when someone ran a migration. It reads "Limited permissions" instead, and
opens the panel on the report carrying the GRANTs that fix it -- the settings page has
already probed, so the panel takes that report rather than asking the user to run Test
connection over work already done. fullyPrivileged is exported from the report component
so the dot and the report cannot disagree about what counts as healthy.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* revert(frontend): keep the data tables settings table as it was

The settings table and the setup wizard are two changes that only shared a file. Splitting
them makes each reviewable: this branch keeps the wizard, and the read-only row, gear
panel, health probe and clickable databases move to their own branch.

The rows go back to the editable form with its pickers and save footer, still opening the
wizard from Add a database. DataTableSettingsPanel, dataTableHealth and dataTableOrigin
had no other consumers and go with them; the connection report stays, because the wizard
shows it too.

DataTableSettingsType keeps `origin`: the wizard writes it, and the review step reads it
back to warn when two data tables would share one database and therefore one
_wm_migrations table.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): confirm before dismissing the data table wizard mid-setup

Closing was guarded while a run was in flight and unguarded before one, which is backwards:
a run leaves a row to resume from, whereas a backdrop click on the review step threw away
the project, the pasted password and the folder with nothing to recover them from.

Backdrop, Escape and the close button now go through one path that asks first. It only asks
when there is something to lose -- no provider chosen yet, or a run that already produced a
result, closes immediately -- so the dialog does not become something to click through.
Continue in the background still leaves in one click; that exit was always the deliberate
one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): stop the wizard claiming the resource folder controls who can use a data table

"Who can use this database" was wrong. Every path that resolves a datatable:// reference --
both executors and the agent-worker endpoint -- reads the resource unchecked, by workspace
and name. A resource in u/admin is usable by everyone's scripts. The folder governs who can
see and edit the connection, and who can reference the resource directly in a SQL step;
neither is who can use the data table. The wizard was contradicting the tab's own
description two screens later.

The folder select and name field become one Path picker, the same one the resource,
variable and script forms use, so the review step reads as a resource path rather than a
permission choice. Its initialPath is snapshotted when the step opens: Path seeds itself
from it, and a live value fights the typing. Finish now also gates on Path's error, so a
taken or malformed path stops the run before it writes anything.

The button that opens all this says "Add a data table" -- the data table is what you get;
the database is a detail chosen along the way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* revert(frontend): move the destructive button restyle out of the wizard PR

This reverts 3881e4d8ea. Making default and subtle destructive buttons red at rest changes
every existing caller of the prop -- the workspace integrations, AI skills, workspace
creation and the instance database drop -- so it is a design-system change, and the call
sites it fixed are the migrations list and the database manager. None of that is the setup
wizard.

Nothing on this branch passes destructive any more, so it leaves with no loose ends.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): make the wizard stepper navigate the steps it already offers

Stepper dispatches a click and paints cursor-pointer on every reached step, but the wizard
never listened, so the breadcrumbs invited a click and did nothing.

They now reach any step already passed, in either direction: going back to check something
should not cost the progress, which means tracking the furthest step reached rather than
the current one. Forward movement still only happens through the primary action, so a step
is never reachable without having been validated -- and changing the intent revokes the
steps ahead of it, or Finish could run against a review built from something the user has
since edited. The five places that cleared the probe on an edit now do both through one
call.

During a run nothing is reachable, and the stepper says so rather than showing a pointer
over steps that will not respond.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): restore the data tables description lost in the branch split

The rewritten description went into DataTableSettings.svelte shortly before that file was
restored wholesale to its pre-rebuild state, so it left with the row rework it had nothing
to do with. The tab went back to describing the plumbing -- a fully managed PostgreSQL
database, reachable from the SDK -- which never answered the question a new user actually
has: why this rather than a Postgres resource.

It leads with what a data table is, then the two things a resource cannot do -- nobody
needs the credentials to query it, and the name can be pointed at another database without
editing anything that uses it -- and closes with what Windmill runs on top. Both middle
claims are the ones every resolution path backs up: datatable:// resolves by workspace and
name, unchecked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(backend): say what is missing when a $res: or $var: reference does not resolve

Both interpolations fetched with fetch_one and mapped the error through to_anyhow, so a
reference to something deleted surfaced as "no rows returned by a query that expected to
return at least one row @workspaces.rs:2169". It names neither the kind of thing that was
missing nor its path, and it is what a data table pointing at a deleted resource reports.

They now fetch_optional and return NotFound naming the path, and datatable resolution adds
the data table on the way out: the caller asked for one by name, and a bare "resource
f/x/y does not exist" leaves them to work out which of them points at it. The health probe
is new, so this string had only just become something users read.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(frontend): gate the data table wizard behind a dev flag

The wizard only appears with `dataTableWizard` set in localStorage; without it the
settings page keeps the inline-row flow it had before this branch, down to the empty-state
copy and the "New Data Table" button, and the wizard component is not mounted at all. The
existing e2e suite drives that button, so the default-off flag is also what keeps it green.

Step 2 of "your own database" becomes one list rather than a segmented control: the
workspace's Postgres resources, then a New resource card that expands in place. A
connection string is not an alternative to a resource, it is how one is written, and the
old layout taught otherwise. The card holds the same connection as a string or as fields
and carries values across when you switch, so `parse` and `compose` have to be inverses --
hence the percent-encoding on both sides, which also fixes a password containing `@`
silently corrupting in the resource form. The Supabase step now uses the same shape.

Names and paths are checked as they are typed rather than at the end of a run that may
have created a billed project first: the data table name against the charset
`edit_datatable_config` enforces, the instance database name against what
`setup_custom_instance_db` will accept, and the resource path against both the resource
and variable namespaces, since the run writes to both and both writes upsert.

`test_datatable_connection_value` refuses `$var:`/`$res:` in its body. It feeds
`transform_json_value_unchecked`, which resolves references with no permission check of its
own, so an admin could otherwise have had the API server decrypt any workspace secret and
hand it to a host the same request chose -- without the audit trail a variable read leaves.
Callers testing something unsaved hold the literal value already.

Alert, SetupChecklist and postgresConnectionString change for everyone, not just behind the
flag: body-only alerts no longer reserve an empty title row, the checklist can nest the
checks a step is made of, and the connection-string parser is shared with the resource form.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore: pin ee-repo-ref to the EE branch merged with EE main

The Supabase proxies the wizard calls are still unmerged, so the ref cannot be an EE
main commit yet; it now names that branch merged with EE main rather than the branch
alone, which was nine commits behind and would have been built against a CE main it
never saw.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(frontend): gate the supabase resource path behind the dev flag

* test(frontend): pin connection string parsing to libpq behaviour

* fix(frontend): keep the supabase resource link off the popup callback path

* refactor(frontend): load the supabase resource dialog only behind the flag

* fix(frontend): refuse a resource path the wizard run does not own

* fix(frontend): let a failed data table setup be corrected without losing what it made

* fix(frontend): let a failed setup reuse the resource path it claimed

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(backend): record the two data table connection tests in the audit log

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): use Section for the data table wizard advanced group

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): read connection strings the way libpq does

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(backend): pin the ee ref back to a commit this branch can build

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): keep a failed setup's claims across the redirect and rollback

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(backend): probe a data table with the auth mode the worker will use

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): keep every part of a connection string through the round trip

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(frontend): give a setup run one record of what it created

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): mark a resource claim by edited_at, not its creator

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): mark every claim by revision, and keep an unconfirmed project's secret

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): refuse to test or save behind a connection string that will not parse

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): refuse a connection string carrying options the resource cannot hold

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): allowlist the connection-string parameters a resource can honour

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): guard every created Supabase project, not just the last one

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat: three-step wizard for importing a hub project

Importing used to be a single page that inherited whatever workspace happened to
be active, with no way to say where the project should go — the hub cannot know,
since it only ever links to *an* instance. `/projects/import` now asks: which
kind of destination, which workspace, then imports.

Nothing is created, switched or written until the last step runs. The wizard's
state is a plain value in the URL (`importWizard/plan.ts`), so the back button,
the stepper and the Back control are the same operation, and none of them can
strand a half-created workspace — there is no state anywhere else to unwind.
`importWizard/execution.svelte.ts` is the only code that acts on a plan: it runs
create → fetch → import as an observable task list, reuses what already
succeeded when retried, and offers to delete the workspace it created if the run
stops early. Its UI needs — the data table migration review — are injected, so
it holds no components.

The old `/projects/install` becomes a redirect: hubs upgrade on their own
schedule and a self-hosted one may keep pointing at it for a long time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: let the import wizard survive sign-in and a missing workspace

Signing in with `rd=/projects/import?hub=...` dropped the destination: the login
redirect only honours `rd` verbatim for `/user/workspaces`, so anyone with more
than one workspace landed on the workspace picker instead — the page the wizard
exists to replace, asking the question it was about to ask. Both copies of that
logic now allow the wizard through.

The root layout's "no workspace selected" redirect skips the wizard too. It
picks the destination itself and may end in a workspace that does not exist yet,
so bouncing it to the picker forces the very choice it is there to make.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore: bench page for the import project card

/kitchen_sink/import_project_card renders the card against fixtures — a real
project, an oversized one, a minimal one — so its layout can be judged without a
hub running or an import in flight.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): do not warn about renaming an item that does not exist yet

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): make the review step read as one list of what will exist

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): keep the picked Supabase project across the redirect, reject connect_timeout

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: check the data table connection from a worker, not the API server

The wizard's connection check ran on the API server through two endpoints added
for it. That server is a different machine with a different identity, so the
answer was about the API server rather than about the worker that will run the
queries: a host reachable from one is not necessarily reachable from the other,
and IAM RDS and Azure workload identity authenticate as whichever process opens
the connection.

Run the privilege query as a preview job instead. A job goes through the
worker's Postgres executor, which is where `PgAuthMode::of` already picks the
authentication mode, and it takes either a resource value or a `$res:` path
exactly as a Postgres step does. Postgres composes the suggested GRANT
statements through `format('%I')`, so identifier quoting stays where it is
already implemented.

Removes `test_datatable_resource_connection` and
`test_datatable_connection_value`, and `connect_as_the_worker_would` with them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor: fold check_datatable_connection back into its only caller

The helper was split out so the two connection-test endpoints could share a
body. Those endpoints are gone, leaving one caller.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* revert: keep the data table connection check schema inline

It was lifted into components so three endpoints could share it. Two of those
are gone, so it is back to one user and the extraction changes nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: restore openapi.yaml to the branch point

The previous commit restored main's tip rather than the merge base, which
carried three unrelated main-only changes into this branch: the resource
mcp_tools truncation fields, the execution_mode description, and a version bump.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(frontend): drop four effects from the data table wizard

Each was doing work a derived, a load callback or a real entry point does
better.

- The name conflict is kept with the name it was raised for and derived from
  it. As an effect it was correct only because it never read what it wrote:
  the pre-flight sets the message and the effect does not re-trigger, so adding
  a read would have cleared it the instant it appeared. The message now also
  comes back if the taken name is retyped, which is what the server will say.
- The default resource selection is seeded inside the fetcher that loads the
  list, where "has the fetch settled" cannot be asked wrong.
- Reset-on-open becomes an exported open(), called by the settings page, so a
  fresh run is set up by the act of opening rather than by a flag emulating
  mount.
- The OAuth connects and the folder list become resources; supabaseAvailable
  and folders are derived from them. defaultFolder takes the list rather than
  reading it, so the fetch can seed off its own result.

Leaves the debounced path check, which is async with an out-of-order guard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(frontend): drop three effects from the Supabase branch

- useSupabaseOauth reports success as onAuthed, alongside the failures it
  already reported. SupabaseResourceConnect was watching `authed` to find out;
  it takes the callback instead, keeping the guard that stops an authorization
  started elsewhere on the page from opening its dialog.
- SupabaseProjectStep loads its orgs and projects through a resource keyed on
  the token, so the `loaded` latch goes and re-authorizing reloads rather than
  keeping the lists from the expired session.
- SetupChecklist records what the user toggled and derives the open state from
  it, a failed step defaulting to open. Recording the open state instead needed
  an effect to force it, and that effect re-ran on every progress update, so a
  description closed while anything was still ticking reopened. A close now
  holds for the life of the checklist, including across Try again.

Leaves the message listener, which subscribes to another window.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): confine the modal restyle to the wizard, and trim the comments

The wider side padding and lighter dialog heading were changing all 17 Modal2
dialogs to suit this one flow. They move behind an opt-in `formStyling`, taken
by the three dialogs this branch owns; every other Modal2 renders as it did.

Also drops two comments that cited a design approval rather than a constraint,
and shortens the blocks that had grown past the four lines AGENTS.md asks for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): use the accent token for the wizard's links

`text-blue-500` is the marketing blue `#3B82F6`, which brand-guidelines.md
rules out in the app interface.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore: point ee-repo-ref at the EE branch head

Picks up EE main, which the branch now needs, and the Supabase proxy auth fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): read sslmode by name, and stop decrypting a secret to date it

- `sslmode` was found by searching the query text, so it also matched inside
  another parameter's value: `?application_name=sslmode=disable` passed the
  allowlist on the parameter name and then parsed as a request to turn TLS off,
  which both the wizard and the resource form saved and probed. Parsed with
  `URLSearchParams` by exact name, with a test.
- `secretMark` read the variable with `decryptSecret` defaulted to true, so
  every write decrypted a secret nothing reads and recorded the decryption --
  including someone else's on the retry about to refuse it. It wants only
  `edited_at`, which is returned either way.
- The probe gave up at 15s while the worker allows its Postgres connect 20s, so
  a host that accepts the connection and never answers was cancelled and
  reported as a missing worker rather than a failed connection.
- The create-mode region and project name did not report an intent change, so
  renaming a project after a name collision left the failure naming the old one.
- Two comments described the code as it was before the claim mark became a
  revision, and a doc comment outlived the field it documented.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): read connection parameters the way libpq does

One reader for both the parser and the allowlist, since they disagreed about
what a string says in two ways that both ended in a weaker connection than was
pasted:

- `URLSearchParams.get` takes the first of a repeated parameter and libpq takes
  the last, so `?sslmode=disable&sslmode=require` was read as `disable`.
- The allowlist folded the parameter name and the parser did not, so
  `?SslMode=verify-full` was refused by neither and honoured by neither, and
  saved as the `require` default.

The parked Supabase run is now handed to `open()` rather than read back off the
`resume` prop it was just assigned to, so restoring it does not depend on when
that prop reaches the component.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): keep connection parameter names case-sensitive

libpq does not fold them: `?SslMode=disable` is rejected as an invalid URI
query parameter rather than read as `sslmode`, which a local server confirms.
Folding made Windmill accept and honour a string Postgres itself refuses;
naming the parameter instead tells the user why it cannot be stored.

The last-value-wins rule for a repeated parameter is unchanged, and matches
what the same server does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): seed the Supabase organization from the project it selects

The loader took `orgs[0]` independently of the project it seeded, so an account
whose first project sits outside its first organization had the review step name
an organization the database does not belong to. Picking a project by hand
already derives it; the seeding now does the same.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): let the probe report an empty search_path instead of failing on it

`format('%I', NULL)` raises rather than returning NULL, so a role whose
search_path names no valid schema failed the whole privilege query and was
reported as an unreachable database. That is the one case `fix_search_path`
exists to name, and it never reached the user. Verified against a local server
with `SET search_path = ''`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): say which of the two refusals a connection string hit

Making parameter names case-sensitive gave `unsupportedConnectionParam` two
reasons to refuse, and the single message explained only one. `?SslMode=` was
answered with "Windmill cannot store SslMode on a Postgres resource", which is
false twice over: sslmode is exactly what the resource stores, and the string
asks for nothing because Postgres rejects the URI. It now names the spelling
when the parameter is one we keep, and the storage limit otherwise.

The folder-list guard also still read the `resume` prop that `open(parked)` was
changed to stop trusting, so the resumed path now comes from whatever `reset`
was handed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): leave the Supabase organization unset when the lookup misses

Falling back to the first organization named one the seeded project is not in,
since `supabaseSummary` prefers `intent.org` over the project's own. Unset, it
falls through to the project's organization identifier — the right one, spelled
as a slug rather than a name.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: harden the import wizard and put it on the design system

Review fixes, then the parts of the wizard that were hand-built where the
design system already had an answer.

Correctness:

- Hub SVGs are sanitised with DOMPurify before `{@html}`. The earlier comment
  claimed the markup came from the hub's own icon package rather than user
  input, which the custom-URL feature makes false: the hub is whatever address
  the user typed.
- The run owns navigation while it is in flight. The stepper refuses to move,
  `beforeNavigate` cancels browser back/forward, and unmounting resolves a
  pending migration review so the executor cannot hang waiting on a component
  that is gone.
- The folder edited on the last step reaches the executor, so a retry after
  changing it imports where the field now says.
- `validateWorkspaceId` and the workspace-entry pair (`listUserWorkspaces` then
  `switchWorkspace`) are extracted, so the wizard and the real create form
  cannot drift on what an id is or on what entering a workspace means.

Design system:

- The destination tiles are `RadioCard`, which gains `showRadio` and a snippet
  `description`; the wizard turns the glyph off because the border and tint
  already say which one is picked. `RadioCard` now also carries `role="radio"`
  and `aria-checked`, which it had neither of, and marks its selection with
  `surface-accent-selected` — the token `FileExplorer`, `TriggersTable` and
  `RunnableRow` all use for the chosen row.
- Form labels follow `brand-guidelines.md` — sentence case, real `<label>`
  elements so the text focuses the field, Caption-styled errors — rather than
  one-off 11px uppercase tertiary text. They use the lighter secondary weight,
  since the fields arrive prefilled and the value carries the meaning.

Folder choice, restored and merged:

- Picking an existing folder came back for an existing-workspace destination.
  `FolderPicker` takes a `workspace` prop so it can list a workspace without
  switching to it, and resolves `whoami` there — its write flags came from
  `$userStore`, i.e. the wrong workspace, which rendered every real folder
  read-only and unselectable. A new workspace has no folders to choose between,
  so it is not asked.
- The progress list and the imported paths are one component: the paths hang
  off the import task that produces them instead of forming a second list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: address review — history, workspace restore, SVG styles

Two blockers and the findings around them.

- The wizard pushed history entries for navigations the user did not ask for.
  `folder` initialises to the project slug while the plan holds none, so the
  mirroring effect fired on mount and pushed a URL differing only by `&folder=`;
  pressing Back returned to the entry without it, which re-fired and re-pushed.
  Back could never leave step 3. `go` now takes `{ replace: true }`, used by that
  effect and by the step guard — the two navigations the page decides on its own.
  The comment claiming `go` replaced was written without checking that `goto`
  forwards to SvelteKit, which defaults `replaceState` to false.
- Undoing a run left the app pointing at the workspace it had just deleted:
  `#ensureWorkspace` switches in, `deleteCreatedWorkspace` deleted without
  switching out. The dead id was persisted on the next navigation, `getUserExt`
  then returned undefined, and the following reload logged the user out. The
  executor now remembers where the app pointed before it started and puts it back.
- `FORBID_TAGS: ['style', 'image']` on the hub SVGs. The profile allows both; an
  inline `<svg><style>` is document-scoped, so a hostile hub could restyle this
  page — including moving the wizard's own Import and Delete controls — and
  `<image href>` is a beacon. The doc comment asserted a guarantee the config did
  not deliver.
- The existing-workspace id is validated like the new one and encoded where it is
  interpolated into `/api/w/<ws>/...`; it arrives from the URL exactly as the new
  one does and ends up in `workspaceStore`.
- `AppConnectInner`'s two RadioCards get a `role="radiogroup"` wrapper, since they
  now carry `role="radio"` and a screen reader cannot place a radio without one.
- `FolderPicker` records a created folder against the membership it is reading, and
  before reloading, so a non-admin can re-pick the folder they just made in another
  workspace instead of finding it `(read-only)`.
- Step 3 shows trigger and data table migration counts once the export is fetched.
  The page this replaced showed them, and the warning underneath talks about
  triggers the user was never told about.
- First tests for the two pure modules: the workspace-id contract the wizard and
  the create form must not drift on, and the plan/URL round trip the whole wizard
  rests on.
- Doc fixes: the retry claim (the granularity is the task, not the item), the bench
  header, a fractional `?step=`, an empty name in the destination card, and the
  three copies of one rationale AGENTS.md asks to state once.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(frontend): pin which refusal a connection string gets

The two messages differ in what they ask the user to do, and the condition
choosing between them — whether the lowercased name is one the resource keeps —
is not visible from either call site. `Connect_Timeout` is the case that keeps
them honest: miscased *and* unstorable, so respelling it would not help and the
message must not suggest it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): hand a failed Supabase leg back to the page holding its run

Denial, a token error and a malformed callback all sent the user to
/resources whether or not a run was parked. Nothing else consumes the park, so
the run stayed in sessionStorage and sprang the wizard open on an unrelated
later visit instead. A parked run now lands on the data tables tab, where the
wizard resumes on the setup step and can authorize again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): let a run reuse the name of a row it could not take back out

`removeRow` reports `kept` when the undo cannot reach the server, so the row
this run wrote stays in the workspace config and comes back in `existingNames`.
The client-side name check then refused the retry on the run's own name, with
no way forward but a rename. The instance database name has carried the same
exemption since it was written; this is the data table name catching up.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): discard a variable check the wizard has moved on from

The post-await guard compared only the path, and the path is built from the
review step's fields -- so picking an existing resource stops the wizard minting
one without changing it. A check already in flight then answered for a branch
nobody was on, and a `true` disabled Finish over a path the run no longer
writes. The cleanup cannot help: it cancels a pending timer, not a live request.

Both sides of the await now ask the same question.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref to 483513b70979aa9497cab869837108d948449984

This commit updates the EE repository reference after PR #715 was merged in windmill-ee-private.

Previous ee-repo-ref: 8604b30a740c5620069208801a7ae50937b61977

New ee-repo-ref: 483513b70979aa9497cab869837108d948449984

Automated by sync-ee-ref workflow.

* feat: a setup step for what the import cannot bring with it

A project's data tables and credentials cannot travel with it: a data table is a
named database connection the workspace owns, and resource values are secrets the
hub never publishes — `importResourceStub` creates every one of them empty. The
wizard used to state that as a dead end. Mid-import it asked the user to cancel,
create the data tables by hand and start over, which for a *new* workspace was
every single time, since a new workspace has no data tables at all.

Step 4 replaces that. It appears only when the run leaves something undone, lists
what that is, and does it in place: a Postgres resource per missing data table
(one merged `editDataTableConfig` write, then the migrations), and the existing
resource editor for each credential. Skipping is allowed and says plainly which
parts of the project will not run.

It is self-sufficient from `workspace` + `slug` — it re-fetches the export rather
than reading the executor — so reloading on it works and the plan in the URL stays
the whole state. Rows are marked done rather than removed, with SaveButton's
confirmation flash, because a checklist line that vanishes when completed reads as
something going wrong.

Two things the step needed from elsewhere:

- `ResourceEditorDrawer` gained `onSaved`. `onRestored` fires only when an old
  version is restored, so a caller showing state derived from the resource had no
  way to know a save had happened — the row kept saying "missing token" after the
  token was filled in.
- The run now loads the destination's membership into `userStore`. The wizard's
  page is reparented out of `(logged)` and never gets that layout's `getUserExt`,
  so anything asking what the user may do reads "no user" and refuses.

`applyOneMigration` is exported for the same reason the step exists: the import
skips a migration whose data table is not configured, and this is where it is not
skipped any more.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat: set up data tables through the wizard, not a hand-rolled form

The setup step drove `editDataTableConfig` itself, which meant it could
name a table and record migrations but could not create the database
behind it — the case a brand-new workspace is always in. It now opens
`AddDataTableWizard`, which owns that whole path.

Four additive props carry what the import flow needs and nothing else,
so `DataTableSettings` is unchanged:

- `initialName` — the migrations only apply to a table of the name they
  target, so the wizard opens on it. Still editable.
- `modalTarget` — `#content` is the `(logged)` shell's scroll container,
  and the import page reparents out of it, so the portal would find
  nothing and the dialog never appear.
- `finishAlso` / `onFinishAlso` — running the migrations was invisible
  until it had already happened. It is now named on the final button
  ("Create data table and run migrations") and reported as the last row
  of the wizard's own checklist, failing there rather than silently.

Rows are marked done rather than removed, so the list still says what
was set up. Resources keep their card and swap "Fill in" for "Saved".
`Finish` is the primary and stays disabled until nothing is outstanding;
`Skip for now` sits beside it, and the info alert explaining the skip
turns into a success one when everything is configured.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: show each credential's own integration icon, and cut the data table blurb

The credentials list marked every row with the same key glyph, so the only
thing distinguishing them was the path. `IconedResourceType` renders the
provider's own mark from the resource type already on the row, falling back
to a generic box for types with no icon.

The data table explanation said "a data table is a database this workspace
owns" directly under a label reading "Data tables to set up", and "this
project ships with one it expects to find" directly next to the count that
says so. Both halves went; what a data table is *for* and what to do next
are what a first-time reader needs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: build the Google sign-in button from the design system

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HtYCxXEn2WujwVvh5aZRCa

* fix: qualify a data table FK target with its schema

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HtYCxXEn2WujwVvh5aZRCa

* fix: confirm before skipping an unconfigured data table

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HtYCxXEn2WujwVvh5aZRCa

* fix: show a loader while the wizard hands off to the workspace

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HtYCxXEn2WujwVvh5aZRCa

* fix: resume an import whose workspace was already created

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HtYCxXEn2WujwVvh5aZRCa

* refactor: draw the import run with SetupChecklist and ask before leaving it

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HtYCxXEn2WujwVvh5aZRCa

* fix: portal the setup step's confirmation above the data table wizard

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HtYCxXEn2WujwVvh5aZRCa

* fix: supply the three APIs the import wizard already calls

`AppConnectDrawer`, `ImportProjectStep` and `execution.svelte.ts` landed
calling into props and exports that were never committed alongside them,
so the branch did not type-check. Each half is here now:

- `AppConnectInner.fillPath` — connect into a resource that already
  exists instead of refusing the path. The import creates every resource
  as an empty stub, so without it the connect flow can only ever say
  "already exists, delete it or pick another path". Opt-in: unset, the
  flow still refuses to write over anything, which is what `ResourcePicker`
  and the resources page rely on.
- `ProjectContentBadges.contentSummary` — the badge counts as one line of
  text, for the import step's task row. Shares `kinds()` with the badges
  so a project cannot be counted two ways.
- `installProject.onMigrationsStart` — fires before the reviewed
  migrations run, which is the only signal that phase has begun; the
  import step draws them as their own checklist row off the back of it.

Also fixes the wizard wedging itself shut: `requestClose` set `dismissing`
and cleared it after awaiting the confirmation, so an `ask` that threw left
the flag set — and the backdrop, Escape and the close button all return
early on it, leaving a reload as the only way out. Now `finally`, plus a
reset on open, since a promise that never settles never reaches `finally`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: offer Connect wherever the connect dialog would actually work

The setup step decided a resource was connectable by looking only at the
instance's configured OAuth clients, while the dialog it opens also accepts
a provider the registry marks client-credentials-capable — those carry their
credentials per resource, so no superadmin has to configure anything. The
two disagreed for bitbucket, coupa, linkedin, servicenow, spotify, visma,
xero and zoho: the step showed "Fill in" where the dialog would have
connected.

Rather than copy the predicate, `oauthRegistry.ts` now owns it, and
`AppConnectInner` reads it from there. That folds in three lookups of the
same registry that had drifted apart inside the component — `registryEntry`,
`isCcCapable`, and a raw index at the connect-template site — so the sandbox
suffix rule is written once instead of twice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor: draw the project card's icons from the ones we already ship

The card fetched each integration icon from the hub as SVG markup, sanitized
it and injected it with `{@html}`. The hub renders those icons out of
`@windmill-labs/components` — this frontend's own package — so it was a
cross-origin round trip to get our own assets back, and it made the card
depend on a read that a hub with `API_SECRET` set refuses outright.

`hubAppIcon` resolves them through `appIconComponent` instead, so they are
components again: no fetch, no DOMPurify, no `{@html}`, and they paint on
first render rather than after a round trip. Integration icons now show even
against a gated hub; only the summary and the uploaded logo still need it.

The one thing the hub was doing for us was resolving `postgres` to the
`postgresql` mark, which its `aliasApp` bridges and our icon map does not —
so that single alias comes along, next to a note pointing at its counterpart.

`ImportProjectSummary.hub` goes with it: it existed to build icon URLs and
nothing read it afterwards.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: two regressions this branch introduced into shared drawers

Found auditing the files here that are used elsewhere in the app.

`AppConnectDrawer`: the guard added to stop the inner component being opened
twice compared the last resource type against the current one, and reset it
to `undefined` on close. The resources page opens the drawer with no resource
type, so both sides were `undefined`, the guard matched, and the second
opening never handed off — the type list came up empty. The drawer destroys
its content on close, so this hit every reopen. Now a flag armed per `open()`
call, which cannot collide with a resource type.

`ResourceEditorDrawer`: adding `onSaved` had turned the Save handler into
`await save(); closeDrawer()`, so the drawer stopped closing immediately and
waited for the write. `save()` catches its own errors and never rejects, so
that was pure added latency for all ten callers. It now starts the save,
closes as it always did, and awaits only to fire `onSaved`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: keep the destination through a first-time signup

Someone who follows a shared hub project without an account signs up, and the
OAuth callback sends a first-time user to onboarding — dropping the `rd` it
had already read out of localStorage. They finish onboarding in an empty
workspace with no sign of what they came to import, and have to go back to the
hub and click again. That is the path this feature exists for.

The callback now passes `rd` on, and onboarding's two exits honour it instead
of hardcoding `/user/workspaces`. Same-origin relative paths only: `//host` is
a valid URL that leaves the origin while still starting with `/`, so the guard
rejects it rather than bouncing a fresh account off-site.

Nothing changes for a signup without `rd`, which is every existing one.

Gets the user to the wizard with the project in hand; they still pick a
destination on step 1. Having onboarding create the workspace and hand into
step 3 is the larger version, not done here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: address review — username, name length, leaving mid-run

**The new-workspace username was never validated.** Step 2 shows the field when
the instance does not derive one, but neither the Continue gate nor
`planProblem` looked at it. `create_workspace` does not close that hole:
`nw.username.ok_or(...)` accepts `Some("")` and never runs the `VALID_USERNAME`
check `join_workspace` does, so a cleared field created a workspace whose owner
has an empty username, and a digit-first one was stored verbatim. Both now
refuse, using the same `validateUsername` the sibling creator has always run.

**The name length was unchecked**, so a >50-char name walked through two more
steps and failed at create. `WORKSPACE_NAME_MAX_LENGTH` sits next to the id
limit and `planProblem` checks it.

**Leaving mid-run did not stop the run.** The dialog promised "The import stops
where it is. Coming back to this link picks it up again", but navigating away
only unmounted the UI: the executor kept going, reached `done`, and called
`clearParkedImport()` — so returning to the link tried to create the workspace
again and failed with "already exists". Worse, the review drawer's teardown
resolved the pending review to `false`, meaning "skip the migrations", and the
orphan imported every item without the tables they need.

Nothing can abort a request already in flight — `installProject` takes no
signal — so `abandon()` stops the run at the next phase boundary and leaves the
workspace parked, and the teardown now resolves `'abort'`, which stops the
import rather than silently dropping the migrations.

Also drops a stale JSDoc above `hubAppIcon` still describing the fetch-and-
sanitize implementation that `ea31f73ed3` replaced.

Adds the coverage the review asked for: the parking decision at the end of a
run, and the two validation gates.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: address review round 2 — XSS, retargeting, abandonment, redirects

**Hub data-table names were inlined as raw HTML.** `skip()` built the
confirmation body as an HTML string, and `createAsyncConfirmationModal` renders
`children` through `createRawSnippet`. A `datatable_name` comes straight from
the hub export, and a hub is not necessarily ours — `hub_base_url` is an
instance setting — so one carrying an event-bearing element ran script in this
authenticated origin. Escaped. Same class as the round-1 SVG finding, in a
different sink.

**The setup step read unretargeted resource paths.** `installProject` rewrites
every resource into `f/<folder>/`, but the step re-fetched the raw export and
used its paths verbatim. Importing into a folder other than the slug made
`getResource` throw for every stub, the catch skipped them, and the step
reported "You're all set" over credentials nobody had filled. It now retargets
the same way the import did, and filters to the import folder — the containment
guard the installer applies, so a crafted export cannot name a path outside it
and get offered for editing.

**Abandoning only stopped between phases.** `installProject` takes a `stopped`
callback now, checked before every write loop, so leaving mid-run stops the
remaining items instead of just the remaining phases.

**A failed setup migration reported success.** `runMigrationsFor` swallowed the
error, so the wizard marked its "Run migrations" step done and closed over a
failure — leaving the data table name taken and no way back to retry. Rethrown,
which is what the wizard's checklist reads.

**`onboardingDestination` used a weaker redirect check.** `/\evil.com` passes
`startsWith('/') && !startsWith('//')` but WHATWG URL parsing resolves it to
another origin. Replaced with `toSameOriginRelativePath`, which already rejects
that, control characters and oversized values.

**Two workspace ids reached step 3 that the backend refuses:** a blank one (the
Continue gate never required `id.trim()`) and `global`, which
`check_w_id_conflict` rejects outright while `existsWorkspace` reports it free.

Also: `size="xs2"` → `unifiedSize="2xs"`, and two doc comments reattached to the
functions they describe.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: address review round 3 — both regressions from round 2

**`resume` never rejected a run from a different plan.** The seed computed its
tag from the plan being rendered, so `run.key === planKey` was true by
construction and the guard could not fire — the comment claimed the opposite.
Finishing an import into workspace X, stepping back to pick workspace Y, then
returning showed X's finished checklist against Y's plan, with a Continue
button, over an import into Y that never happened. `ImportExecution.planTag`
now carries the plan the run was made for, and the seed uses that.

**Abandoning mid-import still reported `done`.** `installProject` returns early
when `stopped` goes true, and it returns exactly as it does on success, so the
tail of `#import` could not tell the two apart: a run stopped after 3 of 10
items wrote `import: done — 3 items`, no error, `done = true`. Since the page
hands that run back on return, the primary button became Continue rather than
Retry and the seven skipped items were silently lost — breaking the promise the
leave dialog makes. The tail now checks the flag and leaves the run failed and
retryable.

`abandon.test.ts` was a hand-written copy of the parking decision, which is why
it guarded neither. It now drives a real `ImportExecution` with the install seam
mocked, abandons from inside the write loop (the only way it happens — `run()`
clears the flag on entry so a retry can proceed), and asserts `done`, the error,
and both parking outcomes. `planTag` is covered too: different destination,
different project, and that the editable folder does not change it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: address review round 4 — unreviewed SQL, premature finish, stale Back

**Setup ran hub SQL nobody had seen.** Step 3 reviews the migrations it can run
there, but the ones deferred to setup went straight to `applyOneMigration`
against whatever database the wizard was pointed at — which can be an existing
resource holding unrelated objects. Each unconfigured row now carries a
disclosure showing exactly what will run, before "Set up" runs it.

**Finish was live while the setup decision was still outstanding.** For a project
with migrations but no resources, `execution.done` exposed the button while
`listDataTables` was still in flight and `setupNeeded` was still false — clicking
in that window left for the workspace and skipped a step the answer, a moment
later, said was needed. It now reads "Checking…" and is disabled until the check
settles.

**A reload on step 4 turned Back into a re-import.** `resume` only carries the
page's in-memory execution, so after a reload Back mounted a fresh step 3
offering Import over a bundle already in — and on a new workspace, a create that
now fails because the finished run cleared its parking. Back exists only while
the page still holds the run, which excludes exactly that case.

**`validateWorkspaceId` over-rejected a fork named `global`.** It reaches the
backend as `wm-fork-global`, which is accepted; only the effective id is checked
now, so a plain `global` is still refused. Covered by a test.

**An abandoned run left the migrate row spinning.** It is appended once the
review settles and set running by `onMigrationsStart`; stopping before its loop
left it on `running` forever, reading as work still in progress on a run that
had stopped.

Also moves the `run()` contract back onto `run()`, and gives `ImportSetupRow` an
optional `extra` snippet for detail that does not fit on one line.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: close the other two routes back into a run-less import step

Round 4 gated the setup step's own Back button when the completed run was no
longer in memory, but that is the least used of three ways back into step 3, and
both reviewers landed on the same gap.

The stepper renders every earlier step as reachable, and `importIsRunning()` is
false after a reload, so its "Import" tab walked straight there. And `onFinish`
pushed step 4 over step 3, leaving the browser's own Back pointing at the same
place.

After a reload there is nothing to hand back: the executor was in memory, and a
clean finish clears the parking, so step 3 mounted with `resume` undefined and
offered a fresh run — re-importing a bundle already in (a wall of path
conflicts), or on a new workspace re-running a create that now fails as already
existing, with no Delete offered because that execution never made it.

`ImportWizardSteps` takes a `lowestStep`, which the page raises to 4 exactly
when the run is gone, and the step-3 → 4 transition replaces rather than pushes.

Verified against a real reload: the stepper stays on step 4 and says why, and
browser Back lands on step 2 with no runnable import.

Also adds the migration-phase abandonment assertion the review asked for — that
no task is left on `running` when a run stops.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test: make the migration-phase abandonment test actually reach it

It asserted over a branch it never ran. The mock `installProject` never called
`onMigrationsStart`, and with `migrations: []` in the export and
`reviewMigrations` returning nothing, `#import` never appended the `migrate` row
at all — so "no task is left running" was true because no task existed. The
comment was wrong too: the real `onMigrationsStart` fires at the head of the
migration loop, past every item loop, not at the start of the writes.

The mock now mirrors that order — item loops, then `onMigrationsStart`, then the
migrations, with `stopped` checked before each write — and a second hook lets a
test abandon after the row is running. The export ships a migration and
`reviewMigrations` returns it, so the row exists to be pinned, and the test
asserts it exists before asserting its status.

Checked by removing the fix: it fails with `expected 'running' not to be
'running'`, and passes with it restored.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor: ask the instance what exists instead of remembering it

The wizard kept a note in `sessionStorage` — "this run created workspace X" —
so a reload could tell that a create had already happened. A note is a second
copy of a fact the instance already holds, and it could outlive the workspace
it named: the comment on `createdWorkspace` said a parked id might point at a
workspace someone else made at that id afterwards, and that there was no way to
tell, because a workspace carries no discriminator.

It carries `owner`. It is set to the creator's email at `INSERT INTO workspace`,
`listWorkspaces` already selects it, and the generated `Workspace` type already
has it. So the question the note was answering can simply be asked:
`probeWorkspace` returns whether a workspace with the plan's id exists among
the caller's, and whether they own it. Ownership is what makes adopting one
safe — an id that exists but belongs to someone else is not this run's work.

`parking.ts` and its test are gone. Nothing in the wizard writes storage now:
the plan is in the URL, what exists is in the instance, and what is in flight is
in memory, which is where in-flight things belong.

`probe.ts` also carries the two reads the follow-up needs — which of the paths
an import would write are already there, and whether a migration's tables exist.
The second is the ground truth for "did this migration run", covering both paths
`applyOneMigration` takes: it records a migration when the data table has them
enabled, and otherwise runs the SQL as a job nothing remembers. The tables
outlive both. It returns `undefined` rather than `false` when it cannot tell,
since "not there" invites a caller to run the migration and "cannot tell" does
not.

Verified against a real reload mid-run: the second attempt makes no
`createWorkspace` call, one `workspaces/list` call, and carries on to the fetch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat: import only what the destination does not already have

A retry resent the whole bundle. Everything that had already landed came back
as "already exists" — nine failures over work that had succeeded, with no way
to tell those from the ones that genuinely failed. The same thing happened
importing into a workspace that already held some of the project.

`installProject` now takes `alreadyPresent`, checked after retargeting because
that is what the items will actually be called, and `probeImportedPaths` fills
it from the destination on every run. On a workspace the run just created the
answer is empty and nothing is skipped, so this costs four scoped reads and
changes nothing about a first import.

Skipping is not replacing. An item that is there is left exactly as it is —
the same promise `updateIfExists: false` already makes for a resource whose
value someone has since filled in.

`InstallResult` gains `skipped`, because "already there" is neither an import
nor a failure and reporting it as either is a lie. The checklist still lists
every item the project ships; a skipped one shows as skipped and says why. The
import row now counts the three outcomes separately — `8 already there` rather
than a green tick over `2 apps, 4 scripts, 2 resources` it did not write. That
last part needed the pre-run breakdown to stand down once the run has an
outcome of its own, or it went on claiming the import had happened.

Checked by removing the gate: two of the four new tests fail. Verified against
a real backend by re-importing Calendly into a workspace that already had it —
0 failures, 0 create requests, and the row reads "8 already there", where the
same run previously produced 9 conflicts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: skip triggers that are already in the destination

probeImportedPaths asked about scripts, flows, apps and resources but not
triggers, so a retry replayed every trigger create into an API that rejects
an existing path — reporting a failure for something already there, which
is the wall the presence probe exists to remove.

Triggers have no prefix-filtered list endpoint, so they cost one call per
kind; the probe only asks when the project actually ships triggers.

The presence set is now keyed by kind as well as path. The five kinds share
one f/<folder>/ namespace, so a trigger and a script may both be called
f/cal/sync, and a flat path set would let either one mask the other.

Also drops expectedPaths, which was exported and tested but never called.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xg8vXUuHCH3aRkf91sfxjx

* fix: don't let a failed migration read as a finished setup

Three linked gaps around the step-4 data table setup:

Finish setup was clickable while the step was still loading. Empty rows and
blanks made outstanding === 0, which reads the same as having nothing to do,
so a quick click left the wizard before the missing data table was even
discovered. Skip already guarded on loading; Finish now does too.

When the data table wizard's appended migration step failed, run.result kept
runSetup's successful verdict, so the primary action offered Done over a
failed row and closing raised no warning. The failure is now tracked apart
from run.result, and Try again re-runs only the appended step — re-running
the setup would ask for the table name it just took and be refused.

A failed row in the import step reopened the full wizard, which rejected the
name it had itself created, leaving no way back to the migration that
actually failed. Such a row now offers "Run migrations again" instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xg8vXUuHCH3aRkf91sfxjx

* fix: read the destination's real state instead of inferring it

Three ways step 4 could report work that had not happened:

AddDataTableWizard wrote through $workspaceStore while ImportSetupStep used
the workspace from the URL plan. The import page is reparented out of
(logged), so nothing re-runs the layout's workspace persistence; after a
reload the store still named the workspace the user came from. "Set up"
would then create the data table there and run the migrations in the
destination. The workspace is now a prop, defaulting to the store so every
other call site is unchanged.

load() marked a row done whenever the data table name existed. The wizard
creates the table and the migrations run after it, so a table can be there
with none of the project's tables inside it — and a reload rebuilds rows
from scratch, hiding the failure. It now asks probeMigrationApplied, which
already existed for exactly this question. An undefined answer ("cannot
tell") keeps whatever the row said rather than inventing an outstanding row.

A reviewed migration could fail in step 3 while the run still reported a
clean finish: the migrate row said failed, but `error` was set only from
item failures, and `error` is what offers Retry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xg8vXUuHCH3aRkf91sfxjx

* fix: keep the migration retry reachable after a reload

The retry-only action needed two things the reload did not have. load()
read the destination's data tables into a local set and dropped them, so
configuredNames was empty and the branch could not fire; it now seeds
configuredNames from the call it already makes.

And the branch keyed on the row saying `failed`, which only holds while the
failure is still in memory. A reload rebuilds every row from scratch, so the
same situation reads as `unconfigured`. It now keys on the data table
existing while its tables do not, which is the same state either way.

Without both, a reloaded failure sent the user back into the wizard, which
refuses the name it created — no way to reach the migration that failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xg8vXUuHCH3aRkf91sfxjx

* docs: state the constraint, not how the code got here

AGENTS.md: "Describe the code as it is, never its drafting history". Nine
comments across the wizard narrated what an earlier iteration did — "used
to remember", "The regression:", "would otherwise warn" — which says
nothing to a reader who never saw it. Each now states the durable reason
directly: what the code must hold to, and what breaks without it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xg8vXUuHCH3aRkf91sfxjx

* fix: page the presence probe, and keep a failed run retryable

probeImportedPaths called each list endpoint once. They paginate at 30 rows
by default, so it answered correctly for a small project and silently
under-reported a large one — every item past the first page went back
through a create call that rejects an existing path. It now pages at 100
until a short page, with a 100-page stop so an endpoint that never returns
one cannot loop.

And a run that finished with failures offered only Finish. `done` is what
the step reads as terminal, not `error`, so a failed migration left no way
to run the SQL again. Retry now sits beside Finish whenever the run reports
an error — beside rather than instead, so a migration that fails every time
cannot trap the user short of step 4.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xg8vXUuHCH3aRkf91sfxjx

* fix: don't offer to discard a data table that was created

Closing the wizard after a failed appended step asked "Leave without adding
a data table?" and warned that what ran had left things behind. Every part
of that is false when the setup itself succeeded: the data table exists and
works, and only its migrations did not run.

hasUnfinishedIntent() now asks only whether the setup succeeded. The import
step is the only caller that passes onFinishAlso, and it shows that failure
on its own row with a way to run it again, and will not let Finish through
while it stands — so closing loses nothing.

The in-dialog "Try again" is unchanged; it is still the direct retry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xg8vXUuHCH3aRkf91sfxjx

* fix: address round 8 — trigger kinds, migration retry target, unknown state

Four findings, two of them real defects in this branch's own work.

The presence key flattened every trigger kind into `trigger`. Each kind is a
separate table keyed on (path, workspace_id), so a workspace can hold a
schedule and an HTTP trigger both called f/cal/sync; whichever existed
answered for the other and the second was reported "already there" without
being imported. The key now carries the kind, which both sides already had.
projectInstall's own doc makes this argument for the five top-level kinds —
it just stopped one level short.

The wizard's in-dialog "Try again" ran runMigrationsFor(wizardFor), but
afterWizard() clears wizardFor as soon as the failed run reports, while the
dialog stays up. It resolved against no row and the step was marked done
over SQL that never ran. The target is now held separately, and an unknown
name throws rather than resolving — a resolved promise is what the appended
step reads as success.

settle() resolved "cannot tell" to done exactly on the reload it was written
for. A data table whose database is unreachable read as Configured and the
step said "You're all set" over a project whose apps fail on open. There is
now an `unknown` state that says so and still counts as outstanding. It also
asked for one full schema per migration; migrations for one data table all
target the same schema, so probeMigrationsApplied reads it once.

run()'s doc still described the pre-probe retry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xg8vXUuHCH3aRkf91sfxjx

* fix: address round 9 — abandon during the probe, and copy that outlived it

Abandoning while probeImportedPaths was in flight returned without settling
anything. `import` goes running before the probe is asked, so the checklist
kept a spinner on a run that had stopped, beside an enabled Retry and with
no explanation. The settling the post-installProject path already did is now
a helper both paths call.

Three pieces of copy still described the behaviour this branch replaced:
the resource alert said an existing path is "reported as failed" when the
probe now leaves it alone and reports it as already there; and the step-4
footer and skip confirmation both told the user to set up a data table that
the new `unknown` state means they already set up — only its schema could
not be read. Those two now branch, so the strong warning stays strong for a
data table that genuinely does not exist.

The presence-key doc named `trigger:http_trigger`; WorkspaceTriggerKind has
no such value. It is `http`, in the comment and in the two test mocks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xg8vXUuHCH3aRkf91sfxjx

* fix: never rerun SQL whose applied state is unknown

`unknown` covers two different unknowns, and this treated them as one. The
schema could not be read, or the SQL names no table `expectedTables` can
resolve — and the second is arbitrary published SQL, which may carry a
non-idempotent INSERT or ALTER. The row offered "Run migrations" and the
footer claimed rerunning was safe; both were claims this code cannot make.

An unknown row now offers "Check again", which re-reads and executes
nothing. That settles the case which actually recovers — a database briefly
unreachable — and leaves Skip, which states the uncertainty, as the way past
one that does not.

The partitions behind the copy also missed `failed` rows entirely: the
footer rendered a title with no body, and Skip described them as unreadable.
Both now group by what it costs the project — tables that are missing
(never created, or a migration that failed) against tables that could not be
verified — which is also what makes the sentences true: a failed row is
configured, so "this data table does not exist yet" was wrong about it.

Skip and the footer now read the same partition instead of each computing
one, so they cannot disagree again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xg8vXUuHCH3aRkf91sfxjx

* docs: the SQL disclosure should not promise a run that cannot happen

An unknown row's only action re-reads the schema; nothing executes its SQL.
The summary still said "Show the SQL this will run", which is the sentence
the previous commit removed from the footer for the same reason. On those
rows it now says what the SQL is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xg8vXUuHCH3aRkf91sfxjx

* fix: a row whose migrations are running does not offer "Set up"

Found by walking every branch on row.status rather than the ones I
remembered: `running` falls through to the catch-all action, which labelled
itself "Set up" in accent. Disabled, so nothing could come of it, but it is
the same label-outruns-state mistake the last rounds were spent on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xg8vXUuHCH3aRkf91sfxjx

* fix: never fill a path a resource of another type already holds

The presence probe matches on path, and a path says nothing about type. A
workspace resource of another kind sitting where the project wanted one of
ours was skipped as "already there", then read for missing fields against
the *project's* expected schema — so it looked like an empty stub, offered
Connect, and had its value replaced with credentials for a different
provider while keeping its own type. A working resource unrelated to the
import, destroyed.

Guarded at both ends. AppConnectInner checks the occupant's type before
updating, because `fillPath` only says "write into this path" and a caller
cannot be trusted to have checked. And the setup step records the conflict,
so the row explains that the project did not get the resource it shipped and
offers no action at all — every action there writes to that path.

Such a row is always listed, however full the occupant's value looks: it is
the only thing that tells the user something is missing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xg8vXUuHCH3aRkf91sfxjx

* fix: fail closed everywhere the import cannot tell

Codex was right that the occupant-type guard failed open: a getResource
that threw became `undefined`, which passed the mismatch test and left
filling enabled — so a transient read failure still overwrote the resource
the guard exists to protect. Only a read that succeeds and answers with
exactly this type now permits the write; a failed read, a missing type and
any other type all refuse.

That was the same "cannot tell, so proceed" this branch already fixed once
in settle(), so the rest of the wizard was swept for it. Two more:

findBlankResources dropped a row whenever getResource threw, on the
assumption that meant absent. Only a 404 means absent — and that failure the
import already reported. Any other error is a read that did not complete,
which says nothing about whether the credential needs filling; dropping the
row reports "all set" over one nobody filled. The row now stays and offers
no action, since none of them can be safe about a path this cannot read.

A resource type whose schema would not load left `required` empty, which
reads as "nothing missing" — so a half-filled resource passed as done. It
stays on the checklist; it just cannot name which fields are short.

The other four catches were checked and are already closed in the right
direction: probeWorkspace reports absent so the caller creates rather than
adopts, probeMigrationsApplied answers undefined which settles to a
non-actionable row, and afterWizard keeps whatever the run last said.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xg8vXUuHCH3aRkf91sfxjx

* fix: a refresh takes every field the fresh read decides

`refreshBlanks` merged only `missing` out of the new scan, so the two fields
added alongside it were left at whatever the row said before. Both reviewers
found the same seam from opposite ends: a resource that had just become
unreadable kept its old readable-looking row, and one that had come back
stayed blocked until a reload.

These fields describe what is at the path now, so the fresh read owns all of
them — and the branch that marks a row done clears them, because a row that
has left the blank list was read and is filled.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xg8vXUuHCH3aRkf91sfxjx

* fix: resolve the username in the destination, and lock a targeted name

Two P1s, both from the same earlier fix being half-done. Routing this
wizard's API calls through an explicit workspace left `$userStore` behind,
and that store describes the workspace the app is in. After a reload on
step 4 it names the workspace the user came from, so a resource path built
from it lands on `u/<someone-else>` inside the destination — failing an
ownership check, or for an admin, quietly putting database credentials in
another member's namespace. The membership is now resolved for the target
workspace, the way FolderPicker already did it.

And `initialName` was documented as "a starting point, not a lock" while
`onFinishAlso` targets that exact name. Renaming `main` to `other` created
`other`, ran the migrations against `main`, failed, and left a data table
nobody asked for. The field is locked when a caller passes follow-up work
bound to the name, and says why; without one it stays editable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xg8vXUuHCH3aRkf91sfxjx

* fix: resolve membership before seeding, and hold the name lock for the dialog

Two follow-ons to the previous commit, both where a value is read live that
should have been settled once.

The username was fetched in an effect while `open()` reset the wizard
immediately, so `defaultFolder()` ran against an empty username and seeded
`u/admin`. It was corrected only if `whoami` happened to win a race against
the folder list, and never if `whoami` failed — which is the case that
matters, since an admin would then save database credentials in another
member's namespace. `open()` now awaits the membership before reset, and a
destination whose membership cannot be read blocks setup outright rather
than guessing a path.

And the name lock read the live `initialName`, which is the caller's
`wizardFor` — cleared from `onDone`, which fires after a *failed* run too,
while the dialog stays up offering Back. The lock released exactly when the
user was most likely to edit the name, so the rename-then-retry path still
diverged from the migration target. It is captured at reset, for the life of
the dialog.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xg8vXUuHCH3aRkf91sfxjx

* fix: do not show the data table dialog before it knows the destination

`open()` became async so it could resolve the destination's membership
before seeding a resource path from it. But `openWizard` still set
`wizardOpen` first, and that is bound to the dialog's `opened` — so the
dialog was mounted, visible and clickable for the whole lookup, with the
username unresolved and `membershipFailed` not yet set. Setup reached in
that window writes exactly the wrong-namespace path the await was added to
prevent, and a late response could reset a dialog the user had already
touched or closed.

`open()` sets `opened` itself, once it has an answer. `wizardFor` alone
mounts the component, which is all `wizard?.open()` needs to exist.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xg8vXUuHCH3aRkf91sfxjx

* fix: honour the configured base path, and guard a second Set up click

Windmill can be served under a prefix (`paths.base`, from VITE_BASE_URL),
and four import entrypoints compared or emitted `/projects/import` without
it. Under a base of `/windmill` the real pathname is
`/windmill/projects/import`, so the layout's picker exemption and both login
redirect checks stopped matching and sent people through the workspace
picker — and the compatibility redirect emitted a path outside the base
entirely, which is a 404. All four are now built from `base`.

`Login.svelte` takes it from `$lib/base` rather than `$app/paths` because it
already did; both read VITE_BASE_URL, and importing the second name into
that file collides with the first.

And the previous commit left Set up clickable while `open()` resolves the
destination membership, deliberately — but with no guard, a second click
starts a second lookup whose `reset()` lands on the dialog the first one
opened, wiping fields already filled. The action is disabled while a dialog
is opening.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xg8vXUuHCH3aRkf91sfxjx

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-08-27 10:39:36 +02:00
Ruben Fiszelandrubenfiszel 52ca19e9ae chore(main): release 1.797.0 (#10848)
* chore(main): release 1.797.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
v1.797.0
2026-08-27 10:15:23 +02:00
Ruben FiszelandClaude Opus 5 69320b28f6 perf: index the suspended-job resume test instead of filtering it (#10863)
* perf: index the suspended-job resume test instead of filtering it

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SEUq14Wz4cC2NzcRyo6CNj

* fix: keep the legacy suspended index until the replacement is recorded

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SEUq14Wz4cC2NzcRyo6CNj

* perf: drop the redundant suspend_until column from the suspended index

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SEUq14Wz4cC2NzcRyo6CNj

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 00:39:03 +02:00
8b80b09f33 fix: restrict filesystem workspace storage to debug builds (#10864)
* fix: restrict filesystem workspace storage to debug builds

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7p2VbtYqaXHGaAskgwVk5

* chore: update ee-repo-ref to b58ad414b098d3d7787001a352bfbb13e43a335f

This commit updates the EE repository reference after PR #747 was merged in windmill-ee-private.

Previous ee-repo-ref: 1b4dada77a8fe2224579c643550c63b1ac2616de

New ee-repo-ref: b58ad414b098d3d7787001a352bfbb13e43a335f

Automated by sync-ee-ref workflow.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-08-26 23:35:44 +02:00
Ruben Fiszelandwindmill-internal-app[bot] f131c3920f fix: keep connection string query parameters under token auth (#10859)
* fix: keep connection string query parameters under token auth

* refactor: fold the database url parsing into one connect-options helper

* docs: state the narrower invariant on base_connect_options

* chore: update ee-repo-ref to 212cc7d61ec38580d4a70d9ac38d7a2cc9daf409

This commit updates the EE repository reference after PR #746 was merged in windmill-ee-private.

Previous ee-repo-ref: a15d08345d7e42526c28382079ad1f575a2d1674

New ee-repo-ref: 212cc7d61ec38580d4a70d9ac38d7a2cc9daf409

Automated by sync-ee-ref workflow.

---------

Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-08-26 23:08:36 +02:00
Ruben FiszelandClaude Opus 5 af15a73b8b chore: move the compose stack to postgres 18 (#10827)
* chore: move the compose stack to postgres 18

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ha6ovKdT9XRoj5FyVe7fEt

* fix: dump the whole cluster in the postgres 18 upgrade recipe

Windmill creates instance datatable, DuckLake and wm_fork_* databases in the
same cluster as windmill, so a single-database pg_dump followed by removing the
volume loses them silently. Dump the cluster with pg_dumpall instead, which also
carries the roles the RLS policies are granted to, with their passwords.

Also wait on the healthcheck before restoring, stop services generically rather
than by name, and ANALYZE after the restore.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ha6ovKdT9XRoj5FyVe7fEt

* fix: analyze every restored database and check the restore for errors

ANALYZE is per-database, so the sibling datatable/DuckLake/wm_fork_* databases the
recipe now restores were left with no planner statistics; vacuumdb --all covers
them. psql does not stop on error and the old volume is gone by that point, so
the restore needs an explicit grep rather than a trusted exit code.

Also note that logical replication slots are never dumped, so a Postgres trigger
reading a database in this cluster comes back disabled until it is re-saved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ha6ovKdT9XRoj5FyVe7fEt

* fix: drop the bootstrapped windmill database before the restore

POSTGRES_DB creates an empty windmill database, so the dump's own CREATE DATABASE
for it fails and its objects load into the entrypoint's database instead, keeping
the new cluster's encoding and collation rather than the dumped ones. Sibling
databases are created by the dump and so were never affected. Dropping it first
makes the restore reproduce the source cluster exactly, and leaves one expected
error instead of two.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ha6ovKdT9XRoj5FyVe7fEt

* docs: move the postgres 18 upgrade runbook out of the compose file

A step-by-step runbook in a config file needed corrections in three consecutive
review rounds, which is the argument for keeping it somewhere it can be fixed
once. The comment keeps only the constraint a reader has to know before touching
the mount, plus a link.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ha6ovKdT9XRoj5FyVe7fEt

* docs: point the postgres 18 upgrade note at windmill.dev

GitHub gists are owned by user accounts, never organisations, so a gist is the
wrong home for the only migration instructions every self-hosted operator gets.
The procedure now lives in the self-host docs page instead.

Depends on windmill-labs/windmilldocs#1704 merging and deploying first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ha6ovKdT9XRoj5FyVe7fEt

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 22:14:27 +02:00
GuilhemandClaude Opus 5 c04b570574 feat: keep a Hub project live while an update is under review (#10814)
* feat: keep a Hub project live while an update is under review

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018At6NKGa6cQP1zakMS686d

* fix: confirm before discarding a Hub update and document the route

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018At6NKGa6cQP1zakMS686d

* fix: bind the discard confirmation to the session that opened it

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018At6NKGa6cQP1zakMS686d

* fix: keep the old wording against a Hub without pending updates

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018At6NKGa6cQP1zakMS686d

* fix: say what the review lock actually blocks, in one alert

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018At6NKGa6cQP1zakMS686d

* feat: let a publisher cancel a Hub submission from the wizard

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018At6NKGa6cQP1zakMS686d

* fix: hide the cancel action on a Hub that cannot withdraw

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018At6NKGa6cQP1zakMS686d

* docs: describe startNewDraft for both Hub versions

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018At6NKGa6cQP1zakMS686d

* feat: warn when an update carries the published pipeline replay

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018At6NKGa6cQP1zakMS686d

* fix: base the stale-replay warning on changed content, not recordings

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018At6NKGa6cQP1zakMS686d

* fix: stop the stale-replay warning leaking across updates

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018At6NKGa6cQP1zakMS686d

* fix: clear the captured cascade when starting another update

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018At6NKGa6cQP1zakMS686d

* fix: abandon an in-flight cascade when starting another update

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018At6NKGa6cQP1zakMS686d

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 20:47:24 +02:00
AlexRV12andClaude Opus 5 e38c449007 fix: recover from unresolvable AI session links instead of a dead end (#10854)
* fix(frontend): delete the open AI session by its stable id

`session` is a $derived lookup into the session list, so it resolves to
undefined as soon as the entry is dropped. Nothing reads it after the
removal today, so this is latent rather than a live bug, but the delete
handler is async and the id is already available as a prop that stays
valid for the whole teardown.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): recover from unresolvable AI session links instead of a dead end

Sessions live only in IndexedDB, keyed in the URL by `session_name`, so a
link that resolves in one browser resolves to nothing in another. That hit
a dead-end "Session not found" page whose only way out was a button — and
it also caught a session of the user's own that had simply never been
touched, since an untouched session is never persisted.

Redirect instead: land on an empty session (reusing one that already
exists, else creating one), replace the URL so back doesn't return to the
broken link, and explain the swap in one dismissible notice above the
composer. Never land on an existing conversation, which would read as a
successful load.

The notice explains one arrival, so it is spent the moment the arrival
ends: a first message sent, the session deselected, or the page left.
Deleting the open session removes it before the handler's own navigation
lands — across HTTP when a fork goes with it — so that teardown is gated,
otherwise recovery claims the gap and reports the session the user just
deleted as missing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 20:46:31 +02:00
b8bf539c3f fix(cli): keep svelte component styles in the raw-app bundle (#10838)
* fix(cli): keep svelte component styles in the raw-app bundle

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test: fold svelte style guard into the plugin test file

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: record the editor-parity constraint on the svelte css option

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(cli): pin esbuild's service cwd before any test file chdirs

esbuild's node API captures process.cwd() when its module is first
imported and spawns its service with that cwd on every (re)start.
createBundle stops the service after each bundle, so the cwd is reused
across the whole run.

Several test files chdir into a temp dir and delete it afterwards. The
first one to bundle therefore pinned the service to a directory that
stopped existing, and the next test to reach esbuild died with

  The service was stopped: ENOENT: no such file or directory,
  posix_spawn '.../@esbuild/linux-x64/bin/esbuild'

The binary is present; ENOENT is posix_spawn rejecting the missing cwd.

Which file tripped it depended on bun's readdir order, so renaming an
unrelated test file was enough to surface it. Importing esbuild from the
preload pins the service to a cwd that outlives the run, independent of
file ordering.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G88YF3sZFnJZUvTLVjqhZc

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2026-08-26 20:24:34 +02:00
hugocasaandClaude Opus 5 ffdf17ef8d fix: force HTTP router rebuild on trigger-change notification (#10849)
* fix: force HTTP router rebuild on trigger-change notification

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: coalesce http trigger change events into one forced rebuild

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: retry the coalesced http router rebuild when it fails

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: mark http routers stale when a forced rebuild fails

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: keep the router invalidation across an in-flight rebuild

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 08:23:25 +02:00
GuilhemandClaude Opus 5 72763c9ba5 tighten spacing between login email and password fields (#10811)
Claude-Session: https://claude.ai/code/session_01BmEVHF8afJmgv6saBRYN6w

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 00:49:48 +02:00
hugocasaandClaude Opus 5 46c363ffa4 fix: require admin on workspace tarball settings export (#10817)
* fix: require admin on workspace tarball settings export

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: name the refused flag in the settings export error

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 00:49:13 +02:00
GuilhemandClaude Opus 5 665f83e1f4 fix(frontend): operator menu opens on hover, pins on click (#10824)
* fix(frontend): operator menu opens on hover, pins on click

The operator hamburger synthesized a trigger click on every mouseenter, so
melt toggled the menu: re-entering an open menu closed it, and a real click
after a hover-open closed it too.

Hover now opens the menu only when closed and closes it 150ms after the
pointer leaves; the portaled content carries the same handlers so moving
between button and list keeps it open. A click is intercepted in the capture
phase: when hover already opened the menu the click is swallowed (melt would
otherwise toggle it shut) and pins it instead, so it stays open until a click
outside or on the trigger.

Opening and closing both go through a synthetic click on the trigger because
melt's menubar renders content only when rootActiveTrigger is set, which only
the trigger's own click handler does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FwLAoY7s9iPkYPGcmnw6UC

* refactor(frontend): move hover-open/pin into Menu's openOnHover prop

The hover machinery duplicated what meltComponents/Popover.svelte already
offers as openOnHover. Menu.svelte owns both the trigger wrapper and the
content div, so the grace timeout, the pin flag and the synthetic trigger
click belong there rather than in the consumer.

OperatorMenu is back to its original markup plus `openOnHover`, and the other
Menubar users can opt in. Popover keeps its own implementation: it is built on
createPopover, not the menubar, and does not need the trigger-click detour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FwLAoY7s9iPkYPGcmnw6UC

* fix(frontend): show the keyboard highlight on operator menu rows

sidebarClasses.hoverBg only reacts to the pointer, so rows styled with it
alone stayed transparent while melt moved data-highlighted through them:
arrow keys walked the menu invisibly. Affected Home, Runs, Schedules and
Tutorials (MenuLink), plus Account settings, Switch theme and All workspaces.

MenuLink adds the highlight only when it is rendered as a menu item; the
sidebar and settings-menu call sites pass no `item`, so nothing changes there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FwLAoY7s9iPkYPGcmnw6UC

* fix(frontend): one highlight state per menu row, accent for the selected one

Menu rows carried a hover rule and a data-highlighted rule at once. Melt moves
data-highlighted with the pointer as well as the keyboard, so the hover rule
was a second, independent state: the row under the pointer and the row the
arrow keys had reached both lit up. Menu rows now style data-highlighted only.
"More triggers" keeps its hover rule — it is a plain div, not a melt item, so
it never receives data-highlighted.

The selected row also painted bg-surface-hover, making the current page
indistinguishable from a highlight. It now uses the accent pair the rest of
the app uses for selection, bg-surface-accent-selected + text-accent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FwLAoY7s9iPkYPGcmnw6UC

* fix(frontend): make "More triggers" a real menu item

It was a hand-rolled <div role="button" tabindex="0">. Melt collects the rows
arrow keys walk with querySelectorAll('[data-melt-menu-id="<menuId>"]'), an
attribute only the item builder stamps on, so the row was skipped — and its
tabindex was no help either, since Tab inside an open menu is intercepted to
close it.

It is now a MenuItem. Melt closes the menu on item click unless the click is
defaultPrevented, and Svelte delegates onclick to the root, which runs after
melt's own listener, so the toggle sits in a capture handler on a wrapper
where it reaches the event first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FwLAoY7s9iPkYPGcmnw6UC

* Revert "selected sidebar row on the accent tokens"

sidebarClasses drives the whole sidebar and SessionPicker, not just the
operator menu; restore selectedBg/selectedText to their previous values.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FwLAoY7s9iPkYPGcmnw6UC

* feat(frontend): open the operator menu below the hamburger

Menu defaults to right-start, which put the operator menu alongside the
trigger and over the page header. bottom-start drops it under the hamburger,
left-aligned. Set on this menu only; the shared default is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FwLAoY7s9iPkYPGcmnw6UC

* feat(frontend): show the operator menu trigger as selected while pinned

Nothing distinguished a pinned menu from one that is merely following the
pointer, so a click gave no feedback. Menu hands `pinned` to the triggr
snippet, and the operator hamburger keeps sidebarClasses.selectedBg while it
holds — the tint that hover gives it, now persisting after the pointer leaves.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FwLAoY7s9iPkYPGcmnw6UC

* refactor(frontend): address Codex/Claude review on the menu hover changes

Reuse debounce from $lib/utils for the hover grace period instead of a
hand-rolled timer, matching how Popover implements the same delay.

Give the "More triggers" capture wrapper role="none" so it doesn't sit
between role="menu" and role="menuitem" as an unlabelled node, and spell out
in the comment why the listener has to be on an ancestor rather than on the
item itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FwLAoY7s9iPkYPGcmnw6UC

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 00:48:51 +02:00
Diego Imbert 07c77ead74 feat(frontend): flag the fork-compare datatable schema diff as legacy (#10829)
* feat(frontend): flag the fork-compare datatable schema diff as legacy

* fix(frontend): make the legacy datatable diff alert copy match the opt-in flag
2026-08-26 00:48:28 +02:00
hugocasaandClaude Opus 5 78331fda8b fix(frontend): key the GitHub App installation selector on installation_id (#10831)
* fix(frontend): key the GitHub App installation selector on installation_id

The GitHub Account ID dropdown used `account_id` as both the option value
and the lookup key. A workspace can hold several installations for the same
org (re-installed, or added from another workspace), so `.find()` resolved to
whichever came first: picking the live installation could hand back a stale,
token-errored one whose `repositories` are empty, leaving the repository
dropdown blank. `RepositorySelector`'s pagination matched the same way and
appended the wrong installation's page.

Both now key on `installation_id`, and the dropdown appends the installation
id to the label only for orgs that appear more than once. Switching
installation remounts `RepositorySelector` and clears the selected
repository, so its loaded pages no longer carry over.

Fixes WIN-2448

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): shorten the duplicate-org installation label

Drop the "installation" word from the disambiguating suffix: the id alone
already tells the two entries apart, and it keeps the errored variant short
enough to read at a glance.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 00:48:12 +02:00
AlexRV12andClaude Opus 5 6b73145e72 fix(frontend): follow the operating workspace in step input forms (#10834)
* fix(frontend): follow the operating workspace in step input forms

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): rethrow auth errors and wire remaining variable pickers

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(frontend): use runed watch for picker workspace reloads

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 00:47:46 +02:00
Diego ImbertandClaude Opus 5 8a6dc27236 feat: configurable expiry for presigned s3 public url signatures (#10835)
* feat: configurable expiry for presigned s3 public url signatures

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015JdZFeMXLGfeFNiQgx9QvA

* fix: describe expiry_secs clamping in the spec and pin the bounds in a test

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015JdZFeMXLGfeFNiQgx9QvA

* fix: omit null expiry_secs from the python sdk sign request

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015JdZFeMXLGfeFNiQgx9QvA

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 00:47:24 +02:00
hugocasaandClaude Opus 5 9fa8159ad1 fix: migrate slack resource-connect oauth to v2 (#10836)
* fix: migrate slack resource-connect oauth to v2

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: keep slack scopes one per entry, as every other provider does

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 00:41:15 +02:00
Ruben FiszelandClaude Opus 5 4658224592 fix(debugger): parse bun 1.4's UUID inspector token (#10828)
* fix(debugger): parse bun 1.4's UUID inspector token

Bun 1.4 changed the inspector URL's token to a hyphenated UUID. The stderr
scraper matched `[a-z0-9]+`, so it stopped at the first hyphen and connected to
a truncated path, which the inspector answers with 404. Every TypeScript debug
session has failed to attach since the 1.4.0 bump, taking the windmill-extra
integration tests with it.

Match the whole path, and only once its line is newline-terminated: a stderr
chunk can end mid-URL and would otherwise be read as a complete, truncated URL.

A close before the handshake completes is now reported as the connection
failure it is, rather than as a finished script, and the debuggee is reaped -
--inspect-wait blocks until a debugger attaches, so a failed attach leaked a bun
process per session.

On the test client, queue events that arrive before their waiter registers: the
server sends 'initialized' immediately behind the 'initialize' response, which
the client could drop and then time out waiting for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XMizaQRcnWRd79t5wWhjBN

* fix(debugger): keep the first terminated event's result on launch failure

A socket that drops after the handshake opens but mid-command-sequence reports
the termination from onclose, carrying the script result, and then fails the
launch. Sending a second terminated from the failure path overwrote that result
with an error-only event. Guard the send the way every other emit site in the
file does, leaving the reaping unconditional.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XMizaQRcnWRd79t5wWhjBN

* fix(debugger): report an inspector drop during setup as the failure it is

The setup commands run over an open socket and none of them reject when it
drops - sendInspectorCommand only has its own timer - so a drop between the
upgrade and Inspector.initialized was reported as a clean termination, and the
error surfaced up to 10s later or, once the duplicate was guarded, not at all.
Draw the line at execution actually starting rather than at the socket opening,
so those failures terminate with the connection error, immediately and once.

Pair the "Failed to start Bun" output with the terminated event it explains,
so a run that already reported its result cannot also be told it failed to
launch.

Prove the inspector URL complete with whitespace rather than an end-of-line:
trailing text on the banner line would otherwise stall the parse for 10s.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XMizaQRcnWRd79t5wWhjBN

* fix(debugger): mark execution started only once the start command is answered

Inspector.initialized is what starts the script, so setting the flag before
awaiting its reply left a drop during that round trip looking like a clean
termination - the same silent failure, narrowed to one command. Its reply
precedes any close on the socket, so the continuation still runs before onclose
and a real run is not misread as a failure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XMizaQRcnWRd79t5wWhjBN

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 00:38:09 +02:00
Ruben FiszelandClaude Opus 5 5dc43c400a report the EE gate instead of a 500 on restart flow at step (#10846)
Claude-Session: https://claude.ai/code/session_01CbayDTXcGCTYuE9m56BRag

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 00:31:21 +02:00
Ruben FiszelandClaude Opus 5 41d111bf38 chore: emit only line tables for workspace crates in dev builds (#10845)
* chore: emit only line tables for workspace crates in dev builds

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UQiM8pqagY19bnWiMebwGa

* docs: correct the CI comments that pinned profile.dev at debug = 2

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UQiM8pqagY19bnWiMebwGa

* docs: scope the windows debuginfo comment to the crates that job builds

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UQiM8pqagY19bnWiMebwGa

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 00:06:51 +02:00
Ruben Fiszelandrubenfiszel 0f3d884c6f chore(main): release 1.796.0 (#10810)
* chore(main): release 1.796.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
v1.796.0
2026-08-24 22:42:33 +02:00
Diego ImbertandClaude Opus 5 3b2a6d7604 feat(datatables): add a down migration from the migration viewer (#10812)
* feat(datatables): add a down migration from the migration viewer

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ha889ogajX9jbqmwTaF8kD

* fix(datatables): refuse an empty down migration and keep the saved one visible

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ha889ogajX9jbqmwTaF8kD

* refactor(datatables): use unifiedSize on the new buttons and fix the lock comment

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ha889ogajX9jbqmwTaF8kD

* fix(datatables): make the add-down exemption atomic against concurrent additions

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ha889ogajX9jbqmwTaF8kD

* fix(datatables): re-test the whole observed row when an upsert skips the lock

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ha889ogajX9jbqmwTaF8kD

* fix(datatables): re-test the observed row even when none was read, and sync the down draft on the leading change

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ha889ogajX9jbqmwTaF8kD

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 22:36:32 +02:00
hugocasaandClaude Opus 5 2906504125 feat: add instance setting to mute zombie job restart alerts (#10813)
* feat: add instance setting to opt out of zombie job restart alerts

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: preserve explicit false for default-on boolean instance settings

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor: invert zombie restart alert setting to a mute flag

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 22:36:21 +02:00
AlexRV12andClaude Opus 5 93081e255f fix(frontend): mint string password secrets in the operating workspace (#10815)
* fix(frontend): mint password secrets in the operating workspace

A `password: true` string argument is rendered by PasswordArgInput, which
mints an ephemeral secret variable on the first keystroke and rebinds the
argument to `$var:<path>`. It minted into `$workspaceStore` — the globally
active navigation workspace.

Session editors operate on a different, possibly forked workspace without
switching `$workspaceStore`, and thread that operating workspace explicitly
as a `workspace` prop. When the two diverged the secret landed where the
user was merely looking while the job ran elsewhere, and the backend failed
with `Variable not found`.

Add the `workspace` prop to PasswordArgInput and thread it through every hop
between a form mount and the minting field, plus the entry points that supply
it. Track `mintedIn` so updates target where the variable actually lives, and
re-mint when the operating workspace moves after a path already exists.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): keep a password field consistent with its argument

A parent can replace the whole args object without remounting this field —
previewing a saved input, say — leaving `path` and `password` describing a
secret the argument no longer points at. Minting from them then copies the
old plaintext over the replacement, and the replacement is lost.

State that rule once as `argReplaced` and gate every mint on it. The
replacement can also land while the create is in flight, so the bound value
is captured before the request and re-checked after it resolves; the variable
that mint produced was never referenced, so it is deleted outright. A mint
that ends without binding re-seeds `password` from what the argument now
holds, so the field stops displaying a secret that will not be submitted and
a later workspace move cannot re-mint the stale plaintext. `updateValue`
returns early before anything is minted, since its 404 retry would otherwise
bind over a replacement it cannot see.

A failed initial mint now raises a toast rather than passing silently, which
also removes the component's last unhandled rejection.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(frontend): guard workspace forwarding to PasswordArgInput

Every hop between the form a caller mounts and the PasswordArgInput that
mints the secret must forward `workspace`, and so must the entry points that
supply it. A hop that drops the prop falls back to the navigation workspace
while the top-level case keeps passing, and no typechecker catches it because
every hop declares `workspace?: string | undefined`.

The forwarded expression is checked rather than the prop's presence, so
`workspace={$workspaceStore}` and `workspace={undefined}` fail. Two ways the
scan could stop guarding without failing are asserted too: an unterminated
mount raises instead of swallowing the rest of the file, and the number of
mounts parsed must equal the number of tag occurrences, so a mount written
inline rather than at the start of a line fails loudly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): list and create variables in the operating workspace

* fix(frontend): surface and bound a failed recovery mint

* test(frontend): end a mount at the first line closing it

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 22:36:07 +02:00
Diego ImbertandClaude Opus 5 25a3e6ea7a fix(ai-chat): keep the composer usable while a question is pending (#10816)
* fix(ai-chat): keep the composer usable while a question is pending

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D2hyAfRT2aFF7uswsdTodL

* fix(ai-chat): keep a typed answer when the question's resolver is gone

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D2hyAfRT2aFF7uswsdTodL

* fix(ai-chat): only advertise the answer affordance on a live question

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D2hyAfRT2aFF7uswsdTodL

* style: trim the pending-question rationale comments to the 4-line cap

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D2hyAfRT2aFF7uswsdTodL

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 22:35:54 +02:00
GuilhemandClaude Opus 5 29c311ab31 fix: qualify foreign key targets in generated datatable migrations (#10821)
* fix: qualify foreign key targets in generated datatable migrations

The schema API reports a foreign key's target as a bare table name when it
lives in the same schema as the table declaring it. Emitted verbatim that
becomes `REFERENCES tickets (id)`, which Postgres resolves against
search_path — and a migration's own schema is never on it, so applying it
fails with `relation "tickets" does not exist` and the whole transaction
rolls back. Nothing is created; the project imports with no tables.

qualifyFkTarget resolves the target the way the FK closure does: the
declaring table's schema first, then any schema holding that table. The
REFERENCES clause is now quoted per part, so a qualified target survives
identifiers that need quoting; the constraint name is still built from the
unquoted value, so the pg_constraint guard still matches what it creates.

`quoteTarget` is opt-in, so alterTable.ts — the only other caller of
renderForeignKey — is byte-identical.

Reproduced and verified against a real data table: hub.windmill.dev's
published helpdesk migration fails as above, and the same SQL with the
target qualified creates both tables and the constraint.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xg8vXUuHCH3aRkf91sfxjx

* fix: resolve bare foreign key targets in the declaring schema

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wq66AbLqo4c5ukWJSc4x4t

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 22:35:21 +02:00
hugocasa 541b6c8496 fix: keep ai chat messages when leaving the page mid-generation (#10809)
* fix: persist ai chat turns mid-generation so leaving the page keeps them

* fix: stop chat checkpoints once the turn commits, keep streamed text visible

* fix: checkpoint streamed answers as they grow and keep half-run tool batches

* fix: checkpoint text as received so a backgrounded tab keeps capturing

* fix: keep buffered tool screenshots in mid-batch chat checkpoints

* fix: decide committed-text at the flush site, condense checkpoint comments

* fix: checkpoint only live streamed text, never text the parser owns

* fix: don't swap the chat transcript out from under a running turn

* fix: close the pre-loading window in the conversation-switch guard
2026-08-24 22:30:45 +02:00
Ruben FiszelandClaude Opus 5 8dbd12ecc1 fix: patch sqlx so a cancelled BEGIN cannot poison a pooled connection (#10823)
* fix: patch sqlx so a cancelled BEGIN cannot poison a pooled connection

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qzqmh52NU8fB9RBQNNkJGt

* test: drop the migration run and fixed sleep from the sqlx patch guard

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qzqmh52NU8fB9RBQNNkJGt

* test: ignore the sqlx patch guard by default and point at it from where sqlx is changed

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qzqmh52NU8fB9RBQNNkJGt

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 22:29:44 +02:00
hugocasaandClaude Fable 5 9c557859c5 feat: AI agent evals: datasets, scored runs and comparison (#10633)
* feat: eval datasets and standalone runs for reusable AI agents

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat: agent eval drawer with case editor, runs and capture entry points

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: document AI agent eval datasets and standalone runs

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: say how many eval cases the list is not showing

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: address review findings on eval datasets

- keep an edited case's conversation and tool inputs: serde(flatten) silently
  drops Box<RawValue> fields, so the update payload is spelled out
- remount the case editor per case so one case's turns cannot leak into another
- require jobs:read / flow_conversations:read on the capture endpoints, which
  UserDB does not gate by token scope
- take the dataset lock in create and update so a delete cannot be undone by a
  concurrent metadata write, and delete cases before metadata
- load more cases beyond the first page, and stop capping the agent picker
- record that the version stamp is taken at enqueue, not at resolution

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: address round-2 review findings on eval datasets

- block operators from dataset and case writes
- pass the editor's operating workspace through the drawer and the capture
  request, instead of assuming the navigation workspace
- discard superseded case-list responses so switching datasets cannot land the
  previous dataset's cases
- reject a dataset without a case_id (or vice versa) rather than running an
  inline case under a dangling association
- run unsaved edits inline instead of silently running the stored case
- surface the API error body on a failed run
- fetch dataset metadata concurrently when listing
- $bindable() without a default on the optional open prop
- correct the permission and enqueue-time-version wording in the docs

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: run an untouched saved case by reference again

The editor writes back keys the stored case omits, so comparing the raw objects
reported every unedited case as edited: the run went inline and lost the
dataset/case stamp its history depends on. Compare a normalized form, and pin it
with a test. Also scope the history query to the drawer's workspace and drop
superseded responses.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat: show a dataset's cases as a table, and fix round-4 review findings

The case list showed one case at a time with no overview. It is now a table with
the case, where it was captured from, and its last run — the last-run column is a
single jobs query on the path stamp rather than a request per row.

Review fixes in the same file:
- keep the edit baseline on the selected case rather than looking it up in the
  loaded page, so a case beyond page 1 is not treated as unedited and run stale
- release the loading state when a superseded case load returns early
- reload every loaded page after a write instead of collapsing to page 1
- last remaining 'resolved to' wording in the version tooltip

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat: run a dataset as an experiment, with scorers as runnables

An experiment runs every case of a dataset against one subject and records the
exact case set it executed, so a result set stays reproducible while the dataset
keeps changing.

Each case runs as its own small flow — the agent, then a step per scorer — so a
case keeps the run stamp, history query and trajectory view a single run already
has, and scorers need no orchestration of their own. Results are read back per
step by node id rather than by walking a nested loop's status.

A scorer is any runnable taking (input, output, expected): a script, a flow, or a
reusable agent used as a judge. A judge is prompted with the case and the answer
as one JSON message; a script or flow receives them as named arguments. Scores
accept a bare number, a boolean or {score}.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat: results table for an experiment, with scorer columns

One row per case: status, the agent's answer, and a column per scorer, with the
mean per scorer above the table and a link into each case's run for its
trajectory. Averages skip cases a scorer produced no number for — counting a
missing score as zero would read as a regression.

The drawer's left pane becomes Cases / Results, and Results carries the scorer
picker and Run dataset.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat: compare an experiment against a baseline

Per-scorer deltas on each row and on the mean, and a filter down to the rows that
regressed. Rows join by case id, so a case added after the baseline ran has no
delta instead of counting as a change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: address round-5 review findings on experiments

- match scorers by label when diffing two experiments; joining by array position
  subtracted one scorer from another whenever the scorer sets differed
- report a row's status from the case job, not the agent step, so a case whose
  scorer failed no longer reads as a success
- delete a dataset's experiments with it: they hold copies of its cases, and a
  recreated dataset of the same path would have exposed them
- select the experiment that Run dataset just started instead of leaving the
  table on the previous one
- expected is scored now, so stop describing it as having no consumer

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: address round-6 review findings on experiments

- hold the dataset lock across an experiment launch, so a delete landing between
  reading the cases and writing the experiment cannot recreate the deleted
  dataset's inputs
- match scorers between experiments on kind and path, not on label: labels
  default to a path's last segment, so f/a/quality and f/b/quality compared
  against each other
- average mean deltas over the cases both runs scored; comparing each run's own
  average reported a regression from a case the baseline never ran, with no
  regressed row to point at
- openapi: the row status is the job's, which is also canceled/skipped; runEval
  takes scorers; the update-case body no longer advertises source, which the
  handler deliberately ignores
- record why the experiment prefix cannot reach a sibling dataset

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: address round-7 review findings on experiments

- release the dataset lock for the push loop and retake it for the write,
  re-checking the dataset still exists: holding it across the whole launch made
  every capture and case edit on that dataset 409 until the last job queued
- assemble experiment results with bounded concurrency; a 100-case, 3-scorer
  experiment was 400 sequential lookups, each itself several queries
- clear the baseline when it becomes the selected experiment, which was
  comparing a run against itself and reporting zero deltas
- take the header mean over the same cases as its delta while comparing, so the
  two numbers beside each other describe the same set
- a canceled or skipped case is no longer the same grey dot as a running one

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: address round-8 review findings on experiments

- verify the dataset's identity, not just its existence, before recording an
  experiment: the path can be deleted and recreated during the push loop, and
  the experiment holds copies of the old dataset's cases
- give the recording lock a longer budget than a case edit, since its jobs are
  already queued and giving up strands them, and say so when it fails
- keep score lookups sequential within a case: nesting two bounded streams
  multiplied into 32 in-flight queries against a 50-connection pool
- clear a baseline that no longer belongs to the loaded experiments, so
  switching datasets does not leave comparison mode on with nothing to compare
- keep a scorer's own mean when the baseline never ran it, instead of blanking a
  column full of numbers
- EvalCaseDraft.expected no longer claims nothing scores it

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: do not trust an experiment's job ids, and require write to record one

Experiment objects live in workspace object storage, which a script can write
directly, and results are read on the unrestricted pool — so a forged experiment
naming another flow job returned output the jobs API would have refused. Only
jobs this server stamped with that experiment's id are read now.

Also from round 9:
- recording an experiment requires write on the dataset, not read: it persists
  into the dataset's namespace and its shared list
- clear the results table when the selection changes and surface a failed load,
  instead of labelling the previous experiment's numbers as the new one's
- a storage fault is no longer reported as a deleted dataset
- the lock-timeout message at the recording site no longer says to retry, which
  would run the whole dataset again on top of the jobs already queued
- ExperimentRow.status documents canceled and skipped

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: bind the experiment trust check to the requested dataset

The previous check matched jobs on the experiment id alone, which the stored
object supplies — so copying another dataset's experiment JSON under a readable
key carried its jobs' output along with it. A job is now only read if it was
stamped for this experiment *and* for the dataset the caller's read access was
checked against, and an experiment that names a different dataset is not served
from this key at all.

Also from round 10:
- add the .sqlx entry for that query; without it every SQLX_OFFLINE build failed
- serve results over GET: as POST the route-scope middleware classified a read
  as ai_evals:write, locking read-only tokens out of their own results
- clear the selected and baseline experiments synchronously when the dataset
  changes, so the previous dataset's id is not requested under the new one

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: address round-11 review findings on experiments and scorers

- give scorers the whole case input, not just the message: an answer that came
  from attachments or a replayed conversation could not be judged on it
- accept a judge's boolean and structured {score} answers, including stringified
  ones, and pin every documented scorer shape with a test
- record an experiment for the cases that did launch when a later push fails,
  instead of leaving those jobs running with nothing to attribute them to
- do not capture a preview parent's synthetic runnable_path as a host flow; the
  saved case could not be rerun
- clear the case table before loading a dataset and surface a failed load, so a
  failure cannot leave the previous dataset's cases under the new name
- keep the results table through a refresh of the same experiment
- exclude flow-step jobs from the per-case last-run lookup
- drop case sets from the experiment list, which is only used to pick a run
- report a database failure at the recording lock as itself, not as contention

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: address round-12 review findings on capture and run history

- load flow_node.flow for flownode parents: an agent inside a deployed branch or
  loop captured without its agent, host flow or tool bindings
- decide host_flow_path by whether the path resolves to a flow, not by job kind:
  excluding previews wholesale also dropped the flow editor's step test, whose
  path is real
- page the per-case last-run lookup by created_before until the loaded cases are
  covered; one page of 200 reported older cases as never run
- do not record an experiment when nothing launched
- only attach the case input to a job when a scorer will read it
- keep the case table through a save; only a different dataset clears it
- drop the superseded duplicate comment on the score parser

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: stop refetching run history on every case write

Reading the case list before the first await made the whole job-history query a
dependency of it, so every save, delete and Load more refetched up to 1000 job
rows and blanked the column. Read untracked instead.

- an empty Last run cell now distinguishes never-ran from not-found-within the
  page bound, which the comment already claimed and the cell did not
- reloading a dataset no longer replaces a populated table with a skeleton
- keep the score-parser comment that describes every shape it handles

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor: keep eval datasets in Postgres instead of object storage

Datasets, cases and experiments become rows (`eval_dataset`, `eval_case`,
`eval_experiment`, `eval_experiment_case`) rather than objects under a
`wmill_eval_datasets/` prefix. What a run produced is still the job's:
only case inputs and an experiment's case snapshot are stored.

This removes the machinery the object store needed:

- The advisory lock and the read-modify-write of a per-dataset JSONL. A
  case is a row, so there is nothing to serialize.
- The launch-time identity check on the dataset. The foreign key makes a
  concurrent delete fail the transaction instead.
- The trust guard on an experiment's job ids, which existed because a
  script can write workspace object storage directly and could forge an
  experiment naming somebody else's job.

An experiment now chooses every job id and records itself before pushing
anything, so a launch that dies partway leaves a recorded case whose job
is missing rather than a running job nothing accounts for; cases that
never reached the queue are removed again.

Row-level security on `eval_dataset` is the authority on who may read or
write a dataset, so `extra_perms` grants work and the rule is not
mirrored in Rust. Cases and experiments carry a read policy derived from
their dataset and no write policy: they are written on the unrestricted
pool after the dataset row itself has been asked, with
`SELECT ... FOR UPDATE`, whether the caller may write it.

Cases are capped at 256 KiB each and 10 000 per dataset, refused rather
than truncated. Attachments are S3 references, not inline bytes, so a
case that approaches either cap is a mistake rather than a use case.

Evals no longer need the `parquet` feature or a configured workspace
object storage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* style: align the eval drawer with the design system

- Scorer chips are `Badge`s rather than a hand-rolled bordered span, and
  the section header is a `Label` with its tooltip, as are the case
  editor's fields (which also gets the label colour right).
- The results table showed status as a coloured bullet, which says
  nothing to a colour-blind reader. It now carries the same icons the
  runs table uses, with the status as its accessible name.
- Feedback colours move to the `-500` shades the brand guidelines name.
- The conversation JSON error uses `TextInput`'s `error` prop for the
  border and the caption style for the message, as elsewhere.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat: author an expected answer, tags and attachments on a case

Every scorer is handed `(input, output, expected)`, but nothing could
produce an `expected` except a conversation capture: the case editor had
no field for it and a captured run left it empty. So:

- The editor gains Expected, Tags and a read-only list of the
  attachments a captured case carries. Expected is plain text, or JSON
  when the answer has structure.
- Capturing from an AI agent run keeps what that run answered, which is
  the only moment a reference answer exists for free.

The results table also laid itself out by content, so a long answer
pushed the scores — the numbers the table exists for — off the edge of
the pane. It is fixed-layout now, with the text columns bounded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: expected is captured from a run and can be authored

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat: link a saved agent when inserting an ai agent step

"AI Agent" in the step picker was a leaf that always created a blank
step, so reusing a saved agent meant inserting a blank one, opening its
step input and linking it there. It is a category now, like Flow and AI
Sandbox, listing the workspace's `ai_agent` resources next to a blank
option, filtered by the picker's own search.

A picked agent produces a step that is already linked rather than one
linked afterwards: `agent` set, no tools, and only the flow-local
`user_message`/`user_attachments` transforms. Seeding the brain keys
there would leave transforms a linked step never reads and that
`AgentResourceBar` strips on its next link change.

Each `on:new` forwarder rebuilds the insert detail field by field
instead of spreading it, so a new field is dropped unless the forwarder
names it. `agentPath` is typed on both `GraphEventHandlers.insert` and
`FlowGraphV2`'s `onInsert` so the next one to forget it fails the check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat: restore the link on cancel and simplify the agent bar

Cancel on an agent edit forked the step into a standalone copy, which is
the opposite of what the word means and needed a paragraph under the
card to explain. It discards the edits and re-links the step now,
leaving the agent untouched; diverging from an agent is Unlink's job, on
the linked card. This flow's `tool_inputs` survive the round trip as
overrides, so Cancel no longer folds them into the tools the way Unlink
does.

Linking a step to a saved agent happens in the step picker at insert
time, so the bar's own resource picker is gone and "Save as agent" is
the one action left. Its `+` button was a trap besides: it opened the
generic resource form, where an agent would have to be written as raw
JSON.

The card itself was `surface-secondary`, the sections token, so in dark
mode it was darker than the pane and read as a sunken well rather than
an elevated card. It uses `surface-tertiary` as the brand table
prescribes, its tool chips are `Badge`s, and the editing card no longer
overflows the pane and clips its own buttons. The remaining tooltip
follows the inline `Label` convention rather than sitting in a flex row
whose gap stacked on the trigger's own margin.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat: rework the AI agent evals surface into one table

Evals become a single pane: a dataset of cases, one column per scorer, one
row per case, with the run being looked at chosen from the toolbar.

Runs are permanent. Running the whole dataset opens one; running a single
case records nothing at all — it is a job, and looking at what it did is
not a claim that it belongs in the history. Its result and its scores sit
over the row until they are saved as a run, which carries the cases that
were not rerun and the scoring jobs themselves, so the number that is
saved is the number that was looked at.

A scorer is a runnable: a judge agent or a script, created in one click and
edited in place. Scores carry a reason and per-assertion checks, shown on
hover with a rescore button.

What ran is always named. A run records the agent version, or — for a
configuration that is not deployed — a hash of it, so a table can say that
its numbers describe an agent that no longer exists: those rows dim and the
table offers to rerun. An agent's draft can be run directly instead of the
deployed value, and once those edits are deployed the runs that made them
are recognised as that version. A step with no agent of its own is
evaluable too, and saving it as an agent moves its history onto it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat: keep an agent's in-progress edits on the agent

Editing a linked agent forks it into the step, which is what makes the
edits runnable there — but the agent is what is being edited, so that is
where the unsaved state belongs. The edit is mirrored into the agent's own
resource draft as it is made.

It then survives leaving the flow, shows the agent as drafted wherever it
appears, and is what evals run when asked to run the draft rather than what
is deployed. Deploying or cancelling clears it; opening Edit without
changing anything does not mark the agent as drafted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat: shape the evals surface around a saved agent

Evals hang off an `ai_agent` resource, so the surface is now only ever about
one: the `draft` subject kind, the standalone-step subject and the move that
carried a step's history onto a newly saved agent are gone.

- A run is permanent and numbered per agent. Running a single case is a trial:
  it answers in the panel and never touches the table.
- "Run scorers only" opens a run of its own that reuses the answers of the run
  you are looking at, so a scorer added later measures what already ran without
  calling the agent again.
- A draft run whose configuration is later deployed is stamped, once, to the
  version it became, so its label stops reading `v23 + edits` forever.
- A scorer can carry a pass threshold, read off the scores already recorded.
- The table is the case, its answer and one number per scorer; datasets are
  created and edited in a drawer; a run that executed an earlier state of the
  current draft says so above the table, in one line.
- Which agent a step is, whether it is being edited, and which version it is on
  is a strip above the step's tabs, because it is true of every tab.
- Capturing a case from a step test or a conversation is dropped, and with it
  the `memory` override on a linked step that nothing set.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat: run past versions of an agent, and number versions per resource

The evals home becomes one table of every run of the agent, whichever dataset
each is of, with one badge per scorer. A list spanning datasets cannot hold every
dataset's scorers to look a name up, so a score carries its name and kind with
its number, and thresholds are joined in per run and column.

Run now asks what to run: the latest agent, resolved when the run executes as a
flow step does, any past version, or the unsaved edits. Pinning is a subject kind
of its own, since a linked step resolves the resource live and inlining is the
only way to run a version that is no longer current.

Scorers move into the edit-dataset drawer. The column header over a run reports
and nothing else: a run is permanent, and a control there that changed the
columns would edit the past from the one place that must not. Adding one offers
four ways rather than two, writing and reusing being different jobs, and both new
kinds open with a summary filled in.

Versions are numbered per resource. `resource_version.id` is one identity
sequence for the whole table, so an agent saved nine times read v4 ... v24, and
the gaps counted writes in workspaces the reader cannot see. The id stays how a
version is addressed; the new number is what it is called, in the resource
history drawer as well as here. It is assigned on write rather than counted on
read because trimming past the cap and clearing a history both take the oldest
rows, and counting the survivors would renumber a version a run already names.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: read the dataset a remembered selection names

Reopening the evals modal restored the last dataset from storage as a bare path,
without reading the row it names. Every "is this already the one?" test compared
against that selection, so all of them short-circuited and the dataset was never
loaded: editing it opened a drawer with no summary, no scorers and no cases.

The remembered path is now brought into context the same way any other choice is,
and the tests compare against the dataset that is loaded rather than the one that
is selected, so a selection can no longer stand for a read that did not happen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat: give dialogs a trail in their header

A dialog deep enough to navigate had nowhere to say where you were: the header
held a fixed title, and the way back was a control each body placed for itself,
somewhere in a toolbar that moves with everything else the toolbar holds. The
header is the one part of the surface that does not move, which is where the
trail belongs.

`Modal` takes an optional `trail` of levels below its title, rendered as a
breadcrumb whose ancestors are the way back. Declarative on purpose: callers of
this depth already hold the state that says where they are, so the dialog reads
it rather than owning a stack they would have to push and pop in step with it.

Escape follows the trail. Leaving a level is what someone deep in a dialog means
by it, and closing the whole surface throws away the navigating they did to get
there; at the root it closes as before. That only works if a dialog can tell it
is the surface being addressed, so `Disposable` now answers `isTopmost()` and the
dialog asks before acting: it keeps Escape for itself, so nothing else was
arbitrating between it and a drawer opened from inside it, and both were acting
on one key press.

Evals is the first caller: its runs list is the root, a run is a level in it, and
the back button that used to sit above the table is gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: portal dialogs out of wherever they were opened from

A dialog rendered in place inherits whatever the calling component happens to sit
inside. One `transform`, `filter` or `overflow` anywhere above it makes its
`fixed` positioning resolve against that ancestor instead of the viewport, and a
surface meant to cover the app is then confined to a box it never asked for: the
nav rail paints over it and its own edges are clipped.

Drawers have always portalled for this reason. Dialogs only did so when an
enclosing pane claimed them, and rendered in place otherwise, so the same screen
could show a drawer over everything and a dialog trapped behind the nav. They now
portal the same way: to the pane when one claims it, to `body` otherwise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: make the dialog's title the first step of its trail

The trail listed levels below the title, so a dialog one level deep read
"Evals > All runs > Run 20 · v6": three steps for two places, the first two of
them the same place under different names. The title is the root, so it is the
root's own segment, and the trail a dialog is given is now the whole path with
that segment at its head.

Its height stopped moving too. A heading carries a line-height of its own, so a
header holding only an h3 stood six pixels shorter than one holding segments as
well, and the dialog's whole top edge stepped as you navigated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: sharpen the evals controls around where you are standing

Each screen now offers what belongs to it. The list starts runs; a run is a
record, so it offers only the one thing that acts on the record itself, which is
measuring the answers it already stored. Starting a fresh run from inside one
asked which agent and which dataset from the screen least about either, and
scoring an existing run was offered from the list, where there is no run to
score. Which run and what it is read against are one question asked twice, so
they sit together rather than at opposite ends of a row.

Choosing what to run is now a toggle over the two states worth naming, the draft
and the saved agent, with every earlier version one click further: running an old
version is deliberate, and a list made all three look alike. The draft is read
when the dialog opens rather than taken from the caller's polled copy, which
could be seconds behind an agent edited a moment ago and would leave the option
out exactly when it is the reason for opening the dialog.

The dataset field carries its path under it and its edit button on hover, as a
resource picker does, so the closed field says what the open list said. Edits
waiting on an agent are a "draft" here as everywhere else in Windmill, rather
than "+ edits". The dialog runs an evaluation rather than "the agent", which is
what it was already called everywhere it is recorded. An agent being edited keeps
its evals button on a line of its own, clear of the decision to save or discard.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: settle the evals controls on the patterns Windmill already has

The version choice uses ToggleButtonMore, as the AI provider picker does: the two
states worth naming stay in the group, the rest are behind the overflow menu, and
the one you pick joins the group rather than appearing in a second control below
it. The deployed one says which version it resolves to.

A run offers nothing to start. Scoring an existing run again was the last thing
left there, and it was one button explaining a distinction that the run and the
dataset already make between them.

The warning that a run executed an earlier draft is about the run on screen, so
it goes when the run does rather than following you back to the list, and it sits
against the table instead of inside a frame of its own.

A dataset just created stays open for its scorers and cases: those are what a
dataset is, they can only be added to one that exists, and closing on create sent
you to find it again to add them. Scorer settings are a cog rather than a word,
now that the row holds three actions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: close the gap in the version toggle and say what naming a dataset does

The overflow trigger is not a pill, so the room it reserves showed as a gap
between it and the button before it; it is pulled in by that much. The dataset
field gets its clear button, which is also the slot the edit button is positioned
against, so the two now sit where a resource picker puts them.

Naming a new dataset said nothing about what happens next, and the drawer looked
like it was missing the rest of itself. It says so instead: a scorer and a case
both belong to a dataset, so there is nothing to attach either to until this one
exists, and creating it leaves the drawer open on them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat: choose a dataset's scorers while naming it

A scorer is a reference to a runnable, not a child of the dataset, so it needs
the dataset's name but not its row. The list is collected in the drawer while the
dataset is being named and sent with the create, which already accepts one, so a
dataset arrives holding the columns that were chosen for it rather than being
made empty and then edited to hold them.

Cases stay where they were: a case *is* a row of the dataset, so there is nothing
for it to be a row of until one exists. The drawer says which of the two is which
instead of leaving the screen looking like it is missing the rest of itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: level the version toggle and name the dataset in its own field

The overflow trigger stands a row taller than a toggle button, so the group grew
to its height and left the sunken background showing under every pill beside it.
Every child of the group is the same height now, which is why the AI provider
picker never had the band: it sizes them all alike.

The dataset field says the summary with the path after it rather than carrying
the path on a line below. The list stacks the two, which a one-line field cannot
do, so it says both the other way round.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: tidy the evals forms and the run's own controls

Picking a scorer that exists chooses between two sources rather than showing
both: the ones already measuring something, and everything else in the workspace.
The first list says what each is called with its path under it and what it
already measures on the right, instead of three columns that were the same path
truncated three ways whenever a scorer had no name of its own.

A dataset's drawer says what it is for on the page rather than under an icon, and
its summary is sized like the field beneath it.

The run's own row lines up with the table under it, the warning above that table
is spaced off the rule rather than sitting on it, and adding a case is gone from
a run: a run is a record of cases that were answered, so curating them from it is
editing what it measured.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat: create a dataset holding the cases written for it

Creating a dataset takes the cases to create it with, so one can be assembled in
a single act instead of made empty and then filled in. The drawer holds them
while the dataset is being named, gives them ids of its own to be edited by, and
sends them with the create.

Every case is checked before the dataset is written. `eval_case` grants users no
write, so the rows cannot be inserted in the transaction that creates the dataset
under the caller's own policies; validating first is what keeps "created holding
these cases" from becoming "created, holding some of them", and the rows that do
follow go in one transaction of their own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: name the button for what it opens, and say what each version is

Starting an evaluation asks which state of the agent and which dataset, and both
cost a provider bill, so a button that read as spending one on the way past was
lying about the click. It opens something, and says so. Running one case from the
panel keeps its own name and its play icon, because that one does run on click.

The version options say what they are rather than what they are not: what a flow
step would or would not run is a fact about somewhere else, and someone choosing
what to evaluate is not standing in it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: give the editing card two rows and mark evals as beta

At the width of a step panel the card's one row wrapped: the line naming the
agent, the line saying what saving does, and the two buttons deciding the edits'
fate all fought for it. Deciding gets a row of its own, and evals sits against the
line it is about, since evals of an agent being edited run the edits.

Evals is named wherever it is offered. It read as a word in one state of the card
and as an icon in the other, which is two things to recognise for one door.

The dialog carries a beta badge against its own name, before any level below it:
every way in lands there, so it is said once and stays put as you navigate.

The version toggle spells out which is which. Both are the agent at v2 and the
difference between them is the whole choice, so it is worth the width.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: name a new dataset, and lay the scorer's settings out like a step's inputs

A new dataset arrives called "Dataset 1", which the path follows as it follows
any summary: a dataset with none was one every table could only call by its path,
and the two seeds are what the summary rule already produces.

Scorer settings put each field's description between its label and its input,
where a step's inputs put theirs, and its inputs are the size the rest of the
drawer uses. The runnable behind the column is a link to it with its kind's icon,
since it is a resource of its own and the one thing about it these fields cannot
change. The line explaining that a pass line re-reads recorded scores went: the
threshold is a number to set, and how it is applied is not a decision being made
here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat: curate a dataset in the drawer and save it in one act

The drawer holds the cases while they are edited and writes them when it is
saved: added, changed and dropped, whichever it is. Typing no longer writes, so a
set is never half saved while someone is still deciding what is in it, and Save
means the same thing whether the dataset exists yet or not.

A case panel offers reading rather than acting. Running one case now and editing
one from a run were the last two ways to change a record from the screen showing
it, and the machinery behind the first went with it. The answer is rendered as
the prose it is, under what it is: the case's result, whichever run is selected
above it.

The rest is what the run's table was doing to its own edges: a column name is
clipped to its column rather than running into the next, the table squares off
against an open panel, and that panel closes with the run it belonged to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: one border above a table, and a link to the run's job

The row above the table drew a bottom border and the table draws its own top
edge, so every table sat under two lines. The row keeps its spacing and the table
keeps its edge.

A column header no longer spins while its scores arrive: the cells under it are
where the numbers are missing, and they say so themselves. The beta badge is the
height of the word beside it rather than of the line it sits on.

A run is one flow and therefore one job, so the run says where that job is: what
it is doing, what it cost and what it logged are all there rather than
reconstructed from the table.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat: stream scores as each scorer finishes, and show them per case

A scorer runs after the agent inside the case's own iteration, so its verdict can
be read as soon as its step is done. Waiting for the iteration to end held every
column of a case back until the last of them finished, which is why answers
arrived one at a time and scores all at once.

Reading a job that is still running needs one guard: a module with nothing in it
is a step that has not run, not one that produced nothing, and recording the
second makes a failure that never goes away.

The panel beside the table shows what each column made of the case and why. The
reason a judge gave was stored and never shown, which is the half of a score that
says anything. It stops repeating the question the header already asks, and a
case still running reads as waiting rather than as an answer that says "Running".

A run is a number beside a dataset, so the list puts the two together.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat: score a case with every scorer at once

The scorers of a case read the answer and never each other, so they ran one after
another for no reason: measuring a case now takes as long as its slowest column
rather than as long as all of them. Each is a branch of its own, kept from
failing the others, so a judge that errors costs its own column and no more.

An iteration is three steps again — answer, payload, scores — rather than one per
scorer, and each branch is named for the column it produces, so the graph of a
run says which scorer did what instead of spelling out an id.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: read a judge's score out of the JSON it nearly wrote

A judge quoting the agent inside its own reason writes those quotes unescaped,
which is invalid JSON and also the most ordinary sentence for it to produce. The
whole verdict was being thrown away over it, so a column that had a number
reported having none.

The number and the reason are now read straight out of such text. Deliberately
not a second JSON parser: it finds the two keys and takes what follows, which is
what survives a quote in the middle of a sentence.

A case still running says so with a spinner rather than with the word "Running"
sitting where its answer goes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat: ask a judge for a shape instead of trusting it to write one

A new judge carries an output schema, so the provider holds it to `{score,
reason}` rather than the prompt asking it to. Windmill already delivers a schema
whichever way the model takes it, a tool for Claude and Bedrock and the native
parameter elsewhere, so there is no list of models to keep here.

An agent with no runs offers its first one where the first row would be, rather
than from a toolbar above a table that has nothing in it.

Starting a run no longer picks a dataset for you. It fell back to whichever came
first, which on an agent that has never run means offering another agent's set as
though it were the obvious one; and with no dataset at all it says so and offers
the one move there is.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat: report a column that failed throughout, and hold the run dialog

The runs overview dropped any column that produced no number, so a judge
that failed on every case of a run vanished from the row and read as a
column nobody had asked for. The aggregate now reports every column that
has cells, with the count of the ones it failed on, and the badge says
"failed" where there is nothing to average. A column with no cells at all
is still left out: that one was added after the run and has nothing to say
about it.

Creating a dataset closes the drawer rather than turning it into an edit
of what it just made: scorers and cases already ship with the create, so
there is nothing left to stay open for. Reached from the run dialog, it
gives the screen back with the new dataset selected, and the dialog keeps
the version you had already chosen.

Also:
- the case panel's job link moves to the panel's own header, where its
  scope is: the job is the whole iteration, not the answer it sat over
- one action in the scorer drawer's header, as its neighbours have. The
  reuse list picks rather than adds, and says which dataset each column
  already measures
- adding a case is the last row of the list it lands in
- the pane shows what it has read rather than an empty state it has not
  earned yet, and its rows say they open
- the linked agent card loses a border it had inside another one

* fix: keep the linked agent card's outline

The card is a thing inside the step's inputs rather than a section of
them, and the outline is what says so. Only the rule inside it goes: the
detail it separates is already set apart by being detail.

* refactor: fit the eval surface to the shipped design

* feat: give a nested dialog a back control and the runs list its own moves

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: put a dialog's description under its title

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor: fold a dialog's back control into the crumb it returns to

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: edit a dataset's cases as a table rather than a list beside a form

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: edit a dataset's cases in the grid the data tables are edited in

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: edit a grid cell of prose in place, and cap a dataset at one page

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor: keep the cell editor's styles beside it, not in the vendored theme

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: keep an empty cell empty and cap the editor's growth

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: name the step that assembles a run for the scorers

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: run the payload step natively, and say so when nothing serves that tag

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: report an answer as answered while its scorers are still running

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: let a scorer say a case is not one it measures

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: score the answer, and leave a case with no expected answer unmeasured

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor: split the evals backend into modules

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: record what a run produced so it outlives its jobs

* fix: read only the agent step's own tool jobs into the payload

* fix: pin a run's configuration and give the judge the attachments

* feat: write a dataset's cases in one transaction

* chore: refresh the sqlx cache for the eval queries

* fix: drop results a newer selection has superseded

* fix: keep a draft the agent editor never opened on

* feat: let a run record what it produced instead of waiting to be read

* fix: serialize the replacements of a dataset's cases

* fix: stop the poller from superseding a read slower than its interval

* chore: refresh the sqlx cache

* fix: keep a failed read from settling a cell as a case with no answer

* fix: hold the case grid while its save is in flight

* fix: keep a failed collect step from failing the run it recorded

* chore: refresh the sqlx cache

* fix: commit an open cell into the save that reads it

* refactor: size the eval buttons with unifiedSize

* docs: describe a run as the one flow it is

* fix: show a run's recorded rows when part of it cannot be collected

* refactor: size the remaining PR-added buttons with unifiedSize

* fix: save the dataset name that was submitted, not the one typed after

* fix: force an open cell into the save that was pressed for it

* fix: refuse to score a run whose evidence could not be read

* fix: hold one lock over a dataset's case count and its writes

* fix: keep one unreadable run from costing the whole runs list

* refactor: drop the banned bindable-default from the eval props

* fix: hold the scorer controls while the dataset is written

* fix: read only the caller's own draft of an agent

* docs: say in the contract that a run pins its configuration

* fix: say a scorer did not run rather than blaming a missing answer

* feat: resume the agent draft you already had when you press Edit

* refactor: build the trail and dataset controls from Button

* fix: clear the open-cell flag when the drawer reopens

* chore: refresh the sqlx cache

* fix: read a run's configuration and its version from one snapshot

* fix: refuse a dataset path or summary the column cannot hold

* refactor: handle the agent draft the way the resource editor does

* fix: run only a configuration the launch actually read

* docs: bound dataset path and summary where they are submitted

* fix: surface a stalled agent draft instead of claiming it is kept

* fix: stop claiming a draft holds edits a failed write never sent

* fix: word a missing score only once the run says whether the case answered

* fix: let a breadcrumb crumb shrink so its truncation applies

* docs: describe where an agent's unsaved edits live and what drops them

* fix: keep harvesting scores when the run cannot yet word a missing one

* fix: report a refused draft write the card was reading as a save

* fix: drop the refused draft write when the server copy is taken instead

* refactor: build the scorer and dataset pickers from the design system

* fix: say what removing a scorer column actually does

* fix: drop a refused draft write wherever the server copy is read

* fix: let a picker row be as tall as the two lines it holds

* docs: record what removing a scorer column does to recorded runs

* fix: send a queued draft write before reopening, and drop only what it refuses

* refactor: write the agent draft at commit points instead of mirroring keystrokes

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor: run an agent's edits from the step instead of keeping them as a draft

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: make the diff badge keyboard operable and refuse an edits run without its edits

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: drop the dataset icon from the scorer picker rows

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: size the evals buttons like the rest of windmill and call a run of edits edits

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: count a brain expression as an edit of the linked agent

* fix: cap scorers per dataset and report a launched run as launched

* fix: harvest scores in one read, refuse duplicate case ids, allow group paths

* fix: mint scorer ids server-side, save a dataset edit in one request, check attachments

* fix: write a dataset edit and its cases in one transaction

* fix: atomic dataset create/edit, reset eval pane per agent, stable pending scorer ids

* refactor: govern eval_case writes by RLS so a dataset edit is one transaction

* fix: pin launch snapshot, order case locks, cap dataset size, guard stale load

* fix: cap dataset bytes on single-case writes, reset run-dialog flag on load failure

* feat: migrate eval datasets on username change, settle unspawned cases, drop unused case endpoints

* fix: resolve scorer scripts as the caller and pin their hash; migrate scorer paths on rename

* fix: bound a failed tool call's error to the payload truncation cap

* fix: pin scorer hash as a hex string, reject missing judges, migrate eval authorship

* fix: record an out-of-range scorer result as an error, not a score

* fix: resolve judges in one caller-scoped read, pin deployed scripts, bound pass_if

* fix: settle unspawned cases only when the run completes, and their score cells too

* feat: reassign eval datasets and their path references when offboarding a user

* fix: use the regex backreference in offboarding eval path rewrites

* fix: register eval datasets in offboarding registries, keep resource-version param name

* refactor: name the resource-version path param id, since it is the row id not the version

* fix: validate dataset paths canonically, clone eval data on fork, surface eval load and launch failures

* docs: note MCP tool results are not yet surfaced to eval scorers

* fix: show the eval error state on any load failure, not only an empty dataset list

* fix: preserve eval case order across a batched save

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVjewUvXFEjNLLw7kxz41h

* docs: scope the eval launch delete-safety guarantee to the assembly window

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVjewUvXFEjNLLw7kxz41h

* fix: only offer deployed scripts as eval scorers, drop unbuilt rescore claim

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVjewUvXFEjNLLw7kxz41h

* fix: enforce 0-1 scorer threshold in the settings drawer and clear stale eval load errors

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVjewUvXFEjNLLw7kxz41h

* fix: scope subject version/hash reads to the caller and keep a 0 pass threshold

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVjewUvXFEjNLLw7kxz41h

* fix: select the saved dataset when creating or renaming from the Run dialog

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVjewUvXFEjNLLw7kxz41h

* fix: gate eval dataset rename on path ownership, not just write access

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVjewUvXFEjNLLw7kxz41h

* fix: tolerate a malformed agent config when resolving the deployed label

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVjewUvXFEjNLLw7kxz41h

* refactor: trim eval code and comments, fix shared select and modal paths

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: drop the rename warning when editing an eval dataset path

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: add eval dataset delete, keep summary on partial edits, settle resultless scorer cells

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: cover parseThreshold and subjectLabel

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: hold dataset Save during a scorer write, derive draft_hash only from the carried draft

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 11:08:56 +02:00
hugocasaandClaude Opus 5 b6e059116a feat: track token cost in AI sessions and chats (#10688)
* feat: track token cost in AI sessions and chats

* fix: address review findings on AI cost tracking

* fix: price inherited and overridden models at their real rates

* fix: stop newer model revisions inheriting an older price

* fix: stop a sub-model inheriting its family's price

* fix: keep alias suffixes resolving to their model's price

* fix: count OpenRouter cache writes and drop unverifiable rates

* refactor: move AI spend out of the chat into workspace and user settings

* fix: pin the usage workspace per turn and stop inventing cache rates

* fix: leave Sonnet 5 unpriced while its promotional rate runs

* docs: record the new table in the schema summary and tighten comments

* fix: mark estimated AI costs with ~ and drop session grouping

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: name the workspace in the self-scoped AI usage title

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: state that overrides never replace a provider-returned cost

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: let a cleared cache rate inherit again and flag partial totals

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: clear a refused rate's error when the input snaps back

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: stop a revision variant inheriting its base family's rate

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: report AI usage before tools run and price self usage consistently

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: key pricing rows on the model id usage is reported under

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: surface Bedrock and Gemini usage the chat proxy was dropping

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: count Gemini tool-use prompt tokens as input

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat: price flat-rate Gemini Flash models

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: state the tool-use token invariant once

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 11:08:13 +02:00
Diego ImbertandClaude Fable 5 7751d3e43e feat(frontend): warn when COEP blocks cross-origin resources in raw app editor preview (#10328)
* feat(frontend): warn when COEP blocks cross-origin resources in raw app editor preview

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gr3udvsSKYyH6mnDvGDqEE

* fix(frontend): hedge COEP toast wording and attach warning on detached preview initial load

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gr3udvsSKYyH6mnDvGDqEE

* chore(frontend): condense COEP warning rationale comment

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gr3udvsSKYyH6mnDvGDqEE

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 08:50:23 +02:00
Ruben Fiszelandrubenfiszel 74af4ed939 chore(main): release 1.795.0 (#10807)
* chore(main): release 1.795.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
v1.795.0
2026-08-22 12:11:36 +02:00
dc27db68de fix: require item read scope on workspace tarball export (#10797)
* fix: require item read scope on workspace tarball export

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: accept a wildcard path grant for whole-domain scope checks

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: let a wildcard path grant delegate the unqualified scope

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2026-08-22 12:06:54 +02:00
hugocasaandClaude Opus 5 40f0cab2ad fix: scope capture deletion to the workspace in the request path (#10795)
* fix: scope capture deletion to the workspace in the request path

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test: layer the capture fixture on base instead of duplicating it

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 10:01:25 +00:00
5b885ae311 fix: keep raw-app files within their app folder on sync pull (#10796)
* fix: keep raw-app files within their app folder on sync pull

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: validate raw-app file keys as stored, closing nul and duplicate-field bypasses

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: guard raw-app runnable ids too and fail closed on unparseable value

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: strip only a leading slash on raw-app file keys to match backend

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: strip only a leading slash on raw-app file keys to match backend

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2026-08-22 10:00:52 +00:00
hugocasaandClaude Opus 5 25d9a20630 fix: require an unscoped token to reach the workspace encryption key (#10798)
* fix: require an unscoped token to read the workspace encryption key

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: hold the encryption key's write path to the same token bar

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: audit a workspace export only once nothing can still reject it

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore: carry the new audit operation into the served openapi spec

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 09:55:44 +00:00
Ruben Fiszel 01fc4f1568 fix: name the requested storage when a workspace storage lookup finds nothing (#10803)
* fix: name the requested storage when a workspace storage lookup finds nothing

* chore: point ee-repo-ref at the merged ee commit
2026-08-22 09:50:30 +00:00
Ruben FiszelandClaude Opus 5 a350f7c68e feat: show the date on the runs dashboard chart axes (#10808)
* test: assert the unpacked repo symlink without following it

`unpack_keeps_a_link_that_stays_in_the_repo` read through the link it had just
unpacked. Windows stores a symlink's target verbatim and its object manager
rejects the `/` in a POSIX one, so `read_to_string` came back with
`ERROR_INVALID_NAME` and the release's `cargo_test_windows` job was red.

Pin what the function is responsible for on every platform — the link is kept
and materialized — and read through it only where a POSIX relative target
resolves.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P3WRtxdKNGdomWX9vaAYGx

* test: key the cli sync-map fixtures with the platform separator

A sync map is keyed with the platform separator on both sides — `FSFSElement`
walks the tree with `path.join`, and the remote `ZipFSElement` starts at
`"." + SEP` and joins from there — while an `!inline` reference is always
forward-slash. `lock_dedup.ts` follows that convention; the fixtures did not,
so on Windows they built a map shape the CLI never produces and 12 of them
failed. `getTypeStrFromPath` is the same story: it matches
`"dependencies" + SEP`, and the test handed it a forward-slashed path.

Build the fixture keys through the separator, leaving the `!inline` references
and the `present` map forward-slash, as `sync.ts` hands them over.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P3WRtxdKNGdomWX9vaAYGx

* ci: skip the discord comment relay when the thread lookup returns none

A rate-limited or unauthorized Discord response carries no thread list, and
under `bash -e` that aborted the step — jq cannot iterate null, nor parse the
HTML error page Cloudflare answers a 429 with — before it reached the "thread
not found, skipping" branch right below. Three comment relays failed that way
on the 1.794.0 head.

Keep the step green for both, but tell them apart: a response with no thread
list is a delivery that was dropped for a reason worth seeing, so it warns with
the body it got, while a PR that genuinely has no thread stays quiet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P3WRtxdKNGdomWX9vaAYGx

* feat: show the date on the runs dashboard chart axes

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019UA8Kikr1QD28g2fyWoSbj

* fix: keep the runs chart date visible on sub-day ranges

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019UA8Kikr1QD28g2fyWoSbj

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 09:43:29 +00:00
Ruben FiszelandClaude Opus 5 4b406e37c0 fix: size the ephemeral job token to the job timeout it must serve (#10804)
* fix: size job token to the premium cloud job timeout

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: give the job token setup headroom and drop dead MAX_TIMEOUT_DURATION

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: cap job token setup slack so self-hosted tokens stay at 7d

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 09:30:08 +00:00
01891cd732 fix: keep workflow-as-code scripts off dedicated workers (#10805)
* fix: keep workflow-as-code scripts off dedicated workers

A dedicated subprocess calls the script's `main`. A workflow-as-code v2
entrypoint exports none, so a WAC script configured as a dedicated worker
failed every run with `entry.module.main is not a function`, and its
checkpoint/dispatch round-trip never ran at all.

Leave such a script unregistered in the dedicated worker map instead. The
worker still holds the script's dedicated tag, so the job falls through to
the regular executor on the same worker and runs correctly; rejecting it at
push time would strand it, since nothing else pulls that tag.

`is_wac_v2` covers only the languages whose executor actually routes a
workflow through the WAC runner: Deno runs a WAC-shaped script as a plain
`main`, so claiming it is WAC would deny it a path it uses correctly today.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VMNiDoBuUY9jzqcuFLT2wU

* chore: update ee-repo-ref to ac02c4696ea0be6a8b8ae154ddd7521bc1c3bbc0

This commit updates the EE repository reference after PR #740 was merged in windmill-ee-private.

Previous ee-repo-ref: bf742f6ea4d435bd47c9ee0ac5ad800925d79672

New ee-repo-ref: ac02c4696ea0be6a8b8ae154ddd7521bc1c3bbc0

Automated by sync-ee-ref workflow.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-08-22 10:57:16 +02:00
Ruben Fiszelandrubenfiszel 1a506b8f22 chore(main): release 1.794.1 (#10801)
* chore(main): release 1.794.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
v1.794.1
2026-08-21 13:45:56 +00:00