Git Explorer
ox alpha · 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 28,357 bytes · SHA-256 2dcd2af28f02
<!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:#0f1117; --panel:#171a23; --panel2:#1e2230; --border:#2a2f42;
--text:#e6e9f2; --muted:#8b93ab; --accent:#4f8cff; --accent2:#9b6bff;
--green:#3ecf8e; --orange:#ffb454; --red:#ff6b6b; --event:#ffd166;
}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(--text);height:100vh;display:flex;flex-direction:column;overflow:hidden}
header{display:flex;align-items:center;gap:14px;padding:10px 18px;background:var(--panel);border-bottom:1px solid var(--border)}
header h1{font-size:16px;font-weight:600}
header h1 span{color:var(--accent)}
.toolbar{display:flex;gap:6px;margin-left:auto;flex-wrap:wrap}
button{background:var(--panel2);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:6px 11px;font-size:12px;cursor:pointer;transition:.15s}
button:hover{border-color:var(--accent);color:var(--accent)}
button.primary{background:var(--accent);border-color:var(--accent);color:#fff}
button.primary:hover{filter:brightness(1.15);color:#fff}
button.danger:hover{border-color:var(--red);color:var(--red)}
main{flex:1;display:flex;min-height:0}
#graphPanel{flex:1.6;display:flex;flex-direction:column;min-width:0}
#graphPanel h2, #side h2{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);padding:8px 14px;border-bottom:1px solid var(--border)}
#graphWrap{flex:1;position:relative;min-height:0}
svg{width:100%;height:100%;display:block}
#side{width:400px;display:flex;flex-direction:column;border-left:1px solid var(--border);background:var(--panel)}
#eventList{flex:1;overflow-y:auto;padding:10px}
.ev-card{background:var(--panel2);border:1px solid var(--border);border-left:3px solid var(--event);border-radius:6px;padding:9px 11px;margin-bottom:8px;cursor:pointer;transition:.15s}
.ev-card:hover,.ev-card.hl{border-color:var(--event);background:#262b3d;transform:translateX(-2px)}
.ev-card .t{font-weight:600;font-size:13px}
.ev-card .m{font-size:11px;color:var(--muted);margin-top:3px}
.ev-card .ref{color:var(--accent);font-family:monospace;font-size:11px}
.ev-card .acts{margin-top:6px;display:flex;gap:6px}
.ev-card .acts button{padding:2px 8px;font-size:10px}
#detail{border-top:1px solid var(--border);padding:12px 14px;max-height:38%;overflow-y:auto;background:var(--panel2)}
#detail h3{font-size:13px;margin-bottom:6px}
#detail .kv{font-size:12px;margin:3px 0}
#detail .kv b{color:var(--muted);font-weight:500;display:inline-block;width:90px}
#detail .empty{color:var(--muted);font-size:12px;font-style:italic}
/* tooltip */
#tip{position:fixed;pointer-events:none;background:#0b0d13;border:1px solid var(--event);border-radius:8px;padding:10px 12px;font-size:12px;max-width:280px;z-index:50;display:none;box-shadow:0 6px 24px rgba(0,0,0,.5)}
#tip .tt{font-weight:700;color:var(--event);margin-bottom:4px}
#tip .tm{color:var(--muted);margin:2px 0}
#tip .tc{font-family:monospace;color:var(--accent);margin-top:4px}
/* modal */
#modalBack{position:fixed;inset:0;background:rgba(0,0,0,.6);display:none;align-items:center;justify-content:center;z-index:100}
#modal{background:var(--panel);border:1px solid var(--border);border-radius:10px;padding:20px;width:420px;max-height:85vh;overflow-y:auto}
#modal h3{margin-bottom:14px;font-size:15px}
#modal label{display:block;font-size:11px;color:var(--muted);margin:10px 0 4px;text-transform:uppercase;letter-spacing:.05em}
#modal input,#modal select,#modal textarea{width:100%;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);padding:8px 10px;font-size:13px;font-family:inherit}
#modal input:focus,#modal select:focus,#modal textarea:focus{outline:none;border-color:var(--accent)}
#modal .row{display:flex;gap:10px;justify-content:flex-end;margin-top:16px}
#modal textarea{resize:vertical;min-height:60px}
.badge{display:inline-block;padding:1px 7px;border-radius:10px;font-size:10px;font-weight:600}
.badge.branch{background:rgba(79,140,255,.18);color:var(--accent)}
.badge.tag{background:rgba(155,107,255,.18);color:var(--accent2)}
.badge.ev{background:rgba(255,209,102,.18);color:var(--event)}
.legend{position:absolute;bottom:10px;left:12px;font-size:11px;color:var(--muted);background:rgba(15,17,23,.8);padding:6px 10px;border-radius:6px;border:1px solid var(--border)}
.legend i{display:inline-block;width:10px;height:10px;border-radius:50%;margin:0 4px 0 10px;vertical-align:-1px}
#toast{position:fixed;bottom:20px;right:20px;background:var(--panel2);border:1px solid var(--accent);border-radius:8px;padding:10px 16px;font-size:13px;z-index:200;display:none}
</style>
</head>
<body>
<header>
<h1>Git<span>·</span>Context Visualizer</h1>
<div class="toolbar">
<button class="primary" id="btnAddCommit">+ Commit</button>
<button id="btnAddBranch">+ Branch</button>
<button id="btnAddTag">+ Tag</button>
<button id="btnAddEvent">+ Event</button>
<button id="btnImport">Import</button>
<button id="btnExport">Export</button>
<button class="danger" id="btnReset">Reset</button>
</div>
</header>
<main>
<div id="graphPanel">
<h2>Git DAG — commits with anchored contextual events</h2>
<div id="graphWrap">
<svg id="graph"></svg>
<div class="legend">
<i style="background:var(--accent)"></i>commit
<i style="background:var(--accent2)"></i>merge
<i style="background:var(--event)"></i>event anchor
<i style="background:none;border:2px solid var(--orange);width:8px;height:8px"></i>HEAD
</div>
</div>
</div>
<div id="side">
<h2>Contextual Events</h2>
<div id="eventList"></div>
<div id="detail"><div class="empty">Select a commit or event to see details.</div></div>
</div>
</main>
<div id="tip"></div>
<div id="modalBack"><div id="modal"></div></div>
<div id="toast"></div>
<script>
"use strict";
/* ================= State ================= */
const LS_KEY = "gitContextVisualizer.v1";
let state = null;
function uid(p){ return p + "_" + Math.random().toString(36).slice(2,8) + Date.now().toString(36).slice(-3); }
function seedState(){
const c1={id:"a1b2c3",msg:"Initial project setup",parents:[],branch:"main",color:"#4f8cff",x:80,y:300,author:"dev"};
const c2={id:"d4e5f6",msg:"Add core module",parents:["a1b2c3"],branch:"main",color:"#4f8cff",x:200,y:300,author:"dev"};
const c3={id:"b7c8d9",msg:"Start feature/login",parents:["d4e5f6"],branch:"feature/login",color:"#3ecf8e",x:320,y:180,author:"alice"};
const c4={id:"e0f1a2",msg:"Hotfix: config typo",parents:["d4e5f6"],branch:"main",color:"#4f8cff",x:320,y:300,author:"bob"};
const c5={id:"c3d4e5",msg:"Login form UI",parents:["b7c8d9"],branch:"feature/login",color:"#3ecf8e",x:440,y:180,author:"alice"};
const c6={id:"f6a7b8",msg:"Auth backend",parents:["c3d4e5"],branch:"feature/login",color:"#3ecf8e",x:560,y:180,author:"alice"};
const c7={id:"a9b0c1",msg:"Merge feature/login",parents:["e0f1a2","f6a7b8"],branch:"main",color:"#9b6bff",x:680,y:240,author:"dev"};
const c8={id:"d2e3f4",msg:"Release v1.0 prep",parents:["a9b0c1"],branch:"main",color:"#4f8cff",x:800,y:300,author:"dev"};
const commits=[c1,c2,c3,c4,c5,c6,c7,c8];
return {
commits,
branches:[
{name:"main",target:"d2e3f4"},
{name:"feature/login",target:"f6a7b8"}
],
tags:[{name:"v0.9",target:"e0f1a2"}],
head:"main",
events:[
{id:uid("ev"),title:"Sprint Kickoff",ref:"SPR-101",commitId:"a1b2c3",desc:"Project bootstrap approved by architecture board.",date:"2024-01-08"},
{id:uid("ev"),title:"Code Freeze",ref:"REL-1.0",commitId:"a9b0c1",desc:"Freeze before release candidate build.",date:"2024-02-20"},
{id:uid("ev"),title:"Feature Completion",ref:"FEAT-LOGIN",commitId:"f6a7b8",desc:"Login feature signed off by QA.",date:"2024-02-14"}
]
};
}
function save(){ localStorage.setItem(LS_KEY, JSON.stringify(state)); }
function load(){
try{ const s=localStorage.getItem(LS_KEY); if(s){ state=JSON.parse(s); return; } }catch(e){}
state = seedState(); save();
}
function commitById(id){ return state.commits.find(c=>c.id===id); }
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); }
/* ================= Graph rendering ================= */
const svg = d3.select("#graph");
const gRoot = svg.append("g");
const gEdges = gRoot.append("g"), gEvents = gRoot.append("g"), gNodes = gRoot.append("g"), gLabels = gRoot.append("g");
svg.call(d3.zoom().scaleExtent([0.3,3]).on("zoom", e=>gRoot.attr("transform", e.transform)));
const tip = document.getElementById("tip");
function layout(){
// order commits by x; assign lanes to avoid overlaps
const sorted=[...state.commits].sort((a,b)=>a.x-b.x);
const lanes=[];
sorted.forEach(c=>{
let lane=0;
while(lanes[lane]!==undefined && Math.abs(lanes[lane]-c.y)<60) lane++;
lanes[lane]=c.y;
});
}
function render(){
const wrap=document.getElementById("graphWrap");
const W=wrap.clientWidth, H=wrap.clientHeight;
svg.attr("viewBox",`0 0 ${W} ${H}`);
const commits=state.commits;
// edges (curved for merges)
const links=[];
commits.forEach(c=>c.parents.forEach((p,i)=>{
const pc=commitById(p); if(!pc) return;
links.push({s:pc,t:c,key:c.id+"->"+p+i,merge:c.parents.length>1});
}));
const diag=d3.linkVertical().x(d=>d.x).y(d=>d.y).source(d=>d.source).target(d=>d.target);
const edge=gEdges.selectAll("path").data(links,d=>d.key);
edge.exit().remove();
edge.enter().append("path")
.attr("fill","none").attr("stroke","#2f3550").attr("stroke-width",2)
.merge(edge)
.attr("d",d=>{
const {s,t}=d;
const mx=(s.x+t.x)/2;
return `M${s.x},${s.y} C${mx},${s.y} ${mx},${t.y} ${t.x},${t.y}`;
})
.attr("stroke",d=>d.merge?"#9b6bff":"#2f3550");
// event anchors
const evData=state.events.filter(e=>commitById(e.commitId));
const ev=gEvents.selectAll("g.ev").data(evData,d=>d.id);
ev.exit().remove();
const evG=ev.enter().append("g").attr("class","ev").style("cursor","pointer");
evG.append("line").attr("stroke","#ffd166").attr("stroke-width",1.5).attr("stroke-dasharray","3,3");
evG.append("circle").attr("r",7).attr("fill","#ffd166").attr("stroke","#0f1117").attr("stroke-width",2);
evG.append("text").attr("font-size",10).attr("fill","#ffd166").attr("text-anchor","middle").attr("y",-12).text(d=>d.ref);
const evAll=evG.merge(ev);
evAll.attr("transform",d=>{
const c=commitById(d.commitId);
const n=evData.filter(e=>e.commitId===d.commitId);
const idx=n.indexOf(d);
const off=(idx-(n.length-1)/2)*22;
d._ax=c.x+off; d._ay=c.y-34;
return `translate(${d._ax},${d._ay})`;
})
.on("click",(e,d)=>{ selectCommit(d.commitId); })
.on("mouseenter",(e,d)=>{
const c=commitById(d.commitId);
tip.innerHTML=`<div class="tt">${esc(d.title)}</div>
<div class="tm">${esc(d.desc||"")}</div>
<div class="tm">Ref: ${esc(d.ref)} · ${esc(d.date||"")}</div>
<div class="tc">⚓ anchored to commit ${c?c.id:"?"} — "${c?esc(c.msg):""}"</div>`;
tip.style.display="block";
moveTip(e);
highlightEvent(d.id,true);
})
.on("mousemove",moveTip)
.on("mouseleave",()=>{ tip.style.display="none"; highlightEvent(d_id(),false); });
function d_id(){ return evAll.attr("data-id"); }
evAll.attr("data-id",d=>d.id);
evAll.select("line")
.attr("x1",0).attr("y1",8)
.attr("x2",0).attr("y2",34);
// connector line to commit drawn in transform coords: line from anchor down to node
evAll.select("line").attr("y2",34);
// nodes
const node=gNodes.selectAll("g.node").data(commits,d=>d.id);
node.exit().remove();
const nG=node.enter().append("g").attr("class","node").style("cursor","pointer");
nG.append("circle").attr("r",11);
nG.append("text").attr("text-anchor","middle").attr("y",4).attr("font-size",9).attr("font-weight","700").attr("fill","#0f1117");
const nAll=nG.merge(node);
nAll.attr("transform",d=>`translate(${d.x},${d.y})`)
.on("click",(e,d)=>selectCommit(d.id))
.on("mouseenter",(e,d)=>{
const evs=state.events.filter(ev=>ev.commitId===d.id);
tip.innerHTML=`<div class="tt" style="color:var(--accent)">${esc(d.id)}</div>
<div class="tm">${esc(d.msg)}</div>
<div class="tm">branch: ${esc(d.branch)}${evs.length?` · ⚓ ${evs.length} event(s)`:``}</div>`;
tip.style.display="block"; moveTip(e);
evs.forEach(ev=>highlightEvent(ev.id,true));
})
.on("mousemove",moveTip)
.on("mouseleave",()=>{ tip.style.display="none"; highlightEvent(null,false); });
nAll.select("circle")
.attr("fill",d=>d.color||"#4f8cff")
.attr("stroke",d=>d.id===selectedId?"#fff":"#0f1117")
.attr("stroke-width",d=>d.id===selectedId?3:2);
nAll.select("text").text(d=>d.id.slice(0,3));
// labels: branch/tag badges + msg
const lab=gLabels.selectAll("g.lab").data(commits,d=>d.id);
lab.exit().remove();
const lG=lab.enter().append("g").attr("class","lab").attr("pointer-events","none");
const lAll=lG.merge(lab);
lAll.attr("transform",d=>`translate(${d.x},${d.y+24})`);
lAll.selectAll("text").remove();
lAll.selectAll("rect").remove();
lAll.each(function(d){
const g=d3.select(this);
const items=[];
state.branches.forEach(b=>{ if(b.target===d.id) items.push({t:b.name,cls:"branch"}); });
state.tags.forEach(t=>{ if(t.target===d.id) items.push({t:t.name,cls:"tag"}); });
if(state.head===d.id || state.branches.find(b=>b.name===state.head && b.target===d.id)) items.push({t:"HEAD",cls:"tag"});
let x=0;
items.forEach(it=>{
g.append("rect").attr("x",x).attr("y",0).attr("rx",8).attr("ry",8)
.attr("fill",it.cls==="branch"?"rgba(79,140,255,.2)":"rgba(155,107,255,.2)")
.attr("stroke",it.cls==="branch"?"#4f8cff":"#9b6bff").attr("stroke-width",1);
const tx=g.append("text").attr("x",x+8).attr("y",11).attr("font-size",9)
.attr("fill",it.cls==="branch"?"#4f8cff":"#9b6bff").text(it.t);
x += 10 + it.t.length*5.6 + 8;
});
g.append("text").attr("y",26).attr("text-anchor","middle").attr("font-size",11).attr("fill","#c8cede")
.text(d.msg.length>26?d.msg.slice(0,25)+"…":d.msg);
});
// head ring
gRoot.selectAll("circle.head").remove();
const headBranch=state.branches.find(b=>b.name===state.head);
const hc=headBranch?commitById(headBranch.target):null;
if(hc){
gRoot.append("circle").attr("class","head").attr("cx",hc.x).attr("cy",hc.y).attr("r",16)
.attr("fill","none").attr("stroke","#ffb454").attr("stroke-width",2).attr("stroke-dasharray","4,3");
}
renderEvents();
}
function moveTip(e){
const pad=14, w=tip.offsetWidth, h=tip.offsetHeight;
let x=e.clientX+pad, y=e.clientY+pad;
if(x+w>innerWidth) x=e.clientX-w-pad;
if(y+h>innerHeight) y=e.clientY-h-pad;
tip.style.left=x+"px"; tip.style.top=y+"px";
}
function highlightEvent(id,on){
gEvents.selectAll("g.ev").classed("hl",function(){ return on && d3.select(this).datum().id===id; })
.select("circle").attr("r",function(){ const hl=d3.select(this.parentNode).classed("hl"); return hl?10:7; });
document.querySelectorAll(".ev-card").forEach(c=>{
c.classList.toggle("hl", on && c.dataset.id===id);
});
}
function esc(s){ return String(s??"").replace(/[&<>"]/g,m=>({"&":"&","<":"<",">":">",'"':"""}[m])); }
/* ================= Event list panel ================= */
let selectedId=null;
function renderEvents(){
const list=document.getElementById("eventList");
list.innerHTML="";
if(!state.events.length){ list.innerHTML=`<div style="color:var(--muted);font-size:12px;padding:8px">No events yet. Click “+ Event”.</div>`; }
state.events.forEach(ev=>{
const c=commitById(ev.commitId);
const div=document.createElement("div");
div.className="ev-card"; div.dataset.id=ev.id;
div.innerHTML=`<div class="t">⚓ ${esc(ev.title)}</div>
<div class="m"><span class="ref">${esc(ev.ref)}</span> → commit <span class="ref">${esc(ev.commitId)}</span>${c?` “${esc(c.msg.slice(0,28))}”`:` <span style="color:var(--red)">(missing commit)</span>`}</div>
<div class="m">${esc(ev.date||"")}</div>
<div class="acts"><button data-a="edit">Edit</button><button data-a="del">Delete</button></div>`;
div.addEventListener("click",e=>{
const a=e.target.dataset.a;
if(a==="edit"){ openEventModal(ev); return; }
if(a==="del"){ state.events=state.events.filter(x=>x.id!==ev.id); save(); render(); toast("Event deleted"); return; }
selectCommit(ev.commitId);
});
list.appendChild(div);
});
}
function selectCommit(id){
selectedId=id;
const c=commitById(id);
const det=document.getElementById("detail");
if(!c){ det.innerHTML=`<div class="empty">Commit not found.</div>`; return; }
const evs=state.events.filter(e=>e.commitId===id);
const brs=state.branches.filter(b=>b.target===id).map(b=>b.name);
const tgs=state.tags.filter(t=>t.target===id).map(t=>t.name);
det.innerHTML=`<h3>Commit ${esc(c.id)}</h3>
<div class="kv"><b>Message</b> ${esc(c.msg)}</div>
<div class="kv"><b>Branch</b> ${esc(c.branch||"—")}</div>
<div class="kv"><b>Parents</b> ${c.parents.length?c.parents.map(esc).join(", "):"— (root)"}</div>
<div class="kv"><b>Refs</b> ${[...brs.map(b=>`<span class="badge branch">${esc(b)}</span>`),...tgs.map(t=>`<span class="badge tag">${esc(t)}</span>`)].join(" ")||"—"}</div>
<div class="kv"><b>Events</b><br>${evs.length?evs.map(e=>`<span class="badge ev">⚓ ${esc(e.title)} (${esc(e.ref)})</span>`).join(" "):"<i style='color:var(--muted)'>none anchored</i>"}</div>
<div style="margin-top:10px;display:flex;gap:6px;flex-wrap:wrap">
<button id="dEdit">Edit</button><button id="dEv">⚓ Anchor Event</button><button class="danger" id="dDel">Delete</button>
</div>`;
document.getElementById("dEdit").onclick=()=>openCommitModal(c);
document.getElementById("dEv").onclick=()=>openEventModal(null,c.id);
document.getElementById("dDel").onclick=()=>deleteCommit(c.id);
render(); // refresh selection stroke
// highlight anchored contextual events for this commit
gEvents.selectAll("g.ev").classed("hl",function(d){ return d.commitId===id; })
.select("circle").attr("r",function(){ return d3.select(this.parentNode).classed("hl")?10:7; });
document.querySelectorAll(".ev-card").forEach(card=>{
const ev=state.events.find(e=>e.id===card.dataset.id);
card.classList.toggle("hl", !!ev && ev.commitId===id);
});
}
/* ================= CRUD: commits ================= */
function deleteCommit(id){
state.commits=state.commits.filter(c=>c.id!==id);
state.commits.forEach(c=>c.parents=c.parents.filter(p=>p!==id));
state.branches=state.branches.filter(b=>b.target!==id);
state.tags=state.tags.filter(t=>t.target!==id);
state.events=state.events.filter(e=>e.commitId!==id);
if(selectedId===id) selectedId=null;
save(); render(); toast("Commit deleted");
}
/* ================= Modals ================= */
const back=document.getElementById("modalBack"), modal=document.getElementById("modal");
function openModal(html){ modal.innerHTML=html; back.style.display="flex"; }
function closeModal(){ back.style.display="none"; }
back.addEventListener("click",e=>{ if(e.target===back) closeModal(); });
function selOptions(sel,val){
return state.commits.map(c=>`<option value="${c.id}" ${c.id===val?"selected":""}>${c.id} — ${esc(c.msg.slice(0,32))}</option>`).join("");
}
function openCommitModal(c){
const isNew=!c;
c=c||{id:"",msg:"",parents:[],branch:state.head,color:"#4f8cff",x:100,y:300,author:""};
openModal(`<h3>${isNew?"New Commit":"Edit Commit"}</h3>
<label>Commit ID</label><input id="mId" value="${esc(c.id)}" ${isNew?"":"readonly"} placeholder="e.g. f3a9c1">
<label>Message</label><input id="mMsg" value="${esc(c.msg)}">
<label>Parents (multi-select; empty = root)</label>
<select id="mPar" multiple size="5">${selOptions(null,null)}</select>
<label>Branch</label><input id="mBranch" value="${esc(c.branch)}" list="branchList">
<datalist id="branchList">${state.branches.map(b=>`<option value="${esc(b.name)}">`).join("")}</datalist>
<label>Author</label><input id="mAuthor" value="${esc(c.author||"")}">
<label>Color</label><input id="mColor" type="color" value="${c.color||"#4f8cff"}">
<div class="row"><button onclick="closeModal()">Cancel</button><button class="primary" id="mSave">Save</button></div>`);
if(!isNew) [...document.getElementById("mPar").options].forEach(o=>{ if(c.parents.includes(o.value)) o.selected=true; });
document.getElementById("mSave").onclick=()=>{
const id=document.getElementById("mId").value.trim();
const msg=document.getElementById("mMsg").value.trim()||"(no message)";
if(!id){ toast("Commit ID required"); return; }
if(isNew && commitById(id)){ toast("Commit ID already exists"); return; }
const parents=[...document.getElementById("mPar").selectedOptions].map(o=>o.value).filter(v=>commitById(v));
const branch=document.getElementById("mBranch").value.trim()||"main";
const obj={id,msg,parents,branch,author:document.getElementById("mAuthor").value.trim(),
color:document.getElementById("mColor").value,
x:c.x ?? 100, y:c.y ?? 300};
if(isNew){
// auto-place: right of rightmost parent
const ps=parents.map(commitById).filter(Boolean);
obj.x = ps.length? Math.max(...ps.map(p=>p.x))+120 : 100;
obj.y = ps.length? ps[0].y : 300;
if(ps.length>1) obj.y=(ps[0].y+ps[1].y)/2;
if(ps.length===1 && ps[0].parents.length===0) obj.y=ps[0].y; // keep
// lane offset if same y as parent
if(ps.length===1 && ps[0].branch!==branch) obj.y=ps[0].y-90;
state.commits.push(obj);
} else {
Object.assign(commitById(id),obj);
}
// keep branch pointer at tip
let br=state.branches.find(b=>b.name===branch);
if(!br){ br={name:branch,target:id}; state.branches.push(br); }
else if(obj.parents.includes(br.target)||!commitById(br.target)) br.target=id;
save(); closeModal(); render(); toast(isNew?"Commit created":"Commit updated");
};
}
function openBranchModal(){
openModal(`<h3>New Branch</h3>
<label>Branch name</label><input id="mName" placeholder="feature/x">
<label>Points to commit</label><select id="mTarget">${selOptions(null, state.branches.find(b=>b.name===state.head)?.target)}</select>
<div class="row"><button onclick="closeModal()">Cancel</button><button class="primary" id="mSave">Create</button></div>`);
document.getElementById("mSave").onclick=()=>{
const n=document.getElementById("mName").value.trim();
if(!n){ toast("Name required"); return; }
if(state.branches.some(b=>b.name===n)){ toast("Branch exists"); return; }
state.branches.push({name:n,target:document.getElementById("mTarget").value});
save(); closeModal(); render(); toast("Branch created");
};
}
function openTagModal(){
openModal(`<h3>New Tag</h3>
<label>Tag name</label><input id="mName" placeholder="v1.0">
<label>Points to commit</label><select id="mTarget">${selOptions(null,null)}</select>
<div class="row"><button onclick="closeModal()">Cancel</button><button class="primary" id="mSave">Create</button></div>`);
document.getElementById("mSave").onclick=()=>{
const n=document.getElementById("mName").value.trim();
if(!n){ toast("Name required"); return; }
state.tags.push({name:n,target:document.getElementById("mTarget").value});
save(); closeModal(); render(); toast("Tag created");
};
}
function openEventModal(ev, presetCommit){
const isNew=!ev;
ev=ev||{id:uid("ev"),title:"",ref:"",commitId:presetCommit|| (state.commits[0]?.id||""),desc:"",date:new Date().toISOString().slice(0,10)};
openModal(`<h3>${isNew?"New Contextual Event":"Edit Contextual Event"}</h3>
<label>Title *</label><input id="mTitle" value="${esc(ev.title)}" placeholder="Code Freeze">
<label>Reference (spatial identifier) *</label><input id="mRef" value="${esc(ev.ref)}" placeholder="REL-1.0">
<label>Association ID — Git commit * (mandatory anchor)</label>
<select id="mCommit">${selOptions(null,ev.commitId)}</select>
<label>Date</label><input id="mDate" type="date" value="${esc(ev.date||"")}">
<label>Description</label><textarea id="mDesc">${esc(ev.desc||"")}</textarea>
<div class="row"><button onclick="closeModal()">Cancel</button><button class="primary" id="mSave">Save</button></div>`);
document.getElementById("mSave").onclick=()=>{
const title=document.getElementById("mTitle").value.trim();
const ref=document.getElementById("mRef").value.trim();
const commitId=document.getElementById("mCommit").value;
if(!title||!ref||!commitId){ toast("Title, Reference and Association commit are required"); return; }
const obj={id:ev.id,title,ref,commitId,date:document.getElementById("mDate").value,desc:document.getElementById("mDesc").value.trim()};
if(isNew) state.events.push(obj); else Object.assign(state.events.find(e=>e.id===ev.id),obj);
save(); closeModal(); render(); selectedId=commitId; render();
toast(isNew?"Event anchored":"Event updated");
};
}
/* ================= Import / Export ================= */
function openImportModal(){
openModal(`<h3>Import State (JSON)</h3>
<label>Paste JSON with commits / branches / tags / events</label>
<textarea id="mJson" style="min-height:220px" placeholder='{"commits":[...],"branches":[...],"tags":[...],"events":[...]}'></textarea>
<div class="row"><button onclick="closeModal()">Cancel</button><button class="primary" id="mSave">Import</button></div>`);
document.getElementById("mSave").onclick=()=>{
try{
const d=JSON.parse(document.getElementById("mJson").value);
if(!Array.isArray(d.commits)) throw new Error("commits[] required");
state={
commits:d.commits.map(c=>({id:String(c.id),msg:c.msg||"(no message)",parents:(c.parents||[]).map(String),
branch:c.branch||"main",color:c.color||"#4f8cff",x:+c.x||100,y:+c.y||300,author:c.author||""})),
branches:(d.branches||[]).map(b=>({name:String(b.name),target:String(b.target)})),
tags:(d.tags||[]).map(t=>({name:String(t.name),target:String(t.target)})),
head:d.head||(d.branches&&d.branches[0]?d.branches[0].name:"main"),
events:(d.events||[]).map(e=>({id:e.id||uid("ev"),title:String(e.title||"Untitled"),
ref:String(e.ref||""),commitId:String(e.commitId||e.associationId||""),desc:e.desc||"",date:e.date||""}))
};
save(); closeModal(); render(); toast("Imported ✓");
}catch(err){ toast("Import failed: "+err.message); }
};
}
function doExport(){
const json=JSON.stringify(state,null,2);
openModal(`<h3>Export State</h3><textarea style="min-height:260px" readonly>${esc(json)}</textarea>
<div class="row"><button onclick="closeModal()">Close</button><button class="primary" id="mCopy">Copy</button></div>`);
document.getElementById("mCopy").onclick=()=>{ navigator.clipboard?.writeText(json); toast("Copied"); };
}
/* ================= Wire up ================= */
document.getElementById("btnAddCommit").onclick=()=>openCommitModal(null);
document.getElementById("btnAddBranch").onclick=openBranchModal;
document.getElementById("btnAddTag").onclick=openTagModal;
document.getElementById("btnAddEvent").onclick=()=>openEventModal(null);
document.getElementById("btnImport").onclick=openImportModal;
document.getElementById("btnExport").onclick=doExport;
document.getElementById("btnReset").onclick=()=>{ state=seedState(); save(); render(); toast("Reset to sample data"); };
window.closeModal=closeModal;
window.addEventListener("resize",render);
load();
render();
</script>
</body>
</html>
<!-- agent-meta {"model":"stealth/ox-alpha","provider":"openrouter","persona":"full","sessionId":"91c878e1-28ca-4072-bd5e-620bb88e18de","tokensIn":194081,"tokensOut":10007,"tokensTotal":204088,"cost":0,"turns":11,"toolCalls":10,"failedToolCalls":0,"timestamp":"2026-08-21T23:03:18.493Z"} -->