char-slop/berry
multipurpose self-modifiable agentic harness
git clone https://git.t4t.associates/char-slop/berry
40b9c5a
main
1import type { Snapshot } from "./types.ts" ; 2 3/** A DOM-based observation, serialized to run in the page's isolated realm. */ 4export async function observe ( 5key :string , 6prefix :string , 7) :Promise < Omit < Snapshot , "tab" | "diagnostics" > & { documentId :string }> { 8await new Promise < void >(( resolve ) => { 9let idle :ReturnType < typeof setTimeout >; 10const finish = () => { 11clearTimeout ( idle ); 12clearTimeout ( deadline ); 13observer . disconnect (); 14resolve (); 15}; 16const observer = new MutationObserver (() => { 17clearTimeout ( idle ); 18idle = setTimeout ( finish , 150 ); 19}); 20const deadline = setTimeout ( finish , 1_000 ); 21observer . observe ( document , { 22subtree :true , 23childList :true , 24attributes :true , 25characterData :true , 26}); 27idle = setTimeout ( finish , 150 ); 28}); 29const refs = new Map < string , Element >(); 30Object . defineProperty ( globalThis , key , { value :refs , configurable :true }); 31const globals = globalThis as unknown as Record < string , string >; 32const documentKey = key + "_document" ; 33if ( ! globals [ documentKey ]) Object . defineProperty ( globalThis , documentKey , { value :prefix }); 34const clean = ( text :string ) => text . replace ( / \s+ / g , " " ). trim (). slice ( 0 , 500 ); 35const visible = ( element :Element ) => { 36const style = getComputedStyle ( element ); 37return ! element . closest ( '[hidden], [inert], [aria-hidden="true"]' ) && 38style . visibility !== "hidden" && style . display !== "none" && 39element . getClientRects (). length > 0 ; 40}; 41const name = ( element :Element ) => { 42const labelled = element . getAttribute ( "aria-labelledby" )?. split ( / \s+ / ). map (( id ) => 43document . getElementById ( id )?. textContent ?? "" 44). join ( " " ); 45const labels = "labels" in element 46 ?[ ...(( element as HTMLInputElement ). labels ?? [])]. map (( label ) => { 47const copy = label . cloneNode ( true ) as Element ; 48copy . querySelectorAll ( "input, textarea, select, button" ). forEach (( control ) => 49control . remove () 50); 51return copy . textContent ; 52}). join ( " " ) 53 :"" ; 54return clean ( 55labelled || element . getAttribute ( "aria-label" ) || labels || 56( element instanceof HTMLInputElement && / ^(submit|button|reset)$ / . test ( element . type ) 57 ?element . value 58 :"" ) || 59( element as HTMLElement ). innerText || element . getAttribute ( "alt" ) || 60element . getAttribute ( "title" ) || element . getAttribute ( "placeholder" ) || "" , 61); 62}; 63const candidates :Element [] = []; 64const visit = ( root :Document | ShadowRoot ) => { 65for ( const element of root . querySelectorAll ( "*" )) { 66if ( 67element . matches ( 68"a[href], button, input:not([type=hidden]), textarea, select, summary, [role], [contenteditable=true], [tabindex]" , 69) && visible ( element ) 70) candidates . push ( element ); 71if ( element . shadowRoot ) visit ( element . shadowRoot ); 72} 73}; 74visit ( document ); 75const elements = candidates . slice ( 0 , 300 ). map (( element , index ) => { 76const ref = ` ${ prefix } e ${ index + 1 } ` ; 77refs . set ( ref , element ); 78const input = element instanceof HTMLInputElement ?element :null ; 79const role = element . getAttribute ( "role" ) || 80( input 81 ?({ 82checkbox :"checkbox" , 83radio :"radio" , 84range :"slider" , 85number :"spinbutton" , 86submit :"button" , 87button :"button" , 88reset :"button" , 89}[ input . type ] ?? "textbox" ) 90 :({ 91A :"link" , 92BUTTON :"button" , 93TEXTAREA :"textbox" , 94SELECT :"combobox" , 95SUMMARY :"button" , 96}[ element . tagName ] ?? 97( element . getAttribute ( "contenteditable" ) === "true" ?"textbox" :"element" ))); 98return { 99 ref, 100 role, 101name :name ( element ), 102 ...( "value" in element && input ?. type !== "password" 103 ?{ value :String ( element . value ). slice ( 0 , 500 ) } 104 :{}), 105 ...( element . matches ( ":disabled, [aria-disabled=true]" ) ?{ disabled :true } :{}), 106 ...( input && / ^(checkbox|radio)$ / . test ( input . type ) ?{ checked :input . checked } :{}), 107 ...( element instanceof HTMLSelectElement 108 ?{ 109options :[ ...element . options ]. slice ( 0 , 100 ). map (( option ) => ({ 110label :option . label , 111value :option . value , 112selected :option . selected , 113})), 114} 115 :{}), 116}; 117}); 118const text = document . body ?. innerText ?? "" ; 119return { 120documentId :globals [ documentKey ], 121url :location . href , 122title :document . title , 123text :text . slice ( 0 , 20_000 ), 124 elements, 125truncated :text . length > 20_000 || candidates . length > 300 , 126frames :[ ...document . querySelectorAll ( "iframe" )]. map (( frame ) => ({ 127url :frame . src , 128title :frame . title , 129})), 130}; 131} 132 133/** Resolve a reference only while its original element is still actionable. */ 134export function resolveElement ( key :string , ref :string ) :Element { 135const refs = ( globalThis as unknown as Record < string , Map < string , Element >>)[ key ]; 136const element = refs ?. get ( ref ); 137if ( ! element ?. isConnected ) throw new Error ( "Stale element reference; take a new snapshot." ); 138if ( element . matches ( ":disabled, [aria-disabled=true]" ) || element . closest ( "[inert]" )) { 139throw new Error ( "Element is disabled." ); 140} 141element . scrollIntoView ({ block :"center" , inline :"center" }); 142const style = getComputedStyle ( element ); 143if ( 144element . closest ( "[hidden], [aria-hidden=true]" ) || style . visibility === "hidden" || 145style . display === "none" || ! element . getClientRects (). length 146) { 147throw new Error ( "Element is no longer visible; take a new snapshot." ); 148} 149return element ; 150}