@@ -64,6 +64,7 @@ import DAGEditorWithDocs from './DAGEditorWithDocs';
6464import { parseValidationMarkers } from './validationMarkers' ;
6565import { AgentSpecOverview } from './AgentSpecOverview' ;
6666import ExternalChangeDialog from './ExternalChangeDialog' ;
67+ import { useEditorScrollAnchor } from './useEditorScrollAnchor' ;
6768import { I18nText } from '@/i18n/I18nText' ;
6869import { I18nProps } from '@/i18n/I18nProps' ;
6970import { useI18n } from '@/i18n/I18nProvider' ;
@@ -121,6 +122,10 @@ function DAGSpec({ fileName, localDags, editorHints }: Props) {
121122 // Reference to the main container div
122123 const containerRef = React . useRef < HTMLDivElement > ( null ) ;
123124
125+ // Keeps the YAML editor still while the live preview above it resizes
126+ const { anchorRef : editorSectionRef , contentRef : previewRef } =
127+ useEditorScrollAnchor ( ) ;
128+
124129 // Reference to save function and refresh callback for keyboard shortcut
125130 const saveHandlerRef = React . useRef < ( ( ) => Promise < void > ) | null > ( null ) ;
126131 const refreshCallbackRef = React . useRef < ( ( ) => void ) | null > ( null ) ;
@@ -206,6 +211,8 @@ function DAGSpec({ fileName, localDags, editorHints }: Props) {
206211
207212 // Live server-side validation of the edited buffer. Cleared whenever the
208213 // buffer stops being dirty (save or discard), which also clears the markers.
214+ // Kept while the next check is pending so the preview above the editor does
215+ // not flip back to the saved spec on every keystroke.
209216 const [ liveValidation , setLiveValidation ] = React . useState < {
210217 errors : string [ ] ;
211218 warnings : string [ ] ;
@@ -224,7 +231,6 @@ function DAGSpec({ fileName, localDags, editorHints }: Props) {
224231 }
225232
226233 const seq = ++ validateSeqRef . current ;
227- setLiveValidation ( null ) ;
228234 setIsValidating ( true ) ;
229235 const timer = window . setTimeout ( ( ) => {
230236 void client
@@ -760,90 +766,95 @@ function DAGSpec({ fileName, localDags, editorHints }: Props) {
760766 className = "flex min-h-0 flex-1 flex-col space-y-6 pb-8"
761767 ref = { containerRef }
762768 >
763- { warnings . length > 0 && (
764- < div
765- role = "status"
766- className = "rounded-md border border-amber-500/30 bg-amber-500/10 p-3 text-sm text-amber-800 dark:text-amber-200"
767- >
768- < div className = "mb-2 flex items-center gap-2 font-medium" >
769- < AlertTriangle className = "h-4 w-4" aria-hidden = "true" />
770- < I18nText text = { 'Warnings' } />
769+ < div ref = { previewRef } className = "flex-shrink-0 space-y-6" >
770+ { warnings . length > 0 && (
771+ < div
772+ role = "status"
773+ className = "rounded-md border border-amber-500/30 bg-amber-500/10 p-3 text-sm text-amber-800 dark:text-amber-200"
774+ >
775+ < div className = "mb-2 flex items-center gap-2 font-medium" >
776+ < AlertTriangle className = "h-4 w-4" aria-hidden = "true" />
777+ < I18nText text = { 'Warnings' } />
778+ </ div >
779+ < ul className = "list-disc space-y-1 pl-5" >
780+ { warnings . map ( ( warning ) => (
781+ < li
782+ key = { warning }
783+ className = "whitespace-normal break-words"
784+ >
785+ { warning }
786+ </ li >
787+ ) ) }
788+ </ ul >
771789 </ div >
772- < ul className = "list-disc space-y-1 pl-5" >
773- { warnings . map ( ( warning ) => (
774- < li
775- key = { warning }
776- className = "whitespace-normal break-words"
777- >
778- { warning }
779- </ li >
780- ) ) }
781- </ ul >
782- </ div >
783- ) }
784- { hasLocalDags && (
785- < div className = "flex-shrink-0" >
786- < div className = "overflow-x-auto -mx-2 px-2 scrollbar-thin scrollbar-thumb-gray-300" >
787- < Tabs className = "w-max min-w-full" >
788- < Tab
789- isActive = { activeTab === 'parent' }
790- onClick = { ( ) => handleActiveTabChange ( 'parent' ) }
791- className = "cursor-pointer whitespace-nowrap"
792- >
793- { data ?. dag ?. name } < I18nText text = { '(Parent)' } />
794- </ Tab >
795- { localDags ?. map (
796- ( localDag : components [ 'schemas' ] [ 'LocalDag' ] ) => (
797- < Tab
798- key = { localDag . name }
799- isActive = { activeTab === localDag . name }
800- onClick = { ( ) =>
801- handleActiveTabChange ( localDag . name )
802- }
803- className = "cursor-pointer whitespace-nowrap"
804- >
805- { localDag . name }
806- </ Tab >
807- )
808- ) }
809- </ Tabs >
790+ ) }
791+ { hasLocalDags && (
792+ < div className = "flex-shrink-0" >
793+ < div className = "overflow-x-auto -mx-2 px-2 scrollbar-thin scrollbar-thumb-gray-300" >
794+ < Tabs className = "w-max min-w-full" >
795+ < Tab
796+ isActive = { activeTab === 'parent' }
797+ onClick = { ( ) => handleActiveTabChange ( 'parent' ) }
798+ className = "cursor-pointer whitespace-nowrap"
799+ >
800+ { data ?. dag ?. name } < I18nText text = { '(Parent)' } />
801+ </ Tab >
802+ { localDags ?. map (
803+ ( localDag : components [ 'schemas' ] [ 'LocalDag' ] ) => (
804+ < Tab
805+ key = { localDag . name }
806+ isActive = { activeTab === localDag . name }
807+ onClick = { ( ) =>
808+ handleActiveTabChange ( localDag . name )
809+ }
810+ className = "cursor-pointer whitespace-nowrap"
811+ >
812+ { localDag . name }
813+ </ Tab >
814+ )
815+ ) }
816+ </ Tabs >
817+ </ div >
810818 </ div >
811- </ div >
812- ) }
813-
814- { ( ( ) => {
815- if ( activeTab === 'parent' ) {
816- // While the buffer is dirty, preview the live validation
817- // result instead of the saved spec.
818- const previewDag = liveValidation ?. dag ?? data ?. dag ;
819- const previewErrors = liveValidation
820- ? liveValidation . errors
821- : data ?. errors ;
819+ ) }
820+
821+ { ( ( ) => {
822+ if ( activeTab === 'parent' ) {
823+ // While the buffer is dirty, preview the live validation
824+ // result instead of the saved spec.
825+ const previewDag = liveValidation ?. dag ?? data ?. dag ;
826+ const previewErrors = liveValidation
827+ ? liveValidation . errors
828+ : data ?. errors ;
829+ return (
830+ previewDag && (
831+ < div className = "flex-shrink-0" >
832+ { renderDAGContent ( previewDag , previewErrors ) }
833+ </ div >
834+ )
835+ ) ;
836+ }
837+ const selectedLocalDag = localDags ?. find (
838+ ( ld : components [ 'schemas' ] [ 'LocalDag' ] ) =>
839+ ld . name === activeTab
840+ ) ;
822841 return (
823- previewDag && (
842+ selectedLocalDag ?. dag && (
824843 < div className = "flex-shrink-0" >
825- { renderDAGContent ( previewDag , previewErrors ) }
844+ { renderDAGContent (
845+ selectedLocalDag . dag ,
846+ selectedLocalDag . errors
847+ ) }
826848 </ div >
827849 )
828850 ) ;
829- }
830- const selectedLocalDag = localDags ?. find (
831- ( ld : components [ 'schemas' ] [ 'LocalDag' ] ) =>
832- ld . name === activeTab
833- ) ;
834- return (
835- selectedLocalDag ?. dag && (
836- < div className = "flex-shrink-0" >
837- { renderDAGContent (
838- selectedLocalDag . dag ,
839- selectedLocalDag . errors
840- ) }
841- </ div >
842- )
843- ) ;
844- } ) ( ) }
851+ } ) ( ) }
852+ </ div >
845853
846- < section className = "flex-shrink-0 space-y-3" >
854+ < section
855+ ref = { editorSectionRef }
856+ className = "flex-shrink-0 space-y-3"
857+ >
847858 < h2 className = "text-lg font-semibold text-foreground" >
848859 < I18nText text = { 'YAML' } />
849860 </ h2 >
0 commit comments