1- import { ArrowLeftIcon } from "@phosphor-icons/react" ;
1+ import { ArrowLeftIcon , CheckCircleIcon } from "@phosphor-icons/react" ;
22import type { LoopSchemas } from "@posthog/api-client/loops" ;
33import { isUploadableSkillSource } from "@posthog/core/message-editor/skillTags" ;
44import { 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