Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 183 additions & 0 deletions docs/_static/css/project-admonitions.css
Original file line number Diff line number Diff line change
@@ -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;
}
137 changes: 137 additions & 0 deletions docs/_static/js/spa-copybutton-reinit.js
Original file line number Diff line number Diff line change
@@ -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 ``<p>`` elements that lack a
* ``.copybtn`` sibling. Because the inserted elements have
* ``class="copybtn"`` and a ``data-clipboard-target`` pointing to a
* ``<p>`` 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 =
'<button class="copybtn o-tooltip--left" data-tooltip="Copy">' +
'<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">' +
'<title>Copy</title>' +
'<path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"/>' +
"</svg></button>";

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);
}
})();
4 changes: 3 additions & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,12 @@ 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_js_file("js/spa-copybutton-reinit.js", loading_method="defer")
app.add_css_file("css/project-admonitions.css")


globals().update(conf)
Loading