Persist live session state in localStorage, fix HTML hot-reload

Two fixes:

1. Session persistence via localStorage: the previous DOM-attribute
   approach (data-impeccable-handled) didn't survive page reloads
   because the DOM is rebuilt from source. Now using localStorage:
   - Session state (id, action, count, arrived, visible variant)
     saved on every state change
   - Handled sessions (accepted/discarded) tracked separately
   - resumeSession() checks localStorage before resuming, skips
     if the session was already handled
   - Visible variant index preserved across reloads (user sees
     the same variant they were looking at)
   - cleanup() clears session, clearHandled() clears on next
     load when the wrapper is gone from source

2. Dev server HTML hot-reload: replaced static Bun HTML imports
   (import homepage from "../public/index.html") with dynamic
   file() serving via the existing serveGenerated() helper. HTML
   edits are now reflected on browser refresh without restarting
   the dev server. This matches how sub-pages already work.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-04-12 18:13:52 -07:00
co-authored by Claude Opus 4.6
parent 3d3c7ba372
commit 26e0b8a786
2 changed files with 79 additions and 14 deletions
+2 -4
View File
@@ -1,8 +1,6 @@
import { serve, file } from "bun";
import path from "node:path";
import { fileURLToPath } from "node:url";
import homepage from "../public/index.html";
import privacy from "../public/privacy.html";
import {
getSkills,
getCommands,
@@ -39,8 +37,8 @@ const server = serve({
port: process.env.PORT || 3000,
routes: {
"/": homepage,
"/privacy": privacy,
"/": () => serveGenerated(path.join(ROOT_DIR, "public/index.html")),
"/privacy": () => serveGenerated(path.join(ROOT_DIR, "public/privacy.html")),
// Legacy URL redirects (kept stable for external links and existing users).
"/cheatsheet": Response.redirect("/docs", 301),
+77 -10
View File
@@ -643,9 +643,9 @@
if (next < 1 || next > arrivedVariants) return;
visibleVariant = next;
showVariantInDOM(currentSessionId, next);
// Update selectedElement to the newly visible variant's content
updateSelectedElement();
updateBarContent('cycling');
saveSession();
}
function updateSelectedElement() {
@@ -705,6 +705,7 @@
} else if (state === 'GENERATING') {
updateBarContent('generating');
}
saveSession();
updating = false;
});
@@ -868,6 +869,7 @@
state = 'GENERATING';
showBar('generating');
saveSession();
if (variantObserver) variantObserver.disconnect();
variantObserver = startVariantObserver(currentSessionId);
}
@@ -875,20 +877,77 @@
function handleAccept() {
if (!currentSessionId || arrivedVariants === 0) return;
sendWS({ type: 'accept', id: currentSessionId, variantId: String(visibleVariant) });
// Mark the wrapper so resumeSession won't pick it up after reload
markSessionHandled();
cleanup();
}
function handleDiscard() {
if (!currentSessionId) return;
sendWS({ type: 'discard', id: currentSessionId });
markSessionHandled();
cleanup();
}
// ---------------------------------------------------------------------------
// Session persistence via localStorage
// ---------------------------------------------------------------------------
// Survives page reloads, browser close/reopen, HMR, and accidental refreshes.
const LS_KEY = PREFIX + '-session';
function saveSession() {
if (!currentSessionId) return;
try {
localStorage.setItem(LS_KEY, JSON.stringify({
id: currentSessionId,
state: state,
action: selectedAction,
count: selectedCount,
expected: expectedVariants,
arrived: arrivedVariants,
visible: visibleVariant,
}));
} catch { /* quota exceeded or private mode */ }
}
function loadSession() {
try {
const raw = localStorage.getItem(LS_KEY);
return raw ? JSON.parse(raw) : null;
} catch { return null; }
}
function clearSession() {
try { localStorage.removeItem(LS_KEY); } catch {}
}
/** Mark session as handled (accepted/discarded). The agent will clean up
* the source, but until it does the wrapper is still in the HTML. This
* prevents resumeSession from picking it up again after reload. */
function markSessionHandled() {
if (!currentSessionId) return;
try {
localStorage.setItem(LS_KEY + '-handled', currentSessionId);
} catch {}
}
function isSessionHandled(id) {
try {
return localStorage.getItem(LS_KEY + '-handled') === id;
} catch { return false; }
}
function clearHandled() {
try { localStorage.removeItem(LS_KEY + '-handled'); } catch {}
}
function cleanup() {
hideBar();
hideHighlight();
stopScrollTracking();
if (variantObserver) { variantObserver.disconnect(); variantObserver = null; }
clearSession();
selectedElement = null;
currentSessionId = null;
selectedAction = 'impeccable';
@@ -936,26 +995,34 @@
// variants before HMR fired. Pick up where we left off.
function resumeSession() {
const wrapper = document.querySelector('[data-impeccable-variants]');
if (!wrapper) return false;
if (!wrapper) { clearSession(); clearHandled(); return false; }
currentSessionId = wrapper.dataset.impeccableVariants;
const sessionId = wrapper.dataset.impeccableVariants;
// Don't resume if this session was already accepted/discarded
if (isSessionHandled(sessionId)) return false;
currentSessionId = sessionId;
expectedVariants = parseInt(wrapper.dataset.impeccableVariantCount || '0');
const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])');
arrivedVariants = variants.length;
visibleVariant = arrivedVariants > 0 ? 1 : 0;
// Find the visible variant's content element for highlight positioning.
// The wrapper has display:contents (no box), so we target the actual
// content element inside the currently visible variant.
const visEl = wrapper.querySelector('[data-impeccable-variant]:not([style*="display: none"]):not([data-impeccable-variant="original"])');
selectedElement = (visEl && visEl.firstElementChild) || visEl || wrapper.parentElement;
// Restore visible variant from localStorage if available, else default to 1
const saved = loadSession();
visibleVariant = (saved && saved.id === sessionId && saved.visible > 0 && saved.visible <= arrivedVariants)
? saved.visible : (arrivedVariants > 0 ? 1 : 0);
// Find the visible variant's content element for highlight positioning
const visEl = wrapper.querySelector('[data-impeccable-variant="' + visibleVariant + '"] > :first-child');
selectedElement = visEl || wrapper.parentElement;
// Set display state BEFORE starting observer (avoid triggering it)
if (arrivedVariants > 0) showVariantInDOM(currentSessionId, 1);
if (visibleVariant > 0) showVariantInDOM(currentSessionId, visibleVariant);
state = arrivedVariants >= expectedVariants ? 'CYCLING' : 'GENERATING';
showBar(state === 'CYCLING' ? 'cycling' : 'generating');
startScrollTracking();
saveSession();
// Start observing for more variants AFTER initial setup
if (variantObserver) variantObserver.disconnect();