Skip to content

[Bug]: Composer styles any $word as a skill chip, even when no such skill exists #14656

Description

@tris203

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

  1. Open a composer in the Android app (new thread or an existing thread).
  2. Type any $word that 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.

Android composer showing HOME, PATH, anything and notaskill styled as skill chips next to the real babysit skill

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:

Web composer showing HOME, PATH, Anything, Notaskill and Foo Bar styled as skill chips after reloading a draft

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) and T3ComposerEditor.ios.tsx use skillLabels.get(token.value) ?? token.value, so an unknown name just falls back to its raw text as the chip label.
  • Web: skillLabelFor in apps/web/src/components/ComposerPromptEditorTiptap.tsx falls back to formatProviderSkillDisplayName({ name }) for unknown names, and atomJsonForSegment in composer-rich-text-doc.ts always emits a composer-skill node.

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

  1. juliusmarminge commented on Oct 1, 2026

    @juliusmarminge
    Member

    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, $HOME is never treated as a skill. Claude only swaps in a $name that'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

    collectComposerInlineTokens marks every $name that matches SKILL_TOKEN_REGEX as a skill. It doesn't check whether that skill actually exists. The regex leaves out amounts like $20 and $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 why anything and notaskill stay 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. SkillInlineText and decorateSkillRuns skip 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 skillLabels changes. On web, skillLabelFor never 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.

  2. added
    bugSomething is broken or behaving incorrectly.
    via-triageFiled through npx t3 triage
    on Oct 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is broken or behaving incorrectly.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions