From fe96c11375d5351e37457ca2ed453cafde08d279 Mon Sep 17 00:00:00 2001 From: Alessandro Pogliaghi Date: Thu, 30 Jul 2026 12:59:40 +0100 Subject: [PATCH] fix(loops): autosave instruction edits Save loop instruction changes after a short idle period or immediately on blur, and show inline save progress and confirmation. Generated-By: PostHog Code Task-Id: db118151-38ac-40f1-9df2-8d79fe4fad4c chore: cleaner --- .../loops/components/LoopDetailView.tsx | 80 ++++++++++++++++--- 1 file changed, 71 insertions(+), 9 deletions(-) diff --git a/packages/ui/src/features/loops/components/LoopDetailView.tsx b/packages/ui/src/features/loops/components/LoopDetailView.tsx index b274013e43..24a955dcd8 100644 --- a/packages/ui/src/features/loops/components/LoopDetailView.tsx +++ b/packages/ui/src/features/loops/components/LoopDetailView.tsx @@ -1,4 +1,4 @@ -import { ArrowLeftIcon } from "@phosphor-icons/react"; +import { ArrowLeftIcon, CheckCircleIcon } from "@phosphor-icons/react"; import type { LoopSchemas } from "@posthog/api-client/loops"; import { isUploadableSkillSource } from "@posthog/core/message-editor/skillTags"; import { useHostTRPC } from "@posthog/host-router/react"; @@ -589,8 +589,31 @@ function InstructionsSection({ loop }: { loop: LoopSchemas.Loop }) { const updateLoop = useUpdateLoop(loop.id); const primarySkill = primaryLoopSkillBundle(loop); const [draft, setDraft] = useState(null); + const [justSaved, setJustSaved] = useState(false); + const draftRef = useRef(null); + const savedInstructionsRef = useRef(loop.instructions); + const saveInFlightRef = useRef(false); // Escape reverts and blurs; skip the resulting onBlur save. const skipCommit = useRef(false); + const autosaveTimer = useRef | null>(null); + const savedTimer = useRef | null>(null); + + const clearAutosave = () => { + if (autosaveTimer.current) clearTimeout(autosaveTimer.current); + autosaveTimer.current = null; + }; + + useEffect( + () => () => { + if (autosaveTimer.current) clearTimeout(autosaveTimer.current); + if (savedTimer.current) clearTimeout(savedTimer.current); + }, + [], + ); + + useEffect(() => { + savedInstructionsRef.current = loop.instructions; + }, [loop.instructions]); const commit = (value: string) => { if (skipCommit.current) { @@ -599,22 +622,42 @@ function InstructionsSection({ loop }: { loop: LoopSchemas.Loop }) { } const trimmed = value.trim(); if (!trimmed) { + draftRef.current = null; setDraft(null); return; } - if (updateLoop.isPending) return; - if (trimmed === loop.instructions.trim()) { - setDraft(null); + if (saveInFlightRef.current) return; + if (trimmed === savedInstructionsRef.current.trim()) { + if (draftRef.current?.trim() === trimmed) { + draftRef.current = null; + setDraft(null); + } return; } + saveInFlightRef.current = true; updateLoop.mutate( { instructions: trimmed }, { - onSuccess: () => { + onSuccess: (savedLoop) => { + saveInFlightRef.current = false; + savedInstructionsRef.current = savedLoop.instructions; + const latestDraft = draftRef.current; + if ( + latestDraft !== null && + latestDraft.trim() !== savedInstructionsRef.current.trim() + ) { + commit(latestDraft); + return; + } + draftRef.current = null; setDraft(null); - toast.success("Instructions updated"); + setJustSaved(true); + if (savedTimer.current) clearTimeout(savedTimer.current); + savedTimer.current = setTimeout(() => setJustSaved(false), 2000); }, onError: (error) => { + saveInFlightRef.current = false; + draftRef.current = null; setDraft(null); toast.error("Failed to update instructions", { description: error.message, @@ -624,6 +667,12 @@ function InstructionsSection({ loop }: { loop: LoopSchemas.Loop }) { ); }; + const queueAutosave = (value: string) => { + clearAutosave(); + setJustSaved(false); + autosaveTimer.current = setTimeout(() => commit(value), 750); + }; + return ( @@ -632,18 +681,31 @@ function InstructionsSection({ loop }: { loop: LoopSchemas.Loop }) { {updateLoop.isPending ? ( Saving… + ) : justSaved ? ( + + + Saved + ) : null}