Repository navigation
[Bug]: Composer styles any $word as a skill chip, even when no such skill exists #14656
Description
Activity
Note
Grok responding on behalf of Julius.
Triage
Thanks for the clear repro and the screenshots from both platforms, @tris203! This is still happening on current
main(094fb230e9), and that code hasn't changed since the commit you tested. I didn't find another open issue that reports this.The good news is that this is only a display problem in the composer. When you send the message,
$HOMEis never treated as a skill. Claude only swaps in a$namethat's an enabled skill users can call, and Cursor only swaps in skill names it has found. The chip is still wrong, though. It can't be edited like normal text, and its popover says "No description is available for this skill."What's happening
collectComposerInlineTokensmarks every$namethat matchesSKILL_TOKEN_REGEXas a skill. It doesn't check whether that skill actually exists. The regex leaves out amounts like$20and$20k, but nothing else filters the matches.- Mobile (Android and iOS) looks up the label with
skillLabels.get(token.value) ?? token.value. That means an unknown name still turns into a chip as soon as you type a space after it. This is whyanythingandnotaskillstay lowercase in the Android screenshot: they're the raw name, not a skill's display name. - Web and desktop don't chip while you type. Pasting, restoring a draft, or anything else that rebuilds the editor goes through
skillLabelFor. For an unknown name, that falls back to a title-cased version of it, which is where "Anything" and "Notaskill" in the web screenshot come from. - Sent messages already get it right.
SkillInlineTextanddecorateSkillRunsskip names that aren't in the skill list.
Suggested fix
A chip should only appear for a skill the current provider can actually run, the same rule the sending code uses.
$HOME,$PATH, made-up names, and amounts should all stay plain text. The tokenizer doesn't need to change. The composers should just stop turning unknown tokens into chips, the way the timeline already does.One catch: the skill list loads after the editor opens and is different for each provider, so a real skill still has to become a chip once its list arrives or when you switch providers. Mobile already redraws when
skillLabelschanges. On web,skillLabelFornever changes, and the editor only rebuilds when the prompt text changes. If the fix only filters inside the label lookup, a draft that's already been built would stay plain text until the next rebuild.- Mobile (Android and iOS) looks up the label with
- addedbugSomething is broken or behaving incorrectly.Something is broken or behaving incorrectly.via-triageFiled through npx t3 triageFiled through npx t3 triage
on Oct 1, 2026
Before submitting
Area
apps/mobile (also apps/web and apps/desktop, via
packages/shared)Steps to reproduce
$wordthat is not a skill, followed by a space. For example:echo $HOME $PATH $anything $notaskill done.Expected behavior
Only a
$namethat matches a skill the current provider actually offers becomes a skill chip.$HOME,$PATH, and made-up names stay plain text.Actual behavior
Every
$wordbecomes a skill chip as soon as it is followed by whitespace. In the screenshot onlybabysitis a real skill;HOME,PATH,anything, andnotaskillare not, and they look identical to it.This is easy to hit in normal prompts, since shell variables (
$HOME,$PATH,$1-style names with a letter) are common when talking to a coding agent. The chip implies a skill will be invoked when none exists, and because chips are atomic it also makes that text awkward to edit.Affected surfaces
T3ComposerEditor.ios.tsx.SkillInlineTextanddecorateSkillRunsboth look the name up in the skill list and leave unknown names as text.Web, after reloading a draft containing the same text:
Cause
collectComposerInlineTokensinpackages/shared/src/composerInlineTokens.tsemits askilltoken for anything matchingSKILL_TOKEN_REGEX. Its own comment says so: "the composer chips any matched$nametoken, known or not". Only numeric amounts like$20and$20kare excluded.The composers then chip every such token without checking it against the provider's skills:
apps/mobile/src/native/T3ComposerEditor.native.tsx(Android) andT3ComposerEditor.ios.tsxuseskillLabels.get(token.value) ?? token.value, so an unknown name just falls back to its raw text as the chip label.skillLabelForinapps/web/src/components/ComposerPromptEditorTiptap.tsxfalls back toformatProviderSkillDisplayName({ name })for unknown names, andatomJsonForSegmentincomposer-rich-text-doc.tsalways emits acomposer-skillnode.The sent-message renderers already do the right thing by skipping names that are not in the skill list, so the composers are inconsistent with the timeline: the chip disappears once the message is sent.
One thing a fix needs to keep in mind: the skill list loads asynchronously and is per provider, so a real skill should still chip once the list arrives or the provider changes.
Impact
Minor bug or occasional failure
Version or commit
mainat 53456bcEnvironment
Android 16 emulator (Pixel), T3 Code Dev client. Web checked in Chromium on Linux against the same server.