Imported from LodyAI/Lody (
packages/components/src/components/mentions/AGENTS.md). Install upstream withnpx skills add LodyAI/Lody --skill mentions. Copyright stays with the author.
src/components/mentions
CLAUDE.md is a symlink to this file. Edit AGENTS.md only.
Mention sources on src/ui/mention. Files: README.md.
Pipeline background: ui-mentions.md.
Triggers, menu, and candidates
@reaches every mention type through the two-level menu. Skills keep their direct$menu,/still opens commands directly, and#opens no menu but keeps its hydrator, so a pasted#123still expands before send.enableAtMentionsis the ONE list of what@reaches, gating both trigger registration and mounting<Mention>; every source with its ownenabledrule (sessions: having any) belongs there too. Placeholder hints advertise$only under the conditions that enable Skill mentions.- Desktop menus render through
MentionContent, capped atvar(--mention-input-width). insertTextmust keep its type's prompt form (@path,#123,$token,/cmd): reaching a type through@must not change what the agent receives. Directory candidates carry BOTHnavigateText(@dir/, descend) andinsertText(@dir, commit).getCandidatesstays lazy; bare@ranks nothing. File menus index/search in a Worker, cancel stale work, and publish current-source results only; no UI-thread fallback. Aggregate results are capped; Roles list all readable entries.- Issues and PRs rank over their own slice of the shared cache, partitioned once
by
useMentionCategories. - File, Session, Agent Role, Issue, and PR candidates use the vendored VS Code
scoreFuzzywith non-contiguous matching, wrapped by any source-specific ordering. Skills and commands keep their own ranking. - A candidate describes its side panel through the neutral
MentionCandidateDetailfields, which render verbatim — put i18n'd text there, never a raw enum. The one exception isdetail.agentRole, renderingsessions/agent-role-detail-pane.tsx: desktop only, fixed height, stable scrollbar gutter. - Lazy work is
MentionCategory.activation; category navigation starts its destination synchronously throughMentionItem.onMentionNavigate, whileselectMentionViewActivationscovers typed/pasted prefixes, direct triggers, and aggregate views. Both share the menu's once-per-menu-open latch, the menu owns no source-specific rule, and categories on one source share itssourceKey. - Activation means "make sure this is loaded", not "revalidate": Issues/PRs gate
on
ISSUE_PR_FRESH_FOR_MS, and only explicit gestures passrefresh({ force: true }). The fetch timestamp rides on the cached entry (survives IndexedDB). An unasked source reportsloading, neverreadywith zero rows.
Hydration and drafts
- Hydrators only add ranges for known tokens/items, preserve existing external
pasted_textranges, and must record akind. Hydration latches the first NON-EMPTY text, not the first render's. - A composer stores its ranges with its draft and restores them through
PersistedMentionHydrator; rebuilding from text is the fallback. Store the narrowPersistedMentionRange, never the live range.mergeHydratedMentionsdrops a hydrated range that OVERLAPS one already present, not merely a duplicate. - A composer that swaps drafts in place (the session one does) must pass
draftKey; the reset runs during render. - Locale files are flat dotted-key maps: i18next runs
keySeparator: false. vscode-fuzzy-score.tsis vendored: keep Microsoft's copyright header, the adjacent MIT license, and the generated third-party attribution when updating it.
Before-send expansion and transcript
useMentionPromptExpansionowns before-send rewrites (expand/getRewrites). Composer copy reuses them viagetExpandedClipboardTextForSelection(session →[@Title](session://…), not@slug). Rewritten:REWRITTEN_SPAN_KINDS; else verbatim.- The transcript chip comes from
MessageTextSpan.mark, FROZEN at send time, never resolved from the catalog at render. A span field must be declared in BOTHsanitizeMessageTextSpansand the strictMessageTextSpanSchema. agent_roleis the one span kind the message COPY button collapses back to its label (getCopyTextFromMessageItems); edit-and-resend still reads the expanded text throughgetTextContentFromMessageItems. Both chip surfaces read one kind → glyph and colour table (mention-chips.tsx).
Skills
$tokens must stay whitespace-free; hydration scans from$to the next whitespace. Known tokens expand touse /token [Skill Path](path)— project skills with their project-relativeSKILL.mdpath, home-scoped (global+system) skills with the CLI-provided absolute path — ordered project → global → system (compareProjectSkillScope).$candidates come fromuseProjectSkills, not Codex's runtime registry. One CLIlist-global-skillshome scan returns theglobalandsystemscopes, each filtered by the provider'sgetRegisteredGlobalSkillDirs/getRegisteredSystemSkillDirs;~/.agents/skillsis a provider-specific alias, never a universal fallback. The scanner handles flat and catalog layouts, and paths outside the registered roots appear only once their dirs are added.- A registered entry mapping to no dir (
deepagents) keeps its empty whitelist; an unregistered agent type getsnull(an emptySetfilters everything).
Sessions
useSessionMentionItemsis the single owner of the mentionable-session list, reading child-inclusiveallActiveSessions, notsessionListAtomsidebar rows; archived and own sessions stay excluded. Project scope is a menu-only filter over that complete list — never scope hydration, expansion, drag insertion, slug resolution, or child-session addressing.- A session mention commits as plain
@<title-slug>(nosession:marker); the range carriessessionId, and expansion rewrites to[@Title](session://<id>). A lone app-origin session URL paste becomes that mention unless Cmd/Ctrl+Shift+V. No range → send verbatim; never resolve a slug; hydration skips tokens the file source knows. Evidence: session:// URI. - Slugs resolve through the live list first, then a
localStorageslug → id map. That store stays synchronous, its key is registered inlib/clear-local-cache.ts, and the write is skipped when the serialized map is unchanged. - A session dragged from the sidebar or a session tab onto a chat surface
becomes a mention; the drop must be a REAL range, not
@<slug>text: usementionActionsRef.insertSessionMention(sessionId)(false for unknown, own, or already-mentioned). Draft and file/diff tabs are not mention sources. The conversation COLUMN paints ONEConversationDropOverlayviaSessionMentionDropLayer, never one per keep-alive tab page.
Agent Roles
- An Agent Role mention has the session mention's shape (plain
@<token>, stable Role id on the committed RANGE), but its rewrite asks the agent to CREATE a Session and carries the Role id only (rootAGENTS.mdowns MCP create/freeze). An unavailable Role stays plain text at send time. The token is DERIVED from the Role's name (getAgentRoleMentionSlug); renaming changes it, and uniqueness uses that token. - A Role candidate's emoji REPLACES the category glyph
(
MentionCandidate.iconEmoji), defaulted throughgetAgentRoleEmoji, and its candidate sets no detailtitle. The committed range shows that emoji throughapplyAgentRoleEmojiChip, boxed to the icon slot and clipped; its agent config and machine ride onAgentRoleMentionItem. - Role candidates pass visibility, executability, then work context: Local
Project pins to its machine; plain chat and GitHub may use authorized machines
unless bound to a
localWorktree. List all readable Roles; disabled rows follow available matches with a reason below the name. Only available Roles can be selected, hydrated from text, or expanded before send; never fall back.