diff --git a/frontend/src/lib/components/copilot/chat/global/userDraftAdapter.ts b/frontend/src/lib/components/copilot/chat/global/userDraftAdapter.ts index 3b1eeab8e8..f2c3f02431 100644 --- a/frontend/src/lib/components/copilot/chat/global/userDraftAdapter.ts +++ b/frontend/src/lib/components/copilot/chat/global/userDraftAdapter.ts @@ -421,9 +421,6 @@ export async function persistGlobalDraft( const itemKind = itemKindFor(type, opts.triggerKind) if (!itemKind) throw new Error(`Unsupported draft type "${type}".`) const storagePath = resolveDraftStoragePath(workspace, itemKind, path) - // Writing the item back means it exists again, so any removal marker left from - // an earlier draft-only discard is void. - UserDraft.clearDraftOnlyDiscard(itemKind, storagePath, { workspace }) UserDraft.seed(itemKind, storagePath, value, { workspace }) await UserDraftDbSyncer.save({ workspace, diff --git a/frontend/src/lib/components/triggers/schedules/ScheduleEditorInner.svelte b/frontend/src/lib/components/triggers/schedules/ScheduleEditorInner.svelte index d396c5fcb7..938f14b78f 100644 --- a/frontend/src/lib/components/triggers/schedules/ScheduleEditorInner.svelte +++ b/frontend/src/lib/components/triggers/schedules/ScheduleEditorInner.svelte @@ -626,10 +626,10 @@ const isSaved = await saveScheduleFromCfg(scheduleCfg, edit, wsId!) if (isSaved) { // A create deploys the schedule enabled whatever the form said (see - // saveScheduleFromCfg), so what counts as written records that — otherwise - // the baseline below claims a state the server does not have. The form - // follows only while it still holds what was sent: changed during the - // request, it is a newer edit, and `keptEdit` below is what protects it. + // saveScheduleFromCfg), so what counts as written records that — the + // baseline would otherwise claim a state the server does not have. The form + // follows only while it still holds what was sent; changed since, it is an + // edit of its own. if (wasCreate) { const sentEnabled = scheduleCfg.enabled scheduleCfg.enabled = true @@ -776,7 +776,20 @@ if (initialConfig) initialConfig.enabled = nEnabled // This request carried the enabled flag alone: anything else the form has // diverged into is an edit of the user's, which a remount would drop. - onUpdate?.(path, path, ws, !draftValuesEqual(getScheduleCfg(), initialConfig)) + const keptEdit = !draftValuesEqual(getScheduleCfg(), initialConfig) + // Setting `enabled` above queued a draft against the pre-toggle baseline. + // It matches the new one, so nothing would ever show it again — but it is + // still a row on the server and a `*` on the list until it is settled away. + if (initialConfig) + settleDraftAfterWrite( + 'trigger_schedule', + $state.snapshot(initialConfig), + getScheduleCfg(), + path, + path, + { workspace: ws ?? undefined } + ) + onUpdate?.(path, path, ws, keptEdit) } } diff --git a/frontend/src/lib/userDraft.svelte.ts b/frontend/src/lib/userDraft.svelte.ts index 44975c0865..d1a95fe86f 100644 --- a/frontend/src/lib/userDraft.svelte.ts +++ b/frontend/src/lib/userDraft.svelte.ts @@ -320,6 +320,10 @@ export const UserDraft = { save(itemKind: UserDraftItemKind, path: string, value: V, opts?: UserDraftOptions): void { const ws = resolveWorkspace(opts) const mk = mapKey(ws, itemKind, path) + // The item has a draft again, so it exists again: a marker still standing + // (nothing consumed it, because nothing was listening) describes an item this + // write replaced, and would send the next editor to touch it away. + draftOnlyDiscards.delete(mk) const entry = entries.get(mk) if (entry) { // The reactive effect in `acquireEntry` observes this write @@ -463,6 +467,7 @@ export const UserDraft = { seed(itemKind: UserDraftItemKind, path: string, value: V, opts?: UserDraftOptions): void { const ws = resolveWorkspace(opts) const mk = mapKey(ws, itemKind, path) + draftOnlyDiscards.delete(mk) const entry = entries.get(mk) if (!entry) { noteMarker(seedMisses, mk) @@ -483,12 +488,6 @@ export const UserDraft = { noteMarker(draftOnlyDiscards, mapKey(resolveWorkspace(opts), itemKind, path)) }, - /** Drop any removal marker for this cell — the item exists again, so a marker - * still standing (nothing consumed it, because nothing was listening) would be - * read by whatever touches it next and send its editor away. */ - clearDraftOnlyDiscard(itemKind: UserDraftItemKind, path: string, opts?: UserDraftOptions): void { - draftOnlyDiscards.delete(mapKey(resolveWorkspace(opts), itemKind, path)) - }, /** Whether the last discard for this cell removed the item outright (see * {@link recordDraftOnlyDiscard}), clearing the record. */ @@ -953,6 +952,8 @@ function acquireEntry( // copy. `untrack` so reactive reads in the predicate (the editor's // post-deploy baseline) don't re-fire the mirror. const atBaseline = untrack(() => val !== undefined && (discardIf?.(val) ?? false)) + // Same as `save`: a value persisted here means the item exists again. + if (val !== undefined && !atBaseline) draftOnlyDiscards.delete(mk) void UserDraftDbSyncer.save({ workspace, itemKind,