← Git Explorer results

Git Explorer

Minimax m3 · full

Initial view of Git Explorer

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 65,458 bytes · SHA-256 969d45091091
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Git & Context DAG Visualizer</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
  :root {
    --bg: #0d1117;
    --panel: #161b22;
    --panel-2: #1c2128;
    --border: #30363d;
    --text: #c9d1d9;
    --text-dim: #8b949e;
    --accent: #58a6ff;
    --accent-2: #f78166;
    --green: #3fb950;
    --purple: #bc8cff;
    --orange: #ffa657;
    --red: #f85149;
    --yellow: #d29922;
    --pink: #ff7b72;
    --shadow: 0 4px 14px rgba(0,0,0,0.5);
  }
  * { box-sizing: border-box; }
  html, body {
    margin: 0; padding: 0;
    background: var(--bg);
    color: var(--text);
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    height: 100vh; overflow: hidden;
  }
  #app {
    display: grid;
    grid-template-columns: 320px 1fr 360px;
    grid-template-rows: 50px 1fr;
    grid-template-areas:
      "header header header"
      "left center right";
    height: 100vh;
    width: 100vw;
  }
  header {
    grid-area: header;
    background: var(--panel);
    border-bottom: 1px solid var(--border);
    display: flex; align-items: center; justify-content: space-between;
    padding: 0 16px;
    z-index: 10;
  }
  header h1 {
    margin: 0; font-size: 16px; font-weight: 600;
    display: flex; align-items: center; gap: 10px;
  }
  header h1 .logo {
    width: 24px; height: 24px;
    background: linear-gradient(135deg, var(--accent), var(--purple));
    border-radius: 6px;
    display: inline-flex; align-items: center; justify-content: center;
    color: #fff; font-size: 14px; font-weight: 700;
  }
  .header-actions { display: flex; gap: 8px; align-items: center; }
  .btn {
    background: var(--panel-2);
    color: var(--text);
    border: 1px solid var(--border);
    padding: 6px 12px;
    border-radius: 6px;
    cursor: pointer;
    font-size: 12px;
    font-weight: 500;
    transition: all 0.15s;
    display: inline-flex; align-items: center; gap: 6px;
  }
  .btn:hover { background: #2d333b; border-color: #444c56; }
  .btn-primary { background: var(--accent); color: #0d1117; border-color: var(--accent); }
  .btn-primary:hover { background: #79b8ff; }
  .btn-danger { color: var(--red); border-color: rgba(248,81,73,0.4); }
  .btn-danger:hover { background: rgba(248,81,73,0.15); }
  .btn-sm { padding: 3px 8px; font-size: 11px; }
  #left {
    grid-area: left;
    background: var(--panel);
    border-right: 1px solid var(--border);
    overflow-y: auto;
    padding: 12px;
  }
  #center {
    grid-area: center;
    background: var(--bg);
    position: relative;
    overflow: hidden;
  }
  #right {
    grid-area: right;
    background: var(--panel);
    border-left: 1px solid var(--border);
    overflow-y: auto;
    padding: 12px;
  }
  .section-title {
    font-size: 11px;
    text-transform: uppercase;
    color: var(--text-dim);
    font-weight: 600;
    letter-spacing: 0.5px;
    margin: 12px 0 8px 0;
    display: flex; align-items: center; justify-content: space-between;
  }
  .section-title:first-child { margin-top: 0; }
  .branch-list { display: flex; flex-direction: column; gap: 4px; margin-bottom: 10px; }
  .branch-item {
    display: flex; align-items: center; gap: 8px;
    padding: 6px 8px;
    background: var(--panel-2);
    border: 1px solid var(--border);
    border-radius: 4px;
    font-size: 12px;
    cursor: pointer;
    transition: all 0.15s;
  }
  .branch-item:hover { border-color: var(--accent); }
  .branch-item.active { border-color: var(--accent); background: rgba(88,166,255,0.1); }
  .branch-color {
    width: 10px; height: 10px; border-radius: 50%;
    flex-shrink: 0;
  }
  .branch-name { flex: 1; font-weight: 500; }
  .branch-sha { color: var(--text-dim); font-family: monospace; font-size: 10px; }
  .event-list { display: flex; flex-direction: column; gap: 6px; }
  .event-card {
    background: var(--panel-2);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 8px 10px;
    cursor: pointer;
    transition: all 0.15s;
    font-size: 12px;
    position: relative;
  }
  .event-card:hover { border-color: var(--accent); transform: translateY(-1px); }
  .event-card.selected { border-color: var(--accent); background: rgba(88,166,255,0.08); }
  .event-card .event-title {
    font-weight: 600;
    margin-bottom: 4px;
    display: flex; align-items: center; gap: 6px;
  }
  .event-card .event-title .pin {
    color: var(--accent-2);
    font-size: 10px;
  }
  .event-card .event-meta {
    color: var(--text-dim);
    font-size: 11px;
    font-family: monospace;
  }
  .event-card .event-desc {
    margin-top: 4px;
    color: var(--text);
    font-size: 11px;
    line-height: 1.4;
  }
  #graph-container {
    width: 100%; height: 100%;
    position: relative;
  }
  #graph-svg {
    width: 100%; height: 100%;
    cursor: grab;
  }
  #graph-svg:active { cursor: grabbing; }
  .tooltip {
    position: absolute;
    background: rgba(28, 33, 40, 0.98);
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: 10px 12px;
    pointer-events: none;
    font-size: 12px;
    max-width: 280px;
    box-shadow: var(--shadow);
    z-index: 100;
    opacity: 0;
    transition: opacity 0.15s;
  }
  .tooltip.visible { opacity: 1; }
  .tooltip-title {
    font-weight: 600;
    color: var(--accent);
    margin-bottom: 4px;
  }
  .tooltip-meta {
    font-size: 10px;
    color: var(--text-dim);
    font-family: monospace;
    margin-bottom: 6px;
  }
  .tooltip-commit {
    background: rgba(88,166,255,0.1);
    border: 1px solid rgba(88,166,255,0.3);
    border-radius: 4px;
    padding: 4px 6px;
    margin-top: 4px;
    font-size: 10px;
    font-family: monospace;
  }
  .form-row {
    display: flex; flex-direction: column; gap: 4px;
    margin-bottom: 8px;
  }
  .form-row label {
    font-size: 11px; color: var(--text-dim); font-weight: 500;
  }
  .input, .select, .textarea {
    background: var(--bg);
    border: 1px solid var(--border);
    color: var(--text);
    padding: 6px 8px;
    border-radius: 4px;
    font-size: 12px;
    font-family: inherit;
    width: 100%;
  }
  .input:focus, .select:focus, .textarea:focus {
    outline: none;
    border-color: var(--accent);
  }
  .textarea { resize: vertical; min-height: 60px; font-family: inherit; }
  .form-actions {
    display: flex; gap: 6px; margin-top: 10px;
  }
  .form-actions .btn { flex: 1; justify-content: center; }
  .selected-commit-info {
    background: var(--panel-2);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 10px;
    margin-top: 10px;
    font-size: 12px;
  }
  .selected-commit-info .label {
    font-size: 10px; color: var(--text-dim); text-transform: uppercase;
    font-weight: 600; letter-spacing: 0.5px;
  }
  .selected-commit-info .value {
    font-family: monospace; color: var(--accent);
    font-size: 11px;
  }
  .selected-commit-info .msg {
    font-size: 12px; margin-top: 6px;
    line-height: 1.4;
  }
  .empty-state {
    color: var(--text-dim);
    font-size: 11px;
    text-align: center;
    padding: 12px;
    font-style: italic;
  }
  .modal-backdrop {
    position: fixed; inset: 0;
    background: rgba(0,0,0,0.6);
    z-index: 1000;
    display: none;
    align-items: center; justify-content: center;
  }
  .modal-backdrop.visible { display: flex; }
  .modal {
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: 10px;
    padding: 20px;
    width: 480px;
    max-width: 90vw;
    max-height: 90vh;
    overflow-y: auto;
    box-shadow: var(--shadow);
  }
  .modal h2 {
    margin: 0 0 16px 0;
    font-size: 16px;
    display: flex; align-items: center; justify-content: space-between;
  }
  .modal h2 .close-btn {
    background: none; border: none; color: var(--text-dim);
    cursor: pointer; font-size: 18px; padding: 0;
  }
  .modal h2 .close-btn:hover { color: var(--text); }
  .commit-node circle {
    cursor: pointer;
    transition: r 0.15s, stroke-width 0.15s;
  }
  .commit-node:hover circle {
    stroke-width: 3px;
  }
  .commit-node.highlighted circle {
    stroke: var(--accent);
    stroke-width: 4px;
    filter: drop-shadow(0 0 8px var(--accent));
  }
  .anchor-badge {
    cursor: pointer;
    transition: transform 0.2s;
  }
  .anchor-badge:hover {
    transform: scale(1.2);
  }
  .branch-label {
    font-size: 10px;
    font-weight: 600;
    font-family: monospace;
  }
  .commit-id-label {
    font-size: 9px;
    fill: var(--text-dim);
    font-family: monospace;
    pointer-events: none;
  }
  .commit-msg-label {
    font-size: 11px;
    fill: var(--text);
    pointer-events: none;
  }
  .link {
    fill: none;
    stroke-width: 2;
    opacity: 0.7;
  }
  .zoom-controls {
    position: absolute;
    bottom: 16px; right: 16px;
    display: flex; flex-direction: column;
    gap: 4px;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 4px;
    z-index: 5;
  }
  .zoom-controls .btn {
    padding: 4px 8px;
    font-size: 14px;
    min-width: 28px;
    justify-content: center;
  }
  .legend {
    position: absolute;
    bottom: 16px; left: 16px;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 8px 12px;
    font-size: 11px;
    z-index: 5;
    box-shadow: var(--shadow);
  }
  .legend-item {
    display: flex; align-items: center; gap: 6px;
    margin: 2px 0;
  }
  .legend-dot {
    width: 10px; height: 10px; border-radius: 50%;
  }
  .storage-status {
    font-size: 11px;
    color: var(--text-dim);
    display: flex; align-items: center; gap: 6px;
  }
  .storage-status .dot {
    width: 6px; height: 6px; border-radius: 50%;
    background: var(--green);
  }
  .badge {
    background: var(--accent);
    color: var(--bg);
    font-size: 10px;
    padding: 1px 6px;
    border-radius: 8px;
    font-weight: 600;
  }
  .delete-x {
    color: var(--text-dim);
    cursor: pointer;
    padding: 0 4px;
    font-size: 14px;
    line-height: 1;
  }
  .delete-x:hover { color: var(--red); }
  .tabs {
    display: flex;
    border-bottom: 1px solid var(--border);
    margin-bottom: 12px;
  }
  .tab {
    padding: 8px 12px;
    font-size: 12px;
    cursor: pointer;
    color: var(--text-dim);
    border-bottom: 2px solid transparent;
    transition: all 0.15s;
  }
  .tab:hover { color: var(--text); }
  .tab.active { color: var(--accent); border-bottom-color: var(--accent); }
