mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-06 16:02:23 +00:00
fix(frontend): stop spurious asset analysis toasts in the flow editor (#10349)
* fix(frontend): stop spurious asset analysis toasts in the flow editor The flow editor asks "Assets were detected in this step. Analyze entire flow for assets?" whenever a raw script step without asset metadata is selected and its code turns out to declare assets. Nothing recorded that the question had already been asked, and the selection watcher is re-created (and fires) on every structural change to the flow, so the prompt reappeared on every step click and every time a step was added. Steps created during the session — most visibly the ones an AI agent inserts one by one — were also treated as legacy steps, so each new step raised its own prompt even though writing their assets only completes an edit the user already made. Ask at most once per editor session, restrict the prompt to the modules the flow was loaded with, and skip re-analyzing a module whose content has not changed since its last parse. Fixes WIN-2251 * fix(frontend): key the asset inference cache on language and replay it inferAssets depends on the module's language as well as its content, and the content-only cache also turned a re-derivation into a no-op whenever the assets field alone was reset (undo/redo, reset to deployed, AI diff apply). Cache the inference result keyed on both inputs and re-apply it on a hit, so a cache hit is idempotent rather than a skip; that also removes the need for analyzeEntireFlow to force a re-parse. Cached values are copied before reaching the flow store, which would otherwise proxy them and let a later replay mutate the cache in place. Accepting "Analyze entire flow" now carries over to modules analyzed later in the session instead of leaving them for a prompt that will not be shown again. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(frontend): build the asset cache key without a raw NUL byte The separator was written as a literal U+0000, which makes git treat the Svelte source as binary: diffs render as +0/-0, blame and log -p stop working, and ripgrep skips the file. Build the key with JSON.stringify instead, which is unambiguous and keeps the file ASCII. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
4b7ab64a48
commit
9bbfe12011
@@ -117,10 +117,32 @@
|
||||
})
|
||||
}
|
||||
})
|
||||
// Prune all additionalAssetsMap entries from deleted modules
|
||||
|
||||
// Ids the flow was loaded with. Only those modules can carry asset metadata predating
|
||||
// the assets feature; anything appearing later was added in this session, so writing
|
||||
// its assets adds nothing on top of a change the user has already made. The editor
|
||||
// only mounts once the flow is loaded, so reading the prop here is the loaded state.
|
||||
const loadedModuleIds = new Set(getAllModules(modules).map((m) => m.id))
|
||||
// Analyzing is a flow-wide action: offer it once, then apply that answer to every
|
||||
// other module of the flow rather than asking again for each one. A prompt left
|
||||
// unanswered stays 'offered' and keeps those modules untouched for the session.
|
||||
let flowAnalysis: 'unoffered' | 'offered' | 'accepted' = 'unoffered'
|
||||
// Last inference per module, keyed on everything inferAssets depends on. The watchers
|
||||
// below are re-created whenever a module is added or removed and the selection watch
|
||||
// fires on creation, so a miss here means a re-parse on every structural edit.
|
||||
type InferredAssets = Extract<Awaited<ReturnType<typeof inferAssets>>, { status: 'ok' }>
|
||||
let analyzed: Record<string, { key: string; result: InferredAssets }> = {}
|
||||
|
||||
// Prune per-module caches from deleted modules
|
||||
$effect(() => {
|
||||
if (!flowGraphAssetsCtx) return
|
||||
const modulesSet = new Set(allModules.map((m) => m.id))
|
||||
for (const key of Object.keys(analyzed)) {
|
||||
if (!modulesSet.has(key)) delete analyzed[key]
|
||||
}
|
||||
for (const key of [...loadedModuleIds]) {
|
||||
if (!modulesSet.has(key)) loadedModuleIds.delete(key)
|
||||
}
|
||||
if (!flowGraphAssetsCtx) return
|
||||
for (const key of Object.keys(flowGraphAssetsCtx.val.additionalAssetsMap)) {
|
||||
if (!modulesSet.has(key)) {
|
||||
delete flowGraphAssetsCtx.val.additionalAssetsMap[key]
|
||||
@@ -129,6 +151,7 @@
|
||||
})
|
||||
|
||||
function analyzeEntireFlow() {
|
||||
flowAnalysis = 'accepted'
|
||||
for (const mod of allModules) {
|
||||
if (mod.value.type === 'rawscript') {
|
||||
parseAndUpdateRawScriptModule(mod.value, mod.id)
|
||||
@@ -136,27 +159,37 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function parseAndUpdateRawScriptModule(
|
||||
v: RawScript,
|
||||
modId: string,
|
||||
isUserEdit: boolean = true
|
||||
) {
|
||||
console.log('Parsing assets for RawScript module', modId)
|
||||
let inferAssetsResult = await inferAssets(v.language, v.content)
|
||||
if (inferAssetsResult.status === 'error') return
|
||||
if (flowGraphAssetsCtx) flowGraphAssetsCtx.val.sqlQueries[modId] = inferAssetsResult.sql_queries
|
||||
let newAssets = inferAssetsResult.assets as AssetWithAltAccessType[]
|
||||
async function parseAndUpdateRawScriptModule(v: RawScript, modId: string, prompt = false) {
|
||||
const key = JSON.stringify([v.language, v.content])
|
||||
let inferred = analyzed[modId]?.key === key ? analyzed[modId].result : undefined
|
||||
if (!inferred) {
|
||||
const inferAssetsResult = await inferAssets(v.language, v.content)
|
||||
if (inferAssetsResult.status === 'error') return
|
||||
inferred = inferAssetsResult
|
||||
analyzed[modId] = { key, result: inferred }
|
||||
}
|
||||
// Copy before handing anything to the flow store: stored values become reactive
|
||||
// proxies, and a later replay of this same inference would mutate the cache.
|
||||
const { assets, sql_queries } = structuredClone(inferred)
|
||||
if (flowGraphAssetsCtx) flowGraphAssetsCtx.val.sqlQueries[modId] = sql_queries
|
||||
let newAssets = assets as AssetWithAltAccessType[]
|
||||
for (const asset of newAssets) {
|
||||
const old = v.assets?.find((a) => assetEq(a, asset))
|
||||
if (old?.alt_access_type) asset.alt_access_type = old.alt_access_type
|
||||
}
|
||||
const normalizedAssets = newAssets.length > 0 ? newAssets : undefined
|
||||
if (!deepEqual(v.assets, normalizedAssets)) {
|
||||
if (!isUserEdit && normalizedAssets && normalizedAssets.length > 0) {
|
||||
if (prompt && flowAnalysis !== 'accepted' && normalizedAssets?.length) {
|
||||
if (flowAnalysis === 'offered') return
|
||||
flowAnalysis = 'offered'
|
||||
// Long-lived because it is the only entry point to analyzeEntireFlow and it is
|
||||
// offered once: a toast the user misses cannot be brought back without a reload.
|
||||
sendUserToast(
|
||||
'Assets were detected in this step. Analyze entire flow for assets?',
|
||||
'warning',
|
||||
[{ label: 'Analyze entire flow', callback: () => analyzeEntireFlow() }]
|
||||
[{ label: 'Analyze entire flow', callback: () => analyzeEntireFlow() }],
|
||||
undefined,
|
||||
20000
|
||||
)
|
||||
} else {
|
||||
v.assets = normalizedAssets
|
||||
@@ -181,7 +214,11 @@
|
||||
// Also recompute if the module is selected
|
||||
watch([() => selectedId === mod.id], () => {
|
||||
if (selectedId === mod.id)
|
||||
parseAndUpdateRawScriptModule(modValue, mod.id, modValue.assets !== undefined)
|
||||
parseAndUpdateRawScriptModule(
|
||||
modValue,
|
||||
mod.id,
|
||||
modValue.assets === undefined && loadedModuleIds.has(mod.id)
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user