mirror of
https://github.com/openai/codex.git
synced 2026-05-04 05:11:37 +03:00
Rename tui_app_server to tui (#16104)
This is a follow-up to https://github.com/openai/codex/pull/15922. That previous PR deleted the old `tui` directory and left the new `tui_app_server` directory in place. This PR renames `tui_app_server` to `tui` and fixes up all references.
This commit is contained in:
306
codex-rs/tui/src/mention_codec.rs
Normal file
306
codex-rs/tui/src/mention_codec.rs
Normal file
@@ -0,0 +1,306 @@
|
||||
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,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct DecodedHistoryText {
|
||||
pub(crate) text: String,
|
||||
pub(crate) mentions: Vec<LinkedMention>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn encode_history_mentions(text: &str, mentions: &[LinkedMention]) -> String {
|
||||
if mentions.is_empty() || text.is_empty() {
|
||||
return text.to_string();
|
||||
}
|
||||
|
||||
let mut mentions_by_name: HashMap<&str, VecDeque<&str>> = HashMap::new();
|
||||
for mention in mentions {
|
||||
mentions_by_name
|
||||
.entry(mention.mention.as_str())
|
||||
.or_default()
|
||||
.push_back(mention.path.as_str());
|
||||
}
|
||||
|
||||
let bytes = text.as_bytes();
|
||||
let mut out = String::with_capacity(text.len());
|
||||
let mut index = 0usize;
|
||||
|
||||
while index < bytes.len() {
|
||||
if bytes[index] == TOOL_MENTION_SIGIL as u8 {
|
||||
let name_start = index + 1;
|
||||
if let Some(first) = bytes.get(name_start)
|
||||
&& is_mention_name_char(*first)
|
||||
{
|
||||
let mut name_end = name_start + 1;
|
||||
while let Some(next) = bytes.get(name_end)
|
||||
&& is_mention_name_char(*next)
|
||||
{
|
||||
name_end += 1;
|
||||
}
|
||||
|
||||
let name = &text[name_start..name_end];
|
||||
if let Some(path) = mentions_by_name.get_mut(name).and_then(VecDeque::pop_front) {
|
||||
out.push('[');
|
||||
out.push(TOOL_MENTION_SIGIL);
|
||||
out.push_str(name);
|
||||
out.push_str("](");
|
||||
out.push_str(path);
|
||||
out.push(')');
|
||||
index = name_end;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let Some(ch) = text[index..].chars().next() else {
|
||||
break;
|
||||
};
|
||||
out.push(ch);
|
||||
index += ch.len_utf8();
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
pub(crate) fn decode_history_mentions(text: &str) -> DecodedHistoryText {
|
||||
let bytes = text.as_bytes();
|
||||
let mut out = String::with_capacity(text.len());
|
||||
let mut mentions = Vec::new();
|
||||
let mut index = 0usize;
|
||||
|
||||
while index < bytes.len() {
|
||||
if bytes[index] == b'['
|
||||
&& let Some((name, path, end_index)) = parse_history_linked_mention(text, bytes, index)
|
||||
{
|
||||
out.push(TOOL_MENTION_SIGIL);
|
||||
out.push_str(name);
|
||||
mentions.push(LinkedMention {
|
||||
mention: name.to_string(),
|
||||
path: path.to_string(),
|
||||
});
|
||||
index = end_index;
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(ch) = text[index..].chars().next() else {
|
||||
break;
|
||||
};
|
||||
out.push(ch);
|
||||
index += ch.len_utf8();
|
||||
}
|
||||
|
||||
DecodedHistoryText {
|
||||
text: out,
|
||||
mentions,
|
||||
}
|
||||
}
|
||||
|
||||
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],
|
||||
start: usize,
|
||||
sigil: char,
|
||||
) -> Option<(&'a str, &'a str, usize)> {
|
||||
let sigil_index = start + 1;
|
||||
if text_bytes.get(sigil_index) != Some(&(sigil as u8)) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let name_start = sigil_index + 1;
|
||||
let first_name_byte = text_bytes.get(name_start)?;
|
||||
if !is_mention_name_char(*first_name_byte) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut name_end = name_start + 1;
|
||||
while let Some(next_byte) = text_bytes.get(name_end)
|
||||
&& is_mention_name_char(*next_byte)
|
||||
{
|
||||
name_end += 1;
|
||||
}
|
||||
|
||||
if text_bytes.get(name_end) != Some(&b']') {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut path_start = name_end + 1;
|
||||
while let Some(next_byte) = text_bytes.get(path_start)
|
||||
&& next_byte.is_ascii_whitespace()
|
||||
{
|
||||
path_start += 1;
|
||||
}
|
||||
if text_bytes.get(path_start) != Some(&b'(') {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut path_end = path_start + 1;
|
||||
while let Some(next_byte) = text_bytes.get(path_end)
|
||||
&& *next_byte != b')'
|
||||
{
|
||||
path_end += 1;
|
||||
}
|
||||
if text_bytes.get(path_end) != Some(&b')') {
|
||||
return None;
|
||||
}
|
||||
|
||||
let path = text[path_start + 1..path_end].trim();
|
||||
if path.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let name = &text[name_start..name_end];
|
||||
Some((name, path, path_end + 1))
|
||||
}
|
||||
|
||||
fn is_mention_name_char(byte: u8) -> bool {
|
||||
matches!(byte, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_' | b'-')
|
||||
}
|
||||
|
||||
fn is_common_env_var(name: &str) -> bool {
|
||||
let upper = name.to_ascii_uppercase();
|
||||
matches!(
|
||||
upper.as_str(),
|
||||
"PATH"
|
||||
| "HOME"
|
||||
| "USER"
|
||||
| "SHELL"
|
||||
| "PWD"
|
||||
| "TMPDIR"
|
||||
| "TEMP"
|
||||
| "TMP"
|
||||
| "LANG"
|
||||
| "TERM"
|
||||
| "XDG_CONFIG_HOME"
|
||||
)
|
||||
}
|
||||
|
||||
fn is_tool_path(path: &str) -> bool {
|
||||
path.starts_with("app://")
|
||||
|| path.starts_with("mcp://")
|
||||
|| path.starts_with("plugin://")
|
||||
|| path.starts_with("skill://")
|
||||
|| path
|
||||
.rsplit(['/', '\\'])
|
||||
.next()
|
||||
.is_some_and(|name| name.eq_ignore_ascii_case("SKILL.md"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn decode_history_mentions_restores_visible_tokens() {
|
||||
let decoded = decode_history_mentions(
|
||||
"Use [$figma](app://figma-1), [$sample](plugin://sample@test), and [$figma](/tmp/figma/SKILL.md).",
|
||||
);
|
||||
assert_eq!(decoded.text, "Use $figma, $sample, and $figma.");
|
||||
assert_eq!(
|
||||
decoded.mentions,
|
||||
vec![
|
||||
LinkedMention {
|
||||
mention: "figma".to_string(),
|
||||
path: "app://figma-1".to_string(),
|
||||
},
|
||||
LinkedMention {
|
||||
mention: "sample".to_string(),
|
||||
path: "plugin://sample@test".to_string(),
|
||||
},
|
||||
LinkedMention {
|
||||
mention: "figma".to_string(),
|
||||
path: "/tmp/figma/SKILL.md".to_string(),
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[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";
|
||||
let encoded = encode_history_mentions(
|
||||
text,
|
||||
&[
|
||||
LinkedMention {
|
||||
mention: "figma".to_string(),
|
||||
path: "app://figma-app".to_string(),
|
||||
},
|
||||
LinkedMention {
|
||||
mention: "sample".to_string(),
|
||||
path: "plugin://sample@test".to_string(),
|
||||
},
|
||||
LinkedMention {
|
||||
mention: "figma".to_string(),
|
||||
path: "/tmp/figma/SKILL.md".to_string(),
|
||||
},
|
||||
],
|
||||
);
|
||||
assert_eq!(
|
||||
encoded,
|
||||
"[$figma](app://figma-app) then [$sample](plugin://sample@test) then [$figma](/tmp/figma/SKILL.md) then $other"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user