From d8655189347f58df9d17e83dc55798baf7964279 Mon Sep 17 00:00:00 2001 From: hugocasa Date: Thu, 25 Jun 2026 16:12:12 +0200 Subject: [PATCH] feat: detect and guard against deploying stale drafts (#9768) * feat: detect and guard against deploying stale drafts Co-Authored-By: Claude Opus 4.8 * feat: extend stale-draft warning to low-code app drafts Co-Authored-By: Claude Opus 4.8 * refactor: discard stale draft on rebase instead of resetting to latest Co-Authored-By: Claude Opus 4.8 * feat: animate AI chat thinking block open/close like tool calls Co-Authored-By: Claude Opus 4.8 * feat: detect stale flow/app drafts by pinned version at load and deploy Co-Authored-By: Claude Opus 4.8 * fix: reset version-staleness state on new drafts and after app deploy Addresses review: new-draft route reuse left stale version/draftBaseVersion (false stale-draft modal on a fresh flow/app); app deploy left parent_version pinned to the superseded base (false 'not latest' on a follow-up deploy). Co-Authored-By: Claude Opus 4.8 --------- Co-authored-by: Claude Opus 4.8 --- .../src/lib/components/CompareDrafts.svelte | 35 +- .../src/lib/components/FlowBuilder.svelte | 10 +- .../apps/editor/AppEditorHeader.svelte | 14 +- frontend/src/lib/components/apps/types.ts | 7 + .../DraftEditorModals.svelte | 33 +- .../copilot/chat/AssistantMessage.svelte | 2 + .../copilot/chat/global/core.test.ts | 270 ++++++++++++++ .../components/copilot/chat/global/core.ts | 345 +++++++++++++++++- .../copilot/chat/global/userDraftAdapter.ts | 11 +- .../copilot/chat/global/workspaceItems.ts | 8 + frontend/src/lib/components/flow_builder.ts | 3 + .../components/raw_apps/rawAppDraftValue.ts | 8 +- frontend/src/lib/userDraft.svelte.ts | 1 + frontend/src/lib/utils_draft_deploy.ts | 9 +- .../(logged)/apps/edit/[...path]/+page.svelte | 28 ++ .../flows/edit/[...path]/+page.svelte | 14 + 16 files changed, 773 insertions(+), 25 deletions(-) diff --git a/frontend/src/lib/components/CompareDrafts.svelte b/frontend/src/lib/components/CompareDrafts.svelte index c8514e1fb6..867268cd1a 100644 --- a/frontend/src/lib/components/CompareDrafts.svelte +++ b/frontend/src/lib/components/CompareDrafts.svelte @@ -192,7 +192,7 @@ // side to diff the name against. Raw apps are fetched via the apps endpoint too // (it auto-detects raw from the deployed row and overlays the raw_app draft). const summaryCache = $state< - Record + Record >({}) async function fetchDraftSummary(item: Row) { @@ -216,9 +216,27 @@ path: item.path, getDraft: true }))) as any + // A draft is stale when the version it forked from no longer matches the + // current deployed head: a newer version was deployed after the draft began. + // Scripts compare `parent_hash` vs the deployed `hash`; flows the pinned + // `version_id` vs the deployed head `version_id`; apps the pinned + // `parent_version` vs the deployed head (`versions[last]`). + const draftBlob = r.draft as any + const appHead = Array.isArray(r.versions) ? r.versions[r.versions.length - 1] : undefined + const stale = + item.draftKind === 'script' + ? !!r.hash && !!draftBlob?.parent_hash && draftBlob.parent_hash !== r.hash + : item.draftKind === 'flow' + ? r.version_id != null && + draftBlob?.version_id != null && + draftBlob.version_id !== r.version_id + : appHead != null && + draftBlob?.parent_version != null && + draftBlob.parent_version !== appHead summaryCache[item.key] = { deployed: r.summary, - draft: (r.draft as any)?.summary, + draft: draftBlob?.summary, + stale, loading: false } } catch (error) { @@ -618,6 +636,19 @@ {/snippet} {/if} + {#if draftItem.mine && summaryCache[draftItem.key]?.stale} + + {#snippet trigger()} + + {/snippet} + {#snippet content()} +
+ Started from an older deployed version. A newer version was deployed after this + draft began. Review the latest deploy before deploying. +
+ {/snippet} +
+ {/if} {#if deploymentStatus[draftItem.key]?.status !== 'deployed'} {#if draftItem.draftKind === 'data_pipeline'}