</style>
</head>
<body>
<div id="app">
  <header>
    <h1><span class="logo">G</span> Git & Context DAG Visualizer</h1>
    <div class="header-actions">
      <span class="storage-status" id="storage-status"><span class="dot"></span> Saved</span>
      <button class="btn" id="btn-import">📥 Import</button>
      <button class="btn" id="btn-export">📤 Export</button>
      <button class="btn" id="btn-reset">🔄 Reset</button>
      <button class="btn btn-primary" id="btn-new-commit">+ New Commit</button>
    </div>
  </header>

  <aside id="left">
    <div class="section-title">
      <span>Branches</span>
      <button class="btn btn-sm" id="btn-new-branch">+ Branch</button>
    </div>
    <div class="branch-list" id="branch-list"></div>

    <div class="section-title">
      <span>Tags</span>
      <button class="btn btn-sm" id="btn-new-tag">+ Tag</button>
    </div>
    <div id="tag-list"></div>
  </aside>

  <main id="center">
    <div id="graph-container">
      <svg id="graph-svg"></svg>
    </div>
    <div class="tooltip" id="tooltip"></div>
    <div class="zoom-controls">
      <button class="btn" id="zoom-in">+</button>
      <button class="btn" id="zoom-out">−</button>
      <button class="btn" id="zoom-reset">⌂</button>
    </div>
    <div class="legend">
      <div class="legend-item"><div class="legend-dot" style="background:#8b949e"></div>Commit</div>
      <div class="legend-item"><div class="legend-dot" style="background:#f78166"></div>Context Event</div>
      <div class="legend-item"><div class="legend-dot" style="background:#3fb950"></div>Branch HEAD</div>
    </div>
  </main>

  <aside id="right">
    <div class="tabs">
      <div class="tab active" data-tab="events">Events</div>
      <div class="tab" data-tab="details">Selected</div>
      <div class="tab" data-tab="add">+ Add Event</div>
    </div>

    <div id="tab-events" class="tab-panel">
      <div class="section-title">
        <span>Contextual Events</span>
        <span class="badge" id="event-count">0</span>
      </div>
      <div class="event-list" id="event-list"></div>
    </div>

    <div id="tab-details" class="tab-panel" style="display:none">
      <div id="selected-info"></div>
    </div>

    <div id="tab-add" class="tab-panel" style="display:none">
      <div class="section-title">Add Contextual Event</div>
      <div class="form-row">
        <label>Title</label>
        <input type="text" class="input" id="new-event-title" placeholder="e.g. Code Freeze, Release v1.0" />
      </div>
      <div class="form-row">
        <label>Type</label>
        <select class="select" id="new-event-type">
          <option value="freeze">🧊 Code Freeze</option>
          <option value="release">🚀 Release</option>
          <option value="feature">✨ Feature Complete</option>
          <option value="bugfix">🐛 Bug Fix</option>
          <option value="review">👀 Code Review</option>
          <option value="milestone">🎯 Milestone</option>
          <option value="hotfix">🔥 Hotfix</option>
          <option value="custom">📌 Custom</option>
        </select>
      </div>
      <div class="form-row">
        <label>Description</label>
        <textarea class="textarea" id="new-event-desc" placeholder="Optional description..."></textarea>
      </div>
      <div class="form-row">
        <label>Associate with Commit</label>
        <select class="select" id="new-event-commit"></select>
      </div>
      <div class="form-actions">
        <button class="btn btn-primary" id="btn-create-event">Create Event</button>
      </div>
    </div>
  </aside>
</div>

<!-- Modal -->
<div class="modal-backdrop" id="modal-backdrop">
  <div class="modal" id="modal">
    <h2><span id="modal-title">Title</span><button class="close-btn" id="modal-close">×</button></h2>
    <div id="modal-content"></div>
  </div>
</div>

<script>
// ============================================================
// STATE & DATA MODEL
// ============================================================
const STORAGE_KEY = 'git_dag_visualizer_v1';

const EVENT_TYPES = {
  freeze:   { color: '#58a6ff', icon: '🧊', label: 'Code Freeze' },
  release:  { color: '#3fb950', icon: '🚀', label: 'Release' },
  feature:  { color: '#bc8cff', icon: '✨', label: 'Feature Complete' },
  bugfix:   { color: '#f85149', icon: '🐛', label: 'Bug Fix' },
  review:   { color: '#ffa657', icon: '👀', label: 'Code Review' },
  milestone:{ color: '#d29922', icon: '🎯', label: 'Milestone' },
  hotfix:   { color: '#ff7b72', icon: '🔥', label: 'Hotfix' },
  custom:   { color: '#f78166', icon: '📌', label: 'Custom' },
};

const BRANCH_COLORS = [
  '#58a6ff', '#3fb950', '#bc8cff', '#ffa657', '#f78166',
  '#ff7b72', '#d29922', '#a371f7', '#1f6feb', '#2ea043'
];

let state = {
  commits: [],      // { id, parents: [id], message, author, timestamp }
  branches: [],     // { name, head, color }
  tags: [],         // { name, commit }
  events: [],       // { id, title, type, description, commitId, timestamp }
  activeBranch: null,
  selectedCommit: null,
  selectedEvent: null,
};

