mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-18 08:03:11 +00:00
fix(sessions): keep what a released editor was holding, and scope the ws selector to the drawer
An editor stays editable while its save is in flight, so a tab left in that window releases a cell that may carry an edit newer than the write — and a released cell reads as no cell at all, which the settle took for a draft nothing was watching and deleted. Keep what the last holder let go of, bounded like the other write markers, and let the settle read that when no handle is left. The linked-workspace selector follows the hand-off button: a session tab is keyed to one workspace, so re-pointing the editor at a linked one from inside a tab acted on an item the session is not about. A removal marker now also requires the delete to be what landed, as `flushDraftDelete` does: an upsert queued behind it displaces the delete, and waiting for the chain then reports idle on a draft that is back. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4
This commit is contained in:
co-authored by
Claude Opus 5
parent
e65929665c
commit
3581a0e380
@@ -206,12 +206,18 @@
|
||||
>
|
||||
History
|
||||
</Button>
|
||||
<WsSpecificVersions
|
||||
kind="resource"
|
||||
workspaceId={effectiveWorkspace}
|
||||
initialPath={path}
|
||||
bind:selected
|
||||
/>
|
||||
<!-- Drawer-only, for the same reason as the hand-off above: a session tab is
|
||||
keyed to one workspace — its title, the chat's write targeting and the
|
||||
deploy set all read it — so re-pointing the editor at a linked workspace
|
||||
from inside one would act on an item the session is not about. -->
|
||||
{#if useDrawer}
|
||||
<WsSpecificVersions
|
||||
kind="resource"
|
||||
workspaceId={effectiveWorkspace}
|
||||
initialPath={path}
|
||||
bind:selected
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
<Button
|
||||
variant="accent"
|
||||
|
||||
@@ -429,7 +429,11 @@
|
||||
{#if useDrawer}
|
||||
<OpenInSessionButton source={sessionSource} />
|
||||
{/if}
|
||||
{#if edit && curWs}
|
||||
<!-- Drawer-only, for the same reason as the hand-off above: a session tab is
|
||||
keyed to one workspace — its title, the chat's write targeting and the
|
||||
deploy set all read it — so re-pointing the editor at a linked workspace
|
||||
from inside one would act on an item the session is not about. -->
|
||||
{#if edit && curWs && useDrawer}
|
||||
<WsSpecificVersions kind="variable" workspaceId={curWs} {initialPath} bind:selected />
|
||||
{/if}
|
||||
<Button
|
||||
|
||||
@@ -6184,10 +6184,13 @@ async function discardLocalDraft(
|
||||
// not leave one standing for a later action to spend, and an edit made across the
|
||||
// delete recreates the draft — leaving the item the marker would report as gone.
|
||||
if (removesItem && discardedKind) {
|
||||
const settled =
|
||||
UserDraftDbSyncer.getState({ workspace, itemKind: discardedKind, path: storagePath })
|
||||
.state === 'none'
|
||||
if (settled) {
|
||||
const query = { workspace, itemKind: discardedKind, path: storagePath }
|
||||
// Settled AND settled on the delete: an upsert queued behind it displaces the
|
||||
// delete, and waiting for the chain then reports idle on a draft that is back.
|
||||
const removed =
|
||||
UserDraftDbSyncer.getState(query).state === 'none' &&
|
||||
UserDraftDbSyncer.lastLandedWasDelete(query)
|
||||
if (removed) {
|
||||
UserDraft.recordDraftOnlyDiscard(discardedKind, storagePath, { workspace })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,6 +142,36 @@ function noteMarker(set: Set<string>, key: string): void {
|
||||
set.delete(oldest)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* What a cell held when its last holder let go. An editor stays editable while
|
||||
* its save is in flight, so a tab closed in that window releases a cell that may
|
||||
* carry an edit newer than the write — and once released there is no handle left
|
||||
* to read it from. Same cap and reason as the marker sets; taken by whoever asks
|
||||
* (`settleDraftAfterWrite`), and dropped when the key is acquired again, since a
|
||||
* live entry answers for itself.
|
||||
*/
|
||||
const releasedValues = new Map<string, unknown>()
|
||||
/** Read once: what it describes is settled by the caller that reads it. */
|
||||
function takeReleasedValue<V>(
|
||||
itemKind: UserDraftItemKind,
|
||||
path: string,
|
||||
opts?: UserDraftOptions
|
||||
): V | undefined {
|
||||
const mk = mapKey(resolveWorkspace(opts), itemKind, path)
|
||||
const value = releasedValues.get(mk) as V | undefined
|
||||
releasedValues.delete(mk)
|
||||
return value
|
||||
}
|
||||
|
||||
function noteReleasedValue(key: string, value: unknown): void {
|
||||
releasedValues.set(key, value)
|
||||
while (releasedValues.size > MAX_WRITE_MARKERS) {
|
||||
const oldest = releasedValues.keys().next().value
|
||||
if (oldest === undefined || oldest === key) break
|
||||
releasedValues.delete(oldest)
|
||||
}
|
||||
}
|
||||
|
||||
const liveEditorDrafts = new Map<string, LiveEditorDraft>()
|
||||
/**
|
||||
* Map keys whose entry should start `syncSuspended` on acquire. Lets
|
||||
@@ -285,9 +315,12 @@ export async function settleDraftAfterWrite<V>(
|
||||
savedPath: string,
|
||||
opts?: UserDraftOptions
|
||||
): Promise<void> {
|
||||
// No cell at all is not a newer edit — the editor has been released, and its
|
||||
// draft is precisely what nothing is watching any more.
|
||||
const diverged = live !== undefined && !draftValuesEqual(live, written)
|
||||
// A released cell reads as no cell at all, and its draft is then precisely what
|
||||
// nothing is watching any more — except when the release itself is what took
|
||||
// the handle away mid-write, in which case what it held still speaks for the
|
||||
// user. Absent both, there is nothing newer to protect.
|
||||
const held = live !== undefined ? live : takeReleasedValue<V>(itemKind, fromPath, opts)
|
||||
const diverged = held !== undefined && !draftValuesEqual(held, written)
|
||||
if (savedPath === fromPath && diverged) return
|
||||
// Sent before this resolves, not left on the keystroke debounce: callers report
|
||||
// the write and remount on it, and both read a cell this has to have finished
|
||||
@@ -890,6 +923,7 @@ function acquireEntry(
|
||||
canBeDisabled = false
|
||||
): void {
|
||||
const mk = mapKey(workspace, itemKind, path)
|
||||
releasedValues.delete(mk)
|
||||
const existing = entries.get(mk)
|
||||
if (existing) {
|
||||
existing.count++
|
||||
@@ -1026,6 +1060,7 @@ function releaseEntry(mk: string): void {
|
||||
// only here, once, at refcount 0. This is what lets multiple holders (warm
|
||||
// session previews + the nav editor) share the entry and drop in any order.
|
||||
if (entry.count <= 0) {
|
||||
noteReleasedValue(mk, snapshotDraftValue(entry.state.val))
|
||||
// The live entry was authoritative while mounted; once gone, drop any
|
||||
// cached write for this key so a later read falls back to the server
|
||||
// rather than a value the editor may have changed in the meantime.
|
||||
|
||||
Reference in New Issue
Block a user