---
/**
 * astroagent in-site console drawer — a multi-turn chat.
 * Inject once in the base layout (before </body>). Ships to every visitor but
 * stays hidden/inert unless /devconsole/ping reports the visitor is authed
 * (token cookie). Real security is server-side on every endpoint; the DOM
 * hiding is only UX. Unlock once by visiting any page with ?devkey=<TOKEN>.
 */
const route = "/devconsole";
const previewRoute = "/_preview";
---

<div id="aa-console" data-route={route} data-preview-route={previewRoute} hidden>
  <button id="aa-handle" type="button" aria-label="Open developer console">▲ astroagent</button>
  <section id="aa-panel" hidden aria-label="Developer console">
    <header>
      <span class="aa-title">astroagent</span>
      <span id="aa-state" class="aa-state"></span>
      <button id="aa-key" type="button" aria-label="Agent auth token" title="Set agent auth token">🔑</button>
      <button id="aa-new" type="button" aria-label="New conversation" title="New conversation (clear)">✚</button>
      <button id="aa-lock" type="button" aria-label="Lock console" title="Lock (hide until next devkey)">🔒</button>
      <button id="aa-close" type="button" aria-label="Collapse">▼</button>
    </header>
    <div id="aa-chat" aria-live="polite"></div>
    <div id="aa-actions" hidden>
      <a id="aa-preview" href="#" target="_blank" rel="noopener">Open preview ↗</a>
      <button id="aa-publish" type="button">Publish</button>
    </div>
    <div id="aa-picks" hidden></div>
    <form id="aa-form">
      <button id="aa-pick" type="button" aria-label="Select an element on the page" title="Select an element on the page (Esc to cancel)">
        <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="6.5"></circle><path d="M12 1.5v4M12 18.5v4M1.5 12h4M18.5 12h4"></path></svg>
      </button>
      <textarea id="aa-prompt" rows="2" placeholder="Ask or change anything — “what pages do I have?”, “make the buttons green”, then “now make them bigger”"></textarea>
      <button id="aa-run" type="submit">Send</button>
    </form>
  </section>

  <div id="aa-hl" hidden><span id="aa-hl-tag"></span></div>

  <div id="aa-authmodal" hidden>
    <div class="aa-modal">
      <div class="aa-modal-h">Agent sign-in required</div>
      <p class="aa-modal-p">The agent's Claude session was lost. Paste a long-lived token — generate one on the server with <code>claude setup-token</code>.</p>
      <input id="aa-authinput" type="password" placeholder="Paste auth token" autocomplete="off" spellcheck="false" />
      <div id="aa-authmsg" class="aa-modal-msg"></div>
      <div class="aa-modal-btns">
        <button id="aa-authcancel" type="button">Cancel</button>
        <button id="aa-authsave" type="button">Save &amp; verify</button>
      </div>
    </div>
  </div>
</div>

