1010import { useState , useCallback , useRef , useEffect } from 'react'
1111import { getOrCreateIframe , getSandboxIframe , removeSandboxIframe } from '../lib/browserUseTools'
1212import { extractHTMLCode } from '../lib/htmlExtract'
13+ import { serializeIframeDomState , type SerializeOptions } from '../lib/domState'
1314
1415export type AgentLoopStatus = 'idle' | 'running' | 'paused' | 'error'
1516
@@ -36,6 +37,10 @@ export interface UseAgentLoopOptions {
3637 onScreenshot ?: ( dataUrl : string , iteration : number ) => void
3738 /** Called when iteration completes — receives iteration count and HTML */
3839 onIterationComplete ?: ( html : string | null , iteration : number ) => void
40+ /** Called before each model turn — receives serialized DOM-state JSON for context injection */
41+ onDomState ?: ( domState : string , iteration : number ) => void
42+ /** DOM-state serialization options (depth + node-count budget) */
43+ domStateOptions ?: SerializeOptions
3944 /** Debounce delay in ms for auto-refresh (default: 500) */
4045 autoRefreshDelay ?: number
4146 /** Iframe target ID (default: 'agent-loop') */
@@ -134,11 +139,14 @@ export function useAgentLoop(options: UseAgentLoopOptions = {}): {
134139 state : AgentLoopState
135140 actions : AgentLoopActions
136141 iframeEl : HTMLIFrameElement | null
142+ currentDomState : string | null
137143 screenshotRef : React . RefCallback < HTMLIFrameElement >
138144} {
139145 const {
140146 onScreenshot,
141147 onIterationComplete,
148+ onDomState,
149+ domStateOptions,
142150 autoRefreshDelay = 500 ,
143151 iframeId = 'agent-loop' ,
144152 } = options
@@ -153,12 +161,15 @@ export function useAgentLoop(options: UseAgentLoopOptions = {}): {
153161 lastScreenshotAt : null ,
154162 } )
155163
164+ const [ currentDomState , setCurrentDomState ] = useState < string | null > ( null )
165+
156166 const statusRef = useRef < AgentLoopStatus > ( 'idle' )
157167 const htmlRef = useRef < string | null > ( null )
158168 const iframeRef = useRef < HTMLIFrameElement | null > ( null )
159169 const abortRef = useRef ( false )
160170 const autoRefreshTimerRef = useRef < ReturnType < typeof setTimeout > | null > ( null )
161171 const iterationRef = useRef ( 0 )
172+ const domStateOptionsRef = useRef ( domStateOptions )
162173
163174 // Create iframe on mount
164175 useEffect ( ( ) => {
@@ -180,6 +191,11 @@ export function useAgentLoop(options: UseAgentLoopOptions = {}): {
180191 }
181192 } , [ ] )
182193
194+ // Update DOM-state options ref when they change
195+ useEffect ( ( ) => {
196+ domStateOptionsRef . current = domStateOptions
197+ } , [ domStateOptions ] )
198+
183199 const start = useCallback (
184200 ( goal : string ) => {
185201 abortRef . current = false
@@ -247,14 +263,47 @@ export function useAgentLoop(options: UseAgentLoopOptions = {}): {
247263 } ) )
248264 }
249265
266+ // Capture DOM-state for context injection (Tier 2)
267+ const iframe = iframeRef . current
268+ if ( iframe && iframe . contentDocument && onDomState ) {
269+ const domJson = serializeIframeDomState ( iframe , domStateOptionsRef . current )
270+ const lines : string [ ] = [ ]
271+ lines . push ( '// DOM State Context' )
272+ lines . push ( `// ${ domJson . nodeCount } nodes, depth ${ domJson . budget . depthReached } /${ domJson . budget . maxDepth } ` )
273+ if ( domJson . budget . nodesTruncated ) lines . push ( '// ⚠️ Truncated — node budget exceeded' )
274+ lines . push ( '' )
275+ for ( const node of domJson . tree ) {
276+ const indent = ' ' . repeat ( node . depth )
277+ const attrs : string [ ] = [ ]
278+ if ( node . id ) attrs . push ( `id="${ node . id } "` )
279+ if ( node . classes ) attrs . push ( `class="${ node . classes } "` )
280+ if ( node . type ) attrs . push ( `type="${ node . type } "` )
281+ if ( node . name ) attrs . push ( `name="${ node . name } "` )
282+ if ( node . value !== undefined ) attrs . push ( `value="${ node . value } "` )
283+ if ( node . checked !== undefined ) attrs . push ( `checked=${ node . checked } ` )
284+ if ( node . disabled !== undefined ) attrs . push ( `disabled=${ node . disabled } ` )
285+ if ( node . selected !== undefined ) attrs . push ( `selected=${ node . selected } ` )
286+ if ( node . focused ) attrs . push ( 'focused' )
287+ if ( node . interactive ) attrs . push ( 'interactive' )
288+ if ( node . ariaRole ) attrs . push ( `role="${ node . ariaRole } "` )
289+ if ( node . ariaLabel ) attrs . push ( `aria-label="${ node . ariaLabel } "` )
290+ const attrStr = attrs . length > 0 ? ` [${ attrs . join ( ', ' ) } ]` : ''
291+ const text = node . text ? ` "${ node . text . slice ( 0 , 80 ) } "` : ''
292+ lines . push ( `${ indent } <${ node . tag } ${ attrStr } >${ text } </${ node . tag } >` )
293+ }
294+ const domStateStr = lines . join ( '\n' )
295+ setCurrentDomState ( domStateStr )
296+ onDomState ( domStateStr , iteration )
297+ }
298+
250299 onIterationComplete ?.( htmlRef . current , iteration )
251300 } catch ( err ) {
252301 setState ( ( prev ) => ( {
253302 ...prev ,
254303 error : err instanceof Error ? err . message : 'Screenshot failed' ,
255304 } ) )
256305 }
257- } , [ iframeId , onScreenshot , onIterationComplete ] )
306+ } , [ iframeId , onScreenshot , onIterationComplete , onDomState ] )
258307
259308 const setHtml = useCallback (
260309 ( html : string | null ) => {
@@ -323,6 +372,7 @@ export function useAgentLoop(options: UseAgentLoopOptions = {}): {
323372 state,
324373 actions : { start, stop, refresh, setHtml, clear } ,
325374 iframeEl : iframeRef . current ,
375+ currentDomState,
326376 screenshotRef,
327377 }
328378}
0 commit comments