Files
store/github-utilities/fetch.sune

1 line
19 KiB
JSON

[{"id":"zp2je1g","name":"Github Fetch","pinned":false,"avatar":"data:image/webp;base64,UklGRooHAABXRUJQVlA4WAoAAAAgAAAAfwAAfwAASUNDUMgBAAAAAAHIAAAAAAQwAABtbnRyUkdCIFhZWiAH4AABAAEAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAACRyWFlaAAABFAAAABRnWFlaAAABKAAAABRiWFlaAAABPAAAABR3dHB0AAABUAAAABRyVFJDAAABZAAAAChnVFJDAAABZAAAAChiVFJDAAABZAAAAChjcHJ0AAABjAAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAAgAAAAcAHMAUgBHAEJYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9YWVogAAAAAAAA9tYAAQAAAADTLXBhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAACAAAAAcAEcAbwBvAGcAbABlACAASQBuAGMALgAgADIAMAAxADZWUDggnAUAADAfAJ0BKoAAgAA+bTaWSKQioiEilmqQgA2JaQDUWEEf6T2k/2DoM/J/rjnHPrF998b+8Haf/z28EgA/I/5f+qviISkP5l/t/Jy8Abw32A/4//Rv+z/iv7J8IP7N/zf8t+SHsm/Jf6p/0f8j8An8c/nP/E/vH+R9531m/tv7B36w/9YhW0wbdUMVAp86gdlU+cb71sDJl67tYOiLTSMimadBQ+aLv5eVhjY/ZjU+DqGaK/9MDthXkm7ZkhaqAg7h5oGKwibYvlEui2hxDGk7FWbw2bCKjg6czZvjt4atJzKb9JmTHTDp5U8Bv05jbiwiPQMi3GGOZtLEXKiCz+dAxEciAAD++kWYZ3E4b1CDua/mTwUqn0OxIcdrKBzJq4jVJowAOO4njfJ8XLdQxj42fhX/3lRqjzfmjdsK7iuy3coypVF8R7FMFJhTs54Y3vHQBYzG9gZTHCm4GNSLsXJ/h2+0+N0PJT1CljHw/whD8hof8/yvv3IMmXWsaI+MfbziKT1Y1XOJCSYIz0vHuY0odaquzoG+xujTI/P3T2g3NW3/b84aTkNzVazMsQM9cCSkv+2tmsqAqcuBCWY7HvTIGMJ+Aj9eyFvGcf9ReHQr5wFKqmzDuE3FHftrDUuyGMRyd3VpqzKqHJSH3XjaYXGUJ1HC5ZoZrH+09SQpAnVWhb/+MXyTn2ITQhCSLmGDCFYM3UZ4cSOJ3/zT0C/PSXA/yS3deH2C46g1NsTf/uVHNshpvzXBS7bAHGVD+y3ZB4qFqlBWo5brCUCLzej1zzV8vPQxeWYYE+Z+yLf/NOltTWLCy83Mq7x+G/85WjyjNh8dlJpkWveyZDx/ZfmNtYcHDf00EOtgUK47KqlMv8jnG6trSY/msUz6Z8b/GFSe+/OWp5dDhdw4Fw9awHzr6K2n/SyteLce1+hQzF3jdbVd7PO8lhLm8HaVZh/fAjzHjE5uUFF9I22XfsF3QacQfw7RwmseKfCHQ7Pxx/XJkb1egK8OQhmCb+ccVSvDkAzLoetr8t/iTvY+ec/zVaDKs6bgY+mjjDb7fRkLWqPuOQPoQRzPZHxc8THwEaBBLMC3Y7BjEluaz1/qx8f6T24smL21PRWK8u88qeMd1eM7GZ958iGU8nv7MFeG64+Z1+KMH2ErOxc+kIumLUJCCODeCqMOrMX+e55oaNXOXcELfW09O2oUCkO+WwVI08ZKllTSIwY3aDbs/zzAcrZ/i68A9jLH44RlsrhYj/SZTvaNHo7VZ68ZRwu2ZeDt0bFBrtIlqc1v/Fq7d3n1Kb8W+fCVTK64DghL0Lp5yKaqGgW1Sx+vyWAzprUvs+YFmKVCZwu/iPtwJdjfbJ7FSlbAIdi+uABvpIRY2f+dWx4kFSWYRZ/w51Xt3niz1jxaZIrw+tx2RyIrvQOImk/CaGYgqjSiNpLqN6LZD1y3SvvO1Yy9uSaeBRqs7+PwdPQleIzKhohc4Dr1xWl2+l8re3A9dDJv/yLH2pv6QB4lXScnF+wFZcWXEn4kx8EAYmntIUqcDwCYuMgKWTV45A3qLyyseP/HSoMdHVJz+5/JNd6C+O3cFjQDrNwIs4P4yl/6k3/Tv9Z3UqNAQz2KIs0TIXJsx18K4tBpyR3PttyPUTTDQBq7aHNmeBr5SaAkflfaJRNdnGAOt6wS2KludQkYL1FSFTGZzm/FobOPqe22PUD8wdUqyf45uPzbbLU46f40Px/+HFKu0tqnpqO+DG464z6Rk9wrbasl7NpsC9HKucWrwyCACsR6S13XqeqraZNPoDq8sBKQkMgb/8pqOWPvkMLDkW8beuhtNEmSfWGn9f/Zdn57z6QkvFfBD9zzdBEb7oPhDOmC49nPqjSJZ9L6fS9MEHxMb6xFoWpMgdqYbk+Pn4a3yB5M3CCfJFpvdsTdSg4o9EAA","url":"gh://sune-org/store/github-utilities/fetch.sune","updatedAt":1769555811986,"settings":{"model":"","temperature":"","top_p":"","top_k":"","frequency_penalty":"","repetition_penalty":"","min_p":"","top_a":"","verbosity":"","reasoning_effort":"default","system_prompt":"","html":"<div id=\"ghFetchSune\" class=\"p-1.5 bg-slate-100 rounded-lg shadow-sm font-sans\" x-data>\n<div class=\"flex items-center justify-between\">\n<div class=\"flex items-center gap-1.5\">\n<i data-lucide=\"github\" class=\"h-4 w-4 text-slate-600\"></i>\n<span class=\"text-xs font-medium text-slate-800\">GitHub Fetch</span>\n<span class=\"text-[10px] text-slate-400\">v2.0.1</span>\n</div>\n<div class=\"flex items-center gap-1\">\n<button id=\"ghFetchAllBtn\" type=\"button\" class=\"rounded-md px-2 py-1 text-xs font-semibold bg-slate-800 text-white hover:bg-slate-700 active:scale-95 transition-all\">Fetch All</button>\n<button type=\"button\" id=\"ghAddRowBtn\" class=\"h-7 w-7 rounded-md bg-slate-200/80 text-slate-600 hover:bg-white active:shadow-inner active:scale-95 flex items-center justify-center transition-all\" title=\"Add URL\"><i data-lucide=\"plus\" class=\"h-4 w-4\"></i></button>\n</div>\n</div>\n<div id=\"ghRows\" class=\"mt-2 space-y-1.5\"></div>\n<div id=\"ghStatus\" class=\"mt-1.5 px-1 text-[11px] text-slate-600 min-h-[1em]\"></div>\n</div>\n\n<script>\n(()=>{\nconst root=document.getElementById('ghFetchSune');\nif(!root)return;\nconst $=s=>root.querySelector(s);\nconst els={rows:$('#ghRows'),add:$('#ghAddRowBtn'),all:$('#ghFetchAllBtn'),status:$('#ghStatus')};\nconst cache={};\nconst icons=()=>{try{lucide.createIcons({attrs:{'aria-hidden':!0}})}catch{}};\nconst getKey=()=>'gh_fetch_sune_shared_urls_v1';\nconst getIgnoreKey=()=>'gh_fetch_sune_shared_ignores_v1';\nconst load=()=>{try{const d=JSON.parse(localStorage.getItem(getKey()));return Array.isArray(d)?d.map(x=>String(x||'')):[''];}catch{return[''];}};\nconst loadIgnores=()=>{try{const d=JSON.parse(localStorage.getItem(getIgnoreKey()));return Array.isArray(d)?d.map(x=>String(x||'')):[];}catch{return[];}};\nconst save=d=>localStorage.setItem(getKey(),JSON.stringify(d.map(u=>(u||'').trim())));\nconst saveIgnores=ig=>localStorage.setItem(getIgnoreKey(),JSON.stringify(ig.map(v=>String(v||''))));\nconst getToken=()=>window.USER?.githubToken||window.USER?.PAT||'';\n\nconst setStatus=(el,msg,kind)=>{\nconst k={error:'text-red-600',ok:'text-emerald-600',info:el.id==='ghStatus'?'text-slate-600':'text-slate-500'};\nel.textContent=msg||'';\nel.className=`${el.dataset.baseClass||''} ${k[kind]||k.info}`;\n};\nconst setGlobalStatus=(m,k='info')=>setStatus(els.status,m,k);\nconst setRowStatus=(i,m,k='info')=>{const n=$(`[data-role=\"row-status\"][data-index=\"${i}\"]`);if(n)setStatus(n,m,k);};\nconst spinner=c=>`<svg class=\"animate-spin ${c}\" viewBox=\"0 0 24 24\"><circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" fill=\"none\"></circle><path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v4A4 4 0 004 12z\"></path></svg>`;\n\nconst ghFetch=async(url,opts={})=>{\nconst h={...opts.headers};\nconst tk=getToken();\nif(tk)h.Authorization=`token ${tk}`;\nconst res=await fetch(url,{...opts,headers:h});\nif(res.status===401)throw new Error('Auth failed. Check GitHub token in Account Settings.');\nif(res.status===403){const rt=res.headers.get('X-RateLimit-Remaining');if(rt==='0')throw new Error('Rate limit exceeded. Add GitHub token for higher limits.');}\nif(res.status===404)throw new Error('Not found. Check URL or add token for private repos.');\nif(!res.ok)throw new Error(`HTTP ${res.status}`);\nreturn res;\n};\n\nconst fetchRawWithToken=async(owner,repo,ref,path)=>{\nconst tk=getToken();\nif(tk){\nconst apiUrl=`https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=${ref}`;\nconst res=await ghFetch(apiUrl,{headers:{Accept:'application/vnd.github.v3.raw'}});\nreturn await res.text();\n}\nconst rawUrl=`https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${path}`;\nreturn await(await ghFetch(rawUrl)).text();\n};\n\nconst getInfo=async(o,r)=>{\nconst k=`${o}/${r}`;\nif(cache[k])return cache[k];\nconst d=await(await ghFetch(`https://api.github.com/repos/${o}/${r}`)).json();\nif(!d.default_branch)throw new Error('No default branch.');\nreturn cache[k]=d;\n};\n\nasync function parse(u){\ntry{\nconst lM=u.match(/#L(\\d+)(?:-L(\\d+))?$/i);\nconst lines=lM?{start:+lM[1],end:+lM[2]||+lM[1]}:null;\nconst urlStr=u.split('#')[0];\ntry{\nconst url=new URL(urlStr);\nlet owner,repo,ref,path;\nif(url.hostname==='github.com'){\nconst p=url.pathname.split('/').filter(Boolean);\nif(p.length>=4&&(p[2]==='blob'||p[2]==='raw')){\n[owner,repo,,ref]=p;path=p.slice(4).join('/');\nreturn{type:'file',p:{owner,repo,ref,path,lines,raw:`https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${path}`}};\n}\n}else if(url.hostname==='raw.githubusercontent.com'){\nconst p=url.pathname.split('/').filter(Boolean);\nif(p.length>=4){\n[owner,repo,ref]=p;path=p.slice(3).join('/');\nreturn{type:'file',p:{owner,repo,ref,path,lines,raw:url.href}};\n}\n}\n}catch{}\nconst p=urlStr.split('/').filter(Boolean);\nif(p.length>=2){\nconst[owner,repo]=p;\nif(p.length>2){\nconst path=p.slice(2).join('/');\nconst{default_branch:ref}=await getInfo(owner,repo);\nreturn{type:'file',p:{owner,repo,ref,path,lines,raw:`https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${path}`}};\n}\nconst info=await getInfo(owner,repo);\nreturn{type:'repo',p:{owner,repo,default_branch:info.default_branch}};\n}\n}catch{return null;}\n}\n\nconst isBin=p=>/\\.(png|jpe?g|gif|bmp|webp|ico|tif?f|svg|zip|rar|7z|tar|gz|bz2|xz|pdf|docx?|xlsx?|pptx?|mp[34]|wav|ogg|webm|mov|avi|mkv|woff2?|ttf|otf|eot|exe|dll|so|dylib|bin|o|a|class|jar|pyc|iso|img|dmg)$/i.test(p);\nconst getLang=p=>(p.split('.').pop()||'').toLowerCase();\nconst getFence=s=>'`'.repeat(Math.max(3,1+Math.max(0,...(s.match(/`+/g)||[]).map(x=>x.length))));\n\nconst fetchFile=async({raw,path,lines,owner,repo,ref})=>{\nconst h=`[${owner}/${repo}@${ref}/${path}](https://github.com/${owner}/${repo}/blob/${ref}/${path})`;\nif(isBin(path))return{md:`${h}\\n\\n*Binary file - content not displayed.*`};\nlet content=await fetchRawWithToken(owner,repo,ref,path);\nlet fileContentMd;\nif(path.toLowerCase().endsWith('.sune')){\ntry{const d=JSON.parse(content),html=d?.[0]?.settings?.html;\nif(typeof html==='string'){const fence=getFence(html);fileContentMd=`${fence}html\\n${html}\\n${fence}`;}}catch{}\n}\nif(!fileContentMd){\nif(lines)content=content.replace(/\\r\\n/g,'\\n').split('\\n').slice(lines.start-1,lines.end).join('\\n');\nconst fence=getFence(content);fileContentMd=`${fence}${getLang(path)}\\n${content}\\n${fence}`;\n}\nreturn{md:`${h}\\n\\n${fileContentMd}`};\n};\n\nconst matchIgnore=(path,patterns)=>{\nif(!patterns?.length)return!1;\nlet ignore=!1;\nfor(const raw of patterns){\nconst s=(raw||'').trim();if(!s||s.startsWith('#'))continue;\nconst negative=s[0]==='!',pat=negative?s.slice(1):s;if(!pat)continue;\nlet q=pat.replace(/[.+^${}()|[\\]\\\\]/g,'\\\\$&');\nq=q.replace(/\\\\\\*\\\\\\*/g,'.*').replace(/\\\\\\*/g,'[^/]*');\nlet reg;\nif(pat.startsWith('/')){const core=q.slice(2);reg=pat.endsWith('/')?`^${core}.*`:`^${core}(/.*)?$`;}\nelse{reg=pat.endsWith('/')?`(^|/)${q}.*`:`(^|/)${q}(/.*)?$`;}\ntry{if(new RegExp(reg).test(path))ignore=!negative;}catch{}\n}\nreturn ignore;\n};\n\nconst runPool=async(jobs,limit,onProgress)=>{\nconst out=new Array(jobs.length);let next=0,active=0;\nreturn new Promise(resolve=>{\nconst kick=()=>{\nif(next>=jobs.length&&!active){resolve(out);return;}\nwhile(active<limit&&next<jobs.length){\nconst i=next++;active++;\njobs[i]().then(r=>{out[i]=r;}).catch(e=>{out[i]={error:e};}).finally(()=>{active--;onProgress?.(i,out[i]);kick();});\n}\n};kick();\n});\n};\n\nconst post=async md=>{\nif(window.USER?.logMany&&Array.isArray(md))return window.USER.logMany(md);\nif(window.USER?.log){if(Array.isArray(md)){for(const m of md)await window.USER.log(m);return;}return window.USER.log(md);}\nreturn new Error('Chat injection failed.');\n};\n\nconst fetchRepo=async({owner,repo,default_branch},i,ignoreStr)=>{\nsetRowStatus(i,'Fetching tree...');\nconst{commit:{sha}}=await(await ghFetch(`https://api.github.com/repos/${owner}/${repo}/branches/${default_branch}`)).json();\nconst{tree,truncated}=await(await ghFetch(`https://api.github.com/repos/${owner}/${repo}/git/trees/${sha}?recursive=1`)).json();\nif(truncated)throw new Error('Repo too large to fetch completely.');\nconst allFiles=tree.filter(n=>n.type==='blob');\nif(!allFiles.length)throw new Error('No files found in repo.');\nconst ignorePatterns=(ignoreStr||'').split('\\n').map(s=>s.trim()).filter(Boolean);\nconst targets=allFiles.filter(f=>!matchIgnore(f.path,ignorePatterns));\nif(!targets.length)throw new Error('All files ignored by patterns.');\nconst total=targets.length;\nlet completed=0,posted=0;\nconst skipped=allFiles.length-total;\n\nconst jobs=targets.map(({path})=>async()=>{\nconst h=`[${owner}/${repo}@${default_branch}/${path}](https://github.com/${owner}/${repo}/blob/${default_branch}/${path})`;\nif(isBin(path))return{ok:!0,md:`${h}\\n\\n*Binary file - content not displayed.*`,path};\ntry{\nlet content=await fetchRawWithToken(owner,repo,sha,path);\nlet fileContentMd;\nif(path.toLowerCase().endsWith('.sune')){\ntry{const d=JSON.parse(content),html=d?.[0]?.settings?.html;\nif(typeof html==='string'){const fence=getFence(html);fileContentMd=`${fence}html\\n${html}\\n${fence}`;}}catch{}\n}\nif(!fileContentMd){const fence=getFence(content);fileContentMd=`${fence}${getLang(path)}\\n${content}\\n${fence}`;}\nreturn{ok:!0,md:`${h}\\n\\n${fileContentMd}`,path};\n}catch(error){return{ok:!1,md:`${h}\\n\\n*Error posting file: ${error.message||error}*`,path};}\n});\n\nconst results=await runPool(jobs,6,(index)=>{\ncompleted++;\nif(completed<=total)setRowStatus(i,`Fetching ${completed}/${total}: ${targets[index]?.path||''}`,'info');\n});\n\nconst okMessages=[],errorMessages=[];\nfor(const r of results){\nif(!r||!r.md)continue;\nif(r.ok){okMessages.push(r.md);posted++;}\nelse errorMessages.push(r.md);\n}\n\nif(okMessages.length){setRowStatus(i,'Posting fetched files...','info');await post(okMessages);}\nif(errorMessages.length)await post(errorMessages);\nreturn{s:posted,t:total,skipped};\n};\n\nconst urls=load();\nconst ignores=loadIgnores();\nconst esc=s=>String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/\"/g,'&quot;');\n\nconst rowTpl=(i,v='')=>{\nconst ig=(ignores[i]||'').trim();\nconst preview=ig?'Ignore: '+ig.replace(/\\n/g,'; '):'';\nreturn`<div><div class=\"flex items-stretch gap-1\"><input data-role=\"url\" data-index=\"${i}\" type=\"text\" placeholder=\"user/repo or user/repo/file or full URL...\" class=\"flex-1 min-w-0 rounded-md border-0 bg-white shadow-inner px-2.5 py-1.5 text-xs text-slate-800 placeholder:text-slate-400 focus:ring-2 focus:ring-slate-400 transition\" value=\"${esc(v)}\"><button data-role=\"copy\" data-index=\"${i}\" type=\"button\" class=\"shrink-0 rounded-md w-8 bg-slate-200/80 text-slate-600 hover:bg-white active:shadow-inner active:scale-95 flex items-center justify-center transition-all\" title=\"Copy URL\"><i data-lucide=\"copy\" class=\"h-4 w-4\"></i></button><button data-role=\"ignore\" data-index=\"${i}\" type=\"button\" class=\"shrink-0 rounded-md w-8 bg-slate-200/80 text-slate-600 hover:bg-white active:shadow-inner active:scale-95 flex items-center justify-center transition-all\" title=\"Edit ignore patterns\"><i data-lucide=\"filter\" class=\"h-4 w-4\"></i></button><button data-role=\"fetch-one\" data-index=\"${i}\" type=\"button\" class=\"shrink-0 rounded-md px-2.5 text-xs font-semibold bg-slate-800 text-white hover:bg-slate-700 active:scale-95 transition-all\">Fetch</button><button data-role=\"remove\" data-index=\"${i}\" type=\"button\" class=\"shrink-0 rounded-md w-8 text-slate-500 bg-slate-200/80 hover:bg-red-100 hover:text-red-600 active:scale-95 flex items-center justify-center transition-all\" title=\"Remove\"><i data-lucide=\"trash-2\" class=\"h-4 w-4\"></i></button></div><div data-role=\"ignore-preview\" data-index=\"${i}\" class=\"mt-0.5 px-1 text-[10px] text-slate-400 min-h-[0.75em] truncate\">${esc(preview)}</div><div data-role=\"row-status\" data-index=\"${i}\" data-base-class=\"mt-0.5 px-1 text-[11px] min-h-[1em]\" class=\"mt-0.5 px-1 text-[11px] text-slate-500 min-h-[1em]\"></div></div>`;\n};\n\nconst render=()=>{els.rows.innerHTML=(urls.length?urls:['']).map((u,i)=>rowTpl(i,u)).join('');icons();};\n\nasync function fetchRow(i){\nconst inp=$(`[data-role=\"url\"][data-index=\"${i}\"]`),btn=$(`[data-role=\"fetch-one\"][data-index=\"${i}\"]`);\nif(!inp||!btn)return!1;\nsetRowStatus(i,'');\nconst url=(inp.value||'').trim();\nif(!url){setRowStatus(i,'Please enter a path or URL.','error');return!1;}\nconst prev=btn.innerHTML;btn.innerHTML=spinner('h-4 w-4');btn.disabled=!0;inp.disabled=!0;\ntry{\nsetRowStatus(i,'Parsing...','info');\nconst parsed=await parse(url);\nif(!parsed)throw new Error('Invalid URL or repo not found.');\nif(parsed.type==='file'){\nconst result=await fetchFile(parsed.p);\nawait post(result.md);\nsetRowStatus(i,'Added to chat.','ok');\n}else{\nconst{s,t,skipped}=await fetchRepo(parsed.p,i,ignores[i]||'');\nconst ignoredInfo=skipped?`, ignored ${skipped}`:'';\nsetRowStatus(i,s===t&&t>0&&!skipped?`Added all ${t} files.`:`Added ${s}/${t} files${ignoredInfo}.`,s===t&&t>0&&!skipped?'ok':'info');\n}\nreturn!0;\n}catch(err){setRowStatus(i,err.message||String(err),'error');return!1;}finally{btn.innerHTML=prev;btn.disabled=!1;inp.disabled=!1;}\n}\n\nels.rows.addEventListener('click',async e=>{\nconst b=e.target.closest('button[data-role]');\nif(!b)return;\nconst i=+b.dataset.index;\nconst actions={\ncopy:async()=>{\nconst n=$(`input[data-index=\"${i}\"]`);\nif(!n?.value){setRowStatus(i,'Nothing to copy.');return;}\ntry{await navigator.clipboard.writeText(n.value);setRowStatus(i,'Copied!','ok');}catch(e){setRowStatus(i,'Copy failed.','error');}\n},\nignore:()=>{\nconst cur=(ignores[i]||'').replace(/\\n/g,', ');\nconst hint='Patterns like .gitignore.\\nExamples:\\nnode_modules/\\n**/secret.txt\\n*.log';\nconst next=prompt(hint,cur);\nif(next===null)return;\nconst normalized=(next||'').split(/[,;]+/).map(s=>s.trim()).filter(Boolean).join('\\n');\nignores[i]=normalized;\nwhile(ignores.length<urls.length)ignores.push('');\nsaveIgnores(ignores);\nconst pv=$(`[data-role=\"ignore-preview\"][data-index=\"${i}\"]`);\nif(pv)pv.textContent=normalized?'Ignore: '+normalized.replace(/\\n/g,'; '):'';\n},\n'fetch-one':()=>fetchRow(i),\nremove:()=>{\nif(urls.length>1){urls.splice(i,1);ignores.splice(i,1);}else{urls[0]='';ignores[0]='';}\nsave(urls);saveIgnores(ignores);render();\n}\n};\nif(actions[b.dataset.role])actions[b.dataset.role]();\n});\n\nels.rows.addEventListener('input',e=>{\nconst n=e.target.closest('input[data-role=\"url\"]');\nif(!n)return;\nurls[+n.dataset.index]=n.value;\nsave(urls);\n});\n\nels.rows.addEventListener('keydown',e=>{\nif(e.key!=='Enter'||!e.target.matches('input[data-role=\"url\"]'))return;\ne.preventDefault();\nfetchRow(+e.target.dataset.index);\n});\n\nels.add.addEventListener('click',()=>{\nurls.push('');ignores.push('');save(urls);saveIgnores(ignores);render();\nconst inp=$(`input[data-index=\"${urls.length-1}\"]`);\nif(inp)inp.focus();\n});\n\nels.all.addEventListener('click',async()=>{\nconst list=urls.map((u,i)=>({u:u.trim(),i})).filter(x=>x.u);\nif(!list.length){setGlobalStatus('No URLs to fetch. Add one with +.','error');return;}\nconst prev=els.all.innerHTML;els.all.innerHTML=spinner('h-4 w-4');els.all.disabled=!0;\nsetGlobalStatus(`Fetching ${list.length} item(s)...`,'info');\nlet s=0,f=0;\nfor(const{i}of list)(await fetchRow(i))?s++:f++;\nels.all.innerHTML=prev;els.all.disabled=!1;\nsetGlobalStatus(!f?`Fetched ${s} item(s) successfully.`:`Completed: ${s} succeeded, ${f} failed.`,!f?'ok':'error');\n});\n\nels.status.dataset.baseClass='mt-1.5 px-1 text-[11px] min-h-[1em]';\nrender();\n})();\n</script>\n","extension_html":"<sune src='https://raw.githubusercontent.com/multipleof4/.sune/refs/heads/main/inline-commit.sune' private></sune>\n<sune src='https://raw.githubusercontent.com/sune-org/store/refs/heads/main/sync.sune' private></sune>\n","hide_composer":false,"include_thoughts":false,"json_output":false,"img_output":false,"aspect_ratio":"1:1","image_size":"1K","ignore_master_prompt":false,"json_schema":"","presence_penalty":0,"max_tokens":0},"storage":{}}]