// ============================================================
// PERSISTENCE
// ============================================================
function saveState() {
  try {
    const persist = {
      commits: state.commits,
      branches: state.branches,
      tags: state.tags,
      events: state.events,
      activeBranch: state.activeBranch,
    };
    localStorage.setItem(STORAGE_KEY, JSON.stringify(persist));
    flashStorageStatus('Saved');
  } catch (e) {
    flashStorageStatus('Error', true);
    console.error('Save failed', e);
  }
}

function loadState() {
  try {
    const raw = localStorage.getItem(STORAGE_KEY);
    if (raw) {
      const data = JSON.parse(raw);
      state.commits = data.commits || [];
      state.branches = data.branches || [];
      state.tags = data.tags || [];
      state.events = data.events || [];
      state.activeBranch = data.activeBranch || (state.branches[0]?.name ?? null);
      return true;
    }
  } catch (e) {
    console.error('Load failed', e);
  }
  return false;
}

function flashStorageStatus(text, isError = false) {
  const el = document.getElementById('storage-status');
  el.innerHTML = `<span class="dot" style="background:${isError ? '#f85149' : '#3fb950'}"></span> ${text}`;
  setTimeout(() => {
    el.innerHTML = `<span class="dot" style="background:#3fb950"></span> Saved`;
  }, 1500);
}

// ============================================================
// UTILITIES
// ============================================================
function uid(prefix = 'id') {
  return `${prefix}_${Math.random().toString(36).slice(2, 8)}${Date.now().toString(36).slice(-3)}`;
}

function shortHash(id) {
  return id.slice(-7);
}

function getCommit(id) {
  return state.commits.find(c => c.id === id);
}

function getCommitChildren(commitId) {
  return state.commits.filter(c => c.parents.includes(commitId));
}

function getEventsForCommit(commitId) {
  return state.events.filter(e => e.commitId === commitId);
}

function getBranchColor(branchName) {
  const branch = state.branches.find(b => b.name === branchName);
  return branch?.color || '#8b949e';
}

function findActiveBranchColor() {
  if (state.activeBranch) {
    return getBranchColor(state.activeBranch);
  }
  return '#8b949e';
}

// Assign columns (lanes) for commits via BFS-like algorithm
function assignLanes() {
  const lanes = {}; // commitId -> laneIndex
  const laneOwners = []; // laneIndex -> branch path (last commit id we tracked)
  const sorted = [...state.commits].sort((a, b) => a.timestamp - b.timestamp);

  // Find roots (commits with no parents in set)
  const roots = sorted.filter(c => c.parents.length === 0 || c.parents.every(p => !getCommit(p)));
  
  // Use a queue, processing commit by commit
  const visited = new Set();
  let nextLane = 0;
  
  // Start with roots in order
  const queue = [...roots];
  
  while (queue.length > 0) {
    const commit = queue.shift();
    if (visited.has(commit.id)) continue;
    visited.add(commit.id);

    // Find a lane: try to continue a parent's lane
    let assignedLane = -1;
    for (const parentId of commit.parents) {
      if (lanes[parentId] !== undefined) {
        assignedLane = lanes[parentId];
        break;
      }
    }
    if (assignedLane === -1) {
      assignedLane = nextLane++;
    }
    lanes[commit.id] = assignedLane;
    laneOwners[assignedLane] = commit.id;

    // Add children
    for (const child of getCommitChildren(commit.id)) {
      if (!visited.has(child.id)) queue.push(child);
    }
  }
  
  // Process any unvisited commits (in case of orphans)
  for (const commit of sorted) {
    if (!visited.has(commit.id)) {
      lanes[commit.id] = nextLane++;
    }
  }
  
  return { lanes, laneCount: nextLane };
}

// Assign x positions based on lanes and y based on timestamp order
function layoutGraph() {
  if (state.commits.length === 0) return { nodes: [], links: [], width: 800, height: 600 };

  const { lanes, laneCount } = assignLanes();
  const sorted = [...state.commits].sort((a, b) => a.timestamp - b.timestamp);
  
  const laneSpacing = 80;
  const rowSpacing = 80;
  const padding = 80;
  
  const nodes = sorted.map((commit, i) => ({
    ...commit,
    x: padding + (lanes[commit.id] || 0) * laneSpacing,
    y: padding + i * rowSpacing,
    lane: lanes[commit.id] || 0,
  }));

  // Determine which color each node should be (based on which branch points here)
  const commitBranch = {};
  for (const branch of state.branches) {
    let current = branch.head;
    const visited = new Set();
    while (current && !visited.has(current)) {
      visited.add(current);
      const commit = getCommit(current);
      if (!commit) break;
      if (!commitBranch[current]) commitBranch[current] = branch.color;
      current = commit.parents[0];
    }
  }
  nodes.forEach(n => {
    n.branchColor = commitBranch[n.id] || '#8b949e';
  });

  const links = [];
  for (const commit of state.commits) {
    for (const parentId of commit.parents) {
      const source = nodes.find(n => n.id === commit.id);
      const target = nodes.find(n => n.id === parentId);
      if (source && target) {
        links.push({ source, target });
      }
    }
  }

  const width = padding * 2 + Math.max(laneCount, 1) * laneSpacing;
  const height = padding * 2 + nodes.length * rowSpacing;

  return { nodes, links, width, height };
}

// ============================================================
// RENDERING
// ============================================================
let svg, gRoot, gLinks, gNodes, gAnchors, gLabels, zoom;
let simulation;

function initGraph() {
  svg = d3.select('#graph-svg');
  svg.selectAll('*').remove();
  
  // Add defs for arrowheads and gradients
  const defs = svg.append('defs');
  
  // Grid pattern
  const pattern = defs.append('pattern')
    .attr('id', 'grid')
    .attr('width', 40)
    .attr('height', 40)
    .attr('patternUnits', 'userSpaceOnUse');
  pattern.append('circle')
    .attr('cx', 1).attr('cy', 1).attr('r', 1)
    .attr('fill', '#21262d');
  
  // Background grid
  svg.append('rect')
    .attr('width', '100%')
    .attr('height', '100%')
    .attr('fill', 'url(#grid)');

  gRoot = svg.append('g').attr('class', 'root');
  gLinks = gRoot.append('g').attr('class', 'links');
  gNodes = gRoot.append('g').attr('class', 'nodes');
  gAnchors = gRoot.append('g').attr('class', 'anchors');
  gLabels = gRoot.append('g').attr('class', 'labels');

  // Zoom
  zoom = d3.zoom()
    .scaleExtent([0.2, 3])
    .on('zoom', (event) => {
      gRoot.attr('transform', event.transform);
    });
  svg.call(zoom);

  // Zoom controls
  document.getElementById('zoom-in').onclick = () => svg.transition().duration(300).call(zoom.scaleBy, 1.3);
  document.getElementById('zoom-out').onclick = () => svg.transition().duration(300).call(zoom.scaleBy, 1 / 1.3);
  document.getElementById('zoom-reset').onclick = () => svg.transition().duration(300).call(zoom.transform, d3.zoomIdentity);
}

