From 65c641a9da931fa19c522d0b3bf26fcf86f5bc1c Mon Sep 17 00:00:00 2001 From: hugocasa Date: Fri, 14 Aug 2026 22:37:20 +0200 Subject: [PATCH] feat: keep an agent's in-progress edits on the agent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Editing a linked agent forks it into the step, which is what makes the edits runnable there — but the agent is what is being edited, so that is where the unsaved state belongs. The edit is mirrored into the agent's own resource draft as it is made. It then survives leaving the flow, shows the agent as drafted wherever it appears, and is what evals run when asked to run the draft rather than what is deployed. Deploying or cancelling clears it; opening Edit without changing anything does not mark the agent as drafted. Co-Authored-By: Claude Opus 5 --- .../flows/content/AgentResourceBar.svelte | 75 ++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/frontend/src/lib/components/flows/content/AgentResourceBar.svelte b/frontend/src/lib/components/flows/content/AgentResourceBar.svelte index a869635c10..b23208e823 100644 --- a/frontend/src/lib/components/flows/content/AgentResourceBar.svelte +++ b/frontend/src/lib/components/flows/content/AgentResourceBar.svelte @@ -7,6 +7,7 @@ import TextInput from '$lib/components/text_input/TextInput.svelte' import { AiEvalsService, ResourceService, type InputTransform } from '$lib/gen' import { workspaceStore } from '$lib/stores' + import { UserDraftDbSyncer } from '$lib/userDraftDbSyncer.svelte' import { sendUserToast } from '$lib/toast' import { Bot, Save, Unlink, Pencil } from 'lucide-svelte' import { @@ -330,6 +331,8 @@ const path = editingPath try { if (await persist(path)) { + // Deployed: the draft described what is now the agent, so it describes nothing. + clearDraft(path) sendUserToast(`Updated agent ${path}`) } } catch (e) { @@ -413,6 +416,67 @@ } } + /** + * Mirror the edit in progress into the agent's own draft. + * + * The step is forked while you edit it, which is what makes the edits runnable here — but the + * agent is what is being edited, so that is where the unsaved state belongs. Kept there, it + * survives closing the flow, shows the agent as drafted wherever it appears, and is what evals + * run when you ask them to run the draft rather than what is deployed. + */ + function mirrorEditToDraft(path: string) { + if (!ws) return + UserDraftDbSyncer.save({ + workspace: ws, + itemKind: 'resource', + path, + value: { + path, + description: '', + args: inputTransformsToAgentConfig(inputTransforms, tools), + labels: undefined, + wsSpecific: false + } + }) + } + + /** Drop it: the edit was saved, so the draft describes nothing that is not deployed, or it was + * abandoned, so it describes nothing at all. */ + function clearDraft(path: string) { + if (!ws) return + UserDraftDbSyncer.save({ workspace: ws, itemKind: 'resource', path, value: null }) + } + + $effect(() => { + const path = editingPath + // Read so an edit to either re-runs this. + const brain = JSON.stringify(inputTransforms) + const toolset = JSON.stringify(tools) + untrack(() => { + if (!path || (brain === mirroredBrain && toolset === mirroredTools)) return + // The state the fork opened on is what the agent already holds: mirroring it would + // mark an untouched agent as drafted. + if (mirroredBrain === undefined) { + mirroredBrain = brain + mirroredTools = toolset + return + } + mirroredBrain = brain + mirroredTools = toolset + mirrorEditToDraft(path) + }) + }) + let mirroredBrain: string | undefined = $state(undefined) + let mirroredTools: string | undefined = $state(undefined) + $effect(() => { + if (!editingPath) { + untrack(() => { + mirroredBrain = undefined + mirroredTools = undefined + }) + } + }) + // Cancel discards the edits and re-links the step, leaving the agent untouched. Diverging from // the agent is Unlink's job, on the linked card. Edit kept this flow's `tool_inputs` off the // forked tools rather than folding them in, so they survive the round trip as overrides. @@ -423,6 +487,7 @@ if (!path) { return } + clearDraft(path) agent = path tools = [] inputTransforms = flowLocalInputs(inputTransforms) @@ -493,8 +558,16 @@
-
{editingPath}
+
+ {editingPath} + {#if mirroredBrain !== undefined} + unsaved changes + {/if} +
+ {#if mirroredBrain !== undefined} + Kept on the agent as a draft, so they survive leaving this flow. + {/if} Saving affects every flow using it {#snippet text()} Save changes writes back to the saved agent and re-links this step, so every flow