char-slop/berry
multipurpose self-modifiable agentic harness
git clone https://git.t4t.associates/char-slop/berry
10b0cd2
main
1import type { ToolView , View , ViewContext } from "@berry/api/web.ts" ; 2import * as j from "@char/justin" ; 3 4const snapshot = j . obj ({ 5tab :j . string , 6url :j . string , 7title :j . string , 8text :j . string , 9elements :j . array ( j . obj ({ 10ref :j . string , 11role :j . string , 12name :j . string , 13value :j . optional ( j . string ), 14disabled :j . optional ( j . boolean ), 15checked :j . optional ( j . boolean ), 16options :j . optional ( j . array ( j . obj ({ label :j . string , value :j . string , selected :j . boolean }))), 17})), 18truncated :j . boolean , 19frames :j . array ( j . obj ({ url :j . string , title :j . string })), 20diagnostics :j . array ( j . string ), 21}); 22const resultOf = j . validation . compile ( j . obj ({ 23snapshot :j . optional ( j . union ( snapshot , j . null )), 24tabs :j . optional ( j . array ( j . obj ({ tab :j . string , url :j . string , title :j . string }))), 25})); 26const dataOf = j . validation . compile ( j . obj ({ 27active :j . boolean , 28tabs :j . array ( snapshot ), 29history :j . array ( j . obj ({ action :j . string , tab :j . string , url :j . string , at :j . number })), 30screenshots :j . array ( 31j . obj ({ tab :j . string , image :j . obj ({ type :j . string , data :j . string , mimeType :j . string }) }), 32), 33})); 34const argsOf = j . validation . compile ( 35j . obj ({ action :j . string , tab :j . optional ( j . string ), url :j . optional ( j . string ) }), 36); 37 38export function summary ( args :unknown ) :string | null { 39const value = argsOf ( args ). value ; 40return value ?[ value . action , value . tab , value . url ]. filter ( Boolean ). join ( " · " ) :null ; 41} 42 43function node < K extends keyof HTMLElementTagNameMap > ( 44tag :K , 45text ?:string , 46) :HTMLElementTagNameMap [ K ] { 47const element = document . createElement ( tag ); 48if ( text !== undefined ) element . textContent = text ; 49return element ; 50} 51 52function renderSnapshot ( value :j . Infer < typeof snapshot >) :HTMLElement { 53const element = node ( "section" ); 54const link = node ( "a" , value . url ); 55if ( / ^https?:\/\/ / . test ( value . url )) { 56link . href = value . url ; 57link . target = "_blank" ; 58link . rel = "noopener noreferrer" ; 59} 60const list = node ( "ul" ); 61for ( const item of value . elements ) { 62list . append ( 63node ( 64"li" , 65` ${ item . ref } · ${ item . role } ${ JSON . stringify ( item . name )} ${ 66item . value !== undefined ? ` = ${ JSON . stringify ( item . value )} ` : "" 67} ${ item . disabled ? " (disabled)" : "" } ${ 68item . checked !== undefined ? ` ( ${ item . checked ? "checked" : "unchecked" } )` : "" 69} ${ 70item . options 71? ` — ${ 72item . options . map (( option ) => 73` ${ option . label } : ${ option . value } ${ option . selected ? " ✓" : "" } ` 74). join ( ", " ) 75} ` 76: "" 77} ` , 78), 79); 80} 81element . append ( 82node ( "h3" , ` ${ value . tab } · ${ value . title || "Untitled" } ` ), 83link , 84node ( "h4" , "Page text" ), 85node ( "pre" , value . text ), 86node ( "h4" , "Elements" ), 87list , 88); 89if ( value . truncated ) element . append ( node ( "p" , "Snapshot truncated." )); 90if ( value . frames . length ) { 91element . append ( node ( 92"p" , 93`Embedded frames (not inspected): ${ 94value . frames . map (( frame ) => frame . title || frame . url ). join ( ", " ) 95} ` , 96)); 97} 98if ( value . diagnostics . length ) { 99element . append ( node ( "h4" , "Console & network" ), node ( "pre" , value . diagnostics . join ( "\n" ))); 100} 101return element ; 102} 103 104export function browserToolView () :ToolView { 105const element = node ( "div" ); 106element . className = "tool-body browser-pane" ; 107return { 108 element, 109update ( call ) { 110const result = resultOf ( call . result ). value ; 111element . replaceChildren ( 112 ...( result ?. snapshot ?[ renderSnapshot ( result . snapshot )] :[ node ( 113"pre" , 114call . error || call . output || 115( result 116 ?result . tabs ?. map (( tab ) => ` ${ tab . tab } · ${ tab . title || tab . url } ` ). join ( "\n" ) || 117"No open tabs." 118 :"" ), 119)]), 120); 121}, 122}; 123} 124 125export function browserView () :View { 126const element = node ( "div" ); 127element . className = "browser-pane" ; 128const toolbar = node ( "div" ); 129const refresh = node ( "button" , "Refresh" ); 130refresh . type = "button" ; 131const close = node ( "button" , "Close browser" ); 132close . type = "button" ; 133close . disabled = true ; 134const status = node ( "p" ); 135status . setAttribute ( "role" , "status" ); 136const tabs = node ( "div" ); 137tabs . setAttribute ( "role" , "group" ); 138tabs . setAttribute ( "aria-label" , "Browser tabs" ); 139const content = node ( "div" ); 140const history = node ( "ol" ); 141toolbar . append ( refresh , close , status ); 142element . append ( toolbar , tabs , content , node ( "h3" , "Recent actions" ), history ); 143let context :ViewContext | undefined ; 144let selected :string | undefined ; 145let controller :AbortController | undefined ; 146let previous :string | undefined ; 147let disposed = false ; 148const load = async () => { 149if ( ! context || disposed ) return ; 150controller ?. abort (); 151const request = controller = new AbortController (); 152try { 153const data = dataOf ( await context . request ( "view" , {}, { signal :request . signal })). value ; 154if ( request . signal . aborted || disposed ) return ; 155if ( ! data ) throw new Error ( "Invalid browser view response." ); 156close . disabled = ! data . active ; 157status . textContent = data . active 158 ?"[TODO: active browser session help]" 159 :"[TODO: historical browser snapshots help]" ; 160if ( ! data . tabs . some (( tab ) => tab . tab === selected )) selected = data . tabs . at ( - 1 )?. tab ; 161const draw = () => { 162const value = data . tabs . find (( tab ) => tab . tab === selected ); 163const screenshot = data . screenshots . find (( entry ) => entry . tab === selected )?. image ; 164const children :Node [] = []; 165if ( screenshot ?. mimeType === "image/png" ) { 166const image = node ( "img" ); 167image . src = `data:image/png;base64, ${ screenshot . data } ` ; 168image . alt = `Latest screenshot of ${ value ?. title || selected } ` ; 169children . push ( image ); 170} 171if ( value ) children . push ( renderSnapshot ( value )); 172else children . push ( node ( "p" , "open a page to get started" )); 173content . replaceChildren ( ...children ); 174tabs . replaceChildren ( ...data . tabs . map (( tab ) => { 175const button = node ( "button" , ` ${ tab . tab } · ${ tab . title || tab . url } ` ); 176button . type = "button" ; 177button . setAttribute ( "aria-pressed" , String ( tab . tab === selected )); 178button . addEventListener ( "click" , () => { 179selected = tab . tab ; 180draw (); 181}); 182return button ; 183})); 184}; 185draw (); 186history . replaceChildren ( 187 ...data . history . toReversed (). map (( item ) => 188node ( "li" , ` ${ item . tab } ${ item . action } ${ item . url ? ` · ${ item . url } ` : "" } ` ) 189), 190); 191} catch ( error ) { 192if ( ! request . signal . aborted && ! disposed ) status . textContent = String ( error ); 193} 194}; 195refresh . addEventListener ( "click" , () => void load ()); 196close . addEventListener ( "click" , async () => { 197if ( ! context ) return ; 198close . disabled = true ; 199try { 200await context . request ( "close" , {}); 201} catch ( error ) { 202if ( ! disposed ) status . textContent = String ( error ); 203close . disabled = false ; 204return ; 205} 206await load (); 207}); 208return { 209 element, 210update ( next ) { 211if ( context ?. tid !== next . tid ) selected = undefined ; 212context = next ; 213const key = ` ${ next . tid } : ${ next . latest } ` ; 214if ( key !== previous ) { 215previous = key ; 216void load (); 217} 218}, 219dispose () { 220disposed = true ; 221controller ?. abort (); 222}, 223}; 224}