function renderGraph() {
  if (!svg) return;
  const { nodes, links, width, height } = layoutGraph();
  
  // Clear
  gLinks.selectAll('*').remove();
  gNodes.selectAll('*').remove();
  gAnchors.selectAll('*').remove();
  gLabels.selectAll('*').remove();

  if (nodes.length === 0) {
    const w = document.getElementById('graph-container').clientWidth;
    const h = document.getElementById('graph-container').clientHeight;
    svg.append('text')
      .attr('x', w / 2).attr('y', h / 2)
      .attr('text-anchor', 'middle')
      .attr('fill', '#8b949e')
      .attr('font-size', 14)
      .text('No commits yet. Click "New Commit" to start.');
    return;
  }

  // Links - curved paths
  const link = gLinks.selectAll('path')
    .data(links)
    .enter().append('path')
    .attr('class', 'link')
    .attr('d', d => {
      const dx = d.target.x - d.source.x;
      const dy = d.target.y - d.source.y;
      const dr = Math.max(30, Math.abs(dx) + Math.abs(dy) * 0.5);
      return `M${d.source.x},${d.source.y} A${dr},${dr} 0 0,1 ${d.target.x},${d.target.y}`;
    })
    .attr('stroke', d => d.source.branchColor || '#8b949e');

  // Commit nodes
  const node = gNodes.selectAll('g')
    .data(nodes, d => d.id)
    .enter().append('g')
    .attr('class', 'commit-node')
    .attr('transform', d => `translate(${d.x}, ${d.y})`)
    .attr('data-id', d => d.id)
    .style('cursor', 'pointer');

  node.append('circle')
    .attr('r', 12)
    .attr('fill', d => d.branchColor || '#8b949e')
    .attr('stroke', '#0d1117')
    .attr('stroke-width', 2);

  // Outer ring for selected
  node.append('circle')
    .attr('class', 'selection-ring')
    .attr('r', 16)
    .attr('fill', 'none')
    .attr('stroke', '#58a6ff')
    .attr('stroke-width', 0)
    .attr('opacity', 0);

  // Hover/click handlers
  node
    .on('mouseenter', (event, d) => {
      const events = getEventsForCommit(d.id);
      showTooltip(event, `
        <div class="tooltip-title">${escapeHtml(d.message)}</div>
        <div class="tooltip-meta">${shortHash(d.id)} · ${d.author || 'unknown'}</div>
        ${events.length > 0 ? `
          <div style="margin-top:6px;font-size:10px;color:#8b949e;text-transform:uppercase;font-weight:600;">${events.length} event${events.length > 1 ? 's' : ''} anchored:</div>
          ${events.map(e => `
            <div style="margin-top:3px;display:flex;align-items:center;gap:4px;">
              <span style="color:${EVENT_TYPES[e.type]?.color || '#f78166'}">${EVENT_TYPES[e.type]?.icon || '📌'}</span>
              <span>${escapeHtml(e.title)}</span>
            </div>
          `).join('')}
        ` : ''}
      `);
    })
    .on('mousemove', moveTooltip)
    .on('mouseleave', hideTooltip)
    .on('click', (event, d) => {
      event.stopPropagation();
      selectCommit(d.id);
    });

  // Commit labels (message)
  gLabels.selectAll('g.commit-label')
    .data(nodes, d => d.id)
    .enter().append('g')
    .attr('class', 'commit-label')
    .attr('transform', d => `translate(${d.x + 18}, ${d.y + 4})`)
    .each(function(d) {
      const g = d3.select(this);
      g.append('text')
        .attr('class', 'commit-msg-label')
        .text(truncate(d.message, 32))
        .attr('fill', d.id === state.selectedCommit ? '#58a6ff' : 'var(--text)');
      g.append('text')
        .attr('class', 'commit-id-label')
        .attr('y', 14)
        .text(shortHash(d.id));
    });

  // Branch labels at head
  for (const branch of state.branches) {
    const head = nodes.find(n => n.id === branch.head);
    if (head) {
      const labelG = gLabels.append('g')
        .attr('transform', `translate(${head.x - 50}, ${head.y - 30})`)
        .style('cursor', 'pointer');
      labelG.append('rect')
        .attr('x', 0).attr('y', 0)
        .attr('width', 92).attr('height', 18)
        .attr('rx', 9)
        .attr('fill', branch.color)
        .attr('opacity', 0.95);
      labelG.append('text')
        .attr('class', 'branch-label')
        .attr('x', 46).attr('y', 12)
        .attr('text-anchor', 'middle')
        .attr('fill', '#0d1117')
        .text(branch.name);
      labelG.on('click', () => selectBranch(branch.name));
    }
  }

  // Tag labels
  for (const tag of state.tags) {
    const commit = nodes.find(n => n.id === tag.commit);
    if (commit) {
      const tagG = gLabels.append('g')
        .attr('transform', `translate(${commit.x + 22}, ${commit.y + 22})`)
        .style('cursor', 'pointer');
      tagG.append('rect')
        .attr('x', 0).attr('y', 0)
        .attr('width', 60).attr('height', 14)
        .attr('rx', 3)
        .attr('fill', '#d29922');
      tagG.append('text')
        .attr('class', 'tag-label')
        .attr('x', 30).attr('y', 10)
        .attr('text-anchor', 'middle')
        .attr('fill', '#0d1117')
        .attr('font-size', 9)
        .attr('font-weight', 600)
        .text('🏷 ' + tag.name);
    }
  }

  // Anchored events - positioned around commit nodes
  for (const commit of nodes) {
    const events = getEventsForCommit(commit.id);
    events.forEach((event, idx) => {
      const angle = (idx / Math.max(events.length, 1)) * Math.PI * 2 - Math.PI / 2;
      const radius = 32;
      const ex = commit.x + Math.cos(angle) * radius;
      const ey = commit.y + Math.sin(angle) * radius;
      
      const etype = EVENT_TYPES[event.type] || EVENT_TYPES.custom;
      
      const anchorG = gAnchors.append('g')
        .attr('class', 'anchor-badge')
        .attr('transform', `translate(${ex}, ${ey})`)
        .style('cursor', 'pointer')
        .attr('data-event-id', event.id);
      
      anchorG.append('circle')
        .attr('r', 11)
        .attr('fill', etype.color)
        .attr('stroke', '#0d1117')
        .attr('stroke-width', 2)
        .attr('class', 'anchor-circle');
      
      anchorG.append('text')
        .attr('text-anchor', 'middle')
        .attr('y', 4)
        .attr('font-size', 11)
        .attr('pointer-events', 'none')
        .text(etype.icon);
      
      // Connection line from commit to anchor (subtle)
      gLinks.append('line')
        .attr('x1', commit.x).attr('y1', commit.y)
        .attr('x2', ex).attr('y2', ey)
        .attr('stroke', etype.color)
        .attr('stroke-width', 1)
        .attr('stroke-dasharray', '2,2')
        .attr('opacity', 0.5);
      
      anchorG
        .on('mouseenter', (e) => {
          showTooltip(e, `
            <div class="tooltip-title">${etype.icon} ${escapeHtml(event.title)}</div>
            <div class="tooltip-meta">${etype.label}</div>
            ${event.description ? `<div style="margin-bottom:6px;font-size:11px;">${escapeHtml(event.description)}</div>` : ''}
            <div class="tooltip-commit">📌 Anchored to: ${shortHash(event.commitId)} "${escapeHtml(commit.message)}"</div>
          `);
        })
        .on('mousemove', moveTooltip)
        .on('mouseleave', hideTooltip)
        .on('click', (e) => {
          e.stopPropagation();
          selectEvent(event.id);
        });
    });
  }

  // Highlight selected
  applySelection();
  applyEventHighlight();
}

function truncate(str, n) {
  if (!str) return '';
  return str.length > n ? str.slice(0, n - 1) + '…' : str;
}

function escapeHtml(str) {
  if (!str) return '';
  return String(str).replace(/[&<>"']/g, c => ({
    '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
  }[c]));
}

// ============================================================
// TOOLTIPS
// ============================================================
function showTooltip(event, html) {
  const tooltip = document.getElementById('tooltip');
  tooltip.innerHTML = html;
  tooltip.classList.add('visible');
  moveTooltip(event);
}

function moveTooltip(event) {
  const tooltip = document.getElementById('tooltip');
  const offset = 14;
  let x = event.clientX + offset;
  let y = event.clientY + offset;
  const rect = tooltip.getBoundingClientRect();
  if (x + rect.width > window.innerWidth) x = event.clientX - rect.width - offset;
  if (y + rect.height > window.innerHeight) y = event.clientY - rect.height - offset;
  tooltip.style.left = x + 'px';
  tooltip.style.top = y + 'px';
}

function hideTooltip() {
  document.getElementById('tooltip').classList.remove('visible');
}

// ============================================================
// SIDEBAR RENDERING
// ============================================================
function renderBranches() {
  const list = document.getElementById('branch-list');
  list.innerHTML = '';
  if (state.branches.length === 0) {
    list.innerHTML = '<div class="empty-state">No branches yet</div>';
    return;
  }
  for (const branch of state.branches) {
    const item = document.createElement('div');
    item.className = 'branch-item' + (branch.name === state.activeBranch ? ' active' : '');
    item.innerHTML = `
      <div class="branch-color" style="background:${branch.color}"></div>
      <div style="flex:1;min-width:0;">
        <div class="branch-name">${escapeHtml(branch.name)}${branch.name === state.activeBranch ? ' <span style="font-size:9px;color:#8b949e;">●</span>' : ''}</div>
        <div class="branch-sha">${shortHash(branch.head)}</div>
      </div>
      <span class="delete-x" data-branch="${escapeHtml(branch.name)}" title="Delete branch">×</span>
    `;
    item.onclick = (e) => {
      if (e.target.classList.contains('delete-x')) {
        deleteBranch(branch.name);
      } else {
        checkoutBranch(branch.name);
      }
    };
    list.appendChild(item);
  }
}

