Repository navigation
Description
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Area
apps/mobile (also apps/web and apps/desktop, via packages/shared)
Steps to reproduce
- Open a composer in the Android app (new thread or an existing thread).
- Type any
$wordthat is not a skill, followed by a space. For example:echo $HOME $PATH $anything $notaskill done.
Expected behavior
Only a $name that matches a skill the current provider actually offers becomes a skill chip. $HOME, $PATH, and made-up names stay plain text.
Actual behavior
Every $word becomes a skill chip as soon as it is followed by whitespace. In the screenshot only babysit is a real skill; HOME, PATH, anything, and notaskill are 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
| Surface | Affected | Notes |
|---|---|---|
| Android composer | Yes | Chips while typing. Screenshot above. |
| iOS composer | Yes (by code, not run) | Same shared tokenizer and same token mapping in T3ComposerEditor.ios.tsx. |
| Web / desktop composer | Yes | Not while typing, but whenever the editor is rebuilt from text: reloading with a saved draft (screenshot below) and, by code, pasting. |
| Sent messages (web and mobile) | No | SkillInlineText and decorateSkillRuns both look the name up in the skill list and leave unknown names as text. |
Web, after reloading a draft containing the same text:
Cause
collectComposerInlineTokens in packages/shared/src/composerInlineTokens.ts emits a skill token for anything matching SKILL_TOKEN_REGEX. Its own comment says so: "the composer chips any matched $name token, known or not". Only numeric amounts like $20 and $20k are excluded.
The composers then chip every such token without checking it against the provider's skills:
- Mobile:
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. - Web:
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
main at 53456bc
Environment
Android 16 emulator (Pixel), T3 Code Dev client. Web checked in Chromium on Linux against the same server.
Activity
juliusmarminge commentedon Oct 1, 2026
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.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.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.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.