Files
Skills/explore/templates/page.html
T
claude c68b2303a0 Add explore skill: interactive pseudocode browser
Serves a localhost page in the pseudo-skill register with the
reference markers as clickable buttons: clicks land in
<workdir>/events.jsonl, a persistent Monitor tail wakes the session,
the session rewrites page.html, and the browser live-reloads over SSE.
Actions per marker: expand / show real code / explain / ask, plus a
page-level ask box; anchors become forge deep-links when the project
has a reachable remote; incidental findings are surfaced on the page
as anchored notes.

The port is configured in one place (the PORT constant atop
scripts/server.py; --port overrides per run). Mechanics proven live
against the aura repo before extraction into this skill.
2026-07-20 12:11:57 +02:00

196 lines
7.1 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>explore — {{TITLE}}</title>
<style>
:root {
--bg: #10141a; --panel: #171c24; --border: #232b36;
--fg: #d6dde6; --dim: #68737f; --comment: #7d8894;
--kw: #c792ea; --str: #a5d6a7;
--marker-bg: #1f3a52; --marker-border: #35618a; --marker-fg: #8fd6ff;
--ok: #63d68f; --busy: #e8c268; --err: #e87979;
}
* { box-sizing: border-box; }
html, body { margin: 0; }
body {
background: var(--bg); color: var(--fg);
font: 15px/1.5 "JetBrains Mono", "Fira Code", ui-monospace, monospace;
max-width: 980px; margin-inline: auto; padding: 2rem 1.5rem 5rem;
}
header { display: flex; align-items: baseline; gap: .9rem; margin-bottom: .4rem; }
h1 { font-size: 1.05rem; margin: 0; font-weight: 600; }
#conn { font-size: .78rem; color: var(--dim); }
#conn.live::before { content: "\25CF "; color: var(--ok); }
#conn.dead::before { content: "\25CF "; color: var(--err); }
p.hint { color: var(--dim); font-size: .82rem; margin: 0 0 1.4rem; }
pre.pseudo {
background: var(--panel); border: 1px solid var(--border); border-radius: 8px;
padding: 1.15rem 1.35rem; overflow-x: auto; line-height: 1.72; margin: 0;
}
.c { color: var(--comment); }
.note {
display: block; margin: .4rem 0 .55rem; padding: .5rem .85rem;
border-left: 3px solid var(--marker-border); border-radius: 0 6px 6px 0;
background: #1a2330; color: #aebccb; font-size: .88em; line-height: 1.55;
white-space: pre-wrap;
}
.note.code { white-space: pre; overflow-x: auto; font-size: .85em; line-height: 1.5; color: var(--fg); }
.note.finding { border-left-color: var(--busy); background: #241f14; color: #d9c9a0; }
.warn { text-decoration: underline wavy var(--busy); text-decoration-skip-ink: none; cursor: help; }
.k { color: var(--kw); }
.s { color: var(--str); }
a.src { color: inherit; text-decoration: underline dotted; text-underline-offset: 3px; }
a.src:hover { color: var(--marker-fg); }
button.marker {
font: inherit; font-size: .92em; line-height: 1.25;
border: 1px solid var(--marker-border); border-radius: 5px;
background: var(--marker-bg); color: var(--marker-fg);
padding: 0 .4em; cursor: pointer; vertical-align: baseline;
}
button.marker:hover { background: #2a4f70; }
/* click menu */
#menu {
position: fixed; z-index: 10; display: none; flex-direction: column;
background: var(--panel); border: 1px solid var(--marker-border);
border-radius: 8px; padding: .3rem; box-shadow: 0 6px 24px #0009;
}
#menu.open { display: flex; }
#menu button {
font: inherit; font-size: .85rem; text-align: left;
background: none; border: 0; color: var(--fg);
padding: .35rem .7rem; border-radius: 5px; cursor: pointer;
}
#menu button:hover { background: var(--marker-bg); color: var(--marker-fg); }
/* page-level Q&A */
#qa { margin-top: 1.3rem; }
#qa .q { color: var(--dim); font-size: .84rem; margin: 0 0 .35rem; }
/* status + ask box */
#status { min-height: 1.4rem; margin: 1rem 0 0; font-size: .84rem; color: var(--dim); }
#status.busy { color: var(--busy); }
#status.err { color: var(--err); }
form#ask { display: flex; gap: .6rem; margin-top: 1.6rem; }
form#ask input {
flex: 1; font: inherit; font-size: .88rem;
background: var(--panel); border: 1px solid var(--border); border-radius: 7px;
color: var(--fg); padding: .5rem .8rem; outline: none;
}
form#ask input:focus { border-color: var(--marker-border); }
form#ask button {
font: inherit; font-size: .88rem; cursor: pointer;
background: var(--marker-bg); color: var(--marker-fg);
border: 1px solid var(--marker-border); border-radius: 7px; padding: .5rem 1rem;
}
form#ask button:hover { background: #2a4f70; }
</style>
</head>
<body>
<header>
<h1>explore &mdash; {{TITLE}}</h1>
<span id="conn" class="dead">connecting&hellip;</span>
</header>
<p class="hint">{{HINT}} Click a <button class="marker" type="button" tabindex="-1" style="pointer-events:none">[n]</button> marker to zoom.</p>
<pre class="pseudo">{{CONTENT}}</pre>
<div id="menu" role="menu">
<button data-action="expand">expand &mdash; one level deeper</button>
<button data-action="code">show real code</button>
<button data-action="explain">explain</button>
<button data-action="ask">ask about this&hellip;</button>
</div>
<p id="status"></p>
<form id="ask">
<input name="q" placeholder="Ask anything about this page&hellip;" autocomplete="off">
<button type="submit">send</button>
</form>
<script>
"use strict";
const menu = document.getElementById("menu");
const statusEl = document.getElementById("status");
let currentMarker = null;
function setStatus(cls, text) {
statusEl.className = cls;
statusEl.textContent = text;
}
async function send(ev) {
const label = ev.marker ? `${ev.action} [${ev.marker}]` : ev.action;
setStatus("busy", `sent: ${label} — waiting for Claude to rewrite the page…`);
try {
const r = await fetch("/event", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(ev),
});
if (!r.ok) throw new Error(`HTTP ${r.status}`);
} catch {
setStatus("err", "send failed — is the explore server running?");
}
}
// marker click -> action menu next to the marker
document.querySelectorAll("button.marker[data-marker]").forEach((btn) => {
btn.addEventListener("click", (e) => {
e.stopPropagation();
currentMarker = btn.dataset.marker;
const rect = btn.getBoundingClientRect();
menu.classList.add("open");
// measure after showing, then clamp into the viewport
const mw = menu.offsetWidth, mh = menu.offsetHeight;
let x = rect.left, y = rect.bottom + 6;
if (x + mw > innerWidth - 8) x = innerWidth - mw - 8;
if (y + mh > innerHeight - 8) y = rect.top - mh - 6;
menu.style.left = `${x}px`;
menu.style.top = `${y}px`;
});
});
menu.addEventListener("click", (e) => {
const action = e.target.dataset && e.target.dataset.action;
if (!action) return;
menu.classList.remove("open");
if (action === "ask") {
const text = prompt(`Question about [${currentMarker}]:`);
if (text === null || text.trim() === "") return;
send({ marker: currentMarker, action, text: text.trim() });
} else {
send({ marker: currentMarker, action });
}
});
document.addEventListener("click", () => menu.classList.remove("open"));
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") menu.classList.remove("open");
});
// free-text question about the whole page
document.getElementById("ask").addEventListener("submit", (e) => {
e.preventDefault();
const input = e.target.elements.q;
const text = input.value.trim();
if (!text) return;
send({ marker: null, action: "ask", text });
input.value = "";
});
// live-reload via SSE
const conn = document.getElementById("conn");
const es = new EventSource("/stream");
es.addEventListener("reload", () => location.reload());
es.onopen = () => { conn.className = "live"; conn.textContent = "live"; };
es.onerror = () => { conn.className = "dead"; conn.textContent = "reconnecting…"; };
</script>
</body>
</html>