| name | widgets |
| description | Render rich HTML inline via `<mcwidget>` tags with theme-aware styling. Load when emitting an mcwidget so the iframe inherits the dashboard theme instead of clashing with light / dark / custom palettes. |
| triggers | mcwidget, widget iframe, chart, table, visual, dashboard, render widget |
Inline widgets (<mcwidget>)
You can embed rich HTML in assistant messages using
<mcwidget title="Title">HTML</mcwidget>. The dashboard renders each tag as
a sandboxed iframe with Tailwind CSS preloaded. Use this for styled visual
content plain markdown cannot express: charts, color-coded tables, styled
cards, visual summaries, simple interactive probes.
The iframe CSP allows script-src from three CDNs: cdn.tailwindcss.com
(preloaded), cdn.jsdelivr.net, and cdnjs.cloudflare.com. Tailwind is
ready without any setup; other libraries (Chart.js, D3, etc.) need a
<script src="…"> tag pulling from one of those two CDNs.
When to use
- Use when styled HTML genuinely helps comprehension — Chart.js graphs,
color-coded comparison tables, status cards, before/after previews, small
interactive probes.
- Don't use for content markdown already handles well — plain prose,
bullet lists, fenced code, simple tables. Widget iframes carry more
overhead than markdown; default to markdown.
- Save to file for anything large.
<mcwidget> bodies fit comfortably
up to a few KB; beyond that, write an HTML file and return the absolute
path so the dashboard shows a thumbnail / link instead.
Theme-aware styling (critical)
The widget iframe inherits the dashboard's active theme through CSS
custom properties injected into each srcdoc. Use these variables instead
of hardcoded colors so widgets render correctly on every theme (light,
dark, and user-defined custom palettes).
Core palette (use these first):
| Variable | Role |
|---|
var(--bg) | Page background |
var(--text) | Foreground text |
var(--card) | Card / panel background |
var(--card-fg) | Foreground text on --card |
var(--border) | Borders and dividers |
var(--accent) | Primary accent / links |
var(--muted) | Muted / secondary text |
var(--ok) | Success state |
var(--warn) | Warning state |
var(--danger) | Error / destructive state |
var(--info) | Info / neutral callout (blue) |
Extended palette (also available):
| Variable | Role |
|---|
var(--bg-elevated) | Raised surface above --bg (headers, modals) |
var(--bg-hover) | Hover state for interactive backgrounds |
var(--text-strong) | High-contrast text emphasis |
var(--muted-strong) | Stronger muted text (still secondary, more legible) |
var(--border-strong) | High-contrast border for emphasis |
var(--accent-hover) | Hover state for accent elements |
var(--accent-subtle) | Tinted background using accent (badges, highlights) |
var(--ok-subtle) | Tinted ok background (success banners) |
var(--warn-subtle) | Tinted warn background (warning banners) |
var(--danger-subtle) | Tinted danger background (error banners) |
With Tailwind, use arbitrary values: bg-[var(--card)],
text-[var(--card-fg)], border-[var(--border)]. Avoid bg-gray-900,
text-white, bg-white, etc — they clash the moment the user switches
themes.
Format
<mcwidget title="Deploy status">
<div class="p-4 rounded-lg" style="background:var(--card);color:var(--text);border:1px solid var(--border)">
<div class="text-sm font-semibold" style="color:var(--ok)">✅ Green across all stages</div>
<div class="text-xs" style="color:var(--muted)">last build 08:42 UTC</div>
<div class="text-xs">
<a href="https://github.com/example-org/example-repo/pull/1234"
target="_blank" rel="noopener noreferrer"
style="color:var(--accent)">#1234</a>
</div>
</div>
</mcwidget>
Rules:
Links
Always open links in a new tab. Every <a> inside an <mcwidget> MUST
carry target="_blank" AND rel="noopener noreferrer". The widget iframe
is sandboxed; without target="_blank", navigation either fails silently
or replaces the iframe content with the link target — both broken UX. The
rel attribute is non-negotiable for security: it blocks reverse-tabnabbing
and prevents the destination from accessing window.opener.
Style links with style="color:var(--accent)" (or Tailwind
text-[var(--accent)]) for theme-correct contrast. Add hover:underline
or underline based on density — long copy benefits from underline,
chips / badges look cleaner without.
<a href="https://github.com/example-org/example-repo/pull/1234"
target="_blank" rel="noopener noreferrer"
style="color:var(--accent)">#1234</a>
Render identifiers as links wherever possible. Inside widgets, bare
IDs and bare URLs are wasted real-estate — the user can't click a plain-text
reference. Whenever you mention a known-format identifier or a bare URL,
render it as an <a> to its canonical https:// target.
URL templates — use these mechanically. Substitute the placeholders
(<org>, <repo>, <n>, <id>) with the real values and emit the
identifier verbatim:
| Identifier | URL template |
|---|
PR / merge request #<n> | https://github.com/<org>/<repo>/pull/<n> |
Issue #<n> | https://github.com/<org>/<repo>/issues/<n> |
Commit <sha> | https://github.com/<org>/<repo>/commit/<sha> |
Docs / wiki page <slug> | https://example.com/docs/<slug> |
| Generic bare URL | itself (https://…) — just wrap it in an <a> |
For any identifier scheme not listed here, follow the same principle: map
the bare reference to its canonical https:// URL. If you don't know the
canonical URL for an identifier, leave it as plain text rather than guessing.
For chat messages, paste the full URL the user shares; never reconstruct.
When the visible label can be made shorter than the URL (e.g. a long doc
title), use a meaningful label (<a href="…">Migration design doc</a>)
rather than dumping the raw URL.
Cost
Each widget iframe is heavier than the equivalent markdown on both render
and context-size budgets. Reach for <mcwidget> only when the visual
structure genuinely helps the reader. If in doubt, write markdown first;
promote to a widget only if the result is clearly worse.
Interactive widgets
Widgets can send events back to the agent. Add data-action and an
optional data-payload (JSON string) attribute to any clickable element:
<button data-action="approve" data-payload='{"id":"123"}'>Approve</button>
When clicked, the dashboard auto-submits a user message of the form
[UI] approve: {"id":"123"}. The agent receives it as a normal message
and can respond with text, a new widget, or both.
Form inputs with name attributes are auto-collected on click and
merged into the payload as formData. Use this for creation forms:
render pre-filled <input> / <select> elements, the user adjusts
values, clicks submit, and the agent receives every field.
Styling for interactive controls (consistent with the dashboard chrome):
- Buttons:
text-xs py-1.5 px-3.5 rounded-md + theme-var background.
- Labels:
text-[11px] + text-[var(--muted)].
- Inputs:
text-sm px-2.5 py-2 rounded-md + bg-[var(--bg)] +
border-[var(--border)].
- Zero hardcoded hex colors. Use the theme variables above for every
background, foreground, and border.
Density config
cfg.dashboard.widget_density (more / less, default more) controls
the wording of the short pointer that lives in the system prompt. less
biases against widgets by default; more encourages them. Users can flip
this in dashboard settings. Either way, this skill holds the full rules —
the main prompt only discovers that <mcwidget> exists.