The chat's discard reports its delete as landed once nothing failed or conflicted,
but the form stays editable across it: an edit made then queues a write behind
the delete and recreates the draft. The removal marker — which sends a hosted
editor away from an item it says is gone — is only published when nothing is
queued behind the delete, so it describes an item that is actually gone.
The variable save's committed flag still followed the acting workspace after the
path it pairs with began following the shown one, and its reopen let the editor
reselect its default workspace. Both follow the workspace the form is showing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4
`WsSpecificVersions` can point the form at a linked workspace, and the form then
followed the acting workspace's write instead — which for a rename made in the
linked one meant following nothing, leaving the form on the item it shows with a
draft handle still keyed to the path that item has left. It follows the shown
workspace's write, falling back to the acting one, which is the same thing
whenever they are the same workspace.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4
An editor adopts what it loaded as its cell's baseline the moment it mounts, and
that load can predate a write that seeded the cell before it existed. Adopting
it cleared the marker that write had left, so the session concluded the write
had reached the form and skipped the re-read, leaving the editor on the older
value. Bootstrap seeds stay out of the bookkeeping now — recording nothing and
consuming nothing — while a real write still resolves a marker either way.
`can_write` also stops snapshotting the current user when the acting workspace
is the one being browsed, so it keeps following the layout's refresh.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4
`can_write` came from the navigation workspace's membership while every read and
write went to the session's, so a fork or linked workspace could disable a user
who may edit there — or offer the controls to one who may not, until the backend
said otherwise. It resolves that workspace's user, falling back to the current
one until it lands and whenever they are the same.
Whether a save kept an edit was decided before the settle, which awaits a request
of its own with the form still editable. Anything typed across it counted as
nothing, the host remounted, and the edit went with it. Read afterwards.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4
`hasDraft` compares the form to the deployed baseline, and that baseline is a
plain value on purpose — made reactive, the sync's effects track every field and
a reset provokes a write that races its own delete. So a toggle that landed
fixed the baseline where nothing was watching, and the drawer kept offering to
discard changes it had already deployed until some other field moved.
The baseline stays plain; a counter beside it marks when it moves, and the
`deployed()` the banner reads depends on that. Bumped where the baseline is
captured, adopted after a save, corrected after a toggle, and cleared.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4
The reading sat inside the branch that captures a baseline, so a schedule opened
with a config handed in — the trigger panel of a script or flow — never took
one, and a save there settled against whatever the last open had left. Both ways
in hold the deployed state on the form at that point, which is what the reading
is about.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4
`loadScheduleCfg` applies the draft overlay as well as the deployed config, so
recording the accepted `enabled` there let a draft's own value pass for one the
server had taken: saving then settled the form back to it, and a schedule the
user had switched on came out off, clean, with nothing said. It is read where
the deployed config is the one on the form, and reset for a schedule that has
none yet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4
The toggle sets `enabled` before its request and puts it back if that fails, so
the form's value is a claim until it lands — and a save adopting it wrote an
uncommitted toggle into its baseline, where cancelling the request left it: a
false dirty state, and a Discard offering a value the server never took. The
committed value is tracked and both the baseline and what counts as written take
it, so the settle compares like for like and drops its draft instead of leaving
a row behind.
The reload queue keys by workspace as well as page: a draft landing in one says
nothing about the same page in another, and reloading it there costs that frame
its scroll. A restore reports the path it restored rather than leaving the
caller to read a live one its own await has outlived.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4
An update sends no `enabled` — the toggle deploys that field by itself — so a
save adopting its click-time snapshot wholesale rewound a toggle that had landed
meanwhile: the form called the deployed state unsaved, and Discard put the old
one back. The baseline takes the field from the toggle, not from the snapshot.
A variable save reported each workspace as it landed, and the first report
retargets the host, whose keying unmounts the editor at the next workspace's
await — releasing the handles the workspaces after it settle against, so their
drafts survived a successful save. Reports wait for the loop to finish.
The list frames now share one debounced reload queue with the chat path, rather
than reloading once per reason for one deploy.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4
Timing the fan-out against the settle was the wrong direction. The settle now
sends its own delete and resolves when it has gone out, so the editors report —
and the schedule host remounts — on a cell that is already resolved: a stalled
delete used to let that remount load the draft the save had just replaced, and
restore it over the deployed value.
A write can still land after all of that, when an edit made during the save is
kept. A list frame reads its rows from a store of its own and nothing reloads it
again, so it now reloads on the write landing rather than on our guess about
when it will.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4
The settle that follows a write parks its draft delete on the autosave debounce,
so the list frame reloaded straight after read the row's `*` and kept it: its
markers come from a store inside the iframe, and nothing reloads it a second
time when the delete lands. The fan-out sends what is parked for the key first.
That needed the variable save to report after settling rather than before —
reporting first meant there was nothing parked yet to send, and the flush passed
over a cell about to queue a delete. Every editor now resolves its cell before
saying so.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4
This commit updates the EE repository reference after PR #782 was merged in windmill-ee-private.
Previous ee-repo-ref: 313c572c9dcbcaafd8a1594df4054f9dd26f395c
New ee-repo-ref: d33ea730c550cdbc7d050aeb6d40dcef3d134e07
Automated by sync-ee-ref workflow.
The fan-out matched only tabs holding the item, so a bare list frame — which has
no row in its location to match — was skipped, and a save, rename or removal made
in a hosted editor left it showing the old rows and draft markers. It reloads,
the way a chat mutation on that page already made it.
Restoring a version replaces the deployed value as squarely as a save does, but
only the reporting editor heard: the other tabs kept the pre-restore value and a
baseline that would write it back over the restored one. It reports too, under
the workspace the history was pointed at.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4
Toggling enable writes the form before awaiting the deploy, so the autosave
queues a draft against the pre-toggle baseline. Advancing the baseline on
success then made that draft equal to it — invisible to the banner, still a row
on the server and a `*` on the list. It is settled now, keeping an unrelated
edit made during the request the way a save does.
A draft-only removal marker was only ever voided by a chat write, so one left
unread — discarded outside a session, where nothing consumes it — could be spent
by an ordinary discard on a path since recreated, sending live editors away from
an item that exists. Every write that gives the item a draft again voids it,
which is also what `persistGlobalDraft` was doing by hand.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4
Adopting it unconditionally overwrote whatever the field held by then, so a
write landing during the request — the chat seeding the cell — was erased before
anything could notice it had, and settled away as if it were the save's own.
What was written still records `true`, since that is what the create sent; the
form follows only while it still holds the value that went with it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4
Creating a schedule sends `enabled: true` whatever the form held, so a
draft-only save left the form and its new baseline recording a state the server
does not have — visible once a kept edit skips the remount that used to hide it.
Also moves the settle comment in VariableEditor back onto its call.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4
Tab ids are unique within a session, but the seeded first tab is `session` in
every one of them — so a same-path save made from one skipped the refresh in all
the others, leaving those editors on the baseline the deploy replaced. The
exclusion carries the session it came from.
Also drops the acting-workspace explanation left above `written`, which reports
every workspace the save wrote.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4
The enable/disable request carries the enabled flag alone, but claimed to leave
nothing behind — so the host remounted, and a summary or args edit still sitting
on the autosave debounce went with it. It now reports the form's real divergence
from the baseline it just moved.
A save also left `edit` and `draftOnly` to the remount, which is exactly what a
kept edit skips: a draft-only schedule created while the user typed stayed in
draft-only mode, with its path still editable and its next save a create. They
follow the write that deployed it instead.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4
`save()` now answers with the workspaces it wrote, and an empty array is truthy —
so the MCP section's `if (!(await save())) return` stopped guarding, and a failed
save ran the enablement bookkeeping its own comment says must not run. It asks
for the length.
The page declined to remount the tab that reported a save, but that tab was
remounting itself: the resource host on every save, the schedule host likewise.
The resource never needed it — its editor adopts the written value as its own
baseline — and the schedule remounts only when the save kept no edit, which is
the one case where re-reading is what the editor wants.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4
The same-path refresh reached the tab that did the save, remounting it over the
edit its own settle had just preserved — the teardown's flush and the fresh
editor's read race, and the older value can win. The reporting tab is skipped:
it has settled its own state, which is what the report is telling everyone else.
A save can also write several workspaces while only one event went out, labelled
with the acting one, and a discard deletes the draft of whichever workspace the
form is showing while claiming the acting one — so a linked workspace's tabs kept
a replaced baseline and the wrong tabs were closed. Each write now reports its
own workspace and the path it wrote there, which retires the acting-workspace
bookkeeping that stood in for it.
A tab re-pointed mid-write no longer swallows the report either: only the local
remount was ever its to suppress.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4
Editors on the same item share its draft cell but each keeps its own deployed
baseline, so a save made in one left the others offering to discard back to the
value it replaced — over a deployment. A save now reports the path it wrote to
whether or not that moved the item, and the tabs on it that stayed put are
re-read instead of re-pointed.
The variable drawer reopens on the committed path after a partial multi-workspace
failure rather than re-keying: re-keying brought its handles back from the values
the drawer opened on, which describe neither what the committed workspace has
deployed nor the draft the failed one still holds.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4
Warm sessions share one draft cell and one server row per item, so a rename or a
draft-only discard made in one left the others editing a path that is gone. The
editor's report now goes to the page, which re-points every tab on that item in
that workspace — the same fan-out the chat tools already use.
Matching on the item is also what the host's own staleness check was for: a
report that lands after its tab has been re-pointed simply finds nothing to
match, so `stillShowing` goes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4
The fact is sticky by design, so a delete that landed earlier for a key kept
answering after the server moved out from under it: a discard that conflicted
left it standing, and the conflict itself is tracked apart from the state the
verdict reads, so both said the draft was gone. It is dropped now wherever the
server stops being ours to describe — on a conflict, and on a remote resync.
Also corrects what the partial-save comments claim. Following the committed
rename re-keys every workspace's handle, so a workspace whose write never
happened does lose sight of its draft in this editor; the draft is still on the
server under the path that workspace holds the item at, which is where it stays
reachable. Saying it stayed reachable here was wrong.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4
The draft hint has a second writer: every editor clears it from an effect as
soon as a workspace sits at its baseline, which a discard does synchronously —
so a delete that then failed read as one that landed, and the host left on an
item still there. `postSave` now records which payload last landed for a key,
a fact only it can write, and the verdict reads that.
A delete winning is also not enough on its own. The form stays editable while
the flush is in flight and a flush only submits what it read at its start, so an
edit made in between sits on the debouncer and would recreate the draft behind
the host's back. The key has to be idle too.
The variable drawer no longer closes when a save failed partway: that is the
case where the workspace still needing one is retried from it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4
The discard verdict read the sync pipeline's own state, which settles whichever
write it ends up sending: the form stays editable after Discard, so typing then
replaces the queued delete with an upsert and the pipeline reports success for
it. The draft-state hint is written by the POST that actually landed, so it
answers what the pipeline cannot — whether the draft is still there.
A multi-workspace save writes in sequence and a later workspace throwing aborts
the rest, but what the acting workspace wrote is deployed either way. Both saves
now report that path, so a host is never left pointing at one the item has
already moved off.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4
A discard queues its delete through the autosave debouncer and returns, so the
host left for the list while the row was still there — and left just the same
when the delete failed outright. The three callbacks now wait for it and report
removal only when it landed; a delete that didn't means the item is still there.
`WsSpecificVersions` can point a form at a linked workspace, where a rename is
that workspace's alone. Both editors took the saved path from whichever version
was selected and handed it to a host acting on another one, and settled every
workspace's draft against that single path. The reported path now comes from the
acting workspace's own write, and each workspace settles against its own.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4
The resource save read its snapshot back out of the live form after the request
returned, so the "did anything change meanwhile" test compared the edit against
itself and always said no — the mid-request edit became the deployed baseline
instead. Its payloads, its submitted path and the schedule's config are now all
pinned before the first await, which also makes the schedule's test see edits to
`args`, `retry` and `labels` rather than only its scalar fields.
The three copies of the settling rule become `settleDraftAfterWrite`. A save
that moved the item resets the cell rather than keeping a diverged one: the edit
cannot follow — a freshly acquired cell reads only its own default — and the
alternative is a draft stranded under a path the item no longer occupies.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4
The forms stay editable during a save, and the post-write reset dropped the
local draft back to the state that was sent — so anything typed in between
vanished when the request returned. The draft is now dropped only while it
still holds what was written; an edit on top of the save survives as the
unsaved change it is, and the hosts that remount pick it back up.
A multi-workspace variable save also adopted its baselines only after every
write had succeeded, so a later workspace throwing left an earlier one's draft
dropped below a baseline that never moved: dirty forever, and retried as a
create it had already made. Each lands as its own write does.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4
The schedule and variable views re-pointed one live editor at each new path, so
the previous item's load — `openEdit`, `editVariable` and everything they await
— finished into the new item's form, baseline, draft cell and permissions,
last response winning. Keying each on its path gives every item its own
instance, with nothing of the next one's to land on. The resource host already
worked this way.
A report's guard also matched kind and path but not workspace, so re-scoping an
unsent session left a rename from the old workspace free to retarget the new
one's tab onto a path that exists only where it was written.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4
Guarding the host was not enough: an editor's own continuation runs first, and
every piece of state it touches after the await belongs to whatever the editor
shows by then. The variable save now pins its payloads, its saved path and its
draft key before the first write and only adopts a baseline while it is still
the variable it saved; the resource save and the schedule remount stop at the
same check.
The host guard also matched on path alone, so a late report about a schedule
reached a resource or variable that happened to share its path. It now matches
the entity kind too, bound at the branch that mounts each editor.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4
A tab id outlives what the tab points at. An editor reports a removal or a
rename after the write it awaited, and by then the tab may have been re-pointed
onto another item — so the report was applied to whatever the tab held, and the
URL built out of that unrelated location: discarding schedule A and opening
schedule B sent B back to the list. Every such report now carries the path it
started from, read before the await, and the host acts only while the tab is
still showing that entity.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4
A discard resolves asynchronously — the schedule's awaits a fetch of the
runnable — so by the time it lands the user may be looking at another tab.
Routing removal through `onBack` moved whichever tab was active, taking that
one to the list while the tab whose item was gone stayed on it. Removal now
retargets by tab id, as the deletion and rename paths already do.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4
The inline draft banner's Discard calls UserDraft.discard straight off, not the
discard_local_draft tool, so nothing told the tab about it. For a draft-only
item the draft IS the item: discarding deletes its only representation and the
tab kept editing a synthesized stand-in for something that no longer exists.
Each editor now reports whether the item survived, and the session views hand
that back to the tab, which returns to the list it was opened from.
A draft-only schedule also opens with its path editable — saving CREATEs — so
the save can land somewhere other than where the tab points. The schedule host
discarded onUpdate's saved path and only remounted, re-opening the old path;
it now retargets the tab like the resource and variable hosts do.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4
* fix(oauth): show the account chooser on Google/Microsoft login
Without `prompt=select_account`, Google and Microsoft silently reuse the single
active browser session, so a user with more than one account has no way to pick
which one to sign in with.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0196aV8v36ukoQcD7L2scvmH
* chore: pin ee ref for the oauth login extra_params fix
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0196aV8v36ukoQcD7L2scvmH
* fix(oauth): only ask for the account chooser on an explicit login click
The login page now sends `user_initiated=true` when someone clicks a
provider button, and the backend applies the provider's `extra_params`
only for those requests.
Someone whose browser holds a single Google session whose email is
already registered under a different login type hits
"an user with the email associated to this login exists but with a
different login type" and, with no account chooser, has no way to offer
a different account. The chooser belongs on that click.
It does not belong on the `auto_login_provider` redirect, whose whole
purpose is to sign a public-app or approval-page visitor in without
interaction.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0196aV8v36ukoQcD7L2scvmH
* fix(oauth): make the account chooser the default, not the opt-in
The login page now flags only the `auto_login_provider` redirect, with
`auto=true`; every other login — a click on a provider button, or the
endpoint opened as a plain URL — gets the provider's extra params.
`/api/oauth/login/*` is whitelisted in `public_app_layer` and reachable
directly, so an opt-in flag would silently drop the account chooser for
every caller that is not our own button.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0196aV8v36ukoQcD7L2scvmH
* chore: update ee-repo-ref to f5d6b6b8dd00b0141308337ac97f4685781f2b1c
This commit updates the EE repository reference after PR #776 was merged in windmill-ee-private.
Previous ee-repo-ref: 5684bb0f63dce08d6ce9ab0183072c8b4fce4b2e
New ee-repo-ref: f5d6b6b8dd00b0141308337ac97f4685781f2b1c
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>