From bd0c1ac82fac17b087109ff50782c61d2af30e94 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Thu, 16 Apr 2026 16:30:14 -0500 Subject: [PATCH 1/2] mcp(docs[_static]): restore prompt / agent-thought / server-prompt styling lost in gp-sphinx migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Commit ``c822f02`` deleted ``docs/_static/css/custom.css`` when adopting gp-sphinx, which dropped 618 lines of CSS — including the project-specific admonition classes that make ``docs/recipes.md`` and ``docs/topics/prompting.md`` readable: * ``.admonition.prompt`` — speech-bubble prompt block (💬 icon, accent border, italicized body) * ``.admonition.agent-thought`` — muted gray-bar "narration" aesthetic * ``span.prompt`` — inline italic with curly quotes * ``div.system-prompt`` / ``div.server-prompt`` — labeled dark code panels The behavior piece (``prompt-copy.js`` + ``copybutton_selector``) survived the migration, so copy-on-prompt still worked; only the *visual cue* that made these blocks look distinct was gone. Every ``:class: prompt`` admonition and every ``[...]{.prompt}`` inline span was rendering as a plain admonition / plain italics. Restored the rules verbatim from ``git show c822f02^:docs/_static/css/custom.css`` into a new ``docs/_static/css/project-admonitions.css`` — named to signal intent (these are libtmux-mcp-specific classes, not theme overrides) — and registered via ``app.add_css_file()`` in ``conf.py::setup`` alongside the existing ``app.add_js_file("js/prompt-copy.js")`` registration so order is deterministic (project CSS loads after ``_gp_setup(app)`` runs). gp-sphinx's ``merge_sphinx_config`` does not set ``html_css_files`` and its ``setup()`` only adds ``js/spa-nav.js``, so there is no collision risk with upstream theme CSS. ``DEFAULT_HTML_STATIC_PATH = ["_static"]`` already covers serving files from ``docs/_static/``. --- docs/_static/css/project-admonitions.css | 183 +++++++++++++++++++++++ docs/conf.py | 3 +- 2 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 docs/_static/css/project-admonitions.css diff --git a/docs/_static/css/project-admonitions.css b/docs/_static/css/project-admonitions.css new file mode 100644 index 0000000..55e057c --- /dev/null +++ b/docs/_static/css/project-admonitions.css @@ -0,0 +1,183 @@ +/* Project-specific admonition styling for libtmux-mcp docs. + * + * These classes are *semantic* extensions added by the project and are not + * shipped by the gp-sphinx theme, so they live here rather than upstream. + * Restored from the pre-gp-sphinx-migration ``custom.css`` (commit c822f02^). + * + * .admonition.prompt — chat-bubble prompts (user → LLM) + * .admonition.agent-thought — gray-bar agent chain-of-thought narration + * span.prompt — inline italic prompt with curly quotes + * div.system-prompt, + * div.server-prompt — labeled dark code panels for AGENTS.md / + * server-emitted prompts + */ + +/* ── Prompt blocks (user → LLM) ─────────────────────────── + * Styled admonition for copy-paste prompts. Chat-bubble + * aesthetic with speech icon. Says "type this into your LLM." + * ────────────────────────────────────────────────────────── */ +div.admonition.prompt { + border: 1px solid color-mix(in srgb, var(--color-link) 25%, transparent); + border-left: 3px solid var(--color-link); + background: color-mix(in srgb, var(--color-link) 4%, var(--color-background-primary)); + border-radius: 6px; + padding: 0.75rem 1rem 0.75rem 2.2rem; + margin: 1.25rem 0; + box-shadow: none; + position: relative; +} + +/* Speech bubble icon */ +div.admonition.prompt::before { + content: "\1F4AC"; + position: absolute; + left: 0.65rem; + top: 0.7rem; + font-size: 0.85rem; + line-height: 1; + opacity: 0.45; +} + +div.admonition.prompt > .admonition-title { + display: none; +} + +div.admonition.prompt > p { + font-size: 0.95rem; + font-style: italic; + color: var(--color-foreground-primary); +} + +div.admonition.prompt > p:last-of-type { + margin-bottom: 0; +} + +/* Copy button — inserted afterend of p:last-child, so it + * lands inside the prompt div. position:relative on the + * prompt div provides the positioning context. */ +div.admonition.prompt > button.copybtn { + background: transparent !important; + cursor: pointer; + border: none !important; +} + +div.admonition.prompt:hover > button.copybtn, +div.admonition.prompt > button.copybtn.success { + opacity: 1; +} + +div.admonition.prompt > p:last-child { + margin-bottom: 0; +} + +/* ── Agent reasoning blocks ────────────────────────────── + * Styled admonition for agent chain-of-thought. Neutral + * gray bar + italic + muted opacity = "internal narration, + * not something you do." + * ────────────────────────────────────────────────────────── */ +div.admonition.agent-thought { + border: none; + border-left: 3px solid var(--color-foreground-border); + background: transparent; + padding: 0.6rem 1rem; + margin: 1rem 0; + box-shadow: none; +} + +div.admonition.agent-thought > .admonition-title { + display: none; +} + +div.admonition.agent-thought > p { + font-style: italic; + color: var(--color-foreground-secondary); + font-size: 0.9rem; +} + +div.admonition.agent-thought > p:last-child { + margin-bottom: 0; +} + +/* ── Inline prompt dialog ──────────────────────────────── + * Inline styled span for user-to-LLM prompts. Supports + * full nested markup via MyST attrs_inline extension: + * [Run `pytest` in my build pane]{.prompt} + * No line-height or word-wrap disruption. WCAG AA contrast. + * ────────────────────────────────────────────────────────── */ +span.prompt { + font-style: italic; + color: var(--color-foreground-primary); +} + +span.prompt::before { + content: "\201C"; + color: var(--color-foreground-muted); +} + +span.prompt::after { + content: "\201D"; + color: var(--color-foreground-muted); +} + +/* ── Labeled code panels ───────────────────────────────── + * Copyable prose in labeled dark panels. Two variants: + * .system-prompt — user-authored fragments for AGENTS.md + * .server-prompt — libtmux-mcp's built-in instructions + * Keeps sphinx-copybutton via .highlight > pre selector. + * ────────────────────────────────────────────────────────── */ +div.system-prompt, +div.server-prompt { + margin: 1.25rem 0; + position: relative; +} + +div.system-prompt > div.highlight, +div.server-prompt > div.highlight { + background: #1f2329 !important; + border: 1px solid color-mix(in srgb, var(--color-link) 20%, transparent); + border-left: 3px solid var(--color-link); + border-radius: 6px; + position: relative; + padding-top: 1.3rem; +} + +div.system-prompt > div.highlight > pre, +div.server-prompt > div.highlight > pre { + background: transparent; + font-size: 13px; + line-height: 1.6; + white-space: pre-wrap; + word-break: break-word; +} + +/* Internal label — quiet uppercase whisper */ +div.system-prompt > div.highlight::before, +div.server-prompt > div.highlight::before { + position: absolute; + top: 0.45rem; + left: 0.85rem; + font-family: var(--font-stack); + font-size: 0.55rem; + font-weight: 500; + letter-spacing: 0.05em; + text-transform: uppercase; + color: #9590b8; + background: transparent; + padding: 0; + line-height: 1; + opacity: 0.8; +} + +div.system-prompt > div.highlight::before { + content: "System prompt"; +} + +div.server-prompt > div.highlight::before { + content: "Built-in server prompt"; +} + +/* Fix copy button background to match panel */ +div.system-prompt .copybtn, +div.server-prompt .copybtn { + background: #1f2329 !important; +} diff --git a/docs/conf.py b/docs/conf.py index f1c462c..2316892 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -125,10 +125,11 @@ def _convert_md_xrefs( def setup(app: Sphinx) -> None: - """Configure Sphinx app hooks and register project-specific JS.""" + """Configure Sphinx app hooks and register project-specific JS/CSS.""" _gp_setup(app) app.connect("autodoc-process-docstring", _convert_md_xrefs) app.add_js_file("js/prompt-copy.js", loading_method="defer") + app.add_css_file("css/project-admonitions.css") globals().update(conf) From 581e80e0253885750bcad43ddbe08f457ce36723 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Thu, 16 Apr 2026 17:04:00 -0500 Subject: [PATCH 2/2] mcp(docs[_static]): re-attach prompt-admonition copy buttons after gp-sphinx SPA swap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ``copybutton_selector`` in ``docs/conf.py`` is ``"div.highlight pre, div.admonition.prompt > p:last-child"`` — we copy *prompt text* as well as code. On full-page load, ``sphinx-copybutton`` iterates that selector and inserts a ``.copybtn`` after every match, then binds ``new ClipboardJS('.copybtn', ...)``; ClipboardJS uses delegated listening on ``document.body``, so those clicks keep working across SPA DOM swaps. On SPA navigation, however, gp-sphinx's ``spa-nav.js::addCopyButtons`` iterates ``"div.highlight pre"`` only — it does NOT re-attach buttons to ``.admonition.prompt > p:last-child``. After an SPA swap, prompt-heavy pages like ``/recipes/`` (no code blocks, eight ``.admonition.prompt``) render naked: no copy affordance at all. This is the user-visible regression introduced by the gp-sphinx migration (``c822f02``). This shim fills the gap: * Capture the first live ``.copybtn`` that appears anywhere in the document as a reusable template, so ``sphinx-copybutton``'s locale-specific tooltip and tabler-copy SVG are preserved exactly. ``FALLBACK_COPYBTN_HTML`` covers the rare case where the user's first page has no ``.copybtn`` anywhere (landing with no code and no prompts) before any SPA nav. * On every SPA swap, re-insert ``.copybtn`` siblings after any ``.admonition.prompt > p:last-child`` lacking one, assigning the ``

`` a ``mcp-promptcell-N`` id and wiring the button's ``data-clipboard-target``. The inserted element plugs into ClipboardJS's existing body delegation *and* the project's capture-phase ``prompt-copy.js`` delegation transparently — no extra click handlers required. Initial-load ordering: at deferred-script execution time ``document.readyState`` is ``"interactive"``, not ``"loading"``, so a naive readyState check would run our initial pass eagerly — *before* sphinx-copybutton's DOMContentLoaded handler fires, letting our fallback template beat sphinx-copybutton's proper one. We gate on ``"complete"`` instead, so we always register a DOMContentLoaded listener (which fires after sphinx-copybutton's, registered earlier when ``readyState`` was ``"loading"``) unless the document is already fully loaded. Verified end-to-end in real Chromium via Playwright: * Fresh load of ``/recipes/``: 8 buttons, all with sphinx-copybutton's ``#codecell{N}`` targets and tabler-copy SVG — shim is inert. * SPA-nav away then back to ``/recipes/``: 8 buttons re-inserted with ``#mcp-promptcell-{N}`` targets. * Click on a shim-inserted button: ``prompt-copy.js``'s capture-phase delegation fires, markdown-preserving ``navigator.clipboard.writeText`` succeeds, ``.success`` class + ``"Copied!"`` tooltip + tabler-check SVG swap — identical feedback to a fresh-load click. * ``/topics/prompting/`` (code-only, no prompts): 4 buttons re-added by gp-sphinx, clicks still fire through ClipboardJS delegation. * Multiple SPA navs in a row: idempotent, no double-insertion. The correct upstream fix is in gp-sphinx — its ``addCopyButtons`` should iterate the full ``copybutton_selector`` (or dispatch a ``spa-nav-complete`` event that consumers like ``sphinx-copybutton`` can hook). Until then, this project-local shim keeps the docs behaving. --- docs/_static/js/spa-copybutton-reinit.js | 137 +++++++++++++++++++++++ docs/conf.py | 1 + 2 files changed, 138 insertions(+) create mode 100644 docs/_static/js/spa-copybutton-reinit.js diff --git a/docs/_static/js/spa-copybutton-reinit.js b/docs/_static/js/spa-copybutton-reinit.js new file mode 100644 index 0000000..f418ae7 --- /dev/null +++ b/docs/_static/js/spa-copybutton-reinit.js @@ -0,0 +1,137 @@ +/** + * Re-attach copy buttons to ``.admonition.prompt > p:last-child`` after + * gp-sphinx's SPA DOM swap. + * + * Context: + * + * - ``copybutton_selector`` in ``docs/conf.py`` is + * ``"div.highlight pre, div.admonition.prompt > p:last-child"`` — we copy + * *prompt text* as well as code. + * - On full-page load, ``sphinx-copybutton`` iterates that selector and + * inserts a ``.copybtn`` after every match, then binds + * ``new ClipboardJS('.copybtn', ...)``. ClipboardJS uses delegated + * listening on ``document.body``, so those clicks keep working across + * SPA DOM swaps. + * - On SPA navigation, gp-sphinx's ``spa-nav.js::addCopyButtons`` iterates + * ``"div.highlight pre"`` only — it does NOT re-attach buttons to + * ``.admonition.prompt > p:last-child``. After an SPA swap, pages like + * ``/recipes/`` (prompt-heavy, no code blocks) render naked: no copy + * affordance at all. + * + * This shim: capture the first ``.copybtn`` that appears anywhere in the + * document as a reusable template (so we pick up ``sphinx-copybutton``'s + * locale-specific tooltip and icon exactly), then after every SPA swap + * re-insert buttons on prompt-admonition ``

`` elements that lack a + * ``.copybtn`` sibling. Because the inserted elements have + * ``class="copybtn"`` and a ``data-clipboard-target`` pointing to a + * ``

`` with a matching ``id``, they plug into ClipboardJS's + * body-delegated listener transparently and behave identically to + * initially-rendered buttons. + * + * ``FALLBACK_COPYBTN_HTML`` covers the rare case where the user's first + * page has no ``.copybtn`` anywhere (e.g. a landing page with no code + * blocks and no prompt admonitions) — the fallback button is a bare + * ``.copybtn`` with the same MDI "content-copy" icon upstream + * ``sphinx-copybutton`` ships. Ugly if tooltip styling needs the exact + * template but functional for clicks. + * + * The correct upstream fix is in gp-sphinx — its ``addCopyButtons`` + * should iterate the full ``copybutton_selector`` (or dispatch a + * ``spa-nav-complete`` event that consumers like ``sphinx-copybutton`` + * can hook). Until then, this project-local shim keeps the docs + * behaving. + */ +(function () { + "use strict"; + + if (!window.MutationObserver) return; + + var PROMPT_TARGET = ".admonition.prompt > p:last-child"; + var FALLBACK_COPYBTN_HTML = + '"; + + var copyBtnTemplate = null; + var idCounter = 0; + + function ensureTemplate() { + if (copyBtnTemplate) return true; + var live = document.querySelector(".copybtn"); + if (live) { + copyBtnTemplate = live.cloneNode(true); + copyBtnTemplate.classList.remove("success"); + copyBtnTemplate.removeAttribute("data-clipboard-target"); + return true; + } + // Fallback: no live .copybtn on page — fabricate from known markup. + var holder = document.createElement("div"); + holder.innerHTML = FALLBACK_COPYBTN_HTML; + copyBtnTemplate = holder.firstChild; + return true; + } + + function ensurePromptButtons() { + if (!ensureTemplate()) return; + document.querySelectorAll(PROMPT_TARGET).forEach(function (p) { + var next = p.nextElementSibling; + if (next && next.classList && next.classList.contains("copybtn")) { + return; + } + if (!p.id) { + p.id = "mcp-promptcell-" + idCounter; + idCounter += 1; + } + var btn = copyBtnTemplate.cloneNode(true); + btn.classList.remove("success"); + btn.setAttribute("data-clipboard-target", "#" + p.id); + p.insertAdjacentElement("afterend", btn); + }); + } + + // Observer has two jobs: + // (a) capture the template the instant sphinx-copybutton inserts its + // first ``.copybtn`` (happens at DOMContentLoaded, regardless of + // listener-registration order vs our own); + // (b) detect SPA-swap completion (a subtree addition that contains a + // ``.admonition.prompt``) and re-insert prompt buttons. + new MutationObserver(function (records) { + var sawCopybtn = false; + var sawArticle = false; + for (var i = 0; i < records.length; i += 1) { + var added = records[i].addedNodes; + for (var j = 0; j < added.length; j += 1) { + var n = added[j]; + if (n.nodeType !== 1) continue; + var cls = n.classList; + if (cls && cls.contains("copybtn")) sawCopybtn = true; + if (cls && cls.contains("admonition") && cls.contains("prompt")) { + sawArticle = true; + } + if (n.querySelector) { + if (!sawCopybtn && n.querySelector(".copybtn")) sawCopybtn = true; + if (!sawArticle && n.querySelector(".admonition.prompt")) { + sawArticle = true; + } + } + } + } + if (sawCopybtn) ensureTemplate(); + if (sawArticle) ensurePromptButtons(); + }).observe(document.body, { childList: true, subtree: true }); + + // Initial-load pass — MUST run after sphinx-copybutton has had its own + // DOMContentLoaded handler attach its buttons, otherwise our fallback + // template beats sphinx-copybutton's localized one to the punch on + // prompt-only pages like ``/recipes/``. At deferred-script execution + // time ``readyState`` is ``"interactive"`` (parse done, DOMContentLoaded + // not yet fired), so register a listener instead of running eagerly. + // ``"complete"`` means everything has already fired — safe to run now. + if (document.readyState === "complete") { + ensurePromptButtons(); + } else { + document.addEventListener("DOMContentLoaded", ensurePromptButtons); + } +})(); diff --git a/docs/conf.py b/docs/conf.py index 2316892..f508f79 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -129,6 +129,7 @@ def setup(app: Sphinx) -> None: _gp_setup(app) app.connect("autodoc-process-docstring", _convert_md_xrefs) app.add_js_file("js/prompt-copy.js", loading_method="defer") + app.add_js_file("js/spa-copybutton-reinit.js", loading_method="defer") app.add_css_file("css/project-admonitions.css")