function renderTags() {
  const list = document.getElementById('tag-list');
  list.innerHTML = '';
  if (state.tags.length === 0) {
    list.innerHTML = '<div class="empty-state">No tags yet</div>';
    return;
  }
  for (const tag of state.tags) {
    const item = document.createElement('div');
    item.className = 'branch-item';
    item.innerHTML = `
      <div class="branch-color" style="background:#d29922"></div>
      <div style="flex:1;min-width:0;">
        <div class="branch-name">🏷 ${escapeHtml(tag.name)}</div>
        <div class="branch-sha">${shortHash(tag.commit)}</div>
      </div>
      <span class="delete-x" data-tag="${escapeHtml(tag.name)}">×</span>
    `;
    item.onclick = (e) => {
      if (e.target.classList.contains('delete-x')) {
        deleteTag(tag.name);
      } else {
        selectCommit(tag.commit);
      }
    };
    list.appendChild(item);
  }
}

function renderEvents() {
  const list = document.getElementById('event-list');
  list.innerHTML = '';
  const sorted = [...state.events].sort((a, b) => b.timestamp - a.timestamp);
  document.getElementById('event-count').textContent = sorted.length;
  if (sorted.length === 0) {
    list.innerHTML = '<div class="empty-state">No events yet.<br>Add one from the "+ Add Event" tab.</div>';
    return;
  }
  for (const event of sorted) {
    const etype = EVENT_TYPES[event.type] || EVENT_TYPES.custom;
    const commit = getCommit(event.commitId);
    const card = document.createElement('div');
    card.className = 'event-card' + (state.selectedEvent === event.id ? ' selected' : '');
    card.innerHTML = `
      <div class="event-title">
        <span class="pin">📍</span>
        <span>${etype.icon} ${escapeHtml(event.title)}</span>
      </div>
      <div class="event-meta">
        ${etype.label} → ${commit ? shortHash(event.commitId) : '???'} ${commit ? `· "${truncate(commit.message, 20)}"` : ''}
      </div>
      ${event.description ? `<div class="event-desc">${escapeHtml(event.description)}</div>` : ''}
    `;
    card.onclick = () => selectEvent(event.id);
    list.appendChild(card);
  }
}

function renderSelectedInfo() {
  const container = document.getElementById('selected-info');
  container.innerHTML = '';
  
  if (state.selectedEvent) {
    const event = state.events.find(e => e.id === state.selectedEvent);
    if (event) {
      const etype = EVENT_TYPES[event.type] || EVENT_TYPES.custom;
      const commit = getCommit(event.commitId);
      container.innerHTML = `
        <div class="section-title">Selected Event</div>
        <div class="selected-commit-info">
          <div class="label">Title</div>
          <div style="font-weight:600;margin:4px 0;">${etype.icon} ${escapeHtml(event.title)}</div>
          <div class="label" style="margin-top:8px;">Type</div>
          <div class="value" style="color:${etype.color}">${etype.label}</div>
          ${event.description ? `
            <div class="label" style="margin-top:8px;">Description</div>
            <div class="msg">${escapeHtml(event.description)}</div>
          ` : ''}
          <div class="label" style="margin-top:8px;">Anchored Commit</div>
          <div class="value">${shortHash(event.commitId)}</div>
          <div class="msg">"${escapeHtml(commit?.message || 'NOT FOUND')}"</div>
          <div class="label" style="margin-top:8px;">Created</div>
          <div class="msg">${new Date(event.timestamp).toLocaleString()}</div>
        </div>
        <div style="display:flex;gap:6px;margin-top:10px;">
          <button class="btn btn-sm" id="edit-event-btn">Edit</button>
          <button class="btn btn-sm btn-danger" id="delete-event-btn">Delete</button>
        </div>
      `;
      document.getElementById('edit-event-btn').onclick = () => openEventEdit(event.id);
      document.getElementById('delete-event-btn').onclick = () => deleteEvent(event.id);
      return;
    }
  }
  
  if (state.selectedCommit) {
    const commit = getCommit(state.selectedCommit);
    if (commit) {
      const events = getEventsForCommit(commit.id);
      const branches = state.branches.filter(b => b.head === commit.id);
      const tags = state.tags.filter(t => t.commit === commit.id);
      container.innerHTML = `
        <div class="section-title">Selected Commit</div>
        <div class="selected-commit-info">
          <div class="label">Hash</div>
          <div class="value">${commit.id}</div>
          <div class="label" style="margin-top:8px;">Message</div>
          <div class="msg">${escapeHtml(commit.message)}</div>
          <div class="label" style="margin-top:8px;">Author</div>
          <div class="msg">${escapeHtml(commit.author || 'unknown')}</div>
          <div class="label" style="margin-top:8px;">Parents</div>
          <div class="msg">${commit.parents.length === 0 ? '<em>root</em>' : commit.parents.map(p => shortHash(p)).join(', ')}</div>
          ${branches.length > 0 ? `
            <div class="label" style="margin-top:8px;">Branches</div>
            <div class="msg">${branches.map(b => `<span style="color:${b.color}">●</span> ${escapeHtml(b.name)}`).join(' ')}</div>
          ` : ''}
          ${tags.length > 0 ? `
            <div class="label" style="margin-top:8px;">Tags</div>
            <div class="msg">${tags.map(t => `🏷 ${escapeHtml(t.name)}`).join(', ')}</div>
          ` : ''}
          <div class="label" style="margin-top:8px;">Anchored Events (${events.length})</div>
          ${events.length > 0 ? `
            <div style="margin-top:6px;display:flex;flex-direction:column;gap:4px;">
              ${events.map(e => {
                const et = EVENT_TYPES[e.type] || EVENT_TYPES.custom;
                return `<div style="display:flex;align-items:center;gap:6px;padding:4px 6px;background:var(--bg);border-radius:4px;font-size:11px;">
                  <span>${et.icon}</span><span>${escapeHtml(e.title)}</span>
                </div>`;
              }).join('')}
            </div>
            <button class="btn btn-sm" id="add-event-for-commit" style="margin-top:8px;">+ Add Event Here</button>
          ` : `
            <button class="btn btn-sm" id="add-event-for-commit" style="margin-top:8px;">+ Anchor Event Here</button>
          `}
        </div>
        <div style="display:flex;gap:6px;margin-top:10px;">
          <button class="btn btn-sm" id="edit-commit-btn">Edit</button>
          <button class="btn btn-sm" id="new-branch-on-btn">+ Branch Here</button>
          <button class="btn btn-sm" id="new-tag-on-btn">+ Tag</button>
          <button class="btn btn-sm btn-danger" id="delete-commit-btn">Delete</button>
        </div>
      `;
      document.getElementById('edit-commit-btn').onclick = () => openCommitEdit(commit.id);
      document.getElementById('delete-commit-btn').onclick = () => deleteCommit(commit.id);
      document.getElementById('new-branch-on-btn').onclick = () => createBranchOnCommit(commit.id);
      document.getElementById('new-tag-on-btn').onclick = () => createTagOnCommit(commit.id);
      const addEvBtn = document.getElementById('add-event-for-commit');
      if (addEvBtn) addEvBtn.onclick = () => { switchTab('add'); document.getElementById('new-event-commit').value = commit.id; };
      return;
    }
  }
  
  container.innerHTML = '<div class="empty-state">Select a commit or event to see details</div>';
}

// ============================================================
// SELECTION
// ============================================================
function selectCommit(id) {
  state.selectedCommit = id;
  state.selectedEvent = null;
  applySelection();
  applyEventHighlight();
  renderEvents();
  renderSelectedInfo();
  updateDetailsTab();
}