<style>
  #aa-console { position: fixed; right: 16px; bottom: 16px; z-index: 2147483000; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
  #aa-handle { background:#111; color:#e6e6e6; border:1px solid #333; border-radius:8px; padding:8px 12px; font-size:12px; cursor:pointer; box-shadow:0 4px 16px rgba(0,0,0,.3); }
  #aa-handle:hover { background:#1b1b1b; }
  #aa-panel { width: min(700px, calc(100vw - 32px)); height: min(64vh, 560px); background:#0c0c0d; color:#d6d6d6; border:1px solid #2a2a2a; border-radius:10px; display:flex; flex-direction:column; box-shadow:0 12px 40px rgba(0,0,0,.5); overflow:hidden; }
  #aa-panel header { display:flex; align-items:center; gap:8px; padding:8px 10px; background:#141416; border-bottom:1px solid #2a2a2a; font-size:12px; }
  #aa-panel .aa-title { font-weight:600; color:#fff; }
  #aa-panel .aa-state { margin-left:auto; color:#8a8a8a; font-size:11px; }
  #aa-close, #aa-lock, #aa-key, #aa-new { background:none; border:none; color:#8a8a8a; cursor:pointer; font-size:12px; padding:2px 4px; }
  #aa-lock:hover, #aa-close:hover, #aa-key:hover, #aa-new:hover { color:#fff; }
  #aa-chat { flex:1; overflow:auto; padding:12px; display:flex; flex-direction:column; gap:8px; }
  .aa-msg { max-width:86%; padding:8px 11px; border-radius:9px; font-size:12px; line-height:1.5; white-space:pre-wrap; word-break:break-word; }
  .aa-user { align-self:flex-end; background:rgba(31,111,235,.22); border:1px solid rgba(31,111,235,.45); color:#dbeafe; }
  .aa-bot { align-self:flex-start; background:#161b22; border:1px solid #2a2a2a; color:#d6d6d6; }
  .aa-bot .aa-tool { display:block; color:#6cb6ff; opacity:.75; font-size:11px; }
  .aa-bot .aa-txt { display:block; margin:2px 0; }
  .aa-bot .aa-emsg { color:#ff7b72; }
  .aa-sys { align-self:center; color:#8a8a8a; font-size:11px; }
  .aa-sys.ok { color:#7ee787; }
  #aa-actions { display:flex; gap:8px; align-items:center; padding:8px 10px; border-top:1px solid #2a2a2a; }
  #aa-actions a { color:#6cb6ff; font-size:12px; text-decoration:none; margin-right:auto; }
  #aa-form { display:flex; gap:8px; padding:10px; border-top:1px solid #2a2a2a; }
  #aa-prompt { flex:1; resize:none; background:#141416; color:#e6e6e6; border:1px solid #2a2a2a; border-radius:6px; padding:8px; font:inherit; font-size:12px; }
  #aa-panel button:not(#aa-close):not(#aa-lock):not(#aa-key):not(#aa-new):not(#aa-pick):not(.aa-x) { background:#238636; color:#fff; border:none; border-radius:6px; padding:8px 14px; font-size:12px; cursor:pointer; }
  #aa-panel button:disabled { opacity:.5; cursor:default; }
  #aa-pick { display:flex; align-items:center; justify-content:center; background:#141416; color:#8a8a8a; border:1px solid #2a2a2a; border-radius:6px; padding:0 11px; cursor:pointer; }
  #aa-pick:hover { color:#fff; border-color:#3a3a3a; }
  #aa-pick.on { background:#1f6feb; color:#fff; border-color:#1f6feb; }
  #aa-handle.aa-picking { background:#1f6feb; border-color:#1f6feb; color:#fff; animation:aa-pulse 1.6s ease-in-out infinite; }
  @keyframes aa-pulse { 0%,100% { box-shadow:0 4px 16px rgba(31,111,235,.35); } 50% { box-shadow:0 4px 24px rgba(31,111,235,.8); } }
  @media (prefers-reduced-motion: reduce) { #aa-handle.aa-picking { animation:none; } }
  #aa-picks { display:flex; flex-direction:column; gap:6px; padding:8px 10px; border-top:1px solid #2a2a2a; max-height:130px; overflow:auto; }
  .aa-chip { display:flex; align-items:center; gap:8px; background:#161b22; border:1px solid #2a2a2a; border-radius:7px; padding:6px 8px; }
  .aa-chip-tag { color:#6cb6ff; font-size:11px; white-space:nowrap; max-width:150px; overflow:hidden; text-overflow:ellipsis; flex-shrink:0; }
  .aa-chip input { flex:1; background:#0c0c0d; color:#e6e6e6; border:1px solid #2a2a2a; border-radius:5px; padding:5px 7px; font:inherit; font-size:11px; min-width:0; }
  .aa-x { background:none; border:none; color:#8a8a8a; cursor:pointer; font-size:13px; padding:0 3px; flex-shrink:0; }
  .aa-x:hover { color:#ff7b72; }
  #aa-hl { position:fixed; pointer-events:none; z-index:2147482998; background:rgba(31,111,235,.14); border:1.5px solid rgba(31,111,235,.9); border-radius:3px; }
  #aa-hl-tag { position:absolute; top:-20px; left:-1.5px; background:#1f6feb; color:#fff; font-size:10px; font-family:ui-monospace, Menlo, monospace; padding:2px 6px; border-radius:3px; white-space:nowrap; }
  #aa-authmodal { position:fixed; inset:0; background:rgba(0,0,0,.55); display:flex; align-items:center; justify-content:center; z-index:2147483001; }
  #aa-authmodal .aa-modal { background:#141416; color:#e6e6e6; border:1px solid #2a2a2a; border-radius:10px; padding:18px; width:min(440px, calc(100vw - 32px)); box-shadow:0 12px 40px rgba(0,0,0,.6); }
  .aa-modal-h { font-weight:600; color:#fff; margin-bottom:6px; }
  .aa-modal-p { font-size:12px; color:#9a9a9a; margin:0 0 12px; line-height:1.5; }
  .aa-modal-p code { color:#6cb6ff; }
  #aa-authinput { width:100%; box-sizing:border-box; background:#0c0c0d; color:#e6e6e6; border:1px solid #2a2a2a; border-radius:6px; padding:9px; font:inherit; font-size:12px; }
  .aa-modal-msg { font-size:12px; min-height:16px; margin:8px 0; }
  .aa-modal-btns { display:flex; gap:8px; justify-content:flex-end; }
  #aa-authsave { background:#238636; color:#fff; border:none; border-radius:6px; padding:8px 14px; font-size:12px; cursor:pointer; }
  #aa-authcancel { background:#30363d !important; color:#fff; border:none; border-radius:6px; padding:8px 14px; font-size:12px; cursor:pointer; }
</style>

<script>
  const root = document.getElementById("aa-console");
  const route = root.dataset.route;
  const $ = (id) => document.getElementById(id);
  const api = (p) => route + p;

  async function boot() {
    const devkey = new URLSearchParams(location.search).get("devkey");
    let authed = false;
    try {
      const r = await fetch(api("/ping") + (devkey ? "?key=" + encodeURIComponent(devkey) : ""), { credentials: "same-origin" });
      authed = (await r.json()).authed;
    } catch { authed = false; }
    if (!authed) { root.remove(); return; }
    root.hidden = false;
    wire();
  }

  function wire() {
    const handle = $("aa-handle"), panel = $("aa-panel");
    const chat = $("aa-chat"), stateEl = $("aa-state"), actions = $("aa-actions");
    const previewLink = $("aa-preview"), runBtn = $("aa-run"), promptEl = $("aa-prompt");
    let convId = null, es = null, bot = null, running = false;

    const open = () => { panel.hidden = false; handle.hidden = true; promptEl.focus(); };
    const close = () => { panel.hidden = true; handle.hidden = false; };
    handle.onclick = open; $("aa-close").onclick = close;
    $("aa-lock").onclick = async () => {
      try { await fetch(api("/logout"), { method:"POST", credentials:"same-origin" }); } catch {}
      root.remove();
    };

    const scroll = () => { chat.scrollTop = chat.scrollHeight; };
    const setState = (s) => (stateEl.textContent = s || "");
    const busy = (b) => { running = b; runBtn.disabled = b; pickBtn.disabled = b; };

    function bubble(cls) { const d = document.createElement("div"); d.className = "aa-msg " + cls; chat.appendChild(d); scroll(); return d; }
    function userMsg(t) { const d = bubble("aa-user"); d.textContent = t; }
    function sysMsg(t, ok) { const d = bubble("aa-sys" + (ok ? " ok" : "")); d.textContent = t; }
    function botStart() { bot = bubble("aa-bot"); return bot; }
    function botText(t) { if (!bot) botStart(); const s = document.createElement("span"); s.className = "aa-txt"; s.textContent = t; bot.appendChild(s); scroll(); }
    function botTool(t) { if (!bot) botStart(); const s = document.createElement("span"); s.className = "aa-tool"; s.textContent = "· " + t; bot.appendChild(s); scroll(); }
    function botErr(t) { if (!bot) botStart(); const s = document.createElement("span"); s.className = "aa-txt aa-emsg"; s.textContent = "✗ " + t; bot.appendChild(s); scroll(); }

    function ensureStream() {
      if (es || !convId) return;
      es = new EventSource(api("/stream") + "?conversationId=" + encodeURIComponent(convId));
      es.onmessage = (m) => {
        let ev; try { ev = JSON.parse(m.data); } catch { return; }
        switch (ev.type) {
          case "turn_start": setState("working…"); break;
          case "progress":
            if (ev.kind === "text") botText(ev.text);
            else if (ev.kind === "tool") botTool(ev.text);
            else if (ev.kind === "log") setState("building preview…");
            break; // ignore 'done' (duplicate of final text)
          case "preview":
            previewLink.href = ev.url; actions.hidden = false;
            sysMsg("✓ preview updated", true);
            break;
          case "turn_end": setState(""); busy(false); bot = null; break;
          case "published":
            sysMsg("✓ published — " + (ev.commit || "").slice(0,8) + " (live)", true); actions.hidden = true;
            sysMsg("reloading to show the live site…");
            // The published event fires after the prod build, so the live site is
            // ready. From a preview page, go to the live equivalent of this route.
            setTimeout(() => {
              const live = stripPreview(location.pathname);
              if (live !== location.pathname) location.href = live;
              else location.reload();
            }, 1200);
            break;
          case "auth_required": setState(""); busy(false); botErr("agent not signed in"); openAuth(); break;
          case "error": setState(""); busy(false); botErr(ev.text || "error"); break;
          case "end": es && es.close(); es = null; convId = null; busy(false); setState(""); break;
        }
      };
      es.onerror = () => { /* keep-alive; browser auto-reconnects */ };
    }

    $("aa-form").onsubmit = async (e) => {
      e.preventDefault();
      if (running) return;
      const message = promptEl.value.trim();
      const selections = picks.map((p) => ({ ...p.data, comment: p.input.value.trim() }));
      if (!message && !selections.length) return;
      promptEl.value = "";
      const shown = [message, ...picks.map((p) => "⌖ " + p.label + (p.input.value.trim() ? " — " + p.input.value.trim() : ""))].filter(Boolean).join("\n");
      userMsg(shown); botStart(); busy(true); setState("working…");
      try {
        const body = convId ? { conversationId: convId, message } : { message };
        if (selections.length) body.selections = selections;
        const r = await fetch(api("/run"), {
          method:"POST", credentials:"same-origin", headers:{ "content-type":"application/json" },
          body: JSON.stringify(body),
        });
        const d = await r.json();
        if (!r.ok) throw new Error(d.error || "run failed");
        convId = d.conversationId;
        clearPicks();
        ensureStream();
      } catch (err) { botErr(err.message); busy(false); setState(""); }
    };

    $("aa-publish").onclick = async () => {
      if (!convId || running) return;
      setState("publishing…");
      try {
        const r = await fetch(api("/publish"), { method:"POST", credentials:"same-origin", headers:{ "content-type":"application/json" }, body: JSON.stringify({ conversationId: convId }) });
        const d = await r.json();
        if (!r.ok) throw new Error(d.error || "publish failed");
      } catch (err) { sysMsg("✗ " + err.message); }
      setState("");
    };

    $("aa-new").onclick = async () => {
      if (running) return;
      try { await fetch(api("/discard"), { method:"POST", credentials:"same-origin", headers:{ "content-type":"application/json" }, body: JSON.stringify({ conversationId: convId }) }); } catch {}
      if (es) { es.close(); es = null; }
      convId = null; bot = null; chat.textContent = ""; actions.hidden = true; setState("");
      clearPicks();
      sysMsg("new conversation");
      promptEl.focus();
    };

    // Enter to send, Shift+Enter for newline.
    promptEl.addEventListener("keydown", (e) => {
      if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); $("aa-form").requestSubmit(); }
    });

    // --- visual element picker: click an element on the page, attach a comment ---
    const pickBtn = $("aa-pick"), picksEl = $("aa-picks"), hl = $("aa-hl"), hlTag = $("aa-hl-tag");
    const prevRoute = root.dataset.previewRoute || "/_preview";
    const MAX_PICKS = 5;
    let picking = false, picks = [], lastTarget = null;

    const isOurs = (t) => t instanceof Element && t.closest("#aa-console");
    // "/_preview/<jobId>/about/" -> "/about/" (no-op on live pages)
    const stripPreview = (p) => p.replace(new RegExp("^" + prevRoute.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "/[^/]+"), "") || "/";

    function hlOn(el) {
      const r = el.getBoundingClientRect();
      hl.style.left = r.left + "px"; hl.style.top = r.top + "px";
      hl.style.width = r.width + "px"; hl.style.height = r.height + "px";
      hlTag.textContent = el.tagName.toLowerCase() + (el.classList[0] ? "." + el.classList[0] : "");
      hl.hidden = false;
    }
    function onMove(e) {
      const t = e.target;
      if (!(t instanceof Element) || isOurs(t)) { hl.hidden = true; lastTarget = null; return; }
      lastTarget = t; hlOn(t);
    }
    function onScroll() { if (picking && lastTarget) hlOn(lastTarget); }
    function onKey(e) { if (e.key === "Escape") { exitPick(); open(); } }
    function onClick(e) {
      const t = e.target;
      if (isOurs(t)) { exitPick(); return; } // our own UI (the handle) — cancel, let the click through
      e.preventDefault(); e.stopImmediatePropagation();
      exitPick();
      addPick(t);
      open();
    }
    function enterPick() {
      if (picking || running) return;
      if (picks.length >= MAX_PICKS) { sysMsg("max " + MAX_PICKS + " selections per turn"); return; }
      picking = true; pickBtn.classList.add("on");
      close(); // collapse to the handle so the panel doesn't cover the page
      handle.textContent = "click any element — Esc cancels";
      handle.classList.add("aa-picking");
      document.body.style.cursor = "crosshair";
      document.addEventListener("mousemove", onMove, true);
      document.addEventListener("click", onClick, true);
      document.addEventListener("keydown", onKey, true);
      document.addEventListener("scroll", onScroll, { passive: true });
    }
    function exitPick() {
      if (!picking) return;
      picking = false; pickBtn.classList.remove("on");
      handle.textContent = "▲ astroagent";
      handle.classList.remove("aa-picking");
      hl.hidden = true; lastTarget = null;
      document.body.style.cursor = "";
      document.removeEventListener("mousemove", onMove, true);
      document.removeEventListener("click", onClick, true);
      document.removeEventListener("keydown", onKey, true);
      document.removeEventListener("scroll", onScroll);
    }
    pickBtn.onclick = () => { if (picking) { exitPick(); open(); } else enterPick(); };

    // Serialized at pick time from the BUILT page DOM. Class attributes and visible
    // text survive Astro's production build verbatim — they're the agent's grep keys.
    function cssPath(el) {
      const parts = [];
      let n = el;
      for (let d = 0; n && n.nodeType === 1 && n.tagName !== "HTML" && d < 8; d++) {
        const tag = n.tagName.toLowerCase();
        if (n.id) { parts.unshift(tag + "#" + n.id); break; }
        const cls = [...n.classList].filter((c) => !c.startsWith("astro-")).slice(0, 2);
        let sel = tag + cls.map((c) => "." + c).join("");
        const sibs = n.parentElement ? [...n.parentElement.children].filter((s) => s.tagName === n.tagName) : [];
        if (sibs.length > 1) sel += ":nth-of-type(" + (sibs.indexOf(n) + 1) + ")";
        parts.unshift(sel);
        if (tag === "body") break;
        n = n.parentElement;
      }
      return parts.join(" > ");
    }
    function ancestorChain(el) {
      const out = [];
      for (let n = el.parentElement; n && n.tagName !== "HTML" && out.length < 6; n = n.parentElement) {
        out.unshift(n.tagName.toLowerCase() + [...n.classList].filter((c) => !c.startsWith("astro-")).slice(0, 2).map((c) => "." + c).join(""));
      }
      return out;
    }
    function trimHtml(el) {
      const c = el.cloneNode(true);
      for (const x of c.querySelectorAll("#aa-console")) x.remove();
      for (const s of c.querySelectorAll("svg")) s.replaceChildren();
      for (const n of [c, ...c.querySelectorAll("*")]) {
        for (const a of [...n.attributes]) if (a.value.length > 150) n.setAttribute(a.name, a.value.slice(0, 150) + "…");
      }
      let h = c.outerHTML;
      if (h.length > 2000) h = h.slice(0, 2000) + "…";
      return h;
    }
    function capture(el) {
      const r = el.getBoundingClientRect();
      return {
        route: stripPreview(location.pathname),
        selector: cssPath(el),
        classes: el.getAttribute("class") || "",
        text: (el.innerText || "").trim().replace(/\s+/g, " ").slice(0, 200),
        html: trimHtml(el),
        ancestors: ancestorChain(el),
        rect: { x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.width), h: Math.round(r.height) },
      };
    }
    function addPick(el) {
      const data = capture(el);
      const label = el.tagName.toLowerCase() + (data.text ? " “" + data.text.slice(0, 24) + (data.text.length > 24 ? "…" : "") + "”" : (el.classList[0] ? "." + el.classList[0] : ""));
      const chip = document.createElement("div"); chip.className = "aa-chip";
      const tag = document.createElement("span"); tag.className = "aa-chip-tag"; tag.textContent = label; tag.title = data.selector;
      const input = document.createElement("input"); input.placeholder = "What should change here?"; input.maxLength = 500;
      const x = document.createElement("button"); x.type = "button"; x.className = "aa-x"; x.textContent = "×"; x.setAttribute("aria-label", "Remove selection");
      chip.append(tag, input, x);
      const pick = { el, data, label, input, chip };
      x.onclick = () => { picks = picks.filter((p) => p !== pick); chip.remove(); picksEl.hidden = !picks.length; };
      tag.onmouseenter = () => { if (pick.el && pick.el.isConnected) hlOn(pick.el); };
      tag.onmouseleave = () => { hl.hidden = true; };
      input.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); $("aa-form").requestSubmit(); } });
      picks.push(pick);
      picksEl.appendChild(chip); picksEl.hidden = false;
      input.focus();
    }
    function clearPicks() { picks = []; picksEl.textContent = ""; picksEl.hidden = true; }

    // --- agent auth token modal (pops up on auth loss, or via the 🔑 button) ---
    const authModal = $("aa-authmodal"), authInput = $("aa-authinput"), authMsg = $("aa-authmsg");
    const openAuth = () => { authMsg.textContent = ""; authInput.value = ""; authModal.hidden = false; authInput.focus(); };
    const closeAuth = () => { authModal.hidden = true; };
    $("aa-key").onclick = openAuth;
    $("aa-authcancel").onclick = closeAuth;
    $("aa-authsave").onclick = async () => {
      const token = authInput.value.trim();
      if (!token) return;
      authMsg.style.color = "#9a9a9a"; authMsg.textContent = "verifying…";
      try {
        const r = await fetch(api("/auth"), { method:"POST", credentials:"same-origin", headers:{ "content-type":"application/json" }, body: JSON.stringify({ token }) });
        const d = await r.json();
        if (d.ok) { authMsg.style.color = "#7ee787"; authMsg.textContent = "✓ saved & verified — send your message again"; setTimeout(closeAuth, 1300); }
        else { authMsg.style.color = "#ff7b72"; authMsg.textContent = "✗ " + (d.error || "failed"); }
      } catch (err) { authMsg.style.color = "#ff7b72"; authMsg.textContent = "✗ " + err.message; }
    };
    // expose for the stream handler
    window.__aaOpenAuth = openAuth;
  }

  boot();
</script>
