Git Explorer
GLM 5.3 Flash · full

Browser check: Artifact runtime error. This is a technical check, not a quality review.
Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 35,281 bytes · SHA-256 137642461af1
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Git & Contextual Event Visualizer</title>
<script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
<style>
:root{
--bg:#12141a; --panel:#1a1e28; --panel2:#212633; --line:#2e3547;
--txt:#dfe4ee; --dim:#8b93a7; --acc:#4fc3f7; --acc2:#ffb74d;
--green:#81c784; --red:#e57373; --purple:#ba68c8; --yellow:#fff176;
}
*{box-sizing:border-box;margin:0;padding:0}
body{background:var(--bg);color:var(--txt);font:14px/1.45 'Segoe UI',system-ui,sans-serif;height:100vh;display:flex;overflow:hidden}
#sidebar{width:340px;min-width:340px;background:var(--panel);border-right:1px solid var(--line);display:flex;flex-direction:column;height:100vh}
#sidebar header{padding:14px 16px;border-bottom:1px solid var(--line)}
#sidebar h1{font-size:16px;letter-spacing:.5px}
#sidebar h1 span{color:var(--acc)}
#sidebar header p{color:var(--dim);font-size:11px;margin-top:2px}
#tabs{display:flex;border-bottom:1px solid var(--line)}
#tabs button{flex:1;background:none;border:none;color:var(--dim);padding:9px 4px;cursor:pointer;font-size:12px;border-bottom:2px solid transparent}
#tabs button.active{color:var(--acc);border-bottom-color:var(--acc)}
#panels{flex:1;overflow-y:auto;padding:12px}
.panel{display:none}
.panel.active{display:block}
h3.sec{font-size:12px;text-transform:uppercase;letter-spacing:1px;color:var(--dim);margin:14px 0 8px}
h3.sec:first-child{margin-top:0}
.card{background:var(--panel2);border:1px solid var(--line);border-radius:8px;padding:10px;margin-bottom:8px;cursor:pointer;transition:border-color .15s}
.card:hover{border-color:var(--acc)}
.card.sel{border-color:var(--acc);box-shadow:0 0 0 1px var(--acc)}
.card .t{font-weight:600;font-size:13px;display:flex;justify-content:space-between;align-items:center;gap:6px}
.card .m{color:var(--dim);font-size:11px;margin-top:3px;word-break:break-all}
.card .acts{display:flex;gap:6px;margin-top:7px}
.card .acts button{font-size:10px;padding:3px 8px}
.pill{display:inline-block;font-size:10px;padding:1px 7px;border-radius:9px;background:#2b3245;color:var(--acc)}
.pill.b{color:var(--green)} .pill.t{color:var(--yellow)} .pill.e{color:var(--acc2)}
button{background:#2b3245;color:var(--txt);border:1px solid var(--line);border-radius:6px;padding:6px 12px;cursor:pointer;font-size:12px}
button:hover{border-color:var(--acc);color:var(--acc)}
button.danger:hover{border-color:var(--red);color:var(--red)}
button.primary{background:var(--acc);color:#10222c;border-color:var(--acc);font-weight:600}
button.primary:hover{filter:brightness(1.1);color:#10222c}
.row{display:flex;gap:8px;margin-bottom:8px}
.row>*{flex:1}
label{display:block;font-size:11px;color:var(--dim);margin:8px 0 3px}
input,select,textarea{width:100%;background:#161a23;border:1px solid var(--line);border-radius:6px;color:var(--txt);padding:7px 9px;font-size:13px;font-family:inherit}
textarea{resize:vertical;min-height:60px}
input:focus,select:focus,textarea:focus{outline:none;border-color:var(--acc)}
#main{flex:1;display:flex;flex-direction:column;height:100vh;min-width:0}
#main header{display:flex;align-items:center;gap:14px;padding:10px 18px;border-bottom:1px solid var(--line);background:var(--panel)}
#main header .title{font-weight:600;font-size:14px}
#legend{margin-left:auto;display:flex;gap:14px;font-size:11px;color:var(--dim)}
#legend i{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:4px;vertical-align:-1px}
#graphWrap{flex:1;position:relative;overflow:hidden;background:radial-gradient(ellipse at 50% 30%,#181c26 0%,#101218 100%)}
svg{display:block}
#tooltip{position:absolute;pointer-events:none;background:#0d1017;border:1px solid var(--acc);border-radius:8px;padding:10px 12px;font-size:12px;max-width:300px;display:none;z-index:10;box-shadow:0 6px 24px rgba(0,0,0,.6)}
#tooltip .tt-title{font-weight:600;color:var(--acc2);margin-bottom:4px}
#tooltip .tt-id{color:var(--dim);font-size:10px;font-family:monospace}
#tooltip .tt-msg{margin-top:4px}
#tooltip .tt-link{margin-top:6px;color:var(--acc);font-size:11px}
#timeline{height:170px;border-top:1px solid var(--line);background:var(--panel);overflow:hidden auto;position:relative}
#timeline h4{font-size:11px;text-transform:uppercase;letter-spacing:1px;color:var(--dim);padding:8px 16px 0}
#toast{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:#0d1017;border:1px solid var(--acc);border-radius:8px;padding:10px 18px;font-size:13px;display:none;z-index:20}
dialog{background:var(--panel);color:var(--txt);border:1px solid var(--line);border-radius:12px;padding:20px;min-width:380px;max-width:480px}
dialog::backdrop{background:rgba(0,0,0,.6)}
dialog h2{font-size:16px;margin-bottom:12px;color:var(--acc)}
dialog .btns{display:flex;gap:8px;justify-content:flex-end;margin-top:16px}
.hint{font-size:11px;color:var(--dim);margin-top:8px}
.mono{font-family:ui-monospace,Consolas,monospace}
::-webkit-scrollbar{width:9px;height:9px}
::-webkit-scrollbar-thumb{background:#2e3547;border-radius:5px}
</style>
</head>
<body>
<div id="sidebar">
<header>
<h1>⎇ Git<span>Scope</span></h1>
<p>Git topology + contextual event fusion</p>
</header>
<div id="tabs">
<button data-tab="commits" class="active">Commits</button>
<button data-tab="refs">Refs</button>
<button data-tab="events">Events</button>
<button data-tab="data">Data</button>
</div>
<div id="panels">
<div class="panel active" id="panel-commits">
<button class="primary" id="btnNewCommit" style="width:100%">+ New Commit</button>
<h3 class="sec">History</h3>
<div id="commitList"></div>
</div>
<div class="panel" id="panel-refs">
<button class="primary" id="btnNewBranch" style="width:100%">+ New Branch</button>
<h3 class="sec">Branches</h3>
<div id="branchList"></div>
<button class="primary" id="btnNewTag" style="width:100%;margin-top:14px">+ New Tag</button>
<h3 class="sec">Tags</h3>
<div id="tagList"></div>
</div>
<div class="panel" id="panel-events">
<button class="primary" id="btnNewEvent" style="width:100%">+ New Contextual Event</button>
<h3 class="sec">External Contextual Events</h3>
<div id="eventList"></div>
</div>
<div class="panel" id="panel-data">
<h3 class="sec">Import</h3>
<label>Commit data (JSON array)</label>
<textarea id="importCommits" placeholder='[{"id":"a1","parents":[],"message":"...","branch":"main"}]'></textarea>
<label>Contextual events (JSON array)</label>
<textarea id="importEvents" placeholder='[{"title":"Code Freeze","ref":"CF-1","commitId":"a1"}]'></textarea>
<div class="row" style="margin-top:8px">
<button class="primary" id="btnImport">Import & Merge</button>
<button id="btnLoadSample">Load Sample</button>
</div>
<div class="row">
<button id="btnExport">Export JSON</button>
<button class="danger" id="btnReset">Reset All</button>
</div>
<p class="hint">State auto-saves to localStorage on every change.</p>
</div>
</div>
</div>
<div id="main">
<header>
<span class="title">Git Graph — Synthesized View</span>
<span id="statusInfo" class="hint"></span>
<div id="legend">
<span><i style="background:#4fc3f7"></i>Commit</span>
<span><i style="background:#ffb74d"></i>Event anchor</span>
<span><i style="background:#81c784"></i>Branch head</span>
<span><i style="background:#fff176"></i>Tag</span>
</div>
</header>
<div id="graphWrap">
<svg id="graph"></svg>
<div id="tooltip"></div>
</div>
<div id="timeline">
<h4>Contextual Flow — external events mapped onto the commit timeline</h4>
<svg id="flow"></svg>
</div>
</div>
<dialog id="dlgCommit">
<h2 id="dlgCommitTitle">New Commit</h2>
<label>ID (short hash)</label><input id="fCid" class="mono" placeholder="e.g. a1b2c3">
<label>Message</label><input id="fCmsg" placeholder="Commit message">
<label>Parent commits (multi-select)</label><select id="fCparents" multiple size="5"></select>
<label>Branch lane</label><select id="fCbranch"></select>
<label>Author</label><input id="fCauthor" placeholder="Author name">
<div class="btns"><button onclick="dlgCommit.close()">Cancel</button><button class="primary" id="btnSaveCommit">Save</button></div>
</dialog>
<dialog id="dlgRef">
<h2 id="dlgRefTitle">New Branch</h2>
<label>Name</label><input id="fRname" placeholder="feature/login">
<label>Type</label><select id="fRtype"><option value="branch">Branch</option><option value="tag">Tag</option></select>
<label>Points to commit</label><select id="fRcommit"></select>
<div class="btns"><button onclick="dlgRef.close()">Cancel</button><button class="primary" id="btnSaveRef">Save</button></div>
</dialog>
<dialog id="dlgEvent">
<h2 id="dlgEventTitle">New Contextual Event</h2>
<label>Title *</label><input id="fEtitle" placeholder="e.g. Code Freeze">
<label>Reference (identifying marker)</label><input id="fEref" placeholder="e.g. CF-2024-01" class="mono">
<label>Association ID — commit this event is anchored to *</label><select id="fEcommit"></select>
<label>Category</label><select id="fEcat"><option>Milestone</option><option>Release</option><option>Code Freeze</option><option>Feature Completion</option><option>Incident</option><option>Review</option></select>
<label>Details</label><textarea id="fEdesc" placeholder="Full description of the external event"></textarea>
<div class="btns"><button onclick="dlgEvent.close()">Cancel</button><button class="primary" id="btnSaveEvent">Save</button></div>
</dialog>
<div id="toast"></div>
<script>
/* ============ STATE ============ */
const LS_KEY='gitscope.state.v1';
let S={commits:[],branches:[],tags:[],events:[],selected:null};
function save(){localStorage.setItem(LS_KEY,JSON.stringify(S));}
function load(){
try{const raw=localStorage.getItem(LS_KEY);if(raw){S=Object.assign(S,JSON.parse(raw));return true;}}catch(e){}
return false;
}
const uid=()=>Math.random().toString(16).slice(2,8);
/* ============ SAMPLE DATA ============ */
const SAMPLE={
commits:[
{id:'a1f9e2',parents:[],message:'Initial project scaffold',branch:'main',author:'ana'},
{id:'b2c8d4',parents:['a1f9e2'],message:'Add core data model',branch:'main',author:'ana'},
{id:'c3d7f1',parents:['b2c8d4'],message:'Implement render pipeline',branch:'main',author:'bo'},
{id:'d4e6a9',parents:['c3d7f1'],message:'Start feature: auth module',branch:'feature/auth',author:'cy'},
{id:'e5f5b8',parents:['d4e6a9'],message:'Login form + validation',branch:'feature/auth',author:'cy'},
{id:'f6a4c7',parents:['c3d7f1'],message:'Start feature: export engine',branch:'feature/export',author:'ana'},
{id:'a7b3d6',parents:['f6a4c7'],message:'CSV export working',branch:'feature/export',author:'ana'},
{id:'b8c2e5',parents:['e5f5b8','c3d7f1'],message:'Merge auth into main',branch:'main',author:'bo'},
{id:'c9d1f4',parents:['b8c2e5'],message:'Hotfix: session timeout',branch:'hotfix/session',author:'cy'},
{id:'d0e9a3',parents:['c9d1f4','b8c2e5'],message:'Merge hotfix into main',branch:'main',author:'bo'},
{id:'e1f8b7',parents:['a7b3d6','d0e9a3'],message:'Merge export engine',branch:'main',author:'ana'},
{id:'f2a7c4',parents:['e1f8b7'],message:'Release prep v1.0',branch:'main',author:'bo'}
],
branches:[{name:'main',commit:'f2a7c4'},{name:'feature/auth',commit:'e5f5b8'},{name:'feature/export',commit:'a7b3d6'},{name:'hotfix/session',commit:'c9d1f4'}],
tags:[{name:'v0.9-beta',commit:'b8c2e5'},{name:'v1.0',commit:'f2a7c4'}],
events:[
{id:'ev1',title:'Sprint Kickoff',ref:'SK-01',commitId:'a1f9e2',category:'Milestone',desc:'Project kickoff, repo created and CI configured.'},
{id:'ev2',title:'Code Freeze',ref:'CF-2024-01',commitId:'b8c2e5',category:'Code Freeze',desc:'Freeze before beta. Only hotfixes allowed after this point.'},
{id:'ev3',title:'Beta Release',ref:'REL-0.9',commitId:'b8c2e5',category:'Release',desc:'Beta shipped to internal testers.'},
{id:'ev4',title:'Auth Feature Completion',ref:'FC-AUTH',commitId:'e5f5b8',category:'Feature Completion',desc:'Login + validation complete, QA sign-off received.'},
{id:'ev5',title:'Security Incident Review',ref:'INC-114',commitId:'c9d1f4',category:'Incident',desc:'Session timeout vulnerability found and patched; postmortem held.'},
{id:'ev6',title:'v1.0 Launch',ref:'REL-1.0',commitId:'f2a7c4',category:'Release',desc:'Public launch of version 1.0.'}
]};
/* ============ HELPERS ============ */
function commitById(id){return S.commits.find(c=>c.id===id);}
function eventById(id){return S.events.find(e=>e.id===id);}
function eventsFor(cid){return S.events.filter(e=>e.commitId===cid);}
function toast(msg){const t=document.getElementById('toast');t.textContent=msg;t.style.display='block';clearTimeout(t._h);t._h=setTimeout(()=>t.style.display='none',2200);}
function fillCommitSelect(sel,includeEmpty){
sel.innerHTML=(includeEmpty?'<option value="">— none —</option>':'')+S.commits.map(c=>`<option value="${c.id}">${c.id} — ${esc(c.message||'').slice(0,40)}</option>`).join('');
}
function esc(s){return String(s??'').replace(/[&<>"]/g,m=>({'&':'&','<':'<','>':'>','"':'"'}[m]));}
/* ============ LAYOUT ============ */
function layout(){
// order commits topologically (parents first), assign x = order, y = lane
const lanes=[]; const pos={}; const done=new Set();
const remaining=[...S.commits];
let guard=remaining.length*remaining.length+10;
while(remaining.length&&guard-->0){
let idx=remaining.findIndex(c=>c.parents.every(p=>done.has(p)));
if(idx<0)idx=0;
const c=remaining.splice(idx,1)[0];
let li=lanes.indexOf(c.branch);
if(li<0){li=lanes.length;lanes.push(c.branch);}
pos[c.id]={x:done.size,lane:li};
done.add(c.id);
}
const W=Math.max(760,done.size*110+160), LH=72, padX=70, padY=54;
const H=padY*2+Math.max(1,lanes.length-1)*LH;
S.commits.forEach(c=>{const p=pos[c.id]||{x:0,lane:0};
c._x=padX+p.x*110; c._y=padY+p.lane*LH; c._order=p.x;});
return {lanes,W,H,padX,padY};
}
/* ============ GRAPH RENDER ============ */
const svg=d3.select('#graph');
const gRoot=svg.append('g');
svg.call(d3.zoom().scaleExtent([0.4,2.5]).on('zoom',ev=>gRoot.attr('transform',ev.transform)));
const gEdges=gRoot.append('g'), gLanes=gRoot.append('g'), gNodes=gRoot.append('g');
function renderGraph(){
const {lanes,W,H}=layout();
svg.attr('width','100%').attr('height','100%')
.attr('viewBox',`0 0 ${W} ${Math.max(H,300)}`);
// lane labels
const laneSel=gLanes.selectAll('g.lane').data(lanes,d=>d);
laneSel.exit().remove();
const laneEnter=laneSel.enter().append('g').attr('class','lane');
laneEnter.append('line').attr('stroke','#232a3a').attr('stroke-dasharray','3 5');
laneEnter.append('text').attr('class','laneLabel').attr('font-size',11).attr('fill','#8b93a7');
const laneAll=laneEnter.merge(laneSel);
lanes.forEach((ln,i)=>{
const y=54+i*72;
laneAll.filter(d=>d===ln).select('line')
.attr('x1',40).attr('x2',W-30).attr('y1',y).attr('y2',y);
laneAll.filter(d=>d===ln).select('text')
.attr('x',40).attr('y',y-10).attr('text-anchor','start').text('⎇ '+ln);
});
// edges
const links=[];
S.commits.forEach(c=>c.parents.forEach(p=>{if(commitById(p))links.push({s:p,t:c.id});}));
const path=(d)=>{
const a=commitById(d.s),b=commitById(d.t);
const x1=a._x,y1=a._y,x2=b._x,y2=b._y,mx=(x1+x2)/2;
return `M${x1},${y1} C${mx},${y1} ${mx},${y2} ${x2},${y2}`;
};
const edgeSel=gEdges.selectAll('path.edge').data(links,d=>d.s+'>'+d.t);
edgeSel.exit().remove();
edgeSel.enter().append('path').attr('class','edge')
.attr('fill','none').attr('stroke','#3a445c').attr('stroke-width',2)
.merge(edgeSel).attr('d',path);
// nodes
const nodeSel=gNodes.selectAll('g.node').data(S.commits,d=>d.id);
nodeSel.exit().remove();
const nEnter=nodeSel.enter().append('g').attr('class','node').style('cursor','pointer');
nEnter.append('circle').attr('class','halo').attr('r',16).attr('fill','transparent');
nEnter.append('circle').attr('class','dot').attr('r',9);
nEnter.append('circle').attr('class','ring').attr('r',9).attr('fill','none').attr('stroke-width',2.5);
nEnter.append('text').attr('class','idLabel').attr('font-size',10).attr('fill','#8b93a7').attr('font-family','monospace');
nEnter.append('text').attr('class','msgLabel').attr('font-size',11).attr('fill','#dfe4ee');
nEnter.append('g').attr('class','anchors');
const nAll=nEnter.merge(nodeSel);
nAll.attr('transform',c=>`translate(${c._x},${c._y})`);
nAll.select('circle.dot').attr('fill',c=>c.branch==='main'?'#4fc3f7':'#5c7aa8');
nAll.select('circle.ring').attr('stroke',c=>S.selected===c.id?'#ffb74d':'transparent');
nAll.select('text.idLabel').attr('x',0).attr('y',-16).attr('text-anchor','middle').text(c=>c.id.slice(0,6));
nAll.select('text.msgLabel').attr('x',0).attr('y',26).attr('text-anchor','middle')
.text(c=>(c.message||'').length>26?(c.message||'').slice(0,25)+'…':(c.message||''));
// event anchors
nAll.each(function(c){
const evs=eventsFor(c.id);
const ag=d3.select(this).select('g.anchors');
const as=ag.selectAll('g.anchor').data(evs,d=>d.id);
as.exit().remove();
const aEnter=as.enter().append('g').attr('class','anchor').style('cursor','pointer');
aEnter.append('circle').attr('r',7).attr('fill','#ffb74d').attr('stroke','#12141a').attr('stroke-width',2);
aEnter.append('text').attr('font-size',8).attr('fill','#12141a').attr('text-anchor','middle').attr('dy',3).attr('font-weight','700').text((d,i)=>i+1);
const aAll=aEnter.merge(as);
aAll.attr('transform',(d,i)=>`translate(${(i-(evs.length-1)/2)*18},-30)`)
.attr('data-event',dd=>dd.id)
.on('mouseenter',(ev,dd)=>showEventTip(ev,dd,c))
.on('mouseleave',hideTip)
.on('click',(ev,dd)=>{ev.stopPropagation();selectEvent(dd.id);});
});
nAll.on('click',(ev,c)=>{ev.stopPropagation();selectCommit(c.id);})
.on('mouseenter',(ev,c)=>showCommitTip(ev,c))
.on('mouseleave',hideTip);
// branch head markers
const heads=[];
S.branches.forEach(b=>{const c=commitById(b.commit);if(c)heads.push({name:b.name,c});});
S.tags.forEach(t=>{const c=commitById(t.commit);if(c)heads.push({name:'🏷 '+t.name,c,tag:true});});
const hSel=gNodes.selectAll('g.head').data(heads,d=>d.name);
hSel.exit().remove();
const hEnter=hSel.enter().append('g').attr('class','head');
hEnter.append('rect').attr('rx',4).attr('height',16);
hEnter.append('text').attr('font-size',9).attr('text-anchor','middle').attr('dy',11);
hEnter.merge(hSel)
.attr('transform',d=>`translate(${d.c._x},${d.c._y+36})`)
.select('rect')
.attr('fill',d=>d.tag?'rgba(255,241,118,.15)':'rgba(129,199,132,.15)')
.attr('stroke',d=>d.tag?'#fff176':'#81c784')
.attr('x',d=>-(d.name.length*3.3+8)).attr('width',d=>d.name.length*6.6+16);
hEnter.merge(hSel).select('text').text(d=>d.name).attr('fill',d=>d.tag?'#fff176':'#81c784');
renderFlow();
document.getElementById('statusInfo').textContent=
`${S.commits.length} commits · ${S.branches.length} branches · ${S.tags.length} tags · ${S.events.length} events`;
}
/* ============ CONTEXTUAL FLOW (secondary view) ============ */
function renderFlow(){
const fsvg=d3.select('#flow');
fsvg.selectAll('*').remove();
const w=fsvg.node().parentElement.clientWidth||800;
const ordered=[...S.commits].sort((a,b)=>a._order-b._order);
const evs=[];
ordered.forEach(c=>eventsFor(c.id).forEach(e=>evs.push({e,c})));
const h=40+evs.length*26;
fsvg.attr('width',w).attr('height',Math.max(h,120)).attr('viewBox',`0 0 ${w} ${Math.max(h,120)}`);
if(!ordered.length)return;
const x=d3.scaleLinear().domain([0,Math.max(1,d3.max(ordered,c=>c._order))]).range([90,w-30]);
// axis
fsvg.append('line').attr('x1',80).attr('x2',w-20).attr('y1',22).attr('y2',22).attr('stroke','#2e3547');
ordered.forEach(c=>{
fsvg.append('circle').attr('cx',x(c._order)).attr('cy',22).attr('r',4).attr('fill','#4fc3f7').style('cursor','pointer')
.on('click',()=>selectCommit(c.id));
});
evs.forEach((d,i)=>{
const y=44+i*26;
fsvg.append('line').attr('x1',x(d.c._order)).attr('x2',x(d.c._order)).attr('y1',22).attr('y2',y)
.attr('stroke','#ffb74d').attr('stroke-opacity',.4).attr('stroke-dasharray','2 3');
const g=fsvg.append('g').attr('transform',`translate(${x(d.c._order)},${y})`).style('cursor','pointer')
.on('click',()=>selectEvent(d.e.id))
.on('mouseenter',ev=>showEventTip(ev,d.e,d.c))
.on('mouseleave',hideTip);
g.append('circle').attr('r',6).attr('fill','#ffb74d');
g.append('text').attr('x',12).attr('dy',4).attr('font-size',11).attr('fill','#dfe4ee')
.text(`${d.e.title} [${d.e.ref||'—'}] → ${d.c.id.slice(0,6)}`);
});
}
/* ============ TOOLTIPS ============ */
const tip=document.getElementById('tooltip'),wrap=document.getElementById('graphWrap');
function placeTip(ev){
const r=wrap.getBoundingClientRect();
let x=ev.clientX-r.left+14,y=ev.clientY-r.top+14;
if(x>r.width-310)x-=330; if(y>r.height-140)y-=150;
tip.style.left=x+'px';tip.style.top=y+'px';tip.style.display='block';
}
function showCommitTip(ev,c){
const evs=eventsFor(c.id);
const br=S.branches.filter(b=>b.commit===c.id).map(b=>b.name);
const tg=S.tags.filter(t=>t.commit===c.id).map(t=>t.name);
tip.innerHTML=`<div class="tt-title">${esc(c.message||'(no message)')}</div>
<div class="tt-id">${c.id} · ${esc(c.branch||'')} · ${esc(c.author||'')}</div>
<div class="tt-msg">parents: ${c.parents.length?c.parents.map(p=>p.slice(0,6)).join(', '):'root'}</div>
${br.length?'<div class="tt-link">⎇ '+br.join(', ')+'</div>':''}
${tg.length?'<div class="tt-link">🏷 '+tg.join(', ')+'</div>':''}
${evs.length?`<div class="tt-link">◆ ${evs.length} contextual event(s): ${evs.map(e=>esc(e.title)).join(', ')}</div>`:'<div class="tt-msg" style="color:#8b93a7">no contextual events</div>'}`;
placeTip(ev);
}
function showEventTip(ev,e,c){
tip.innerHTML=`<div class="tt-title">◆ ${esc(e.title)}</div>
<div class="tt-id">ref: ${esc(e.ref||'—')} · ${esc(e.category||'')}</div>
<div class="tt-msg">${esc(e.desc||'')}</div>
<div class="tt-link">⚓ anchored to commit ${c.id.slice(0,6)} — “${esc(c.message||'')}”</div>`;
placeTip(ev);
}
function hideTip(){tip.style.display='none';}
/* ============ SELECTION SYNC ============ */
function selectCommit(id){
S.selected=id; save();
gNodes.selectAll('g.node').select('circle.ring')
.attr('stroke',c=>c.id===id?'#ffb74d':'transparent');
gNodes.selectAll('g.anchor').attr('opacity',null);
gNodes.selectAll('g.node').filter(c=>c.id===id).selectAll('g.anchor')
.attr('opacity',1).select('circle').attr('fill','#ff8a65');
gNodes.selectAll('g.node').filter(c=>c.id!==id).selectAll('g.anchor').select('circle').attr('fill','#ffb74d');
renderLists();
highlightEventCards(id);
}
function selectEvent(id){
const e=eventById(id); if(!e)return;
selectCommit(e.commitId);
gNodes.selectAll('g.anchor').filter(d=>d.id===id).select('circle').attr('fill','#e57373').attr('r',9);
setTimeout(()=>gNodes.selectAll('g.anchor').filter(d=>d.id===id).select('circle').attr('r',7),400);
}
function highlightEventCards(cid){
document.querySelectorAll('#eventList .card').forEach(el=>{
el.classList.toggle('sel',el.dataset.commit===cid);
});
}
/* ============ LISTS (CRUD UI) ============ */
function renderLists(){
const cl=document.getElementById('commitList');
cl.innerHTML=S.commits.length?[...S.commits].sort((a,b)=>(b._order??0)-(a._order??0)).map(c=>`
<div class="card ${S.selected===c.id?'sel':''}" data-id="${c.id}">
<div class="t"><span class="mono">${c.id.slice(0,6)}</span><span class="pill b">${esc(c.branch||'')}</span></div>
<div class="m">${esc(c.message||'')} · ${esc(c.author||'')}</div>
<div class="m">parents: ${c.parents.map(p=>p.slice(0,6)).join(', ')||'root'}</div>
${eventsFor(c.id).length?`<div class="m" style="color:var(--acc2)">◆ ${eventsFor(c.id).map(e=>esc(e.title)).join(', ')}</div>`:''}
<div class="acts">
<button data-act="editC">Edit</button>
<button data-act="delC" class="danger">Delete</button>
</div>
</div>`).join(''):'<p class="hint">No commits yet.</p>';
const bl=document.getElementById('branchList');
bl.innerHTML=S.branches.length?S.branches.map((b,i)=>`
<div class="card" data-i="${i}">
<div class="t"><span>⎇ ${esc(b.name)}</span></div>
<div class="m mono">→ ${esc(b.commit||'none')}</div>
<div class="acts"><button data-act="editR" data-type="branch">Edit</button><button data-act="delR" data-type="branch" class="danger">Delete</button></div>
</div>`).join(''):'<p class="hint">No branches.</p>';
const tl=document.getElementById('tagList');
tl.innerHTML=S.tags.length?S.tags.map((t,i)=>`
<div class="card" data-i="${i}">
<div class="t"><span>🏷 ${esc(t.name)}</span></div>
<div class="m mono">→ ${esc(t.commit||'none')}</div>
<div class="acts"><button data-act="editR" data-type="tag">Edit</button><button data-act="delR" data-type="tag" class="danger">Delete</button></div>
</div>`).join(''):'<p class="hint">No tags.</p>';
const el=document.getElementById('eventList');
el.innerHTML=S.events.length?S.events.map(e=>{
const c=commitById(e.commitId);
return `<div class="card" data-id="${e.id}" data-commit="${e.commitId||''}">
<div class="t"><span>◆ ${esc(e.title)}</span><span class="pill e">${esc(e.category||'')}</span></div>
<div class="m mono">ref: ${esc(e.ref||'—')}</div>
<div class="m">⚓ commit <span class="mono">${esc(e.commitId||'—')}</span>${c?' — “'+esc(c.message||'').slice(0,36)+'”':' <span style="color:var(--red)">(missing!)</span>'}</div>
<div class="acts"><button data-act="editE">Edit</button><button data-act="goC">Go to commit</button><button data-act="delE" class="danger">Delete</button></div>
</div>`;}).join(''):'<p class="hint">No contextual events yet.</p>';
}
/* list click delegation */
document.getElementById('panels').addEventListener('click',ev=>{
const btn=ev.target.closest('button[data-act]');
const card=ev.target.closest('.card');
if(btn&&card){
const act=btn.dataset.act;
if(act==='editC')openCommitDlg(card.dataset.id);
if(act==='delC')deleteCommit(card.dataset.id);
if(act==='editE')openEventDlg(card.dataset.id);
if(act==='delE')deleteEvent(card.dataset.id);
if(act==='goE'||act==='goC')selectCommit(card.dataset.commit);
if(act==='editR')openRefDlg(btn.dataset.type,+card.dataset.i);
if(act==='delR')deleteRef(btn.dataset.type,+card.dataset.i);
ev.stopPropagation();return;
}
if(card&&card.dataset.id&&card.closest('#eventList'))selectEvent(card.dataset.id);
else if(card&&card.dataset.id&&card.closest('#commitList'))selectCommit(card.dataset.id);
});
/* ============ DIALOGS ============ */
function openCommitDlg(id){
const c=id?commitById(id):null;
document.getElementById('dlgCommitTitle').textContent=c?'Edit Commit '+c.id:'New Commit';
const ps=document.getElementById('fCparents');
ps.innerHTML=S.commits.filter(x=>!c||x.id!==c.id).map(x=>`<option value="${x.id}">${x.id.slice(0,6)} — ${esc(x.message||'').slice(0,30)}</option>`).join('');
if(c)[...ps.options].forEach(o=>o.selected=c.parents.includes(o.value));
const bs=document.getElementById('fCbranch');
const names=[...new Set([...S.branches.map(b=>b.name),'main',...S.commits.map(x=>x.branch).filter(Boolean)])];
bs.innerHTML=names.map(n=>`<option>${esc(n)}</option>`).join('')+`<option value="__new">+ new branch…</option>`;
if(c)bs.value=c.branch||'main';
document.getElementById('fCid').value=c?c.id:'';
document.getElementById('fCid').disabled=!!c;
document.getElementById('fCmsg').value=c?(c.message||''):'';
document.getElementById('fCauthor').value=c?(c.author||''):'';
document.getElementById('btnSaveCommit').dataset.id=id||'';
dlgCommit.showModal();
}
document.getElementById('btnSaveCommit').onclick=()=>{
const id=document.getElementById('btnSaveCommit').dataset.id;
let cid=document.getElementById('fCid').value.trim()||uid();
const msg=document.getElementById('fCmsg').value.trim();
const parents=[...document.getElementById('fCparents').selectedOptions].map(o=>o.value);
let branch=document.getElementById('fCbranch').value;
if(branch==='__new'){branch=prompt('New branch name:');if(!branch)return;}
const author=document.getElementById('fCauthor').value.trim();
for(const p of parents){if(!commitById(p)){toast('Unknown parent '+p);return;}}
if(id){Object.assign(commitById(id),{message:msg,parents,branch,author});}
else{
if(commitById(cid)){toast('Commit ID already exists');return;}
S.commits.push({id:cid,parents,message:msg,branch,author});
if(!S.branches.some(b=>b.name===branch))S.branches.push({name:branch,commit:cid});
}
const head=S.branches.find(b=>b.name===branch);
if(head&&!id)head.commit=cid;
save();dlgCommit.close();renderAll();
};
function deleteCommit(id){
const evs=eventsFor(id).length;
if(!confirm(`Delete commit ${id.slice(0,6)}?${evs?` Its ${evs} contextual event(s) will also be deleted.`:''}`))return;
S.commits=S.commits.filter(c=>c.id!==id);
S.commits.forEach(c=>c.parents=c.parents.filter(p=>p!==id));
S.events=S.events.filter(e=>e.commitId!==id);
S.branches=S.branches.filter(b=>b.commit!==id);
S.tags=S.tags.filter(t=>t.commit!==id);
if(S.selected===id)S.selected=null;
save();renderAll();
}
function openRefDlg(type,idx){
const r=type==='branch'?S.branches[idx]:S.tags[idx];
document.getElementById('dlgRefTitle').textContent=r?`Edit ${type} ${r.name}`:(type==='branch'?'New Branch':'New Tag');
document.getElementById('fRtype').value=type;
fillCommitSelect(document.getElementById('fRcommit'));
document.getElementById('fRname').value=r?r.name:'';
if(r)document.getElementById('fRcommit').value=r.commit||'';
document.getElementById('btnSaveRef').dataset.i=r?idx:'';
document.getElementById('btnSaveRef').dataset.type=type;
dlgRef.showModal();
}
document.getElementById('btnSaveRef').onclick=()=>{
const name=document.getElementById('fRname').value.trim();
const commit=document.getElementById('fRcommit').value;
const type=document.getElementById('btnSaveRef').dataset.type;
const i=document.getElementById('btnSaveRef').dataset.i;
if(!name){toast('Name required');return;}
const arr=type==='branch'?S.branches:S.tags;
if(i===''){arr.push({name,commit});}
else Object.assign(arr[+i],{name,commit});
save();dlgRef.close();renderAll();
};
function deleteRef(type,i){
(type==='branch'?S.branches:S.tags).splice(i,1);
save();renderAll();
}
function openEventDlg(id){
const e=id?eventById(id):null;
document.getElementById('dlgEventTitle').textContent=e?'Edit Event':'New Contextual Event';
fillCommitSelect(document.getElementById('fEcommit'));
document.getElementById('fEtitle').value=e?(e.title||''):'';
document.getElementById('fEref').value=e?(e.ref||''):'';
document.getElementById('fEcat').value=e?(e.category||'Milestone'):'Milestone';
document.getElementById('fEdesc').value=e?(e.desc||''):'';
if(e)document.getElementById('fEcommit').value=e.commitId||'';
document.getElementById('btnSaveEvent').dataset.id=id||'';
dlgEvent.showModal();
}
document.getElementById('btnSaveEvent').onclick=()=>{
const title=document.getElementById('fEtitle').value.trim();
const commitId=document.getElementById('fEcommit').value;
if(!title){toast('Title is required');return;}
if(!commitId){toast('Association ID (commit) is required');return;}
const data={title,ref:document.getElementById('fEref').value.trim(),
commitId,category:document.getElementById('fEcat').value,
desc:document.getElementById('fEdesc').value.trim()};
const id=document.getElementById('btnSaveEvent').dataset.id;
if(id)Object.assign(eventById(id),data);
else S.events.push({id:uid(),...data});
save();dlgEvent.close();renderAll();selectCommit(commitId);
};
function deleteEvent(id){
S.events=S.events.filter(e=>e.id!==id);
save();renderAll();
}
/* ============ IMPORT / EXPORT ============ */
document.getElementById('btnImport').onclick=()=>{
try{
const cs=JSON.parse(document.getElementById('importCommits').value||'[]');
const es=JSON.parse(document.getElementById('importEvents').value||'[]');
let nc=0,ne=0;
cs.forEach(c=>{
if(!c.id)return;
const ex=commitById(c.id);
const rec={id:c.id,parents:c.parents||[],message:c.message||'',branch:c.branch||'main',author:c.author||''};
if(ex)Object.assign(ex,rec);else{S.commits.push(rec);nc++;}
if(rec.branch&&!S.branches.some(b=>b.name===rec.branch))S.branches.push({name:rec.branch,commit:rec.id});
});
es.forEach(e=>{
if(!e.title||!e.commitId)return;
if(e.id&&eventById(e.id)){Object.assign(eventById(e.id),e);}
else{S.events.push({id:e.id||uid(),title:e.title,ref:e.ref||'',commitId:e.commitId,category:e.category||'Milestone',desc:e.desc||''});ne++;}
});
save();renderAll();toast(`Imported ${nc} commits, ${ne} events`);
}catch(err){toast('Invalid JSON: '+err.message);}
};
document.getElementById('btnLoadSample').onclick=()=>{
S=JSON.parse(JSON.stringify(SAMPLE));S.selected=null;
save();renderAll();toast('Sample data loaded');
};
document.getElementById('btnExport').onclick=()=>{
const {commits,branches,tags,events}=S;
const blob=new Blob([JSON.stringify({commits,branches,tags,events},null,2)],{type:'application/json'});
const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='gitscope-export.json';a.click();
};
document.getElementById('btnReset').onclick=()=>{
if(!confirm('Reset all data?'))return;
S={commits:[],branches:[],tags:[],events:[],selected:null};
localStorage.removeItem(LS_KEY);renderAll();
};
/* ============ INIT ============ */
document.getElementById('btnNewCommit').onclick=()=>openCommitDlg();
document.getElementById('btnNewBranch').onclick=()=>openRefDlg('branch');
document.getElementById('btnNewTag').onclick=()=>openRefDlg('tag');
document.getElementById('btnNewEvent').onclick=()=>openEventDlg();
document.querySelectorAll('#tabs button').forEach(b=>b.onclick=()=>{
document.querySelectorAll('#tabs button').forEach(x=>x.classList.remove('active'));
document.querySelectorAll('.panel').forEach(x=>x.classList.remove('active'));
b.classList.add('active');
document.getElementById('panel-'+b.dataset.tab).classList.add('active');
});
window.addEventListener('resize',()=>renderFlow());
function renderAll(){renderGraph();renderLists();}
if(!load()){
S=JSON.parse(JSON.stringify(SAMPLE));
save();
}
renderAll();
</script>
</body>
</html>
<!-- agent-meta {"model":"z-ai/glm-5.3-flash","provider":"openrouter","persona":"full","sessionId":"485ad130-24fe-4700-9bd0-a331440dfc0c","tokensIn":342625,"tokensOut":12647,"tokensTotal":355272,"cost":0.014155959999999999,"turns":16,"toolCalls":15,"failedToolCalls":0,"timestamp":"2026-08-28T04:09:30.980Z"} -->