56 lines
8.0 KiB
Python
56 lines
8.0 KiB
Python
# ruff: noqa: E501
|
|
"""Self-contained standards-first MCP Apps review surface."""
|
|
|
|
from __future__ import annotations
|
|
|
|
CHANGE_REVIEW_WIDGET_URI = "ui://itworx-gitea/change-review-v4.html"
|
|
|
|
CHANGE_REVIEW_WIDGET_HTML = r"""<!doctype html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>Repository change review</title>
|
|
<style>
|
|
:root{color-scheme:light dark;font-family:ui-sans-serif,system-ui,-apple-system,sans-serif;--fg:#202124;--card:transparent}
|
|
:root[data-theme="dark"]{--fg:#eceff4;color-scheme:dark}:root[data-theme="light"]{--fg:#202124;color-scheme:light}
|
|
*{box-sizing:border-box}body{margin:0;padding:14px;background:transparent;color:var(--fg)}
|
|
.shell{display:grid;gap:12px}.top{display:flex;gap:10px;align-items:flex-start;justify-content:space-between}
|
|
h1{font-size:17px;margin:0 0 4px}.muted{opacity:.68;font-size:12px}.pill{border-radius:999px;padding:5px 9px;font-size:12px;font-weight:650;background:#e9edf5;color:#233044}
|
|
.ok{background:#daf3df;color:#145c26}.warn{background:#fff0c2;color:#6a4c00}.bad{background:#f9dddd;color:#7b2020}
|
|
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:9px}.card{border:1px solid color-mix(in srgb,currentColor 18%,transparent);border-radius:10px;padding:10px;min-width:0;background:var(--card)}
|
|
.label{font-size:11px;text-transform:uppercase;letter-spacing:.06em;opacity:.62}.value{font-size:13px;font-weight:600;margin-top:4px;overflow-wrap:anywhere}
|
|
ul{list-style:none;margin:5px 0 0;padding:0;display:grid;gap:5px}li{display:flex;gap:8px;justify-content:space-between;font-size:12px;border-bottom:1px solid color-mix(in srgb,currentColor 10%,transparent);padding:4px 0}li:last-child{border:0}
|
|
button,.button{font:inherit;border:1px solid color-mix(in srgb,currentColor 25%,transparent);background:transparent;color:inherit;border-radius:8px;padding:7px 10px;cursor:pointer;text-decoration:none}button:hover,.button:hover{background:color-mix(in srgb,currentColor 7%,transparent)}button:disabled{opacity:.45;cursor:default}
|
|
.actions{display:flex;gap:7px;flex-wrap:wrap}.empty{font-size:12px;opacity:.66;padding-top:5px}.error{color:#a12626;font-size:12px}
|
|
@media(prefers-color-scheme:dark){:root:not([data-theme="light"]){--fg:#eceff4}.pill{background:#303849;color:#edf2ff}.ok{background:#183f23;color:#b9f1c5}.warn{background:#4a390d;color:#ffe8a3}.bad{background:#4a2020;color:#ffc6c6}}
|
|
</style>
|
|
</head>
|
|
<body><main class="shell" id="root" aria-live="polite"><div class="muted">Loading change review…</div></main>
|
|
<script>
|
|
(() => {
|
|
const root=document.getElementById('root');let current=null;let nextId=1;const pending=new Map();
|
|
const nl=(document.documentElement.lang||navigator.language||'en').toLowerCase().startsWith('nl');
|
|
const t=(en,nlText)=>nl?nlText:en;document.title=t('Repository change review','Wijziging beoordelen');
|
|
const text=(tag,value,cls)=>{const node=document.createElement(tag);node.textContent=value??'';if(cls)node.className=cls;return node};
|
|
const card=(label,value)=>{const node=text('div','','card');node.append(text('div',label,'label'),text('div',value||'—','value'));return node};
|
|
const unwrap=value=>value?.structuredContent?.data??value?.structuredContent??value?.data??value?.toolOutput?.data??value?.toolOutput?.structuredContent?.data??value;
|
|
function request(method,params){const id=nextId++;window.parent.postMessage({jsonrpc:'2.0',id,method,params},'*');return new Promise((resolve,reject)=>{const timeout=setTimeout(()=>{pending.delete(id);reject(new Error('MCP Apps bridge timeout'))},4000);pending.set(id,{resolve,reject,timeout})})}
|
|
function setTheme(theme){if(theme==='dark'||theme==='light')document.documentElement.dataset.theme=theme}
|
|
function render(raw){
|
|
const data=unwrap(raw);if(!data||typeof data!=='object'){root.replaceChildren(text('div',t('No review data returned.','Geen reviewgegevens ontvangen.'),'empty'));return}
|
|
current=data;const pr=data.pullRequest||{};const runs=Array.isArray(data.validationRuns)?data.validationRuns:[];const cmp=data.comparison||{};const files=Array.isArray(cmp.files)?cmp.files:[];
|
|
const state=data.readyToMerge?t('Ready to merge','Klaar om te mergen'):(!pr.number?t('PR not created','PR nog niet aangemaakt'):t('Needs attention','Actie nodig'));
|
|
const stateClass=data.readyToMerge?'pill ok':(!pr.number?'pill warn':'pill bad');const heading=text('div');heading.append(text('h1',`${data.repositoryRef||t('Repository','Repository')} · ${data.branch||t('change','wijziging')}`),text('div',`${t('Head','Head')} ${String(data.commitSha||'').slice(0,12)}`,'muted'));
|
|
const top=text('section','','top');top.append(heading,text('span',state,stateClass));const grid=text('section','','grid');
|
|
grid.append(card(t('Pull request','Pull request'),pr.number?`#${pr.number} · ${pr.state}`:t('Not created','Niet aangemaakt')),card(t('Target','Doel'),pr.baseRef||'—'),card(t('Changed files','Gewijzigde bestanden'),String(files.length||0)),card(t('Validation','Validatie'),runs[0]?`${runs[0].status} · ${runs[0].conclusion||t('pending','in afwachting')}`:t('Not run','Niet uitgevoerd')));
|
|
const fileCard=text('section','','card');fileCard.append(text('div',t('Changed files','Gewijzigde bestanden'),'label'));const list=text('ul');files.slice(0,20).forEach(file=>{const row=text('li');row.append(text('span',file.path),text('span',file.status,'muted'));list.append(row)});fileCard.append(files.length?list:text('div',t('No comparison available.','Geen vergelijking beschikbaar.'),'empty'));
|
|
const runCard=text('section','','card');runCard.append(text('div',t('Recent validation runs','Recente validaties'),'label'));const runList=text('ul');runs.slice(0,8).forEach(run=>{const row=text('li');row.append(text('span',run.name||`${t('Run','Run')} ${run.id}`),text('span',`${run.status} · ${run.conclusion||t('pending','in afwachting')}`,'muted'));runList.append(row)});runCard.append(runs.length?runList:text('div',t('No validation runs yet.','Nog geen validaties.'),'empty'));
|
|
const actions=text('section','','actions');const refresh=text('button',t('Refresh','Vernieuwen'));refresh.dataset.action='refresh';refresh.onclick=refreshReview;actions.append(refresh);if(/^https:\/\//.test(pr.sourceUri||'')){const open=text('a',t('Open pull request','Pull request openen'),'button');open.href=pr.sourceUri;open.target='_blank';open.rel='noopener noreferrer';actions.append(open)}root.replaceChildren(top,grid,fileCard,runCard,actions);
|
|
}
|
|
async function refreshReview(){if(!current?.repositoryRef||!current?.branch)return;const button=document.querySelector('[data-action="refresh"]');if(button)button.disabled=true;try{let result;try{result=await request('tools/call',{name:'gitea.change.review',arguments:{repositoryRef:current.repositoryRef,branch:current.branch}})}catch(bridgeError){if(!window.openai?.callTool)throw bridgeError;result=await window.openai.callTool('gitea.change.review',{repositoryRef:current.repositoryRef,branch:current.branch})}render(result)}catch(error){root.append(text('div',`${t('Refresh failed','Vernieuwen mislukt')}: ${error?.message||error}`,'error'))}finally{if(button)button.disabled=false}}
|
|
window.addEventListener('message',event=>{if(event.source!==window.parent)return;const message=event.data;if(!message||message.jsonrpc!=='2.0')return;if(message.id!==undefined&&pending.has(message.id)){const item=pending.get(message.id);pending.delete(message.id);clearTimeout(item.timeout);message.error?item.reject(message.error):item.resolve(message.result);return}if(message.method==='ui/notifications/tool-result')render(message.params?.structuredContent)},{passive:true});
|
|
window.addEventListener('openai:set_globals',event=>{const globals=event.detail?.globals??event.detail??{};setTheme(globals.theme);if(globals.toolOutput!=null)render(globals.toolOutput)});setTheme(window.openai?.theme);let hydrationAttempts=0;const hydrate=()=>{const output=window.openai?.toolOutput;if(output!=null){render(output);return}hydrationAttempts+=1;if(hydrationAttempts<50){setTimeout(hydrate,100);return}render(null)};hydrate();
|
|
})();
|
|
</script></body></html>"""
|