chore: use @plugin instead of $plugin for plaintext mentions (#13921)

change plaintext plugin-mentions from `$plugin` to `@plugin`, ensure TUI
can correctly decode these from history.

tested locally, added/updated tests.
This commit is contained in:
sayan-oai 2026-03-07 17:36:39 -08:00 committed by GitHub
parent bf5c2f48a5
commit 590cfa6176
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 98 additions and 16 deletions

View file

@ -55,6 +55,7 @@ pub use mcp_connection_manager::SandboxState;
pub use text_encoding::bytes_to_string_smart;
mod mcp_tool_call;
mod memories;
pub mod mention_syntax;
mod mentions;
mod message_history;
mod model_provider_info;

View file

@ -0,0 +1,4 @@
// Default plaintext sigil for tools.
pub const TOOL_MENTION_SIGIL: char = '$';
// Plugins use `@` in linked plaintext outside TUI.
pub const PLUGIN_TEXT_MENTION_SIGIL: char = '@';

View file

@ -5,11 +5,13 @@ use std::path::PathBuf;
use codex_protocol::user_input::UserInput;
use crate::connectors;
use crate::mention_syntax::PLUGIN_TEXT_MENTION_SIGIL;
use crate::mention_syntax::TOOL_MENTION_SIGIL;
use crate::plugins::PluginCapabilitySummary;
use crate::skills::SkillMetadata;
use crate::skills::injection::ToolMentionKind;
use crate::skills::injection::app_id_from_path;
use crate::skills::injection::extract_tool_mentions;
use crate::skills::injection::extract_tool_mentions_with_sigil;
use crate::skills::injection::plugin_config_name_from_path;
use crate::skills::injection::tool_kind_for_path;
@ -19,10 +21,17 @@ pub(crate) struct CollectedToolMentions {
}
pub(crate) fn collect_tool_mentions_from_messages(messages: &[String]) -> CollectedToolMentions {
collect_tool_mentions_from_messages_with_sigil(messages, TOOL_MENTION_SIGIL)
}
fn collect_tool_mentions_from_messages_with_sigil(
messages: &[String],
sigil: char,
) -> CollectedToolMentions {
let mut plain_names = HashSet::new();
let mut paths = HashSet::new();
for message in messages {
let mentions = extract_tool_mentions(message);
let mentions = extract_tool_mentions_with_sigil(message, sigil);
plain_names.extend(mentions.plain_names().map(str::to_string));
paths.extend(mentions.paths().map(str::to_string));
}
@ -50,7 +59,7 @@ pub(crate) fn collect_explicit_app_ids(input: &[UserInput]) -> HashSet<String> {
.collect()
}
/// Collect explicit structured `plugin://...` mentions.
/// Collect explicit structured or linked `plugin://...` mentions.
pub(crate) fn collect_explicit_plugin_mentions(
input: &[UserInput],
plugins: &[PluginCapabilitySummary],
@ -73,7 +82,11 @@ pub(crate) fn collect_explicit_plugin_mentions(
UserInput::Mention { path, .. } => Some(path.clone()),
_ => None,
})
.chain(collect_tool_mentions_from_messages(&messages).paths)
.chain(
// Plugin plaintext links use `@`, not the default `$` tool sigil.
collect_tool_mentions_from_messages_with_sigil(&messages, PLUGIN_TEXT_MENTION_SIGIL)
.paths,
)
.filter(|path| tool_kind_for_path(path.as_str()) == ToolMentionKind::Plugin)
.filter_map(|path| plugin_config_name_from_path(path.as_str()).map(str::to_string))
.collect();
@ -222,7 +235,7 @@ mod tests {
];
let mentioned = collect_explicit_plugin_mentions(
&[text_input("use [$sample](plugin://sample@test)")],
&[text_input("use [@sample](plugin://sample@test)")],
&plugins,
);
@ -238,7 +251,7 @@ mod tests {
let mentioned = collect_explicit_plugin_mentions(
&[
text_input("use [$sample](plugin://sample@test)"),
text_input("use [@sample](plugin://sample@test)"),
UserInput::Mention {
name: "sample".to_string(),
path: "plugin://sample@test".to_string(),
@ -263,4 +276,16 @@ mod tests {
assert_eq!(mentioned, Vec::<PluginCapabilitySummary>::new());
}
#[test]
fn collect_explicit_plugin_mentions_ignores_dollar_linked_plugin_mentions() {
let plugins = vec![plugin("sample@test", "sample")];
let mentioned = collect_explicit_plugin_mentions(
&[text_input("use [$sample](plugin://sample@test)")],
&plugins,
);
assert_eq!(mentioned, Vec::<PluginCapabilitySummary>::new());
}
}

View file

@ -7,6 +7,7 @@ use crate::analytics_client::InvocationType;
use crate::analytics_client::SkillInvocation;
use crate::analytics_client::TrackEventsContext;
use crate::instructions::SkillInstructions;
use crate::mention_syntax::TOOL_MENTION_SIGIL;
use crate::mentions::build_skill_name_counts;
use crate::skills::SkillMetadata;
use codex_otel::SessionTelemetry;
@ -232,10 +233,10 @@ pub(crate) fn normalize_skill_path(path: &str) -> &str {
/// resource path is present, it is captured for exact path matching while also tracking
/// the name for fallback matching.
pub(crate) fn extract_tool_mentions(text: &str) -> ToolMentions<'_> {
extract_tool_mentions_with_sigil(text, '$')
extract_tool_mentions_with_sigil(text, TOOL_MENTION_SIGIL)
}
fn extract_tool_mentions_with_sigil(text: &str, sigil: char) -> ToolMentions<'_> {
pub(crate) fn extract_tool_mentions_with_sigil(text: &str, sigil: char) -> ToolMentions<'_> {
let text_bytes = text.as_bytes();
let mut mentioned_names: HashSet<&str> = HashSet::new();
let mut mentioned_paths: HashSet<&str> = HashSet::new();

View file

@ -14,6 +14,7 @@ use crate::skills_helpers::skill_description;
use crate::skills_helpers::skill_display_name;
use codex_chatgpt::connectors::AppInfo;
use codex_core::connectors::connector_mention_slug;
use codex_core::mention_syntax::TOOL_MENTION_SIGIL;
use codex_core::skills::model::SkillDependencies;
use codex_core::skills::model::SkillInterface;
use codex_core::skills::model::SkillMetadata;
@ -296,8 +297,6 @@ pub(crate) struct ToolMentions {
linked_paths: HashMap<String, String>,
}
const TOOL_MENTION_SIGIL: char = '$';
fn extract_tool_mentions_from_text(text: &str) -> ToolMentions {
extract_tool_mentions_from_text_with_sigil(text, TOOL_MENTION_SIGIL)
}

View file

@ -1,14 +1,15 @@
use std::collections::HashMap;
use std::collections::VecDeque;
use codex_core::mention_syntax::PLUGIN_TEXT_MENTION_SIGIL;
use codex_core::mention_syntax::TOOL_MENTION_SIGIL;
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct LinkedMention {
pub(crate) mention: String,
pub(crate) path: String,
}
const TOOL_MENTION_SIGIL: char = '$';
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct DecodedHistoryText {
pub(crate) text: String,
@ -77,10 +78,7 @@ pub(crate) fn decode_history_mentions(text: &str) -> DecodedHistoryText {
while index < bytes.len() {
if bytes[index] == b'['
&& let Some((name, path, end_index)) =
parse_linked_tool_mention(text, bytes, index, TOOL_MENTION_SIGIL)
&& !is_common_env_var(name)
&& is_tool_path(path)
&& let Some((name, path, end_index)) = parse_history_linked_mention(text, bytes, index)
{
out.push(TOOL_MENTION_SIGIL);
out.push_str(name);
@ -105,6 +103,31 @@ pub(crate) fn decode_history_mentions(text: &str) -> DecodedHistoryText {
}
}
fn parse_history_linked_mention<'a>(
text: &'a str,
text_bytes: &[u8],
start: usize,
) -> Option<(&'a str, &'a str, usize)> {
// TUI writes `$name`, but may read plugin `[@name](plugin://...)` links from other clients.
if let Some(mention @ (name, path, _)) =
parse_linked_tool_mention(text, text_bytes, start, TOOL_MENTION_SIGIL)
&& !is_common_env_var(name)
&& is_tool_path(path)
{
return Some(mention);
}
if let Some(mention @ (name, path, _)) =
parse_linked_tool_mention(text, text_bytes, start, PLUGIN_TEXT_MENTION_SIGIL)
&& !is_common_env_var(name)
&& path.starts_with("plugin://")
{
return Some(mention);
}
None
}
fn parse_linked_tool_mention<'a>(
text: &'a str,
text_bytes: &[u8],
@ -225,6 +248,35 @@ mod tests {
);
}
#[test]
fn decode_history_mentions_restores_plugin_links_with_at_sigil() {
let decoded = decode_history_mentions(
"Use [@sample](plugin://sample@test) and [$figma](app://figma-1).",
);
assert_eq!(decoded.text, "Use $sample and $figma.");
assert_eq!(
decoded.mentions,
vec![
LinkedMention {
mention: "sample".to_string(),
path: "plugin://sample@test".to_string(),
},
LinkedMention {
mention: "figma".to_string(),
path: "app://figma-1".to_string(),
},
]
);
}
#[test]
fn decode_history_mentions_ignores_at_sigil_for_non_plugin_paths() {
let decoded = decode_history_mentions("Use [@figma](app://figma-1).");
assert_eq!(decoded.text, "Use [@figma](app://figma-1).");
assert_eq!(decoded.mentions, Vec::<LinkedMention>::new());
}
#[test]
fn encode_history_mentions_links_bound_mentions_in_order() {
let text = "$figma then $sample then $figma then $other";