Commit Graph
147 Commits
Author SHA1 Message Date
ca44043e12 feat: let operators compose flows when the workspace grants the right (#11228)
* feat: let a workspace withdraw operator schedule and trigger writes

Operators can create, edit and delete schedules and triggers today through the
API, CLI and MCP, while the operator_settings flags beside them only hide those
pages. An admin who wants operators to see what is scheduled without letting
them change it cannot express that. Add manage_schedules and manage_triggers as
enforced settings, gated at the schedule handlers and at the generic TriggerCrud
routes so every trigger kind is covered by one check.

They name capabilities operators already hold, so they are granted unless
withdrawn, and absence has to mean "never configured" rather than a value. The
read coalesces to true; the update endpoint merges into the stored jsonb with
the two fields as Option<bool>, so an omitted key keeps what is stored.
operator_settings is git-synced as a whole object, so a settings file written
before these keys existed reaches the endpoint on every pull, and a serde or SQL
default of either polarity would turn that pull into a silent withdrawal or
restoration.

The rights are read through a per-process cache, so withdrawing one publishes a
notify_event that drops the entry on every replica.

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

* feat: let operators compose flows when the workspace grants the right

Adds operator_settings.builder_flows: a workspace setting that lets every
operator compose flows out of runnables that already exist. It does not make
them authors. The boundary the operator role draws is authoring code and running
arbitrary code, and this does not move it: check_flow_is_composition_only walks
the value and refuses anything carrying code, including the shapes an obvious
walk misses (code hoisted into a flow_node, an AI agent step's tools, and a
linked ai_agent resource whose tool list is resolved at run time).

What the walk cannot settle it returns for the caller to authorize under RLS:
the worker tags the steps pin, every runnable they reference, and the (path,
hash) of every version-pinned step. Composing a path is enough to run it and to
run it as whoever it runs as, since the worker resolves a step's path with the
root DB handle and adopts that runnable's on_behalf_of. A pinned hash needs its
own check because dispatch ignores the path beside it.

The gate runs on every write and on both request-supplied-value paths, flow
preview and flow dependencies, or either becomes the way to run what the write
path refuses.

Operators of a builder workspace consume a full author seat; the EE companion
carries the counting.

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

* feat: enforce operator write rights on the router and in the UI

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

* fix: close the capture gap and gate the trigger editors' write actions

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

* fix: gate acl writes and the native trigger drawer behind manage rights

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

* fix: refuse operator writes with 403 and gate sharing at the drawer

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

* perf: resolve identity in the operator write gate only for writes

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

* fix: gate the suspended-jobs actions and stop the route check refusing reads

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

* fix: explain the empty-state create button when operator writes are withdrawn

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

* fix: audit operator settings changes and fold path writes into native rows

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

* fix: open locked editors read-only and group the operator settings

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

* fix: skip email and azure lookups on editor open while triggers are locked

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

* docs: state each operator-rights rationale once in comments

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

* fix: address CI review findings on operator write rights

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

* fix: keep capture move gated and skip it in the builders while locked

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

* fix: refuse builder-rights violations with 403 so operators stay logged in

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

* refactor: trim duplication in the operator builder gates

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

* fix: hide build app from builder operators on the flow page

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

* chore: point at the companion EE PR merged with EE main

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

* fix: keep a builder's drafts list loading past drafts they cannot write

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

* fix: hide saved agents from builder operators in the step picker

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

* chore: note the inlined seat rule and drop orphaned sqlx entries

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

* fix: stop a builder's step test from logging them out

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

* fix: refuse a builder's dependency job on a path it cannot write

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

* fix: keep builders from adding dynamic dropdown code to a flow

A flow's dropdown code runs as whoever loads its form, so a builder may keep or drop the code stored on the flow it updates, never add or change it. The builder's editor hides the dropdown types and code, and previews options through the deployed flow; the inline dropdown refusal is a 403 so it no longer logs operators out. Also trims rationale comments repeated across sites.

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

* fix: show why saving operator settings failed

The seat-cap refusal on granting builder rights explains what to do; the toast now carries the server's message instead of a generic failure.

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

* fix: check builder flow drafts like deploys and treat dropdown code as code

A developer who loads a builder's flow draft in the editor runs its dynamic dropdown code as themselves, so a builder's draft now passes the same checks as a deploy. Dropdown code is refused like step code rather than kept or dropped, which also removes the exact-match comparison that refused builders over whitespace.

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

* fix: bill a builder workspace's operators as developers on cloud

The cloud seat count behind the Premium page, the sidebar usage and the fork cap still weighed every operator at half a seat, while the builder right makes them authors. The out-of-repo invoicing job must follow the same rule.

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

* fix: check a builder's flow draft as it will be stored

Draft storage strips NUL escapes after the builder check, so a key ending in one (value\u0000, x-windmill-dyn-select-code\u0000) passed the check as an unknown field and was stored under its plain name. The check now reads the sanitized text.

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

* fix: list a builder's flow drafts and hide hub imports from builders

A builder's undeployed flows now appear in the home list, the flow list and the folder counts. Hub project imports and templates bring scripts and apps along, so builders are no longer offered them. The docs record builders' JavaScript expressions as an accepted risk.

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

* fix: keep the stored builder right when a settings payload omits it

A git-synced settings file written before the key existed withdrew the right on every push. builder_flows now follows the manage_* rights: an omitted key leaves the stored value, and the CLI does not count it as a difference.

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

* fix: word builder refusals by what the flow contains, test the tag refusal

A builder refused on a developer's flow never changed its code, so the refusals now describe the flow ("has inline code, so only a developer can edit this flow") rather than an authoring attempt. The grant confirmation uses the neutral dialog: granting changes billing but destroys nothing. The integration test pins the refusal of a worker tag the workspace cannot use.

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

* fix: read builder rights from the operating workspace, gate the flow page's audit logs entry

Builder rights now come from useOperatorBuilderFlows(), next to the schedule and trigger locks, so an editor embedded for another workspace answers about that workspace; the legacy AI chat, one instance for the whole app, reads the navigation workspace. The flow page's Audit logs entry follows the operator audit_logs setting now that builders open that menu. Operator settings reset every value on load, null settings included, so nothing carries over from the previous workspace.

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

* fix: pick a dynamic dropdown's code source by the operating user's role

The flow input editor, the flow test panel and the flow chat send the dropdown request to the operating workspace, so they now also choose inline versus deployed code by the role held there, through useOperatingUser(), instead of the navigation workspace's.

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

* chore: update ee-repo-ref to 40ac1c5f8cbce3843b582d9b392d3f3cc7eca3e6

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

Previous ee-repo-ref: 31c9e66884b8ca805b20bbfad41fc428fbedbc0e

New ee-repo-ref: 40ac1c5f8cbce3843b582d9b392d3f3cc7eca3e6

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>
2026-09-30 18:09:14 +02:00
9a1c6e5081 feat: let a workspace withdraw operator schedule and trigger writes (#11226)
* feat: let a workspace withdraw operator schedule and trigger writes

Operators can create, edit and delete schedules and triggers today through the
API, CLI and MCP, while the operator_settings flags beside them only hide those
pages. An admin who wants operators to see what is scheduled without letting
them change it cannot express that. Add manage_schedules and manage_triggers as
enforced settings, gated at the schedule handlers and at the generic TriggerCrud
routes so every trigger kind is covered by one check.

They name capabilities operators already hold, so they are granted unless
withdrawn, and absence has to mean "never configured" rather than a value. The
read coalesces to true; the update endpoint merges into the stored jsonb with
the two fields as Option<bool>, so an omitted key keeps what is stored.
operator_settings is git-synced as a whole object, so a settings file written
before these keys existed reaches the endpoint on every pull, and a serde or SQL
default of either polarity would turn that pull into a silent withdrawal or
restoration.

The rights are read through a per-process cache, so withdrawing one publishes a
notify_event that drops the entry on every replica.

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

* feat: enforce operator write rights on the router and in the UI

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

* fix: close the capture gap and gate the trigger editors' write actions

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

* fix: gate acl writes and the native trigger drawer behind manage rights

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

* fix: refuse operator writes with 403 and gate sharing at the drawer

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

* perf: resolve identity in the operator write gate only for writes

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

* fix: gate the suspended-jobs actions and stop the route check refusing reads

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

* fix: explain the empty-state create button when operator writes are withdrawn

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

* fix: audit operator settings changes and fold path writes into native rows

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

* fix: open locked editors read-only and group the operator settings

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

* fix: skip email and azure lookups on editor open while triggers are locked

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

* docs: state each operator-rights rationale once in comments

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

* fix: address CI review findings on operator write rights

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

* fix: keep capture move gated and skip it in the builders while locked

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

* fix: keep admin and operator exclusive when setting a workspace role

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

* refactor: use the shared section component for operator settings groups

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2026-09-25 17:34:58 +00:00
hugocasaandClaude Opus 5 3bd89e92d8 feat: collapse the fork members setting and show its state in a badge (#11220)
* feat: collapse the fork members setting and show its state in a badge

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

* fix: show the fork members badge next to the title, only when on, like other section badges

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-25 17:12:51 +02:00
Ruben FiszelandClaude Opus 5 4d12ea4614 feat: deploy from the UI to a workspace on another instance (#11245)
* feat: deploy from the UI to a workspace on another instance

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

* fix: keep the remote deploy proxy from being spent by a link

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

* fix: key remote deploy proxy URLs instead of a global client header

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

* fix: keep the remote deploy proxy key out of logs and restricted hands

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

* fix: serialize remote deploy connect with account and key changes

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

* feat: key remote deploy tokens to the account and connect by signing in

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

* fix: bind remote deploy connect to its target and order its locks

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

* fix: serialize remote deploy connect with target changes

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

* docs: list every lock remote deploy connect takes in auth-surface

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

* fix: never wait on the membership lock in remote deploy connect

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

* fix: keep a superseded target response out of the settings form

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

* fix: void stale remote deploy tokens on read instead of locking in connect

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

* fix: void remote deploy tokens older than the last target change

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

* fix: order remote deploy connections by when their connect started

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

* fix: bind stored remote deploy tokens to the membership and target they were connected under

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

* fix: answer remote deploy connect without settings as no target

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 01:40:53 +02:00
6186a0645d feat: edit variables, resources and triggers in their own session tab (#11206)
* feat: edit variables, resources and triggers in their own session tab

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

* fix: take a trigger's flow kind from its config and name the written trigger

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

* refactor: one operating-workspace context for editors acting on a session's workspace

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

* fix: reload page item tabs only in sessions acting on the tool's workspace

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

* fix: resolve open in workspace for every preview tab kind in one place

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

* fix: guard every component under a session editor against the navigation workspace

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

* fix: show a page item tab whose item is gone instead of mounting its editor

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

* fix: follow a saved page item in the tab's own workspace and restore in-frame rows

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

* feat: close a page item tab from its editor header

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

* fix: judge trigger permissions by the user acting in the session workspace

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

* fix: judge every session-editor permission by the user acting in that workspace

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

* fix: keep session permissions reactive to the acting role and restore tabs as last seen

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

* fix: default Path's owner controls to the user acting in its workspace

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2026-09-19 23:29:14 +02:00
hugocasaandClaude Opus 5 37e493ae66 feat: add an instance setting to refuse a token in MCP URLs (#11162)
* feat: add an instance setting to refuse a token in MCP URLs

MCP clients are commonly configured with the token in the URL
(`/api/mcp/w/{workspace}/mcp?token=...`). A URL-borne credential ends up in
browser history, proxy logs and referrers, so an instance can now turn that
channel off with the `mcp_disable_token_query_param` global setting and leave
the Authorization header as the only way in, which sends MCP clients through
the OAuth flow the endpoints already advertise.

The rejection is a middleware on both the workspaced and the gateway MCP
mounts, layered outside everything that reads a token and inside the
WWW-Authenticate layer, so the 401 carries the resource pointer a client needs
to start OAuth discovery.

Off by default. With it on, the token drawer and the home connect drawer stop
offering to mint a token for an MCP URL and hand over the bare URL instead.

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

* fix: read the MCP URL policy when a URL is asked for, and drop the all-workspaces option

Two review findings on the token drawer:

The policy was read once per page load and cached for the browser session, so a
superadmin turning the setting on left every open tab handing out `?token=` URLs
the server now refuses. Both entry points now read it when the user actually asks
for an MCP URL: when MCP mode is entered, and when the connect drawer opens.

The workspace picker offered "All workspaces / Multi-workspace", but the gateway's
consent screen binds the token it issues to the one workspace picked there, so
OAuth has no multi-workspace grant to hand out. That entry is now token-only.

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

* fix: don't guess the MCP URL policy, and say where the switch lands on restart

Review findings:

The comment on the settings load claimed `MODE=mcp` as the target deployment, but
that mode joins no monitor loop, so the startup pass is its only read and a change
lands on restart. That is true of every global setting there, `base_url` included;
the comment now says so, and the setting description tells an operator running
dedicated MCP servers what to expect.

A failed settings probe resolved to "tokens allowed", so with the switch on the
drawer would mint a non-expiring token and hand over a URL the server refuses for
as long as it exists. The probe now propagates its error and the panel reports it
with a retry, creating nothing until the answer is known.

The test passed a valid token, so it could not tell a rejection before
authentication from one after it. It now also sends a token that was never valid
and asserts the middleware's own message, which fails if the layer moves inward.

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

* fix: withhold the MCP URL until a workspace is picked

With no persisted workspace the store starts undefined, so opening the drawer from
/user/workspaces before choosing one rendered a copyable
`/api/mcp/w/undefined/mcp`. It reads like a real URL and a client pointed at it
would never connect. The panel now asks for a workspace instead, matching the guard
the token branch already has on its generate button.

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

* docs: drop the coverage-status note from the MCP switch test

It documented what the test does not reach rather than a constraint the next
reader could break; that belongs in the PR, not the module doc. The layer-order
rationale, which is what a future edit would break, stays.

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

* refactor: fall back to the bare MCP URL instead of alerting on a failed read

When the setting read fails, show the bare URL rather than an error with a retry.
It works whichever way the setting is, so no alert is needed, and it still never
mints a token for a URL the server may refuse. The connect drawer's wording falls
back the same way so the blurb matches the panel.

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

* refactor: move the MCP URL token setting to Core

It sat in the Auth/OAuth/SAML list, which the settings sidebar shows under SSO,
suggesting a dependency on SSO that does not exist: MCP OAuth has Windmill act as
the authorization server, and any login method, password included, completes it.
It is an instance-wide credential policy, so it now lives with the other ones in
Core, kept out of quick setup like its neighbours.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 15:24:00 +02:00
hugocasaandClaude Opus 5 9d335de87a feat: add a workspace toggle that adds its admins and developers to new forks (#11215)
* feat: add a workspace toggle that adds its admins and developers to new forks

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

* fix: add members copied into a fork as manual members, not instance-group ones

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

* style: keep the fork members copy comment at the query and drop the raw spacer

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 15:15:35 +02:00
hugocasaandClaude Opus 5 9320312eac feat: cap user token expiration with an instance setting (#11159)
* feat: cap user token expiration with an instance setting

Adds `max_token_expiration_days`, an instance-wide ceiling on how far ahead a
token created through `POST /users/tokens/create` may expire. With it set, that
route refuses a token with no expiration and one that expires past the window;
absent or non-positive, nothing changes.

Only the user-facing handler enforces it. Server-side mints (native trigger
webhook tokens, app embed tokens, sessions) pick a lifetime the caller never
chooses and go straight to `create_token_internal`, so they stay uncapped, as
does the superadmin `impersonate` route.

Service accounts are exempt, in the workspace the token targets or in any
workspace for a global token, so unattended automation can keep longer-lived
credentials.

The token form now surfaces the API error instead of only logging it, and offers
"Expires In" in MCP mode as well: that mode always sent no expiration, which the
cap refuses, leaving MCP URLs impossible to generate.

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

* fix: shorten over-long token expirations instead of refusing them

Refusing a non-compliant request breaks the callers that cannot comply. The CLI
authorization page, `wmill user create-token` and the editor's language-server
token each pick a lifetime — usually none at all — with no way to read the
setting, so a cap made browser login hang and the editor lose its LSP root
rather than stopping the long-lived tokens the setting is aimed at.

`cap_token_expiration` now returns the expiration to store, shortening a request
that asks for too long or for none. The policy still holds absolutely, no caller
can break, and there is no clock-skew boundary where an expiration exactly at the
ceiling flips to an error. The token form needed no changes at all, so its MCP
and error-toast edits are gone with it.

Also drops the Enterprise badge on the setting, which nothing enforced, notes the
mint paths in docs/auth-surface.md, and pins that `tokens/impersonate` and the
second-workspace case stay outside the exemption.

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

* fix: drop the unintended token-form change and correct the exemption docs

The token form needed no change once the ceiling shortens rather than refuses,
but the earlier revert restored from the index, which already held the staged
edit, so the MCP expiration field and the error toast stayed on the branch with
a comment justifying them by a refusal that no longer happens.

docs/auth-surface.md claimed a service-account row in any workspace exempts
outright; that only holds for a workspace-less token, which has no workspace to
match. A ceiling written as a string, which the YAML instance config and config
sync can both produce, now has a test.

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

* feat: offer only expirations within the ceiling in the token form

The server shortens a token that asks for longer than `max_token_expiration_days`
or for no expiration, which the token form could not tell anyone: a user picking
"No expiration" got the ceiling silently. The form now reads the setting and,
with one set, drops "No expiration" and every choice above it, adds the ceiling
itself as "N days (maximum)" and selects it, and says the instance limits tokens
to N days.

MCP mode hides the expiration field and always sent none, so with a ceiling the
field now shows there too and keeps its value across the toggle. Without a
ceiling the form is unchanged.

Reading it needs no superadmin: the setting joins the keys any logged-in user
can read through `GET /settings/global/{key}`. It holds a policy, not a secret.

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

* fix: make the token form and the server agree on what counts as a ceiling

The form parsed `max_token_expiration_days` more loosely than
`cap_token_expiration`, so the two could disagree on whether a ceiling exists
at all. A `7.0` from the YAML instance config, or a string such as "7.0" or
"1e1", made the form hide "No expiration" and announce a 7-day limit while the
server capped nothing; a value between chrono's and JavaScript's date limits
preselected an expiration the server could not parse.

Both now read the same thing as a ceiling: a whole number of days from 1 to
1,000,000, stored as an integer, an integral float or a string of digits.
`parseMaxTokenExpirationDays` holds the frontend's copy, and the instance
settings validation uses it too, so the settings page no longer accepts a value
the server would ignore. The bound replaces the date-range guard on both sides.

Also corrects the rationale for shortening rather than refusing: the setting is
now readable by any logged-in user, so those callers do not read it rather than
cannot, and CLIs already installed never will.

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

* fix: cap service-account tokens like everyone else's

The ticket exempted service accounts from `max_token_expiration_days`, but
their tokens are the long-lived ones a rotation policy is meant to bound, and
the exemption let any workspace admin get an uncapped token by impersonating
one. It also left the token form unable to agree with the server: an admin
impersonating a service account was offered only capped choices while the
server would have kept any.

`cap_token_expiration` now takes just the requested expiration, with no
per-caller lookup, and the service-account query and its cache entry are gone.

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

* fix: cap superadmin impersonation tokens and pin the frontend parser

`POST /users/tokens/impersonate` wrote its own token row with whatever
expiration the superadmin sent, so it was the one route left that could mint a
token that never expires with `max_token_expiration_days` set. The ceiling only
decides the stored expiration (the auth lookup never reads the setting), so
leaving it uncapped meant exactly that. It now goes through
`cap_token_expiration` like `create_token`; nothing in Windmill calls it, so no
caller changes.

Also adds `tokenExpiration.test.ts`, pinning which stored values
`parseMaxTokenExpirationDays` reads as a ceiling against the server's reading,
and documents that tokens existing when the setting is turned on or lowered
keep their expiration.

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

* fix: reject a max_token_expiration_days the token routes cannot read

The settings API and config sync stored any value for the key, and the token
routes can only read an unparseable one as no ceiling. A typo such as `7.5` or
"7.0" was accepted and silently turned the policy off.

`parse_max_token_expiration_days` in windmill-common is now the single server
reading of the setting: null or empty clears it, a whole number of days within
the bound is the ceiling, anything else is an error. The settings write hook and
`sync_global_settings_declarative` reject that error, and `cap_token_expiration`
reads through the same function, logging a value written around both.

Tests: the parser's accept/clear/reject table (the same table as the frontend
parser's), the settings API refusing 7.5, and config sync refusing "7.0".

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

* fix: reserve the CLI login token label so its expiry does not email the user

With a token expiration ceiling, the token the CLI authorization page mints now
expires, so every `wmill` login earned an "expiring soon" and an "expired and
deleted" email and critical alert. The CLI already signs in again on its own
when that token stops working, so those notifications ask the user to do
nothing.

The page now labels it `cli-login:<username>` (previously `cli-<username>`),
reserved in `is_user_token` and its SQL and Svelte mirrors: no expiry
notifications, and the label cannot be edited. A colon-terminated namespace
like `embed_app:` and `impersonation:` keeps hand-made labels clear of it. Not
in `is_server_minted_label`, since the page mints through `/users/tokens/create`.

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

* fix: skip the expiring-soon warning for tokens that were short-lived from the start

A token whose whole lifetime fits in the 7-day warning window got its
"expiring soon" email (and critical alert, when enabled) minutes after it was
created, about a lifetime its creator had just picked. With an expiration
ceiling of 7 days or less that is every token created from the form or
`wmill token create`.

`register_token_expiry_notification` no longer queues a warning for such a
token. The window is now `TOKEN_EXPIRY_WARNING_DAYS`, shared with
`check_expiring_tokens`, so shortening the warning window can never leave
tokens of an intermediate lifetime with no warning at all. The "expired and
deleted" notice still goes out for every user token.

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

* fix: exempt service-account tokens from the expiration ceiling again

Service accounts are the identity automation that needs a long-lived credential
runs as, so their tokens are exempt from `max_token_expiration_days` once more:
a service account in the workspace the token names, or in any workspace for a
workspace-less token. `tokens/impersonate` checks the impersonated account, so
a superadmin minting a token for a service account gets the same exemption.

The token form applies the same rule for the account it is running as, when
that account is a service account in the current workspace, which is what an
admin impersonating one sees; otherwise it would offer only capped choices
while the server keeps any.

Any workspace admin can create and impersonate a service account to hold an
uncapped token, so the ceiling bounds personal tokens; the doc comment and
docs/auth-surface.md say so.

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

* fix: decide the token form's service-account exemption from the token's workspace

The form treated the account as a service account only when it was one in the
workspace the app was on, while the server checks the workspace the token is
for, or any workspace for a workspace-less token. With an email that is a
service account in one workspace and an ordinary member of another, picking the
other workspace in MCP mode offered "No expiration" and the server silently
stored the ceiling; the reverse hid the exemption.

`GET /workspaces/users` now returns each membership's `is_service_account`
(its query already joins the `usr` row), and the form applies the server's rule
to the token's own workspace. The selection becomes a derived value held within
the ceiling, so switching to a capped workspace never leaves an unoffered choice
selected.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 14:37:39 +02:00
hugocasaandClaude Opus 5 9d348f84c7 fix: skip expiry notifications for app embed and SDK tokens (#11169)
* fix: skip expiry notifications for app embed and SDK tokens

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

* refactor: share app token label prefixes between mint sites and the check

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

* fix: skip expiry alerts for impersonation and test-connection tokens

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 17:28:53 +02:00
a9a9335a34 fix: keep the instance users table's actions and header in view (#11145)
* fix: keep the instance users table's actions and header in view

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015pMjimm9tUMXtD8rkWB62w

* fix: lock only the User option for group-granted roles and keep pinned cells opaque

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015pMjimm9tUMXtD8rkWB62w

* fix: close the instance settings drawer from the manage-in-workspace menu item

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015pMjimm9tUMXtD8rkWB62w

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2026-09-16 00:26:56 +02:00
Ruben Fiszel a95e950529 feat(cli): list, get and restore trashed items with wmill trash (#11125)
* feat(cli): list, get and restore trashed items from the CLI

* docs(cli): tell agents a sync push deletion is restorable with wmill trash

* refactor(cli): share the ApiError formatting and type trash flags as integers
2026-09-14 20:06:56 +00:00
91e6dc39ce feat: pre-approved cloud accounts: login links, OAuth adoption, setup, and the trial bridge (#10875)
* feat: single-use login links and oauth-claimable pending accounts

* docs: capture the auth surface facts behind login links

* fix: accept stringified email_verified from oauth userinfo

* docs: describe the oauth claim rule in the auth surface notes

* fix: harden login-link redirects and sweep expired links

* chore: bump ee-repo-ref

* fix: keep expired login links a day so an open still reads as expired

* fix: refuse login links for superadmin and devops accounts

* fix: re-check the account's roles when a login link is opened

* feat: pre-approved cloud accounts finish their setup and start their trial from Windmill

* feat: dev-only localStorage opt-in to the cloud UI on localhost

* feat: finish-setup entry in the desktop settings menu

* style: pulse the settings row while account setup is pending; shorter, blue finish-setup entry

* fix: list the configured providers in the finish-setup modal

* fix: open the finish-setup modal after the menu has closed

* feat: finish-setup provider sign-in keeps the session when the provider asserts another address

* chore: pin the EE companion commit

* fix: plain toast for the finish-setup refusal

* style: format the dev cloud override

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

* feat: onboarding skips the source question an invite already answered

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

* fix: type the finish-setup icons and login_type as the frontend uses them

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy

* feat: invited accounts get a workspace name, hub picks and starter prompts from their invite

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy

* fix: the workspace form reads the invite's name itself, so the picker prefills it too

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy

* feat: an empty workspace offers the projects its invite picked, one click from importing

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy

* style: picked projects get identical import buttons

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy

* feat: a pinned sidebar banner until an invited account has credentials of its own

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy

* style: the account-setup row speaks the rail's language, tinted not filled

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy

* chore: pin ee-repo-ref to the import fix

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy

* refactor: picked projects live in the template picker only; account-setup row moves to the rail footer

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy

* fix: review round — no portal login for job tokens, finish-setup failures keep the session, prompt labels deduped

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy

* fix: CI round — trial start is a POST, profile cache follows the session, setup row on MenuButton

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy

* fix: CI round — no password road where password login is off, cache note on the login form, trial refusal surfaced

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy

* fix: CI round — set_password guarded on its read, refusal stays on the page, docs and formatting

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy

* fix: CI round — popup OAuth clears the profile cache, portal helper crate-private, refusal toast stays

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy

* fix: a refused trial is recorded inline in the rail, not in a day-long toast

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy

* fix: the refusal notice uses the rail's button and has a collapsed form

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy

* fix: CI round — SSO can finish account setup, with the same mismatch refusal as OAuth

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy

* fix: SSO finish-setup rides in RelayState and the refusal notice is a status region

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy

* fix: keep the finish-setup cookie beside RelayState, hoist the status region, pin session-keyed profile cache

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy

* fix: empty live region for the trial refusal, drop the setup cookie once adopted, telemetry inventory

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy

* fix: the trial refusal survives the responsive sidebar swap

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy

* fix: the trial refusal is shown to the account it answers, modal open prop is required

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy

* feat: an invited account skips the whole onboarding survey

Its source is the invite and its use case was researched before the invite went
out, so neither question is asked: the known source is recorded and onboarding
opens on naming the workspace. Accounts without an invite profile see the survey
exactly as before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy

* fix: an invited account with a workspace leaves onboarding before anything paints

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy

* fix: account-setup state resets on sign-out, onboarding shows a loading state while it settles

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy

* style: keep the refresh doc comment on refresh

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy

* fix: profile lists are distinct, and the offer table notes what a users-import does to it

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy

* chore: update ee-repo-ref to 1ba6fe83451f0a1f8fafe04b7187087d51e0f769

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

Previous ee-repo-ref: be42722d09832ffff709a1f710f3e97e34d513b2

New ee-repo-ref: 1ba6fe83451f0a1f8fafe04b7187087d51e0f769

Automated by sync-ee-ref workflow.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-09-14 20:04:58 +00:00
GuilhemandClaude Opus 5 d8b9174235 feat(ai-sessions): turn skills on by default, and group them by folder (#11058)
* feat(ai-sessions): turn skills on by default, and group them by folder

A skill is instructions the workspace wrote for the assistant to use, so what
carrying one costs is context rather than access. Selecting each one before it
applied made publishing a skill a two-step affair, and left most of them unused.

Skills now default to on. No storage is rewritten to get there: the preference
keeps its key and holds a decision per path, so the older array of enabled paths
still reads as "these were on" and only the paths nobody decided about move. MCP
servers stay opt-in through the same factory — their tools reach an external
system, which is a different question from context.

The Skills settings list groups into a tree once skills span more than one
folder, with a switch per folder acting on everything beneath it, and the list
answers the keyboard: Up/Down walk it, Left/Right fold, Space flips the switch
under the highlight, Enter opens the skill.

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

* fix: give a modal the option to stand only as tall as the window

`AIPromptsModal` asks for 1000px of height, which is taller than a laptop
window: the dialog then scrolled inside the overlay while its list scrolled
inside the dialog — two scrollbars, one of them moving the modal itself. The
cap `Modal2` appeared to have, `max-h-screen-80`, is defined nowhere in the
tailwind config, so it never applied to anything.

`fixedHeight="viewport"` is a new value that stands as tall as the window
allows. Deliberately a definite height rather than a max-height: bodies here
size against the box with `h-full` / `grow min-h-0` and scroll inside it, and a
max-height leaves them nothing to resolve against — they grow past the surface
instead. Every existing size keeps the height it has today, so no other modal
moves. The two classes that resolved to nothing are removed.

The prompts modal and the assistant settings modal take the new value; both
already scroll inside themselves.

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

* fix: pin read_skill's gate in its test, and say who a delete affects

The refusal `read_skill` gives for a path that is not a skill changed shape —
it checks the workspace listing now, not just the off-switch — and its test was
still asserting the old wording against an unmocked listing.

The delete confirmation said everyone "who selected it" loses the skill, which
stopped being true when skills started defaulting to on.

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

* fix: keep the keyboard walk when the list scrolls under the pointer

The mouse takes the skills list back on a real movement over it, not on
`mouseenter`. The browser fires that one whenever rows arrive under a
stationary pointer — every scroll the keyboard itself causes, and every folder
collapse — so walking Down past the bottom of the list handed control back to a
mouse nobody had touched, and the next press restarted at the top.

Also from the review round: the "+" menu sorted skills on-first, a key that is
constant now that they start on, and pushed the one row it did move — a skill
just turned off there — out of the shortcut that turns it back on. It orders by
path. The remaining "selection" wording follows the vocabulary the rest of this
change moved to.

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

* fix: carry the keyboard walk on from the row the mouse left it on

Handing the list to the mouse dropped the highlight, so the next arrow press
started again at the top. It moves to the row under the pointer instead —
invisible while the mouse leads, since drawing and acting both wait on the
keyboard being in charge, and exactly where someone would expect the walk to
carry on from.

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

* fix: cap the AI prompts modal from its own call site

Reverts `Modal2` and the assistant settings modal to what they were. The
prompts modal asks for `xxl`, 1000px, which is taller than a laptop window, so
the dialog scrolled inside the overlay while its list scrolled inside the
dialog. It now passes `max-h-[80vh]` through the `css.popup` the component
already forwards.

The height stays definite underneath, which is what lets the list bound its own
scroller, and nothing outside this one modal changes.

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

* refactor: drive the skills list highlight with useListHighlight

The Tools section next door already had this: `useListHighlight` owns the
highlighted index, wrapping, `scrollIntoView`, and the rule that a scroll under
a resting pointer must not hand the list back to the mouse — the bug this
section rediscovered the hard way. Reusing it drops the parallel implementation.

What stays local is what is actually a tree: Left and Right fold a folder or
step into it, Space flips the switch under the highlight, and Enter opens the
lit skill. `restingIndex` is what keeps the highlight on a folder through a
fold, where a search would instead send it back to its top hit.

The keys are answered at the window rather than on the list: leaving the editor
parks focus elsewhere, and a container-scoped handler goes silent when it does.
`move` is now returned by the composable, for the step into a folder's children.

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

* fix: stop the fold's sticky row resetting the keyboard walk

`stickyKey` is read through `restingIndex`, which `useListHighlight` calls
inside the effect that reacts to the row count. As `$state` it was also a
dependency of that effect, so clearing it on the next arrow re-ran the effect
and wrote the highlight back to nothing: after collapsing a folder, one Down
lit nothing and the one after it started again at the top.

It is a plain variable now, read when the effect runs and invalidating nothing.

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

* fix: keep one lit row, and keep the fold's sticky row to its fold

Three from the review of the `useListHighlight` swap:

The sticky row a fold takes is now given up as soon as that fold has rendered.
Held until the next arrow, it pulled the highlight back to that folder on any
later change — another fold, a save, a delete, a workspace switch.

Space and Enter on a focused control bring the highlight to that control's row
before the control answers them. A switch keeps focus after a plain click, and
the row drawn as highlighted was then a different one from the row that flipped.

Up and Down carry on from a row reached with Tab. `useListHighlight` cannot see
that by itself: `ListRow` puts the row's id on its outer div while focus sits on
the button inside it.

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

* fix: land the highlight on a named row rather than stepping to it

`move` counts steps from wherever the highlight is, and from nothing lit it can
only reach an end of the list — so the three places that meant "put it on this
row" (a row reached with Tab, the row of a focused control, a folder's parent)
sent it to the first row whenever nothing was lit yet. `useListHighlight` grows
a `moveTo` for naming the row outright, and those three use it.

The handler's own doc still said the keys are answered on the list; they went
back to the window when the editor's page transition proved able to take focus
away from it.

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

* fix: fold from the header click the way every other fold does

The header's own click wrote `collapsed` directly instead of going through
`fold`, so the row count changed with no row named to keep: the highlight reset,
and since the highlight is the header's only hover feedback, it went flat under
a pointer that had not moved and stayed flat.

Also from the round: a duplicated `svelte-ignore`, the missing one on the header
wrapper that takes `onmouseenter`, and a trailing comma prettier wanted gone.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 10:27:37 +00:00
Alexander PetricandClaude Fable 5.1 fd46450e9c add a self-host CTA to the cloud premium plans settings page (#11032)
* feat: add a self-host CTA to the cloud premium plans settings page

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wg3HU8oPp8rGVHo1oWvrSB

* fix: render the self-host CTA icons at one size

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wg3HU8oPp8rGVHo1oWvrSB

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 10:02:09 +00:00
fce635d3c4 feat: guest app execution mode, a role that takes no seat (#10929)
* feat: guest app execution mode, a fourth role that takes no seat

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

* fix: make the guest grant a server-minted label, not a declarable scope

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

* chore: pin ee-repo-ref to the guest session companion branch

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

* fix: close the relabel hole, guest embed tokens, read-path switch, custom-path entry

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

* fix: guest tokens are not rescopable and guest embed tokens keep the sentinel

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

* fix: guest-derived tokens share one constraint set; gate sign-in on guest discovery

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

* fix: the label alone governs a guest; refuse guests with accounts; unserialize discovery

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

* fix: guest discovery fails closed; SAML aborts if the guest cookie write fails

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

* refactor: enforce the guest switch once at the auth door; sign-in for a guest of another app

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

* fix: guest app-mode decided once at the on-behalf resolver; clear a stale guest session before offering another app's sign-in

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

* fix: a guest may use anonymous apps; await the stale-session logout; trim comments

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

* fix: a guest's path confinement waits for the app's mode, so anonymous apps stay open to it

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

* fix: guest target survives http (Lax cookie), rides SAML RelayState; tell account holders on arrival

* fix: a guest uses an anonymous app as itself; S3 uploads confined by app mode

* fix: a guest upload needs an app policy; a missing app does not skip the confinement

* fix: guests are gated on the Enterprise plan server-side; pin ee-repo-ref

* fix: the guest plan gate fails closed on non-enterprise builds; settings report the effective switch

* fix: guest controls read the plan, not the key; gate the guest tests on the features they need

* docs: tighten the guest session invariant comments

* feat: 100 free guests per 30 days, then a quarter seat each on Enterprise and a hard cap elsewhere; superadmin guest list; refusals reach the page

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BayTppRCstWX6qTf3LMco5

* fix: the cap is exact, an account ends a guest session at the door, popups close, and guest mode survives the CLI round trip

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BayTppRCstWX6qTf3LMco5

* feat: a superadmin switch over guests for the whole instance; the pre-existing-user flag keeps its meaning

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BayTppRCstWX6qTf3LMco5

* fix: drop the dead guest-access helper, name the instance setting once, guests tab states, CE save order

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BayTppRCstWX6qTf3LMco5

* fix: a guest app path is refused at the mint if it could widen the scope; the instance toggle waits for its reload

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BayTppRCstWX6qTf3LMco5

* fix: guests stop at the launched-by-me job grant; canonical app paths at the mint and discovery; the toggle ends on the stored value

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BayTppRCstWX6qTf3LMco5

* fix: only the scope grammar's own characters bar an app path from guests, refused at deploy as well as at the mint

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BayTppRCstWX6qTf3LMco5

* fix: the deploy-time guest path guard checks the destination of a rename and refuses a leading slash

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BayTppRCstWX6qTf3LMco5

* fix: a workspace rename keeps the guest switch; the rename guard reads the deployed mode under the row lock

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BayTppRCstWX6qTf3LMco5

* fix: guest_activity follows a workspace rename and goes with a workspace delete

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BayTppRCstWX6qTf3LMco5

* chore: pin ee-repo-ref to the state-bound guest target

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BayTppRCstWX6qTf3LMco5

* chore: pin ee-repo-ref; the guest cookie is never cleared by a callback

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BayTppRCstWX6qTf3LMco5

* docs: the workspace-scoped guest_activity delete moves an instance-wide count; assert the mint records the guest

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BayTppRCstWX6qTf3LMco5

* test: the seeded allowance is a day old, so only the mint can write today's guest_activity row

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BayTppRCstWX6qTf3LMco5

* chore: update ee-repo-ref to 1a10132e4f3cb442c7d0c2cf6e5d92d150bf6e07

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

Previous ee-repo-ref: 32841072aa396bff91d30bd91854fa348cb3c439

New ee-repo-ref: 1a10132e4f3cb442c7d0c2cf6e5d92d150bf6e07

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-09-04 22:47:28 +02:00
GuilhemandClaude Opus 5 5d5ad4e897 feat: edit folders and groups in a drawer that saves once (#10873)
* fix: portal the confirmation modal so drawers cannot cover it

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

* fix: log a folder acl grant under the permission it granted

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

* fix: keep a table's actions column at its right edge

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

* feat: edit a folder in a drawer that saves once

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

* refactor: call the people on a folder or item members

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

* fix: edit a folder against the workspace the drawer targets

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

* refactor: drop the now-unused sticky actions column

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

* docs: correct the script editor drawer's modal placement note

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

* fix: pin the actions column without losing the row's hover tint

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

* feat: show the pinned column's seam only while the table overflows

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

* fix: draw the pinned column's seam as a shadow so it does not scroll away

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

* fix: fade the pinned column's tint in step with its row

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

* refactor: address review nits on the folder editor and pinned cell

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

* fix: keep the folder draft across a user-store refresh

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

* refactor: extract and test the folder draft's dirty check and permission diff

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

* fix: stop the folder editor showing state the server refused

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

* fix: keep a folder draft that no request ever reached the server

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

* fix: keep unapplied folder edits dirty when a save partially fails

* fix: block folder form edits while a save is in flight

* fix: commit a typed folder label before save snapshots the draft

* fix: count a typed folder label as an unsaved change

* fix: keep escape in the label input from closing what encloses it

* fix: capitalize folder table headers and drop a dead portal target

* refactor: make the confirmation modal portal opt-in per call site

* docs: name the stacking context that actually traps the discard dialog

* fix: report a half-landed member removal so the baseline reconciles

* feat: edit a group in a drawer that saves once

* fix: freeze the group name once the group exists

* fix: revoke the caller's own group acl last so the rest of the save is authorized

* docs: state the group call-ordering invariant once

* fix: report a failing post-save reload instead of dropping the rejection

* fix: hand the folder list reload back so a failure is reported

* fix: treat a rejected group create as inconclusive and catch a throwing onSaved

* revert: stop inferring a group was created from its name being taken

* fix: say when a failed group create may have saved the group anyway

* fix: key the may-have-been-created hint on the name conflict, not the status

* fix: skip the may-have-been-created hint when the group is known to exist

* feat: open a folder's group member from its row

* fix: stop showing the caller as an admin when the read failed

* fix: give up the caller's own folder admin last, and label a create as one

* fix: drop a folder member's acl before its owner entry

* fix: remove a folder owner before their acl, and correct the rls rationale

* docs: say the refusal is on the caller's last admin handle

* fix: defer only the folder rows the caller is an admin through

* docs: describe callerOwners as what the caller passes in

* docs: drop the call-site restatement of the diff's own invariant

* docs: record manager as a legacy group role

* fix: treat a sent request as possibly committed when reconciling

* fix: reconcile on any failed edit, and compare members as a set

* fix: keep write access when only the reconcile read fails

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 23:22:55 +02:00
Ruben Fiszel 4808f21b6b add 180 and 365 day token expiration options (#10920) 2026-09-01 12:59:50 +00:00
7dd88c470c fix: unify billable seat counting and prevent fork subscriptions (#10818)
* fix: unify billable seat counting and prevent fork subscriptions

* fix: authorize candidate before reading its plan, scope seat breakdown

* chore: pin ee ref for the stripe checkout fork guard

* fix: grant the billable_member view and widen the paid-plan check

* refactor: keep the seat rule in rust instead of a view and function

* docs: correct the attach guard summary after widening the plan check

* revert: keep cloud out of the ci test feature set

* chore: update ee-repo-ref to 9ff97cd818e85940fec282c92161e98c1b8583e2

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

Previous ee-repo-ref: 0ec0b42565a41f271a45bf24a93467d110c36df3

New ee-repo-ref: 9ff97cd818e85940fec282c92161e98c1b8583e2

Automated by sync-ee-ref workflow.

---------

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 17:13:02 +02:00
e866b68cdf feat: surface execution usage in the sidebar and explain what an execution is (#10760)
* feat: surface execution usage in the sidebar and explain what an execution is

Users read "executions" as a job count and are surprised by the real number,
which meters a second of compute. Every place the UI prints an execution count
now says so, and the sidebar carries a usage meter for the quota that will bind
first.

Adds SidebarUsage at the bottom of both sidebar surfaces: a ring in the
collapsed rail, a labelled bar when expanded, and a modal breaking down every
quota. On the free tier it meters the per-user and per-workspace 1000-execution
caps; on a paid plan it meters workspace usage against the executions the
workspace's seats already include.

Item.tooltip was inert on disabled dropdown rows: DropdownSubmenuItem rendered
the info icon inside the disabled button, which swallows hover, and the row's
own title attribute shadowed any wrapper title. Both renderers now fall back to
a wrapper title the way DropdownV2Inner already intended.

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

* fix: keep the usage meter tied to the workspace it describes

isPremiumStore held the previous workspace's tier across a switch, which no
consumer noticed while it only gated affordances — the usage meter is the first
surface to render a number from it, and would have shown a paid seat quota for a
free workspace. It is now undefined until the active workspace's tier is known,
and a superseded response no longer writes.

The seat fetch had the same shape: a slow response for the workspace we left
overwrote the current count and stayed wrong until the next switch.

The usage wrapper also carried the padding the brand-mark row used to own, which
shifted the sidebar bottom by 4px on every instance where the meter renders
nothing. The component owns its own padding instead.

Names the collapsed ring for assistive tech, which otherwise saw an unlabelled
button whose only signal was the arc's color.

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

* fix: scope the usage meter to the billing workspace and a known tier

A fork's usage, tier and bill all resolve to its billing root, but its member
list is deliberately a subset of the root's, so counting fork members metered
root usage against a fork-sized cap and invented billed-seat overages. Seats now
come from the billing root, and the paid meter stays hidden when that root is
not visible from the fork.

The tier was cleared only after the user-store round-trip, so the meter rendered
the previous workspace's tier for the length of it — a free→paid switch showed
the 1000-execution hard cap on a paid workspace, not a race but every time. The
clear now happens before the first await.

Workspace usage had neither guard: a superseded response overwrote the store
permanently, and the meter is the first surface to print that number as its
headline rather than bury it in a dropdown.

The free-tier counters keyed off `!$isPremiumStore`, which reads an unknown tier
as free and flashed the free-tier blocks during a paid-to-paid switch. They wait
for a known tier instead.

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

* fix: never render an unresolved execution count as zero

The workspace-usage clear wrote 0, which is a real usage value: an in-flight or
failed fetch rendered as a green "0/1,000" bar, and a rejection left it there for
the session because loadUsage had no failure path. Usage is now undefined until
it resolves, each endpoint is assigned on its own so one failing leaves the
other's number intact, and a quota is listed only once its own usage, tier and
cap are known. The legacy counters show an em dash rather than a fabricated 0.

The fork gates read an unknown tier as not-premium, so clearing the tier on
switch made the fork entry point disappear for the length of the fetch on a
paid-to-paid switch. They hold while the tier is unknown.

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

* fix: read the usage endpoints as numbers, and fall back to the free tier

Both usage endpoints serve text/plain, so the client hands back a string despite
the generated `number` type. Interpolation and arithmetic coerced it, which is
why nothing noticed before, but `toLocaleString` on a string returns it
unchanged — a five-figure count rendered without its thousands separator against
a formatted cap.

A failed tier fetch left the tier unknown for the session, and consumers hold
premium-only affordances through the unknown window so a free workspace kept
offering them. It falls back to the free tier instead.

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

* fix: keep an unknown tier unknown, and refresh the seat cap on demand

Falling back to the free tier on a failed tier fetch fixed the affordance gates
by lying to the meter: a paid workspace's real five-figure usage rendered
against the 1000 hard cap, red, under "jobs stop running for the rest of the
month". The tier stays unknown instead, and the two consumers get what each
needs — the meter hides, while affordances read `maybePremium`, which holds
through the pending window but fails closed once the fetch has failed.

Membership changes elsewhere don't reach this component, so the seat cap could
show an overage against a cap that had since grown. It re-resolves when the
modal opens, which is when the number is read rather than glanced at.

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

* fix: let anything showing executions re-read them

The counters were written in one place, the root layout, on a workspace change
only — so a tab left open all day showed the count from whenever the workspace
was opened, and the modal-open refresh could only reach the seat cap, leaving a
freshly computed denominator over a stale numerator.

Moves the fetch to lib/usage.ts, next to the stores it writes, so the meter can
refresh both numbers when its modal opens. Seats follow a membership signal that
WorkspaceUserSettings bumps where it already refetches after every mutation, so
the cap stops lagging a role change without either side owning the other.

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

* fix: order concurrent usage and seat refreshes

The workspace id doesn't order two requests for the same workspace, and both
refreshes can now have two in flight: usage through A→B→A or a modal-open
refresh landing on one already running, seats through a membership bump
arriving mid-request. An older response could win and restore the count it
replaced. Each refresh takes a generation and only writes if it is still the
newest.

The membership signal also fired on a plain read, so opening the users tab made
every consumer re-fetch a list identical to the one it held. It bumps on an
observed change to the member set instead, never on the first read.

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

* fix: count only billable seats, and order the tier requests

The cap counted every member row, while the backend bills
`NOT disabled AND NOT is_service_account` — a workspace with service accounts
got an inflated included quota, which hides a real overage rather than inventing
one. The seat basis matches `count_paid_seats` now, and the membership signature
carries both fields so enabling or disabling a member re-resolves the cap.

The tier fetch was the one refresh still ordered by workspace id alone, so a
late failure for a workspace could raise the failure flag over a tier a newer
request had already resolved. It takes a generation like the other two.

The membership signature is keyed by workspace: this page survives a workspace
switch, and comparing one workspace's members against another's reported a
membership change where only the workspace had changed.

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

* fix: compare the member set only against the same workspace's

Qualifying the signature with the workspace put the workspace inside the value
being compared, so a switch made every comparison unequal and bumped the version
unconditionally — the opposite of the intent, and worse than before the key. The
workspace is the key now, not part of the payload: a different one has nothing
to compare against and re-baselines silently.

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

* refactor: hold the usage and tier fetches in resources

Every one of these values belongs to a workspace but lived in a bare store, so
each writer and reader re-derived "does this still describe what I'm rendering?"
by hand. Nine sites did, and the ones that forgot were most of this branch's
review findings: three stale-workspace overwrites, three A→B→A races, and two
placeholders (`0` executions, `false` tier) that read as real data because an
in-band value was standing in for "not known".

`resource` from runed — which frontend/AGENTS.md prescribes for async data, and
which ~80 files here already use — supplies all three properties as behaviour
rather than convention: a superseded fetch is discarded, the value resets when
its key changes, and loading and error are states instead of magic values. The
seat count keys on the billing root and the membership version, so both a
workspace switch and an added member re-resolve it.

That removes three generation counters, two workspace trackers, and the manual
clear-and-compare around each fetch. What remains is one publish site that
asserts the value still carries the active workspace before it reaches a store.

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

* fix: order the resource responses that runed does not

The refactor claimed `resource` discards a superseded fetch. It does not: its
only ordering is an AbortController whose signal the generated client cannot
consume, and `current = result` runs unconditionally once a fetch resolves. So a
late answer for a workspace we had left still landed in `current`, and the
publish site — which trusted `current` — cleared the value on screen for the
workspace we were on. That reinstated the races the generation counters had
covered.

`loading` was standing in for the missing ordering, and it cannot: it is also
true during a `refetch()`, when `current` is still the right value. Gating on it
meant every re-read blanked the meter, and clicking it unmounted the modal that
same click had opened, since both sit behind the quota it had just cleared.

Values now carry the scope they describe and `scopedValue` keeps the newest one
matching the active scope, so a superseded answer neither publishes nor erases,
and a re-read leaves the display alone. The account-wide user counter keys on
the account, so a workspace switch no longer clears it.

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

* fix: order responses within a scope, not just across scopes

The tag carried what a value described but not when it was asked for, so two
fetches for one scope — a refetch landing on an in-flight load, or a second
membership invalidation — were indistinguishable and the older won if it landed
last. That left the seat cap reading the pre-change number until the next bump
or switch, which is the stale cap the generation counters had covered.

Widening the tag to the resource key would have fixed it by blanking the bar on
every membership change, so the issue order travels alongside the scope instead:
`tagged` stamps each request as it is issued, and only a strictly newer answer
for the current scope replaces the held one.

The unit tests now cover the same-key case they missed; both new ones fail
against the key-only guard.

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

* fix: ignore a user list a newer read has overtaken

`lastSeen` was written unconditionally after the await, so a response for a
workspace already left overwrote the baseline for the workspace on screen. The
next real membership change there then compared against a baseline that was
never taken for it, re-baselined silently, and never bumped
`workspaceMembershipVersion` — leaving the sidebar on the old seat cap. The
`users` assignment had the same hole: an overtaken list could paint over a
newer one.

Both now go through a single check: a read whose issue order is behind the last
applied one is dropped before it touches either.

Also trims the two `scopedValue` docstrings and the membership rationale to the
four lines AGENTS.md allows, and records there that a failed refresh keeps the
last successful value rather than blanking.

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

* fix: do not claim a plan before the tier resolves

Widening `isPremiumStore` to `boolean | undefined` left `UserMenu`'s `{:else}`
catching the unresolved state: with the tier still in flight, or after the
request failed, a free workspace was told it was on the "Premium plan". Both
branches under that block assert a plan, so the block now renders only once the
tier is known — which also keeps the bordered divider from appearing empty
while it resolves.

Verified against the running instance with the tier stubbed slow: unresolved
shows neither branch, `false` shows the free counters, `true` shows "Premium
plan". Reverting the guard reproduces the wrong label at 300ms.

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

* test: pin how a late answer orders against the read that replaced it

Returning to a scope whose earlier read is still in flight is the one case the
guard resolves by scope rather than by sequence, and the suite only covered it
with nothing outstanding. It now covers the late answer itself: it stands while
it is the only value describing the scope, the read issued on returning
supersedes it, and it cannot come back afterwards.

Also gives the meter the explicit `type="button"` the sibling sidebar rows use.

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

* fix: size the modal's plan button with unifiedSize

`size` is deprecated on `Button`. `unifiedSize="sm"` renders the plan button at
the same height and weight as the modal's own Close button.

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

* fix: match the plan button to the modal's own action button

`unifiedSize="sm"` is `h-7`, and the Cancel button `Modal` renders beside it is
`px-3 py-[7px]`, i.e. 32px — so the two sat 4px apart. `md` is the unified size
that lands on 32px, which pairs them without putting a deprecated prop back.

Measured both boxes rather than the new one alone: 32px and 32px, same top.

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

* feat: instrument the execution meter, and bill-align PremiumInfo's seats

The meter's only interaction is opening the modal, so that is what it counts:
`usage_meter/opened`, keyed by the plan tier and the quota that was tightest —
`free:user`, `free:workspace`, `paid:workspace`. The full set is a type next to
the call site so the vocabulary stays readable in one place.

The pair is registered in `FEATURE_USAGE_KINDS` (windmill-ee-private), without
which the post is dropped with a 204 and records nothing. Verified both halves:
the browser posts
`{"feature":"usage_meter","kind":"opened","key":"free:user","value":1}`, the
running EE image drops it because its registry predates the entry, and
`is_recordable_event` accepts it once the entry is there.

`PremiumInfo` computed its seats from an unfiltered user list, so the billing
page counted disabled members and service accounts that `count_paid_seats` does
not bill. Same filter as the sidebar's cap now.

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

* chore: point ee-repo-ref at the usage_meter registration

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

* fix: read the member list before the seat rows that depend on it

`loadPremiumInfo` reads `users` after its own await and nothing recomputes the
seat rows when the list lands, so whenever `premium_info` won the race the page
rendered zero developers, zero operators and zero seats and kept them. The list
is now fetched first, and a failure to read it no longer costs the rest of the
page.

Also refreshes the registered-action inventory in `docs/feature-telemetry.md`,
which the new pair makes 21 across nine features.

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

* docs: scope the seat comment to the counter it matches

The comment claimed parity with the seats actually charged, which nothing in
this repo computes: `count_paid_seats` documents itself as counting provisioned
members rather than billing's active-user population, and the Stripe quantity
is not derived here. What the filter buys is agreement with that counter.

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

* chore: update ee-repo-ref to c6902ec2c51dc0ce30962afbfab3e456c5d9b831

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

Previous ee-repo-ref: bbc48fae6b73b6d72fe2e125e6003794a4ece167

New ee-repo-ref: c6902ec2c51dc0ce30962afbfab3e456c5d9b831

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>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2026-08-20 22:17:15 +02:00
hugocasaandClaude Opus 5 c09de594b6 feat: version resource values with history, diff and restore (#10596)
* feat: version resource values with history, diff and restore

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

* fix: record resource versions in a trigger so direct writes are covered

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

* fix: show the selected version's value and tighten history write access

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

* perf: gate resource version recording in trigger WHEN clauses

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

* feat: clear a resource's past versions, and address review nits

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

* fix: restore the displayed version and keep author attribution on pooled writes

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

* fix: scope history to the selected workspace and gate clearing on ownership

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

* fix: gate restore on write access and clearing on the signed-in workspace

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

* refactor(frontend): share the version-history row between script and resource drawers

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

* perf: trim resource version history in the monitor sweep, not on write

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

* fix(frontend): match the script versions drawer shell for resource history

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

* perf(frontend): highlight version values instead of mounting monaco

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

* fix(frontend): match the script drawer's code preview presentation

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

* fix: rank version trim in one windowed pass instead of a correlated delete

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

* refactor(frontend): treat the newest version as current by position

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

* perf: gate the resource version trim to an hourly sweep

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

* fix: unnest the version row action and correct the trim cadence docs

* perf: cap the history listing and use sets for reference lookup

* feat: warn when a resource is written more than 60 times a minute

* fix: lower the resource write advisory to 20 per minute

* fix: discard stale history loads and never diff against an unread value

* fix: correct the write advisory boundary and document the eviction lock

* fix: read history and the live value from one snapshot

* refactor: read the drawer's diff baseline from versions, not the live resource

* fix: open the history drawer with no version selected

* fix: disarm the clear confirmation and clear the pane when the selection moves

* fix: explain the missing diff and drop a guard that can no longer fire

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 21:32:18 +02:00
Ruben FiszelandClaude Opus 5 6d21d30242 fix: bound ScopeSelector badge heights and correct three scope-chip defects (#10523)
* fix: cap ScopeSelector badge container heights

The "Selected Scopes" summary and each domain header rendered their badges
in unconstrained flex-wrap containers. With path-restricted scopes the badge
strings run long, so a handful of them wrapped over many rows and pushed the
scope domain list and the token form's action buttons below the fold.

Cap the summary at 8rem and the per-domain header at 4rem, both scrolling
vertically past that.

Fixes WIN-2318

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

* fix: correct scope chip disabled state, summary readability and domain widening

Follow-ups to #10517, all in ScopeSelector:

- The shared scopeChip snippet bound the component-level `disabled` for its remove
  button, but a scope card computes `isDisabled = disabled || isScopeDisabled(...)`.
  A scope superseded by its `:write` sibling greyed out its checkbox and its path
  button while the `x` on its path chips stayed live, so those paths could still be
  destroyed. The effective state is now passed in.

- Truncating a chip hides the paths being granted, which is the point of the
  Selected Scopes summary. Chips there now wrap instead; the tight per-domain header
  and the per-scope path lists keep truncating.

- Ticking a domain checkbox re-added its write and run scopes bare, dropping any
  resource paths configured on them: a token restricted to one path silently became
  a token for the whole domain, and unticking did not bring the paths back. The
  checkbox already reads as checked when those scopes are path-restricted, so it now
  carries the paths over. Both branches of the requires_resource_path conditional it
  replaces pushed the same bare value, so nothing was reading that flag.

- The path popover tooltip explained that no paths means full access but never that
  each path added widens the scope's reach, which is what reads backwards next to
  the "Add path" button.

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

* fix: correct the path tooltip and drop the domain-header height cap

Review nits from #10523.

The tooltip claimed every path added widens the scope's reach, which is only true
from the second path on: the first replaces a bare, full-access scope with a
path-restricted one, narrowing it. Stating what each state means avoids the
direction question entirely.

The domain header no longer caps its height. Truncation holds every chip to one row
and a domain has a handful of scopes, so the row cannot run away, while the cap put
a 64px scroller inside the scrollable domain list that swallowed wheel events
crossing it — and clipped mid-row, since 64px is not a multiple of the row height.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 20:18:50 +02:00
Ruben FiszelandClaude Opus 5 ae2f584de8 fix: keep the token scope builder inside its panel when scopes get long (#10517)
* fix: keep the token scope builder inside its panel when scopes get long

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

* fix: label the scope path popover 'Add path' once paths exist

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 19:13:36 +02:00
Ruben FiszelandClaude Opus 4.8 97f4477069 feat(forks): let a fork's creator manage developers on it without being an admin (#10166)
* feat(forks): let a fork's creator manage developers on it without being an admin

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

* fix(forks): bind the fork-creator grant to the member's parent username and lock the delete

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 15:13:36 +02:00
Ruben FiszelandClaude Opus 4.8 8343203ec2 feat(mcp): add multi-workspace MCP tokens via the gateway endpoint (#10043)
* feat(mcp): add multi-workspace MCP tokens via the gateway endpoint

A single MCP token with no bound workspace (workspace_id NULL + mcp scope)
now works across every workspace the token owner can access, served through
the existing /api/mcp/gateway endpoint. This avoids having to register one
MCP server entry per workspace in clients like Claude/Cursor.

In multi-workspace mode the runner exposes a synthetic `list_workspaces`
tool plus the generic API endpoint tools, each workspace-scoped one gaining
a required `workspace_id` argument (mirroring the proxy pattern users built
externally). Per-workspace scripts/flows are not enumerated to avoid
flooding the tool list — they are run via runScriptByPath/runFlowByPath
with an explicit workspace_id.

Auth is resolved per tool call: the gateway middleware detects a
workspace-less mcp token and marks the request MultiWorkspaceMcp, and the
runner resolves a per-workspace ApiAuthed from the raw token via the
AuthCache (validating membership; superadmins may act in any workspace).
Single-workspace tokens are unchanged.

Frontend: the MCP token creation flow gains an "All workspaces" option that
produces a workspace-less token and the gateway URL.

Fixes WIN-2153

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

* test(mcp): cover multi-workspace endpoint tool transformation

Unit tests for endpoint_tool_to_mcp_tool_multi and list_workspaces_tool:
workspace-scoped tools gain a required workspace_id arg, global tools are
left unchanged, workspace_id is not duplicated, and list_workspaces takes
no arguments.

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

* fix(mcp): forward script/flow args for runScriptByPath/runFlowByPath

These endpoints have an additionalProperties body (no declared properties),
so build_request_body previously returned an empty body and dropped every
script/flow argument. This was latent for the per-path run endpoints and
became load-bearing in multi-workspace mode, where scripts/flows can only be
run via runScriptByPath/runFlowByPath — parameterized runs silently lost
their arguments.

build_request_body now forwards all arguments not consumed by a path/query
parameter for pass-through (additionalProperties) bodies, keeping the strict
declared-only behavior for endpoints with explicit properties. The runner
strips the synthetic workspace_id argument before dispatch so it can't leak
into the forwarded body.

Reported by Codex review on #10043.

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

* feat(mcp): note workspace_id requirement in multi-workspace tool descriptions

Workspace-scoped tools already gain a required workspace_id parameter (with
its own schema description) in multi-workspace mode, but the tool's prose
description was unchanged. Append a note so models/clients that read the
description text know to pass workspace_id (and to call list_workspaces
first). Global tool descriptions are left untouched.

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

* refactor(mcp): trim multi-workspace tool/arg descriptions

The workspace_id note repeats across every workspace-scoped tool in each
tools/list, so keep it terse: description suffix "Requires `workspace_id`."
and arg description "Target workspace id (from list_workspaces)." to avoid
spending tokens on repeated boilerplate.

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

* fix(mcp): enforce script/flow path scopes for multi-workspace run-by-path

In multi-workspace mode runScriptByPath/runFlowByPath are the only way to run
scripts/flows, but they were authorized against the endpoint scope only — never
the caller's mcp:scripts:/mcp:flows: path scopes. A granular token could run
items outside its allowed paths (e.g. mcp:scripts:f/team/* + mcp:endpoints:*
running f/other/secret), and a mcp:endpoints:* token could run arbitrary
scripts.

Now these two endpoints are authorized by the script/flow scope of the
requested path (matching single-workspace mode's per-item tools): exposed in
list_tools only when the token grants some script/flow (McpScopeConfig::has_any),
and at call time the path is checked via is_allowed("script"/"flow", path).

Verified e2e: mcp:scripts:f/team/* runs f/team/* but is denied f/other/*;
mcp:endpoints:* alone no longer exposes or runs run-by-path.

Reported by Codex + Pi review on #10043.

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

* fix(mcp): deny run-by-path for mcp:favorites multi-workspace tokens

mcp:favorites sets granular=false, so the previous run-by-path scope check
(gated on `granular`) was skipped entirely — a default "Favorites only"
all-workspaces token could run any script/flow by naming its path, bypassing
the favorites restriction.

Favorites are an enumerated set reachable only through per-item tools, not by
arbitrary path, so they grant nothing for run-by-path. has_any() now returns
true only for mcp:all (not favorites), and the call-time check drops the
`granular` gate and relies on is_allowed() directly (already false for
favorites, true for mcp:all, pattern-matched for granular).

Verified e2e: mcp:favorites no longer exposes or runs run-by-path; mcp:all
still runs; granular script scopes still path-enforced.

Reported by Codex review on #10043.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 23:32:30 +02:00
GuilhemandClaude Opus 4.8 95031903eb feat(sessions): v2 unified sidebar with family/fork scoping and preview router (#9816)
* feat(sessions): prototype session-mode layout wrapper (design exploration)

Do not merge — design exploration of an optional full-page 'session mode' layout for AI sessions.

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

* feat(sessions): add full-screen toggle for the session panel

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

* feat(sessions): workspace-tree rail with browse mode and collapsible sidebar

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

* feat(sessions): restore sessions page with iframe preview of current view

Roll back the session-mode layout wrapper: sessions is a dedicated /sessions
page again rather than a layout toggled over the live app. Opening a session
from a Windmill page captures that page as the session's preview target; the
page shows the chat beside a preview panel that iframes the target, with a
breadcrumb and full-screen toggle.

- Remove SessionShell wrapper and the sticky sessionLayout flag; +layout.svelte
  always renders the normal global sidebar. Sidebar components introduced
  alongside the wrapper are kept for the upcoming sidebar rework.
- sessionMode.svelte.ts: per-session preview-URL map (captureSessionView /
  sessionPreviewUrl) + withMenuHidden to drop the previewed page's own sidebar
  via the nomenubar flag.
- Drop the #content sidebar gutter (pl-12/pl-40) when the menu is hidden, so
  the nomenubar preview fills the panel edge-to-edge.
- SessionPicker: activate() navigates to /sessions; createAndOpen() seeds the
  new session's preview from the current page.

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

* feat(sessions): add exit (X) button to chat header

Add a close button at the top-right of the session chat header that leaves the
sessions page and navigates to the session's target (the previewed page), so
exiting lands on exactly what was being previewed, full-screen with the sidebar.

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

* feat(sidebar): promote workspace picker and widen the sidebar

Replace the Windmill logo header with the workspace picker so the active
workspace is the sidebar's anchor: show the workspace name (not the id) in a
stronger weight, with a down-chevron and a bottom-aligned dropdown. Add the
same dropdown chevron to every other sidebar menu trigger (Favorites, User,
Settings, secondary/Help groups) via an opt-in MenuButton option, and widen
the expanded sidebar from w-40 to w-48 (content offset kept in sync).

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

* feat(sessions): collapsible preview panel + sidebar session entry polish

Add a collapse control to the sessions preview panel (top-left, matching the
legacy editor's PanelRightClose), animated with an x-axis slide. The panes
carry no explicit size so Splitpanes auto-distributes — the chat fills the
width when the preview collapses and splits evenly when both are shown. When
collapsed, a floating "Open side panel" Button (top-right) brings it back.

Also gather the AI sessions section into the Favorites/Search container via a
new embedded mode on SessionPicker, replace the small "+" with a full sidebar
"New AI session" entry, and drop the chat header's exit (X) button.

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

* feat(sessions): split family/fork picking with a global breadcrumb

Separate workspace-family selection from fork selection. The sidebar
workspace picker now lists families (roots) only and shows the active
family name even inside a fork. A persistent `family · fork` breadcrumb
lives in the global logged layout (WorkspaceBreadcrumb, rendered via a
new AiChatLayout topBar snippet): the fork segment opens the fork picker
popover, staging a pending fork on a draft session (the old in-chat
SessionWorkspaceBar semantics) or switching workspace directly elsewhere.
WorkspaceFamilyPicker gains onRequestCreateFork to route create-fork to
the global fork modal in non-session contexts.

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

* revert(sessions): drop the global fork breadcrumb top bar

Remove WorkspaceBreadcrumb and its AiChatLayout topBar wiring; restore
the in-chat SessionWorkspaceBar for draft fork-picking and the original
WorkspaceFamilyPicker. The sidebar workspace picker stays family-only
(roots, no forks listed).

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

* feat(sessions): family/fork-scoped sidebar with scope header

Restructure the sidebar into a family-scoped region (workspace family
header → New AI session → session list) and a workspace-scoped region
(a Fork scope header → Favorites + Search → workspace items), split by a
full-width divider. The new WorkspaceScopeHeader is a full-width
root/fork picker: accent-styled on a fork (text + faded border), with a
bottom "<workspace> settings" link; picking a different fork from a
session navigates home. The family header keeps the root's color when
inside a fork, and drops "Fork current workspace" / "Workspace settings"
(now surfaced via the scope header and the bottom Settings dropdown).
The session preview header shows "family · fork <page path>".

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

* fix(sessions): drop colon from "Workspace root" scope label

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

* feat(sessions): turn the preview breadcrumb into a page router

Every breadcrumb segment now opens a drill picker that lists workspace pages
(Home, Runs, Workspace settings, …) alongside scripts/flows/apps. Picking
either steers the preview iframe without leaving the sessions page. The
non-item case resolves to the page's real name (e.g. "Workspace settings").

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

* feat(sessions): tabbed preview with mounted tabs + Home quick-access

The preview is now a tiny tabbed browser: the first tab is pinned to the
session's view, "+" opens the router picker to add more, and every tab stays
mounted (stacked + visibility-toggled) so switching preserves each page's
state. Per tab, the commanded `url` is decoupled from the observed `loc` so
in-iframe navigation never reloads the frame. Home is also pulled up as the
first quick-access item in the router picker.

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

* feat(sessions): persist preview tabs with the session in IndexedDB

Save the open preview tabs (+ active tab) onto the session record so reopening
a session restores its tabs. Write-behind is debounced since a tab's observed
location churns as the user browses; transient (unsent) sessions skip it.

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

* perf(sessions): lazy-mount preview tab iframes

Only boot a tab's iframe the first time it's activated, then keep it mounted.
Restoring a session with N saved tabs now boots just the active tab instead of
N full Windmill apps at once.

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

* feat(sessions): fold the breadcrumb picker into the preview tabs

Drop the separate family·fork/path breadcrumb bar. The active tab now doubles
as its own router picker (click it to re-point the tab); inactive tabs switch
on click. Removes the now-unused PreviewRouterSegment.

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

* feat(raw-apps): auto-compact the editor when it opens narrow

On the first measured layout, if the editor container is under 800px, drop to
the merged single-pane view and retract the file sidebar (e.g. when shown in
the narrow session preview pane). Applied once on open; the sidebar is set
without persisting so it never overrides the user's saved preference.

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

* feat(sessions): auto-refresh preview tabs after mutating chat tools

Add a tool-completion hook in the shared chat dispatcher; the sessions page
subscribes and debounced-reloads every mounted preview tab when a write/deploy/
delete tool finishes (matched by verb prefix, so read/test/navigate tools skip).

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

* fix(sessions): keep nav bar hidden across in-preview navigation

The sessions preview iframes load pages with `nomenubar=true`, but the
layout recomputed `menuHidden` from the current URL on every navigation,
so a client-side nav inside the preview (an in-page link or redirect)
dropped the flag and the global nav popped back in. Make the hidden state
sticky for the document's lifetime when running inside an iframe; the top
window is unaffected so the oauth-callback toggle still works.

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

* fix(sessions): persist hidden nav across full reloads in preview iframe

The in-memory sticky flag was lost on a full document load inside the
preview (a navigation that drops the `nomenubar` query param), so the
global nav — including the mobile burger — reappeared. Store the sticky
state in sessionStorage so it survives full reloads within the iframe's
browsing context. The top window is unaffected.

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

* fix(sessions): refuse to mount sessions UI inside a preview iframe

A preview tab navigating back to /sessions would mount another sessions
page with its own preview iframes, nesting endlessly. When the page
detects it is running inside an iframe, render a stub that breaks out to
the top-level window instead of mounting the full UI.

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

* feat(sessions): Workspace ⇄ AI Sessions mode switch + workspace-decoupled session chat

Add a route-derived mode switch that flips the sidebar rail between the classic
workspace navigation and a dedicated AI-sessions sidebar, cleanly separating
sessions from the workspace nav.

- SessionModeSwitch (Workspace | AI Sessions) in the rail; session mode is
  exactly "on /sessions", so the switch just navigates in/out (sessionSwitch).
- Session chats target their own (possibly forked) workspace via
  AIChatManager.operatingWorkspace/workspaceResolver without mutating the global
  workspaceStore; "Acting on" header strip shown once a session has started.
- Flow editor AI button becomes "Open in AI session": saves the draft, then
  opens a new session targeting the current flow.
- New sessions: no default preview (empty state instead of iframing home, panel
  collapsed); preview-panel collapse persisted per-session on the record.
- Persist nav-rail collapse (manual toggle only) and drop the editor-route
  auto-collapse that fought it.
- Smaller fork picker; add a `preview` proxy so `vite preview` reaches the
  backend for production-build demos.

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

* fix(ai-chat): replay assistant turns verbatim so thinking blocks validate

The global AI chat reconstructs each assistant turn from an OpenAI-shaped
message, keeping only the thinking/redacted_thinking blocks and re-injecting
them at the front of the content array. When a turn interleaves thinking with
the native web_search tool and ends in a tool call, this reorders the thinking
blocks and drops the server_tool_use / web_search_tool_result blocks. Anthropic
validates each thinking block's signature against the blocks that precede it in
the latest assistant message, so the replayed turn is rejected:

  400 invalid_request_error
  "messages.N.content.M: `thinking` or `redacted_thinking` blocks in the latest
   assistant message cannot be modified. These blocks must remain as they were
   in the original response."

Preserve the full `finalMessage.content` verbatim (`_anthropicContent`) and
re-emit it unchanged, instead of extracting and reordering thinking blocks. Skip
the standalone text message that the streamer emits for the same turn (its text
is already inside `_anthropicContent`). The previous thinking-only path is kept
as a fallback for sessions persisted before this change.

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

* fix(sessions): give empty-state preview picker its own open state

* feat(sessions): render preview editors as components, not iframes

Introduce a PreviewTabHost seam that routes each preview tab to either an
in-process editor (the session's script/flow/raw_app target, reusing the
existing *EditorView wrappers + shared runtime) or an iframe fallback for
pages and other items, behind a uniform reload(). resolvePreviewTab classifies
a tab from its URL + the session target.

Also intercept in-iframe navigation to an editor route (logged layout
beforeNavigate): post the target up to the sessions page, which promotes the
active tab to the live editor component, so an editor is never booted inside
an iframe.

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

* fix(sessions): drive open_preview tool through the multi-tab model

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

* docs: plan SessionPreviewTabs deep module for sessions preview tabs

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

* refactor(sessions): own preview tabs in a SessionPreviewTabs deep module

Collapse the three drifting preview-tab copies (page-local state, session
record, legacy previewUrls localStorage) into one live owner held on
SessionRuntime.previewTabs. Both the sessions page (renderer) and the
open_preview/get_preview_status tools cross it, so both sync effects and the
localStorage seed disappear; url/target writes become atomic.

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

* fix(sessions): gate the Workspace/Sessions switch behind the global-AI dev flag

The SessionModeSwitch is the only entry point into the AI-sessions
experience, so gate it on wm_dev_global_ai like the global chat and the
sessions page — otherwise the unfinished mode ships to prod.

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

* fix(sessions): pin the settings footer and normalize row text in the fork dropdown

Split the family picker menu into a scrollable body + a pinned settings
footer so the workspace-settings link stays visible while the fork list
scrolls. Give every row a uniform text-primary font-normal style (rows
were inheriting a bold 600 weight; the settings link was text-secondary).

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

* feat(sidebar): fold theme switch into the settings dropdown and keep it in session mode

Move the Switch-theme toggle into the sidebar Settings dropdown and reorder
its entries (bottom-to-top: Instance, Workspace, User). The dropdown now
renders in both navigation and session modes; session mode hides only the
workspace-settings entry (the rail's global workspace doesn't map to a
session's forked workspace).

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

* fix(fork): validate fork name/id length before creation

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

* fix(sessions): title open-in-workspace button "Open in workspace"

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

* fix(sessions): keep AI chat working when the sessions dev flag is off

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

* fix(sessions): match burger drawer width and keep it open on mode toggle

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

* feat(sessions): surface the dev-workspace badge across session workspace pickers

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

* fix(sessions): dedup navigate, sanitize hydration, cap mounted tabs

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

* feat(fork): support forks of forks via a base-workspace picker

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

* feat(sidebar): family expansion, pinned menu actions, animated popovers

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

* feat(sessions): persist unsent drafts, gate preview, loading state

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

* fix(sidebar): group fork picker on top and unfold the session list

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

* fix(sessions): capture splitter pointer so off-window release ends drag

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

* refactor(sessions): retire the pinned preview tab (dot and no-close)

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

* feat(sessions): shared open-in-AI-session button across editors

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

* fix(sessions): dedup page tabs, flush on hide, review cleanups

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

* feat(sessions): give unsent drafts a side panel, reset tabs on retarget

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

* fix(sessions): keep the session fork icon neutral except when detached

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

* fix(sessions): scope session-mode restore and transient reuse to family

* fix(sessions): preserve session mode across workspace switches

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

* fix(sessions): reconcile open session with family on workspace switch

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

* fix(sessions): lazy-load runtime in session switch to keep it node-testable

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

* feat(sidebar): add bottom brand mark and standalone workers/logs links

* feat(sidebar): add name+id copy tooltip to workspace picker

* style(sidebar): add spacing between settings and brand mark

* feat(forks): id-based fork creation, fork color theming, picker polish

* feat(forks): copy-id in session header, inert chip, fork form polish

* fix(sidebar): restore logs, help, user and leave-workspace menus

* feat(sidebar): carry active tick on collapsed family root

* feat(dev): add settings-menu kitchen sink page

* fix(sessions): fail closed for unbound persisted sessions in family scope

* fix(sidebar): keep workspace URL param in sync across switches

* feat(sessions): remove home page from preview tab navigation

* refactor(sidebar): dedupe shared helpers and address review findings

* feat(sessions): keep preview hosts alive across session switches

* feat: workspace settings links in session rail, acting badge and family picker

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

* fix: refresh session changes bar after out-of-window deploy

The session "Edits" bar re-fetched its draft list and existence checks
only on AI turn-end, tab visibilitychange, and drawer open. Deploying an
item from a full-page editor in a second browser window left the bar
stale: that tab never goes hidden, so visibilitychange never fires, and
the badge kept reading "1 draft" while opening the drawer showed no
pending change.

Add a window `focus` listener alongside visibilitychange so returning to
the session window re-syncs the bar, and refresh the dock when a badge is
clicked so the drawer always opens on fresh state.

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

* feat(sessions): show name/id copy tooltip on acting badge, drop inline copy

* fix: keep editor header cloud indicator visible at narrow widths

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

* fix(sessions): scope preview reload to the mutated item

Reloading every mounted preview tab on any mutating chat tool blank-
rebooted unrelated raw-app previews: a raw app that isn't the session's
live-editor target renders as an /apps_raw/edit iframe, and reloadAllTabs
hard-reloaded it (frame.location.reload) on every write/deploy elsewhere.

Pass the tool args through the completion listener and scope the reload:
an item-route iframe reloads only when its item was actually touched. The
changed item is args.path for workspace-path tools; the raw-app file tools
(write_app_file, …) pass a leading-'/' frontend file path and edit the
active session's target app, so scope to the target; anything else is
unresolved and reloads everything (safe fallback). Changed paths accumulate
across the 500ms debounce. Non-item pages still always reload; live-editor
slots still no-op.

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

* fix(diff): honor side-by-side/unified toggle and widen draft drawer

Monaco forces inline view below its 900px renderSideBySideInlineBreakpoint, which overrode our SIDE_BY_SIDE_MIN_WIDTH gate and made the toggle a no-op in the ~800px draft drawer. Disable useInlineViewWhenSpaceIsLimited so our width logic wins, and widen the drawer default 1200->1500px.

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

* fix(diff): vertically center the element-header icon with its path

The path renders as ExternalEditLink's inline-flex <a> in production, which sat ~2px low on the wrapper's line-box baseline. Make the path wrapper flex+items-center so the icon and path align by box.

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

* fix(diff): reflect the auto-unified downgrade in the drawer view toggle

The side-by-side/unified downgrade lived inside each DiffEditor's width gate, so the drawer toggle still showed side-by-side when the narrow column rendered inline. Measure the diff column, make the drawer authoritative (force inline when narrow), and reflect it in the toggle (unified selected, side-by-side disabled) while preserving the user's preference for when it widens again. Shared SIDE_BY_SIDE_MIN_WIDTH via diffEditorTypes.

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

* feat(sidebar): make the nav rail resizable with rem scaling

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

* fix(diff): gate Monaco auto-inline behind a prop to keep narrow diffs unified

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

* fix(ui): restore instant popover/dropdown default, opt sidebar and sessions in

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

* feat(sidebar): restore delete-forked-workspace action in the settings menu

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

* feat(tooltip): add cursor anchoring option and use it for the name/id tooltip

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

* chore(dev): remove settings-menu kitchen sink scaffolding

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

* chore(docs): remove session-preview-tabs owner plan doc

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

* chore(dev): drop vite preview-server proxy scaffolding

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

* feat(sidebar): scroll nav as one block with fade hints, pin settings to bottom

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

* fix(sidebar): guard against concurrent pointer drags leaking resize listeners

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

* fix: scope session preview, LLM proxy, and raw-app workspace switch

Address PR review findings: session preview iframes, the AI chat LLM
proxy client, and the raw-app workspace-switch guard all now resolve the
session's effective workspace instead of the global navigation workspace.

- withMenuHidden appends the session workspace as ?workspace= so preview
  iframes render fork-scoped pages against the fork, not the nav workspace.
- AIChatManager builds the proxy clients from operatingWorkspace so the
  LLM request hits the session workspace's /ai/proxy, not the global
  singleton (init'd only on global workspace changes).
- workspaceSwitchUrl adds /apps_raw/edit|get to EDIT_PAGES so switching
  workspace from a raw-app editor/viewer goes home like other item pages.

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

* fix: scope session model, preview picker, and open-in-workspace to session

Second-layer workspace-scoping fixes from PR review: three more paths
resolved the global navigation workspace instead of the session's
effective workspace.

- SessionWrapper loads copilot config (models/providers) for the session's
  acting workspace, so getCurrentModel/modelProvider match the workspace
  the chat writes to, not the nav workspace.
- PreviewRouterPicker takes a workspaceId prop; the sessions page passes the
  session's effective workspace so the breadcrumb/+ picker lists fork items
  and its drafts, not the nav workspace's.
- 'Open in workspace' appends ?workspace= via the new withWorkspaceParam so
  the full-page link opens the active preview under the session workspace.

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

* fix(sessions): only the active session loads global copilot config

Follow-up to the session-workspace copilot fix: SessionWrapper's
loadCopilot effect ran in every warm/hidden wrapper, and since
copilotInfo/copilotSessionModel are global, a background session in a
different workspace could finish loading after the active one and leave
the active chat on the wrong provider/model. Gate the load on
currentSessionId so only the active session writes the shared config.

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

* fix(sessions): guard copilot load race + scope app handoff to workspace

Two more session-vs-navigation workspace fixes from PR review:

- loadCopilot now applies only the most recent call's result via a
  monotonic token, so a stale async load from a just-switched-away session
  can't clobber the active session's global model/provider config.
- navigateEditorTo carries the session workspace on the low-code app
  handoff (goto /apps/edit) so the app opens in the fork the session acts
  on, not the navigation workspace.

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

* fix(sessions): scope live-editor breadcrumb picker to session workspace

The session preview's live script/flow/raw-app editors mount with a
session workspaceId, but their EditorHeader breadcrumb picker
(WorkspaceItemDrillPicker) still loaded items and drafts from the global
navigation workspace. Thread an optional workspaceId prop from each
builder's autosaveWorkspace through EditorHeader -> BreadcrumbSegment ->
WorkspaceItemDrillPicker; it falls back to $workspaceStore, so non-session
editors are unchanged.

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

* docs(sessions): fix stale setSessionTabs transient-persistence comment

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

* fix(sessions): scope live-editor deploy/save/triggers to session workspace

The session preview's live script/flow/raw-app editors load and autosave
against the session's acting workspace, but their internal deploy,
save-draft, trigger-loading, fork-eligibility, worker-tags and
live-editor-draft operations read $workspaceStore directly. Since a session
deliberately leaves $workspaceStore on the navigation workspace, a
fork-scoped session deployed/saved to the wrong workspace (verified: deploy
POSTed to the nav workspace and 400'd).

Introduce an opWorkspace derived (autosaveWorkspace ?? $workspaceStore) in
each builder and route the operation reads through it. autosaveWorkspace is
only set by the session editor views, so opWorkspace equals $workspaceStore
for every non-session editor — no behavior change outside sessions. Verified
in-browser: a fork-session deploy now POSTs to the fork (201 Created) while a
normal editor still targets the navigation workspace.

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

* fix(sessions): staged pending fork chip uses default accent, not parent's color

A staged pending fork's effective workspace resolves to its parent
(setSessionPendingFork sets pending_workspace_id = parent_workspace_id),
so WorkspaceScopeTrigger read the parent workspace's color and painted the
'Acting on' chip in the parent's hue (e.g. yellow) instead of the neutral
fork accent. A real fork shows its own color; a not-yet-created one has
none, so fall back to the default fork accent unless the creation form
passes an explicit color preview.

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

* fix(sessions): scope raw-app deploy/save/version to session workspace

The raw-app create/update/version/diff/save operations live in
RawAppEditorHeader (not RawAppEditor), and still read $workspaceStore — so
a fork-scoped session's raw-app deploy targeted the navigation workspace,
the same class of bug already fixed for scripts and flows. Route those
operation reads through opWorkspace (autosaveWorkspace ?? $workspaceStore);
the inSessionPane-guarded draft-cleanup blocks are intentionally
non-session and keep $workspaceStore. Verified in-browser: a fork-session
raw-app deploy POSTs update_raw to the session fork (200).

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

* fix(sessions): key live-editor load cache on workspace, not just path

The script/flow/raw-app loaders returned early when loadedPath matched the
requested path, ignoring the workspace. Retargeting a session to the same
item path in a different fork kept the old workspace's loaded content while
the editor props switched to the new workspace — so save/deploy/autosave
could write stale old-workspace content into the new fork. Add
loadedWorkspace to the load slot and include it in the early-return guard so
a same-path/different-workspace retarget reloads. Verified in-browser: the
script re-fetches from the new fork on an acting-workspace switch.

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

* fix(sessions): drop stale content when a live editor retargets to a new workspace

Follow-up to keying the load cache on workspace: the loaders reloaded on a
workspace retarget but did not clear loadedPath during the fetch, so
SessionEditorTarget's loadedPath-keyed ready/notFound/stale gates still
treated the editor as ready on the old workspace's content — the outbound
draft sync (now wired to the new workspace) could write stale content into
the new fork, and a 404 kept rendering the old editor. Clear loadedPath on a
workspace change too (like a force reload), so the loading/not-found gates
and the draft-sync ready check resolve correctly. Same-workspace path swaps
are unaffected (loadedWorkspace still matches, so the old editor stays
visible during the swap).

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

* fix(sessions): await committed-workspace copilot config before a session send

getCurrentModel() reads the global copilotInfo when the request builds, but
SessionWrapper's loadCopilot for the active session is fire-and-forget — so a
send right after switching to a session in another workspace could pick the
previous workspace's provider/model while the proxy clients and tools target
the new workspace. Track the workspace copilotInfo reflects (copilotWorkspace)
and, in the session beforeSend hook (awaited before the request builds), load
the committed workspace's config when it doesn't already match. Verified
in-browser: sending a session committed to a workspace whose copilot config
wasn't yet loaded fires get_copilot_info for it just before the LLM proxy call.

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

* fix(sessions): navigate to a fresh session on reset; dedupe preview page tabs

Two review findings:

- resetToNewSession (deleting/archiving the open session) and the sidebar
  delete of the active last session created/selected a fresh session but left
  the URL on the old session_name. The page derives the visible session from
  that query, not currentSessionId, so it showed the deleted session's
  not-found state (or stayed on the archived one). Navigate to the fresh
  session, matching how activate()/enterSessionMode already switch sessions.

- Preview page-tab dedupe: the iframe reports its location with the injected
  nomenubar/workspace params, but tabs dedupe the observed loc against the
  workspace-less canonical url, so reopening a page spawned a duplicate tab.
  Canonicalize the observed loc in observeLocation (dropping both params);
  covered by a new sessionPreviewTabs test.

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

* fix(sessions): don't persist preview-iframe workspace; scope fork ducklakes to base

Two review findings:

- A sessions-preview iframe runs the logged layout, which persisted its
  ?workspace= (the session's fork) to localStorage — shared with the
  top-level app, so opening a fork preview clobbered the navigation
  workspace and reloads restored into the fork. Skip the persist when
  embedded; $workspaceStore is still set in-memory for the iframe's own API
  calls. Verified: opening a fork /runs preview leaves localStorage.workspace
  on the top-level workspace.

- ForkDucklakeSection listed ducklakes from $workspaceStore while a
  fork-of-fork is created from the selected base, so it could show the root's
  lakes and submit shared_ducklakes the base doesn't have. Add a
  sourceWorkspace prop (base ?? $workspaceStore) like ForkDatatableSection,
  and pass baseWorkspaceId at the mount.

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

* fix(sessions): keep preview iframe on session fork across reloads and open-in-workspace

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

* fix(sessions): scope worker-tag pickers to the session's effective workspace

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 14:27:00 +02:00
hugocasaandClaude Opus 4.8 7c7d7474cc feat: support workspace forks on cloud using parent workspace limits (#9864)
* feat: support workspace forks on cloud using parent workspace limits

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

* docs: clarify count_paid_seats approximates rather than mirrors billing seats

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

* fix: non-admin fork UI, attach cap, and fork-count for cloud forks

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

* fix: cloud fork billing cache on rename, usage display, attach cap edge

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

* feat: fork count in cloud quotas + fork billing points to parent

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

* fix: invalidate billing/fork caches on fork deletion for id reuse

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

* fix: gate fork usage remap on CLOUD_HOSTED, not just the cloud feature

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

* docs: note cloud feature vs CLOUD_HOSTED gating in backend guide

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

* fix: reserve fork-cap slots for an attach candidate's whole subtree

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

* fix: invalidate team-plan cache on delete, raise fork depth cap

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

* feat: cap fork nesting depth (MAX_FORK_DEPTH, default 5)

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

* fix: fork count/height robust to cycles and deleted intermediates

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

* fix(frontend): reset fork button loading state on creation error

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

* fix: invalidate billing cache for attached fork subtree; helper auth docs

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 23:59:02 +02:00
Diego ImbertandClaude Opus 4.8 a49c0871d7 fix(frontend): show inline workspace name editor on general settings (Fixes GIT-911) (#9892)
* fix(frontend): show inline workspace name editor on general settings

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

* fix(frontend): guard rename and support enter-to-save on workspace name

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 23:43:18 +02:00
centdixandClaude Opus 4.8 5709a564fb feat(frontend): add user-level toggle to disable Windmill AI (#9585)
* feat(frontend): add user-level toggle to disable Windmill AI

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

* feat(frontend): hide AI sessions sidebar section when AI is disabled

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 20:04:01 +02:00
GuilhemandClaude Opus 4.8 b894f783f1 feat(ai-chat): quick access to AI prompt settings from chat (#9508)
* feat(ai-chat): quick access to AI prompt settings from chat

Add a cog next to the AI model selector in the chat that opens a dropdown
to edit the current chat mode's user (localStorage) and workspace (DB
ai_config) AI prompts, reusing the shared AIPromptsModal. Workspace prompt
editing is admin-only; non-admins get a read-only view with an info Alert.
The dropdown and modal footer include an admin-only deep-link to the full
AI settings page, opening in a new tab.

Workspace save re-applies the saved custom_prompts onto the store after the
ai_config round-trip, since effective_ai_config falls back to the instance
config (and would drop them) when the workspace has no providers of its own.

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

* fix(ai-chat): snapshot mode when opening prompt settings modal

Prompt edits were keyed off the live chat mode (a $derived of
aiChatManager.mode). If the chat mode changed while the modal was open,
save/reset/hasChanges and the modal's target mode would follow the new
mode and write to the wrong key. Capture the mode into an activeMode
snapshot at open time and use it throughout the modal lifecycle.

Identified by cubic.

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

* fix(ai-chat): base-prefix AI settings links and reset hasChanges after save

- Prefix the AI-settings deep-links (dropdown item + modal footer) with
  {base} so they resolve under a deployment base path instead of 404ing.
- After a successful save, sync customPrompts to the trimmed value so
  hasChanges flips back to false (Save/Reset disable, Reset no longer
  snaps the textarea).

Identified by Claude review.

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

* fix(ai-chat): gate workspace prompt editing on workspace having own providers

When a workspace has no AI providers of its own (it uses instance defaults),
the backend never makes workspace custom_prompts effective — get_copilot_info
returns the instance config verbatim. A workspace prompt saved in that state
would be dead config that silently disappears on reload, and the earlier
re-apply hack also transiently replaced instance prompts in copilotInfo.

Mirror the settings page (AISettings.svelte): detect the workspace's own
providers in the same getSettings fetch used to seed the prompt, and when there
are none, surface the workspace prompt read-only with an explanatory Alert
(new readOnlyReason prop on AIPromptsModal) instead of an editable field.
Since editing is now gated to workspaces with their own providers,
effective_ai_config equals the saved config, so the re-apply is reverted to a
plain setCopilotInfo(effective).

Identified by Claude review.

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

* fix(ai-chat): await async save before closing prompts modal

AIPromptsModal.handleSave called onSave() without awaiting, then closed the
modal immediately — so the workspace save round-trip was still in flight when
the modal dismissed, with no loading state and any error toast landing after
the modal was gone.

Make handleSave await onSave (now typed to allow a Promise), show a loading
state on Save while in flight, and keep the modal open if the save throws.
AIChatSettingsMenu.save() re-throws on failure so the modal stays open. The
synchronous user-prompt path and existing callers are unaffected.

Identified by Pi review.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 14:54:45 +00:00
hugocasaandClaude Opus 4.8 e8e0701a36 feat(api): add endpoint to update token label (#9474)
* feat(api): add endpoint to update token label

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

* fix(api): prevent renaming the session token label

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

* fix(api): restrict token-label edits to user tokens, not just session

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

* fix(frontend): edit token label in the edit modal instead of inline

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

* fix(api): reject relabeling tokens to reserved system-token names

Centralize the is_user_token classifier in windmill-common and reuse it
to reject labels colliding with system-token namespaces (ephemeral*,
debugger-token, mcp-oauth-*), not just session.

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

* fix(api): match ephemeral label case-insensitively and cap label length

Align the canonical is_user_token, the SQL guard and the frontend mirror on
a case-insensitive `ephemeral` match (so a token can't be relabeled to a
casing the backend allows but the UI hides), reject labels over the
VARCHAR(1000) column limit with a 400, and add unit tests for is_user_token.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 17:33:31 +00:00
Ruben Fiszelandwindmill-internal-app[bot] b125eca762 feat(service-accounts): allow choosing role at creation time (#9307)
* [ee] feat(service-accounts): allow choosing role at creation time

Previously, service accounts were hardcoded to operator and could not be
used as the CLI sync user since they had no write access. They also only
counted as 0.5 seat each.

This change:
- Extends `NewServiceAccount` to accept optional `is_admin` / `operator`
  (defaults to `operator=true` for backward compatibility).
- Exposes a role picker in `AddUser.svelte` when creating a service
  account (Operator / Developer / Admin).
- Lets admins update a service account's role from the user list (it
  used to be locked to "Operator" with a tooltip).
- Updates the OpenAPI spec + regenerates the frontend client.

A developer/admin service account counts as 1 seat under the existing
seat-cap logic (operators stay at 0.5).

Companion PR on windmill-ee-private updates the `INSERT INTO usr` to
honour the chosen role.

Fixes WIN-1985

* [ee] feat(service-accounts): wm_deployers opt-in for Dev role

When creating a service account with role=Developer, surface a toggle
"Add to wm_deployers" (recommended). Members of wm_deployers can deploy
on behalf of other users — the typical setup when the service account is
used as the CLI sync / CI deploy identity.

- `NewServiceAccount` gains an optional `add_to_deployers` flag.
- Frontend defaults the toggle to on but only shows it under Developer
  (admins have it implicitly; operators can't deploy).
- Tooltip links to docs.windmill.dev "Run on behalf of".

Companion EE PR updates the handler to INSERT into usr_to_group for
wm_deployers when the flag is set.

Refs WIN-1985

* chore: update ee-repo-ref to 974ed42067d9f63acb42332b671b8c01ffd4b625

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

Previous ee-repo-ref: f7dbc3cc2ba21c396f4828881e3b9d9ab6f50c69

New ee-repo-ref: 974ed42067d9f63acb42332b671b8c01ffd4b625

Automated by sync-ee-ref workflow.

* [ee] fix(service-accounts): unhardcode role in superadmin user list

Two review issues from the merged #9307 / #589:

1. P1 — The global Users tab in #superadmin-settings still pinned every
   service account to "Operator". Now it shows the actual role
   (Admin / Operator / Developer), derived from the SA's usr row.

   - `list_users_as_super_admin`: replaced `true as operator_only` with
     the real `operator` value, and added `is_workspace_admin` from the
     row (NULL for password users since their admin status is
     per-workspace).
   - `global_whoami`: when the email belongs to a service account, look
     up its real `operator` / `is_admin` instead of pinning to operator.
   - `SuperadminSettingsInner.svelte`: drop the hardcoded "Operator"
     badge; render Admin / Operator / Developer using the new fields,
     matching the workspace-level view.

2. P2 — Regenerate the bundled `openapi-deref.{yaml,json}` so the
   `createServiceAccount` body (now exposing `is_admin`, `operator`,
   `add_to_deployers`) and the new `GlobalUserInfo.is_workspace_admin`
   field show up at runtime in `/api/openapi.{yaml,json}`.

Bumps `ee-repo-ref.txt` to the EE follow-up that adds the offline
seat-cap check on `create_service_account`.

Refs WIN-1985

* chore: update ee-repo-ref to b7a6068c1f3dc845e012959268b2426f0de4d697

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

Previous ee-repo-ref: 0b1307c21d1bfd6fb43a03c2ba39d2a8bf8e6470

New ee-repo-ref: b7a6068c1f3dc845e012959268b2426f0de4d697

Automated by sync-ee-ref workflow.

---------

Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-05-25 16:18:38 +00:00
d666e8431c feat: read-only flag on API tokens (#9144)
* feat: read-only flag on API tokens, orthogonal to scopes

Add a per-token `read_only` boolean set at creation time. When true, the
token can only call HTTP methods classified as Read (GET/HEAD/OPTIONS).
Mutating methods and job-run actions are rejected with 403, regardless of
which scopes are attached. Surfaced as a prominent toggle in the standard
token-creation flow and a discreet `2xs` toggle in MCP mode (where users
often want write access, so we don't bias them toward enabling it).

MCP enforcement: read-only tokens hide all script/flow/hub tools from
`list_tools` and only see endpoint tools whose method is GET, and the
runner rejects `call_tool` on anything mutating.

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

* fix: review fixes for read-only token flag

- Exempt /api/mcp/* and /mcp/* paths from the read-only middleware check.
  MCP transport runs over POST (streamable HTTP / SSE), so otherwise the
  middleware would 403 every MCP request before the runner could enforce
  read-only at the tool-call level.
- Tighten is_endpoint_read_only to GET only, matching the read_only_hint
  that create_endpoint_annotations actually emits.
- Add unit test for check_read_only_for_route covering GET/HEAD/OPTIONS,
  mutating methods, and run paths.

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

* chore: bump ee-repo-ref to read-only-trigger-toggle

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

* fix(frontend): make read-only toggle discreet in both modes

Match the MCP-mode treatment in standard mode: text-tertiary, 2xs, shared
"Read-only" label. The tooltip switches per mode so the explanation still
fits the context.

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

* fix(frontend): gate read-only toggle behind Limit token permissions

The read-only toggle now only shows when the user has limited the token's
scopes (standard mode) or in MCP mode (which always picks an MCP scope).
Turning the limit off also resets read-only so it doesn't silently stick.

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

* feat(frontend): hide incompatible MCP tools when read-only is on

When the read-only toggle is on in MCP mode:
- Endpoint badges and the custom-mode endpoint MultiSelect filter to GET.
- Already-selected non-GET endpoints are pruned from the scope.
- The scripts/flows preview is replaced with a note explaining they're
  hidden (the runner already rejects script/flow runs for read-only).

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

* fix(frontend): place read-only toggle at top of limited scope area

The previous gate required at least one scope to be picked before the
read-only toggle appeared, which made it look missing while the user was
still building their scope list. Move the toggle inside ScopesPicker:
- Standard mode: sits directly under the "Limit token permissions" toggle
  whenever Limit is on, before the scope selector.
- MCP mode: sits at the top of the MCP scope block.

readOnly is now $bindable on ScopesPicker so CreateToken still owns the
value. The auto-reset on un-limit moves into ScopesPicker too.

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

* fix(frontend): nest read-only toggle inside the scope list card

Place the read-only toggle at the top of the scope list (between the
Selected Scopes summary and the bordered domain list) via a new optional
topSlot snippet on ScopeSelector. Keeps ScopeSelector decoupled from
read-only specifics; ScopesPicker fills the slot.

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

* chore: update ee-repo-ref to 9bc8160be50b3e57a60daf4e1b71c389a6e02b8a

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

Previous ee-repo-ref: f53d26e6685dfd60bfa67686fbd7358169cfd130

New ee-repo-ref: 9bc8160be50b3e57a60daf4e1b71c389a6e02b8a

Automated by sync-ee-ref workflow.

* fix: address CI review for read-only token flag

- P1 (Codex): narrow the MCP middleware exemption from "any /api/mcp/*"
  to just the streamable HTTP transport endpoints
  (/api/mcp/gateway, /api/mcp/w/{ws}/{mcp,sse,list_tools}). Without this,
  a read-only token could POST /api/mcp/gateway/oauth/server/approve and
  mint a follow-on non-read-only MCP token via the OAuth code/token
  exchange.
- P2 (Claude/cubic): fix test comment/assertion mismatch — the run-path
  assertion now exercises GET (which is what the RUN_PATH_ACTIONS
  elevation comment describes) in addition to POST. Add a regression
  assertion for /api/mcp/gateway/oauth/server/approve.
- P2 (cubic): short-circuit script/flow/hub-script/resource fetches in
  MCP list_tools when read_only is on — they would only be discarded
  below, so skipping the DB and resource fan-out is pure win.
- P2 (cubic): when scopes are pre-supplied via the CreateToken prop, the
  ScopesPicker isn't rendered, which previously hid the read-only
  toggle entirely. Render it next to the pre-supplied scopes display.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-05-13 15:26:21 +00:00
centdixandClaude Opus 4.5 110384580e refactor: add global ai chat mode with workspace-item draft tools (#9056)
* docs: add global ai mode plan

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

* feat: add global ai draft mode

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

* fix: scope global ai mode to scripts and flows

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

* refactor: simplify global ai workspace item shape

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

* refactor: split global ai write tool into per-type tools

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

* feat: add global ai schedule and trigger workspace item tools

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

* feat: add dev-only /global_drafts route to inspect ai draft store

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

* feat: add edit_script and patch_flow_json global ai tools

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

* feat: add deploy_workspace_item global ai tool with confirmation

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

* feat: emit open-resource action card after deploy_workspace_item

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

* feat: add delete_workspace_item global ai tool with confirmation

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

* chore(system_prompts): emit RESOURCES_BASE and resource/variable zod schemas

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

* feat: add global ai resource and variable workspace item tools

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

* fix: search_resource_types uses listResourceType to avoid embedding feature dep

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

* Revert "fix: search_resource_types uses listResourceType to avoid embedding feature dep"

This reverts commit 6d1d19514a.

* feat: emit open-resource action card for variable and resource deploys

* feat: add global ai raw app workspace item tools

* feat: split raw-app prompt into chat-only authoring and cli prefix

* feat: add init_app global ai tool to scaffold raw apps from templates

* fix: pass write_flow value as JSON string for gemini compat

* refactor: hoist countExactMatches and applyExactReplace to chat/shared

* refactor: extract editableFlowJson module shared with global mode

* fix(global): preserve flow schema and groups across draft and deploy

* feat: extract inline scripts from flow reads and patches in global mode

* refactor: add findAndReplace helper for match-validated text patches

* refactor: extract getInlineRunnableContent helper for app file tools

* refactor: extract assertNotGeneratedAppFile guard for /wmill.d.ts

* feat: gate global ai mode behind localStorage flag for dev rollout

* chore: bump svelte to ^5.55.5 in raw app template (sync with main)

* fix: isolate global ai draft rollout

* fix: preserve global ai deploy metadata

* fix: harden global ai draft tools

* chore: remove global ai plan doc

* fix: align raw app prompt guidance

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-12 09:42:07 +00:00
Diego ImbertandClaude Opus 4.7 20ecd904e7 feat(operators): allow operators to access assets page (#9095)
* feat(operators): allow operators to access assets page

Adds the "assets" key to workspace operator_settings (defaulting to true
for existing and new workspaces) and toggles the frontend default so the
assets page is visible to operators by default.

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

* nit: remove settings btn when not available

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 18:36:34 +00:00
hugocasaandClaude Opus 4.7 e9e72fbbf8 feat: edit scopes on existing API tokens (#8967)
* feat: edit scopes on existing API tokens

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

* fix: address PR review feedback on token scope edit

- add SECURITY DEFINER to notify_token_scopes_change so trigger fires under windmill_user/admin roles (cubic P1)
- drop banned $bindable(default) on optional props (CLAUDE.md): make ScopesPicker.value and EditTokenScopesModal.open required
- detect MCP only when *every* scope starts with mcp: so mixed/null-scope tokens fall back to standard picker without dropping non-mcp scopes
- audit log scope payload via serde_json instead of Rust {:?}

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 19:49:56 +00:00
centdixandClaude Opus 4.5 f35e10cc0a feat: add homepage connect drawer (#8880)
* feat: add homepage connect drawer

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

* fix: reset connect drawer state

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

* fix: polish home connect button

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

* fix: use standard home connect button style

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

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-20 13:07:57 +00:00
hugocasaandClaude Opus 4.6 435b25e6a4 feat: add user offboarding flow with object reassignment (#8647)
* feat: add user offboarding flow with object reassignment

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

* fix: require new_operator for permissioned_as when reassigning to folder

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

* fix: update on_behalf_of_email on scripts/flows during offboarding

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

* refactor: extract offboarding to separate module and add integration tests

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

* fix: delete tokens, add operator preview counts, remove token reassignment UI

Tokens are now always deleted during offboarding. Preview now shows
scripts/flows/apps with on_behalf_of and schedules/triggers with
permissioned_as referencing the departing user (even outside their path).
Token reassignment UI removed since webhooks break on path changes anyway.

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

* feat: rich preview with path lists, warnings, and downloadable report

Preview now returns full path lists (not just counts) for owned objects
and objects executing on behalf of the user. Adds warnings for:
- HTTP triggers (webhook URLs will change)
- Email triggers (addresses will change)
- Broken $var:/$res: references in resources/variables
Frontend provides "Export list" button to download affected content.

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

* test: add coverage for dynamic queries (triggers, extra_perms, operator schedules)

Adds HTTP trigger, extra_perms reference, and shared schedule to test
fixture. Tests verify that non-macro sqlx queries (trigger reassignment,
extra_perms cleanup, operator schedule update) work correctly.

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

* fix: remove broken_references, add full dynamic query test coverage

Remove broken_references field from preview (user's resources/variables
are already in the owned paths list). Add shared HTTP trigger fixture
to test all dynamic query paths: trigger operator preview (line 232),
trigger permissioned_as update for non-user-path (line 951), and
extra_perms cleanup on trigger tables (line 983).

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

* feat: add referencing field to preview for content/value path references

Preview now includes a 'referencing' section listing scripts (by content),
flows (by value JSON), apps (by policy/extra_perms), and resources (by value)
that contain references to u/{username}/ paths. These references may break
after reassignment. Shown in export list and as a warning in the UI.

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

* fix: rename objects to items in UI, detect on_behalf_of items in hasItems

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

* fix: replace remaining objects with items in UI text

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

* refactor: rename operator to on_behalf_of, separate owned vs on-behalf UI sections

- Rename new_operator to new_on_behalf_of_user in API and frontend
- Rename op_ prefixed variables to obo_ in backend
- UI now shows separate sections for owned items and items running
  on behalf, with the operator selector shown only when needed
- canSubmit logic updated: operator needed for folder targets OR
  when on-behalf items exist

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

* feat: CSV export, side-by-side layout, always accept new_on_behalf_of_user

- Export affected items as CSV instead of text
- Owned items and on-behalf items shown side by side in summary boxes
- new_on_behalf_of_user always accepted (defaults to target user for
  user targets, required for folder targets)
- On_behalf_of selector always visible, auto-defaults when user target
  is selected

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

* fix: proper pluralization and bottom-aligned counts in summary boxes

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

* fix: stack info boxes vertically, referencing box as warning style at top

Info boxes (owned, on-behalf, referencing) now one per row instead of
side-by-side. Referencing box uses warning colors. Webhook/email trigger
alerts shown below boxes. Proper pluralization in global modal too.

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

* fix: CSV exports only referencing items, export button inside warning box

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

* feat: use ToggleButtonGroup for user/folder, add reassign toggle on remove

- User/Folder selection now uses ToggleButtonGroup component
- When removing a user, a "Reassign items before removing" toggle lets
  the admin skip reassignment and just delete directly
- In reassign-only mode, the toggle is not shown (always reassigns)

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

* feat: show token details with labels and scopes in preview

Preview now returns token label, scopes, and expiration instead of just
a count. Frontend shows a dedicated token box listing each token with
its scopes. Test updated to verify token label in preview response.

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

* feat: extract shared offboarding components, per-type trigger links, hash deep linking

- Extract OffboardItemsBox, OffboardReassignControls, OffboardWorkspaceSection,
  and offboarding-utils.ts as shared components used by both workspace and global modals
- Change triggers in OffboardAffectedPaths from Vec<String> to HashMap<String, Vec<String>>
  so frontend knows which trigger page to link to
- Add hash-based deep linking to all 9 trigger pages and schedules page
- Preserve URL hash in updateQueryFilters across all trigger pages
- Only open editor drawer if the item is found in the list
- Reassign toggle at top with warning alert when disabled (both modals)
- Referencing items box uses yellow warning variant with expandable path links
- Cleaner labels: "Move u/{username}/* items to", "Update triggers/runnables permissions to"

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

* fix: rename on_behalf_of section label to match flow advanced settings

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

* chore: regenerate sqlx query cache

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

* fix: address PR review issues for offboarding

- Add 9 trigger tables to check_path_conflicts for user-friendly conflict messages
- Fix submit button no-op when user has only on-behalf items (show target selector, fix canSubmit)
- Only delete workspace user when reassignment entry exists (prevent orphaned objects)
- Add $azure_kv: prefix to vault secret query (match rename_user pattern)
- Use Svelte 5 onSelected callback instead of deprecated on:selected
- Make ScriptBuilder section label conditional on canPreserve
- Fix CSV export to include trigger paths via flattenPaths utility
- Fix test_offboard_reassign_only to remove conflicts and assert on response
- Parallelize workspace config fetches in global modal with Promise.all
- Delete tokens when deleting workspace user
- Return structured JSON from global offboard endpoint

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

* sqlx

* fix: address second round of PR review issues

- Accumulate per-workspace OffboardSummary in global offboard instead of returning zeros
- Delete workspace user unconditionally when delete_user=true (prevent orphaned usr rows)
- Filter archived/deleted scripts in check_path_conflicts to match preview
- Reset form state when workspace offboard modal reopens
- Move hashHandled=true inside trigger-found guard on all 10 deep-link pages

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

* test: improve offboarding integration tests

- Add second workspace to fixture for multi-workspace global offboard testing
- Add test_global_offboard_execution: verifies items reassigned across 2
  workspaces, user deleted from both, and password row deleted from instance
- Add test_offboard_invalid_target: verifies 400 for nonexistent user,
  nonexistent folder, and invalid target format
- Fix test_offboard_to_user: use single DELETE, add explicit new_on_behalf_of_user
- Fix test_global_offboard_preview: assert 2 workspaces instead of 1

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

* fix: address third round of PR review issues

- Fix ScriptBuilder tooltip to match conditional section label wording
- Clear stale conflicts in global modal on reopen
- Fix test_offboard_to_folder to assert on specific moved path, not pre-existing data
- Allow deleting user with zero items (show Offboard button, skip reassignment)

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

* fix: add global token deletion warning in instance-level offboard modal

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

* update sqlx

* fix: add raw_app path and dependency_map path reassignment to offboarding

Audit found these tables with user-scoped paths were not being updated:
- raw_app: mirrors app paths, needs path reassignment
- dependency_map: importer_path and imported_path reference user paths

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

* fix: move user cleanup to delete_workspace_user_internal, fix review issues

- Move extra_perms, folder owners, drafts, favorites, inputs, captures
  cleanup into delete_workspace_user_internal so any user deletion gets
  proper cleanup (not just offboard path)
- Fix flow INSERT missing labels and lock_error_logs columns (data loss)
- Fix validate_target returning 404 instead of 400 for nonexistent targets
- Fix canSubmit blocking delete when user has no items to reassign
- Fix token preview query filtering out tokens without scopes
- Fix token warning messages: workspace-level mentions webhooks/HTTP triggers,
  instance-level mentions API calls using credentials
- Fix "Schedules and triggers" -> "Triggers and runnables" wording
- Show token section at instance level only when tokens exist
- Show Offboard button at instance level when user has no items but deleteUser=true

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 17:31:36 +00:00
Ruben FiszelandClaude Opus 4.6 342defecd2 block adding/inviting members to admins workspace (#8721)
* fix: block adding/inviting members to admins workspace on CE

The admins workspace is reserved for superadmins only. On CE (non-enterprise),
prevent adding or inviting users to it via both API and UI.

Backend: add #[cfg(not(feature = "enterprise"))] guards to invite_user and
add_user endpoints that reject requests targeting the admins workspace.

Frontend: show an info alert on the admins workspace members page and hide
the add/invite/auto-add buttons.

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

* fix: use derived variable for admins workspace alert consistency

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 11:06:14 +00:00
Ruben Fiszel 80cf26bb61 nit npm checks 2026-03-27 19:39:55 +00:00
Ruben FiszelandClaude Opus 4.6 3959fe8297 feat: add workspace-level service accounts (#8560)
* feat: add workspace-level service accounts (EE)

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

* sqlx

* sqlx

* chore: update ee-repo-ref

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 19:23:03 +00:00
Ruben Fiszel 69ce946241 feat: add trashbin system for soft-deleting items (#8519) 2026-03-26 09:51:34 +00:00
centdixandClaude Opus 4.6 db5e03610d feat: add instance-level AI settings (#8453)
* feat: add instance-level AI settings with workspace fallback

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

* feat: add AI step to onboarding setup wizard

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

* fix: thread workspace prop through resource editor and disable chat offset

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

* Revert "fix: thread workspace prop through resource editor and disable chat offset"

This reverts commit 9fea9cc0c239f6432d1fef1487c45e74ab752e21.

* fix: set workspace store and disable chat offset during AI setup step

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

* fix: thread workspace and disableChatOffset props through resource editors

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

* fix: populate workspace and user stores for AI step path component

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

* fix: initialize AI clients for test key during onboarding

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

* refactor: extract AI config state into InstanceAISettings component

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

* refactor: move AI config state ownership into AISettings component

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

* Persist instance AI settings before navigation

* Reload effective workspace AI state after save

* Scope AI key tests to the rendered workspace

* Add post-create AI onboarding for new workspaces

* Unify instance AI settings header

* Fix instance AI drawer offset on workspace selection

* Add instance AI fallback settings behavior

* Update sqlx metadata

* Update sqlx metadata

* Clarify active instance AI in workspace settings

* Refresh workspace AI state after instance AI save

* Declare instance AI summary in API schema

* Normalize empty instance AI config handling

* Clean up workspace AI settings UI

* Unify AI config provider checks

* Split AI settings metadata from effective config

* Propagate instance AI cache invalidation across servers

* Fix AI settings dirty state tracking

* Update sqlx metadata

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 19:18:36 +00:00
51957f7d92 feat: mcp oauth gateway (#8443)
* feat: extract McpScopeSelector into reusable component

Extract scope selection UI from CreateToken.svelte and mcp_authorize page
into a shared McpScopeSelector.svelte component to reduce duplication.

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

* feat: add MCP gateway endpoint for workspace-agnostic access

Add /api/mcp/gateway endpoint that allows MCP clients to connect without
knowing the workspace ID upfront. During OAuth, the user picks their
workspace on the consent page. The token is then scoped to that workspace.

This enables a single URL for the Anthropic connectors directory.

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

* fix: address PR review nits

- Use onClick prop instead of legacy on:click directive in McpScopeSelector
- Remove unused catch variable in workspace loading

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

* refactor: deduplicate gateway OAuth handlers into shared inner functions

Extract build_oauth_metadata, build_protected_resource_metadata,
oauth_authorize_inner, and oauth_approve_inner so gateway handlers
are thin wrappers. Also revert formatting-only changes in auth.rs.

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

* fix: gate run_inline test helpers behind cfg(feature = "run_inline")

Imports and helper functions were not gated, causing unused-import and
dead-code errors when compiling without the run_inline feature.

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

* Update SQLx metadata

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-03-20 16:12:59 +00:00
Ruben FiszelandClaude Opus 4.6 391da1d5af add cloud quota usage display and version pruning (#8433)
* feat: add cloud quota usage display and version pruning

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

* fix: hard-delete pruned scripts so quota actually decreases

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

* feat: update quota error messages to reference workspace settings

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 12:03:01 +00:00
Diego Imbert 08215c708b Display workspace ID in select subtitle when creating token (#8407) 2026-03-17 12:33:11 +00:00
hugocasaandClaude Opus 4.5 8667329110 fix: skip token expiry notifications for debugger and mcp-oauth tokens (#8316)
* fix: skip token expiry notifications for debugger and mcp-oauth tokens

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

* fix: update frontend isUserToken to match backend filter

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

* chore: add cross-reference comments to token filter functions

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

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-11 10:47:21 +00:00
Diego ImbertandClaude Opus 4.6 5d79f33590 Final Svelte 5 migration (#8211)
* Remove $$props.field usage

* Rename slots to ensure no hyphen

* _props

* _trigger

* OnSelectedIteration type correct capitalization

* rename _content

* Remove afterUpdate

* Migrate everything to svelte 5

* array bind

* Fix popover

* type never

* nit fixes

* Fixed many trivial errors

* onClick

* Fix errors

* use let:

* nit typing

* fix: wrap state_referenced_locally vars with untrack()

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

* Add untrack import

* Fix all syntax errors due to untrack migration

* Fix undefined errors

* Fix more undefined errors

* untrack(() => initialOpen)

* svelte-ignore

* Fix state_descriptors_fixed error in Chart.svelte

Use $state.snapshot() to pass plain copies of data/options to Chart.js
instead of $state proxies. Chart.js's listenArrayEvents tries to define
property descriptors on data arrays, which Svelte 5 proxies reject.

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

* nit typing

* Merge issue

* Fix "path is not set" error in resource picker / editor

* Fix InputTransformForm error when rerunning some flows

* fix npm run check

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-05 18:11:40 +01:00
centdixandClaude Opus 4.6 ce041e8a5e feat: hash-based MCP tool names for long paths (#8133)
* feat: replace _TRUNC with hash-based MCP tool names (50 char limit)

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

* fix: reduce MCP tool name limit from 50 to 40 chars

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

* refactor: use path prefix filtering instead of separate DB query for hashed name resolution

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

* fix: remove long path warning from MCP token creation (hashing handles long names)

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

* refactor: unify tool prefix parsing and fix extract_path_prefix_from_hashed for Hs- names

- Replace `is_hashed_name` + `parse_hashed_name` with unified `parse_tool_prefix`
  that returns `(type_str, is_hub, is_hashed)` in one call
- Fix `extract_path_prefix_from_hashed` to dynamically determine prefix length
  (3 for `Hs-`, 2 for `S-`/`F-`) instead of hardcoding index 2
- Simplify `reverse_transform` to reuse `parse_tool_prefix`
- Add tests for invalid prefixes and `Hs-` prefix handling

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

* fix: escape LIKE wildcards in MCP hashed name path prefix query

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

* fix: respect favorites scope in hashed tool name resolution

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

* refactor: deduplicate MCP tool name resolution and rename get_path_or_id

- Extract `unescape_path` helper in transform.rs to deduplicate the
  3-step placeholder unescape logic
- Extract `find_matching_path` helper in runner.rs to deduplicate
  script/flow candidate matching via ToolableItem trait
- Remove verbose tracing::info! logs from hashed tool resolution hot path
- Fix doc comment referencing nonexistent `is_hashed_name` function
- Rename `get_path_or_id` to `get_transformed_path` for clarity

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

* fix: update stale doc comments to reflect MAX_PATH_LENGTH=40

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

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-05 12:04:20 +00:00
hugocasaandClaude Opus 4.6 e56ccd200b feat: token expiration notifications (#8190)
* feat: add token expiration notifications via email, critical alerts, and webhooks

- Monitor loop checks for tokens expiring within 7 days and sends
  email notifications to token owners. Tracks notification state via
  new `expiry_notified` column on the token table to avoid duplicates.
- When tokens expire and are deleted, owners are also notified.
- Critical alerts (in-app UI) are gated behind a new instance setting
  `critical_alerts_on_token_expiry` (off by default); emails are
  always sent regardless of the setting.
- Add TokenExpiringSoon and TokenExpired webhook message variants for
  workspace webhook integrations.
- Frontend: show expiration badges and a warning banner on the tokens
  table for tokens expiring within 30 days.
- Exclude session and ephemeral tokens from all notifications.

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

* refactor: use separate token_expiry_notification table for dedup

- Replace `expiry_notified` column on token table with a dedicated
  `token_expiry_notification` table (token, expiration)
- Insert notification row on token creation via shared
  `register_token_expiry_notification()` helper
- Delete notification row atomically when sending the notification
- Clean up orphaned rows in `delete_expired_items()`
- No FK constraint to avoid cascade overhead on token deletions
- Add index on expiration column for efficient range queries

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

* fix: calendar-based expiration badge and move notification cleanup

- Fix daysUntilExpiration to compare calendar dates instead of time diff
- Move notification row cleanup from delete_expired_items to
  check_expiring_tokens to keep it off the hot path
- Use simple expiration <= now() index scan instead of NOT EXISTS join

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 05:22:46 +00:00
Guilhem dd419ade94 fix(frontend): redesign instance settings (#7916)
* handle dirty config

* Move instance update in drawer actions

* Put windmill version in drawer header

* Use sidebar instead of tabs

* Rework user section

* improve user table

* Handle EE

* Add settings section header

* test-1

* option 2

* create new settings group

* harmonize all settings inputs

* improve members/users table styling

* refactor instance setup

* show user count

* nit

* Create setting card component

* harmonize instance settings and workspace settings

* fix python version loader

* nit

* clean code

* nit

* add email validation

* fix reactivity issue on default value

* fix dirty config check

* fix object storage dirty config check

* Fix object storage settings reactivity

* fix indexer dirty reactivity

* Add validation for indexer

* fix sso dirty issues

* clean

* nit
2026-02-12 09:50:01 +00:00