From 59f2eefe5331e9c45061cca2d4ea16080560efeb Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 27 Mar 2026 19:57:48 -0500 Subject: [PATCH 1/9] =?UTF-8?q?feat(docs[=5Fext]):=20Add=20{toolicon}=20ro?= =?UTF-8?q?le=20=E2=80=94=20icon-only=20square=20badge=20(WIP)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: Exploring a compact badge variant for dense inline prose where the full {tool} badge is too wide and {toolref} has no visual tier indicator at all. what: - Add {toolicon} role: code-linked tool name with square icon-only badge positioned to the left of the element - Badge is 14x14px square with 3px border-radius, emoji only, no text - CSS: .sd-badge.icon-only with fixed pixel sizing for crisp rendering - Added to demo page for visual testing - WIP: vertical alignment and sizing still being refined --- docs/_ext/fastmcp_autodoc.py | 43 ++++++++++++++++++++++++++++++++---- docs/_static/css/custom.css | 28 +++++++++++++++++++++++ docs/demo.md | 4 ++++ 3 files changed, 71 insertions(+), 4 deletions(-) diff --git a/docs/_ext/fastmcp_autodoc.py b/docs/_ext/fastmcp_autodoc.py index f6d0811..a7adbb0 100644 --- a/docs/_ext/fastmcp_autodoc.py +++ b/docs/_ext/fastmcp_autodoc.py @@ -881,10 +881,11 @@ def _resolve_tool_refs( doctree: nodes.document, fromdocname: str, ) -> None: - """Resolve ``{tool}`` and ``{toolref}`` placeholders into links. + """Resolve ``{tool}``, ``{toolref}``, and ``{toolicon}`` placeholders. - ``{tool}`` renders as ``code`` + safety badge. + ``{tool}`` renders as ``code`` + safety badge (text + icon). ``{toolref}`` renders as ``code`` only (no badge). + ``{toolicon}`` renders as ``code`` + icon-only badge (square, no text). Runs at ``doctree-resolved`` — after all labels are registered and standard ``{ref}`` resolution is done. @@ -896,6 +897,7 @@ def _resolve_tool_refs( for node in list(doctree.findall(_tool_ref_placeholder)): target = node.get("reftarget", "") show_badge = node.get("show_badge", True) + icon_only = node.get("icon_only", False) label_info = domain.labels.get(target) if label_info is None: node.replace_self(nodes.literal("", target.replace("-", "_"))) @@ -914,9 +916,20 @@ def _resolve_tool_refs( newnode["classes"].append("reference") newnode["classes"].append("internal") - newnode += nodes.literal("", tool_name) + if icon_only: + # Icon badge goes BEFORE the element, outside it + tool_info = tool_data.get(tool_name) + if tool_info: + badge = _safety_badge(tool_info.safety) + badge["classes"].append("icon-only") + badge.children.clear() + badge += nodes.Text("") + newnode += badge + newnode += nodes.literal("", tool_name) + else: + newnode += nodes.literal("", tool_name) - if show_badge: + if not icon_only and show_badge: tool_info = tool_data.get(tool_name) if tool_info: newnode += nodes.Text(" ") @@ -962,6 +975,27 @@ def _toolref_role( return [node], [] +def _toolicon_role( + name: str, + rawtext: str, + text: str, + lineno: int, + inliner: object, + options: dict[str, object] | None = None, + content: list[str] | None = None, +) -> tuple[list[nodes.Node], list[nodes.system_message]]: + """Inline role ``:toolicon:`capture-pane``` → code + square icon badge. + + Like ``{tool}`` but the badge is icon-only (no text label), square, + and compact — designed for inline prose where the full badge is too wide. + """ + target = text.strip().replace("_", "-") + node = _tool_ref_placeholder( + rawtext, reftarget=target, show_badge=False, icon_only=True, + ) + return [node], [] + + def _badge_role( name: str, rawtext: str, @@ -987,6 +1021,7 @@ def setup(app: Sphinx) -> ExtensionMetadata: app.connect("doctree-resolved", _resolve_tool_refs) app.add_role("tool", _tool_role) app.add_role("toolref", _toolref_role) + app.add_role("toolicon", _toolicon_role) app.add_role("badge", _badge_role) app.add_directive("fastmcp-tool", FastMCPToolDirective) app.add_directive("fastmcp-tool-input", FastMCPToolInputDirective) diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index 874e40d..e81d1f3 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -309,6 +309,34 @@ h4:has(> .sd-badge) { .sd-badge.sd-bg-warning::before { content: "✏️"; } /* mutating */ .sd-badge.sd-bg-danger::before { content: "💣"; } /* destructive */ +/* ── Icon-only badge ({toolicon} role) ─────────────────── + * Square, no text, just the emoji from ::before. + * Designed for inline prose where the full badge is too wide. + * ────────────────────────────────────────────────────────── */ +.sd-badge.icon-only { + display: inline-flex !important; + align-items: center; + justify-content: center; + padding: 1px; + border-radius: 3px; + gap: 0; + font-size: 0; + line-height: 1; + width: 14px; + height: 14px; + min-width: 0; + min-height: 0; + vertical-align: -0.1em; + margin-right: 0.3em; + margin-left: 0; +} + +.sd-badge.icon-only::before { + font-size: 10px; + line-height: 1; + margin: 0; +} + /* ── Context-aware badge sizing ─────────────────────────── */ h2 .sd-badge, h3 .sd-badge { diff --git a/docs/demo.md b/docs/demo.md index 3b43dfc..07214b9 100644 --- a/docs/demo.md +++ b/docs/demo.md @@ -24,6 +24,10 @@ Standalone badges via `{badge}`: {toolref}`capture-pane` · {toolref}`send-keys` · {toolref}`search-panes` · {toolref}`wait-for-text` · {toolref}`kill-pane` · {toolref}`create-session` · {toolref}`split-window` +### `{toolicon}` — code-linked with icon-only square badge + +{toolicon}`capture-pane` · {toolicon}`send-keys` · {toolicon}`search-panes` · {toolicon}`wait-for-text` · {toolicon}`kill-pane` · {toolicon}`create-session` · {toolicon}`split-window` + ### `{ref}` — plain text link {ref}`capture-pane` · {ref}`send-keys` · {ref}`search-panes` · {ref}`wait-for-text` · {ref}`kill-pane` · {ref}`create-session` · {ref}`split-window` From f1cc04ec9bfe9c5af58f46d7ae38ceda84045540 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 27 Mar 2026 20:03:12 -0500 Subject: [PATCH 2/9] feat(docs[_ext]): Add 4 toolicon placement variants for A/B testing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: Need to test all icon-badge positions to find the best visual fit. what: - {tooliconl}: icon left, outside code — [🔍] `capture_pane` - {tooliconr}: icon right, outside code — `capture_pane` [🔍] - {tooliconil}: icon inline-left, inside code — `[🔍]capture_pane` - {tooliconir}: icon inline-right, inside code — `capture_pane[🔍]` - {toolicon} remains as alias for {tooliconl} - Factory function _make_toolicon_role creates role variants - CSS: .icon-only-inline for inside-code variants (tighter margins) - Demo page shows all 4 variants side by side --- docs/_ext/fastmcp_autodoc.py | 98 ++++++++++++++++++++++++------------ docs/_static/css/custom.css | 19 +++++++ docs/demo.md | 16 +++++- 3 files changed, 100 insertions(+), 33 deletions(-) diff --git a/docs/_ext/fastmcp_autodoc.py b/docs/_ext/fastmcp_autodoc.py index a7adbb0..063bf21 100644 --- a/docs/_ext/fastmcp_autodoc.py +++ b/docs/_ext/fastmcp_autodoc.py @@ -881,11 +881,14 @@ def _resolve_tool_refs( doctree: nodes.document, fromdocname: str, ) -> None: - """Resolve ``{tool}``, ``{toolref}``, and ``{toolicon}`` placeholders. + """Resolve ``{tool}``, ``{toolref}``, and ``{toolicon*}`` placeholders. ``{tool}`` renders as ``code`` + safety badge (text + icon). ``{toolref}`` renders as ``code`` only (no badge). - ``{toolicon}`` renders as ``code`` + icon-only badge (square, no text). + ``{toolicon}``/``{tooliconl}`` — icon-only badge left of code. + ``{tooliconr}`` — icon-only badge right of code. + ``{tooliconil}`` — icon-only badge inside code, left of text. + ``{tooliconir}`` — icon-only badge inside code, right of text. Runs at ``doctree-resolved`` — after all labels are registered and standard ``{ref}`` resolution is done. @@ -897,7 +900,7 @@ def _resolve_tool_refs( for node in list(doctree.findall(_tool_ref_placeholder)): target = node.get("reftarget", "") show_badge = node.get("show_badge", True) - icon_only = node.get("icon_only", False) + icon_pos = node.get("icon_pos", "") label_info = domain.labels.get(target) if label_info is None: node.replace_self(nodes.literal("", target.replace("-", "_"))) @@ -916,24 +919,44 @@ def _resolve_tool_refs( newnode["classes"].append("reference") newnode["classes"].append("internal") - if icon_only: - # Icon badge goes BEFORE the element, outside it + if icon_pos: tool_info = tool_data.get(tool_name) + badge = None if tool_info: badge = _safety_badge(tool_info.safety) badge["classes"].append("icon-only") + if icon_pos.startswith("inline"): + badge["classes"].append("icon-only-inline") badge.children.clear() badge += nodes.Text("") - newnode += badge - newnode += nodes.literal("", tool_name) + + if icon_pos == "left": + if badge: + newnode += badge + newnode += nodes.literal("", tool_name) + elif icon_pos == "right": + newnode += nodes.literal("", tool_name) + if badge: + newnode += badge + elif icon_pos == "inline-left": + code_node = nodes.literal("", "") + if badge: + code_node += badge + code_node += nodes.Text(tool_name) + newnode += code_node + elif icon_pos == "inline-right": + code_node = nodes.literal("", "") + code_node += nodes.Text(tool_name) + if badge: + code_node += badge + newnode += code_node else: newnode += nodes.literal("", tool_name) - - if not icon_only and show_badge: - tool_info = tool_data.get(tool_name) - if tool_info: - newnode += nodes.Text(" ") - newnode += _safety_badge(tool_info.safety) + if show_badge: + tool_info = tool_data.get(tool_name) + if tool_info: + newnode += nodes.Text(" ") + newnode += _safety_badge(tool_info.safety) node.replace_self(newnode) @@ -975,25 +998,34 @@ def _toolref_role( return [node], [] -def _toolicon_role( - name: str, - rawtext: str, - text: str, - lineno: int, - inliner: object, - options: dict[str, object] | None = None, - content: list[str] | None = None, -) -> tuple[list[nodes.Node], list[nodes.system_message]]: - """Inline role ``:toolicon:`capture-pane``` → code + square icon badge. +def _make_toolicon_role( + icon_pos: str, +) -> t.Callable[..., tuple[list[nodes.Node], list[nodes.system_message]]]: + """Create an icon-only tool reference role for a given position.""" + + def role_fn( + name: str, + rawtext: str, + text: str, + lineno: int, + inliner: object, + options: dict[str, object] | None = None, + content: list[str] | None = None, + ) -> tuple[list[nodes.Node], list[nodes.system_message]]: + target = text.strip().replace("_", "-") + node = _tool_ref_placeholder( + rawtext, reftarget=target, show_badge=False, icon_pos=icon_pos, + ) + return [node], [] - Like ``{tool}`` but the badge is icon-only (no text label), square, - and compact — designed for inline prose where the full badge is too wide. - """ - target = text.strip().replace("_", "-") - node = _tool_ref_placeholder( - rawtext, reftarget=target, show_badge=False, icon_only=True, - ) - return [node], [] + return role_fn + + +_toolicon_role = _make_toolicon_role("left") +_tooliconl_role = _make_toolicon_role("left") +_tooliconr_role = _make_toolicon_role("right") +_tooliconil_role = _make_toolicon_role("inline-left") +_tooliconir_role = _make_toolicon_role("inline-right") def _badge_role( @@ -1022,6 +1054,10 @@ def setup(app: Sphinx) -> ExtensionMetadata: app.add_role("tool", _tool_role) app.add_role("toolref", _toolref_role) app.add_role("toolicon", _toolicon_role) + app.add_role("tooliconl", _tooliconl_role) + app.add_role("tooliconr", _tooliconr_role) + app.add_role("tooliconil", _tooliconil_role) + app.add_role("tooliconir", _tooliconir_role) app.add_role("badge", _badge_role) app.add_directive("fastmcp-tool", FastMCPToolDirective) app.add_directive("fastmcp-tool-input", FastMCPToolInputDirective) diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index e81d1f3..a3fcec4 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -331,12 +331,31 @@ h4:has(> .sd-badge) { margin-left: 0; } +/* Right-of-code variant: flip margins */ +code.docutils + .sd-badge.icon-only { + margin-left: 0.3em; + margin-right: 0; +} + .sd-badge.icon-only::before { font-size: 10px; line-height: 1; margin: 0; } +/* Inline variants (inside ): tighter, inherit vertical alignment */ +.sd-badge.icon-only-inline { + vertical-align: baseline; + margin-right: 0.2em; + margin-left: 0; +} + +/* Inline-right: flip margins */ +code.docutils .sd-badge.icon-only-inline:last-child { + margin-left: 0.2em; + margin-right: 0; +} + /* ── Context-aware badge sizing ─────────────────────────── */ h2 .sd-badge, h3 .sd-badge { diff --git a/docs/demo.md b/docs/demo.md index 07214b9..a933aae 100644 --- a/docs/demo.md +++ b/docs/demo.md @@ -24,9 +24,21 @@ Standalone badges via `{badge}`: {toolref}`capture-pane` · {toolref}`send-keys` · {toolref}`search-panes` · {toolref}`wait-for-text` · {toolref}`kill-pane` · {toolref}`create-session` · {toolref}`split-window` -### `{toolicon}` — code-linked with icon-only square badge +### `{tooliconl}` — icon left, outside code -{toolicon}`capture-pane` · {toolicon}`send-keys` · {toolicon}`search-panes` · {toolicon}`wait-for-text` · {toolicon}`kill-pane` · {toolicon}`create-session` · {toolicon}`split-window` +{tooliconl}`capture-pane` · {tooliconl}`send-keys` · {tooliconl}`search-panes` · {tooliconl}`wait-for-text` · {tooliconl}`kill-pane` · {tooliconl}`create-session` · {tooliconl}`split-window` + +### `{tooliconr}` — icon right, outside code + +{tooliconr}`capture-pane` · {tooliconr}`send-keys` · {tooliconr}`search-panes` · {tooliconr}`wait-for-text` · {tooliconr}`kill-pane` · {tooliconr}`create-session` · {tooliconr}`split-window` + +### `{tooliconil}` — icon inline-left, inside code + +{tooliconil}`capture-pane` · {tooliconil}`send-keys` · {tooliconil}`search-panes` · {tooliconil}`wait-for-text` · {tooliconil}`kill-pane` · {tooliconil}`create-session` · {tooliconil}`split-window` + +### `{tooliconir}` — icon inline-right, inside code + +{tooliconir}`capture-pane` · {tooliconir}`send-keys` · {tooliconir}`search-panes` · {tooliconir}`wait-for-text` · {tooliconir}`kill-pane` · {tooliconir}`create-session` · {tooliconir}`split-window` ### `{ref}` — plain text link From 8d70e1c1535f2a0e7f493fe26272bedc67d3d60a Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 27 Mar 2026 20:14:55 -0500 Subject: [PATCH 3/9] style(docs): Strip box from icon-only badges, fix inline emoji rendering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: 14px colored square was too heavy — emoji alone carries the tier signal. Inline variants had invisible icons (font-size: 0.8em of 0 = 0). what: - Remove background, border, fixed dimensions from .sd-badge.icon-only - Bare emoji with opacity: 0.88, no containment box - Fix inline ::before to use 0.8rem (absolute) not 0.8em (relative to 0) - Inline variants use vertical-align: middle - Tighter margins: 0.2em outside, 0.15em inside code --- docs/_static/css/custom.css | 51 ++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index a3fcec4..c2f9b0e 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -309,50 +309,61 @@ h4:has(> .sd-badge) { .sd-badge.sd-bg-warning::before { content: "✏️"; } /* mutating */ .sd-badge.sd-bg-danger::before { content: "💣"; } /* destructive */ -/* ── Icon-only badge ({toolicon} role) ─────────────────── - * Square, no text, just the emoji from ::before. - * Designed for inline prose where the full badge is too wide. +/* ── Icon-only badge ({toolicon*} roles) ───────────────── + * Bare emoji glyph — no box, no border, no background. + * The emoji color signals the tier. Lightweight semantic + * mark that attaches to the tool name, not competes with it. * ────────────────────────────────────────────────────────── */ .sd-badge.icon-only { display: inline-flex !important; align-items: center; justify-content: center; - padding: 1px; - border-radius: 3px; + padding: 0; + border: none; + background: transparent !important; + border-radius: 0; gap: 0; font-size: 0; line-height: 1; - width: 14px; - height: 14px; + width: auto; + height: auto; min-width: 0; min-height: 0; - vertical-align: -0.1em; - margin-right: 0.3em; + vertical-align: -0.15em; + margin-right: 0.2em; margin-left: 0; } -/* Right-of-code variant: flip margins */ -code.docutils + .sd-badge.icon-only { - margin-left: 0.3em; - margin-right: 0; -} - .sd-badge.icon-only::before { - font-size: 10px; + font-size: 0.85rem; line-height: 1; margin: 0; + opacity: 0.88; } -/* Inline variants (inside ): tighter, inherit vertical alignment */ +/* Right-of-code variant: flip margins */ +code.docutils + .sd-badge.icon-only { + margin-left: 0.2em; + margin-right: 0; +} + +/* Inline variants (inside ): no background, sit on baseline */ .sd-badge.icon-only-inline { - vertical-align: baseline; - margin-right: 0.2em; + background: transparent !important; + border: none; + padding: 0; + vertical-align: middle; + margin-right: 0.15em; margin-left: 0; } +.sd-badge.icon-only-inline::before { + font-size: 0.8rem; +} + /* Inline-right: flip margins */ code.docutils .sd-badge.icon-only-inline:last-child { - margin-left: 0.2em; + margin-left: 0.15em; margin-right: 0; } From 28bcb8721e04e8a96c54a3c9c9f286cde59e1985 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 27 Mar 2026 20:16:27 -0500 Subject: [PATCH 4/9] style(docs): Optically refine all toolicon variants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: Expert feedback identified outside icons as too heavy, vertical alignment too aggressive, inside spacing too loose, and bomb emoji carrying more visual mass than the other glyphs. what: - Outside variants: 0.72rem (was 0.85rem), vertical-align -0.06em (was -0.15em) - Inside variants: 0.78rem, vertical-align -0.01em, margins 0.12em (was 0.15em) - Right-side variants get tighter gap than left (asymmetric: suffix needs less space) - Bomb emoji (💣): slightly smaller and lower opacity than 🔍/✏️ - display: block on ::before for consistent line-height clipping --- docs/_static/css/custom.css | 47 ++++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index c2f9b0e..6382abd 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -311,15 +311,17 @@ h4:has(> .sd-badge) { /* ── Icon-only badge ({toolicon*} roles) ───────────────── * Bare emoji glyph — no box, no border, no background. - * The emoji color signals the tier. Lightweight semantic - * mark that attaches to the tool name, not competes with it. + * Outside variants are smaller and quieter than inside ones. + * Prefix/suffix spacing is asymmetric (suffix tighter). * ────────────────────────────────────────────────────────── */ + +/* Outside variants: smaller, lighter */ .sd-badge.icon-only { display: inline-flex !important; align-items: center; justify-content: center; padding: 0; - border: none; + border: none !important; background: transparent !important; border-radius: 0; gap: 0; @@ -329,41 +331,54 @@ h4:has(> .sd-badge) { height: auto; min-width: 0; min-height: 0; - vertical-align: -0.15em; - margin-right: 0.2em; + vertical-align: -0.06em; + margin-right: 0.18em; margin-left: 0; } .sd-badge.icon-only::before { - font-size: 0.85rem; + font-size: 0.72rem; line-height: 1; margin: 0; - opacity: 0.88; + opacity: 0.85; + display: block; } -/* Right-of-code variant: flip margins */ +/* Right-of-code variant: tighter gap than left */ code.docutils + .sd-badge.icon-only { - margin-left: 0.2em; + margin-left: 0.14em; margin-right: 0; } -/* Inline variants (inside ): no background, sit on baseline */ +/* Bomb emoji has more visual mass — slightly smaller */ +.sd-badge.sd-bg-danger.icon-only::before { + font-size: 0.68rem; + opacity: 0.8; +} + +/* Inline variants (inside ): tighter, baseline-aligned */ .sd-badge.icon-only-inline { background: transparent !important; - border: none; + border: none !important; padding: 0; - vertical-align: middle; - margin-right: 0.15em; + vertical-align: -0.01em; + margin-right: 0.12em; margin-left: 0; } .sd-badge.icon-only-inline::before { - font-size: 0.8rem; + font-size: 0.78rem; +} + +/* Inline bomb: slightly smaller */ +.sd-badge.sd-bg-danger.icon-only-inline::before { + font-size: 0.72rem; + opacity: 0.8; } -/* Inline-right: flip margins */ +/* Inline-right: tighter than inline-left */ code.docutils .sd-badge.icon-only-inline:last-child { - margin-left: 0.15em; + margin-left: 0.1em; margin-right: 0; } From 82109253b6cdc4c65320380fff22f8c51b3f35e7 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 27 Mar 2026 20:27:03 -0500 Subject: [PATCH 5/9] =?UTF-8?q?fix(docs):=20Fix=20icon-only=20badge=20marg?= =?UTF-8?q?ins=20=E2=80=94=20em=20units=20resolved=20to=200=20with=20font-?= =?UTF-8?q?size:0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: .sd-badge.icon-only has font-size:0 to hide text. Margins in em units (0.25em, 0.15em) resolved to 0px since em is relative to the element's own font-size. Discovered via Playwright computed style audit. what: - Switch outside variant margins to fixed px (5px right, 4px left) - Restore colored square box for tooliconl/tooliconr (was bare emoji) - Keep bare emoji for tooliconil/tooliconir (inside code, no box) --- docs/_static/css/custom.css | 50 ++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index 6382abd..ac2fce2 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -310,57 +310,56 @@ h4:has(> .sd-badge) { .sd-badge.sd-bg-danger::before { content: "💣"; } /* destructive */ /* ── Icon-only badge ({toolicon*} roles) ───────────────── - * Bare emoji glyph — no box, no border, no background. - * Outside variants are smaller and quieter than inside ones. - * Prefix/suffix spacing is asymmetric (suffix tighter). + * Outside variants (l/r): colored square box with emoji. + * Inside variants (il/ir): bare emoji, no box — blends + * into the code chip as a seamless annotation. * ────────────────────────────────────────────────────────── */ -/* Outside variants: smaller, lighter */ +/* Outside variants: colored square box */ .sd-badge.icon-only { display: inline-flex !important; align-items: center; justify-content: center; + width: 16px; + height: 16px; padding: 0; - border: none !important; - background: transparent !important; - border-radius: 0; + box-sizing: border-box; + border-radius: 3px; gap: 0; font-size: 0; line-height: 1; - width: auto; - height: auto; min-width: 0; min-height: 0; - vertical-align: -0.06em; - margin-right: 0.18em; + vertical-align: middle; + transform: translateY(-1px); + margin-right: 5px; margin-left: 0; } .sd-badge.icon-only::before { - font-size: 0.72rem; + font-size: 10px; line-height: 1; + font-style: normal; + font-weight: normal; margin: 0; - opacity: 0.85; display: block; + opacity: 0.9; } -/* Right-of-code variant: tighter gap than left */ +/* Right-of-code variant: slightly tighter than left */ code.docutils + .sd-badge.icon-only { - margin-left: 0.14em; + margin-left: 4px; margin-right: 0; } -/* Bomb emoji has more visual mass — slightly smaller */ -.sd-badge.sd-bg-danger.icon-only::before { - font-size: 0.68rem; - opacity: 0.8; -} - -/* Inline variants (inside ): tighter, baseline-aligned */ +/* Inline variants (inside ): bare emoji, no box */ .sd-badge.icon-only-inline { background: transparent !important; border: none !important; padding: 0; + width: auto; + height: auto; + border-radius: 0; vertical-align: -0.01em; margin-right: 0.12em; margin-left: 0; @@ -368,12 +367,7 @@ code.docutils + .sd-badge.icon-only { .sd-badge.icon-only-inline::before { font-size: 0.78rem; -} - -/* Inline bomb: slightly smaller */ -.sd-badge.sd-bg-danger.icon-only-inline::before { - font-size: 0.72rem; - opacity: 0.8; + opacity: 0.85; } /* Inline-right: tighter than inline-left */ From 001e98ab4c495800b22cc7aa5428c4ca141b989b Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 27 Mar 2026 20:27:57 -0500 Subject: [PATCH 6/9] style: Format fastmcp_autodoc.py with ruff --- docs/_ext/fastmcp_autodoc.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/_ext/fastmcp_autodoc.py b/docs/_ext/fastmcp_autodoc.py index 063bf21..8bb6382 100644 --- a/docs/_ext/fastmcp_autodoc.py +++ b/docs/_ext/fastmcp_autodoc.py @@ -1014,7 +1014,10 @@ def role_fn( ) -> tuple[list[nodes.Node], list[nodes.system_message]]: target = text.strip().replace("_", "-") node = _tool_ref_placeholder( - rawtext, reftarget=target, show_badge=False, icon_pos=icon_pos, + rawtext, + reftarget=target, + show_badge=False, + icon_pos=icon_pos, ) return [node], [] From d72f73d2d8d3e68a74b142bc9f02f7bb14c68444 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 27 Mar 2026 20:37:22 -0500 Subject: [PATCH 7/9] style(docs): Tighten tooliconl icon-to-code gap from 5px to 3px MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: Verified via Playwright that tooliconl had 5px gap while tooliconr had 4px. The asymmetry was visually noticeable. what: - .sd-badge.icon-only margin-right: 5px → 3px - tooliconl now 3px, tooliconr remains 4px (close and balanced) --- docs/_static/css/custom.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index ac2fce2..a0c9ea0 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -332,7 +332,7 @@ h4:has(> .sd-badge) { min-height: 0; vertical-align: middle; transform: translateY(-1px); - margin-right: 5px; + margin-right: 3px; margin-left: 0; } From dd5a59cdda395a5ecd9ad661b1e90dc564ddc8bb Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 28 Mar 2026 04:57:18 -0500 Subject: [PATCH 8/9] fix(docs): Use flexbox on icon links to fix compound gap Replace per-variant margin hacks with inline-flex on the parent . Flexbox collapses HTML whitespace nodes between siblings, eliminating the triple-stacking gap (badge margin + whitespace + code margin-left). - Add inline-flex + gap: 3px on a.reference:has(> .sd-badge.icon-only) - Zero code element margin inside icon links - Remove old margin-right/margin-left and translateY overrides - Remove code.docutils + .sd-badge.icon-only right-variant rule --- docs/_static/css/custom.css | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index a0c9ea0..652ea82 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -315,6 +315,17 @@ h4:has(> .sd-badge) { * into the code chip as a seamless annotation. * ────────────────────────────────────────────────────────── */ +/* Outside icon links: flexbox collapses whitespace nodes */ +a.reference:has(> .sd-badge.icon-only) { + display: inline-flex; + align-items: center; + gap: 3px; +} + +a.reference:has(> .sd-badge.icon-only) > code { + margin: 0; +} + /* Outside variants: colored square box */ .sd-badge.icon-only { display: inline-flex !important; @@ -330,10 +341,7 @@ h4:has(> .sd-badge) { line-height: 1; min-width: 0; min-height: 0; - vertical-align: middle; - transform: translateY(-1px); - margin-right: 3px; - margin-left: 0; + margin: 0; } .sd-badge.icon-only::before { @@ -346,12 +354,6 @@ h4:has(> .sd-badge) { opacity: 0.9; } -/* Right-of-code variant: slightly tighter than left */ -code.docutils + .sd-badge.icon-only { - margin-left: 4px; - margin-right: 0; -} - /* Inline variants (inside ): bare emoji, no box */ .sd-badge.icon-only-inline { background: transparent !important; From 41a155006ebfe974452c17e2ca28a2c4cc770a54 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 28 Mar 2026 05:16:27 -0500 Subject: [PATCH 9/9] docs(recipes): Switch tool references from {tool} to {tooliconl} Replace full-text safety badges with compact icon-left squares. The colored icon conveys the safety tier at a glance without the visual weight of the full badge text in recipe prose. --- docs/recipes.md | 66 ++++++++++++++++++++++++------------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/docs/recipes.md b/docs/recipes.md index c40ad61..a1a5738 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -33,7 +33,7 @@ or what port it chose. > command and working directory, not terminal content. The dev server printed > its URL to the terminal minutes ago, so I need to search terminal content. -The agent calls {tool}`search-panes` with `pattern: "Local:"` and +The agent calls {tooliconl}`search-panes` with `pattern: "Local:"` and `session_name: "myapp"`. The response comes back with pane `%5` in the `react` window, matched line: `Local: http://localhost:5173/`. @@ -42,21 +42,21 @@ window, matched line: `Local: http://localhost:5173/`. > The server is alive and its URL is known. I do not need to start anything. > I just need an idle pane for running tests. -The agent calls {tool}`list-panes` on the `myapp` session. Several panes show +The agent calls {tooliconl}`list-panes` on the `myapp` session. Several panes show `pane_current_command: zsh` -- idle shells. It picks `%4` in the same window. ### Act -The agent calls {tool}`send-keys` in pane `%4`: +The agent calls {tooliconl}`send-keys` in pane `%4`: `PLAYWRIGHT_BASE_URL=http://localhost:5173 pnpm exec playwright test` -Then it calls {tool}`wait-for-text` on pane `%4` with `pattern: "passed|failed|timed out"`, `regex: true`, and `timeout: 120`. Once the -wait resolves, it calls {tool}`capture-pane` on `%4` with `start: -80` to +Then it calls {tooliconl}`wait-for-text` on pane `%4` with `pattern: "passed|failed|timed out"`, `regex: true`, and `timeout: 120`. Once the +wait resolves, it calls {tooliconl}`capture-pane` on `%4` with `start: -80` to read the test results. ```{tip} The agent's first instinct might be to *start* a Vite server. But -{tool}`search-panes` reveals one is already running. This avoids a port +{tooliconl}`search-panes` reveals one is already running. This avoids a port conflict, a wasted pane, and the most common agent mistake: treating tmux like a blank shell. ``` @@ -85,8 +85,8 @@ suite needs a live API server. > First I need to know what exists in the `backend` session. If a server is > already running, I should reuse it instead of starting a duplicate. -The agent calls {tool}`list-panes` for the `backend` session. No pane is -running a server process. A {tool}`search-panes` call for `"listening"` +The agent calls {tooliconl}`list-panes` for the `backend` session. No pane is +running a server process. A {tooliconl}`search-panes` call for `"listening"` returns no matches. ### Decide @@ -96,16 +96,16 @@ returns no matches. ### Act -The agent calls {tool}`split-window` with `session_name: "backend"` to -create a new pane, then calls {tool}`send-keys` in that pane: +The agent calls {tooliconl}`split-window` with `session_name: "backend"` to +create a new pane, then calls {tooliconl}`send-keys` in that pane: `npm run serve`. -The agent calls {tool}`wait-for-text` on the server pane with +The agent calls {tooliconl}`wait-for-text` on the server pane with `pattern: "Listening on"` and `timeout: 30`. Once the wait resolves, the -agent calls {tool}`send-keys` in the original pane: -`npm test -- --integration`, then {tool}`wait-for-text` with +agent calls {tooliconl}`send-keys` in the original pane: +`npm test -- --integration`, then {tooliconl}`wait-for-text` with `pattern: "passed|failed|error"` and `regex: true`, then -{tool}`capture-pane` to read the test results. +{tooliconl}`capture-pane` to read the test results. ```{warning} Calling {toolref}`capture-pane` immediately after {toolref}`send-keys` is a @@ -139,7 +139,7 @@ them failed, but they stepped away and do not remember which pane. > slow. Instead I will search for common failure indicators across all panes > at once. -The agent calls {tool}`search-panes` with +The agent calls {tooliconl}`search-panes` with `pattern: "FAIL|ERROR|error:|Traceback"`, `regex: true`, scoped to `session_name: "ci"`. @@ -150,12 +150,12 @@ The agent calls {tool}`search-panes` with ### Act -The agent calls {tool}`capture-pane` on `%3` with `start: -60`, then on +The agent calls {tooliconl}`capture-pane` on `%3` with `start: -60`, then on `%6` with `start: -60`. ```{tip} If the error scrolled off the visible screen, use `content_start: -200` (or -deeper) when calling {tool}`search-panes`. The `content_start` parameter +deeper) when calling {tooliconl}`search-panes`. The `content_start` parameter makes search reach into scrollback history, not just the visible screen. ``` @@ -183,7 +183,7 @@ interrupt it, verify the pane is responsive, and re-run the command. > `enter: false` or tmux will send Ctrl-C followed by Enter, which could > confirm a prompt I did not intend to answer. -The agent calls {tool}`send-keys` with `keys: "C-c"` and `enter: false` on +The agent calls {tooliconl}`send-keys` with `keys: "C-c"` and `enter: false` on the target pane. ### Decide @@ -192,7 +192,7 @@ the target pane. > for a shell prompt to reappear. Developers use custom prompts, so I cannot > just look for `$`. -The agent calls {tool}`wait-for-text` with `pattern: "[$#>%] *$"`, +The agent calls {tooliconl}`wait-for-text` with `pattern: "[$#>%] *$"`, `regex: true`, and `timeout: 5`. > If the wait resolves, the shell is back. If it times out, the process @@ -202,8 +202,8 @@ The agent calls {tool}`wait-for-text` with `pattern: "[$#>%] *$"`, ### Act If the wait times out, the agent sends `C-\` (also with `enter: false`). If -that also fails, it calls {tool}`kill-pane` on the stuck pane, then -{tool}`split-window` to create a replacement, then {tool}`send-keys` to +that also fails, it calls {tooliconl}`kill-pane` on the stuck pane, then +{tooliconl}`split-window` to create a replacement, then {tooliconl}`send-keys` to re-run. ```{warning} @@ -233,9 +233,9 @@ fresh results. ### Discover -The agent calls {tool}`list-panes` to find the pane by title, cwd, or +The agent calls {tooliconl}`list-panes` to find the pane by title, cwd, or current command. If more than one pane is plausible, it uses -{tool}`capture-pane` with a small range to confirm the target. +{tooliconl}`capture-pane` with a small range to confirm the target. ### Decide @@ -245,10 +245,10 @@ current command. If more than one pane is plausible, it uses ### Act -The agent calls {tool}`clear-pane`, then {tool}`send-keys` with -`keys: "pytest"`, then {tool}`wait-for-text` with +The agent calls {tooliconl}`clear-pane`, then {tooliconl}`send-keys` with +`keys: "pytest"`, then {tooliconl}`wait-for-text` with `pattern: "passed|failed|error"` and `regex: true`, then -{tool}`capture-pane` to read the fresh output. +{tooliconl}`capture-pane` to read the fresh output. ### The non-obvious part @@ -275,7 +275,7 @@ server pane", "the test pane"). > Before creating anything, I need to check whether a session with this name > already exists. Creating a duplicate will fail. -The agent calls {tool}`list-sessions`. No session named `myproject` exists. +The agent calls {tooliconl}`list-sessions`. No session named `myproject` exists. ### Decide @@ -285,16 +285,16 @@ The agent calls {tool}`list-sessions`. No session named `myproject` exists. ### Act -The agent calls {tool}`create-session` with `session_name: "myproject"` and -`start_directory: "/home/dev/myproject"`. Then {tool}`split-window` twice +The agent calls {tooliconl}`create-session` with `session_name: "myproject"` and +`start_directory: "/home/dev/myproject"`. Then {tooliconl}`split-window` twice (with `direction: "right"` and `direction: "below"`), followed by -{tool}`select-layout` with `layout: "main-vertical"`. +{tooliconl}`select-layout` with `layout: "main-vertical"`. -The agent calls {tool}`set-pane-title` on each pane: `editor`, `server`, +The agent calls {tooliconl}`set-pane-title` on each pane: `editor`, `server`, `tests`. -The agent calls {tool}`send-keys` in the server pane: `npm run dev`, then -{tool}`wait-for-text` for `pattern: "ready|listening|Local:"` with +The agent calls {tooliconl}`send-keys` in the server pane: `npm run dev`, then +{tooliconl}`wait-for-text` for `pattern: "ready|listening|Local:"` with `regex: true` and `timeout: 30`. ```{tip}