← Git Explorer results

Git Explorer

GLM 5.3 Flash · full

Initial view of Git Explorer

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=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[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"} -->