@fullstackhouse/cerber 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/runner/prompt.js
CHANGED
|
@@ -76,11 +76,13 @@ Summary shape:
|
|
|
76
76
|
- \`### ⚠️ \` worth knowing — risk, follow-up, breaking change. Skip it when there is none.
|
|
77
77
|
- The emoji and the order are fixed. The words after the emoji are yours: write the heading this PR needs ("### 🎯 Searching an order number returned every order"), and fall back to the plain label ("### 🎯 The problem") only when nothing sharper fits.
|
|
78
78
|
- A small PR gets one or two short paragraphs and NO headings. Headings on a three-line summary are noise.
|
|
79
|
+
- The summary describes the PR, never your review of it. Verification you performed ("I ran the tests", "checks came back green", "couldn't check X") belongs in the verdict's reasoning and nowhere else.
|
|
79
80
|
|
|
80
81
|
Verdict shape ("reasoning"):
|
|
81
82
|
- One lead sentence: the finding that decides it, or "No blockers." Then bullets — never a second paragraph.
|
|
82
83
|
- Each bullet opens with a bolded two-or-three-word lead and holds one fact: "**Ran the tests** — ...", "**Read the caller** — ...", "**Couldn't check** — ...". Two to five of them.
|
|
83
84
|
- What you could not settle gets its own bullet at the end. Say what would settle it.
|
|
85
|
+
- The verdict holds your evidence, not the PR's story. Name a finding or risk in a bullet, but its explanation lives once — in the summary or the comment that owns it. Never retell what the PR does here.
|
|
84
86
|
|
|
85
87
|
Comment shape ("body"):
|
|
86
88
|
- The first sentence is the finding, and stands on its own line. Then a blank line, then the evidence, the fix, or a code suggestion. A five-sentence paragraph is a wall — break it.
|
|
@@ -235,7 +237,7 @@ Rules:
|
|
|
235
237
|
- "line" is a line in the NEW version of the file and must appear in the diff. Use null for a file-level comment.
|
|
236
238
|
- "severity" is the finding's merge impact — "blocker" (would not approve with this in; the only tier that blocks), "minor" (should fix, author's call), "nit" (taste), null (not a finding: a question, a note). Grade as if the finding is true; hedge in the reply, never in the grade. A verdict revision must still follow from the worst finding left standing.
|
|
237
239
|
- Your reply is prose to a person, not a changelog. The cockpit already shows what you changed — do not list your own edits back to them.
|
|
238
|
-
- A revision keeps the shape the review is written in: a rewritten summary keeps its \`###\` sections (🎯 the problem, 🔧 the fix, 🔍 details, ⚠️ worth knowing — the ones that earn their place), a rewritten "reasoning" stays one lead sentence plus bullets, and a comment body leads with the finding on its own line and never writes its own grade in — every surface prefixes that from "severity".
|
|
240
|
+
- A revision keeps the shape the review is written in: a rewritten summary keeps its \`###\` sections (🎯 the problem, 🔧 the fix, 🔍 details, ⚠️ worth knowing — the ones that earn their place), a rewritten "reasoning" stays one lead sentence plus bullets, and a comment body leads with the finding on its own line and never writes its own grade in — every surface prefixes that from "severity". The division of labor holds too: the summary describes the PR and the verdict's reasoning holds the review's own verification — neither retells the other.
|
|
239
241
|
|
|
240
242
|
Writing style:
|
|
241
243
|
- Plain words, short sentences, no jargon. Someone who has NOT read the diff should follow you.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fullstackhouse/cerber",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "AI code-review cockpit: Claude reviews your PRs into local artifacts; nothing reaches GitHub until you say so.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Full Stack House (https://fullstack.house)",
|
|
@@ -233,4 +233,4 @@ ${s}</tr>
|
|
|
233
233
|
`}tablecell(s){let i=this.parser.parseInline(s.tokens),r=s.header?"th":"td";return(s.align?`<${r} align="${s.align}">`:`<${r}>`)+i+`</${r}>
|
|
234
234
|
`}strong({tokens:s}){return`<strong>${this.parser.parseInline(s)}</strong>`}em({tokens:s}){return`<em>${this.parser.parseInline(s)}</em>`}codespan({text:s}){return`<code>${Oa(s,!0)}</code>`}br(s){return"<br>"}del({tokens:s}){return`<del>${this.parser.parseInline(s)}</del>`}link({href:s,title:i,tokens:r}){let l=this.parser.parseInline(r),u=Xg(s);if(u===null)return l;s=u;let d='<a href="'+s+'"';return i&&(d+=' title="'+Oa(i)+'"'),d+=">"+l+"</a>",d}image({href:s,title:i,text:r,tokens:l}){l&&(r=this.parser.parseInline(l,this.parser.textRenderer));let u=Xg(s);if(u===null)return Oa(r);s=u;let d=`<img src="${s}" alt="${Oa(r)}"`;return i&&(d+=` title="${Oa(i)}"`),d+=">",d}text(s){return"tokens"in s&&s.tokens?this.parser.parseInline(s.tokens):"escaped"in s&&s.escaped?s.text:Oa(s.text)}},$f=class{strong({text:s}){return s}em({text:s}){return s}codespan({text:s}){return s}del({text:s}){return s}html({text:s}){return s}text({text:s}){return s}link({text:s}){return""+s}image({text:s}){return""+s}br(){return""}checkbox({raw:s}){return s}},ba=class Tf{constructor(i){Tt(this,"options");Tt(this,"renderer");Tt(this,"textRenderer");this.options=i||Wi,this.options.renderer=this.options.renderer||new ic,this.renderer=this.options.renderer,this.renderer.options=this.options,this.renderer.parser=this,this.textRenderer=new $f}static parse(i,r){return new Tf(r).parse(i)}static parseInline(i,r){return new Tf(r).parseInline(i)}parse(i){var l,u;this.renderer.parser=this;let r="";for(let d=0;d<i.length;d++){let g=i[d];if((u=(l=this.options.extensions)==null?void 0:l.renderers)!=null&&u[g.type]){let p=g,m=this.options.extensions.renderers[p.type].call({parser:this},p);if(m!==!1||!["space","hr","heading","code","table","blockquote","list","checkbox","html","def","paragraph","text"].includes(p.type)){r+=m||"";continue}}let _=g;switch(_.type){case"space":{r+=this.renderer.space(_);break}case"hr":{r+=this.renderer.hr(_);break}case"heading":{r+=this.renderer.heading(_);break}case"code":{r+=this.renderer.code(_);break}case"table":{r+=this.renderer.table(_);break}case"blockquote":{r+=this.renderer.blockquote(_);break}case"list":{r+=this.renderer.list(_);break}case"checkbox":{r+=this.renderer.checkbox(_);break}case"html":{r+=this.renderer.html(_);break}case"def":{r+=this.renderer.def(_);break}case"paragraph":{r+=this.renderer.paragraph(_);break}case"text":{r+=this.renderer.text(_);break}default:{let p='Token with "'+_.type+'" type was not found.';if(this.options.silent)return console.error(p),"";throw new Error(p)}}}return r}parseInline(i,r=this.renderer){var u,d;this.renderer.parser=this;let l="";for(let g=0;g<i.length;g++){let _=i[g];if((d=(u=this.options.extensions)==null?void 0:u.renderers)!=null&&d[_.type]){let m=this.options.extensions.renderers[_.type].call({parser:this},_);if(m!==!1||!["escape","html","link","image","checkbox","strong","em","codespan","br","del","text"].includes(_.type)){l+=m||"";continue}}let p=_;switch(p.type){case"escape":{l+=r.text(p);break}case"html":{l+=r.html(p);break}case"link":{l+=r.link(p);break}case"image":{l+=r.image(p);break}case"checkbox":{l+=r.checkbox(p);break}case"strong":{l+=r.strong(p);break}case"em":{l+=r.em(p);break}case"codespan":{l+=r.codespan(p);break}case"br":{l+=r.br(p);break}case"del":{l+=r.del(p);break}case"text":{l+=r.text(p);break}default:{let m='Token with "'+p.type+'" type was not found.';if(this.options.silent)return console.error(m),"";throw new Error(m)}}}return l}},ec,Is=(ec=class{constructor(s){Tt(this,"options");Tt(this,"block");this.options=s||Wi}preprocess(s){return s}postprocess(s){return s}processAllTokens(s){return s}emStrongMask(s){return s}provideLexer(s=this.block){return s?ga.lex:ga.lexInline}provideParser(s=this.block){return s?ba.parse:ba.parseInline}},Tt(ec,"passThroughHooks",new Set(["preprocess","postprocess","processAllTokens","emStrongMask"])),Tt(ec,"passThroughHooksRespectAsync",new Set(["preprocess","postprocess","processAllTokens"])),ec),LS=class{constructor(...s){Tt(this,"defaults",Uf());Tt(this,"options",this.setOptions);Tt(this,"parse",this.parseMarkdown(!0));Tt(this,"parseInline",this.parseMarkdown(!1));Tt(this,"Parser",ba);Tt(this,"Renderer",ic);Tt(this,"TextRenderer",$f);Tt(this,"Lexer",ga);Tt(this,"Tokenizer",ac);Tt(this,"Hooks",Is);this.use(...s)}walkTokens(s,i){var l,u;let r=[];for(let d of s)switch(r=r.concat(i.call(this,d)),d.type){case"table":{let g=d;for(let _ of g.header)r=r.concat(this.walkTokens(_.tokens,i));for(let _ of g.rows)for(let p of _)r=r.concat(this.walkTokens(p.tokens,i));break}case"list":{let g=d;r=r.concat(this.walkTokens(g.items,i));break}default:{let g=d;(u=(l=this.defaults.extensions)==null?void 0:l.childTokens)!=null&&u[g.type]?this.defaults.extensions.childTokens[g.type].forEach(_=>{let p=g[_].flat(1/0);r=r.concat(this.walkTokens(p,i))}):g.tokens&&(r=r.concat(this.walkTokens(g.tokens,i)))}}return r}use(...s){let i=this.defaults.extensions||{renderers:{},childTokens:{}};return s.forEach(r=>{let l={...r};if(l.async=this.defaults.async||l.async||!1,r.extensions&&(r.extensions.forEach(u=>{if(!u.name)throw new Error("extension name required");if("renderer"in u){let d=i.renderers[u.name];d?i.renderers[u.name]=function(...g){let _=u.renderer.apply(this,g);return _===!1&&(_=d.apply(this,g)),_}:i.renderers[u.name]=u.renderer}if("tokenizer"in u){if(!u.level||u.level!=="block"&&u.level!=="inline")throw new Error("extension level must be 'block' or 'inline'");let d=i[u.level];d?d.unshift(u.tokenizer):i[u.level]=[u.tokenizer],u.start&&(u.level==="block"?i.startBlock?i.startBlock.push(u.start):i.startBlock=[u.start]:u.level==="inline"&&(i.startInline?i.startInline.push(u.start):i.startInline=[u.start]))}"childTokens"in u&&u.childTokens&&(i.childTokens[u.name]=u.childTokens)}),l.extensions=i),r.renderer){let u=this.defaults.renderer||new ic(this.defaults);for(let d in r.renderer){if(!(d in u))throw new Error(`renderer '${d}' does not exist`);if(["options","parser"].includes(d))continue;let g=d,_=r.renderer[g],p=u[g];u[g]=(...m)=>{let b=_.apply(u,m);return b===!1&&(b=p.apply(u,m)),b||""}}l.renderer=u}if(r.tokenizer){let u=this.defaults.tokenizer||new ac(this.defaults);for(let d in r.tokenizer){if(!(d in u))throw new Error(`tokenizer '${d}' does not exist`);if(["options","rules","lexer"].includes(d))continue;let g=d,_=r.tokenizer[g],p=u[g];u[g]=(...m)=>{let b=_.apply(u,m);return b===!1&&(b=p.apply(u,m)),b}}l.tokenizer=u}if(r.hooks){let u=this.defaults.hooks||new Is;for(let d in r.hooks){if(!(d in u))throw new Error(`hook '${d}' does not exist`);if(["options","block"].includes(d))continue;let g=d,_=r.hooks[g],p=u[g];Is.passThroughHooks.has(d)?u[g]=m=>{if(this.defaults.async&&Is.passThroughHooksRespectAsync.has(d))return(async()=>{let E=await _.call(u,m);return p.call(u,E)})();let b=_.call(u,m);return p.call(u,b)}:u[g]=(...m)=>{if(this.defaults.async)return(async()=>{let E=await _.apply(u,m);return E===!1&&(E=await p.apply(u,m)),E})();let b=_.apply(u,m);return b===!1&&(b=p.apply(u,m)),b}}l.hooks=u}if(r.walkTokens){let u=this.defaults.walkTokens,d=r.walkTokens;l.walkTokens=function(g){let _=[];return _.push(d.call(this,g)),u&&(_=_.concat(u.call(this,g))),_}}this.defaults={...this.defaults,...l}}),this}setOptions(s){return this.defaults={...this.defaults,...s},this}lexer(s,i){return ga.lex(s,i??this.defaults)}parser(s,i){return ba.parse(s,i??this.defaults)}parseMarkdown(s){return(i,r)=>{let l={...r},u={...this.defaults,...l},d=this.onError(!!u.silent,!!u.async);if(this.defaults.async===!0&&l.async===!1)return d(new Error("marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise."));if(typeof i>"u"||i===null)return d(new Error("marked(): input parameter is undefined or null"));if(typeof i!="string")return d(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(i)+", string expected"));if(u.hooks&&(u.hooks.options=u,u.hooks.block=s),u.async)return(async()=>{let g=u.hooks?await u.hooks.preprocess(i):i,_=await(u.hooks?await u.hooks.provideLexer(s):s?ga.lex:ga.lexInline)(g,u),p=u.hooks?await u.hooks.processAllTokens(_):_;u.walkTokens&&await Promise.all(this.walkTokens(p,u.walkTokens));let m=await(u.hooks?await u.hooks.provideParser(s):s?ba.parse:ba.parseInline)(p,u);return u.hooks?await u.hooks.postprocess(m):m})().catch(d);try{u.hooks&&(i=u.hooks.preprocess(i));let g=(u.hooks?u.hooks.provideLexer(s):s?ga.lex:ga.lexInline)(i,u);u.hooks&&(g=u.hooks.processAllTokens(g)),u.walkTokens&&this.walkTokens(g,u.walkTokens);let _=(u.hooks?u.hooks.provideParser(s):s?ba.parse:ba.parseInline)(g,u);return u.hooks&&(_=u.hooks.postprocess(_)),_}catch(g){return d(g)}}}onError(s,i){return r=>{if(r.message+=`
|
|
235
235
|
Please report this to https://github.com/markedjs/marked.`,s){let l="<p>An error occurred:</p><pre>"+Oa(r.message+"",!0)+"</pre>";return i?Promise.resolve(l):l}if(i)return Promise.reject(r);throw r}}},Ji=new LS;function yt(s,i){return Ji.parse(s,i)}yt.options=yt.setOptions=function(s){return Ji.setOptions(s),yt.defaults=Ji.defaults,Ab(yt.defaults),yt};yt.getDefaults=Uf;yt.defaults=Wi;function zS(...s){return Ji.use(...s),yt.defaults=Ji.defaults,Ab(yt.defaults),yt}yt.use=zS;yt.walkTokens=function(s,i){return Ji.walkTokens(s,i)};yt.parseInline=Ji.parseInline;yt.Parser=ba;yt.parser=ba.parse;yt.Renderer=ic;yt.TextRenderer=$f;yt.Lexer=ga;yt.lexer=ga.lex;yt.Tokenizer=ac;yt.Hooks=Is;yt.parse=yt;yt.options;yt.setOptions;yt.walkTokens;yt.parseInline;ba.parse;ga.lex;yt.setOptions({gfm:!0,breaks:!1});function Hl({text:s,className:i}){const r=de.useMemo(()=>{const l=yt.parse(s??"",{async:!1});return I0.sanitize(l)},[s]);return s!=null&&s.trim()?h.jsx("div",{className:`md ${i??""}`,dangerouslySetInnerHTML:{__html:r}}):null}const Jg=["all","awaiting","drafted"];function Wg(s){return s.status==="awaiting"||s.status==="running"?"awaiting":s.status==="sent"?"sent":"drafted"}const lc=s=>!!s.pr.state&&s.pr.state!=="OPEN",eb=["ready","awaiting","running","failed","reviewed","skipped","sent"];function Ul(s){return[...s].sort((i,r)=>eb.indexOf(i.status)-eb.indexOf(r.status)||r.updatedAt.localeCompare(i.updatedAt))}const Bb=["sent","reviewed","skipped"];function Yf(s){return Ul(s.filter(i=>!lc(i)&&!Bb.includes(i.status)))}function US(s,i){if(!(i!=null&&i.enabled)||i.awaiting.length===0)return[];const r=new Set(Yf(s).map(u=>u.id)),l=new Set(i.awaiting.map(u=>u.id));return Ul(s.filter(u=>l.has(u.id)&&!r.has(u.id)))}const Pi=(s,i)=>i.some(r=>r.id===s.id);function df(s,i){var r;return i!=null&&i.enabled?((r=i.awaiting.find(l=>l.id===s.id))==null?void 0:r.reply)??"unknown":"unknown"}const BS={none:{label:"you haven't replied",title:"GitHub still lists you as a requested reviewer and you have said nothing on the PR — no review, no comment. Whoever opened it is waiting on you."},you:{label:"waiting on them",title:"You had the last word in the conversation but never submitted a review, so GitHub still lists you as a requested reviewer. Nothing is blocked on you until they answer."},them:{label:"they replied last",title:"You commented and someone has answered since, so the conversation is back with you — and GitHub still lists you as a requested reviewer, because a comment is not a review."},unknown:{label:"unanswered on GitHub",title:"You have never submitted a review on GitHub for this PR, so it still lists you as a requested reviewer. Cerber could not read the conversation, so it can't say whether you replied there."}},IS=s=>BS[s];function jS(s){const i=s.length,r=i===1?"1 review request":`${i} review requests`,l=s.some(lc)?"settled or archived":"settled";return`Nothing new in the inbox — but GitHub still has ${r} open on you. You ${l} ${i===1?"it":"them"} here without submitting a review, so ${i===1?"it sits":"they sit"} in the drawers below, tagged with whose move it is.`}function hf(s,i){const r=s.filter(l=>Pi(l,i)).length;return r===0?"":` · ${r} open review request${r===1?"":"s"}`}function Ff(s,i=Date.now()){const r=Math.max(0,Math.round((i-new Date(s).getTime())/1e3));return r<45?"now":r<3600?`${Math.round(r/60)}m`:r<86400?`${Math.round(r/3600)}h`:`${Math.round(r/86400)}d`}const HS={approve:"approve",comment:"comment",request_changes:"changes"},Ib={APPROVE:"approve",COMMENT:"comment",REQUEST_CHANGES:"request changes"};function Nf(s){return s.status==="running"?{label:"reviewing…",tone:"running"}:s.status==="awaiting"?{label:"review now",tone:"awaiting"}:s.status==="failed"?{label:"run failed",tone:"failed"}:s.sent?{label:`sent · ${Ib[s.sent.event]??s.sent.event}`,tone:"sent"}:s.verdict?{label:`${s.verdict.recommendation.replace("_"," ")} ${s.verdict.confidence}`,tone:HS[s.verdict.recommendation]??"none"}:{label:"—",tone:"none"}}function pf(s){return s.trusted?"ran the code":s.withSource?"read the full source":"read the diff only"}function qS(s){if(s.commentCount===0)return"no comments drafted";const i=`${s.commentCount} comment${s.commentCount===1?"":"s"} drafted`;return s.driftedCount?`${i}, ${s.driftedCount} off-diff`:i}function GS(s,i,r=Date.now()){var u,d;if(s.status==="running")return{reasoning:"Claude is reading the current head. This row updates itself when the run lands.",meta:[`started ${Ff(s.updatedAt,r)} ago`,pf(s)]};if(s.status==="awaiting")return{reasoning:"Found by the inbox poll, not drafted yet — auto-review was off when it arrived, or it is still queued behind another run.",meta:["no run yet","press r to review now"]};if(s.status==="failed")return{reasoning:s.runError??"The review run failed before it produced a draft.",meta:["press r to try again"]};if(s.sent)return{reasoning:`Sent to GitHub as ${Ib[s.sent.event]??s.sent.event} on ${new Date(s.sent.at).toLocaleDateString()}. Kept here as a record; the conversation can't continue.`,meta:[s.sent.auto?"auto-sent by the daemon":"sent by you",pf(s)]};const l=[pf(s)];return s.status==="reviewed"&&l.unshift("you marked this reviewed"),s.status==="skipped"&&l.unshift("you skipped this"),s.costUsd!=null&&l.push(`≈$${s.costUsd.toFixed(2)} at API rates`),l.push(qS(s)),i!=null&&i.enabled&&s.status==="ready"&&((u=s.verdict)==null?void 0:u.recommendation)==="approve"&&l.push(s.verdict.confidence>=i.autoSendThreshold?`auto-send candidate at ${s.verdict.confidence}%`:`below the ${i.autoSendThreshold}% auto-send bar`),{reasoning:((d=s.verdict)==null?void 0:d.reasoning)??"This review has a draft but no verdict.",meta:l}}const Wr={APPROVE:"approve",COMMENT:"comment",REQUEST_CHANGES:"request changes"},tb={APPROVE:"approve",COMMENT:"comment",REQUEST_CHANGES:"changes"};function $S(s){return(s==null?void 0:s.recommendation)==="approve"?"APPROVE":(s==null?void 0:s.recommendation)==="request_changes"?"REQUEST_CHANGES":"COMMENT"}function jb(s,i){const r=F_(s);return i.filter(l=>{var u;return!l.drifted&&l.line!=null&&((u=r.get(l.path))==null?void 0:u.has(l.line))})}function Hb(s){const i=s.comments.filter(u=>u.status==="dropped"),r=s.comments.filter(u=>u.status!=="dropped"),l=jb(s.diff,r);return{inline:l,folded:r.filter(u=>!l.includes(u)),dropped:i}}function nb(s){const i=s.comments.filter(u=>u.status!=="dropped");return["blocker","minor","nit"].map(u=>({tier:u,n:i.filter(d=>d.severity===u).length})).filter(({n:u})=>u>0).map(({tier:u,n:d})=>`${Eb[u]} ${d} ${u}${d===1?"":"s"}`).join(" · ")}function YS(s){if(!s.verdict||!s.comments.some(u=>u.severity!=null))return null;const r=s.comments.filter(u=>u.status!=="dropped"&&u.severity==="blocker").length,l=s.verdict.recommendation;return l==="approve"&&r>0?`the verdict says approve, but ${r} blocker${r===1?"":"s"} still stand${r===1?"s":""}`:l==="request_changes"&&r===0?"the verdict says request changes, but no blocker still stands":null}function ab(s){const{inline:i,folded:r}=Hb(s);if(i.length===0&&r.length===0)return"no inline comments · body only";const l=[`${i.length} inline`];return r.length>0&&l.push(`${r.length} folded into the body`),l.join(" · ")}const ib={approve:"approve",comment:"comment",request_changes:"changes"},FS={approve:"approve",comment:"comment",request_changes:"changes"};function KS(s,i){return s.status==="dropped"?"none":s.drifted?"comment":s.origin==="user"?"awaiting":i}function qb(s,i=20){const r=document.getElementById(`c-${s}`);if(r){r.scrollIntoView({behavior:"smooth",block:"center"});return}i>0&&requestAnimationFrame(()=>qb(s,i-1))}function ZS(s){const i=s,r=i==null?void 0:i.tagName;return r==="INPUT"||r==="TEXTAREA"||r==="SELECT"||(i==null?void 0:i.isContentEditable)===!0}function XS({patch:s,comments:i,renderComment:r}){const l=de.useMemo(()=>s.trim()?q_(s,{drawFileList:!1,matching:"lines",outputFormat:"line-by-line"}):"",[s]),u=de.useRef(null),[d,g]=de.useState([]);if(de.useEffect(()=>{var T,x;const p=u.current;if(!p)return;p.innerHTML=l,Qv(p);const m=new Map;for(const N of p.querySelectorAll(".d2h-file-wrapper")){const R=(x=(T=N.querySelector(".d2h-file-name"))==null?void 0:T.textContent)==null?void 0:x.trim();R&&m.set(R,N)}const b=[],E=new Map;for(const N of i){if(N.line==null)continue;const R=m.get(N.path);if(!R)continue;const k=Array.from(R.querySelectorAll(".d2h-diff-tbody > tr")).find(F=>{var X,ne;return((ne=(X=F.querySelector(".line-num2"))==null?void 0:X.textContent)==null?void 0:ne.trim())===String(N.line)});if(!k)continue;const z=document.createElement("tr");z.className="inline-comment-row";const $=document.createElement("td");$.colSpan=2;const G=document.createElement("div");$.appendChild(G),z.appendChild($),(E.get(k)??k).after(z),E.set(k,z),b.push({id:N.id,el:G})}g(b)},[l,i]),!l)return h.jsx("p",{className:"muted",children:"No diff for this chapter."});const _=i.filter(p=>!d.some(m=>m.id===p.id));return h.jsxs(h.Fragment,{children:[h.jsx("div",{className:"diff",ref:u}),d.map(({id:p,el:m})=>{const b=i.find(E=>E.id===p);return b?G_.createPortal(r(b),m,p):null}),_.map(p=>r(p))]})}function Gb({comment:s,onUpdate:i,onDelete:r,onDiscuss:l,readOnly:u,flash:d}){const[g,_]=de.useState(!1),[p,m]=de.useState(s.body),b=s.status==="approved",E=s.status==="dropped",T=s.origin==="user"?"yours":E?"dropped":b?"kept":"drafted";return h.jsxs("div",{id:`c-${s.id}`,className:`comment comment-${s.status}${d?" comment-flash":""}`,children:[h.jsxs("div",{className:"comment-head",children:[h.jsxs("span",{className:"comment-loc",children:[s.path,s.line!=null?`:${s.line}`:""]}),h.jsx("span",{className:`tag tag-${s.origin==="user"?"yours":T}`,children:T}),s.drifted&&h.jsx("span",{className:"tag tag-drift",title:"The code this comment pointed at is gone from the diff, so it can't post inline.",children:"drifted — posts in the body"}),!s.drifted&&s.originalLine!=null&&s.originalLine!==s.line&&h.jsxs("span",{className:"faint",title:"This comment followed its code to a new line.",children:["was :",s.originalLine]}),h.jsx("span",{className:"grow"}),!u&&(g?h.jsxs(h.Fragment,{children:[h.jsx("button",{className:"btn btn-sm",onClick:()=>{i({body:p}),_(!1)},children:"save"}),h.jsx("button",{className:"btn btn-sm",onClick:()=>{m(s.body),_(!1)},children:"cancel"})]}):h.jsxs(h.Fragment,{children:[h.jsxs("button",{className:`btn btn-sm${b?" btn-keep-on":""}`,title:"Keep this comment — it posts with the review",onClick:()=>i({status:b?"draft":"approved"}),children:[h.jsx(pt,{name:"check"}),"keep"]}),h.jsxs("button",{className:"btn btn-sm",title:"Edit the wording",onClick:()=>_(!0),children:[h.jsx(pt,{name:"edit"}),"edit"]}),h.jsxs("button",{className:`btn btn-sm${E?" btn-drop-on":""}`,title:"Drop it — stays local, never posts",onClick:()=>i({status:E?"draft":"dropped"}),children:[h.jsx(pt,{name:"drop"}),E?"dropped":"drop"]}),l&&h.jsxs("button",{className:"btn btn-sm btn-accent",title:"Point the chat at this comment",onClick:l,children:[h.jsx(pt,{name:"comment"}),"discuss"]}),s.origin==="user"&&h.jsx("button",{className:"btn btn-sm",title:"Delete the comment you wrote",onClick:r,children:"✕"})]}))]}),g?h.jsx("textarea",{className:"comment-edit",value:p,onChange:x=>m(x.target.value),rows:Math.max(3,p.split(`
|
|
236
|
-
`).length)}):h.jsx(Hl,{className:"comment-body prose",text:Z_(s.body,s.severity??null)})]})}function PS({files:s,chapterId:i,onAdd:r}){const[l,u]=de.useState(!1),[d,g]=de.useState(s[0]??""),[_,p]=de.useState(""),[m,b]=de.useState("");return l?h.jsxs("div",{className:"add-comment",children:[h.jsxs("div",{className:"add-comment-row",children:[h.jsx("select",{value:d,onChange:E=>g(E.target.value),children:s.map(E=>h.jsx("option",{value:E,children:E},E))}),h.jsx("input",{placeholder:"line (optional)",value:_,onChange:E=>p(E.target.value.replace(/\D/g,"")),size:12})]}),h.jsx("textarea",{placeholder:"Your comment…",value:m,onChange:E=>b(E.target.value),rows:3}),h.jsxs("div",{className:"add-comment-row",children:[h.jsx("button",{className:"btn",disabled:!m.trim()||!d,onClick:()=>{r({path:d,line:_?Number(_):null,body:m.trim(),chapterId:i}),b(""),p(""),u(!1)},children:"add"}),h.jsx("button",{className:"btn",onClick:()=>u(!1),children:"cancel"})]})]}):h.jsxs("button",{className:"btn btn-dashed",onClick:()=>u(!0),children:[h.jsx(pt,{name:"plus"}),"add a comment of your own"]})}function QS({chapter:s,n:i,diff:r,comments:l,open:u,onToggle:d,onUpdateComment:g,onDeleteComment:_,onAddComment:p,onDiscuss:m,readOnly:b,anchorRef:E,flash:T}){const x=de.useMemo(()=>$_(r,s.files),[r,s.files]),N=de.useMemo(()=>jb(x,l),[x,l]),R=l.filter(z=>!N.includes(z)),k=z=>h.jsx(Gb,{comment:z,onUpdate:$=>g(z.id,$),onDelete:()=>_(z.id),onDiscuss:m&&(()=>m({target:"comment",id:z.id})),readOnly:b,flash:T===z.id},z.id);return h.jsxs("section",{className:"chapter",ref:E,children:[h.jsxs("header",{className:"chapter-head",onClick:d,children:[h.jsx("span",{className:"faint",children:u?"▾":"▸"}),h.jsxs("h3",{children:[i," · ",s.title]}),h.jsxs("span",{className:"faint chapter-meta",children:[s.files.length," file",s.files.length===1?"":"s",l.length>0?` · ${l.length} comment${l.length===1?"":"s"}`:" · no comments"]}),h.jsx("span",{className:"grow"}),m&&h.jsxs("button",{className:"btn btn-sm",onClick:z=>{z.stopPropagation(),m({target:"chapter",id:s.id})},children:[h.jsx(pt,{name:"comment"}),"discuss"]})]}),u&&h.jsxs("div",{className:"chapter-body",children:[h.jsx(Hl,{className:"prose chapter-explanation",text:s.explanation}),R.map(k),h.jsx(XS,{patch:x,comments:N,renderComment:k}),!b&&s.files.length>0&&h.jsx(PS,{files:s.files,chapterId:s.id,onAdd:p})]})]})}function VS(s,i){switch(s.kind){case"summary":return"rewrote the summary";case"verdict":return`changed the verdict to ${s.verdict.recommendation.replace("_"," ")}`;case"chapter":{const r=i.chapters.find(l=>l.id===s.chapterId);return`rewrote the "${(r==null?void 0:r.title)??s.chapterId}" chapter`}case"comment-edit":{const r=i.comments.find(l=>l.id===s.commentId);return r?`rewrote the comment on ${r.path}:${r.line??"file"}`:"rewrote a comment"}case"comment-drop":{const r=i.comments.find(l=>l.id===s.commentId);return r?`dropped the comment on ${r.path}:${r.line??"file"}`:"dropped a comment"}case"comment-add":return`added a comment on ${s.path}:${s.line??"file"}`}}function xf(s,i){var l;if(s.target==="summary")return"the summary";if(s.target==="verdict")return"the verdict";if(s.target==="chapter")return((l=i.chapters.find(u=>u.id===s.id))==null?void 0:l.title)??"a chapter";const r=i.comments.find(u=>u.id===s.id);return r?`${r.path}:${r.line??"file"}`:"a comment"}function JS({turn:s,artifact:i}){return h.jsxs("div",{className:`chat-turn chat-turn-${s.role}`,children:[h.jsx("div",{className:"chat-turn-who",children:s.role==="user"?"You":"Reviewer"}),s.refs.length>0&&h.jsx("div",{className:"chat-refs",children:s.refs.map((r,l)=>h.jsx("span",{className:"chat-ref",children:xf(r,i)},l))}),h.jsx(Hl,{className:"chat-turn-body prose",text:s.body}),s.revisions.map((r,l)=>h.jsxs("div",{className:"chat-revision",children:["↳ ",VS(r,i)]},l)),s.refused.map((r,l)=>h.jsxs("div",{className:"chat-refusal",title:r.reason,children:["⃠ left alone — ",r.reason]},l))]})}function WS({artifact:s,reviewKey:i,refs:r,onClearRefs:l,onDropRef:u,onArtifact:d,readOnly:g,inputRef:_}){const[p,m]=de.useState(""),[b,E]=de.useState(!1),[T,x]=de.useState(null),N=s.chat??[],R=s.pendingChat??null,k=R!=null&&R.error==null,z=b||k,$=(F,X,ne)=>{!F||z||(E(!0),x(null),vb(i,{message:F,refs:X}).then(ae=>{d(ae),ne==null||ne()}).catch(ae=>x(String(ae.message??ae))).finally(()=>E(!1)))},G=()=>$(p.trim(),r,()=>{m(""),l()});return h.jsxs("section",{className:"card chat-panel",children:[h.jsxs("header",{className:"card-head",children:[h.jsx("h2",{children:"talk to the reviewer"}),h.jsx("span",{className:"faint",children:"it revises the draft as you go · nothing here reaches GitHub — only send does"})]}),h.jsxs("div",{className:"card-body",children:[(N.length>0||R)&&h.jsxs("div",{className:"chat-log",children:[N.map(F=>h.jsx(JS,{turn:F,artifact:s},F.id)),R&&h.jsxs("div",{className:"chat-turn chat-turn-user chat-turn-pending",children:[h.jsx("div",{className:"chat-turn-who",children:"You"}),R.refs.length>0&&h.jsx("div",{className:"chat-refs",children:R.refs.map((F,X)=>h.jsx("span",{className:"chat-ref",children:xf(F,s)},X))}),h.jsx(Hl,{className:"chat-turn-body prose",text:R.message}),R.error?h.jsxs("div",{className:"chat-failed",children:["✕ this turn didn't finish — ",R.error,!g&&h.jsxs(h.Fragment,{children:[" ",h.jsx("button",{className:"btn btn-sm",onClick:()=>$(R.message,R.refs),children:"ask again"})," ",h.jsx("button",{className:"btn btn-sm",onClick:()=>ev(i).then(d).catch(F=>x(String(F.message??F))),children:"dismiss"})]})]}):h.jsx("div",{className:"chat-waiting",children:R.progress.length>0?h.jsx("ol",{className:"chat-progress",children:R.progress.map((F,X)=>h.jsx("li",{className:X===R.progress.length-1?"now":void 0,children:F},X))}):"Thinking… it re-reads the code before answering."})]})]}),g?h.jsx("p",{className:"faint",children:"This review was sent — the conversation is kept as a record, but it can't continue."}):h.jsxs(h.Fragment,{children:[r.length>0&&h.jsxs("div",{className:"chat-refs chat-refs-pending",children:[h.jsx("span",{className:"faint",children:"about:"}),r.map((F,X)=>h.jsxs("button",{className:"chat-ref",onClick:()=>u(X),title:"Remove",children:[xf(F,s)," ✕"]},X))]}),h.jsx("textarea",{ref:_,className:"chat-input",rows:2,value:p,disabled:b,placeholder:N.length===0?"the summary restates the author's claims — say what you actually verified":"say more…",onChange:F=>m(F.target.value),onKeyDown:F=>{F.key==="Enter"&&(F.metaKey||F.ctrlKey)&&G()}}),h.jsxs("div",{className:"chat-actions",children:[h.jsxs("button",{className:"btn btn-dark",onClick:G,disabled:z||p.trim()==="",children:[h.jsx(pt,{name:"send"}),k?"waiting for the answer…":b?"sending…":"send message",h.jsx(Vi,{children:"⌘↵"})]}),h.jsx("span",{className:"grow"}),s.preChat&&h.jsx("button",{className:"btn btn-sm",disabled:z,title:"Put the review back the way the AI first wrote it. The conversation stays.",onClick:()=>{E(!0),tv(i).then(d).catch(F=>x(String(F.message??F))).finally(()=>E(!1))},children:"reset the review"})]})]}),T&&h.jsx("div",{className:"error",children:T})]})]})}function eT({artifact:s,reviewKey:i,event:r,overridden:l,onPickEvent:u,sending:d,onSend:g,error:_,footer:p,next:m,onAdvance:b}){var G;const[E,T]=de.useState(null),[x,N]=de.useState(null),[R,k]=de.useState(!1),[z,$]=de.useState(!1);return de.useEffect(()=>{R&&nv(i,r).then(F=>{T(F),N(null)}).catch(F=>N(String(F)))},[i,r,s,R]),s.sent?h.jsxs(h.Fragment,{children:[h.jsxs("div",{className:"sent-strip",children:[h.jsxs("strong",{children:["✔ sent as ",Wr[s.sent.event]]}),h.jsxs("span",{className:"faint",children:[new Date(s.sent.at).toLocaleString()," · ",ab(s),s.sent.auto?" · auto-sent by the daemon":""]}),h.jsx("span",{className:"grow"}),s.sent.url&&h.jsxs("a",{href:s.sent.url,target:"_blank",rel:"noreferrer",children:["view on GitHub ",h.jsx(pt,{name:"external"})]}),h.jsxs("button",{className:"btn",onClick:b,children:[m?`next review: ${m.pr.repo}#${m.pr.number}`:"back to the queue",h.jsx(pt,{name:"arrowRight"})]})]}),h.jsx("div",{className:"post-actions",children:p})]}):h.jsxs("div",{className:"send-panel",children:[h.jsxs("div",{className:"send-top",children:[h.jsx("span",{className:"lab",children:"send to github"}),h.jsx("span",{className:"grow"}),h.jsx("span",{className:"faint",children:ab(s)}),h.jsxs("button",{className:"link",onClick:()=>k(!R),children:[h.jsx(pt,{name:"eye"}),R?"hide what gets posted":"see what gets posted"]})]}),h.jsxs("div",{className:"send-action",children:[h.jsxs("button",{className:`send-btn tone-bg-${tb[r]}`,disabled:d,onClick:g,children:[h.jsx(pt,{name:r==="APPROVE"?"approve":r==="COMMENT"?"comment":"changes",size:15}),d?"sending…":`${Wr[r]} on ${s.id}`," ",h.jsx(Vi,{children:"s"})]}),h.jsxs("div",{className:"send-as",children:[h.jsxs("button",{className:"send-as-toggle",onClick:()=>$(!z),children:["as ",h.jsx("b",{children:Wr[r]})," ",z?"▴":"▾"]}),h.jsx("div",{className:"send-as-why",children:l?`you chose this — the verdict says ${((G=s.verdict)==null?void 0:G.recommendation.replace("_"," "))??"nothing"}`:"follows the verdict"}),z&&h.jsx("div",{className:"menu",children:["REQUEST_CHANGES","COMMENT","APPROVE"].map(F=>h.jsxs("button",{className:`menu-item${r===F?" menu-item-on":""}`,onClick:()=>{u(F),$(!1)},children:[h.jsx("span",{className:`tone-${tb[F]}`,children:h.jsx(pt,{name:F==="APPROVE"?"approve":F==="COMMENT"?"comment":"changes"})}),h.jsx("span",{className:"grow",children:Wr[F]}),h.jsx("span",{className:"menu-check",children:r===F?"✓":""})]},F))})]})]}),_&&h.jsx("p",{className:"error",children:_}),R&&(x?h.jsx("p",{className:"error",children:x}):E?h.jsx("pre",{className:"body-preview",children:E.body}):h.jsx("p",{className:"faint",children:"building the body…"})),h.jsx("div",{className:"send-footer",children:p})]})}function tT({artifact:s,freshness:i,rerunning:r,onRerun:l}){const u=s.status==="running",d=(i==null?void 0:i.prState)??s.pr.state,g=d==="CLOSED"||d==="MERGED",_=s.refresh,p=i!=null&&i.changed?_:null;return u?h.jsxs("div",{className:"freshness freshness-running",children:[h.jsx("strong",{children:"Re-reviewing…"})," Claude is reading the current head. This page updates itself when the run lands — it takes a few minutes."]}):!g&&!p?null:h.jsxs("div",{className:"freshness",children:[g&&h.jsxs("div",{children:[h.jsxs("strong",{children:["This PR is ",d==="MERGED"?"merged":"closed","."]})," A review can still be sent, but nobody is waiting for it."]}),p&&h.jsxs("div",{children:[h.jsx("strong",{children:"The PR moved on since this review."})," Now at"," ",h.jsx("code",{children:p.toSha.slice(0,7)}),", reviewed at"," ",h.jsx("code",{children:p.fromSha.slice(0,7)}),"."," ",p.moved>0&&h.jsxs(h.Fragment,{children:[p.moved," comment(s) followed the code. "]}),p.drifted>0&&h.jsxs(h.Fragment,{children:[p.drifted," could not — the code they point at is gone, so they will post in the review body instead of inline."," "]}),"The summary and verdict still describe the commit that was reviewed.",!s.sent&&h.jsx("button",{className:"btn btn-sm",disabled:r,onClick:()=>l(),children:r?"starting…":"re-review at the new head"})]})]})}function nT({reviewKey:s}){var sn,kn,Tn,Nn,Cn;const[i,r]=de.useState(null),[l,u]=de.useState(null),[d,g]=de.useState(null),[_,p]=de.useState(null),[m,b]=de.useState(!1),[E,T]=de.useState([]),[x,N]=de.useState([]),R=de.useRef(null),[k,z]=de.useState(new Set),[$,G]=de.useState(0),F=de.useRef(new Map),X=de.useRef(null),ne=de.useRef(null),[ae,M]=de.useState(null),[q,V]=de.useState(!1),[se,_e]=de.useState(null),[oe,De]=de.useState(null);de.useEffect(()=>{if(!oe)return;const le=setTimeout(()=>De(null),1800);return()=>clearTimeout(le)},[oe]);const ge=le=>{var pe,Re;N(lt=>lt.some(Vn=>Vn.target===le.target&&Vn.id===le.id)?lt:[...lt,le]),(pe=R.current)==null||pe.scrollIntoView({behavior:"smooth",block:"center"}),(Re=R.current)==null||Re.focus()};de.useEffect(()=>{let le=!1;return window.scrollTo({top:0}),g(null),p(null),M(null),_e(null),Gm(s).then(pe=>{if(!le)return r(pe),W_(s).then(Re=>{le||(g(Re),Re.changed&&r(Re.artifact))}).catch(Re=>!le&&p(String(Re.message??Re)))}).catch(pe=>!le&&u(String(pe))),()=>{le=!0}},[s]),de.useEffect(()=>{zf().then(T).catch(()=>{})},[]),de.useEffect(()=>{const le=ne.current;if(!le)return;const pe=()=>document.documentElement.style.setProperty("--top-h",`${le.offsetHeight}px`);pe();const Re=new ResizeObserver(pe);return Re.observe(le),()=>{Re.disconnect(),document.documentElement.style.removeProperty("--top-h")}},[i==null?void 0:i.id]);const U=(i==null?void 0:i.pendingChat)!=null&&i.pendingChat.error==null;de.useEffect(()=>{if((i==null?void 0:i.status)!=="running"&&!U)return;const le=setInterval(()=>{Gm(s).then(pe=>{r(pe),pe.status!=="running"&&b(!1)}).catch(()=>{})},3e3);return()=>clearInterval(le)},[i==null?void 0:i.status,U,s]);const J=(i==null?void 0:i.sent)!=null,ue=de.useMemo(()=>Yf(E),[E]),K=ue.findIndex(le=>le.key===s),fe=(K>0?ue[K-1]:null)??null,v=(K>=0&&K<ue.length-1?ue[K+1]:null)??null,C=le=>{window.location.hash=`#/r/${encodeURIComponent(le)}`},j=()=>{v?C(v.key):window.location.hash="#/"},re=de.useMemo(()=>{if(!i)return[];const le=K_(i.diff,i.chapters);return le.length>0?[...i.chapters,{id:"__other",title:"other changes",explanation:"Files not assigned to any chapter by the AI.",files:le}]:i.chapters},[i]),ve=le=>{const pe=re[le];return pe?(G(le),z(Re=>{const lt=new Set(Re);return lt.delete(pe.id),lt}),pe):null},Oe=le=>{var Re;const pe=ve(le);pe&&((Re=F.current.get(pe.id))==null||Re.scrollIntoView({behavior:"smooth",block:"start"}))},Se=(le,pe)=>{ve(le),De(pe),qb(pe)},Ce=ae??$S(i==null?void 0:i.verdict),Le=(sn=i==null?void 0:i.verdict)==null?void 0:sn.recommendation;de.useEffect(()=>M(null),[Le]),de.useEffect(()=>_e(null),[Ce]);const Ye=()=>{!i||i.sent||q||(V(!0),_e(null),av(s,Ce).then(r).catch(le=>_e(String(le.message??le))).finally(()=>V(!1)))};if(de.useEffect(()=>{const le=pe=>{ZS(pe.target)||pe.metaKey||pe.ctrlKey||pe.altKey||(pe.key==="["&&fe?C(fe.key):pe.key==="]"&&v?C(v.key):pe.key==="n"?Oe(Math.min(re.length-1,$+1)):pe.key==="N"?Oe(Math.max(0,$-1)):pe.key==="s"&&!J&&Ye())};return window.addEventListener("keydown",le),()=>window.removeEventListener("keydown",le)},[fe==null?void 0:fe.key,v==null?void 0:v.key,re,$,J,Ce,i==null?void 0:i.sent,q]),l&&!i)return h.jsx("p",{className:"error pad",children:l});if(!i)return h.jsx("p",{className:"muted pad",children:"Loading…"});const Je=le=>{b(!0),u(null),_b(s,le).then(r).catch(pe=>{u(String(pe)),b(!1)})},Me=le=>le.then(r).catch(pe=>u(String(pe))),At=le=>bf(s,{status:le}).then(j).catch(pe=>u(String(pe))),Fe=(le,pe)=>Me(P_(s,le,pe)),Rn=le=>Me(V_(s,le)),it=le=>Me(Q_(s,le)),vn=i.comments.filter(le=>!le.chapterId||!re.some(pe=>pe.id===le.chapterId)),{inline:Pn,folded:Jt,dropped:Mn}=Hb(i),pn=i.comments.filter(le=>le.status!=="dropped"&&le.drifted).length,jn=i.verdict?ib[i.verdict.recommendation]:"none",Qn=YS(i),nt=i.pendingChat!=null&&i.pendingChat.error==null,Ht=()=>Me(vb(s,{message:"Some findings were dropped, edited or re-graded after this review was drafted. Update the summary and verdict to match what still stands — do not re-review the whole PR.",refs:[{target:"verdict",id:null}]})),Nt=(kn=i.run)!=null&&kn.trusted?"ran the code":(Tn=i.run)!=null&&Tn.withSource?"read the full source":i.run?"read the diff only":"no run yet",Sn=le=>{var pe;return h.jsxs("button",{className:`verdict-pick${((pe=i.verdict)==null?void 0:pe.recommendation)===le?` verdict-pick-on tone-${ib[le]}`:""}`,title:le==="approve"?"Approve":le==="comment"?"Comment without blocking":"Request changes",onClick:()=>Me(bf(s,{verdictRecommendation:le})),children:[h.jsx(pt,{name:FS[le]}),le.replace("_"," ")]},le)};return h.jsxs("div",{className:"review",children:[h.jsxs("div",{className:"review-top",ref:ne,children:[h.jsx("div",{className:"crumb",children:h.jsxs("div",{className:"bar-inner",children:[h.jsx("a",{href:"#/",children:"← queue"}),h.jsx("span",{className:"crumb-sep",children:"/"}),h.jsx("span",{className:"crumb-slug",children:i.id}),h.jsxs("span",{className:"walk",children:[h.jsx("button",{className:"walk-btn",disabled:!fe,title:fe?`[ — ${fe.id}: ${fe.pr.title}`:"no more reviews this way",onClick:()=>fe&&C(fe.key),children:h.jsx(pt,{name:"chevronLeft"})}),h.jsx("button",{className:"walk-btn",disabled:!v,title:v?`] — ${v.id}: ${v.pr.title}`:"no more reviews this way",onClick:()=>v&&C(v.key),children:h.jsx(pt,{name:"chevronRight"})})]}),K>=0&&h.jsxs("span",{className:"faint",children:[K+1," of ",ue.length," awaiting"]}),h.jsx("span",{className:"faint crumb-next",children:v?`next: ${v.pr.title}`:K>=0?"last in the queue":""}),h.jsx("span",{className:"grow"}),h.jsx("a",{href:"#/settings",className:"topbar-link",title:"settings","aria-label":"settings",children:h.jsx(pt,{name:"settings",size:15})})]})}),h.jsx("div",{className:"review-head",children:h.jsxs("div",{className:"wrap",children:[h.jsxs("div",{className:"review-head-top",children:[h.jsx("a",{className:"review-title",href:i.pr.url,target:"_blank",rel:"noreferrer",children:i.pr.title}),i.verdict&&(J?h.jsxs("span",{className:`chip tone-${jn}`,children:[i.verdict.recommendation.replace("_"," ")," · ",i.verdict.confidence,"%"]}):h.jsxs("button",{className:`chip chip-btn tone-${jn}`,title:"change it, or send — both are at the foot of the page",onClick:()=>{var le;return(le=X.current)==null?void 0:le.scrollIntoView({behavior:"smooth",block:"center"})},children:[i.verdict.recommendation.replace("_"," ")," · ",i.verdict.confidence,"%",h.jsx(pt,{name:"down",size:12})]})),h.jsx("span",{className:"grow"})]}),h.jsxs("div",{className:"review-head-meta",children:[h.jsx("span",{children:i.pr.author}),h.jsx("span",{className:"crumb-sep",children:"·"}),h.jsxs("span",{children:[i.pr.headRefName," → ",i.pr.baseRefName,i.pr.headSha?` · ${i.pr.headSha.slice(0,7)}`:""]}),h.jsx("span",{className:"crumb-sep",children:"·"}),h.jsxs("span",{children:[i.pr.changedFiles,"f ",h.jsxs("span",{className:"add",children:["+",i.pr.additions]})," ",h.jsxs("span",{className:"del",children:["−",i.pr.deletions]})]}),h.jsx("span",{className:"crumb-sep",children:"·"}),h.jsx("span",{title:"Whether the run read a local checkout of the PR head, or the diff alone.",children:Nt}),((Nn=i.run)==null?void 0:Nn.costUsd)!=null&&h.jsxs(h.Fragment,{children:[h.jsx("span",{className:"crumb-sep",children:"·"}),h.jsxs("span",{title:"What this review would cost at API token rates. Riding a Claude subscription, it draws on your usage limits instead.",children:["≈$",i.run.costUsd.toFixed(2)," at API rates"]})]})]})]})})]}),h.jsxs("div",{className:"wrap review-body",children:[h.jsxs("aside",{className:"rail",children:[re.length>0&&h.jsxs(h.Fragment,{children:[h.jsx("div",{className:"lab",children:"walkthrough"}),re.map((le,pe)=>h.jsxs("div",{className:"rail-group",children:[h.jsxs("button",{className:`rail-item${k.has(le.id)?"":" rail-item-on"}`,onClick:()=>Oe(pe),children:[h.jsxs("span",{className:"grow",children:[pe+1," · ",le.title]}),h.jsxs("span",{className:"rail-count",children:[le.files.length,"f"]})]}),i.comments.filter(Re=>Re.chapterId===le.id).map(Re=>h.jsxs("button",{className:`rail-comment${Re.status==="dropped"?" rail-comment-dropped":""}`,title:`${Re.path}${Re.line!=null?`:${Re.line}`:""} — ${Re.body}`,onClick:()=>Se(pe,Re.id),children:[h.jsx("span",{className:`rail-dot tone-${KS(Re,jn)}`,children:"●"}),h.jsxs("span",{className:"rail-comment-loc",children:[Re.path.split("/").pop(),Re.line!=null?`:${Re.drifted?"~":""}${Re.line}`:""]})]},Re.id))]},le.id))]}),h.jsx("div",{className:`lab${re.length>0?" rail-lab":""}`,children:"comments"}),h.jsxs("div",{className:"rail-facts",children:[h.jsxs("div",{children:[i.comments.length-Mn.length," keeping · ",Mn.length," dropped"]}),h.jsxs("div",{children:[Pn.length," inline · ",Jt.length," folded into body"]}),pn>0&&h.jsxs("div",{className:"warn",title:"The code these comments pointed at is gone from the diff.",children:[pn," drifted"]})]}),h.jsx("div",{className:"lab rail-lab",children:"run"}),h.jsxs("div",{className:"rail-facts",children:[h.jsxs("div",{children:["status ",h.jsx("b",{children:i.status})]}),!J&&i.status!=="running"&&h.jsx("button",{className:"link",disabled:m,onClick:()=>Je(!0),children:m?"starting…":i.run?"re-review at head":"review now"})]})]}),h.jsxs("div",{className:"main",children:[h.jsx(tT,{artifact:i,freshness:d,rerunning:m,onRerun:Je}),_&&h.jsxs("div",{className:"freshness",title:_,children:[h.jsx("strong",{children:"Couldn't check GitHub for newer commits."})," This is the review as it was drafted — if the PR has moved on since, nothing here knows it yet."]}),l&&h.jsx("div",{className:"error",children:l}),((Cn=i.run)==null?void 0:Cn.error)&&h.jsxs("div",{className:"error",children:["Run failed: ",i.run.error]}),i.verdict&&h.jsx("div",{className:`card card-why tone-border-${jn}`,children:h.jsxs("div",{className:"card-body",children:[h.jsx("div",{className:"lab",children:"why"}),nb(i)&&h.jsx("div",{className:"sev-counts",title:"Live findings by grade. Only blockers block.",children:nb(i)}),h.jsx(Hl,{className:"prose lead",text:i.verdict.reasoning}),!J&&h.jsxs("button",{className:"btn btn-sm",onClick:()=>ge({target:"verdict",id:null}),children:[h.jsx(pt,{name:"comment"}),"discuss the verdict"]})]})}),h.jsx("div",{className:"card",children:h.jsxs("div",{className:"card-body",children:[h.jsx("div",{className:"lab",children:"summary"}),h.jsx(Hl,{className:"prose lead",text:i.summary||"(no summary)"}),!J&&h.jsxs("button",{className:"btn btn-sm",onClick:()=>ge({target:"summary",id:null}),children:[h.jsx(pt,{name:"comment"}),"discuss the summary"]})]})}),re.map((le,pe)=>h.jsx(QS,{chapter:le,n:pe+1,diff:i.diff,comments:i.comments.filter(Re=>Re.chapterId===le.id),open:!k.has(le.id),onToggle:()=>z(Re=>{const lt=new Set(Re);return lt.has(le.id)?lt.delete(le.id):lt.add(le.id),lt}),onUpdateComment:Fe,onDeleteComment:Rn,onAddComment:it,onDiscuss:J?void 0:ge,readOnly:J,flash:oe,anchorRef:Re=>{Re?F.current.set(le.id,Re):F.current.delete(le.id)}},le.id)),vn.length>0&&h.jsxs("section",{className:"card",children:[h.jsxs("header",{className:"card-head",children:[h.jsx("h2",{children:"unattached comments"}),h.jsx("span",{className:"faint",children:"not part of any chapter — they post in the body"})]}),h.jsx("div",{className:"card-body",children:vn.map(le=>h.jsx(Gb,{comment:le,onUpdate:pe=>Fe(le.id,pe),onDelete:()=>Rn(le.id),onDiscuss:J?void 0:()=>ge({target:"comment",id:le.id}),readOnly:J},le.id))})]}),h.jsx(WS,{artifact:i,reviewKey:s,refs:x,onClearRefs:()=>N([]),onDropRef:le=>N(pe=>pe.filter((Re,lt)=>lt!==le)),onArtifact:r,readOnly:J,inputRef:R}),!J&&Qn&&h.jsxs("div",{className:"verdict-hint",children:[h.jsxs("span",{children:[h.jsx("strong",{children:Qn})," — the verdict was written before those edits."]}),h.jsxs("button",{className:"btn btn-sm",onClick:Ht,disabled:nt,children:[h.jsx(pt,{name:"comment"}),nt?"the reviewer is busy…":"ask the reviewer to re-true it"]})]}),i.verdict&&!J&&h.jsxs("div",{className:"verdict-bar",ref:X,children:[h.jsx("span",{className:"lab",children:"verdict"}),["approve","comment","request_changes"].map(Sn),h.jsx("span",{className:"grow"}),h.jsx("span",{className:"faint",children:"what you send follows this unless you say otherwise"})]}),h.jsx(eT,{artifact:i,reviewKey:s,event:Ce,overridden:ae!=null,onPickEvent:M,sending:q,onSend:Ye,error:se,next:v,onAdvance:j,footer:h.jsxs(h.Fragment,{children:[h.jsx("span",{className:"faint",children:"not sending?"}),!J&&h.jsxs(h.Fragment,{children:[h.jsxs("button",{className:"btn btn-sm",title:v?`Settle it locally and go to ${v.id}`:"Settle it locally and go back to the queue",onClick:()=>At("reviewed"),children:[h.jsx(pt,{name:"check"}),"mark reviewed"]}),h.jsxs("button",{className:"btn btn-sm",title:v?`Leave it and go to ${v.id}`:"Leave it and go back to the queue",onClick:()=>At("skipped"),children:[h.jsx(pt,{name:"skip"}),"skip"]})]}),h.jsxs("a",{className:"btn btn-sm",href:iv(s),children:[h.jsx(pt,{name:"download"}),"export .md"]})]})})]})]})]})}const lb="cerber.notify.seen",wf="cerber.notify",aT=1e4,$b=s=>{try{return localStorage.getItem(s)}catch{return null}},Af=(s,i)=>{try{localStorage.setItem(s,i)}catch{}};function iT(s,i){const r=new Set(i);return Yf(s).filter(l=>!r.has(l.key))}function lT(s){return s.map(i=>i.key)}const sb=s=>`${s.pr.repo}#${s.pr.number}`;function sT(s){if(s.length===0)return null;const i=`cerber:${s.map(u=>u.key).sort().join("|")}`;if(s.length===1){const u=s[0];return{title:`${sb(u)} awaits your review`,body:`${u.pr.title} — ${u.pr.author}`,tag:i,key:u.key}}const r=s.slice(0,3).map(sb),l=s.length-r.length;return{title:`${s.length} PRs await your review`,body:l>0?`${r.join(", ")} and ${l} more`:r.join(", "),tag:i,key:null}}function jl(){return typeof Notification>"u"?"unsupported":Notification.permission==="denied"?"blocked":$b(wf)==="off"?"off":Notification.permission==="granted"?"on":"ask"}async function rT(){const s=jl();if(s==="unsupported"||s==="blocked")return s;if(s==="on")return Af(wf,"off"),jl();if(Af(wf,"on"),Notification.permission==="default")try{await Notification.requestPermission()}catch{}return jl()}function cT(s){const i=new Notification(s.title,{body:s.body,tag:s.tag,icon:"/favicon.svg"});i.onclick=()=>{window.focus(),window.location.hash=s.key?`#/r/${encodeURIComponent(s.key)}`:"#/",i.close()}}function oT(){const s=de.useRef(null);de.useEffect(()=>{let i=!0;const r=$b(lb);if(r)try{const d=JSON.parse(r);Array.isArray(d)&&(s.current=d.filter(g=>typeof g=="string"))}catch{}const l=()=>zf().then(d=>{if(!i)return;const g=s.current;if(s.current=lT(d),Af(lb,JSON.stringify(s.current)),!g||jl()!=="on"||document.visibilityState==="visible"&&document.hasFocus())return;const _=sT(iT(d,g));_&&cT(_)}).catch(()=>{});l();const u=setInterval(l,aT);return()=>{i=!1,clearInterval(u)}},[])}function Yb(){const[s,i]=de.useState(jl);return de.useEffect(()=>{const r=()=>i(jl());return window.addEventListener("focus",r),()=>window.removeEventListener("focus",r)},[]),[s,()=>void rT().then(i)]}const js=s=>{window.location.hash=`#/r/${encodeURIComponent(s)}`};function uT(s){const i=s,r=i==null?void 0:i.tagName;return r==="INPUT"||r==="TEXTAREA"||r==="SELECT"||(i==null?void 0:i.isContentEditable)===!0}function fT({daemon:s}){return s!=null&&s.enabled?h.jsx("div",{className:"daemon-bar",children:h.jsxs("div",{className:"bar-inner",children:[s.pollEnabled?h.jsxs("span",{className:"daemon-live",children:[h.jsx("span",{className:"dot"}),s.repos.length>0?s.repos.join(", "):"PRs awaiting you"," · every"," ",Math.round(s.intervalMs/6e4),"m"]}):h.jsxs("span",{className:"daemon-live",children:[h.jsx("span",{className:"dot dot-off"}),"polling off (settings) — local reviews only"]}),h.jsxs("span",{children:["auto-review ",h.jsx("b",{children:s.autoReview?"on":"off"})]}),h.jsxs("span",{children:["auto-send ",h.jsx("b",{children:s.autoSend==="on"?"on":"shadow"})," ≥",s.autoSendThreshold,"%",s.autoSend==="on"?` · ${s.autoSent} sent`:` · ${s.autoSendCandidates} candidate${s.autoSendCandidates===1?"":"s"}`]}),s.trustedRuns&&h.jsxs("span",{className:"warn",title:"Trust rules are set and auto-review is on: reviews of matching PRs run their code unattended. Settings → Trusted PRs, or serve --no-trust.",children:[h.jsx(pt,{name:"zap",size:12})," trusted PRs run with Bash"]}),h.jsx("span",{className:"grow"}),s.lastPollError?h.jsx("span",{className:"danger",title:s.lastPollError,children:"discovery offline — showing local reviews only"}):h.jsx("span",{className:"faint",children:s.polling?"polling now…":[s.lastSummary?`last: ${s.lastSummary}`:null,s.nextPollAt?`next ${new Date(s.nextPollAt).toLocaleTimeString()}`:null].filter(Boolean).join(" · ")})]})}):null}function dT({r:s,selected:i,onSelect:r,onOpen:l}){const u=Nf(s);return h.jsxs("div",{className:`row${i?" row-on":""}`,onClick:l,onMouseEnter:r,role:"button",tabIndex:-1,children:[h.jsx("span",{className:"col-caret",children:"›"}),h.jsxs("span",{className:"col-slug",title:`${s.pr.owner}/${s.pr.repo}#${s.pr.number}`,children:[s.pr.repo,"#",s.pr.number]}),h.jsx("span",{className:"col-author",title:s.pr.author,children:s.pr.author}),h.jsx("span",{className:"col-title",title:s.pr.title,children:s.pr.title}),s.pr.isDraft&&h.jsx("span",{className:"tag tag-draft",title:"Still a draft — its author asked for your review anyway",children:"draft"}),h.jsx("span",{className:"col-comments",children:s.commentCount||""}),h.jsx("span",{className:"col-size",children:s.pr.changedFiles>0?h.jsxs(h.Fragment,{children:[s.pr.changedFiles,"f ",h.jsxs("span",{className:"add",children:["+",s.pr.additions]})," ",h.jsxs("span",{className:"del",children:["−",s.pr.deletions]})]}):"—"}),h.jsx("span",{className:`col-verdict tone-${u.tone}`,children:u.label}),h.jsx("span",{className:"col-age",children:Ff(s.updatedAt)})]})}function mf({r:s,verdict:i,tag:r,awaiting:l}){const u=l?IS(l):null;return h.jsxs("div",{className:`row row-settled${u?" row-awaiting":""}`,onClick:()=>js(s.key),role:"button",tabIndex:-1,children:[h.jsx("span",{className:"col-caret"}),h.jsxs("span",{className:"col-slug",title:`${s.pr.owner}/${s.pr.repo}#${s.pr.number}`,children:[s.pr.repo,"#",s.pr.number]}),h.jsx("span",{className:"col-author",title:s.pr.author,children:s.pr.author}),h.jsx("span",{className:"col-title",title:s.pr.title,children:s.pr.title}),s.pr.isDraft&&h.jsx("span",{className:"tag tag-draft",title:"Still a draft — its author asked for your review anyway",children:"draft"}),u&&h.jsx("span",{className:"tag tag-awaiting",title:u.title,children:u.label}),r&&h.jsx("span",{className:"tag tag-done",children:r}),h.jsx("span",{className:"col-comments",children:s.commentCount||""}),h.jsx("span",{className:"col-size",children:s.pr.changedFiles>0?`${s.pr.changedFiles}f`:"—"}),h.jsx("span",{className:"col-verdict",children:i}),h.jsx("span",{className:"col-age",children:Ff(s.updatedAt)})]})}function rb({autoFocus:s}){const[i,r]=de.useState(""),[l,u]=de.useState(!1),[d,g]=de.useState(null),_=p=>{p.preventDefault();const m=i.trim();!m||l||(u(!0),g(null),J_(m).then(b=>js(b.key)).catch(b=>{g(String(b instanceof Error?b.message:b)),u(!1)}))};return h.jsxs("form",{className:"pull",onSubmit:_,children:[h.jsx(pt,{name:"plus"}),h.jsx("input",{className:"pull-input",value:i,autoFocus:s,disabled:l,onChange:p=>{r(p.target.value),d&&g(null)},placeholder:"paste a PR URL to review it","aria-label":"Review any PR by URL"}),i.trim()&&h.jsx("button",{className:"btn btn-small",type:"submit",disabled:l,children:l?"starting…":"review"}),d&&h.jsx("span",{className:"pull-error",children:d})]})}function gf({open:s,onToggle:i,label:r,children:l}){return h.jsxs("div",{className:"drawer",children:[h.jsxs("button",{className:"drawer-toggle",onClick:i,children:[h.jsx("span",{className:"faint",children:s?"▾":"▸"})," ",r]}),s&&h.jsx("div",{className:"drawer-rows",children:l})]})}function hT(){var ue;const[s,i]=de.useState(null),[r,l]=de.useState(null),[u,d]=de.useState(null),[g,_]=de.useState(!1),[p,m]=de.useState(!1),[b,E]=de.useState(!1),[T,x]=de.useState(new Set),[N,R]=de.useState("all"),[k,z]=de.useState(null),$=de.useRef(!0);de.useEffect(()=>($.current=!0,()=>{$.current=!1}),[]);const G=()=>{zf().then(K=>$.current&&i(K)).catch(K=>$.current&&d(String(K))),X_().then(K=>$.current&&l(K)).catch(()=>{})};de.useEffect(()=>{G();const K=setInterval(G,1e4);return()=>clearInterval(K)},[]);const F=s??[],X=de.useMemo(()=>Ul(F.filter(lc)),[F]),ne=de.useMemo(()=>F.filter(K=>!lc(K)),[F]),ae=de.useMemo(()=>Ul(ne.filter(K=>!Bb.includes(K.status))),[ne]),M=de.useMemo(()=>Ul(ne.filter(K=>K.status==="reviewed"||K.status==="skipped")),[ne]),q=de.useMemo(()=>Ul(ne.filter(K=>K.status==="sent")),[ne]),V=de.useMemo(()=>US(F,r),[F,r]),se=de.useMemo(()=>N==="all"?ae:ae.filter(K=>Wg(K)===N),[ae,N]),_e=de.useMemo(()=>Jg.reduce((K,fe)=>(K[fe]=fe==="all"?ae.length:ae.filter(v=>Wg(v)===fe).length,K),{all:0,awaiting:0,drafted:0,sent:0}),[ae]),oe=se.find(K=>K.key===k)??se[0]??null,De=K=>{x(fe=>new Set(fe).add(K)),_b(K).then(()=>G()).catch(fe=>$.current&&d(String(fe))).finally(()=>{$.current&&x(fe=>{const v=new Set(fe);return v.delete(K),v})})};if(de.useEffect(()=>{const K=fe=>{if(uT(fe.target)||fe.metaKey||fe.ctrlKey||fe.altKey)return;const v=se.findIndex(j=>j.key===(oe==null?void 0:oe.key)),C=j=>{const re=se[Math.min(se.length-1,Math.max(0,v+j))];re&&z(re.key)};fe.key==="j"||fe.key==="ArrowDown"?(fe.preventDefault(),C(1)):fe.key==="k"||fe.key==="ArrowUp"?(fe.preventDefault(),C(-1)):fe.key==="Enter"&&oe?js(oe.key):fe.key==="r"&&oe&&oe.status!=="sent"&&oe.status!=="running"&&De(oe.key)};return window.addEventListener("keydown",K),()=>window.removeEventListener("keydown",K)},[se,oe==null?void 0:oe.key]),u&&!s)return h.jsx("p",{className:"error",children:u});if(!s)return h.jsx("p",{className:"muted pad",children:"Loading…"});const ge=ae.reduce((K,fe)=>K+(fe.costUsd??0),0),U=oe?GS(oe,r):null,J=oe?T.has(oe.key):!1;return h.jsxs(h.Fragment,{children:[h.jsx(fT,{daemon:r}),u&&h.jsx("div",{className:"wrap",children:h.jsx("p",{className:"error",children:u})}),ae.length===0?h.jsx("div",{className:"wrap",children:h.jsxs("div",{className:"empty",children:[r!=null&&r.enabled?V.length>0?h.jsxs(h.Fragment,{children:[h.jsx("p",{children:jS(V)}),h.jsxs("button",{className:"btn",onClick:()=>{M.some(K=>Pi(K,V))&&E(!0),q.some(K=>Pi(K,V))&&m(!0),X.some(K=>Pi(K,V))&&_(!0)},children:[h.jsx(pt,{name:"arrowRight"}),"show ",V.length===1?"it":`all ${V.length}`]})]}):h.jsxs("p",{children:["Inbox empty — nothing awaits your review.",r.lastPollError?" (Discovery is offline; check gh auth.)":""]}):h.jsx("p",{children:"No reviews yet."}),h.jsx(rb,{autoFocus:!0})]})}):h.jsxs("div",{className:"wrap",children:[h.jsxs("div",{className:"tabs",children:[Jg.map(K=>h.jsxs("button",{className:`tab${N===K?" tab-on":""}`,onClick:()=>R(K),children:[K," ",h.jsx("span",{className:"tab-count",children:_e[K]})]},K)),h.jsx("span",{className:"grow"}),h.jsx(rb,{}),ge>0&&h.jsxs("span",{className:"faint",title:"What these reviews would cost at API token rates. Riding a Claude subscription, they draw on your usage limits instead.",children:["≈$",ge.toFixed(2)," at API rates"]})]}),h.jsxs("div",{className:"table-head",children:[h.jsx("span",{className:"col-caret"}),h.jsx("span",{className:"col-slug",children:"repo#pr"}),h.jsx("span",{className:"col-author",children:"author"}),h.jsx("span",{className:"col-title",children:"title"}),h.jsx("span",{className:"col-comments",children:"c"}),h.jsx("span",{className:"col-size",children:"size"}),h.jsx("span",{className:"col-verdict",children:"verdict"}),h.jsx("span",{className:"col-age",children:"upd"})]}),se.length===0?h.jsxs("p",{className:"muted pad",children:["Nothing ",N," right now."]}):se.map(K=>h.jsx(dT,{r:K,selected:K.key===(oe==null?void 0:oe.key),onSelect:()=>z(K.key),onOpen:()=>js(K.key)},K.key)),oe&&U&&h.jsxs("div",{className:"cursor-strip",children:[h.jsxs("div",{className:"cursor-strip-body",children:[h.jsxs("div",{className:"lab",children:[oe.pr.owner,"/",oe.pr.repo,"#",oe.pr.number,oe.pr.headRefName&&oe.pr.baseRefName?` · ${oe.pr.headRefName} → ${oe.pr.baseRefName}`:""]}),h.jsx("p",{className:"prose",children:U.reasoning}),h.jsxs("div",{className:"cursor-strip-meta",children:[U.meta.join(" · "),((ue=oe.sent)==null?void 0:ue.url)&&h.jsxs(h.Fragment,{children:[" · ",h.jsx("a",{href:oe.sent.url,target:"_blank",rel:"noreferrer",children:"view on GitHub"})]})]})]}),h.jsxs("div",{className:"cursor-strip-actions",children:[h.jsxs("button",{className:"btn-primary",onClick:()=>js(oe.key),children:[h.jsx(pt,{name:"arrowRight"}),"open review ",h.jsx(Vi,{children:"↵"})]}),h.jsxs("div",{className:"cursor-strip-row",children:[h.jsxs("button",{className:"btn",disabled:oe.status==="sent"||oe.status==="skipped",title:"Set the local queue status to skipped. Nothing is sent.",onClick:()=>bf(oe.key,{status:"skipped"}).then(G).catch(K=>d(String(K))),children:[h.jsx(pt,{name:"skip"}),"skip"]}),h.jsxs("button",{className:"btn",disabled:J||oe.status==="running"||oe.status==="sent",title:oe.status==="sent"?"This review was sent — it is a record now.":"Draft a review of the PR's current head.",onClick:()=>De(oe.key),children:[h.jsx(pt,{name:"rerun"}),J?"starting…":oe.status==="awaiting"||oe.status==="failed"?"review now":"re-review"]})]})]})]})]}),(M.length>0||q.length>0||X.length>0)&&h.jsxs("div",{className:"wrap drawers",children:[M.length>0&&h.jsx(gf,{open:b,onToggle:()=>E(!b),label:`settled — ${M.length} you marked reviewed or skipped${hf(M,V)}`,children:M.map(K=>h.jsx(mf,{r:K,verdict:Nf(K).label,tag:K.status,awaiting:Pi(K,V)?df(K,r):null},K.key))}),q.length>0&&h.jsx(gf,{open:p,onToggle:()=>m(!p),label:`sent — ${q.length} review${q.length===1?"":"s"} on GitHub${hf(q,V)}`,children:q.map(K=>h.jsx(mf,{r:K,verdict:Nf(K).label,awaiting:Pi(K,V)?df(K,r):null},K.key))}),X.length>0&&h.jsx(gf,{open:g,onToggle:()=>_(!g),label:`archived — ${X.length} merged/closed PR${X.length===1?"":"s"}${hf(X,V)}`,children:X.map(K=>{var fe;return h.jsx(mf,{r:K,verdict:((fe=K.pr.state)==null?void 0:fe.toLowerCase())??"",awaiting:Pi(K,V)?df(K,r):null},K.key)})})]})]})}const pT=[{rule:"@acme/*",meaning:"anyone in the acme org"},{rule:"@acme/devs",meaning:"anyone on that GitHub team"},{rule:"@teammate",meaning:"one person, by login"},{rule:"!@acme/contractors",meaning:"deny — beats every rule above"}],cb=s=>s instanceof Error?s.message:String(s),mT={unsupported:"this browser has no notifications to give",blocked:"this browser has blocked notifications for cerber",off:"tell me when a PR lands in the queue",ask:"tell me when a PR lands in the queue (the browser will ask first)",on:"tell me when a PR lands in the queue"};function gT(){const[s,i]=Yb(),[r,l]=de.useState(null),[u,d]=de.useState(""),[g,_]=de.useState(null),[p,m]=de.useState(!1);de.useEffect(()=>{lv().then(l).catch(E=>_(cb(E)))},[]);const b=E=>{m(!0),_(null),E.then(l).catch(T=>_(cb(T))).finally(()=>m(!1))};return g&&!r?h.jsx("p",{className:"error",children:g}):r?h.jsxs("div",{className:"settings",children:[h.jsx("a",{href:"#/",className:"back",children:"← queue"}),h.jsx("h1",{children:"Inbox"}),h.jsx("p",{children:"By default cerber keeps the queue itself: it polls GitHub for PRs awaiting your review and drafts a review for each, so they're ready when you open the cockpit. Nothing is ever sent without you. Changes apply on the next poll — no restart."}),h.jsxs("label",{className:"inbox-toggle",children:[h.jsx("input",{type:"checkbox",checked:r.daemon.poll,disabled:p,onChange:E=>b($m({poll:E.target.checked}))})," ","poll GitHub for PRs awaiting my review (every ",r.daemon.intervalMinutes,"m)"]}),h.jsxs("label",{className:"inbox-toggle",children:[h.jsx("input",{type:"checkbox",checked:r.daemon.autoReview,disabled:p||!r.daemon.poll,onChange:E=>b($m({autoReview:E.target.checked}))})," ","draft a review automatically (off: awaiting PRs wait for a click)"]}),h.jsxs("p",{className:"muted",children:["Interval, concurrency and a repo filter live in ",h.jsx("code",{children:r.path})," under"," ",h.jsx("code",{children:"daemon"})," — hand-editable, like everything here."]}),h.jsx("h1",{children:"Notifications"}),h.jsx("p",{children:"The cockpit spends its day as a background tab, so it tells you when a PR lands in the queue: one desktop notification per poll. A single arrival names the PR and opens that review when you click it; several landing at once fold into one notification that opens the queue. Nothing is announced while you're looking straight at the cockpit, and a PR this browser has already seen never arrives twice."}),h.jsxs("label",{className:"inbox-toggle",children:[h.jsx("input",{type:"checkbox",checked:s==="on",disabled:s==="unsupported"||s==="blocked",onChange:i})," ",mT[s]]}),h.jsxs("p",{className:"muted",children:["This one is per-browser, not per-machine: the permission belongs to this browser, so the switch lives here rather than in ",h.jsx("code",{children:r.path}),". Granting it is the browser's prompt, not cerber's — a browser that has blocked cerber has to be un-blocked from the padlock next to the address bar."]}),h.jsx("h1",{children:"Trusted PRs"}),h.jsxs("p",{children:["Every review reads the PR's code. A ",h.jsx("strong",{children:"trusted"})," review may also run it — the test that covers the change, a typecheck, ",h.jsx("code",{children:"git log"})," — so it can report what happened instead of guessing. That means executing code from the PR on this machine, the same as checking the branch out and running the tests yourself."]}),h.jsxs("p",{className:"muted",children:["Trust the people, not the code: nothing about a PR can earn it. Rules live in"," ",h.jsx("code",{children:r.path}),"."]}),h.jsx("p",{className:"muted",children:'Only people can be trusted — there is no way to trust a repository. Anyone can open a PR against a repo you own, so trusting the repo would trust them too. Membership is checked against GitHub when the review runs, and a check that fails counts as "not a member".'}),h.jsxs("div",{className:"trust-add",children:[h.jsx("input",{value:u,"aria-label":"Trust rule",placeholder:"@org/*, @org/team, @login, !deny",disabled:p,onChange:E=>d(E.target.value),onKeyDown:E=>{E.key==="Enter"&&!p&&u.trim()&&(b(_u(u.trim())),d(""))}}),h.jsx("button",{className:"btn",disabled:p||!u.trim(),onClick:()=>{b(_u(u.trim())),d("")},children:"trust"})]}),g&&h.jsx("p",{className:"error",children:g}),r.trust.length===0?h.jsx("p",{className:"muted",children:"Nobody is trusted yet — every review reads the checkout and runs nothing."}):h.jsx("table",{className:"trust-table",children:h.jsx("tbody",{children:r.trust.map(E=>h.jsxs("tr",{className:E.denies?"trust-deny":void 0,children:[h.jsx("td",{children:h.jsx("code",{children:E.rule})}),h.jsx("td",{className:"muted",children:E.explanation}),h.jsx("td",{className:"trust-actions",children:h.jsx("button",{className:"btn",disabled:p,onClick:()=>b(_u(E.rule,!0)),children:"remove"})})]},E.rule))})}),h.jsx("h2",{children:"Rule syntax"}),h.jsx("table",{className:"trust-table",children:h.jsx("tbody",{children:pT.map(E=>h.jsxs("tr",{children:[h.jsx("td",{children:h.jsx("code",{children:E.rule})}),h.jsx("td",{className:"muted",children:E.meaning})]},E.rule))})}),h.jsxs("p",{className:"muted",children:["A trusted review is handed no GitHub credentials — no token, no ",h.jsx("code",{children:"gh"})," login, no git or ssh identity — so a push fails to authenticate rather than being merely discouraged. It is not a sandbox though: it has ",h.jsx("code",{children:"Bash"})," and this machine, so trust people you would let run their branch here anyway. The daemon reviews unattended, so with rules set it runs matching PRs' code with nobody watching — start it with ",h.jsx("code",{children:"--no-trust"})," to prevent that."]})]}):h.jsx("p",{className:"muted",children:"Loading…"})}function ob(){const s=window.location.hash,i=s.match(/^#\/r\/(.+)$/);return i?{view:"detail",key:decodeURIComponent(i[1])}:s==="#/settings"?{view:"settings"}:{view:"queue"}}const ub={unsupported:"",blocked:"This browser blocked notifications for cerber. Turn them back on in its site settings — the padlock next to the address bar.",off:"Notifications off. Click to be told when a PR lands in the queue.",ask:"Click to be told when a PR lands in the queue — the browser will ask you first.",on:"Telling you when a PR lands in the queue. Click to stop."};function bT(){const[s,i]=Yb();return s==="unsupported"?null:h.jsx("button",{className:`topbar-link bell bell-${s}`,title:ub[s],"aria-label":ub[s],"aria-pressed":s==="on",onClick:i,children:h.jsx(pt,{name:s==="on"?"bell":"bellOff",size:15})})}function yT(){const[s,i]=de.useState(ob());return oT(),de.useEffect(()=>{const r=()=>i(ob());return window.addEventListener("hashchange",r),()=>window.removeEventListener("hashchange",r)},[]),s.view==="detail"?h.jsx("div",{className:"app",children:h.jsx(nT,{reviewKey:s.key})}):h.jsxs("div",{className:"app",children:[h.jsx("header",{className:"topbar",children:h.jsxs("div",{className:"bar-inner",children:[h.jsxs("a",{href:"#/",className:"brand",children:[h.jsx(l0,{}),"cerber"]}),h.jsx("span",{className:"tagline",children:"nothing reaches GitHub until you say so"}),h.jsx("span",{className:"grow"}),s.view==="queue"&&h.jsxs(h.Fragment,{children:[h.jsxs("span",{className:"hint",children:[h.jsx(Vi,{children:"j"}),h.jsx(Vi,{children:"k"}),"move"]}),h.jsxs("span",{className:"hint",children:[h.jsx(Vi,{children:"↵"}),"open"]}),h.jsxs("span",{className:"hint",children:[h.jsx(Vi,{children:"r"}),"review now"]})]}),h.jsx(bT,{}),h.jsx("a",{href:"#/settings",className:"topbar-link",title:"settings","aria-label":"settings",children:h.jsx(pt,{name:"settings",size:15})})]})}),s.view==="queue"?h.jsx(hT,{}):h.jsx(gT,{})]})}i_.createRoot(document.getElementById("root")).render(h.jsx(JE.StrictMode,{children:h.jsx(yT,{})}));
|
|
236
|
+
`).length)}):h.jsx(Hl,{className:"comment-body prose",text:Z_(s.body,s.severity??null)})]})}function PS({files:s,chapterId:i,onAdd:r}){const[l,u]=de.useState(!1),[d,g]=de.useState(s[0]??""),[_,p]=de.useState(""),[m,b]=de.useState("");return l?h.jsxs("div",{className:"add-comment",children:[h.jsxs("div",{className:"add-comment-row",children:[h.jsx("select",{value:d,onChange:E=>g(E.target.value),children:s.map(E=>h.jsx("option",{value:E,children:E},E))}),h.jsx("input",{placeholder:"line (optional)",value:_,onChange:E=>p(E.target.value.replace(/\D/g,"")),size:12})]}),h.jsx("textarea",{placeholder:"Your comment…",value:m,onChange:E=>b(E.target.value),rows:3}),h.jsxs("div",{className:"add-comment-row",children:[h.jsx("button",{className:"btn",disabled:!m.trim()||!d,onClick:()=>{r({path:d,line:_?Number(_):null,body:m.trim(),chapterId:i}),b(""),p(""),u(!1)},children:"add"}),h.jsx("button",{className:"btn",onClick:()=>u(!1),children:"cancel"})]})]}):h.jsxs("button",{className:"btn btn-dashed",onClick:()=>u(!0),children:[h.jsx(pt,{name:"plus"}),"add a comment of your own"]})}function QS({chapter:s,n:i,diff:r,comments:l,open:u,onToggle:d,onUpdateComment:g,onDeleteComment:_,onAddComment:p,onDiscuss:m,readOnly:b,anchorRef:E,flash:T}){const x=de.useMemo(()=>$_(r,s.files),[r,s.files]),N=de.useMemo(()=>jb(x,l),[x,l]),R=l.filter(z=>!N.includes(z)),k=z=>h.jsx(Gb,{comment:z,onUpdate:$=>g(z.id,$),onDelete:()=>_(z.id),onDiscuss:m&&(()=>m({target:"comment",id:z.id})),readOnly:b,flash:T===z.id},z.id);return h.jsxs("section",{className:"chapter",ref:E,children:[h.jsxs("header",{className:"chapter-head",onClick:d,children:[h.jsx("span",{className:"faint",children:u?"▾":"▸"}),h.jsxs("h3",{children:[i," · ",s.title]}),h.jsxs("span",{className:"faint chapter-meta",children:[s.files.length," file",s.files.length===1?"":"s",l.length>0?` · ${l.length} comment${l.length===1?"":"s"}`:" · no comments"]}),h.jsx("span",{className:"grow"}),m&&h.jsxs("button",{className:"btn btn-sm",onClick:z=>{z.stopPropagation(),m({target:"chapter",id:s.id})},children:[h.jsx(pt,{name:"comment"}),"discuss"]})]}),u&&h.jsxs("div",{className:"chapter-body",children:[h.jsx(Hl,{className:"prose chapter-explanation",text:s.explanation}),R.map(k),h.jsx(XS,{patch:x,comments:N,renderComment:k}),!b&&s.files.length>0&&h.jsx(PS,{files:s.files,chapterId:s.id,onAdd:p})]})]})}function VS(s,i){switch(s.kind){case"summary":return"rewrote the summary";case"verdict":return`changed the verdict to ${s.verdict.recommendation.replace("_"," ")}`;case"chapter":{const r=i.chapters.find(l=>l.id===s.chapterId);return`rewrote the "${(r==null?void 0:r.title)??s.chapterId}" chapter`}case"comment-edit":{const r=i.comments.find(l=>l.id===s.commentId);return r?`rewrote the comment on ${r.path}:${r.line??"file"}`:"rewrote a comment"}case"comment-drop":{const r=i.comments.find(l=>l.id===s.commentId);return r?`dropped the comment on ${r.path}:${r.line??"file"}`:"dropped a comment"}case"comment-add":return`added a comment on ${s.path}:${s.line??"file"}`}}function xf(s,i){var l;if(s.target==="summary")return"the summary";if(s.target==="verdict")return"the verdict";if(s.target==="chapter")return((l=i.chapters.find(u=>u.id===s.id))==null?void 0:l.title)??"a chapter";const r=i.comments.find(u=>u.id===s.id);return r?`${r.path}:${r.line??"file"}`:"a comment"}function JS({turn:s,artifact:i}){return h.jsxs("div",{className:`chat-turn chat-turn-${s.role}`,children:[h.jsx("div",{className:"chat-turn-who",children:s.role==="user"?"You":"Reviewer"}),s.refs.length>0&&h.jsx("div",{className:"chat-refs",children:s.refs.map((r,l)=>h.jsx("span",{className:"chat-ref",children:xf(r,i)},l))}),h.jsx(Hl,{className:"chat-turn-body prose",text:s.body}),s.revisions.map((r,l)=>h.jsxs("div",{className:"chat-revision",children:["↳ ",VS(r,i)]},l)),s.refused.map((r,l)=>h.jsxs("div",{className:"chat-refusal",title:r.reason,children:["⃠ left alone — ",r.reason]},l))]})}function WS({artifact:s,reviewKey:i,refs:r,onClearRefs:l,onDropRef:u,onArtifact:d,readOnly:g,inputRef:_}){const[p,m]=de.useState(""),[b,E]=de.useState(!1),[T,x]=de.useState(null),N=s.chat??[],R=s.pendingChat??null,k=R!=null&&R.error==null,z=b||k,$=(F,X,ne)=>{!F||z||(E(!0),x(null),vb(i,{message:F,refs:X}).then(ae=>{d(ae),ne==null||ne()}).catch(ae=>x(String(ae.message??ae))).finally(()=>E(!1)))},G=()=>$(p.trim(),r,()=>{m(""),l()});return h.jsxs("section",{className:"card chat-panel",children:[h.jsxs("header",{className:"card-head",children:[h.jsx("h2",{children:"talk to the reviewer"}),h.jsx("span",{className:"faint",children:"it revises the draft as you go · nothing here reaches GitHub — only send does"})]}),h.jsxs("div",{className:"card-body",children:[(N.length>0||R)&&h.jsxs("div",{className:"chat-log",children:[N.map(F=>h.jsx(JS,{turn:F,artifact:s},F.id)),R&&h.jsxs("div",{className:"chat-turn chat-turn-user chat-turn-pending",children:[h.jsx("div",{className:"chat-turn-who",children:"You"}),R.refs.length>0&&h.jsx("div",{className:"chat-refs",children:R.refs.map((F,X)=>h.jsx("span",{className:"chat-ref",children:xf(F,s)},X))}),h.jsx(Hl,{className:"chat-turn-body prose",text:R.message}),R.error?h.jsxs("div",{className:"chat-failed",children:["✕ this turn didn't finish — ",R.error,!g&&h.jsxs(h.Fragment,{children:[" ",h.jsx("button",{className:"btn btn-sm",onClick:()=>$(R.message,R.refs),children:"ask again"})," ",h.jsx("button",{className:"btn btn-sm",onClick:()=>ev(i).then(d).catch(F=>x(String(F.message??F))),children:"dismiss"})]})]}):h.jsx("div",{className:"chat-waiting",children:R.progress.length>0?h.jsx("ol",{className:"chat-progress",children:R.progress.map((F,X)=>h.jsx("li",{className:X===R.progress.length-1?"now":void 0,children:F},X))}):"Thinking… it re-reads the code before answering."})]})]}),g?h.jsx("p",{className:"faint",children:"This review was sent — the conversation is kept as a record, but it can't continue."}):h.jsxs(h.Fragment,{children:[r.length>0&&h.jsxs("div",{className:"chat-refs chat-refs-pending",children:[h.jsx("span",{className:"faint",children:"about:"}),r.map((F,X)=>h.jsxs("button",{className:"chat-ref",onClick:()=>u(X),title:"Remove",children:[xf(F,s)," ✕"]},X))]}),h.jsx("textarea",{ref:_,className:"chat-input",rows:2,value:p,disabled:b,placeholder:N.length===0?"the summary restates the author's claims — say what you actually verified":"say more…",onChange:F=>m(F.target.value),onKeyDown:F=>{F.key==="Enter"&&(F.metaKey||F.ctrlKey)&&G()}}),h.jsxs("div",{className:"chat-actions",children:[h.jsxs("button",{className:"btn btn-dark",onClick:G,disabled:z||p.trim()==="",children:[h.jsx(pt,{name:"send"}),k?"waiting for the answer…":b?"sending…":"send message",h.jsx(Vi,{children:"⌘↵"})]}),h.jsx("span",{className:"grow"}),s.preChat&&h.jsx("button",{className:"btn btn-sm",disabled:z,title:"Put the review back the way the AI first wrote it. The conversation stays.",onClick:()=>{E(!0),tv(i).then(d).catch(F=>x(String(F.message??F))).finally(()=>E(!1))},children:"reset the review"})]})]}),T&&h.jsx("div",{className:"error",children:T})]})]})}function eT({artifact:s,reviewKey:i,event:r,overridden:l,onPickEvent:u,sending:d,onSend:g,error:_,footer:p,next:m,onAdvance:b}){var G;const[E,T]=de.useState(null),[x,N]=de.useState(null),[R,k]=de.useState(!1),[z,$]=de.useState(!1);return de.useEffect(()=>{R&&nv(i,r).then(F=>{T(F),N(null)}).catch(F=>N(String(F)))},[i,r,s,R]),s.sent?h.jsxs(h.Fragment,{children:[h.jsxs("div",{className:"sent-strip",children:[h.jsxs("strong",{children:["✔ sent as ",Wr[s.sent.event]]}),h.jsxs("span",{className:"faint",children:[new Date(s.sent.at).toLocaleString()," · ",ab(s),s.sent.auto?" · auto-sent by the daemon":""]}),h.jsx("span",{className:"grow"}),s.sent.url&&h.jsxs("a",{href:s.sent.url,target:"_blank",rel:"noreferrer",children:["view on GitHub ",h.jsx(pt,{name:"external"})]}),h.jsxs("button",{className:"btn",onClick:b,children:[m?`next review: ${m.pr.repo}#${m.pr.number}`:"back to the queue",h.jsx(pt,{name:"arrowRight"})]})]}),h.jsx("div",{className:"post-actions",children:p})]}):h.jsxs("div",{className:"send-panel",children:[h.jsxs("div",{className:"send-top",children:[h.jsx("span",{className:"lab",children:"send to github"}),h.jsx("span",{className:"grow"}),h.jsx("span",{className:"faint",children:ab(s)}),h.jsxs("button",{className:"link",onClick:()=>k(!R),children:[h.jsx(pt,{name:"eye"}),R?"hide what gets posted":"see what gets posted"]})]}),h.jsxs("div",{className:"send-action",children:[h.jsxs("button",{className:`send-btn tone-bg-${tb[r]}`,disabled:d,onClick:g,children:[h.jsx(pt,{name:r==="APPROVE"?"approve":r==="COMMENT"?"comment":"changes",size:15}),d?"sending…":`${Wr[r]} on ${s.id}`," ",h.jsx(Vi,{children:"s"})]}),h.jsxs("div",{className:"send-as",children:[h.jsxs("button",{className:"send-as-toggle",onClick:()=>$(!z),children:["as ",h.jsx("b",{children:Wr[r]})," ",z?"▴":"▾"]}),h.jsx("div",{className:"send-as-why",children:l?`you chose this — the verdict says ${((G=s.verdict)==null?void 0:G.recommendation.replace("_"," "))??"nothing"}`:"follows the verdict"}),z&&h.jsx("div",{className:"menu",children:["REQUEST_CHANGES","COMMENT","APPROVE"].map(F=>h.jsxs("button",{className:`menu-item${r===F?" menu-item-on":""}`,onClick:()=>{u(F),$(!1)},children:[h.jsx("span",{className:`tone-${tb[F]}`,children:h.jsx(pt,{name:F==="APPROVE"?"approve":F==="COMMENT"?"comment":"changes"})}),h.jsx("span",{className:"grow",children:Wr[F]}),h.jsx("span",{className:"menu-check",children:r===F?"✓":""})]},F))})]})]}),_&&h.jsx("p",{className:"error",children:_}),R&&(x?h.jsx("p",{className:"error",children:x}):E?h.jsx("pre",{className:"body-preview",children:E.body}):h.jsx("p",{className:"faint",children:"building the body…"})),h.jsx("div",{className:"send-footer",children:p})]})}function tT({artifact:s,freshness:i,rerunning:r,onRerun:l}){const u=s.status==="running",d=(i==null?void 0:i.prState)??s.pr.state,g=d==="CLOSED"||d==="MERGED",_=s.refresh,p=i!=null&&i.changed?_:null;return u?h.jsxs("div",{className:"freshness freshness-running",children:[h.jsx("strong",{children:"Re-reviewing…"})," Claude is reading the current head. This page updates itself when the run lands — it takes a few minutes."]}):!g&&!p?null:h.jsxs("div",{className:"freshness",children:[g&&h.jsxs("div",{children:[h.jsxs("strong",{children:["This PR is ",d==="MERGED"?"merged":"closed","."]})," A review can still be sent, but nobody is waiting for it."]}),p&&h.jsxs("div",{children:[h.jsx("strong",{children:"The PR moved on since this review."})," Now at"," ",h.jsx("code",{children:p.toSha.slice(0,7)}),", reviewed at"," ",h.jsx("code",{children:p.fromSha.slice(0,7)}),"."," ",p.moved>0&&h.jsxs(h.Fragment,{children:[p.moved," comment(s) followed the code. "]}),p.drifted>0&&h.jsxs(h.Fragment,{children:[p.drifted," could not — the code they point at is gone, so they will post in the review body instead of inline."," "]}),"The summary and verdict still describe the commit that was reviewed.",!s.sent&&h.jsx("button",{className:"btn btn-sm",disabled:r,onClick:()=>l(),children:r?"starting…":"re-review at the new head"})]})]})}function nT({reviewKey:s}){var sn,kn,Tn,Nn,Cn;const[i,r]=de.useState(null),[l,u]=de.useState(null),[d,g]=de.useState(null),[_,p]=de.useState(null),[m,b]=de.useState(!1),[E,T]=de.useState([]),[x,N]=de.useState([]),R=de.useRef(null),[k,z]=de.useState(new Set),[$,G]=de.useState(0),F=de.useRef(new Map),X=de.useRef(null),ne=de.useRef(null),[ae,M]=de.useState(null),[q,V]=de.useState(!1),[se,_e]=de.useState(null),[oe,De]=de.useState(null);de.useEffect(()=>{if(!oe)return;const le=setTimeout(()=>De(null),1800);return()=>clearTimeout(le)},[oe]);const ge=le=>{var pe,Re;N(lt=>lt.some(Vn=>Vn.target===le.target&&Vn.id===le.id)?lt:[...lt,le]),(pe=R.current)==null||pe.scrollIntoView({behavior:"smooth",block:"center"}),(Re=R.current)==null||Re.focus()};de.useEffect(()=>{let le=!1;return window.scrollTo({top:0}),g(null),p(null),M(null),_e(null),Gm(s).then(pe=>{if(!le)return r(pe),W_(s).then(Re=>{le||(g(Re),Re.changed&&r(Re.artifact))}).catch(Re=>!le&&p(String(Re.message??Re)))}).catch(pe=>!le&&u(String(pe))),()=>{le=!0}},[s]),de.useEffect(()=>{zf().then(T).catch(()=>{})},[]),de.useEffect(()=>{const le=ne.current;if(!le)return;const pe=()=>document.documentElement.style.setProperty("--top-h",`${le.offsetHeight}px`);pe();const Re=new ResizeObserver(pe);return Re.observe(le),()=>{Re.disconnect(),document.documentElement.style.removeProperty("--top-h")}},[i==null?void 0:i.id]);const U=(i==null?void 0:i.pendingChat)!=null&&i.pendingChat.error==null;de.useEffect(()=>{if((i==null?void 0:i.status)!=="running"&&!U)return;const le=setInterval(()=>{Gm(s).then(pe=>{r(pe),pe.status!=="running"&&b(!1)}).catch(()=>{})},3e3);return()=>clearInterval(le)},[i==null?void 0:i.status,U,s]);const J=(i==null?void 0:i.sent)!=null,ue=de.useMemo(()=>Yf(E),[E]),K=ue.findIndex(le=>le.key===s),fe=(K>0?ue[K-1]:null)??null,v=(K>=0&&K<ue.length-1?ue[K+1]:null)??null,C=le=>{window.location.hash=`#/r/${encodeURIComponent(le)}`},j=()=>{v?C(v.key):window.location.hash="#/"},re=de.useMemo(()=>{if(!i)return[];const le=K_(i.diff,i.chapters);return le.length>0?[...i.chapters,{id:"__other",title:"other changes",explanation:"Files not assigned to any chapter by the AI.",files:le}]:i.chapters},[i]),ve=le=>{const pe=re[le];return pe?(G(le),z(Re=>{const lt=new Set(Re);return lt.delete(pe.id),lt}),pe):null},Oe=le=>{var Re;const pe=ve(le);pe&&((Re=F.current.get(pe.id))==null||Re.scrollIntoView({behavior:"smooth",block:"start"}))},Se=(le,pe)=>{ve(le),De(pe),qb(pe)},Ce=ae??$S(i==null?void 0:i.verdict),Le=(sn=i==null?void 0:i.verdict)==null?void 0:sn.recommendation;de.useEffect(()=>M(null),[Le]),de.useEffect(()=>_e(null),[Ce]);const Ye=()=>{!i||i.sent||q||(V(!0),_e(null),av(s,Ce).then(r).catch(le=>_e(String(le.message??le))).finally(()=>V(!1)))};if(de.useEffect(()=>{const le=pe=>{ZS(pe.target)||pe.metaKey||pe.ctrlKey||pe.altKey||(pe.key==="["&&fe?C(fe.key):pe.key==="]"&&v?C(v.key):pe.key==="n"?Oe(Math.min(re.length-1,$+1)):pe.key==="N"?Oe(Math.max(0,$-1)):pe.key==="s"&&!J&&Ye())};return window.addEventListener("keydown",le),()=>window.removeEventListener("keydown",le)},[fe==null?void 0:fe.key,v==null?void 0:v.key,re,$,J,Ce,i==null?void 0:i.sent,q]),l&&!i)return h.jsx("p",{className:"error pad",children:l});if(!i)return h.jsx("p",{className:"muted pad",children:"Loading…"});const Je=le=>{b(!0),u(null),_b(s,le).then(r).catch(pe=>{u(String(pe)),b(!1)})},Me=le=>le.then(r).catch(pe=>u(String(pe))),At=le=>bf(s,{status:le}).then(j).catch(pe=>u(String(pe))),Fe=(le,pe)=>Me(P_(s,le,pe)),Rn=le=>Me(V_(s,le)),it=le=>Me(Q_(s,le)),vn=i.comments.filter(le=>!le.chapterId||!re.some(pe=>pe.id===le.chapterId)),{inline:Pn,folded:Jt,dropped:Mn}=Hb(i),pn=i.comments.filter(le=>le.status!=="dropped"&&le.drifted).length,jn=i.verdict?ib[i.verdict.recommendation]:"none",Qn=YS(i),nt=i.pendingChat!=null&&i.pendingChat.error==null,Ht=()=>Me(vb(s,{message:"Some findings were dropped, edited or re-graded after this review was drafted. Update the summary and verdict to match what still stands — do not re-review the whole PR.",refs:[{target:"verdict",id:null}]})),Nt=(kn=i.run)!=null&&kn.trusted?"ran the code":(Tn=i.run)!=null&&Tn.withSource?"read the full source":i.run?"read the diff only":"no run yet",Sn=le=>{var pe;return h.jsxs("button",{className:`verdict-pick${((pe=i.verdict)==null?void 0:pe.recommendation)===le?` verdict-pick-on tone-${ib[le]}`:""}`,title:le==="approve"?"Approve":le==="comment"?"Comment without blocking":"Request changes",onClick:()=>Me(bf(s,{verdictRecommendation:le})),children:[h.jsx(pt,{name:FS[le]}),le.replace("_"," ")]},le)};return h.jsxs("div",{className:"review",children:[h.jsxs("div",{className:"review-top",ref:ne,children:[h.jsx("div",{className:"crumb",children:h.jsxs("div",{className:"bar-inner",children:[h.jsx("a",{href:"#/",children:"← queue"}),h.jsx("span",{className:"crumb-sep",children:"/"}),h.jsx("span",{className:"crumb-slug",children:i.id}),h.jsxs("span",{className:"walk",children:[h.jsx("button",{className:"walk-btn",disabled:!fe,title:fe?`[ — ${fe.id}: ${fe.pr.title}`:"no more reviews this way",onClick:()=>fe&&C(fe.key),children:h.jsx(pt,{name:"chevronLeft"})}),h.jsx("button",{className:"walk-btn",disabled:!v,title:v?`] — ${v.id}: ${v.pr.title}`:"no more reviews this way",onClick:()=>v&&C(v.key),children:h.jsx(pt,{name:"chevronRight"})})]}),K>=0&&h.jsxs("span",{className:"faint",children:[K+1," of ",ue.length," awaiting"]}),h.jsx("span",{className:"faint crumb-next",children:v?`next: ${v.pr.title}`:K>=0?"last in the queue":""}),h.jsx("span",{className:"grow"}),h.jsx("a",{href:"#/settings",className:"topbar-link",title:"settings","aria-label":"settings",children:h.jsx(pt,{name:"settings",size:15})})]})}),h.jsx("div",{className:"review-head",children:h.jsxs("div",{className:"wrap",children:[h.jsxs("div",{className:"review-head-top",children:[h.jsx("a",{className:"review-title",href:i.pr.url,target:"_blank",rel:"noreferrer",children:i.pr.title}),i.verdict&&(J?h.jsxs("span",{className:`chip tone-${jn}`,children:[i.verdict.recommendation.replace("_"," ")," · ",i.verdict.confidence,"%"]}):h.jsxs("button",{className:`chip chip-btn tone-${jn}`,title:"change it, or send — both are at the foot of the page",onClick:()=>{var le;return(le=X.current)==null?void 0:le.scrollIntoView({behavior:"smooth",block:"center"})},children:[i.verdict.recommendation.replace("_"," ")," · ",i.verdict.confidence,"%",h.jsx(pt,{name:"down",size:12})]})),h.jsx("span",{className:"grow"})]}),h.jsxs("div",{className:"review-head-meta",children:[h.jsx("span",{children:i.pr.author}),h.jsx("span",{className:"crumb-sep",children:"·"}),h.jsxs("span",{children:[i.pr.headRefName," → ",i.pr.baseRefName,i.pr.headSha?` · ${i.pr.headSha.slice(0,7)}`:""]}),h.jsx("span",{className:"crumb-sep",children:"·"}),h.jsxs("span",{children:[i.pr.changedFiles,"f ",h.jsxs("span",{className:"add",children:["+",i.pr.additions]})," ",h.jsxs("span",{className:"del",children:["−",i.pr.deletions]})]}),h.jsx("span",{className:"crumb-sep",children:"·"}),h.jsx("span",{title:"Whether the run read a local checkout of the PR head, or the diff alone.",children:Nt}),((Nn=i.run)==null?void 0:Nn.costUsd)!=null&&h.jsxs(h.Fragment,{children:[h.jsx("span",{className:"crumb-sep",children:"·"}),h.jsxs("span",{title:"What this review would cost at API token rates. Riding a Claude subscription, it draws on your usage limits instead.",children:["≈$",i.run.costUsd.toFixed(2)," at API rates"]})]})]})]})})]}),h.jsxs("div",{className:"wrap review-body",children:[h.jsxs("aside",{className:"rail",children:[re.length>0&&h.jsxs(h.Fragment,{children:[h.jsx("div",{className:"lab",children:"walkthrough"}),re.map((le,pe)=>h.jsxs("div",{className:"rail-group",children:[h.jsxs("button",{className:`rail-item${k.has(le.id)?"":" rail-item-on"}`,onClick:()=>Oe(pe),children:[h.jsxs("span",{className:"grow",children:[pe+1," · ",le.title]}),h.jsxs("span",{className:"rail-count",children:[le.files.length,"f"]})]}),i.comments.filter(Re=>Re.chapterId===le.id).map(Re=>h.jsxs("button",{className:`rail-comment${Re.status==="dropped"?" rail-comment-dropped":""}`,title:`${Re.path}${Re.line!=null?`:${Re.line}`:""} — ${Re.body}`,onClick:()=>Se(pe,Re.id),children:[h.jsx("span",{className:`rail-dot tone-${KS(Re,jn)}`,children:"●"}),h.jsxs("span",{className:"rail-comment-loc",children:[Re.path.split("/").pop(),Re.line!=null?`:${Re.drifted?"~":""}${Re.line}`:""]})]},Re.id))]},le.id))]}),h.jsx("div",{className:`lab${re.length>0?" rail-lab":""}`,children:"comments"}),h.jsxs("div",{className:"rail-facts",children:[h.jsxs("div",{children:[i.comments.length-Mn.length," keeping · ",Mn.length," dropped"]}),h.jsxs("div",{children:[Pn.length," inline · ",Jt.length," folded into body"]}),pn>0&&h.jsxs("div",{className:"warn",title:"The code these comments pointed at is gone from the diff.",children:[pn," drifted"]})]}),h.jsx("div",{className:"lab rail-lab",children:"run"}),h.jsxs("div",{className:"rail-facts",children:[h.jsxs("div",{children:["status ",h.jsx("b",{children:i.status})]}),!J&&i.status!=="running"&&h.jsx("button",{className:"link",disabled:m,onClick:()=>Je(!0),children:m?"starting…":i.run?"re-review at head":"review now"})]})]}),h.jsxs("div",{className:"main",children:[h.jsx(tT,{artifact:i,freshness:d,rerunning:m,onRerun:Je}),_&&h.jsxs("div",{className:"freshness",title:_,children:[h.jsx("strong",{children:"Couldn't check GitHub for newer commits."})," This is the review as it was drafted — if the PR has moved on since, nothing here knows it yet."]}),l&&h.jsx("div",{className:"error",children:l}),((Cn=i.run)==null?void 0:Cn.error)&&h.jsxs("div",{className:"error",children:["Run failed: ",i.run.error]}),h.jsx("div",{className:"card",children:h.jsxs("div",{className:"card-body",children:[h.jsx("div",{className:"lab",children:"summary"}),h.jsx(Hl,{className:"prose lead",text:i.summary||"(no summary)"}),!J&&h.jsxs("button",{className:"btn btn-sm",onClick:()=>ge({target:"summary",id:null}),children:[h.jsx(pt,{name:"comment"}),"discuss the summary"]})]})}),i.verdict&&h.jsx("div",{className:`card card-why tone-border-${jn}`,children:h.jsxs("div",{className:"card-body",children:[h.jsx("div",{className:"lab",children:"why"}),nb(i)&&h.jsx("div",{className:"sev-counts",title:"Live findings by grade. Only blockers block.",children:nb(i)}),h.jsx(Hl,{className:"prose lead",text:i.verdict.reasoning}),!J&&h.jsxs("button",{className:"btn btn-sm",onClick:()=>ge({target:"verdict",id:null}),children:[h.jsx(pt,{name:"comment"}),"discuss the verdict"]})]})}),re.map((le,pe)=>h.jsx(QS,{chapter:le,n:pe+1,diff:i.diff,comments:i.comments.filter(Re=>Re.chapterId===le.id),open:!k.has(le.id),onToggle:()=>z(Re=>{const lt=new Set(Re);return lt.has(le.id)?lt.delete(le.id):lt.add(le.id),lt}),onUpdateComment:Fe,onDeleteComment:Rn,onAddComment:it,onDiscuss:J?void 0:ge,readOnly:J,flash:oe,anchorRef:Re=>{Re?F.current.set(le.id,Re):F.current.delete(le.id)}},le.id)),vn.length>0&&h.jsxs("section",{className:"card",children:[h.jsxs("header",{className:"card-head",children:[h.jsx("h2",{children:"unattached comments"}),h.jsx("span",{className:"faint",children:"not part of any chapter — they post in the body"})]}),h.jsx("div",{className:"card-body",children:vn.map(le=>h.jsx(Gb,{comment:le,onUpdate:pe=>Fe(le.id,pe),onDelete:()=>Rn(le.id),onDiscuss:J?void 0:()=>ge({target:"comment",id:le.id}),readOnly:J},le.id))})]}),h.jsx(WS,{artifact:i,reviewKey:s,refs:x,onClearRefs:()=>N([]),onDropRef:le=>N(pe=>pe.filter((Re,lt)=>lt!==le)),onArtifact:r,readOnly:J,inputRef:R}),!J&&Qn&&h.jsxs("div",{className:"verdict-hint",children:[h.jsxs("span",{children:[h.jsx("strong",{children:Qn})," — the verdict was written before those edits."]}),h.jsxs("button",{className:"btn btn-sm",onClick:Ht,disabled:nt,children:[h.jsx(pt,{name:"comment"}),nt?"the reviewer is busy…":"ask the reviewer to re-true it"]})]}),i.verdict&&!J&&h.jsxs("div",{className:"verdict-bar",ref:X,children:[h.jsx("span",{className:"lab",children:"verdict"}),["approve","comment","request_changes"].map(Sn),h.jsx("span",{className:"grow"}),h.jsx("span",{className:"faint",children:"what you send follows this unless you say otherwise"})]}),h.jsx(eT,{artifact:i,reviewKey:s,event:Ce,overridden:ae!=null,onPickEvent:M,sending:q,onSend:Ye,error:se,next:v,onAdvance:j,footer:h.jsxs(h.Fragment,{children:[h.jsx("span",{className:"faint",children:"not sending?"}),!J&&h.jsxs(h.Fragment,{children:[h.jsxs("button",{className:"btn btn-sm",title:v?`Settle it locally and go to ${v.id}`:"Settle it locally and go back to the queue",onClick:()=>At("reviewed"),children:[h.jsx(pt,{name:"check"}),"mark reviewed"]}),h.jsxs("button",{className:"btn btn-sm",title:v?`Leave it and go to ${v.id}`:"Leave it and go back to the queue",onClick:()=>At("skipped"),children:[h.jsx(pt,{name:"skip"}),"skip"]})]}),h.jsxs("a",{className:"btn btn-sm",href:iv(s),children:[h.jsx(pt,{name:"download"}),"export .md"]})]})})]})]})]})}const lb="cerber.notify.seen",wf="cerber.notify",aT=1e4,$b=s=>{try{return localStorage.getItem(s)}catch{return null}},Af=(s,i)=>{try{localStorage.setItem(s,i)}catch{}};function iT(s,i){const r=new Set(i);return Yf(s).filter(l=>!r.has(l.key))}function lT(s){return s.map(i=>i.key)}const sb=s=>`${s.pr.repo}#${s.pr.number}`;function sT(s){if(s.length===0)return null;const i=`cerber:${s.map(u=>u.key).sort().join("|")}`;if(s.length===1){const u=s[0];return{title:`${sb(u)} awaits your review`,body:`${u.pr.title} — ${u.pr.author}`,tag:i,key:u.key}}const r=s.slice(0,3).map(sb),l=s.length-r.length;return{title:`${s.length} PRs await your review`,body:l>0?`${r.join(", ")} and ${l} more`:r.join(", "),tag:i,key:null}}function jl(){return typeof Notification>"u"?"unsupported":Notification.permission==="denied"?"blocked":$b(wf)==="off"?"off":Notification.permission==="granted"?"on":"ask"}async function rT(){const s=jl();if(s==="unsupported"||s==="blocked")return s;if(s==="on")return Af(wf,"off"),jl();if(Af(wf,"on"),Notification.permission==="default")try{await Notification.requestPermission()}catch{}return jl()}function cT(s){const i=new Notification(s.title,{body:s.body,tag:s.tag,icon:"/favicon.svg"});i.onclick=()=>{window.focus(),window.location.hash=s.key?`#/r/${encodeURIComponent(s.key)}`:"#/",i.close()}}function oT(){const s=de.useRef(null);de.useEffect(()=>{let i=!0;const r=$b(lb);if(r)try{const d=JSON.parse(r);Array.isArray(d)&&(s.current=d.filter(g=>typeof g=="string"))}catch{}const l=()=>zf().then(d=>{if(!i)return;const g=s.current;if(s.current=lT(d),Af(lb,JSON.stringify(s.current)),!g||jl()!=="on"||document.visibilityState==="visible"&&document.hasFocus())return;const _=sT(iT(d,g));_&&cT(_)}).catch(()=>{});l();const u=setInterval(l,aT);return()=>{i=!1,clearInterval(u)}},[])}function Yb(){const[s,i]=de.useState(jl);return de.useEffect(()=>{const r=()=>i(jl());return window.addEventListener("focus",r),()=>window.removeEventListener("focus",r)},[]),[s,()=>void rT().then(i)]}const js=s=>{window.location.hash=`#/r/${encodeURIComponent(s)}`};function uT(s){const i=s,r=i==null?void 0:i.tagName;return r==="INPUT"||r==="TEXTAREA"||r==="SELECT"||(i==null?void 0:i.isContentEditable)===!0}function fT({daemon:s}){return s!=null&&s.enabled?h.jsx("div",{className:"daemon-bar",children:h.jsxs("div",{className:"bar-inner",children:[s.pollEnabled?h.jsxs("span",{className:"daemon-live",children:[h.jsx("span",{className:"dot"}),s.repos.length>0?s.repos.join(", "):"PRs awaiting you"," · every"," ",Math.round(s.intervalMs/6e4),"m"]}):h.jsxs("span",{className:"daemon-live",children:[h.jsx("span",{className:"dot dot-off"}),"polling off (settings) — local reviews only"]}),h.jsxs("span",{children:["auto-review ",h.jsx("b",{children:s.autoReview?"on":"off"})]}),h.jsxs("span",{children:["auto-send ",h.jsx("b",{children:s.autoSend==="on"?"on":"shadow"})," ≥",s.autoSendThreshold,"%",s.autoSend==="on"?` · ${s.autoSent} sent`:` · ${s.autoSendCandidates} candidate${s.autoSendCandidates===1?"":"s"}`]}),s.trustedRuns&&h.jsxs("span",{className:"warn",title:"Trust rules are set and auto-review is on: reviews of matching PRs run their code unattended. Settings → Trusted PRs, or serve --no-trust.",children:[h.jsx(pt,{name:"zap",size:12})," trusted PRs run with Bash"]}),h.jsx("span",{className:"grow"}),s.lastPollError?h.jsx("span",{className:"danger",title:s.lastPollError,children:"discovery offline — showing local reviews only"}):h.jsx("span",{className:"faint",children:s.polling?"polling now…":[s.lastSummary?`last: ${s.lastSummary}`:null,s.nextPollAt?`next ${new Date(s.nextPollAt).toLocaleTimeString()}`:null].filter(Boolean).join(" · ")})]})}):null}function dT({r:s,selected:i,onSelect:r,onOpen:l}){const u=Nf(s);return h.jsxs("div",{className:`row${i?" row-on":""}`,onClick:l,onMouseEnter:r,role:"button",tabIndex:-1,children:[h.jsx("span",{className:"col-caret",children:"›"}),h.jsxs("span",{className:"col-slug",title:`${s.pr.owner}/${s.pr.repo}#${s.pr.number}`,children:[s.pr.repo,"#",s.pr.number]}),h.jsx("span",{className:"col-author",title:s.pr.author,children:s.pr.author}),h.jsx("span",{className:"col-title",title:s.pr.title,children:s.pr.title}),s.pr.isDraft&&h.jsx("span",{className:"tag tag-draft",title:"Still a draft — its author asked for your review anyway",children:"draft"}),h.jsx("span",{className:"col-comments",children:s.commentCount||""}),h.jsx("span",{className:"col-size",children:s.pr.changedFiles>0?h.jsxs(h.Fragment,{children:[s.pr.changedFiles,"f ",h.jsxs("span",{className:"add",children:["+",s.pr.additions]})," ",h.jsxs("span",{className:"del",children:["−",s.pr.deletions]})]}):"—"}),h.jsx("span",{className:`col-verdict tone-${u.tone}`,children:u.label}),h.jsx("span",{className:"col-age",children:Ff(s.updatedAt)})]})}function mf({r:s,verdict:i,tag:r,awaiting:l}){const u=l?IS(l):null;return h.jsxs("div",{className:`row row-settled${u?" row-awaiting":""}`,onClick:()=>js(s.key),role:"button",tabIndex:-1,children:[h.jsx("span",{className:"col-caret"}),h.jsxs("span",{className:"col-slug",title:`${s.pr.owner}/${s.pr.repo}#${s.pr.number}`,children:[s.pr.repo,"#",s.pr.number]}),h.jsx("span",{className:"col-author",title:s.pr.author,children:s.pr.author}),h.jsx("span",{className:"col-title",title:s.pr.title,children:s.pr.title}),s.pr.isDraft&&h.jsx("span",{className:"tag tag-draft",title:"Still a draft — its author asked for your review anyway",children:"draft"}),u&&h.jsx("span",{className:"tag tag-awaiting",title:u.title,children:u.label}),r&&h.jsx("span",{className:"tag tag-done",children:r}),h.jsx("span",{className:"col-comments",children:s.commentCount||""}),h.jsx("span",{className:"col-size",children:s.pr.changedFiles>0?`${s.pr.changedFiles}f`:"—"}),h.jsx("span",{className:"col-verdict",children:i}),h.jsx("span",{className:"col-age",children:Ff(s.updatedAt)})]})}function rb({autoFocus:s}){const[i,r]=de.useState(""),[l,u]=de.useState(!1),[d,g]=de.useState(null),_=p=>{p.preventDefault();const m=i.trim();!m||l||(u(!0),g(null),J_(m).then(b=>js(b.key)).catch(b=>{g(String(b instanceof Error?b.message:b)),u(!1)}))};return h.jsxs("form",{className:"pull",onSubmit:_,children:[h.jsx(pt,{name:"plus"}),h.jsx("input",{className:"pull-input",value:i,autoFocus:s,disabled:l,onChange:p=>{r(p.target.value),d&&g(null)},placeholder:"paste a PR URL to review it","aria-label":"Review any PR by URL"}),i.trim()&&h.jsx("button",{className:"btn btn-small",type:"submit",disabled:l,children:l?"starting…":"review"}),d&&h.jsx("span",{className:"pull-error",children:d})]})}function gf({open:s,onToggle:i,label:r,children:l}){return h.jsxs("div",{className:"drawer",children:[h.jsxs("button",{className:"drawer-toggle",onClick:i,children:[h.jsx("span",{className:"faint",children:s?"▾":"▸"})," ",r]}),s&&h.jsx("div",{className:"drawer-rows",children:l})]})}function hT(){var ue;const[s,i]=de.useState(null),[r,l]=de.useState(null),[u,d]=de.useState(null),[g,_]=de.useState(!1),[p,m]=de.useState(!1),[b,E]=de.useState(!1),[T,x]=de.useState(new Set),[N,R]=de.useState("all"),[k,z]=de.useState(null),$=de.useRef(!0);de.useEffect(()=>($.current=!0,()=>{$.current=!1}),[]);const G=()=>{zf().then(K=>$.current&&i(K)).catch(K=>$.current&&d(String(K))),X_().then(K=>$.current&&l(K)).catch(()=>{})};de.useEffect(()=>{G();const K=setInterval(G,1e4);return()=>clearInterval(K)},[]);const F=s??[],X=de.useMemo(()=>Ul(F.filter(lc)),[F]),ne=de.useMemo(()=>F.filter(K=>!lc(K)),[F]),ae=de.useMemo(()=>Ul(ne.filter(K=>!Bb.includes(K.status))),[ne]),M=de.useMemo(()=>Ul(ne.filter(K=>K.status==="reviewed"||K.status==="skipped")),[ne]),q=de.useMemo(()=>Ul(ne.filter(K=>K.status==="sent")),[ne]),V=de.useMemo(()=>US(F,r),[F,r]),se=de.useMemo(()=>N==="all"?ae:ae.filter(K=>Wg(K)===N),[ae,N]),_e=de.useMemo(()=>Jg.reduce((K,fe)=>(K[fe]=fe==="all"?ae.length:ae.filter(v=>Wg(v)===fe).length,K),{all:0,awaiting:0,drafted:0,sent:0}),[ae]),oe=se.find(K=>K.key===k)??se[0]??null,De=K=>{x(fe=>new Set(fe).add(K)),_b(K).then(()=>G()).catch(fe=>$.current&&d(String(fe))).finally(()=>{$.current&&x(fe=>{const v=new Set(fe);return v.delete(K),v})})};if(de.useEffect(()=>{const K=fe=>{if(uT(fe.target)||fe.metaKey||fe.ctrlKey||fe.altKey)return;const v=se.findIndex(j=>j.key===(oe==null?void 0:oe.key)),C=j=>{const re=se[Math.min(se.length-1,Math.max(0,v+j))];re&&z(re.key)};fe.key==="j"||fe.key==="ArrowDown"?(fe.preventDefault(),C(1)):fe.key==="k"||fe.key==="ArrowUp"?(fe.preventDefault(),C(-1)):fe.key==="Enter"&&oe?js(oe.key):fe.key==="r"&&oe&&oe.status!=="sent"&&oe.status!=="running"&&De(oe.key)};return window.addEventListener("keydown",K),()=>window.removeEventListener("keydown",K)},[se,oe==null?void 0:oe.key]),u&&!s)return h.jsx("p",{className:"error",children:u});if(!s)return h.jsx("p",{className:"muted pad",children:"Loading…"});const ge=ae.reduce((K,fe)=>K+(fe.costUsd??0),0),U=oe?GS(oe,r):null,J=oe?T.has(oe.key):!1;return h.jsxs(h.Fragment,{children:[h.jsx(fT,{daemon:r}),u&&h.jsx("div",{className:"wrap",children:h.jsx("p",{className:"error",children:u})}),ae.length===0?h.jsx("div",{className:"wrap",children:h.jsxs("div",{className:"empty",children:[r!=null&&r.enabled?V.length>0?h.jsxs(h.Fragment,{children:[h.jsx("p",{children:jS(V)}),h.jsxs("button",{className:"btn",onClick:()=>{M.some(K=>Pi(K,V))&&E(!0),q.some(K=>Pi(K,V))&&m(!0),X.some(K=>Pi(K,V))&&_(!0)},children:[h.jsx(pt,{name:"arrowRight"}),"show ",V.length===1?"it":`all ${V.length}`]})]}):h.jsxs("p",{children:["Inbox empty — nothing awaits your review.",r.lastPollError?" (Discovery is offline; check gh auth.)":""]}):h.jsx("p",{children:"No reviews yet."}),h.jsx(rb,{autoFocus:!0})]})}):h.jsxs("div",{className:"wrap",children:[h.jsxs("div",{className:"tabs",children:[Jg.map(K=>h.jsxs("button",{className:`tab${N===K?" tab-on":""}`,onClick:()=>R(K),children:[K," ",h.jsx("span",{className:"tab-count",children:_e[K]})]},K)),h.jsx("span",{className:"grow"}),h.jsx(rb,{}),ge>0&&h.jsxs("span",{className:"faint",title:"What these reviews would cost at API token rates. Riding a Claude subscription, they draw on your usage limits instead.",children:["≈$",ge.toFixed(2)," at API rates"]})]}),h.jsxs("div",{className:"table-head",children:[h.jsx("span",{className:"col-caret"}),h.jsx("span",{className:"col-slug",children:"repo#pr"}),h.jsx("span",{className:"col-author",children:"author"}),h.jsx("span",{className:"col-title",children:"title"}),h.jsx("span",{className:"col-comments",children:"c"}),h.jsx("span",{className:"col-size",children:"size"}),h.jsx("span",{className:"col-verdict",children:"verdict"}),h.jsx("span",{className:"col-age",children:"upd"})]}),se.length===0?h.jsxs("p",{className:"muted pad",children:["Nothing ",N," right now."]}):se.map(K=>h.jsx(dT,{r:K,selected:K.key===(oe==null?void 0:oe.key),onSelect:()=>z(K.key),onOpen:()=>js(K.key)},K.key)),oe&&U&&h.jsxs("div",{className:"cursor-strip",children:[h.jsxs("div",{className:"cursor-strip-body",children:[h.jsxs("div",{className:"lab",children:[oe.pr.owner,"/",oe.pr.repo,"#",oe.pr.number,oe.pr.headRefName&&oe.pr.baseRefName?` · ${oe.pr.headRefName} → ${oe.pr.baseRefName}`:""]}),h.jsx("p",{className:"prose",children:U.reasoning}),h.jsxs("div",{className:"cursor-strip-meta",children:[U.meta.join(" · "),((ue=oe.sent)==null?void 0:ue.url)&&h.jsxs(h.Fragment,{children:[" · ",h.jsx("a",{href:oe.sent.url,target:"_blank",rel:"noreferrer",children:"view on GitHub"})]})]})]}),h.jsxs("div",{className:"cursor-strip-actions",children:[h.jsxs("button",{className:"btn-primary",onClick:()=>js(oe.key),children:[h.jsx(pt,{name:"arrowRight"}),"open review ",h.jsx(Vi,{children:"↵"})]}),h.jsxs("div",{className:"cursor-strip-row",children:[h.jsxs("button",{className:"btn",disabled:oe.status==="sent"||oe.status==="skipped",title:"Set the local queue status to skipped. Nothing is sent.",onClick:()=>bf(oe.key,{status:"skipped"}).then(G).catch(K=>d(String(K))),children:[h.jsx(pt,{name:"skip"}),"skip"]}),h.jsxs("button",{className:"btn",disabled:J||oe.status==="running"||oe.status==="sent",title:oe.status==="sent"?"This review was sent — it is a record now.":"Draft a review of the PR's current head.",onClick:()=>De(oe.key),children:[h.jsx(pt,{name:"rerun"}),J?"starting…":oe.status==="awaiting"||oe.status==="failed"?"review now":"re-review"]})]})]})]})]}),(M.length>0||q.length>0||X.length>0)&&h.jsxs("div",{className:"wrap drawers",children:[M.length>0&&h.jsx(gf,{open:b,onToggle:()=>E(!b),label:`settled — ${M.length} you marked reviewed or skipped${hf(M,V)}`,children:M.map(K=>h.jsx(mf,{r:K,verdict:Nf(K).label,tag:K.status,awaiting:Pi(K,V)?df(K,r):null},K.key))}),q.length>0&&h.jsx(gf,{open:p,onToggle:()=>m(!p),label:`sent — ${q.length} review${q.length===1?"":"s"} on GitHub${hf(q,V)}`,children:q.map(K=>h.jsx(mf,{r:K,verdict:Nf(K).label,awaiting:Pi(K,V)?df(K,r):null},K.key))}),X.length>0&&h.jsx(gf,{open:g,onToggle:()=>_(!g),label:`archived — ${X.length} merged/closed PR${X.length===1?"":"s"}${hf(X,V)}`,children:X.map(K=>{var fe;return h.jsx(mf,{r:K,verdict:((fe=K.pr.state)==null?void 0:fe.toLowerCase())??"",awaiting:Pi(K,V)?df(K,r):null},K.key)})})]})]})}const pT=[{rule:"@acme/*",meaning:"anyone in the acme org"},{rule:"@acme/devs",meaning:"anyone on that GitHub team"},{rule:"@teammate",meaning:"one person, by login"},{rule:"!@acme/contractors",meaning:"deny — beats every rule above"}],cb=s=>s instanceof Error?s.message:String(s),mT={unsupported:"this browser has no notifications to give",blocked:"this browser has blocked notifications for cerber",off:"tell me when a PR lands in the queue",ask:"tell me when a PR lands in the queue (the browser will ask first)",on:"tell me when a PR lands in the queue"};function gT(){const[s,i]=Yb(),[r,l]=de.useState(null),[u,d]=de.useState(""),[g,_]=de.useState(null),[p,m]=de.useState(!1);de.useEffect(()=>{lv().then(l).catch(E=>_(cb(E)))},[]);const b=E=>{m(!0),_(null),E.then(l).catch(T=>_(cb(T))).finally(()=>m(!1))};return g&&!r?h.jsx("p",{className:"error",children:g}):r?h.jsxs("div",{className:"settings",children:[h.jsx("a",{href:"#/",className:"back",children:"← queue"}),h.jsx("h1",{children:"Inbox"}),h.jsx("p",{children:"By default cerber keeps the queue itself: it polls GitHub for PRs awaiting your review and drafts a review for each, so they're ready when you open the cockpit. Nothing is ever sent without you. Changes apply on the next poll — no restart."}),h.jsxs("label",{className:"inbox-toggle",children:[h.jsx("input",{type:"checkbox",checked:r.daemon.poll,disabled:p,onChange:E=>b($m({poll:E.target.checked}))})," ","poll GitHub for PRs awaiting my review (every ",r.daemon.intervalMinutes,"m)"]}),h.jsxs("label",{className:"inbox-toggle",children:[h.jsx("input",{type:"checkbox",checked:r.daemon.autoReview,disabled:p||!r.daemon.poll,onChange:E=>b($m({autoReview:E.target.checked}))})," ","draft a review automatically (off: awaiting PRs wait for a click)"]}),h.jsxs("p",{className:"muted",children:["Interval, concurrency and a repo filter live in ",h.jsx("code",{children:r.path})," under"," ",h.jsx("code",{children:"daemon"})," — hand-editable, like everything here."]}),h.jsx("h1",{children:"Notifications"}),h.jsx("p",{children:"The cockpit spends its day as a background tab, so it tells you when a PR lands in the queue: one desktop notification per poll. A single arrival names the PR and opens that review when you click it; several landing at once fold into one notification that opens the queue. Nothing is announced while you're looking straight at the cockpit, and a PR this browser has already seen never arrives twice."}),h.jsxs("label",{className:"inbox-toggle",children:[h.jsx("input",{type:"checkbox",checked:s==="on",disabled:s==="unsupported"||s==="blocked",onChange:i})," ",mT[s]]}),h.jsxs("p",{className:"muted",children:["This one is per-browser, not per-machine: the permission belongs to this browser, so the switch lives here rather than in ",h.jsx("code",{children:r.path}),". Granting it is the browser's prompt, not cerber's — a browser that has blocked cerber has to be un-blocked from the padlock next to the address bar."]}),h.jsx("h1",{children:"Trusted PRs"}),h.jsxs("p",{children:["Every review reads the PR's code. A ",h.jsx("strong",{children:"trusted"})," review may also run it — the test that covers the change, a typecheck, ",h.jsx("code",{children:"git log"})," — so it can report what happened instead of guessing. That means executing code from the PR on this machine, the same as checking the branch out and running the tests yourself."]}),h.jsxs("p",{className:"muted",children:["Trust the people, not the code: nothing about a PR can earn it. Rules live in"," ",h.jsx("code",{children:r.path}),"."]}),h.jsx("p",{className:"muted",children:'Only people can be trusted — there is no way to trust a repository. Anyone can open a PR against a repo you own, so trusting the repo would trust them too. Membership is checked against GitHub when the review runs, and a check that fails counts as "not a member".'}),h.jsxs("div",{className:"trust-add",children:[h.jsx("input",{value:u,"aria-label":"Trust rule",placeholder:"@org/*, @org/team, @login, !deny",disabled:p,onChange:E=>d(E.target.value),onKeyDown:E=>{E.key==="Enter"&&!p&&u.trim()&&(b(_u(u.trim())),d(""))}}),h.jsx("button",{className:"btn",disabled:p||!u.trim(),onClick:()=>{b(_u(u.trim())),d("")},children:"trust"})]}),g&&h.jsx("p",{className:"error",children:g}),r.trust.length===0?h.jsx("p",{className:"muted",children:"Nobody is trusted yet — every review reads the checkout and runs nothing."}):h.jsx("table",{className:"trust-table",children:h.jsx("tbody",{children:r.trust.map(E=>h.jsxs("tr",{className:E.denies?"trust-deny":void 0,children:[h.jsx("td",{children:h.jsx("code",{children:E.rule})}),h.jsx("td",{className:"muted",children:E.explanation}),h.jsx("td",{className:"trust-actions",children:h.jsx("button",{className:"btn",disabled:p,onClick:()=>b(_u(E.rule,!0)),children:"remove"})})]},E.rule))})}),h.jsx("h2",{children:"Rule syntax"}),h.jsx("table",{className:"trust-table",children:h.jsx("tbody",{children:pT.map(E=>h.jsxs("tr",{children:[h.jsx("td",{children:h.jsx("code",{children:E.rule})}),h.jsx("td",{className:"muted",children:E.meaning})]},E.rule))})}),h.jsxs("p",{className:"muted",children:["A trusted review is handed no GitHub credentials — no token, no ",h.jsx("code",{children:"gh"})," login, no git or ssh identity — so a push fails to authenticate rather than being merely discouraged. It is not a sandbox though: it has ",h.jsx("code",{children:"Bash"})," and this machine, so trust people you would let run their branch here anyway. The daemon reviews unattended, so with rules set it runs matching PRs' code with nobody watching — start it with ",h.jsx("code",{children:"--no-trust"})," to prevent that."]})]}):h.jsx("p",{className:"muted",children:"Loading…"})}function ob(){const s=window.location.hash,i=s.match(/^#\/r\/(.+)$/);return i?{view:"detail",key:decodeURIComponent(i[1])}:s==="#/settings"?{view:"settings"}:{view:"queue"}}const ub={unsupported:"",blocked:"This browser blocked notifications for cerber. Turn them back on in its site settings — the padlock next to the address bar.",off:"Notifications off. Click to be told when a PR lands in the queue.",ask:"Click to be told when a PR lands in the queue — the browser will ask you first.",on:"Telling you when a PR lands in the queue. Click to stop."};function bT(){const[s,i]=Yb();return s==="unsupported"?null:h.jsx("button",{className:`topbar-link bell bell-${s}`,title:ub[s],"aria-label":ub[s],"aria-pressed":s==="on",onClick:i,children:h.jsx(pt,{name:s==="on"?"bell":"bellOff",size:15})})}function yT(){const[s,i]=de.useState(ob());return oT(),de.useEffect(()=>{const r=()=>i(ob());return window.addEventListener("hashchange",r),()=>window.removeEventListener("hashchange",r)},[]),s.view==="detail"?h.jsx("div",{className:"app",children:h.jsx(nT,{reviewKey:s.key})}):h.jsxs("div",{className:"app",children:[h.jsx("header",{className:"topbar",children:h.jsxs("div",{className:"bar-inner",children:[h.jsxs("a",{href:"#/",className:"brand",children:[h.jsx(l0,{}),"cerber"]}),h.jsx("span",{className:"tagline",children:"nothing reaches GitHub until you say so"}),h.jsx("span",{className:"grow"}),s.view==="queue"&&h.jsxs(h.Fragment,{children:[h.jsxs("span",{className:"hint",children:[h.jsx(Vi,{children:"j"}),h.jsx(Vi,{children:"k"}),"move"]}),h.jsxs("span",{className:"hint",children:[h.jsx(Vi,{children:"↵"}),"open"]}),h.jsxs("span",{className:"hint",children:[h.jsx(Vi,{children:"r"}),"review now"]})]}),h.jsx(bT,{}),h.jsx("a",{href:"#/settings",className:"topbar-link",title:"settings","aria-label":"settings",children:h.jsx(pt,{name:"settings",size:15})})]})}),s.view==="queue"?h.jsx(hT,{}):h.jsx(gT,{})]})}i_.createRoot(document.getElementById("root")).render(h.jsx(JE.StrictMode,{children:h.jsx(yT,{})}));
|
package/web/dist/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
7
7
|
<title>cerber</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-ZJJfADjP.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/assets/index-DjQ8jays.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|