Exclusivobeta
Context Rings <mv-context-rings>
Torna visível, e editável, o que um assistente de IA “sabe” para a tarefa atual: três anéis concêntricos (“This task” no centro, “Always” para preferências e regras, “Ambient” para fluxos: e-mail, agenda, chat) e uma bandeja “Out of context”. Cada item é um chip (ícone da fonte, tamanho conforme o custo em tokens) distribuído sem colisões em volta do seu anel; arraste-o para outro anel com uma mola ou mova-o pelo teclado. Um medidor fino em volta do anel externo compara os tokens com o orçamento: quando ele é ultrapassado, os itens ambient não fixados de menor prioridade esmaecem com um rótulo “will be ignored”, na ordem de remoção. Novos itens ambient entram em órbita; uma visualização em lista assume em telas pequenas.
| Categoria | Exibição de dados |
|---|---|
| Tipo | Web Component (<mv-context-rings>) |
| Status | beta |
| Kit | IA que você pode verificar |
| Keywords | exclusive, ai, assistant, agent, context, context-window, memory, tokens, budget, gauge, drag-and-drop, transparency, user-control, rings, orbit |
When to use
- Users must see and edit which files, rules and streams an AI assistant uses for the current task
- A token budget should show which low-priority context will be ignored when over the limit
- Context items must be moved between task, always-on and ambient scopes, or excluded
Avoid when
- The goal is to capture the outcome and constraints of a request as the user types → use Intent Composer instead
- A plain token usage indicator is enough, without per-item control → use Progress instead
- The context holds hundreds of items; rings get crowded and fall back to a list anyway
Instalação
node scripts/add.mjs context-rings --out ./src/marvelousAgente de IA com o servidor MCP do Marvelous UI: install_components({ slugs: ["context-rings"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Arquivos copiados (dependências incluídas): tokens/tokens.css, core/base.css, core/dismiss.js, core/dom.js, core/element.js, core/focus.js, core/motion.js, core/observe.js, core/pointer.js, core/position.js, components/context-rings/context-rings.js, components/context-rings/context-rings.css.
Uso
Início rápido, a menor marcação que funciona:
<mv-context-rings budget="8000" task="Reply to Dana about the late delivery">
<script type="application/json">[
{ "id": "mail", "ring": "local", "label": "Latest email from Dana", "source": "email", "tokens": 1800 },
{ "id": "tone", "ring": "global", "label": "Tone: warm, concise", "source": "style", "tokens": 120, "pinned": true },
{ "id": "thread", "ring": "ambient", "label": "#support channel", "source": "slack", "tokens": 7400 }
]</script>
</mv-context-rings>Marcação de referência, para usar como ponto de partida e personalizar com atributos, data-* e variáveis CSS:
<div id="cr-demo" style="width:min(100%,70rem);margin-inline:auto">
<style>
#cr-demo .cr-bar { display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:.75rem 1rem; margin:0 0 .75rem }
#cr-demo .cr-bar p { margin:0; color:var(--mv-fg-muted); font-size:.8125rem; max-width:40rem }
#cr-demo .cr-controls { display:flex; align-items:center; flex-wrap:wrap; gap:.5rem }
#cr-demo .cr-controls label { display:inline-flex; align-items:center; gap:.5rem; color:var(--mv-fg-muted); font-size:.8125rem }
#cr-demo .cr-controls .mv-select { width:auto; min-width:7.5rem }
#cr-demo .cr-log { display:flex; align-items:center; flex-wrap:wrap; gap:.5rem; margin:.75rem 0 0; color:var(--mv-fg-muted); font-size:.75rem; font-variant-numeric:tabular-nums }
#cr-demo .cr-log code { font:500 .7rem/1 var(--mv-font-mono); padding:.2rem .4rem; border-radius:var(--mv-radius-sm); background:var(--mv-bg-muted); color:var(--mv-fg) }
</style>
<div class="cr-bar">
<p>Drag a chip to another ring, or to “Out of context”. Keyboard: <kbd class="mv-kbd">Enter</kbd> opens the card, <kbd class="mv-kbd">1</kbd> <kbd class="mv-kbd">2</kbd> <kbd class="mv-kbd">3</kbd> move it, <kbd class="mv-kbd">P</kbd> pins it.</p>
<div class="cr-controls">
<label>Budget
<select class="mv-select" id="cr-budget">
<option value="24000">24k tokens</option>
<option value="32000" selected>32k tokens</option>
<option value="64000">64k tokens</option>
</select>
</label>
<button class="mv-button" data-variant="outline" data-size="sm" type="button" id="cr-stream">Stream a new ambient item</button>
</div>
</div>
<mv-context-rings id="cr-rings" budget="32000" task="Reply to Harper & Lane about the late delivery"></mv-context-rings>
<p class="cr-log"><code>mv-context-change</code><span id="cr-log">Context ready to send.</span></p>
<script type="module">
const rings = document.getElementById("cr-rings");
const min = (m) => Date.now() - m * 60000;
rings.items = [
// This task
{ id: "mail-last", ring: "local", label: "Latest email from Dana Harper", source: "email", tokens: 1800, updated: min(38), excerpt: "Hi, we still haven't received the 40 Arlo tables ordered on September 2. Can you give us a firm delivery date? Without an answer by Friday we'll have to cancel." },
{ id: "contract", ring: "local", label: "Contract 2025.pdf", source: "pdf", sourceLabel: "PDF · Drive", tokens: 9600, updated: min(60 * 24 * 41), excerpt: "Section 7 (Lead times): for delays over 15 business days, the customer is entitled to a 5% discount on the affected order." },
{ id: "ticket", ring: "local", label: "Ticket #4821 · late order", source: "ticket", tokens: 2400, updated: min(95), pinned: true, excerpt: "Order HL-2291 held at the Denver warehouse: pallets waiting for the carrier (pickup slot 9/24)." },
{ id: "crm", ring: "local", label: "Account history", source: "crm", tokens: 1600, updated: min(60 * 5), excerpt: "Customer since 2019 · 6 orders · 1 dispute closed in 2023 (partial delivery)." },
// Always
{ id: "tone", ring: "global", label: "Tone: warm, concise", source: "style", tokens: 120, pinned: true, priority: 2, excerpt: "Short sentences, no internal jargon, always offer a next step." },
{ id: "signature", ring: "global", label: "Signature", source: "profile", tokens: 180, priority: 2, excerpt: "Sam Okafor · Customer Care, Nomad Workshop · +1 (415) 555-0142" },
{ id: "glossary", ring: "global", label: "Product glossary", source: "glossary", tokens: 3200, updated: min(60 * 24 * 6), excerpt: "Arlo: folding oak table · Mist: stackable chair · HL-: prefix for Harper & Lane orders." },
{ id: "refunds", ring: "global", label: "Refund policy", source: "doc", tokens: 1900, updated: min(60 * 24 * 12), excerpt: "Goodwill credit up to 8% without manager approval." },
// Ambient
{ id: "thread", ring: "ambient", label: "Harper & Lane thread", source: "email", tokens: 5400, priority: 2, pinned: true, updated: min(40), excerpt: "12 messages since September 2, including 3 follow-ups from the customer." },
{ id: "agenda", ring: "ambient", label: "This week's calendar", source: "calendar", tokens: 1200, updated: min(15), excerpt: "Thursday 10:00 AM: logistics sync with the Denver warehouse." },
{ id: "support", ring: "ambient", label: "#support channel", source: "slack", sourceLabel: "Slack · #support", tokens: 4600, priority: 0, updated: min(60 * 3), excerpt: "Leah: “The carrier confirms a Tuesday pickup for HL-2291.”" },
{ id: "recent", ring: "ambient", label: "Recent files", source: "file", tokens: 2600, priority: 0, updated: min(60 * 26), excerpt: "Inventory_Sept.xlsx, Delivery_schedule.pdf, Quote_Mist.docx" },
{ id: "notes", ring: "ambient", label: "Monday meeting notes", source: "note", tokens: 1800, updated: min(60 * 30), excerpt: "Carrier delays rising; warn B2B accounts over $20,000." },
// Out of context
{ id: "random", ring: "excluded", label: "#random channel", source: "slack", sourceLabel: "Slack · #random", tokens: 4100, priority: 0, updated: min(20) },
{ id: "personal", ring: "excluded", label: "Personal calendar", source: "calendar", tokens: 900, updated: min(60 * 2) },
];
const queue = [
{ id: "reply", label: "Reply from Dana Harper", source: "email", tokens: 900, priority: 2, excerpt: "“Thanks. A Tuesday delivery works for us if you confirm it in writing.”" },
{ id: "invite", label: "Invite: Harper & Lane sync", source: "calendar", tokens: 400, priority: 1, excerpt: "Thursday 2:30 PM · video call · organizer: Leah Martin" },
{ id: "leah", label: "Message from Leah", source: "slack", sourceLabel: "Slack · #support", tokens: 700, priority: 1, excerpt: "“Pickup confirmed Tuesday 8 AM, sending you the bill of lading.”" },
{ id: "invoice", label: "Invoice INV-2291.pdf", source: "pdf", tokens: 1500, priority: 0, excerpt: "Invoice HL-2291 · 40 × Arlo table · $12,960.00" },
];
const stream = document.getElementById("cr-stream");
stream.addEventListener("click", () => {
const next = queue.shift();
if (next) rings.add({ ...next, ring: "ambient", updated: Date.now() });
if (!queue.length) { stream.disabled = true; stream.textContent = "No more new items"; }
});
document.getElementById("cr-budget").addEventListener("change", (e) => { rings.budget = Number(e.target.value); });
const log = document.getElementById("cr-log");
const k = (n) => `${(n / 1000).toLocaleString("en-US", { maximumFractionDigits: 1 })}k`;
rings.addEventListener("mv-context-change", (e) => {
const { used, budget, evicted } = e.detail;
log.textContent = `${k(used)} / ${k(budget)} tokens · ${evicted.length ? `${evicted.length} ignored (${evicted.join(", ")})` : "nothing ignored"}`;
});
</script>
</div>API
Attributes
| Name | Tipo | Default | Description |
|---|---|---|---|
budget | number (tokens) | 32000 | The assistant's token budget. When exceeded, unpinned ambient items are marked “will be ignored” (lowest priority first, then oldest, then heaviest) until the total fits again. |
task | string | Task title shown as the heading (“Reply to Harper & Lane about the late delivery”). Also names the region. | |
view | rings | list | (auto) | Forces a view. Unset: rings, unless the component is narrower than list-below or too crowded for readable chips (list). The “List view” button writes this attribute. |
list-below | number (px) | 600 | Component width under which the automatic view switches to the list (ResizeObserver). |
data-view / data-budget | rings | list · ok | near | over | Reflected by the component (effective view, budget state), usable from CSS. |
Properties
| Name | Tipo | Description |
|---|---|---|
items | { id, label, ring, tokens, source, sourceLabel?, pinned?, priority?, excerpt?, updated? }[] | The model. ring: "local" | "global" | "ambient" | "excluded". source: email, calendar, chat, slack, file, pdf, profile, style, preference, doc, glossary, ticket, note, crm, history, web, memory (icon and label), or free text. priority: 0 low, 1 normal (default), 2 high. updated: Date, timestamp or ISO string. Reading returns copies. Can be set before the element is defined; a child <script type="application/json"> works too. |
used | number | Tokens sent (rings, not the tray), before eviction. Read-only. |
evicted | string[] | Ids that will be ignored, in eviction order. Read-only. |
Methods
| Name | Description |
|---|---|
add(item, { animate? }) | Adds an item (or updates the one with the same id). An ambient item orbits in from outside the rings with a faint pulse. |
move(id, ring) | Moves an item to a ring or to "excluded". Returns false if it was already there. |
pin(id, pinned?) | Pins (or unpins, or toggles): a pinned item is never dropped automatically. |
remove(id) | Removes an item entirely. |
Events
| Name | Description |
|---|---|
mv-context-change | After every change (drag and drop, keyboard, methods, budget). detail: { items, used, budget, evicted } (evicted = ignored ids, in order). Not fired when items is assigned. |
Content structure
| Name | Description |
|---|---|
script[type="application/json"] | Optional: array of items read on mount when items was not provided. |
CSS classes
| Name | Description |
|---|---|
mv-context-rings-chip / -chip-body | Chip (button): position on the outer element, visuals and scale on the body. data-ring, data-weight (0-3, cost), data-pinned, data-evicted, data-fresh. |
mv-context-rings-band / -tab | SVG ellipses of the rings (data-ring) and the name tabs on their top edge. |
mv-context-rings-gauge-track / -fill / -over / -tick | Budget gauge: track, share within budget, overflow, budget mark. |
mv-context-rings-tray | “Out of context” tray (drop target). |
mv-context-rings-list / -section / -row | List view: four labelled sections, one row per item. |
mv-context-rings-pop | Detail card (role=dialog, top layer): source, excerpt, cost, last update, actions. |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-context-rings-accent | var(--mv-accent) | Accent color: center ring, drop targets, gauge. |
--mv-context-rings-warning | var(--mv-warning) | Over-budget color (always paired with an icon and a label). |
--mv-context-rings-local / -global / -ambient | Fill of each ring. | |
--mv-context-rings-bg | var(--mv-surface) | Component background. |
Accessibility
A region named by the task title. Each ring (and the tray) is a role="group" labelled with its item count and cost; chips are real buttons with roving focus (arrows, Home/End; Tab moves between rings), in clockwise order. Full alternative to drag and drop: Enter opens a card (role=dialog, focus trapped, Escape returns focus) with “Move to…”, “Pin” (aria-pressed) and “Exclude”; direct shortcuts 1, 2, 3 (rings), Delete or 0 (exclude), P (pin), exposed through aria-keyshortcuts and an aria-describedby hint. Each chip's accessible name gives source, cost, pinned state and eviction rank. A live region announces moves and budget state changes; the budget is also a role="meter" with aria-valuetext. Ring identity never relies on color: name tabs on the rings, solid vs dashed strokes; the over-budget state pairs an icon with text. “List view” (aria-pressed) presents the same model as four labelled lists, automatically below list-below. Reduced motion (prefers-reduced-motion or data-motion="reduce"): no FLIP, orbit or pulse. No animation loop: everything is event-driven (ResizeObserver, Web Animations).