char-slop/berry
multipurpose self-modifiable agentic harness
git clone https://git.t4t.associates/char-slop/berry
10b0cd2
main
1/** A sidebar link's inline name editor; the input sits beside the link, not inside it. */ 2export function conversationName ( 3link :HTMLAnchorElement , 4label :HTMLElement , 5save :( title :string | null ) => Promise < unknown >, 6) { 7const editor = document . createElement ( "input" ); 8editor . className = "conversation-name" ; 9editor . autocomplete = "off" ; 10editor . setAttribute ( "aria-label" , "Conversation name" ); 11editor . hidden = true ; 12let title :string | undefined ; 13let fallback = "" ; 14let initial = "" ; 15let editing = false ; 16let saving = false ; 17let disposed = false ; 18const finish = ( focus :boolean ) => { 19editing = false ; 20editor . hidden = true ; 21link . hidden = false ; 22if ( focus && ! disposed ) link . focus (); 23}; 24const start = ( event ?:Event ) => { 25event ?. preventDefault (); 26if ( editing || disposed ) return ; 27editing = true ; 28initial = title ?? fallback ; 29editor . value = initial ; 30editor . removeAttribute ( "aria-invalid" ); 31editor . title = "save: Enter, cancel: Escape" ; 32link . hidden = true ; 33editor . hidden = false ; 34editor . focus (); 35editor . select (); 36}; 37const submit = async ( focus :boolean ) => { 38if ( ! editing || saving || disposed ) return ; 39const next = editor . value . trim () || null ; 40if ( editor . value === initial || next === ( title ?? null )) return finish ( focus ); 41const before = title ; 42saving = true ; 43editor . readOnly = true ; 44try { 45await save ( next ); 46if ( disposed ) return ; 47if ( title === before ) title = next ?? undefined ; 48label . textContent = title ?? fallback ; 49finish ( focus ); 50} catch ( error ) { 51if ( disposed ) return ; 52editor . setAttribute ( "aria-invalid" , "true" ); 53editor . title = error instanceof Error ?error . message :String ( error ); 54if ( focus ) editor . focus (); 55} finally { 56saving = false ; 57editor . readOnly = false ; 58} 59}; 60let touch = false ; 61link . addEventListener ( "pointerdown" , ( event ) => { 62touch = event . pointerType === "touch" ; 63}); 64link . addEventListener ( "dblclick" , ( event ) => { 65if ( ! touch && ! matchMedia ( "(pointer: coarse), (hover: none)" ). matches ) start ( event ); 66}); 67link . addEventListener ( "keydown" , ( event ) => { 68if ( event . key === "F2" ) start ( event ); 69}); 70editor . addEventListener ( "keydown" , ( event ) => { 71if ( event . key !== "Enter" && event . key !== "Escape" ) return ; 72if ( event . isComposing ) return ; 73event . preventDefault (); 74if ( saving ) return ; 75if ( event . key === "Escape" ) finish ( true ); 76else void submit ( true ); 77}); 78editor . addEventListener ( "blur" , () => void submit ( false )); 79return { 80 editor, 81 start, 82update ( next :string | undefined , preview :string ) { 83title = next ; 84fallback = preview ; 85label . textContent = title ?? fallback ; 86}, 87dispose () { 88disposed = true ; 89}, 90}; 91}