make dollar-mention always clarify item category (skill, app, plugin) (#14147)

#### What

###### Context + Problem

With the introduction of plugins, we now have one more type of
`$`-mentionable item in the TUI's popup menu on `$`. Apps, skills, and
plugins can all have the same user-facing name, and we attempt to
distinguish with a category tag suffix, like `[App]`. This has a few
problems:

- We decide to show tags by the text that will be inserted into the
conversation, not the actual user-visible text, so two visibly-identical
entries can have no clarifying category tag suffix
- The category tag is a suffix and commonly gets cut off by long
descriptions
- The skill category tag is currently only displayed on repo skills as
`[Repo]`, which is confusing to most users
- The plugin category tag is currently `[<marketplace-name>]`, which is
also confusing to most users

###### Solution
- **Always** show a **prefix** category tag that is `[Skill]`, `[App]`,
or `[Plugin]`. No conditional rendering or copy.

Before:
<img width="801" height="153" alt="image"
src="https://github.com/user-attachments/assets/448e06e7-2af8-4c14-9804-ed1ca17cf514"
/>

After:
<img width="800" height="118" alt="image"
src="https://github.com/user-attachments/assets/57895b41-06fe-4d92-887b-68704c5a15fd"
/>

I also feel this clarifies the results at-a-glance while you scroll:


https://github.com/user-attachments/assets/cbdd5840-53d9-4656-812c-6e816755e1fd

### Tests
Added + updated tests (including snapshots), tested locally
This commit is contained in:
sayan-oai
2026-03-09 19:35:11 -07:00
committed by GitHub
parent 1165a16e6f
commit a5af11211a
4 changed files with 85 additions and 18 deletions

View File

@@ -3579,8 +3579,7 @@ impl ChatComposer {
insert_text: format!("${skill_name}"),
search_terms,
path: Some(skill.path_to_skills_md.to_string_lossy().into_owned()),
category_tag: (skill.scope == codex_protocol::protocol::SkillScope::Repo)
.then(|| "[Repo]".to_string()),
category_tag: Some("[Skill]".to_string()),
});
}
}
@@ -3631,8 +3630,7 @@ impl ChatComposer {
insert_text: format!("${plugin_name}"),
search_terms,
path: Some(format!("plugin://{}", plugin.config_name)),
category_tag: (!marketplace_name.is_empty())
.then(|| format!("[{marketplace_name}]")),
category_tag: Some("[Plugin]".to_string()),
});
}
}
@@ -3660,16 +3658,6 @@ impl ChatComposer {
}
}
let mut counts: HashMap<String, usize> = HashMap::new();
for mention in &mentions {
*counts.entry(mention.insert_text.clone()).or_insert(0) += 1;
}
for mention in &mut mentions {
if counts.get(&mention.insert_text).copied().unwrap_or(0) <= 1 {
mention.category_tag = None;
}
}
mentions
}
@@ -5343,6 +5331,60 @@ mod tests {
});
}
#[test]
fn mention_popup_type_prefixes_snapshot() {
snapshot_composer_state_with_width("mention_popup_type_prefixes", 72, false, |composer| {
composer.set_connectors_enabled(true);
composer.set_text_content("$goog".to_string(), Vec::new(), Vec::new());
composer.set_skill_mentions(Some(vec![SkillMetadata {
name: "google-calendar-skill".to_string(),
description: "Find availability and plan event changes".to_string(),
short_description: None,
interface: Some(codex_core::skills::model::SkillInterface {
display_name: Some("Google Calendar".to_string()),
short_description: None,
icon_small: None,
icon_large: None,
brand_color: None,
default_prompt: None,
}),
dependencies: None,
policy: None,
permission_profile: None,
path_to_skills_md: PathBuf::from("/tmp/repo/google-calendar/SKILL.md"),
scope: codex_protocol::protocol::SkillScope::Repo,
}]));
composer.set_plugin_mentions(Some(vec![PluginCapabilitySummary {
config_name: "google-calendar@debug".to_string(),
display_name: "Google Calendar".to_string(),
description: Some(
"Connect Google Calendar for scheduling, availability, and event management."
.to_string(),
),
has_skills: false,
mcp_server_names: vec!["google-calendar".to_string()],
app_connector_ids: Vec::new(),
}]));
composer.set_connector_mentions(Some(ConnectorsSnapshot {
connectors: vec![AppInfo {
id: "google_calendar".to_string(),
name: "Google Calendar".to_string(),
description: Some("Look up events and availability".to_string()),
logo_url: None,
logo_url_dark: None,
distribution_channel: None,
branding: None,
app_metadata: None,
labels: None,
install_url: Some("https://example.test/google-calendar".to_string()),
is_accessible: true,
is_enabled: true,
plugin_display_names: Vec::new(),
}],
}));
});
}
#[test]
fn set_connector_mentions_excludes_disabled_apps_from_mention_popup() {
let (tx, _rx) = unbounded_channel::<AppEvent>();

View File

@@ -98,14 +98,26 @@ impl SkillPopup {
.map(|(idx, indices, _score)| {
let mention = &self.mentions[idx];
let name = truncate_text(&mention.display_name, MENTION_NAME_TRUNCATE_LEN);
let description = mention.description.clone().unwrap_or_default();
let description = match (
mention.category_tag.as_deref(),
mention.description.as_deref(),
) {
(Some(tag), Some(description)) if !description.is_empty() => {
Some(format!("{tag} {description}"))
}
(Some(tag), _) => Some(tag.to_string()),
(None, Some(description)) if !description.is_empty() => {
Some(description.to_string())
}
_ => None,
};
GenericDisplayRow {
name,
name_prefix_spans: Vec::new(),
match_indices: indices,
display_shortcut: None,
description: Some(description).filter(|desc| !desc.is_empty()),
category_tag: mention.category_tag.clone(),
description,
category_tag: None,
is_disabled: false,
disabled_reason: None,
wrap_indent: None,

View File

@@ -0,0 +1,13 @@
---
source: tui/src/bottom_pane/chat_composer.rs
expression: terminal.backend()
---
" "
" $goog "
" "
" "
" Google Calendar [Skill] Find availability and plan event changes "
" Google Calendar [Plugin] Connect Google Calendar for scheduling, ava…"
" Google Calendar [App] Look up events and availability "
" "
" Press enter to insert or esc to close "

View File

@@ -8,6 +8,6 @@ expression: terminal.backend()
" "
" "
" "
" Sample Plugin Plugin that includes the Figma MCP server and Skills for common workflows "
" Sample Plugin [Plugin] Plugin that includes the Figma MCP server and Skills for common workflows "
" "
" Press enter to insert or esc to close "