/** Production project-decompiler page. Data is escaped as base64url, not inline script. */ import type { GraphState, ProjectGraph } from "./graph-contract.ts"; import { mergeGraph } from "./graph-contract.ts"; import { graphApprovable } from "./graph-state.ts"; import { escapeHtml } from "./page.ts"; export interface GraphPageData { graph: Omit; state: GraphState; nodes: ReturnType["nodes"]; edges: ReturnType["edges"]; approvable: boolean; } export function graphPageData( graph: ProjectGraph, state: GraphState, ): GraphPageData { const merged = mergeGraph(graph, state.deltas); const { source: _source, ...document } = graph; return { graph: document, state, ...merged, approvable: graphApprovable(graph, state), }; } export function renderGraphPage( graph: ProjectGraph, state: GraphState, ): string { const encoded = Buffer.from( JSON.stringify(graphPageData(graph, state)), "utf8", ).toString("base64url"); return `${escapeHtml(graph.title)}
QRproject decompiler${escapeHtml(graph.scope.toUpperCase())}${escapeHtml(graph.revision.slice(0, 12))}
Drag the background to pan. Drag nodes to arrange. Enhance in place.
analysis ready
`; } export const GRAPH_PAGE_CSS = String.raw` :root{--bg:#181818;--panel:#222;--panel2:#1d1d1d;--line:#444;--ink:#e4e4e4;--muted:#999;--yellow:#ffdd33;--green:#73c990;--red:#ff4c4c;--blue:#9ab7d3;--orange:#ff9d45;--grid:#292929;color-scheme:dark}*{box-sizing:border-box}html,body,[data-app]{width:100%;height:100%;margin:0;overflow:hidden}body{background:var(--bg);color:var(--ink);font:12px/1.45 ui-monospace,"SFMono-Regular",Consolas,monospace}button,input,textarea{font:inherit;color:inherit}button{border:1px solid var(--line);background:#202020;padding:5px 9px;cursor:pointer}button:hover,button:focus-visible{border-color:var(--yellow);color:var(--yellow)}button:disabled{opacity:.4;cursor:not-allowed}.top{height:46px;border-bottom:1px solid var(--line);display:flex;align-items:center;gap:12px;padding:0 11px;background:#202020}.brand{display:grid;place-items:center;width:28px;height:28px;border:1px solid var(--yellow);color:var(--yellow);font-weight:800}.top strong{font-size:13px}.top code,.mode{color:var(--muted)}.mode{border-left:1px solid var(--line);padding-left:12px}.top input{margin-left:auto;width:min(280px,30vw);background:#1b1b1b;border:1px solid var(--line);padding:6px 9px}.shell{height:calc(100% - 70px);display:grid;grid-template-columns:248px 1fr}aside{border-right:1px solid var(--line);background:#202020;overflow:auto;padding-bottom:12px}aside h2{height:36px;margin:0;padding:11px 10px;border-bottom:1px solid var(--line);font-size:10px;text-transform:uppercase;letter-spacing:.12em;color:var(--muted)}.tree{padding:7px 0}.tree-row{display:flex;gap:6px;align-items:center;width:100%;min-height:25px;border:0;padding:3px 10px;text-align:left;color:var(--muted);white-space:nowrap}.tree-guide{position:relative;align-self:stretch;flex:0 0 16px;border-left:1px solid #444}.tree-guide.last:after{content:"";position:absolute;left:0;top:50%;width:12px;border-top:1px solid #555}.tree-row:hover,.tree-row.active{background:#393522;color:var(--yellow)}.tree-row .tree-title{overflow:hidden;text-overflow:ellipsis}.tree-row .marker{margin-left:auto;color:var(--yellow)}.tree-row.changed{color:var(--ink)}.tree-row.inferred{opacity:.68}.legend{margin:14px 9px 0;border:1px solid var(--line);padding:9px;display:grid;gap:7px;color:var(--muted)}.legend span{display:flex;gap:8px}.legend i{width:7px;height:7px;border-radius:50%;margin-top:5px}.legend .changed{background:var(--red)}.legend .impact{background:var(--yellow)}.legend .context{background:#777}.work{min-width:0;min-height:0;position:relative;display:grid;grid-template-rows:36px 34px 1fr}.contextbar{display:flex;align-items:center;gap:12px;border-bottom:1px solid var(--line);padding:0 10px;background:#202020;z-index:4}.contextbar nav{display:flex;min-width:0}.crumb{border:0;padding:3px 4px;background:none;color:var(--muted)}.crumb.current{color:var(--yellow)}.crumb+.crumb:before{content:"/";color:#666;margin-right:8px}.hint{color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.zoom{margin-left:auto;display:flex}.zoom button{padding:3px 7px}.tabs{display:flex;border-bottom:1px solid var(--line);background:#1d1d1d}.tab{border:0;border-right:1px solid var(--line);padding:8px 13px;color:var(--muted)}.tab.active{color:var(--yellow);background:#2c2a1c;border-bottom:1px solid var(--yellow)}.tab-close{margin-left:9px}.viewport{position:relative;overflow:scroll;scrollbar-width:none;background-color:var(--bg);background-image:linear-gradient(var(--grid) 1px,transparent 1px),linear-gradient(90deg,var(--grid) 1px,transparent 1px);background-size:90px 90px;cursor:grab;touch-action:none;overscroll-behavior:none}.viewport::-webkit-scrollbar{display:none}.viewport.panning{cursor:grabbing}.canvas{position:relative;width:200000px;height:200000px;transform:scale(var(--zoom,.82));transform-origin:0 0}.canvas>svg{position:absolute;inset:0;width:100%;height:100%;overflow:visible;pointer-events:none}.edge{stroke:#777;stroke-width:1.4;fill:none}.edge.inferred{stroke:var(--yellow);stroke-dasharray:7 6}.edge-label{fill:var(--muted);font-size:10px}.node{position:absolute;width:290px;background:rgba(34,34,34,.97);border:1px solid var(--line);border-left:3px solid #777;box-shadow:0 3px 10px #0008;user-select:none}.node.overlay-added,.node.overlay-modified,.node.overlay-deleted{border-left-color:var(--red)}.node.overlay-impacted{border-left-color:var(--yellow)}.node.confidence-inferred{border-style:dashed}.node.selected{outline:1px solid var(--yellow)}.node.dragging{z-index:8;box-shadow:0 8px 24px #000}.node.enhancing{outline:2px solid var(--yellow);box-shadow:0 0 24px #ffdd3344}.node.enhancing .kind{animation:pulse .7s ease-in-out infinite alternate}@keyframes pulse{to{opacity:.25}}.node-head{display:flex;gap:7px;align-items:center;padding:7px 9px;border-bottom:1px solid var(--line);cursor:move}.kind{color:var(--yellow)}.node-title{font-weight:700}.node-tag{margin-left:auto;color:var(--muted);font-size:9px;text-transform:uppercase}.node-body{padding:8px}.path{color:var(--green);font-size:10px;overflow-wrap:anywhere}.summary{white-space:pre-wrap;color:#ccc}.evidence{font-size:10px;color:var(--muted);padding:5px 0;border-top:1px solid #333}.code-node{position:relative;margin:18px 0 4px 22px;border:1px solid #4b4b4b;background:#151515;box-shadow:0 4px 14px #0008;user-select:text}.code-node:before{content:"";position:absolute;left:-23px;top:17px;width:22px;border-top:1px solid var(--green)}.code-head{display:flex;gap:10px;align-items:center;padding:6px 9px;border-bottom:1px solid #3a3a3a;color:var(--green)}.code-head .language{margin-left:auto;color:var(--muted);text-transform:uppercase;font-size:9px}.code{margin:0;padding:9px 0;background:#121212;white-space:pre-wrap;overflow-wrap:anywhere;user-select:text;color:#d8d8d8;tab-size:2}.code-line{display:block;min-height:1.45em;padding-right:10px;cursor:pointer}.code-line:hover{background:#ffffff08}.code-line.selected-line{background:#ffdd3312;outline:1px solid #8d7e24}.code-line.has-comment .line-no{color:var(--yellow)}.line-no{display:inline-block;width:42px;margin-right:10px;padding-right:9px;text-align:right;color:#555;border-right:1px solid #303030;user-select:none}.kw{color:var(--yellow);font-weight:600}.str{color:var(--green)}.num{color:var(--orange)}.type{color:var(--blue)}.syntax-comment{color:#777;font-style:italic}.answer{margin-top:7px;padding:7px;border-left:2px solid var(--blue);background:#1a1a1a;white-space:pre-wrap}.comment-thread{margin-top:8px;padding:7px;border-left:2px solid var(--blue);background:#171717}.comment-head{display:flex;gap:8px;margin-bottom:4px;color:var(--muted);font-size:9px}.thread-anchor{border-bottom:1px solid #333;padding-bottom:5px}.thread-message{margin-top:6px;padding:7px;white-space:pre-wrap}.reviewer-message{background:#1d1d1d}.agent-message{margin-left:16px;border-left:2px solid var(--green);background:#151b17}.comment-status{margin-left:auto;text-transform:uppercase}.reviewer-message[data-delivery=queued] .comment-status,.reviewer-message[data-delivery=active] .comment-status{color:var(--yellow)}.reviewer-message[data-delivery=answered] .comment-status{color:var(--green)}.thread-actions{display:flex;gap:5px;margin-top:7px}.thread-actions button{padding:3px 7px;font-size:10px}.line-thread{margin:0 9px 9px}.line-thread .inline-composer,.comment-thread .inline-composer{border-color:#8d7e24}.actions{display:flex;gap:5px;flex-wrap:wrap;margin-top:8px}.actions button{padding:3px 7px;font-size:10px}.actions [data-action=enhance]{border-color:#8d7e24;color:var(--yellow);font-weight:700}.actions [data-action=enhance]:before{content:"+ ";color:var(--yellow)}.inline-composer{margin-top:9px;padding:8px;border:1px solid #555;background:#191919}.inline-composer label{display:flex;justify-content:space-between;margin-bottom:6px;color:var(--yellow)}.inline-composer .shortcut{color:var(--muted);font-size:9px}.inline-composer textarea{display:block;width:100%;min-height:74px;resize:vertical;padding:8px;border:1px solid var(--line);background:#111;user-select:text}.inline-composer .row{margin-top:7px}.children{position:relative;display:grid;grid-template-columns:repeat(var(--child-count,1),minmax(280px,1fr));gap:28px;padding:34px 12px 12px;margin-top:8px;border:1px solid #363636;background:#1a1a1a}.children:before{content:"children";position:absolute;top:9px;left:12px;color:#666;font-size:9px;text-transform:uppercase;letter-spacing:.12em}.children>.node{position:relative!important;left:auto!important;top:auto!important;width:auto}.node.expanded{width:max(720px,calc(var(--child-count,1)*308px + 24px))}.minimap{position:absolute;right:12px;bottom:12px;width:148px;height:96px;padding:0;background:#171717dd;border:1px solid var(--line);z-index:6}.minimap svg{display:block;width:100%;height:100%}.map-world{fill:#222;stroke:#666}.map-node{fill:#777}.map-node.changed{fill:var(--red)}.map-window{fill:#ffdd3317;stroke:var(--yellow);stroke-width:2}footer{height:24px;border-top:1px solid var(--line);display:flex;align-items:center;gap:16px;padding:0 9px;color:var(--muted);background:#202020}footer [data-status]{color:var(--green)}footer button{border:0;padding:2px 8px;color:var(--yellow)}footer [data-send-review]{margin-left:auto;border:1px solid #8d7e24}.overlay{position:fixed;inset:0;background:#000b;display:grid;place-items:center;z-index:20}.overlay[hidden]{display:none}.dialog{width:min(620px,calc(100vw - 30px));max-height:calc(100vh - 30px);overflow:auto;border:1px solid var(--yellow);background:#222;padding:20px;box-shadow:0 15px 60px #000}.dialog h2{margin-top:0}.dialog textarea{width:100%;min-height:130px;resize:vertical;border:1px solid var(--line);background:#181818;padding:10px}.dialog .close{float:right}.feedback{min-height:20px;margin-top:9px;color:var(--muted)}.feedback.error{color:var(--red)}.row{display:flex;gap:9px;margin-top:10px}.closing{position:fixed;right:18px;bottom:18px;z-index:30;display:grid;gap:4px;min-width:300px;padding:14px;border:1px solid var(--yellow);background:#222;box-shadow:0 12px 40px #000}.closing[hidden]{display:none}.closing small{color:var(--muted)}.danger{border-color:var(--red);color:var(--red)}.fullscreen .top,.fullscreen aside,.fullscreen footer{display:none}.fullscreen .shell{height:100%;grid-template-columns:1fr}.fullscreen .work{height:100%}@media(max-width:850px){.shell{grid-template-columns:180px 1fr}.node.expanded{width:580px}.children{grid-template-columns:1fr;--child-count:1!important}.hint{display:none}}@media(max-width:620px){.shell{grid-template-columns:1fr}aside{display:none}.top code{display:none}.node.expanded{width:calc(100vw - 45px)}.minimap{display:none}}@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important}} `; export const GRAPH_PAGE_JS = String.raw` const app=document.querySelector('[data-app]'); let data=JSON.parse(new TextDecoder().decode(Uint8Array.from(atob(app.dataset.payload.replace(/-/g,'+').replace(/_/g,'/')),c=>c.charCodeAt(0)))); const viewport=document.querySelector('[data-viewport]'),canvas=document.querySelector('[data-canvas]'),nodeLayer=document.querySelector('[data-nodes]'),edgeSvg=document.querySelector('[data-edges]'); let zoom=.82,selected=data.graph.roots[0],activeTab='canvas',tabs=[],filter='',positions=new Map(),offsets=new Map(),loadedCode=new Map(),codeOpen=new Set(),enhancing=new Set(),composer=null,manuallyMoved=new Set(); const status=document.querySelector('[data-status]'),WORLD=100000,WORLD_SIZE=200000; function byId(id){return data.nodes.find(n=>n.id===id)} function children(id){return data.nodes.filter(n=>n.parentId===id)} function expanded(id){return data.state.deltas.some(d=>d.parentId===id)} function ancestors(id){const out=[];let n=byId(id);while(n){out.unshift(n);n=n.parentId?byId(n.parentId):null}return out} function visibleRoots(){if(activeTab==='canvas')return data.graph.roots.map(byId).filter(Boolean);const n=byId(activeTab);return n&&expanded(n.id)&&children(n.id).length?children(n.id):n?[n]:[]} function esc(s){return String(s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]))} function highlightLine(line){const pattern=/(\/\/.*$|#.*$|\/\*.*?\*\/|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\x60(?:\\.|[^\x60\\])*\x60|\b(?:async|await|function|class|interface|const|let|var|return|if|else|for|while|throw|new|import|from|export|default|type|struct|impl|fn|pub|match|use|extends|implements|try|catch|finally|switch|case|break|continue)\b|\b(?:true|false|null|undefined|None|Some|Ok|Err)\b|\b\d+(?:\.\d+)?\b|\b[A-Z][A-Za-z0-9_]*\b)/g;let out='',at=0;for(const match of line.matchAll(pattern)){out+=esc(line.slice(at,match.index));const value=match[0];let kind='kw';if(value.startsWith('//')||value.startsWith('#')||value.startsWith('/*'))kind='syntax-comment';else if(value.startsWith('"')||value.startsWith("'")||value.charCodeAt(0)===96)kind='str';else if(/^\d/.test(value))kind='num';else if(/^[A-Z]/.test(value))kind='type';out+=''+esc(value)+'';at=match.index+value.length}return out+esc(line.slice(at))} function highlight(code,start=1,commented=new Set()){return String(code).split('\n').map((line,i)=>{const number=start+i;return ''+number+''+highlightLine(line)+''}).join('')} function messageHtml(message){if(message.author==='agent')return '
Agent
'+esc(message.body)+'
';return '
Reviewer'+esc(message.delivery==='active'?'agent working':message.delivery)+'
'+esc(message.body)+'
'} function composerHtml(n,line,thread){if(!composer||composer.id!==n.id||(composer.line||undefined)!==(line||undefined)||(composer.thread||undefined)!==(thread||undefined))return '';const anchor=line?(n.file||n.title)+':'+line:n.title,editing=composer.mode==='edit';return '
'+(editing?'':'')+'
'} function commentHtml(c){const last=c.messages.at(-1),draft=last?.author==='reviewer'&&last.delivery==='draft',n=byId(c.nodeId);return '
'+esc((c.file?c.file+':':'')+c.lines)+'
'+c.messages.map(messageHtml).join('')+'
'+(draft?'':'')+'
'+composerHtml(n,/^\d+$/.test(c.lines)?c.lines:undefined,c.id)+'
'} function codeNodeHtml(n){if(!codeOpen.has(n.id))return '';const code=n.code||loadedCode.get(n.id);if(!code)return '';const start=Math.max(1,parseInt(String(n.lines||'1').match(/\d+/)?.[0]||'1',10)),lineComments=data.state.comments.filter(c=>c.nodeId===n.id&&/^\d+$/.test(c.lines)),commented=new Set(lineComments.map(c=>c.lines));return '
exact code'+esc(n.file||n.title)+''+esc(n.language||'text')+'
'+highlight(code,start,commented)+'
'+lineComments.map(commentHtml).join('')+(composer?.id===n.id&&composer.line&&!composer.thread?composerHtml(n,composer.line,undefined):'')+'
'} function nodeHtml(n,nested=false){const kids=children(n.id),open=expanded(n.id),busy=enhancing.has(n.id),showCode=codeOpen.has(n.id),answers=data.state.questions.filter(q=>q.nodeId===n.id),comments=data.state.comments.filter(q=>q.nodeId===n.id);return '
'+(busy?'[...]':open?'[-]':n.expandable?'[+]':'fn')+''+esc(n.title)+''+esc(n.kind)+'
'+(n.file?'
'+esc(n.file+':'+n.lines)+'
':'')+'

'+esc(n.summary)+'

'+n.evidence.map(e=>'
'+esc(e.confidence+' '+e.file+':'+e.lines)+'
').join('')+answers.map(a=>'
Q: '+esc(a.question)+'\n'+esc(a.answer)+'
').join('')+comments.filter(c=>!codeOpen.has(n.id)||!/^\d+$/.test(c.lines)).map(commentHtml).join('')+'
'+(n.expandable&&!open?'':'')+''+(n.file?'':'')+'
'+composerHtml(n,undefined)+codeNodeHtml(n)+(open&&kids.length?'
'+kids.map(k=>nodeHtml(k,true)).join('')+'
':'')+'
'} function rootRanks(roots){const ids=new Set(roots.map(n=>n.id)),rank=new Map(roots.map(n=>[n.id,0]));for(let pass=0;pass(rank.get(edge.target)||0)){rank.set(edge.target,next);changed=true}}if(!changed)break}return rank} function arrangeRoots(roots){const rank=rootRanks(roots),levels=new Map();for(const n of roots){const level=rank.get(n.id)||0;if(!levels.has(level))levels.set(level,[]);levels.get(level).push(n)}let y=WORLD;for(const level of [...levels.keys()].sort((a,b)=>a-b)){const nodes=levels.get(level),widths=nodes.map(n=>document.querySelector('[data-nodes]>.node[data-id="'+CSS.escape(n.id)+'"]')?.offsetWidth||290),total=widths.reduce((a,b)=>a+b,0)+Math.max(0,nodes.length-1)*90;let x=WORLD-total/2,maxHeight=0;nodes.forEach((n,i)=>{const el=document.querySelector('[data-nodes]>.node[data-id="'+CSS.escape(n.id)+'"]');if(!manuallyMoved.has(n.id))positions.set(n.id,{x,y});const p=positions.get(n.id);if(el&&p){el.style.left=p.x+'px';el.style.top=p.y+'px';maxHeight=Math.max(maxHeight,el.offsetHeight)}x+=widths[i]+90});y+=maxHeight+150}scheduleGeometry()} function layout(){const roots=visibleRoots().filter(matches);roots.forEach((n,i)=>{if(!positions.has(n.id))positions.set(n.id,{x:WORLD+i*380,y:WORLD})});nodeLayer.innerHTML=roots.map(n=>nodeHtml(n)).join('');for(const el of nodeLayer.children){const p=positions.get(el.dataset.id);el.style.left=p.x+'px';el.style.top=p.y+'px'}document.querySelectorAll('.children>.node').forEach(el=>{const p=offsets.get(el.dataset.id);if(p)el.style.transform='translate('+p.x+'px,'+p.y+'px)'});requestAnimationFrame(()=>arrangeRoots(roots))} function matches(n){if(!filter)return true;const hay=(n.title+' '+n.summary+' '+(n.file||'')).toLowerCase();return hay.includes(filter)||children(n.id).some(matches)} function grow(){let right=WORLD_SIZE,bottom=WORLD_SIZE;document.querySelectorAll('.node').forEach(el=>{if(el.parentElement!==nodeLayer)return;right=Math.max(right,el.offsetLeft+el.offsetWidth+viewport.clientWidth/zoom);bottom=Math.max(bottom,el.offsetTop+el.offsetHeight+viewport.clientHeight/zoom)});canvas.style.width=right+'px';canvas.style.height=bottom+'px';edgeSvg.setAttribute('viewBox','0 0 '+right+' '+bottom)} function drawEdges(){edgeSvg.replaceChildren();const cr=canvas.getBoundingClientRect();for(const e of data.edges){const a=document.querySelector('.node[data-id="'+CSS.escape(e.source)+'"]'),b=document.querySelector('.node[data-id="'+CSS.escape(e.target)+'"]');if(!a||!b)continue;const ar=a.getBoundingClientRect(),br=b.getBoundingClientRect();const x1=(ar.left+ar.width/2-cr.left)/zoom,y1=(ar.top+ar.height-cr.top)/zoom,x2=(br.left+br.width/2-cr.left)/zoom,y2=(br.top-cr.top)/zoom;const path=document.createElementNS('http://www.w3.org/2000/svg','path');path.setAttribute('d','M '+x1+' '+y1+' C '+x1+' '+(y1+40)+' '+x2+' '+(y2-40)+' '+x2+' '+y2);path.setAttribute('class','edge '+e.confidence);edgeSvg.append(path)}} function renderTree(){const tree=document.querySelector('[data-tree]');const rows=[];function add(n,depth){rows.push('');children(n.id).forEach(c=>add(c,depth+1))}data.graph.roots.map(byId).filter(Boolean).forEach(n=>add(n,0));tree.innerHTML=rows.join('')} function renderTabs(){document.querySelector('[data-tabs]').innerHTML=''+tabs.map(id=>'').join('')} function renderContext(){const path=activeTab==='canvas'?(selected?ancestors(selected):[]):ancestors(selected&&ancestors(selected).some(n=>n.id===activeTab)?selected:activeTab);document.querySelector('[data-context]').innerHTML=''+path.map((n,i)=>'').join('')} function render(){renderTree();renderTabs();renderContext();layout();document.querySelector('[data-count]').textContent=data.nodes.length+' nodes / '+data.edges.length+' edges';document.querySelector('[data-review-progress]').textContent=data.nodes.length+' graph claims, '+data.state.comments.length+' comments';document.querySelector('[data-approve]').disabled=!data.approvable||data.state.outcome!=='open';document.querySelector('[data-send-review]').disabled=data.state.comments.length===0||data.state.outcome!=='open'} async function act(payload){status.textContent='working...';const response=await fetch('action',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(payload)});const result=await response.json();if(result.data)data=result.data;if(!response.ok||!result.ok)throw new Error(result.error||'request failed');status.textContent=result.message||'updated';render();return result} function select(id,snap=false){selected=id;render();if(!snap)return;requestAnimationFrame(()=>{const el=document.querySelector('.node[data-id="'+CSS.escape(id)+'"]');if(!el)return;el.scrollIntoView({block:'start',inline:'center'});const nr=el.getBoundingClientRect(),vr=viewport.getBoundingClientRect();viewport.scrollLeft+=nr.left+nr.width/2-(vr.left+vr.width/2);viewport.scrollTop+=nr.top-vr.top-18;renderMapWindow()})} nodeLayer.addEventListener('click',async e=>{const node=e.target.closest('.node');if(!node)return;selected=node.dataset.id;const codeLine=e.target.closest('[data-code-line]');if(codeLine){composer={id:selected,line:codeLine.dataset.codeLine,mode:'new',draft:''};render();requestAnimationFrame(()=>document.querySelector('.node[data-id="'+CSS.escape(selected)+'"] .inline-composer textarea')?.focus());return}const threadButton=e.target.closest('button[data-thread-action]');if(threadButton){const threadId=threadButton.closest('[data-thread-id]').dataset.threadId,thread=data.state.comments.find(c=>c.id===threadId),line=/^\d+$/.test(thread.lines)?thread.lines:undefined,action=threadButton.dataset.threadAction;try{if(action==='queue')await act({action:'queue-comment',node:selected,thread:threadId,message:threadButton.dataset.message});else if(action==='edit'){const message=thread.messages.find(m=>m.id===threadButton.dataset.message);composer={id:selected,line,thread:threadId,message:message.id,mode:'edit',draft:message.body};render()}else if(action==='reply'){composer={id:selected,line,thread:threadId,mode:'reply',draft:''};render()}requestAnimationFrame(()=>document.querySelector('.node[data-id="'+CSS.escape(selected)+'"] .inline-composer textarea')?.focus())}catch(err){status.textContent=err.message;render()}return}const button=e.target.closest('button[data-action]');if(!button){renderTree();renderContext();return}e.stopPropagation();const action=button.dataset.action;try{if(action==='enhance'){enhancing.add(selected);status.textContent='Enhancing '+byId(selected).title+'...';render();try{await act({action:'enhance',node:selected})}finally{enhancing.delete(selected);render()}}else if(action==='code'){if(codeOpen.has(selected)){codeOpen.delete(selected);render()}else{if(!byId(selected).code&&!loadedCode.has(selected)){button.disabled=true;button.textContent='Loading code...';const result=await act({action:'code',node:selected});loadedCode.set(selected,result.code)}codeOpen.add(selected);render()}}else if(action==='comment'){composer=composer?.id===selected&&!composer.line&&!composer.thread?null:{id:selected,mode:'new',draft:''};render();requestAnimationFrame(()=>document.querySelector('.node[data-id="'+CSS.escape(selected)+'"] .inline-composer textarea')?.focus())}else if(action==='focus'){if(!tabs.includes(selected))tabs.push(selected);activeTab=selected;render()}}catch(err){enhancing.delete(selected);status.textContent=err.message;render()}}); nodeLayer.addEventListener('click',e=>{if(!e.target.closest('[data-cancel-composer]'))return;composer=null;render()}); nodeLayer.addEventListener('keydown',e=>{if(e.key==='Enter'&&(e.ctrlKey||e.metaKey)){const form=e.target.closest('[data-composer]');if(form){e.preventDefault();form.requestSubmit(form.querySelector('[data-submit-mode=send],[data-submit-mode=edit]'))}}}); nodeLayer.addEventListener('submit',async e=>{const form=e.target.closest('[data-composer]');if(!form)return;e.preventDefault();const node=form.closest('.node'),box=form.querySelector('textarea'),submitMode=e.submitter?.dataset.submitMode||'send',button=e.submitter,current={id:node.dataset.id,line:form.dataset.line||undefined,thread:form.dataset.thread||undefined,message:form.dataset.message||undefined,mode:form.dataset.mode||'new',draft:box.value};if(button){button.disabled=true;button.textContent=submitMode==='send'?'Queueing...':'Saving...'}status.textContent=button?.textContent||'Saving...';try{const action=current.mode==='edit'?'edit-comment':current.mode==='reply'?(submitMode==='send'?'send-reply':'reply-comment'):(submitMode==='send'?'send-comment':'add-comment');await act({action,node:current.id,comment:box.value,...(current.line?{line:current.line}:{}),...(current.thread?{thread:current.thread}:{}),...(current.message?{message:current.message}:{})});composer=null;render()}catch(err){composer=current;status.textContent=err.message;render();requestAnimationFrame(()=>document.querySelector('.node[data-id="'+CSS.escape(current.id)+'"] .inline-composer textarea')?.focus())}}); function settleCollision(id){const moved=document.querySelector('[data-nodes]>.node[data-id="'+CSS.escape(id)+'"]');if(!moved)return;const a=positions.get(id);for(const other of nodeLayer.children){if(other===moved)continue;const b=positions.get(other.dataset.id);if(a.xb.x&&a.yb.y){b.x=a.x+moved.offsetWidth+28;other.style.left=b.x+'px'}}grow();drawEdges()} let geometryFrame=0;function scheduleGeometry(){if(geometryFrame)return;geometryFrame=requestAnimationFrame(()=>{geometryFrame=0;grow();drawEdges();drawMap()})} let drag=null;nodeLayer.addEventListener('pointerdown',e=>{if(e.button!==0||e.target.closest('button,input,textarea,pre,.inline-composer,.code-node'))return;const el=e.target.closest('.node');if(!el)return;const nested=el.parentElement!==nodeLayer;const current=nested?(offsets.get(el.dataset.id)||{x:0,y:0}):positions.get(el.dataset.id);drag={el,id:el.dataset.id,nested,x:e.clientX,y:e.clientY,p:{...current},pointerId:e.pointerId};el.classList.add('dragging');el.setPointerCapture(e.pointerId);e.preventDefault()});nodeLayer.addEventListener('pointermove',e=>{if(!drag||e.pointerId!==drag.pointerId)return;const p={x:drag.p.x+(e.clientX-drag.x)/zoom,y:drag.p.y+(e.clientY-drag.y)/zoom};if(drag.nested){offsets.set(drag.id,p);drag.el.style.transform='translate('+p.x+'px,'+p.y+'px)'}else{p.x=Math.max(0,p.x);p.y=Math.max(0,p.y);positions.set(drag.id,p);drag.el.style.left=p.x+'px';drag.el.style.top=p.y+'px'}scheduleGeometry()});function endDrag(e){if(!drag||e.pointerId!==drag.pointerId)return;if(!drag.nested){manuallyMoved.add(drag.id);settleCollision(drag.id)}drag.el.classList.remove('dragging');drag=null;scheduleGeometry()}nodeLayer.addEventListener('pointerup',endDrag);nodeLayer.addEventListener('pointercancel',endDrag); function syncGrid(){const size=90*zoom;viewport.style.backgroundSize=size+'px '+size+'px';viewport.style.backgroundPosition=(-viewport.scrollLeft%size)+'px '+(-viewport.scrollTop%size)+'px'} function rebaseWorld(){const edge=5000,shift=50000;let dx=0,dy=0;if(viewport.scrollLeftviewport.scrollWidth-viewport.clientWidth-edge)dx=-shift;if(viewport.scrollTopviewport.scrollHeight-viewport.clientHeight-edge)dy=-shift;if(!dx&&!dy)return;for(const [id,p] of positions){p.x+=dx/zoom;p.y+=dy/zoom;const el=document.querySelector('[data-nodes]>.node[data-id="'+CSS.escape(id)+'"]');if(el){el.style.left=p.x+'px';el.style.top=p.y+'px'}}viewport.scrollLeft+=dx;viewport.scrollTop+=dy;if(pan){pan.l+=dx;pan.t+=dy}scheduleGeometry()} let pan=null;viewport.addEventListener('pointerdown',e=>{if(e.button!==0||e.target.closest('.node,button,input,textarea,.minimap'))return;pan={x:e.clientX,y:e.clientY,l:viewport.scrollLeft,t:viewport.scrollTop,pointerId:e.pointerId};viewport.classList.add('panning');viewport.setPointerCapture(e.pointerId);e.preventDefault()});viewport.addEventListener('pointermove',e=>{if(!pan||e.pointerId!==pan.pointerId)return;viewport.scrollLeft=pan.l-(e.clientX-pan.x);viewport.scrollTop=pan.t-(e.clientY-pan.y);rebaseWorld();e.preventDefault()});function endPan(e){if(!pan||e.pointerId!==pan.pointerId)return;pan=null;viewport.classList.remove('panning')}viewport.addEventListener('pointerup',endPan);viewport.addEventListener('pointercancel',endPan);viewport.addEventListener('lostpointercapture',e=>{if(pan?.pointerId===e.pointerId){pan=null;viewport.classList.remove('panning')}});viewport.addEventListener('scroll',()=>{syncGrid();renderMapWindow()},{passive:true});viewport.addEventListener('wheel',e=>{e.preventDefault();const r=viewport.getBoundingClientRect(),x=e.clientX-r.left,y=e.clientY-r.top,wx=(viewport.scrollLeft+x)/zoom,wy=(viewport.scrollTop+y)/zoom;zoom=Math.max(.45,Math.min(1.5,zoom*(e.deltaY<0?1.1:.9)));canvas.style.setProperty('--zoom',zoom);viewport.scrollLeft=wx*zoom-x;viewport.scrollTop=wy*zoom-y;requestAnimationFrame(()=>{syncGrid();drawEdges();drawMap()})},{passive:false}); document.querySelector('[data-tree]').addEventListener('click',e=>{const row=e.target.closest('[data-tree-id]');if(!row)return;const id=row.dataset.treeId;const exact=tabs.includes(id)?id:null;const ancestor=ancestors(id).reverse().find(n=>tabs.includes(n.id));activeTab=exact||ancestor?.id||'canvas';select(id,true)});document.querySelector('[data-tabs]').addEventListener('click',e=>{const tab=e.target.closest('[data-tab]');if(!tab)return;const id=tab.dataset.tab;if(e.target.closest('[data-close-tab]')){tabs=tabs.filter(x=>x!==id);if(activeTab===id)activeTab='canvas'}else activeTab=id;render()});document.querySelector('[data-context]').addEventListener('click',e=>{const c=e.target.closest('[data-crumb]');if(c)c.dataset.crumb==='canvas'?(activeTab='canvas',render()):select(c.dataset.crumb,true)}); document.querySelector('[data-filter]').addEventListener('input',e=>{filter=e.target.value.trim().toLowerCase();layout()});document.querySelectorAll('[data-zoom]').forEach(b=>b.addEventListener('click',()=>{zoom=Math.max(.45,Math.min(1.5,zoom*(b.dataset.zoom==='+'?1.1:.9)));canvas.style.setProperty('--zoom',zoom);syncGrid();drawEdges();drawMap()}));function centerGraph(){const roots=visibleRoots(),points=roots.map(n=>positions.get(n.id)).filter(Boolean);const x=points.length?points.reduce((sum,p)=>sum+p.x,0)/points.length:WORLD,y=points.length?Math.min(...points.map(p=>p.y)):WORLD;viewport.scrollLeft=x*zoom-viewport.clientWidth/2;viewport.scrollTop=y*zoom-44;syncGrid()} document.querySelector('[data-reset]').addEventListener('click',()=>{zoom=1;canvas.style.setProperty('--zoom',zoom);centerGraph();drawEdges();drawMap()});document.querySelector('[data-full]').addEventListener('click',()=>{app.classList.toggle('fullscreen');requestAnimationFrame(()=>{drawEdges();drawMap()})}); let currentMapBounds={left:WORLD-500,top:WORLD-300,width:1000,height:600};function graphBounds(){const roots=visibleRoots(),boxes=roots.map(n=>{const p=positions.get(n.id),el=document.querySelector('[data-nodes]>.node[data-id="'+CSS.escape(n.id)+'"]');return p&&el?{x:p.x,y:p.y,w:el.offsetWidth,h:el.offsetHeight}:null}).filter(Boolean);if(!boxes.length)return currentMapBounds;const left=Math.min(...boxes.map(b=>b.x))-120,top=Math.min(...boxes.map(b=>b.y))-100,right=Math.max(...boxes.map(b=>b.x+b.w))+120,bottom=Math.max(...boxes.map(b=>b.y+b.h))+100;return {left,top,width:Math.max(1,right-left),height:Math.max(1,bottom-top)}} function drawMap(){const g=document.querySelector('[data-map-nodes]');g.replaceChildren();currentMapBounds=graphBounds();const b=currentMapBounds;for(const n of visibleRoots()){const p=positions.get(n.id),el=document.querySelector('[data-nodes]>.node[data-id="'+CSS.escape(n.id)+'"]');if(!p||!el)continue;const r=document.createElementNS('http://www.w3.org/2000/svg','rect');r.setAttribute('x',1+(p.x-b.left)/b.width*146);r.setAttribute('y',1+(p.y-b.top)/b.height*94);r.setAttribute('width',Math.max(4,el.offsetWidth/b.width*146));r.setAttribute('height',Math.max(4,el.offsetHeight/b.height*94));r.setAttribute('class','map-node '+(n.overlay==='unchanged'||n.overlay==='context'?'':'changed'));g.append(r)}renderMapWindow()} function renderMapWindow(){const r=document.querySelector('[data-map-window]'),b=currentMapBounds,x=1+(viewport.scrollLeft/zoom-b.left)/b.width*146,y=1+(viewport.scrollTop/zoom-b.top)/b.height*94;r.setAttribute('x',Math.max(1,Math.min(147,x)));r.setAttribute('y',Math.max(1,Math.min(95,y)));r.setAttribute('width',Math.min(146,viewport.clientWidth/zoom/b.width*146));r.setAttribute('height',Math.min(94,viewport.clientHeight/zoom/b.height*94))}document.querySelector('[data-minimap]').addEventListener('click',e=>{const r=e.currentTarget.getBoundingClientRect(),b=currentMapBounds,x=b.left+(e.clientX-r.left)/r.width*b.width,y=b.top+(e.clientY-r.top)/r.height*b.height;viewport.scrollLeft=x*zoom-viewport.clientWidth/2;viewport.scrollTop=y*zoom-viewport.clientHeight/2}); let closingPage=false,pollFailures=0,lastState=JSON.stringify(data.state);function closeSoon(title='Review finished.'){if(closingPage)return;closingPage=true;const panel=document.querySelector('[data-closing]'),count=document.querySelector('[data-countdown]'),fallback=document.querySelector('[data-close-fallback]');document.querySelector('[data-closing-title]').textContent=title;panel.hidden=false;let left=3;count.textContent=left;const timer=setInterval(()=>{left-=1;count.textContent=Math.max(0,left);if(left>0)return;clearInterval(timer);window.close();setTimeout(()=>{fallback.textContent='This browser kept the tab open. You can close it now.'},350)},1000)} setInterval(async()=>{if(closingPage)return;try{const response=await fetch('state',{cache:'no-store'});if(!response.ok)throw new Error('closed');const result=await response.json();pollFailures=0;const encoded=JSON.stringify(result.data.state);if(encoded!==lastState){const box=document.querySelector('.inline-composer textarea');if(composer&&box)composer.draft=box.value;data=result.data;lastState=encoded;render()}if(data.state.outcome!=='open')closeSoon()}catch{pollFailures+=1;if(pollFailures>=2)closeSoon('Review closed.')}},750); document.querySelectorAll('[data-close]').forEach(b=>b.addEventListener('click',()=>b.closest('.overlay').hidden=true));document.querySelector('[data-send-review]').addEventListener('click',async()=>{try{await act({action:'send-review'});closeSoon('Review sent.')}catch(err){status.textContent=err.message}});document.querySelector('[data-final]').addEventListener('click',()=>{document.querySelector('[data-review]').hidden=false});document.querySelector('[data-approve]').addEventListener('click',async()=>{const form=document.querySelector('[data-review] form'),feedback=form.querySelector('.feedback');try{await act({action:'approve',comment:form.querySelector('textarea').value});feedback.textContent='Approved.';closeSoon('Review approved.')}catch(err){feedback.textContent=err.message;feedback.className='feedback error'}});document.querySelector('[data-changes]').addEventListener('click',async()=>{const form=document.querySelector('[data-review] form'),body=form.querySelector('textarea').value,feedback=form.querySelector('.feedback');try{await act({action:'request-changes',comment:body});feedback.textContent='Changes requested.';closeSoon('Changes requested.')}catch(err){feedback.textContent=err.message;feedback.className='feedback error'}}); document.addEventListener('keydown',e=>{if(e.key==='Escape'){document.querySelectorAll('.overlay').forEach(x=>x.hidden=true);if(composer){composer=null;render()}}if(e.key==='/'&&!['INPUT','TEXTAREA'].includes(e.target.tagName)){e.preventDefault();document.querySelector('[data-filter]').focus()}});canvas.style.setProperty('--zoom',zoom);render();requestAnimationFrame(()=>{centerGraph();renderMapWindow()}); `;