function selectEvent(id) {
  const event = state.events.find(e => e.id === id);
  if (!event) return;
  state.selectedEvent = id;
  state.selectedCommit = event.commitId;
  applySelection();
  applyEventHighlight();
  renderEvents();
  renderSelectedInfo();
  updateDetailsTab();
  // Pan to the event
  const node = gNodes.selectAll('.commit-node').filter(d => d.id === event.commitId);
  if (!node.empty()) {
    const d = node.datum();
    const svgEl = document.getElementById('graph-svg');
    const rect = svgEl.getBoundingClientRect();
    const transform = d3.zoomIdentity
      .translate(rect.width / 2 - d.x, rect.height / 2 - d.y)
      .scale(1);
    svg.transition().duration(500).call(zoom.transform, transform);
  }
}

function selectBranch(name) {
  state.activeBranch = name;
  const branch = state.branches.find(b => b.name === name);
  if (branch) {
    selectCommit(branch.head);
  }
  saveState();
  renderBranches();
}

function applySelection() {
  gNodes.selectAll('.commit-node').each(function(d) {
    const node = d3.select(this);
    if (d.id === state.selectedCommit) {
      node.classed('highlighted', true);
    } else {
      node.classed('highlighted', false);
    }
  });
}

function applyEventHighlight() {
  // Highlight an anchor if its event is selected
  gAnchors.selectAll('.anchor-badge').each(function() {
    const evId = d3.select(this).attr('data-event-id');
    const circle = d3.select(this).select('.anchor-circle');
    if (evId === state.selectedEvent) {
      circle.attr('stroke', '#fff').attr('stroke-width', 3).attr('r', 13);
    } else {
      circle.attr('stroke', '#0d1117').attr('stroke-width', 2).attr('r', 11);
    }
  });
}

// ============================================================
// CRUD OPERATIONS
// ============================================================
function createCommit({ message, author, parents }) {
  const commit = {
    id: uid('c'),
    parents: parents || [],
    message: message || 'Untitled commit',
    author: author || 'user',
    timestamp: Date.now(),
  };
  state.commits.push(commit);
  // Move active branch head
  if (state.activeBranch) {
    const branch = state.branches.find(b => b.name === state.activeBranch);
    if (branch) branch.head = commit.id;
  }
  saveState();
  renderAll();
  return commit;
}

function updateCommit(id, updates) {
  const commit = getCommit(id);
  if (!commit) return;
  Object.assign(commit, updates);
  saveState();
  renderAll();
}

function deleteCommit(id) {
  if (!confirm('Delete this commit? Children will be re-parented or orphaned.')) return;
  const commit = state.commits.find(c => c.id === id);
  if (!commit) return;
  // Re-parent children to commit's parent
  const children = state.commits.filter(c => c.parents.includes(id));
  const newParent = commit.parents[0] || null;
  for (const child of children) {
    child.parents = child.parents.filter(p => p !== id);
    if (newParent && !child.parents.includes(newParent)) {
      child.parents.unshift(newParent);
    }
  }
  // Remove from branches
  for (const branch of state.branches) {
    if (branch.head === id) {
      branch.head = newParent;
    }
  }
  // Remove events
  state.events = state.events.filter(e => e.commitId !== id);
  // Remove commit
  state.commits = state.commits.filter(c => c.id !== id);
  if (state.selectedCommit === id) state.selectedCommit = null;
  saveState();
  renderAll();
}

function createBranch(name, head) {
  if (state.branches.find(b => b.name === name)) {
    alert('Branch name already exists');
    return;
  }
  const color = BRANCH_COLORS[state.branches.length % BRANCH_COLORS.length];
  state.branches.push({ name, head, color });
  state.activeBranch = name;
  saveState();
  renderAll();
}

function deleteBranch(name) {
  if (state.branches.length <= 1) {
    alert('Cannot delete the only branch');
    return;
  }
  if (!confirm(`Delete branch "${name}"?`)) return;
  state.branches = state.branches.filter(b => b.name !== name);
  if (state.activeBranch === name) {
    state.activeBranch = state.branches[0]?.name;
  }
  saveState();
  renderAll();
}

function checkoutBranch(name) {
  state.activeBranch = name;
  saveState();
  renderBranches();
}

function createTag(name, commitId) {
  if (state.tags.find(t => t.name === name)) {
    alert('Tag name already exists');
    return;
  }
  state.tags.push({ name, commit: commitId });
  saveState();
  renderAll();
}

function deleteTag(name) {
  state.tags = state.tags.filter(t => t.name !== name);
  saveState();
  renderAll();
}

function createEvent({ title, type, description, commitId }) {
  const event = {
    id: uid('e'),
    title, type, description,
    commitId,
    timestamp: Date.now(),
  };
  state.events.push(event);
  saveState();
  renderAll();
  return event;
}

function updateEvent(id, updates) {
  const event = state.events.find(e => e.id === id);
  if (!event) return;
  Object.assign(event, updates);
  saveState();
  renderAll();
}

function deleteEvent(id) {
  state.events = state.events.filter(e => e.id !== id);
  if (state.selectedEvent === id) state.selectedEvent = null;
  saveState();
  renderAll();
}

// ============================================================
// MODALS
// ============================================================
function openModal(title, contentHtml) {
  document.getElementById('modal-title').textContent = title;
  document.getElementById('modal-content').innerHTML = contentHtml;
  document.getElementById('modal-backdrop').classList.add('visible');
}

function closeModal() {
  document.getElementById('modal-backdrop').classList.remove('visible');
}

function openCommitEdit(id) {
  const commit = getCommit(id);
  if (!commit) return;
  const branches = state.branches.map(b => `<option value="${b.id || b.name}">${escapeHtml(b.name)} @ ${shortHash(b.head)}</option>`).join('');
  openModal('Edit Commit', `
    <div class="form-row">
      <label>Message</label>
      <input type="text" class="input" id="edit-commit-msg" value="${escapeHtml(commit.message)}" />
    </div>
    <div class="form-row">
      <label>Author</label>
      <input type="text" class="input" id="edit-commit-author" value="${escapeHtml(commit.author)}" />
    </div>
    <div class="form-row">
      <label>Parent Commit (single)</label>
      <select class="select" id="edit-commit-parent">
        <option value="">(root)</option>
        ${state.commits.filter(c => c.id !== id).map(c => `<option value="${c.id}" ${commit.parents[0] === c.id ? 'selected' : ''}>${shortHash(c.id)} - ${escapeHtml(truncate(c.message, 40))}</option>`).join('')}
      </select>
    </div>
    <div class="form-actions">
      <button class="btn" id="cancel-edit-commit">Cancel</button>
      <button class="btn btn-primary" id="save-edit-commit">Save</button>
    </div>
  `);
  document.getElementById('cancel-edit-commit').onclick = closeModal;
  document.getElementById('save-edit-commit').onclick = () => {
    const message = document.getElementById('edit-commit-msg').value;
    const author = document.getElementById('edit-commit-author').value;
    const parent = document.getElementById('edit-commit-parent').value || null;
    updateCommit(id, {
      message,
      author,
      parents: parent ? [parent] : [],
    });
    closeModal();
  };
}

function openEventEdit(id) {
  const event = state.events.find(e => e.id === id);
  if (!event) return;
  openModal('Edit Event', `
    <div class="form-row">
      <label>Title</label>
      <input type="text" class="input" id="edit-event-title" value="${escapeHtml(event.title)}" />
    </div>
    <div class="form-row">
      <label>Type</label>
      <select class="select" id="edit-event-type">
        ${Object.entries(EVENT_TYPES).map(([k, v]) => `<option value="${k}" ${event.type === k ? 'selected' : ''}>${v.icon} ${v.label}</option>`).join('')}
      </select>
    </div>
    <div class="form-row">
      <label>Description</label>
      <textarea class="textarea" id="edit-event-desc">${escapeHtml(event.description || '')}</textarea>
    </div>
    <div class="form-row">
      <label>Anchor to Commit</label>
      <select class="select" id="edit-event-commit">
        ${state.commits.map(c => `<option value="${c.id}" ${event.commitId === c.id ? 'selected' : ''}>${shortHash(c.id)} - ${escapeHtml(truncate(c.message, 40))}</option>`).join('')}
      </select>
    </div>
    <div class="form-actions">
      <button class="btn" id="cancel-edit-event">Cancel</button>
      <button class="btn btn-primary" id="save-edit-event">Save</button>
    </div>
  `);
  document.getElementById('cancel-edit-event').onclick = closeModal;
  document.getElementById('save-edit-event').onclick = () => {
    updateEvent(id, {
      title: document.getElementById('edit-event-title').value,
      type: document.getElementById('edit-event-type').value,
      description: document.getElementById('edit-event-desc').value,
      commitId: document.getElementById('edit-event-commit').value,
    });
    closeModal();
  };
}

