From 7e056fed05f01fbd4e480dd57a97985bd07ce0e7 Mon Sep 17 00:00:00 2001 From: Matthew Zeng Date: Wed, 21 Jan 2026 23:25:38 -0800 Subject: [PATCH] update --- codex-rs/Cargo.lock | 1 - codex-rs/tui/Cargo.toml | 1 - codex-rs/tui/src/app.rs | 11 - codex-rs/tui/src/app_event.rs | 16 -- codex-rs/tui/src/bottom_pane/chat_composer.rs | 212 ++++------------- codex-rs/tui/src/bottom_pane/command_popup.rs | 3 - codex-rs/tui/src/bottom_pane/mod.rs | 13 -- codex-rs/tui/src/bottom_pane/skill_popup.rs | 79 ++----- codex-rs/tui/src/chatwidget.rs | 214 ------------------ codex-rs/tui/src/chatwidget/tests.rs | 1 - codex-rs/tui/src/skills_helpers.rs | 4 + codex-rs/tui/src/slash_command.rs | 4 - 12 files changed, 74 insertions(+), 485 deletions(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 83f6a037a0..9f5a483cdd 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1749,7 +1749,6 @@ dependencies = [ "codex-app-server-protocol", "codex-arg0", "codex-backend-client", - "codex-chatgpt", "codex-cli", "codex-common", "codex-core", diff --git a/codex-rs/tui/Cargo.toml b/codex-rs/tui/Cargo.toml index 820fe20e93..1d20542365 100644 --- a/codex-rs/tui/Cargo.toml +++ b/codex-rs/tui/Cargo.toml @@ -30,7 +30,6 @@ codex-ansi-escape = { workspace = true } codex-app-server-protocol = { workspace = true } codex-arg0 = { workspace = true } codex-backend-client = { workspace = true } -codex-chatgpt = { workspace = true } codex-common = { workspace = true, features = [ "cli", "elapsed", diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 8a98ab71d8..7d2636bf33 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -1032,14 +1032,6 @@ impl App { )); tui.frame_requester().schedule_frame(); } - AppEvent::OpenAppLink { - title, - instructions, - url, - } => { - self.chat_widget - .open_app_link_view(title, instructions, url); - } AppEvent::StartFileSearch(query) => { if !query.is_empty() { self.file_search.on_user_query(query); @@ -1051,9 +1043,6 @@ impl App { AppEvent::RateLimitSnapshotFetched(snapshot) => { self.chat_widget.on_rate_limit_snapshot(Some(snapshot)); } - AppEvent::ConnectorsLoaded(result) => { - self.chat_widget.on_connectors_loaded(result); - } AppEvent::UpdateReasoningEffort(effort) => { self.on_update_reasoning_effort(effort); } diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index 0f69b0da2b..68053a1d21 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -10,7 +10,6 @@ use std::path::PathBuf; -use codex_chatgpt::connectors::ConnectorInfo; use codex_common::approval_presets::ApprovalPreset; use codex_core::protocol::Event; use codex_core::protocol::RateLimitSnapshot; @@ -40,11 +39,6 @@ pub(crate) enum WindowsSandboxFallbackReason { ElevationFailed, } -#[derive(Debug, Clone)] -pub(crate) struct ConnectorsSnapshot { - pub(crate) connectors: Vec, -} - #[allow(clippy::large_enum_variant)] #[derive(Debug)] pub(crate) enum AppEvent { @@ -94,19 +88,9 @@ pub(crate) enum AppEvent { /// Result of refreshing rate limits RateLimitSnapshotFetched(RateLimitSnapshot), - /// Result of prefetching connectors. - ConnectorsLoaded(Result), - /// Result of computing a `/diff` command. DiffResult(String), - /// Open the app link view in the bottom pane. - OpenAppLink { - title: String, - instructions: String, - url: String, - }, - InsertHistoryCell(Box), StartCommitAnimation, diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index fbd2b075f9..f75a26024c 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -3,7 +3,7 @@ //! It is responsible for: //! //! - Editing the input buffer (a [`TextArea`]), including placeholder "elements" for attachments. -//! - Routing keys to the active popup (slash commands, file search, skill/connector mentions). +//! - Routing keys to the active popup (slash commands, file search, skill mentions). //! - Handling submit vs newline on Enter. //! - Turning raw key streams into explicit paste operations on platforms where terminals //! don't provide reliable bracketed paste (notably Windows). @@ -104,7 +104,6 @@ use super::footer::reset_mode_after_activity; use super::footer::toggle_shortcut_mode; use super::paste_burst::CharDecision; use super::paste_burst::PasteBurst; -use super::skill_popup::MentionItem; use super::skill_popup::SkillPopup; use crate::bottom_pane::paste_burst::FlushResult; use crate::bottom_pane::prompt_args::expand_custom_prompt; @@ -127,7 +126,6 @@ use codex_protocol::user_input::ByteRange; use codex_protocol::user_input::TextElement; use crate::app_event::AppEvent; -use crate::app_event::ConnectorsSnapshot; use crate::app_event_sender::AppEventSender; use crate::bottom_pane::LocalImageAttachment; use crate::bottom_pane::textarea::TextArea; @@ -136,8 +134,6 @@ use crate::clipboard_paste::normalize_pasted_path; use crate::clipboard_paste::pasted_image_format; use crate::history_cell; use crate::ui_consts::LIVE_PREFIX_COLS; -use codex_chatgpt::connectors; -use codex_chatgpt::connectors::ConnectorInfo; use codex_core::skills::model::SkillMetadata; use codex_file_search::FileMatch; use std::cell::RefCell; @@ -229,12 +225,10 @@ pub(crate) struct ChatComposer { context_window_percent: Option, context_window_used_tokens: Option, skills: Option>, - connectors_snapshot: Option, - dismissed_mention_popup_token: Option, + dismissed_skill_popup_token: Option, /// When enabled, `Enter` submits immediately and `Tab` requests queuing behavior. steer_enabled: bool, collaboration_modes_enabled: bool, - connectors_enabled: bool, } #[derive(Clone, Debug)] @@ -292,11 +286,9 @@ impl ChatComposer { context_window_percent: None, context_window_used_tokens: None, skills: None, - connectors_snapshot: None, - dismissed_mention_popup_token: None, + dismissed_skill_popup_token: None, steer_enabled: false, collaboration_modes_enabled: false, - connectors_enabled: false, }; // Apply configuration via the setter to keep side-effects centralized. this.set_disable_paste_burst(disable_paste_burst); @@ -307,10 +299,6 @@ impl ChatComposer { self.skills = skills; } - pub fn set_connector_mentions(&mut self, connectors_snapshot: Option) { - self.connectors_snapshot = connectors_snapshot; - } - /// Enables or disables "Steer" behavior for submission keys. /// /// When steer is enabled, `Enter` produces [`InputResult::Submitted`] (send immediately) and @@ -325,10 +313,6 @@ impl ChatComposer { self.collaboration_modes_enabled = enabled; } - pub fn set_connectors_enabled(&mut self, enabled: bool) { - self.connectors_enabled = enabled; - } - fn layout_areas(&self, area: Rect) -> [Rect; 3] { let footer_props = self.footer_props(); let footer_hint_height = self @@ -1245,8 +1229,8 @@ impl ChatComposer { KeyEvent { code: KeyCode::Esc, .. } => { - if let Some(tok) = self.current_mention_token() { - self.dismissed_mention_popup_token = Some(tok); + if let Some(tok) = self.current_skill_token() { + self.dismissed_skill_popup_token = Some(tok); } self.active_popup = ActivePopup::None; (InputResult::None, true) @@ -1259,11 +1243,9 @@ impl ChatComposer { modifiers: KeyModifiers::NONE, .. } => { - let selected = popup - .selected_mention() - .map(|mention| mention.insert_text.clone()); - if let Some(insert_text) = selected { - self.insert_selected_mention(&insert_text); + let selected = popup.selected_skill().map(|skill| skill.name.clone()); + if let Some(name) = selected { + self.insert_selected_skill(&name); } self.active_popup = ActivePopup::None; (InputResult::None, true) @@ -1382,21 +1364,12 @@ impl ChatComposer { (rebuilt, rebuilt_elements) } - pub fn skills(&self) -> Option<&Vec> { - self.skills.as_ref() + fn skills_enabled(&self) -> bool { + self.skills.as_ref().is_some_and(|s| !s.is_empty()) } - fn mentions_enabled(&self) -> bool { - let skills_ready = self - .skills - .as_ref() - .is_some_and(|skills| !skills.is_empty()); - let connectors_ready = self.connectors_enabled - && self - .connectors_snapshot - .as_ref() - .is_some_and(|snapshot| !snapshot.connectors.is_empty()); - skills_ready || connectors_ready + pub fn skills(&self) -> Option<&Vec> { + self.skills.as_ref() } /// Extract a token prefixed with `prefix` under the cursor, if any. @@ -1512,8 +1485,8 @@ impl ChatComposer { Self::current_prefixed_token(textarea, '@', false) } - fn current_mention_token(&self) -> Option { - if !self.mentions_enabled() { + fn current_skill_token(&self) -> Option { + if !self.skills_enabled() { return None; } Self::current_prefixed_token(&self.textarea, '$', true) @@ -1571,7 +1544,7 @@ impl ChatComposer { self.textarea.set_cursor(new_cursor); } - fn insert_selected_mention(&mut self, insert_text: &str) { + fn insert_selected_skill(&mut self, skill_name: &str) { let cursor_offset = self.textarea.cursor(); let text = self.textarea.text(); let safe_cursor = Self::clamp_to_char_boundary(text, cursor_offset); @@ -1592,7 +1565,7 @@ impl ChatComposer { .unwrap_or(after_cursor.len()); let end_idx = safe_cursor + end_rel_idx; - let inserted = insert_text.to_string(); + let inserted = format!("${skill_name}"); let mut new_text = String::with_capacity(text.len() - (end_idx - start_idx) + inserted.len() + 1); @@ -1640,11 +1613,9 @@ impl ChatComposer { if let Some((name, _rest, _rest_offset)) = parse_slash_name(&text) { let treat_as_plain_text = input_starts_with_space || name.contains('/'); if !treat_as_plain_text { - let is_builtin = Self::built_in_slash_commands_for_input( - self.collaboration_modes_enabled, - self.connectors_enabled, - ) - .any(|(command_name, _)| command_name == name); + let is_builtin = + Self::built_in_slash_commands_for_input(self.collaboration_modes_enabled) + .any(|(command_name, _)| command_name == name); let prompt_prefix = format!("{PROMPTS_CMD_PREFIX}:"); let is_known_prompt = name .strip_prefix(&prompt_prefix) @@ -1809,11 +1780,9 @@ impl ChatComposer { let first_line = self.textarea.text().lines().next().unwrap_or(""); if let Some((name, rest, _rest_offset)) = parse_slash_name(first_line) && rest.is_empty() - && let Some((_n, cmd)) = Self::built_in_slash_commands_for_input( - self.collaboration_modes_enabled, - self.connectors_enabled, - ) - .find(|(n, _)| *n == name) + && let Some((_n, cmd)) = + Self::built_in_slash_commands_for_input(self.collaboration_modes_enabled) + .find(|(n, _)| *n == name) { self.textarea.set_text_clearing_elements(""); Some(InputResult::Command(cmd)) @@ -1833,11 +1802,9 @@ impl ChatComposer { if let Some((name, rest, _rest_offset)) = parse_slash_name(&text) && !rest.is_empty() && !name.contains('/') - && let Some((_n, cmd)) = Self::built_in_slash_commands_for_input( - self.collaboration_modes_enabled, - self.connectors_enabled, - ) - .find(|(command_name, _)| *command_name == name) + && let Some((_n, cmd)) = + Self::built_in_slash_commands_for_input(self.collaboration_modes_enabled) + .find(|(command_name, _)| *command_name == name) && cmd == SlashCommand::Review { self.textarea.set_text_clearing_elements(""); @@ -2233,22 +2200,22 @@ impl ChatComposer { self.active_popup = ActivePopup::None; return; } - let mention_token = self.current_mention_token(); + let skill_token = self.current_skill_token(); - let allow_command_popup = file_token.is_none() && mention_token.is_none(); + let allow_command_popup = file_token.is_none() && skill_token.is_none(); self.sync_command_popup(allow_command_popup); if matches!(self.active_popup, ActivePopup::Command(_)) { self.dismissed_file_popup_token = None; - self.dismissed_mention_popup_token = None; + self.dismissed_skill_popup_token = None; return; } - if let Some(token) = mention_token { - self.sync_mention_popup(token); + if let Some(token) = skill_token { + self.sync_skill_popup(token); return; } - self.dismissed_mention_popup_token = None; + self.dismissed_skill_popup_token = None; if let Some(token) = file_token { self.sync_file_search_popup(token); @@ -2300,11 +2267,9 @@ impl ChatComposer { return rest_after_name.is_empty(); } - let builtin_match = Self::built_in_slash_commands_for_input( - self.collaboration_modes_enabled, - self.connectors_enabled, - ) - .any(|(cmd_name, _)| fuzzy_match(cmd_name, name).is_some()); + let builtin_match = + Self::built_in_slash_commands_for_input(self.collaboration_modes_enabled) + .any(|(cmd_name, _)| fuzzy_match(cmd_name, name).is_some()); if builtin_match { return true; @@ -2357,12 +2322,10 @@ impl ChatComposer { _ => { if is_editing_slash_command_name { let collaboration_modes_enabled = self.collaboration_modes_enabled; - let connectors_enabled = self.connectors_enabled; let mut command_popup = CommandPopup::new( self.custom_prompts.clone(), CommandPopupFlags { collaboration_modes_enabled, - connectors_enabled, }, ); command_popup.on_composer_text_change(first_line.to_string()); @@ -2374,14 +2337,12 @@ impl ChatComposer { fn built_in_slash_commands_for_input( collaboration_modes_enabled: bool, - connectors_enabled: bool, ) -> impl Iterator { let allow_elevate_sandbox = windows_degraded_sandbox_active(); built_in_slash_commands() .into_iter() .filter(move |(_, cmd)| allow_elevate_sandbox || *cmd != SlashCommand::ElevateSandbox) .filter(move |(_, cmd)| collaboration_modes_enabled || *cmd != SlashCommand::Collab) - .filter(move |(_, cmd)| connectors_enabled || *cmd != SlashCommand::Connectors) } pub(crate) fn set_custom_prompts(&mut self, prompts: Vec) { @@ -2427,95 +2388,32 @@ impl ChatComposer { self.dismissed_file_popup_token = None; } - fn sync_mention_popup(&mut self, query: String) { - if self.dismissed_mention_popup_token.as_ref() == Some(&query) { + fn sync_skill_popup(&mut self, query: String) { + if self.dismissed_skill_popup_token.as_ref() == Some(&query) { return; } - let mentions = self.mention_items(); - if mentions.is_empty() { - self.active_popup = ActivePopup::None; - return; - } + let skills = match self.skills.as_ref() { + Some(skills) if !skills.is_empty() => skills.clone(), + _ => { + self.active_popup = ActivePopup::None; + return; + } + }; match &mut self.active_popup { ActivePopup::Skill(popup) => { popup.set_query(&query); - popup.set_mentions(mentions); + popup.set_skills(skills); } _ => { - let mut popup = SkillPopup::new(mentions); + let mut popup = SkillPopup::new(skills); popup.set_query(&query); self.active_popup = ActivePopup::Skill(popup); } } } - fn mention_items(&self) -> Vec { - let mut mentions = Vec::new(); - - if let Some(skills) = self.skills.as_ref() { - for skill in skills { - let display_name = skill_display_name(skill).to_string(); - let description = skill_description(skill); - let skill_name = skill.name.clone(); - let search_terms = if display_name == skill.name { - vec![skill_name.clone()] - } else { - vec![skill_name.clone(), display_name.clone()] - }; - mentions.push(MentionItem { - display_name, - description, - insert_text: format!("${skill_name}"), - search_terms, - }); - } - } - - if self.connectors_enabled - && let Some(snapshot) = self.connectors_snapshot.as_ref() - { - for connector in &snapshot.connectors { - if !connector.is_accessible { - continue; - } - let display_name = connectors::connector_display_label(connector); - let description = Some(Self::connector_brief_description(connector)); - let search_terms = vec![display_name.clone(), connector.connector_id.clone()]; - mentions.push(MentionItem { - display_name: display_name.clone(), - description, - insert_text: display_name.clone(), - search_terms, - }); - } - } - - mentions - } - - fn connector_brief_description(connector: &ConnectorInfo) -> String { - let status_label = if connector.is_accessible { - "Connected" - } else { - "Can be installed" - }; - match Self::connector_description(connector) { - Some(description) => format!("{status_label} - {description}"), - None => status_label.to_string(), - } - } - - fn connector_description(connector: &ConnectorInfo) -> Option { - connector - .connector_description - .as_deref() - .map(str::trim) - .filter(|description| !description.is_empty()) - .map(str::to_string) - } - fn set_has_focus(&mut self, has_focus: bool) { self.has_focus = has_focus; } @@ -2554,25 +2452,6 @@ impl ChatComposer { } } -fn skill_display_name(skill: &SkillMetadata) -> &str { - skill - .interface - .as_ref() - .and_then(|interface| interface.display_name.as_deref()) - .unwrap_or(&skill.name) -} - -fn skill_description(skill: &SkillMetadata) -> Option { - let description = skill - .interface - .as_ref() - .and_then(|interface| interface.short_description.as_deref()) - .or(skill.short_description.as_deref()) - .unwrap_or(&skill.description); - let trimmed = description.trim(); - (!trimmed.is_empty()).then(|| trimmed.to_string()) -} - impl Renderable for ChatComposer { fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> { if !self.input_enabled { @@ -2741,7 +2620,6 @@ mod tests { use tempfile::tempdir; use crate::app_event::AppEvent; - use crate::bottom_pane::AppEventSender; use crate::bottom_pane::ChatComposer; use crate::bottom_pane::InputResult; diff --git a/codex-rs/tui/src/bottom_pane/command_popup.rs b/codex-rs/tui/src/bottom_pane/command_popup.rs index 775ca4fa10..f2c178dfa2 100644 --- a/codex-rs/tui/src/bottom_pane/command_popup.rs +++ b/codex-rs/tui/src/bottom_pane/command_popup.rs @@ -39,7 +39,6 @@ pub(crate) struct CommandPopup { #[derive(Clone, Copy, Debug, Default)] pub(crate) struct CommandPopupFlags { pub(crate) collaboration_modes_enabled: bool, - pub(crate) connectors_enabled: bool, } impl CommandPopup { @@ -49,7 +48,6 @@ impl CommandPopup { .into_iter() .filter(|(_, cmd)| allow_elevate_sandbox || *cmd != SlashCommand::ElevateSandbox) .filter(|(_, cmd)| flags.collaboration_modes_enabled || *cmd != SlashCommand::Collab) - .filter(|(_, cmd)| flags.connectors_enabled || *cmd != SlashCommand::Connectors) .collect(); // Exclude prompts that collide with builtin command names and sort by name. let exclude: HashSet = builtins.iter().map(|(n, _)| (*n).to_string()).collect(); @@ -468,7 +466,6 @@ mod tests { Vec::new(), CommandPopupFlags { collaboration_modes_enabled: true, - connectors_enabled: false, }, ); popup.on_composer_text_change("/collab".to_string()); diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index 6e880339c2..d8039b5d0a 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -15,7 +15,6 @@ //! hint. The pane schedules redraws so those hints can expire even when the UI is otherwise idle. use std::path::PathBuf; -use crate::app_event::ConnectorsSnapshot; use crate::app_event_sender::AppEventSender; use crate::bottom_pane::queued_user_messages::QueuedUserMessages; use crate::bottom_pane::unified_exec_footer::UnifiedExecFooter; @@ -38,7 +37,6 @@ use ratatui::layout::Rect; use ratatui::text::Line; use std::time::Duration; -mod app_link_view; mod approval_overlay; mod request_user_input; pub(crate) use approval_overlay::ApprovalOverlay; @@ -108,7 +106,6 @@ pub(crate) use chat_composer::InputResult; use codex_protocol::custom_prompts::CustomPrompt; use crate::status_indicator_widget::StatusIndicatorWidget; -pub(crate) use app_link_view::AppLinkView; pub(crate) use experimental_features_view::BetaFeatureItem; pub(crate) use experimental_features_view::ExperimentalFeaturesView; pub(crate) use list_selection_view::SelectionAction; @@ -201,11 +198,6 @@ impl BottomPane { self.request_redraw(); } - pub fn set_connectors_snapshot(&mut self, snapshot: Option) { - self.composer.set_connector_mentions(snapshot); - self.request_redraw(); - } - pub fn set_steer_enabled(&mut self, enabled: bool) { self.composer.set_steer_enabled(enabled); } @@ -215,11 +207,6 @@ impl BottomPane { self.request_redraw(); } - pub fn set_connectors_enabled(&mut self, enabled: bool) { - self.composer.set_connectors_enabled(enabled); - self.request_redraw(); - } - pub fn status_widget(&self) -> Option<&StatusIndicatorWidget> { self.status.as_ref() } diff --git a/codex-rs/tui/src/bottom_pane/skill_popup.rs b/codex-rs/tui/src/bottom_pane/skill_popup.rs index c8ad5c4c6b..44afb1ff0b 100644 --- a/codex-rs/tui/src/bottom_pane/skill_popup.rs +++ b/codex-rs/tui/src/bottom_pane/skill_popup.rs @@ -14,34 +14,30 @@ use super::selection_popup_common::render_rows_single_line; use crate::key_hint; use crate::render::Insets; use crate::render::RectExt; -use crate::text_formatting::truncate_text; -use codex_common::fuzzy_match::fuzzy_match; +use codex_core::skills::model::SkillMetadata; -#[derive(Clone, Debug)] -pub(crate) struct MentionItem { - pub(crate) display_name: String, - pub(crate) description: Option, - pub(crate) insert_text: String, - pub(crate) search_terms: Vec, -} +use crate::skills_helpers::match_skill; +use crate::skills_helpers::skill_description; +use crate::skills_helpers::skill_display_name; +use crate::skills_helpers::truncated_skill_display_name; pub(crate) struct SkillPopup { query: String, - mentions: Vec, + skills: Vec, state: ScrollState, } impl SkillPopup { - pub(crate) fn new(mentions: Vec) -> Self { + pub(crate) fn new(skills: Vec) -> Self { Self { query: String::new(), - mentions, + skills, state: ScrollState::new(), } } - pub(crate) fn set_mentions(&mut self, mentions: Vec) { - self.mentions = mentions; + pub(crate) fn set_skills(&mut self, skills: Vec) { + self.skills = skills; self.clamp_selection(); } @@ -68,11 +64,11 @@ impl SkillPopup { self.state.ensure_visible(len, MAX_POPUP_ROWS.min(len)); } - pub(crate) fn selected_mention(&self) -> Option<&MentionItem> { + pub(crate) fn selected_skill(&self) -> Option<&SkillMetadata> { let matches = self.filtered_items(); let idx = self.state.selected_idx?; - let mention_idx = matches.get(idx)?; - self.mentions.get(*mention_idx) + let skill_idx = matches.get(idx)?; + self.skills.get(*skill_idx) } fn clamp_selection(&mut self) { @@ -92,14 +88,14 @@ impl SkillPopup { matches .into_iter() .map(|(idx, indices, _score)| { - let mention = &self.mentions[idx]; - let name = truncate_text(&mention.display_name, 21); - let description = mention.description.clone().unwrap_or_default(); + let skill = &self.skills[idx]; + let name = truncated_skill_display_name(skill); + let description = skill_description(skill).to_string(); GenericDisplayRow { name, match_indices: indices, display_shortcut: None, - description: Some(description).filter(|desc| !desc.is_empty()), + description: Some(description), disabled_reason: None, wrap_indent: None, } @@ -112,48 +108,23 @@ impl SkillPopup { let mut out: Vec<(usize, Option>, i32)> = Vec::new(); if filter.is_empty() { - for (idx, _mention) in self.mentions.iter().enumerate() { + for (idx, _skill) in self.skills.iter().enumerate() { out.push((idx, None, 0)); } return out; } - for (idx, mention) in self.mentions.iter().enumerate() { - let mut best_match: Option<(Option>, i32)> = None; - - if let Some((indices, score)) = fuzzy_match(&mention.display_name, filter) { - best_match = Some((Some(indices), score)); - } - - for term in &mention.search_terms { - if term == &mention.display_name { - continue; - } - - if let Some((_indices, score)) = fuzzy_match(term, filter) { - match best_match.as_mut() { - Some((best_indices, best_score)) => { - if score > *best_score { - *best_score = score; - *best_indices = None; - } - } - None => { - best_match = Some((None, score)); - } - } - } - } - - if let Some((indices, score)) = best_match { + for (idx, skill) in self.skills.iter().enumerate() { + let display_name = skill_display_name(skill); + if let Some((indices, score)) = match_skill(filter, display_name, &skill.name) { out.push((idx, indices, score)); } } out.sort_by(|a, b| { a.2.cmp(&b.2).then_with(|| { - let an = self.mentions[a.0].display_name.as_str(); - let bn = self.mentions[b.0].display_name.as_str(); + let an = skill_display_name(&self.skills[a.0]); + let bn = skill_display_name(&self.skills[b.0]); an.cmp(bn) }) }); @@ -182,7 +153,7 @@ impl WidgetRef for SkillPopup { &rows, &self.state, MAX_POPUP_ROWS, - "no matches", + "no skills", ); if let Some(hint_area) = hint_area { let hint_area = Rect { @@ -200,7 +171,7 @@ fn skill_popup_hint_line() -> Line<'static> { Line::from(vec![ "Press ".into(), key_hint::plain(KeyCode::Enter).into(), - " to insert or ".into(), + " to select or ".into(), key_hint::plain(KeyCode::Esc).into(), " to close".into(), ]) diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index ab500a8ba5..ba894c3e97 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -31,7 +31,6 @@ use std::time::Instant; use crate::version::CODEX_CLI_VERSION; use codex_app_server_protocol::AuthMode; use codex_backend_client::Client as BackendClient; -use codex_chatgpt::connectors; use codex_core::config::Config; use codex_core::config::ConstraintResult; use codex_core::config::types::Notifications; @@ -121,7 +120,6 @@ use tracing::debug; const DEFAULT_MODEL_DISPLAY_NAME: &str = "loading"; use crate::app_event::AppEvent; -use crate::app_event::ConnectorsSnapshot; use crate::app_event::ExitMode; #[cfg(target_os = "windows")] use crate::app_event::WindowsSandboxEnableMode; @@ -374,15 +372,6 @@ enum RateLimitSwitchPromptState { Shown, } -#[derive(Debug, Clone, Default)] -enum ConnectorsCacheState { - #[default] - Uninitialized, - Loading, - Ready(ConnectorsSnapshot), - Failed(String), -} - #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub(crate) enum ExternalEditorState { #[default] @@ -457,7 +446,6 @@ pub(crate) struct ChatWidget { /// bottom pane is treated as "running" while this is populated, even if no agent turn is /// currently executing. mcp_startup_status: Option>, - connectors_cache: ConnectorsCacheState, // Queue of interruptive UI events deferred during an active write cycle interrupts: InterruptManager, // Accumulates the current reasoning block text to extract a header @@ -728,7 +716,6 @@ impl ChatWidget { self.bottom_pane .set_history_metadata(event.history_log_id, event.history_entry_count); self.set_skills(None); - self.bottom_pane.set_connectors_snapshot(None); self.thread_id = Some(event.session_id); self.forked_from = event.forked_from_id; self.current_rollout_path = Some(event.rollout_path.clone()); @@ -764,9 +751,6 @@ impl ChatWidget { cwds: Vec::new(), force_reload: true, }); - if self.connectors_enabled() { - self.prefetch_connectors(); - } if let Some(user_message) = self.initial_user_message.take() { self.submit_user_message(user_message); } @@ -802,12 +786,6 @@ impl ChatWidget { self.request_redraw(); } - pub(crate) fn open_app_link_view(&mut self, title: String, instructions: String, url: String) { - let view = crate::bottom_pane::AppLinkView::new(title, instructions, url); - self.bottom_pane.show_view(Box::new(view)); - self.request_redraw(); - } - pub(crate) fn open_feedback_consent(&mut self, category: crate::app_event::FeedbackCategory) { let params = crate::bottom_pane::feedback_upload_consent_params( self.app_event_tx.clone(), @@ -1928,7 +1906,6 @@ impl ChatWidget { unified_exec_processes: Vec::new(), agent_turn_running: false, mcp_startup_status: None, - connectors_cache: ConnectorsCacheState::default(), interrupts: InterruptManager::new(), reasoning_buffer: String::new(), full_reasoning_buffer: String::new(), @@ -1960,9 +1937,6 @@ impl ChatWidget { widget.bottom_pane.set_collaboration_modes_enabled( widget.config.features.enabled(Feature::CollaborationModes), ); - widget - .bottom_pane - .set_connectors_enabled(widget.config.features.enabled(Feature::Connectors)); widget } @@ -2051,7 +2025,6 @@ impl ChatWidget { unified_exec_processes: Vec::new(), agent_turn_running: false, mcp_startup_status: None, - connectors_cache: ConnectorsCacheState::default(), interrupts: InterruptManager::new(), reasoning_buffer: String::new(), full_reasoning_buffer: String::new(), @@ -2083,9 +2056,6 @@ impl ChatWidget { widget.bottom_pane.set_collaboration_modes_enabled( widget.config.features.enabled(Feature::CollaborationModes), ); - widget - .bottom_pane - .set_connectors_enabled(widget.config.features.enabled(Feature::Connectors)); widget } @@ -2418,9 +2388,6 @@ impl ChatWidget { SlashCommand::Mcp => { self.add_mcp_output(); } - SlashCommand::Connectors => { - self.add_connectors_output(); - } SlashCommand::Rollout => { if let Some(path) = self.rollout_path() { self.add_info_message( @@ -3014,28 +2981,6 @@ impl ChatWidget { } } - fn prefetch_connectors(&mut self) { - if !self.connectors_enabled() { - return; - } - if matches!(self.connectors_cache, ConnectorsCacheState::Loading) { - return; - } - - self.connectors_cache = ConnectorsCacheState::Loading; - let config = self.config.clone(); - let app_event_tx = self.app_event_tx.clone(); - tokio::spawn(async move { - let result: Result = async { - let connectors = connectors::list_connectors(&config).await?; - Ok(ConnectorsSnapshot { connectors }) - } - .await; - let result = result.map_err(|err| format!("Failed to load apps: {err}")); - app_event_tx.send(AppEvent::ConnectorsLoaded(result)); - }); - } - fn prefetch_rate_limits(&mut self) { self.stop_rate_limit_poller(); @@ -4358,19 +4303,6 @@ impl ChatWidget { CollaborationMode::Custom(settings) }; } - - if feature == Feature::Connectors { - self.bottom_pane.set_connectors_enabled(enabled); - if enabled { - self.prefetch_connectors(); - if let ConnectorsCacheState::Ready(snapshot) = &self.connectors_cache { - self.bottom_pane - .set_connectors_snapshot(Some(snapshot.clone())); - } - } else { - self.bottom_pane.set_connectors_snapshot(None); - } - } } pub(crate) fn set_full_access_warning_acknowledged(&mut self, acknowledged: bool) { @@ -4494,10 +4426,6 @@ impl ChatWidget { self.request_redraw(); } - fn connectors_enabled(&self) -> bool { - self.config.features.enabled(Feature::Connectors) - } - /// Build a placeholder header cell while the session is configuring. fn placeholder_session_header_cell( config: &Config, @@ -4567,135 +4495,6 @@ impl ChatWidget { } } - pub(crate) fn add_connectors_output(&mut self) { - if !self.connectors_enabled() { - self.add_info_message( - "Apps are disabled.".to_string(), - Some("Enable the connectors feature to use $ or /apps.".to_string()), - ); - return; - } - - if matches!(self.connectors_cache, ConnectorsCacheState::Uninitialized) { - self.prefetch_connectors(); - } - let connectors_snapshot = match &self.connectors_cache { - ConnectorsCacheState::Ready(snapshot) => Some(snapshot.clone()), - ConnectorsCacheState::Failed(err) => { - self.add_to_history(history_cell::new_error_event(err.clone())); - None - } - ConnectorsCacheState::Loading | ConnectorsCacheState::Uninitialized => { - self.add_to_history(history_cell::new_info_event( - "Apps are still loading.".to_string(), - Some("Try again in a moment.".to_string()), - )); - None - } - }; - if let Some(snapshot) = connectors_snapshot { - if snapshot.connectors.is_empty() { - self.add_info_message("No apps available.".to_string(), None); - } else { - self.open_connectors_popup(&snapshot.connectors); - } - } - self.request_redraw(); - } - - fn open_connectors_popup(&mut self, connectors: &[connectors::ConnectorInfo]) { - let mut items: Vec = Vec::with_capacity(connectors.len()); - for connector in connectors { - let connector_label = connectors::connector_display_label(connector); - let connector_title = connector_label.clone(); - let description = Self::connector_brief_description(connector); - let search_value = format!("{connector_label} {}", connector.connector_id); - let mut item = SelectionItem { - name: connector_label, - description: Some(description), - search_value: Some(search_value), - ..Default::default() - }; - let (selected_label, missing_label, instructions) = if connector.is_accessible { - ( - "Press Enter to view the app link.", - "App link unavailable.", - "Open this app's connector page in your browser.", - ) - } else { - ( - "Press Enter to view the install link.", - "Install link unavailable.", - "Install this app in your browser, then reload Codex.", - ) - }; - if let Some(install_url) = connector.install_url.clone() { - let title = connector_title.clone(); - let instructions = instructions.to_string(); - item.actions = vec![Box::new(move |tx| { - tx.send(AppEvent::OpenAppLink { - title: title.clone(), - instructions: instructions.clone(), - url: install_url.clone(), - }); - })]; - item.dismiss_on_select = true; - item.selected_description = Some(selected_label.to_string()); - } else { - item.actions = vec![Box::new(move |tx| { - tx.send(AppEvent::InsertHistoryCell(Box::new( - history_cell::new_info_event(missing_label.to_string(), None), - ))); - })]; - item.dismiss_on_select = true; - item.selected_description = Some(missing_label.to_string()); - } - items.push(item); - } - - self.bottom_pane.show_selection_view(SelectionViewParams { - title: Some("Apps".to_string()), - subtitle: Some( - "Use $ to insert an app in the prompt. Browse apps to view connector links." - .to_string(), - ), - footer_hint: Some(Self::connectors_popup_hint_line()), - items, - is_searchable: true, - search_placeholder: Some("Type to search apps".to_string()), - ..Default::default() - }); - } - - fn connectors_popup_hint_line() -> Line<'static> { - Line::from(vec![ - "Press ".into(), - key_hint::plain(KeyCode::Esc).into(), - " to close.".into(), - ]) - } - - fn connector_brief_description(connector: &connectors::ConnectorInfo) -> String { - let status_label = if connector.is_accessible { - "Connected" - } else { - "Can be installed" - }; - match Self::connector_description(connector) { - Some(description) => format!("{status_label} ยท {description}"), - None => status_label.to_string(), - } - } - - fn connector_description(connector: &connectors::ConnectorInfo) -> Option { - connector - .connector_description - .as_deref() - .map(str::trim) - .filter(|description| !description.is_empty()) - .map(str::to_string) - } - /// Forward file-search results to the bottom pane. pub(crate) fn apply_file_search_result(&mut self, query: String, matches: Vec) { self.bottom_pane.on_file_search_result(query, matches); @@ -4871,19 +4670,6 @@ impl ChatWidget { self.set_skills_from_response(&ev); } - pub(crate) fn on_connectors_loaded(&mut self, result: Result) { - self.connectors_cache = match result { - Ok(connectors) => ConnectorsCacheState::Ready(connectors), - Err(err) => ConnectorsCacheState::Failed(err), - }; - if let ConnectorsCacheState::Ready(snapshot) = &self.connectors_cache { - self.bottom_pane - .set_connectors_snapshot(Some(snapshot.clone())); - } else { - self.bottom_pane.set_connectors_snapshot(None); - } - } - pub(crate) fn open_review_popup(&mut self) { let mut items: Vec = Vec::new(); diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 2db7984e8a..d0d37f0a69 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -822,7 +822,6 @@ async fn make_chatwidget_manual( unified_exec_processes: Vec::new(), agent_turn_running: false, mcp_startup_status: None, - connectors_cache: ConnectorsCacheState::default(), interrupts: InterruptManager::new(), reasoning_buffer: String::new(), full_reasoning_buffer: String::new(), diff --git a/codex-rs/tui/src/skills_helpers.rs b/codex-rs/tui/src/skills_helpers.rs index 1a8c78bb10..8332be886f 100644 --- a/codex-rs/tui/src/skills_helpers.rs +++ b/codex-rs/tui/src/skills_helpers.rs @@ -26,6 +26,10 @@ pub(crate) fn truncate_skill_name(name: &str) -> String { truncate_text(name, SKILL_NAME_TRUNCATE_LEN) } +pub(crate) fn truncated_skill_display_name(skill: &SkillMetadata) -> String { + truncate_skill_name(skill_display_name(skill)) +} + pub(crate) fn match_skill( filter: &str, display_name: &str, diff --git a/codex-rs/tui/src/slash_command.rs b/codex-rs/tui/src/slash_command.rs index def31c991d..ab63d6c7c8 100644 --- a/codex-rs/tui/src/slash_command.rs +++ b/codex-rs/tui/src/slash_command.rs @@ -30,8 +30,6 @@ pub enum SlashCommand { Mention, Status, Mcp, - #[strum(serialize = "apps")] - Connectors, Logout, Quit, Exit, @@ -65,7 +63,6 @@ impl SlashCommand { SlashCommand::ElevateSandbox => "set up elevated agent sandbox", SlashCommand::Experimental => "toggle beta features", SlashCommand::Mcp => "list configured MCP tools", - SlashCommand::Connectors => "browse app connectors", SlashCommand::Logout => "log out of Codex", SlashCommand::Rollout => "print the rollout file path", SlashCommand::TestApproval => "test approval request", @@ -99,7 +96,6 @@ impl SlashCommand { | SlashCommand::Status | SlashCommand::Ps | SlashCommand::Mcp - | SlashCommand::Connectors | SlashCommand::Feedback | SlashCommand::Quit | SlashCommand::Exit => true,