Add toggle to disable snapshot branching

New "Branch" button next to "Hide Auto" in the search row. When
toggled off: captures have no parentId, sidebar/timeline show a flat
timestamp-sorted list, branch navigators are hidden, and pruning
skips tree-aware protection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-26 12:18:06 +01:00
parent 284f4e9538
commit d7bd9c4991

View File

@@ -44,6 +44,7 @@ let svgClipCounter = 0; // unique prefix for SVG clipPath IDs
let sidebarTooltipEl = null; // tooltip element for sidebar hover previews let sidebarTooltipEl = null; // tooltip element for sidebar hover previews
const lastCapturedIdMap = new Map(); // workflowKey -> id of most recent capture (for parentId chaining) const lastCapturedIdMap = new Map(); // workflowKey -> id of most recent capture (for parentId chaining)
const activeBranchSelections = new Map(); // forkPointId -> selected child index const activeBranchSelections = new Map(); // forkPointId -> selected child index
let branchingEnabled = true;
const sessionWorkflows = new Map(); // workflowKey -> { firstSeen, lastSeen } const sessionWorkflows = new Map(); // workflowKey -> { firstSeen, lastSeen }
// ─── Server API Layer ─────────────────────────────────────────────── // ─── Server API Layer ───────────────────────────────────────────────
@@ -1376,11 +1377,13 @@ async function captureSnapshot(label = "Auto") {
// Determine parentId for branching // Determine parentId for branching
let parentId = null; let parentId = null;
if (branchingEnabled) {
if (activeSnapshotId) { if (activeSnapshotId) {
parentId = activeSnapshotId; // fork from swapped snapshot parentId = activeSnapshotId; // fork from swapped snapshot
} else if (lastCapturedIdMap.has(workflowKey)) { } else if (lastCapturedIdMap.has(workflowKey)) {
parentId = lastCapturedIdMap.get(workflowKey); // continuation parentId = lastCapturedIdMap.get(workflowKey); // continuation
} }
}
const record = { const record = {
id: generateId(), id: generateId(),
@@ -1396,6 +1399,7 @@ async function captureSnapshot(label = "Auto") {
try { try {
await db_put(record); await db_put(record);
if (branchingEnabled) {
// Compute protected IDs: ancestors of this capture + fork points // Compute protected IDs: ancestors of this capture + fork points
const allRecs = await db_getAllForWorkflow(workflowKey); const allRecs = await db_getAllForWorkflow(workflowKey);
const tempTree = buildSnapshotTree(allRecs); const tempTree = buildSnapshotTree(allRecs);
@@ -1406,6 +1410,9 @@ async function captureSnapshot(label = "Auto") {
} }
ancestors.add(record.id); // protect the just-captured snapshot ancestors.add(record.id); // protect the just-captured snapshot
await pruneSnapshots(workflowKey, [...ancestors]); await pruneSnapshots(workflowKey, [...ancestors]);
} else {
await pruneSnapshots(workflowKey);
}
} catch { } catch {
return false; return false;
} }
@@ -1441,11 +1448,13 @@ async function captureNodeSnapshot(label = "Node Trigger") {
// Determine parentId for branching // Determine parentId for branching
let parentId = null; let parentId = null;
if (branchingEnabled) {
if (activeSnapshotId) { if (activeSnapshotId) {
parentId = activeSnapshotId; parentId = activeSnapshotId;
} else if (lastCapturedIdMap.has(workflowKey)) { } else if (lastCapturedIdMap.has(workflowKey)) {
parentId = lastCapturedIdMap.get(workflowKey); parentId = lastCapturedIdMap.get(workflowKey);
} }
}
const record = { const record = {
id: generateId(), id: generateId(),
@@ -1462,6 +1471,7 @@ async function captureNodeSnapshot(label = "Node Trigger") {
try { try {
await db_put(record); await db_put(record);
if (branchingEnabled) {
// Compute protected IDs: ancestors + fork points // Compute protected IDs: ancestors + fork points
const allRecs = await db_getAllForWorkflow(workflowKey); const allRecs = await db_getAllForWorkflow(workflowKey);
const tempTree = buildSnapshotTree(allRecs); const tempTree = buildSnapshotTree(allRecs);
@@ -1471,6 +1481,9 @@ async function captureNodeSnapshot(label = "Node Trigger") {
} }
protectedNodeIds.add(record.id); protectedNodeIds.add(record.id);
await pruneNodeSnapshots(workflowKey, [...protectedNodeIds]); await pruneNodeSnapshots(workflowKey, [...protectedNodeIds]);
} else {
await pruneNodeSnapshots(workflowKey);
}
} catch { } catch {
return false; return false;
} }
@@ -2660,9 +2673,22 @@ async function buildSidebar(el) {
filterItems(searchInput.value.toLowerCase()); filterItems(searchInput.value.toLowerCase());
}); });
const branchToggleBtn = document.createElement("button");
branchToggleBtn.className = "snap-filter-auto-btn active";
branchToggleBtn.textContent = "Branch";
branchToggleBtn.title = "Toggle snapshot branching";
branchToggleBtn.addEventListener("click", async () => {
branchingEnabled = !branchingEnabled;
branchToggleBtn.classList.toggle("active", branchingEnabled);
activeBranchSelections.clear();
if (sidebarRefresh) await sidebarRefresh().catch(() => {});
if (timelineRefresh) await timelineRefresh().catch(() => {});
});
searchRow.appendChild(searchInput); searchRow.appendChild(searchInput);
searchRow.appendChild(searchClear); searchRow.appendChild(searchClear);
searchRow.appendChild(autoFilterBtn); searchRow.appendChild(autoFilterBtn);
searchRow.appendChild(branchToggleBtn);
// Workflow selector // Workflow selector
const selectorRow = document.createElement("div"); const selectorRow = document.createElement("div");
@@ -3042,21 +3068,26 @@ async function buildSidebar(el) {
return; return;
} }
let records;
let tree = null;
let forkPointIds = new Set();
if (branchingEnabled) {
// Build tree and get display path for current branch // Build tree and get display path for current branch
const tree = buildSnapshotTree(allRecords); tree = buildSnapshotTree(allRecords);
const displayPath = getDisplayPath(tree, activeBranchSelections); const displayPath = getDisplayPath(tree, activeBranchSelections);
// newest first for display records = [...displayPath].reverse();
const records = [...displayPath].reverse();
// Build set of fork point IDs and record positions for branch nav insertion
const forkPointIds = new Set();
for (const [parentId, children] of tree.childrenOf) { for (const [parentId, children] of tree.childrenOf) {
if (children.length > 1) forkPointIds.add(parentId); if (children.length > 1) forkPointIds.add(parentId);
} }
} else {
// Flat: all records newest-first
records = [...allRecords].sort((a, b) => b.timestamp - a.timestamp);
}
for (const rec of records) { for (const rec of records) {
// Insert branch navigator above fork-point snapshots // Insert branch navigator above fork-point snapshots
if (forkPointIds.has(rec.id)) { if (branchingEnabled && forkPointIds.has(rec.id)) {
const children = tree.childrenOf.get(rec.id); const children = tree.childrenOf.get(rec.id);
const selectedIndex = Math.min(activeBranchSelections.get(rec.id) ?? 0, children.length - 1); const selectedIndex = Math.min(activeBranchSelections.get(rec.id) ?? 0, children.length - 1);
const nav = buildBranchNavigator(rec.id, children, selectedIndex, refresh); const nav = buildBranchNavigator(rec.id, children, selectedIndex, refresh);
@@ -3234,6 +3265,7 @@ async function buildSidebar(el) {
if (!confirmed) return; if (!confirmed) return;
} }
// Fork-point deletion: rebuild tree from fresh data, then re-parent children // Fork-point deletion: rebuild tree from fresh data, then re-parent children
if (branchingEnabled) {
const freshRecords = await db_getAllForWorkflow(rec.workflowKey); const freshRecords = await db_getAllForWorkflow(rec.workflowKey);
const freshTree = buildSnapshotTree(freshRecords); const freshTree = buildSnapshotTree(freshRecords);
const children = freshTree.childrenOf.get(rec.id); const children = freshTree.childrenOf.get(rec.id);
@@ -3247,6 +3279,7 @@ async function buildSidebar(el) {
await db_updateMeta(rec.workflowKey, child.id, { parentId: newParent }); await db_updateMeta(rec.workflowKey, child.id, { parentId: newParent });
} }
} }
}
await db_delete(rec.workflowKey, rec.id); await db_delete(rec.workflowKey, rec.id);
pickerDirty = true; pickerDirty = true;
await refresh(); await refresh();
@@ -3430,15 +3463,21 @@ function buildTimeline() {
return; return;
} }
let records;
let tree = null;
let forkPointSet = new Set();
if (branchingEnabled) {
// Show only current branch's markers // Show only current branch's markers
const tree = buildSnapshotTree(allRecords); tree = buildSnapshotTree(allRecords);
const records = getDisplayPath(tree, activeBranchSelections); records = getDisplayPath(tree, activeBranchSelections);
// Identify fork points on this path
const forkPointSet = new Set();
for (const [parentId, children] of tree.childrenOf) { for (const [parentId, children] of tree.childrenOf) {
if (children.length > 1) forkPointSet.add(parentId); if (children.length > 1) forkPointSet.add(parentId);
} }
} else {
// Flat: all records in timestamp order
records = [...allRecords].sort((a, b) => a.timestamp - b.timestamp);
}
for (const rec of records) { for (const rec of records) {
const marker = document.createElement("div"); const marker = document.createElement("div");
@@ -3481,8 +3520,8 @@ function buildTimeline() {
swapSnapshot(rec); swapSnapshot(rec);
}); });
// Fork point: vertical stack — up arrow, marker with badge, down arrow // Fork point: vertical stack — up arrow, marker, down arrow
if (forkPointSet.has(rec.id)) { if (branchingEnabled && forkPointSet.has(rec.id)) {
const children = tree.childrenOf.get(rec.id); const children = tree.childrenOf.get(rec.id);
const selectedIndex = Math.min(activeBranchSelections.get(rec.id) ?? 0, children.length - 1); const selectedIndex = Math.min(activeBranchSelections.get(rec.id) ?? 0, children.length - 1);