Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.

Commit fe96c11

Browse files
committed
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
1 parent 4eee585 commit fe96c11

1 file changed

Lines changed: 71 additions & 9 deletions

File tree

packages/ui/src/features/loops/components/LoopDetailView.tsx

Lines changed: 71 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ArrowLeftIcon } from "@phosphor-icons/react";
1+
import { ArrowLeftIcon, CheckCircleIcon } from "@phosphor-icons/react";
22
import type { LoopSchemas } from "@posthog/api-client/loops";
33
import { isUploadableSkillSource } from "@posthog/core/message-editor/skillTags";
44
import { useHostTRPC } from "@posthog/host-router/react";
@@ -589,8 +589,31 @@ function InstructionsSection({ loop }: { loop: LoopSchemas.Loop }) {
589589
const updateLoop = useUpdateLoop(loop.id);
590590
const primarySkill = primaryLoopSkillBundle(loop);
591591
const [draft, setDraft] = useState<string | null>(null);
592+
const [justSaved, setJustSaved] = useState(false);
593+
const draftRef = useRef<string | null>(null);
594+
const savedInstructionsRef = useRef(loop.instructions);
595+
const saveInFlightRef = useRef(false);
592596
// Escape reverts and blurs; skip the resulting onBlur save.
593597
const skipCommit = useRef(false);
598+
const autosaveTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
599+
const savedTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
600+
601+
const clearAutosave = () => {
602+
if (autosaveTimer.current) clearTimeout(autosaveTimer.current);
603+
autosaveTimer.current = null;
604+
};
605+
606+
useEffect(
607+
() => () => {
608+
if (autosaveTimer.current) clearTimeout(autosaveTimer.current);
609+
if (savedTimer.current) clearTimeout(savedTimer.current);
610+
},
611+
[],
612+
);
613+
614+
useEffect(() => {
615+
savedInstructionsRef.current = loop.instructions;
616+
}, [loop.instructions]);
594617

595618
const commit = (value: string) => {
596619
if (skipCommit.current) {
@@ -599,22 +622,42 @@ function InstructionsSection({ loop }: { loop: LoopSchemas.Loop }) {
599622
}
600623
const trimmed = value.trim();
601624
if (!trimmed) {
625+
draftRef.current = null;
602626
setDraft(null);
603627
return;
604628
}
605-
if (updateLoop.isPending) return;
606-
if (trimmed === loop.instructions.trim()) {
607-
setDraft(null);
629+
if (saveInFlightRef.current) return;
630+
if (trimmed === savedInstructionsRef.current.trim()) {
631+
if (draftRef.current?.trim() === trimmed) {
632+
draftRef.current = null;
633+
setDraft(null);
634+
}
608635
return;
609636
}
637+
saveInFlightRef.current = true;
610638
updateLoop.mutate(
611639
{ instructions: trimmed },
612640
{
613-
onSuccess: () => {
641+
onSuccess: (savedLoop) => {
642+
saveInFlightRef.current = false;
643+
savedInstructionsRef.current = savedLoop.instructions;
644+
const latestDraft = draftRef.current;
645+
if (
646+
latestDraft !== null &&
647+
latestDraft.trim() !== savedInstructionsRef.current.trim()
648+
) {
649+
commit(latestDraft);
650+
return;
651+
}
652+
draftRef.current = null;
614653
setDraft(null);
615-
toast.success("Instructions updated");
654+
setJustSaved(true);
655+
if (savedTimer.current) clearTimeout(savedTimer.current);
656+
savedTimer.current = setTimeout(() => setJustSaved(false), 2000);
616657
},
617658
onError: (error) => {
659+
saveInFlightRef.current = false;
660+
draftRef.current = null;
618661
setDraft(null);
619662
toast.error("Failed to update instructions", {
620663
description: error.message,
@@ -624,6 +667,12 @@ function InstructionsSection({ loop }: { loop: LoopSchemas.Loop }) {
624667
);
625668
};
626669

670+
const queueAutosave = (value: string) => {
671+
clearAutosave();
672+
setJustSaved(false);
673+
autosaveTimer.current = setTimeout(() => commit(value), 750);
674+
};
675+
627676
return (
628677
<Flex direction="column" gap="3">
629678
<Flex align="center" gap="2">
@@ -632,18 +681,31 @@ function InstructionsSection({ loop }: { loop: LoopSchemas.Loop }) {
632681
</Text>
633682
{updateLoop.isPending ? (
634683
<Text className="text-[11px] text-gray-10">Saving…</Text>
684+
) : justSaved ? (
685+
<Flex align="center" gap="1" className="text-(--green-11)">
686+
<CheckCircleIcon size={13} />
687+
<Text className="text-[11px]">Saved</Text>
688+
</Flex>
635689
) : null}
636690
</Flex>
637691
<Textarea
638692
value={draft ?? loop.instructions}
639-
disabled={updateLoop.isPending}
640693
aria-label="Loop instructions"
641694
className="max-h-[400px] min-h-[200px] bg-(--color-panel-solid) text-[12.5px] leading-relaxed"
642-
onChange={(e) => setDraft(e.currentTarget.value)}
643-
onBlur={(e) => commit(e.currentTarget.value)}
695+
onChange={(e) => {
696+
const value = e.currentTarget.value;
697+
draftRef.current = value;
698+
setDraft(value);
699+
queueAutosave(value);
700+
}}
701+
onBlur={(e) => {
702+
clearAutosave();
703+
commit(e.currentTarget.value);
704+
}}
644705
onKeyDown={(e) => {
645706
if (e.key === "Escape") {
646707
skipCommit.current = true;
708+
draftRef.current = null;
647709
setDraft(null);
648710
e.currentTarget.blur();
649711
}

0 commit comments

Comments
 (0)