function openNewCommit() {
  const parentOptions = state.commits.map(c => `<option value="${c.id}">${shortHash(c.id)} - ${escapeHtml(truncate(c.message, 40))}</option>`).join('');
  const branchOptions = state.branches.map(b => `<option value="${b.name}" ${b.name === state.activeBranch ? 'selected' : ''}>${escapeHtml(b.name)} @ ${shortHash(b.head)}</option>`).join('');
  openModal('New Commit', `
    <div class="form-row">
      <label>Message</label>
      <input type="text" class="input" id="new-commit-msg" placeholder="e.g. Add login feature" />
    </div>
    <div class="form-row">
      <label>Author</label>
      <input type="text" class="input" id="new-commit-author" value="user" />
    </div>
    <div class="form-row">
      <label>Branch</label>
      <select class="select" id="new-commit-branch">${branchOptions}</select>
    </div>
    <div class="form-row">
      <label>Parent Commit (from selected branch)</label>
      <select class="select" id="new-commit-parent">
        <option value="">(root)</option>
        ${parentOptions}
      </select>
    </div>
    <div class="form-actions">
      <button class="btn" id="cancel-new-commit">Cancel</button>
      <button class="btn btn-primary" id="save-new-commit">Create</button>
    </div>
  `);
  document.getElementById('cancel-new-commit').onclick = closeModal;
  document.getElementById('save-new-commit').onclick = () => {
    const message = document.getElementById('new-commit-msg').value || 'Untitled commit';
    const author = document.getElementById('new-commit-author').value || 'user';
    const branchName = document.getElementById('new-commit-branch').value;
    const parent = document.getElementById('new-commit-parent').value || null;
    if (branchName) state.activeBranch = branchName;
    const commit = createCommit({ message, author, parents: parent ? [parent] : [] });
    if (branchName) {
      const branch = state.branches.find(b => b.name === branchName);
      if (branch) branch.head = commit.id;
    }
    closeModal();
  };
}

function openNewBranch() {
  const commitOptions = state.commits.map(c => `<option value="${c.id}">${shortHash(c.id)} - ${escapeHtml(truncate(c.message, 40))}</option>`).join('');
  openModal('New Branch', `
    <div class="form-row">
      <label>Branch Name</label>
      <input type="text" class="input" id="new-branch-name" placeholder="e.g. feature/auth" />
    </div>
    <div class="form-row">
      <label>Branch from Commit</label>
      <select class="select" id="new-branch-commit">${commitOptions}</select>
    </div>
    <div class="form-actions">
      <button class="btn" id="cancel-new-branch">Cancel</button>
      <button class="btn btn-primary" id="save-new-branch">Create</button>
    </div>
  `);
  document.getElementById('cancel-new-branch').onclick = closeModal;
  document.getElementById('save-new-branch').onclick = () => {
    const name = document.getElementById('new-branch-name').value.trim();
    const head = document.getElementById('new-branch-commit').value;
    if (!name) { alert('Please enter a branch name'); return; }
    createBranch(name, head);
    closeModal();
  };
}

function createBranchOnCommit(commitId) {
  const name = prompt('Branch name:');
  if (!name) return;
  createBranch(name, commitId);
}

function createTagOnCommit(commitId) {
  const name = prompt('Tag name:');
  if (!name) return;
  createTag(name, commitId);
}

function openNewTag() {
  const commitOptions = state.commits.map(c => `<option value="${c.id}">${shortHash(c.id)} - ${escapeHtml(truncate(c.message, 40))}</option>`).join('');
  openModal('New Tag', `
    <div class="form-row">
      <label>Tag Name</label>
      <input type="text" class="input" id="new-tag-name" placeholder="e.g. v1.0.0" />
    </div>
    <div class="form-row">
      <label>Tag at Commit</label>
      <select class="select" id="new-tag-commit">${commitOptions}</select>
    </div>
    <div class="form-actions">
      <button class="btn" id="cancel-new-tag">Cancel</button>
      <button class="btn btn-primary" id="save-new-tag">Create</button>
    </div>
  `);
  document.getElementById('cancel-new-tag').onclick = closeModal;
  document.getElementById('save-new-tag').onclick = () => {
    const name = document.getElementById('new-tag-name').value.trim();
    const commit = document.getElementById('new-tag-commit').value;
    if (!name) { alert('Please enter a tag name'); return; }
    createTag(name, commit);
    closeModal();
  };
}

function openImport() {
  openModal('Import Data', `
    <p style="font-size:12px;color:var(--text-dim);margin:0 0 10px 0;">Paste JSON data with commits, branches, tags, and events. Existing data will be replaced.</p>
    <div class="form-row">
      <label>JSON Data</label>
      <textarea class="textarea" id="import-data" style="min-height:200px;font-family:monospace;font-size:11px;" placeholder='{"commits":[...], "branches":[...], "tags":[...], "events":[...]}'></textarea>
    </div>
    <div class="form-actions">
      <button class="btn" id="load-sample">Load Sample</button>
      <button class="btn" id="cancel-import">Cancel</button>
      <button class="btn btn-primary" id="save-import">Import</button>
    </div>
  `);
  document.getElementById('cancel-import').onclick = closeModal;
  document.getElementById('load-sample').onclick = () => {
    document.getElementById('import-data').value = JSON.stringify(getSampleData(), null, 2);
  };
  document.getElementById('save-import').onclick = () => {
    try {
      const data = JSON.parse(document.getElementById('import-data').value);
      if (data.commits) state.commits = data.commits;
      if (data.branches) state.branches = data.branches;
      if (data.tags) state.tags = data.tags;
      if (data.events) state.events = data.events;
      state.activeBranch = data.activeBranch || (state.branches[0]?.name ?? null);
      state.selectedCommit = null;
      state.selectedEvent = null;
      saveState();
      renderAll();
      closeModal();
    } catch (e) {
      alert('Invalid JSON: ' + e.message);
    }
  };
}

