Notes

How it works

Set contextMentions.enabled: true and register one or more sources. Each source has a search(query) (filter items) and a resolve(item) (return a dispatch payload). The widget ships createStaticMentionSource + defaultMentionFilter for the common client-side case.

Both displays share the entire pipeline (the source search/resolve, the menu, abort-on-remove, and the submit bundle) and only change the display surface. Chips (the default) attach a removable pill row; setting display: "inline" swaps the composer for a contenteditable surface and drops atomic tokens into the sentence. The inline engine ships as a separate ~3 kB lazy chunk that loads only when display: "inline" is set, so chip-only and feature-off installs pay nothing.

Try it

  • Click the @ button in the composer, or type @
  • Filter by typing (e.g. @app), navigate with ↑/↓, select with Enter
  • The Files source resolves on select; the Page source resolves at submit (reads the live DOM)
  • Chips: remove a chip with its ×, or Backspace on an empty composer
  • Inline: keep typing around a token and it stays whole and moves with the caret; Backspace next to a token deletes the whole token, not a character
  • Send — the resolved file body still reaches the model either way (echoed below the reply)

Chip rendering variants

Use the Chip rendering control in the rail (visible when Display is Chips) to compare the same feature at five levels of customization:

  • Default — the built-in menu and chip, Files source only (no smart DOM). The baseline every install gets for free.
  • Badges — a custom renderMentionItem: source badges and highlighted query matches, plus the smart-DOM Page source.
  • Hover — a custom renderMentionChip: hover a Page chip to outline its live element on the page; Files chips preview their resolved content.
  • Preview — hover any chip for a small popover previewing its resolved content (Files read from ctx.payload, the Page source re-reads the live element); click a Page chip to scroll to and flash its element on the page.
  • Full — badges + hover together, i.e. the shareable createSmartDomMentionsExperience factory as-is.

All five are produced by one factory (src/mentions/smart-dom-mentions-experience.ts) via feature flags — showing the config is a portable, shareable unit.

Token styling

Use the Token styling control in the rail (visible when Display is Inline) to compare the three levels of control over how a token looks:

  • Colored — give each item a color and the whole pill (tint + icon + text) recolors. Here it varies by file type, the pluggable "color per type" knob.
  • Monochrome — omit color and tokens fall back to the theme accent, one uniform look.
  • Custom — a renderMentionToken hook replaces the token DOM entirely (this variant renders a bordered, monospace, dot-prefixed chip). Hosts can also theme by source in CSS via .persona-mention-token[data-mention-source="files"].

Config snippet

import { createStaticMentionSource } from "@runtypelabs/persona"; const config = { contextMentions: { enabled: true, // display: "inline", // atomic tokens in the sentence (default is "chip") sources: [ createStaticMentionSource({ id: "files", label: "Files", items: [ { id: "app", label: "App.tsx", iconName: "file-code", color: "#2563eb" }, ], resolve: (item) => ({ llmAppend: readFile(item.id) }), }), ], }, };

The affordance button (and when to show it)

Typing @ is a power-user convention that's hard to reach on mobile and invisible to keyboard and screen-reader users, so the widget also paints a visible button as an accessible entry to the same menu. The @ channel shows it by default; extra channels like / are typed-only (showButton: false). The icon defaults to a + "add context" glyph, not a literal @.

  • Show it for consumer, mobile, or accessibility-sensitive surfaces.
  • Hide it (showButton: false) for desktop power users who know to type @.
  • Retheme it with buttonIconName: "at-sign" and buttonTooltipText.

Want it only on small screens? showButton is a plain boolean, so use CSS against the widget's own size (not the viewport): the composer is a size-query container named persona-composer, and .persona-mention-button is a supported hook. Keep in mind this drops the accessible entry point at wide sizes.

@container persona-composer (min-width: 480px) { .persona-mention-button { display: none; } }

What reaches the model

Tokens change how the message looks, not what the model receives. The one difference from chip mode: chip mode removes what you typed (@App) from the sentence, while inline mode keeps @App.tsx in place, so the model sees where in your sentence the file was mentioned. Cursor, ChatGPT, and Claude attachments work the same way.

The resolved content is identical in both modes. Each mention's body is wrapped in a fenced code block carrying its label, blocks are joined, and your typed prose follows last. The echo backend below prints exactly what reached the model.

The wrapper is configurable via contextMentions.llmFormat:

const config = { contextMentions: { // "fenced" (default): ```label … ``` with automatic fence escalation // "document": Anthropic's <document><source> long-context shape llmFormat: "document", // or take full control per mention: // llmFormat: (entry, index) => `<file path="${entry.label}">\n${entry.text}\n</file>`, }, };