Files
store/github-utilities/recent.sune

1 line
18 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
[{"id":"znphli6","name":"GitHub Recent","pinned":false,"avatar":"","url":"gh://sune-org/store/github-utilities/recent.sune","updatedAt":1762646018591,"settings":{"model":"google/gemini-2.5-pro","temperature":"","top_p":"","top_k":"","frequency_penalty":"","repetition_penalty":"","min_p":"","top_a":"","verbosity":"","reasoning_effort":"default","system_prompt":"","html":"<!-- Sune: My GitHub Commits (Direct API) · v2.0.0 -->\n<!--\nMaster, heres what was wrong and how this fixes it:\n\nPrevious issues:\n- We relied on /users/:login/events (public events only).\n- That wont see:\n - Private repo commits\n - Some activity that doesnt surface as PushEvent\n- We also tried to use \"since\" with that endpoint (ignored by GitHub for events).\n\nCorrect approach:\n- Use the authenticated search API:\n GET /search/commits?q=author:<you>\n with Accept: application/vnd.github.cloak-preview+json\n- This:\n - Uses your token and sees what your token is allowed to see (including private).\n - Works reliably for “recent commits by me”.\n- This sune:\n - Reads USER.githubToken.\n - Resolves your login from /user.\n - Calls /search/commits?q=author:<login>&sort=author-date&order=desc.\n - Filters client-side by days.\n - Shows a debug log so you can verify queries/results.\n-->\n\n<section\n id=\"sune-github-commits\"\n class=\"w-full max-w-3xl mx-auto my-3 px-3\n bg-white border border-gray-200 rounded-2xl\n shadow-sm sm:shadow-md\n text-xs text-gray-800\"\n x-data=\"githubCommitsSune()\"\n x-init=\"init()\"\n>\n <!-- Header -->\n <header class=\"flex items-center gap-2 py-2 border-b border-gray-100\">\n <div class=\"h-7 w-7 rounded-xl bg-gray-900 text-white flex items-center justify-center text-[11px]\">\n GH\n </div>\n <div class=\"flex-1 min-w-0\">\n <div class=\"flex items-center gap-1\">\n <h2 class=\"text-[11px] font-semibold truncate\">\n My Recent GitHub Commits\n </h2>\n <span class=\"text-[9px] text-gray-400\">v2.0.0</span>\n </div>\n <p class=\"text-[9px] text-gray-500 truncate\">\n Uses USER.githubToken to show your latest commits (via /search/commits).\n </p>\n </div>\n <button\n type=\"button\"\n @click=\"collapsed = !collapsed\"\n class=\"ml-1 h-7 w-7 rounded-xl flex items-center justify-center\n bg-gray-50 hover:bg-gray-100 text-gray-500\"\n :title=\"collapsed ? 'Expand' : 'Collapse'\"\n >\n <i data-lucide=\"chevron-down\"\n class=\"w-4 h-4 transition-transform\"\n :class=\"collapsed ? '-rotate-90' : 'rotate-0'\"></i>\n </button>\n </header>\n\n <!-- Body -->\n <div\n x-show=\"!collapsed\"\n x-transition\n class=\"py-2 flex flex-col gap-2\"\n style=\"display:none\"\n >\n <!-- Status / Controls -->\n <div class=\"flex items-center gap-2\">\n <div class=\"flex items-center gap-1.5 text-[9px] text-gray-500\">\n <span\n class=\"inline-flex h-1.5 w-1.5 rounded-full\"\n :class=\"{\n 'bg-emerald-500': status === 'ok',\n 'bg-amber-400': status === 'warn',\n 'bg-red-500': status === 'error',\n 'bg-gray-300': status === 'idle'\n }\"\n ></span>\n <span x-text=\"statusText\"></span>\n </div>\n\n <div class=\"flex items-center gap-1 ml-auto\">\n <button\n type=\"button\"\n @click=\"refresh()\"\n class=\"inline-flex items-center gap-1 px-1.5 py-0.5 rounded-lg\n bg-gray-900 text-white text-[9px] hover:bg-gray-800\"\n >\n <i data-lucide=\"refresh-cw\" class=\"w-3 h-3\"></i>\n <span>Refresh</span>\n </button>\n\n <button\n type=\"button\"\n @click=\"toggleSettings()\"\n class=\"inline-flex items-center justify-center h-5 w-5 rounded-lg\n bg-gray-50 hover:bg-gray-100 text-gray-500\"\n title=\"Settings\"\n >\n <i data-lucide=\"settings\" class=\"w-3 h-3\"></i>\n </button>\n\n <button\n type=\"button\"\n @click=\"toggleLog()\"\n class=\"inline-flex items-center justify-center h-5 px-1.5 rounded-lg\n bg-gray-50 hover:bg-gray-100 text-[8px] text-gray-500\"\n :class=\"showLog ? 'bg-gray-900 text-white hover:bg-gray-800' : ''\"\n title=\"Debug log\"\n >\n log\n </button>\n </div>\n </div>\n\n <!-- Settings -->\n <div\n x-show=\"showSettings\"\n x-transition\n class=\"p-2 bg-gray-50 rounded-xl border border-gray-100 flex flex-col gap-1.5 text-[9px]\"\n style=\"display:none\"\n >\n <div class=\"flex items-center gap-2\">\n <label class=\"w-20 text-gray-600\">Username</label>\n <input\n x-model.trim=\"settings.username\"\n @change=\"persistSettings();refresh()\"\n type=\"text\"\n placeholder=\"auto from /user\"\n class=\"flex-1 px-2 py-1 rounded-lg border border-gray-200\n text-[9px] placeholder:text-gray-300 focus:outline-none\n focus:ring-1 focus:ring-gray-300\"\n />\n </div>\n <div class=\"flex items-center gap-2\">\n <label class=\"w-20 text-gray-600\">Per page</label>\n <input\n x-model.number=\"settings.perPage\"\n @change=\"persistSettings();refresh()\"\n type=\"number\" min=\"1\" max=\"50\"\n class=\"w-14 px-1.5 py-0.5 rounded-lg border border-gray-200\n text-[9px] focus:outline-none focus:ring-1 focus:ring-gray-300\"\n />\n <label class=\"ml-1 text-gray-600\">Since (days)</label>\n <input\n x-model.number=\"settings.sinceDays\"\n @change=\"persistSettings();refresh()\"\n type=\"number\" min=\"1\" max=\"365\"\n class=\"w-14 px-1.5 py-0.5 rounded-lg border border-gray-200\n text-[9px] focus:outline-none focus:ring-1 focus:ring-gray-300\"\n />\n </div>\n <div class=\"flex justify-between items-center pt-0.5\">\n <p class=\"text-[8px] text-gray-500 pr-1\">\n Query: <code class=\"bg-gray-200 px-1 rounded\">author:you</code> via <code>/search/commits</code>.\n </p>\n <button\n type=\"button\"\n @click=\"resetSettings()\"\n class=\"px-1.5 py-0.5 rounded-lg border border-gray-200\n text-[8px] text-gray-500 hover:bg-gray-100\"\n >\n Reset\n </button>\n </div>\n </div>\n\n <!-- Debug Log -->\n <div\n x-show=\"showLog\"\n x-transition\n class=\"mt-1 p-1.5 bg-slate-950 rounded-xl border border-slate-800\n text-[8px] font-mono text-slate-200 space-y-0.5 max-h-32 overflow-y-auto\"\n style=\"display:none\"\n >\n <template x-if=\"!logLines.length\">\n <div class=\"text-slate-500\">Log is empty. Refresh to see diagnostics.</div>\n </template>\n <template x-for=\"(line, i) in logLines\" :key=\"i\">\n <div class=\"flex gap-1\">\n <span class=\"text-slate-500 shrink-0\" x-text=\"line.t\"></span>\n <span class=\"truncate\" x-text=\"line.m\"></span>\n </div>\n </template>\n </div>\n\n <!-- Empty / Error -->\n <template x-if=\"!loading && commits.length === 0\">\n <div class=\"mt-1 p-2 rounded-xl border border-dashed border-gray-200 text-[9px] text-gray-500 flex flex-col gap-1\">\n <span x-show=\"!hasToken\">\n Add your GitHub token in Account → API to view commits.\n </span>\n <span x-show=\"hasToken && status === 'ok'\">\n No commits matched the current query window. Check log for query details.\n </span>\n <span x-show=\"status === 'error'\" class=\"text-red-500\" x-text=\"errorMessage\"></span>\n </div>\n </template>\n\n <!-- Commits -->\n <div\n x-show=\"commits.length\"\n class=\"mt-0.5 space-y-1 overflow-y-auto no-scrollbar pr-1\"\n style=\"max-height:40vh;display:none\"\n >\n <template x-for=\"c in commits\" :key=\"c.sha\">\n <div\n class=\"group flex flex-col gap-0.5 px-2 py-1.5 rounded-xl border border-gray-100\n bg-gray-50/60 hover:bg-gray-100/90 transition\"\n >\n <div class=\"flex items-center gap-1.5\">\n <div class=\"h-4 w-4 rounded-full bg-gray-900 text-white text-[8px] flex items-center justify-center\">\n <i data-lucide=\"git-commit\" class=\"w-3 h-3\"></i>\n </div>\n <div class=\"flex-1 min-w-0\">\n <p class=\"text-[9px] font-medium text-gray-800 truncate\" x-text=\"c.messageShort\"></p>\n <p class=\"text-[8px] text-gray-500 truncate\">\n <span class=\"text-gray-700\" x-text=\"c.repo\"></span>\n <span class=\"mx-0.5\">•</span>\n <span x-text=\"c.branch || 'commit'\"></span>\n </p>\n </div>\n <span\n class=\"ml-1 text-[8px] px-1.5 py-0.5 rounded-full bg-gray-900/90 text-white whitespace-nowrap\"\n x-text=\"c.ago\"\n ></span>\n </div>\n <div class=\"flex items-center gap-1.5 text-[7.5px] text-gray-500 mt-0.5\">\n <span class=\"truncate\" x-text=\"c.sha.slice(0,7)\"></span>\n <span class=\"mx-0.5 text-gray-300\">•</span>\n <span class=\"truncate\" x-text=\"c.author\"></span>\n <a\n class=\"ml-auto inline-flex items-center gap-0.5 text-[7.5px] text-gray-500\n hover:text-gray-900 hover:underline\"\n :href=\"c.url\"\n target=\"_blank\"\n rel=\"noopener\"\n >\n View\n <i data-lucide=\"external-link\" class=\"w-2.5 h-2.5\"></i>\n </a>\n </div>\n </div>\n </template>\n </div>\n </div>\n</section>\n\n<script>\n;(() => {\n const lsKey=()=>{try{return'sune_github_commits_v2_'+(window.SUNE?.id||'default')}catch(_){return'sune_github_commits_v2_default'}}\n const load=()=>{try{return{\n username:'',\n perPage:20,\n sinceDays:14,\n ...JSON.parse(localStorage.getItem(lsKey())||'{}')\n }}catch(_){return{username:'',perPage:20,sinceDays:14}}}\n const save=s=>{try{localStorage.setItem(lsKey(),JSON.stringify(s))}catch(_){}}\n const ago=iso=>{\n const t=new Date(iso).getTime();if(!t)return''\n const d=(Date.now()-t)/1e3\n if(d<60)return`${Math.max(1,Math.floor(d))}s`\n const m=d/60;if(m<60)return`${Math.floor(m)}m`\n const h=m/60;if(h<24)return`${Math.floor(h)}h`\n const dd=h/24;if(dd<7)return`${Math.floor(dd)}d`\n const w=dd/7;if(w<4)return`${Math.floor(w)}w`\n const mo=dd/30;if(mo<12)return`${Math.floor(mo)}mo`\n const y=dd/365;return`${Math.floor(y)}y`\n }\n\n // Worker for /search/commits to offload JSON parsing\n const searchCommitsWorker=p=>new Promise(res=>{\n try{\n const blob=new Blob([`\n self.onmessage=async e=>{\n const{url,headers}=e.data;\n try{\n const r=await fetch(url,{headers});\n const rate=r.headers.get('x-ratelimit-remaining');\n if(!r.ok){\n const txt=await r.text().catch(()=> '');\n self.postMessage({error:'HTTP '+r.status,body:txt.slice(0,400),rate});\n return;\n }\n const data=await r.json();\n self.postMessage({items:data.items||[],total:data.total_count||0,rate});\n }catch(err){\n self.postMessage({error:String(err&&err.message||err||'Error')});\n }\n };\n `],{type:'application/javascript'});\n const w=new Worker(URL.createObjectURL(blob));\n w.onmessage=e=>{w.terminate();res(e.data||{})}\n w.postMessage(p)\n }catch(err){res({error:String(err&&err.message||err||'Worker error')})}\n })\n\n window.githubCommitsSune=()=>({\n version:'2.0.0',\n settings:load(),\n commits:[],\n loading:false,\n status:'idle',\n statusText:'Waiting…',\n errorMessage:'',\n collapsed:false,\n showSettings:false,\n showLog:false,\n logLines:[],\n hasToken:false,\n\n log(m){\n const t=new Date().toISOString().split('T')[1].replace('Z','')\n this.logLines.push({t,m:String(m)})\n if(this.logLines.length>160)this.logLines.shift()\n },\n toggleLog(){this.showLog=!this.showLog},\n\n persistSettings(){\n const s=this.settings\n if(!s.perPage||s.perPage<1)s.perPage=20\n if(s.perPage>50)s.perPage=50\n if(!s.sinceDays||s.sinceDays<1)s.sinceDays=14\n if(s.sinceDays>365)s.sinceDays=365\n save(s)\n },\n resetSettings(){\n this.settings={username:'',perPage:20,sinceDays:14}\n this.persistSettings()\n this.refresh()\n },\n toggleSettings(){this.showSettings=!this.showSettings},\n\n init(){\n this.hasToken=!!(window.USER&&window.USER.githubToken)\n this.log('init; hasToken='+(this.hasToken?'yes':'no'))\n if(!this.hasToken){\n this.status='warn'\n this.statusText='Add your GitHub token in Account → API.'\n return\n }\n this.refresh()\n },\n\n async resolveUsername(token){\n let u=(this.settings.username||'').trim()\n if(u){\n this.log('using configured username='+u)\n return u\n }\n try{\n this.log('GET /user to resolve login')\n const r=await fetch('https://api.github.com/user',{\n headers:{Authorization:'token '+token,Accept:'application/vnd.github+json'}\n })\n this.log('/user -> '+r.status)\n if(r.ok){\n const me=await r.json()\n u=me.login||''\n if(u){\n this.settings.username=u\n this.persistSettings()\n this.log('resolved username='+u)\n }\n }else{\n this.log('failed to resolve /user; status='+r.status)\n }\n }catch(e){\n this.log('error resolving /user: '+(e&&e.message||e))\n }\n return u\n },\n\n async refresh(){\n this.errorMessage=''\n this.loading=true\n this.status='idle'\n this.statusText='Loading commits…'\n this.commits=[]\n this.log('refresh')\n\n const token=window.USER&&window.USER.githubToken\n if(!token){\n this.loading=false\n this.hasToken=false\n this.status='warn'\n this.statusText='Missing GitHub token. Set it in Account → API.'\n this.log('no token')\n return\n }\n this.hasToken=true\n\n const username=await this.resolveUsername(token)\n if(!username){\n this.loading=false\n this.status='error'\n this.statusText='Cannot resolve your GitHub username.'\n this.errorMessage='Set \"Username\" in settings or ensure your token is valid.'\n this.log('no username; aborting')\n return\n }\n\n // Build search query: author:<user> pushed within sinceDays\n const d=new Date()\n d.setDate(d.getDate()-(this.settings.sinceDays||14))\n const sinceISO=d.toISOString().slice(0,10) // YYYY-MM-DD\n const perPage=this.settings.perPage||20\n\n const q=`author:${encodeURIComponent(username)}+author-date:>=${sinceISO}`\n const url=`https://api.github.com/search/commits?q=${q}&sort=author-date&order=desc&per_page=${perPage}`\n\n this.log('search url='+url)\n\n const {items,total,rate,error,body}=await searchCommitsWorker({\n url,\n headers:{\n Authorization:'token '+token,\n Accept:'application/vnd.github.cloak-preview+json'\n }\n })\n\n if(rate!=null)this.log('rate remaining='+rate)\n\n if(error){\n this.loading=false\n this.status='error'\n this.statusText='Failed to load commits.'\n this.errorMessage=error\n if(body)this.log('body='+body)\n this.log('error='+error)\n return\n }\n\n this.log('total_count='+(total||0)+'; items='+(items?items.length:0))\n\n const list=[]\n try{\n for(const it of items||[]){\n const c=it.commit||{}\n const msg=(c.message||'').split('\\n')[0]\n const sha=it.sha\n const repoFull=it.repository?.full_name||'unknown'\n const authorName=c.author?.name||it.author?.login||'unknown'\n const date=c.author?.date||c.committer?.date||it.committer?.date\n list.push({\n sha,\n messageShort:msg||'(no message)',\n author:authorName,\n repo:repoFull,\n branch:'', // branch not available from search API; keep minimal\n url:it.html_url||(`https://github.com/${repoFull}/commit/${sha}`),\n ago:date?ago(date):'',\n ts:date?new Date(date).getTime():0\n })\n }\n }catch(e){\n this.loading=false\n this.status='error'\n this.statusText='Parse error.'\n this.errorMessage=String(e&&e.message||e)\n this.log('parse error='+this.errorMessage)\n return\n }\n\n this.log('raw entries='+list.length)\n\n const seen=new Set\n const normalized=(list||[])\n .filter(c=>c.sha)\n .sort((a,b)=>(b.ts||0)-(a.ts||0))\n .filter(c=>{\n const k=c.sha\n if(seen.has(k))return false\n seen.add(k)\n return true\n })\n\n this.commits=normalized\n this.loading=false\n\n if(!normalized.length){\n this.status='ok'\n this.statusText='No commits matched the query.'\n this.log('no commits after normalization')\n }else{\n this.status='ok'\n this.statusText=`Loaded ${normalized.length} commit${normalized.length>1?'s':''}.`\n this.log('showing '+normalized.length+' commits')\n }\n\n try{window.lucide&&window.lucide.createIcons()}catch(_){}\n }\n })\n})()\n</script>\n","extension_html":"<sune src='https://raw.githubusercontent.com/sune-org/store/refs/heads/main/sync.sune' private></sune>","hide_composer":false,"include_thoughts":false,"json_output":false,"ignore_master_prompt":false,"json_schema":""},"storage":{}}]