function openExport() {
  const data = {
    commits: state.commits,
    branches: state.branches,
    tags: state.tags,
    events: state.events,
    activeBranch: state.activeBranch,
    exportedAt: new Date().toISOString(),
  };
  const json = JSON.stringify(data, null, 2);
  openModal('Export Data', `
    <p style="font-size:12px;color:var(--text-dim);margin:0 0 10px 0;">Copy this JSON or download as a file.</p>
    <div class="form-row">
      <textarea class="textarea" id="export-data" style="min-height:300px;font-family:monospace;font-size:11px;" readonly>${escapeHtml(json)}</textarea>
    </div>
    <div class="form-actions">
      <button class="btn" id="copy-export">📋 Copy</button>
      <button class="btn btn-primary" id="download-export">📥 Download</button>
      <button class="btn" id="close-export">Close</button>
    </div>
  `);
  document.getElementById('close-export').onclick = closeModal;
  document.getElementById('copy-export').onclick = () => {
    navigator.clipboard.writeText(json).then(() => flashStorageStatus('Copied!'));
  };
  document.getElementById('download-export').onclick = () => {
    const blob = new Blob([json], { type: 'application/json' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `git-dag-${Date.now()}.json`;
    a.click();
    URL.revokeObjectURL(url);
  };
}

function resetData() {
  if (!confirm('Reset all data and reload sample? This cannot be undone.')) return;
  localStorage.removeItem(STORAGE_KEY);
  state.commits = [];
  state.branches = [];
  state.tags = [];
  state.events = [];
  state.activeBranch = null;
  state.selectedCommit = null;
  state.selectedEvent = null;
  const sample = getSampleData();
  state.commits = sample.commits;
  state.branches = sample.branches;
  state.tags = sample.tags;
  state.events = sample.events;
  state.activeBranch = sample.activeBranch;
  saveState();
  renderAll();
}

// ============================================================
// SAMPLE DATA
// ============================================================
function getSampleData() {
  // Build a small realistic-ish branch topology
  const c0 = { id: 'c_init01', parents: [], message: 'Initial commit', author: 'alice', timestamp: Date.now() - 1000000 };
  const c1 = { id: 'c_setup02', parents: ['c_init01'], message: 'Set up project structure', author: 'alice', timestamp: Date.now() - 900000 };
  const c2 = { id: 'c_readme03', parents: ['c_setup02'], message: 'Add README and LICENSE', author: 'alice', timestamp: Date.now() - 850000 };
  const c3 = { id: 'c_auth_a01', parents: ['c_readme03'], message: 'Auth: scaffold login form', author: 'bob', timestamp: Date.now() - 800000 };
  const c4 = { id: 'c_auth_a02', parents: ['c_auth_a01'], message: 'Auth: add JWT validation', author: 'bob', timestamp: Date.now() - 750000 };
  const c5 = { id: 'c_api_b01', parents: ['c_readme03'], message: 'API: add user endpoints', author: 'carol', timestamp: Date.now() - 730000 };
  const c6 = { id: 'c_api_b02', parents: ['c_api_b01'], message: 'API: pagination support', author: 'carol', timestamp: Date.now() - 700000 };
  const c7 = { id: 'c_ui_c01', parents: ['c_readme03'], message: 'UI: redesign with new theme', author: 'dave', timestamp: Date.now() - 680000 };
  const c8 = { id: 'c_auth_a03', parents: ['c_auth_a02'], message: 'Auth: add password reset', author: 'bob', timestamp: Date.now() - 600000 };
  const c9 = { id: 'c_merge01', parents: ['c_auth_a03', 'c_api_b02'], message: 'Merge auth + api', author: 'alice', timestamp: Date.now() - 550000 };
  const c10 = { id: 'c_hotfix01', parents: ['c_merge01'], message: 'Hotfix: critical XSS vulnerability', author: 'alice', timestamp: Date.now() - 500000 };
  const c11 = { id: 'c_merge02', parents: ['c_hotfix01', 'c_ui_c01'], message: 'Merge UI into main', author: 'alice', timestamp: Date.now() - 450000 };
  const c12 = { id: 'c_release01', parents: ['c_merge02'], message: 'Bump version to 1.0.0', author: 'alice', timestamp: Date.now() - 400000 };
  const c13 = { id: 'c_doc01', parents: ['c_release01'], message: 'Update changelog', author: 'alice', timestamp: Date.now() - 350000 };
  const c14 = { id: 'c_perf01', parents: ['c_release01'], message: 'Perf: optimize query layer', author: 'carol', timestamp: Date.now() - 300000 };
  const c15 = { id: 'c_v11001', parents: ['c_doc01', 'c_perf01'], message: 'Release v1.1.0 prep', author: 'alice', timestamp: Date.now() - 200000 };

  const commits = [c0, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15];

  const branches = [
    { name: 'main', head: c15.id, color: '#3fb950' },
    { name: 'feature/auth', head: c8.id, color: '#58a6ff' },
    { name: 'feature/api', head: c6.id, color: '#bc8cff' },
    { name: 'feature/ui', head: c7.id, color: '#ffa657' },
    { name: 'hotfix/xss', head: c10.id, color: '#f85149' },
  ];

  const tags = [
    { name: 'v1.0.0', commit: c12.id },
    { name: 'v1.1.0-rc', commit: c15.id },
  ];

  const events = [
    { id: 'e_001', title: 'Project Kickoff', type: 'milestone', description: 'Initial repo + team agreement on architecture.', commitId: c0.id, timestamp: Date.now() - 990000 },
    { id: 'e_002', title: 'Design Review', type: 'review', description: 'Reviewed login UX mockups with product team.', commitId: c3.id, timestamp: Date.now() - 790000 },
    { id: 'e_003', title: 'API Freeze', type: 'freeze', description: 'Public API contract frozen for the v1.0 release.', commitId: c5.id, timestamp: Date.now() - 720000 },
    { id: 'e_004', title: 'Auth Feature Complete', type: 'feature', description: 'Login + JWT + password reset fully working.', commitId: c8.id, timestamp: Date.now() - 590000 },
    { id: 'e_005', title: 'Critical XSS Discovered', type: 'hotfix', description: 'Security team reported XSS in comment field. Patch shipped same day.', commitId: c10.id, timestamp: Date.now() - 490000 },
    { id: 'e_006', title: 'Release v1.0.0', type: 'release', description: 'Tagged and deployed to production.', commitId: c12.id, timestamp: Date.now() - 390000 },
    { id: 'e_007', title: 'Performance Sprint', type: 'milestone', description: 'Began query optimization initiative.', commitId: c14.id, timestamp: Date.now() - 290000 },
    { id: 'e_008', title: 'Release v1.1.0', type: 'release', description: 'Performance improvements and changelog delivered.', commitId: c15.id, timestamp: Date.now() - 190000 },
  ];

  return { commits, branches, tags, events, activeBranch: 'main' };
}

// ============================================================
// RENDER ALL
// ============================================================
function renderAll() {
  renderBranches();
  renderTags();
  renderEvents();
  renderSelectedInfo();
  renderGraph();
  populateCommitSelect();
}

function populateCommitSelect() {
  const select = document.getElementById('new-event-commit');
  if (!select) return;
  select.innerHTML = '';
  for (const commit of state.commits) {
    const opt = document.createElement('option');
    opt.value = commit.id;
    opt.textContent = `${shortHash(commit.id)} - ${truncate(commit.message, 40)}`;
    select.appendChild(opt);
  }
  // Set to currently selected commit if any
  if (state.selectedCommit) select.value = state.selectedCommit;
}

// ============================================================
// TABS
// ============================================================
function switchTab(name) {
  document.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t.dataset.tab === name));
  document.querySelectorAll('.tab-panel').forEach(p => {
    p.style.display = p.id === `tab-${name}` ? 'block' : 'none';
  });
  if (name === 'details') renderSelectedInfo();
}

function updateDetailsTab() {
  switchTab('details');
}

// ============================================================
// EVENT WIRING
// ============================================================
function wireEvents() {
  document.getElementById('btn-new-commit').onclick = openNewCommit;
  document.getElementById('btn-new-branch').onclick = openNewBranch;
  document.getElementById('btn-new-tag').onclick = openNewTag;
  document.getElementById('btn-import').onclick = openImport;
  document.getElementById('btn-export').onclick = openExport;
  document.getElementById('btn-reset').onclick = resetData;
  document.getElementById('modal-close').onclick = closeModal;
  document.getElementById('modal-backdrop').onclick = (e) => {
    if (e.target.id === 'modal-backdrop') closeModal();
  };

  document.getElementById('btn-create-event').onclick = () => {
    const title = document.getElementById('new-event-title').value.trim();
    const type = document.getElementById('new-event-type').value;
    const description = document.getElementById('new-event-desc').value.trim();
    const commitId = document.getElementById('new-event-commit').value;
    if (!title) { alert('Please enter an event title'); return; }
    if (!commitId) { alert('Please select a commit'); return; }
    createEvent({ title, type, description, commitId });
    document.getElementById('new-event-title').value = '';
    document.getElementById('new-event-desc').value = '';
    switchTab('events');
  };

  document.querySelectorAll('.tab').forEach(tab => {
    tab.onclick = () => switchTab(tab.dataset.tab);
  });

  // Click on background to deselect
  svg.on('click', () => {
    state.selectedCommit = null;
    state.selectedEvent = null;
    applySelection();
    applyEventHighlight();
    renderEvents();
    renderSelectedInfo();
  });

  // Resize handler
  window.addEventListener('resize', () => renderGraph());
}

// ============================================================
// INIT
// ============================================================
function init() {
  initGraph();
  wireEvents();
  
  const loaded = loadState();
  if (!loaded || state.commits.length === 0) {
    const sample = getSampleData();
    state.commits = sample.commits;
    state.branches = sample.branches;
    state.tags = sample.tags;
    state.events = sample.events;
    state.activeBranch = sample.activeBranch;
    saveState();
  }
  
  renderAll();
}

document.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>
<!-- agent-meta {"model":"minimax/minimax-m3","provider":"openrouter","persona":"full","sessionId":"8f0a9ba3-4a56-4742-b141-c8624f6911ab","tokensIn":258759,"tokensOut":19292,"tokensTotal":278051,"turns":11,"toolCalls":10,"failedToolCalls":0,"timestamp":"2026-07-24T18:07:09.544Z"} -->