mirror of
https://github.com/openai/codex.git
synced 2026-06-02 03:11:59 +00:00
Compare commits
1 Commits
dev/honor-
...
etraut/lis
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7cb7a9d87 |
@@ -40,7 +40,12 @@ use crate::update_action::UpdateAction;
|
||||
use crate::version::CODEX_CLI_VERSION;
|
||||
use codex_ansi_escape::ansi_escape_line;
|
||||
use codex_app_server_client::InProcessAppServerClient;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
use codex_app_server_protocol::ConfigLayerSource;
|
||||
use codex_app_server_protocol::RequestId as AppServerRequestId;
|
||||
use codex_app_server_protocol::SkillErrorInfo as AppServerSkillErrorInfo;
|
||||
use codex_app_server_protocol::SkillsListParams;
|
||||
use codex_app_server_protocol::SkillsListResponse;
|
||||
use codex_core::AuthManager;
|
||||
use codex_core::CodexAuth;
|
||||
use codex_core::ThreadManager;
|
||||
@@ -212,6 +217,42 @@ fn emit_skill_load_warnings(app_event_tx: &AppEventSender, errors: &[SkillErrorI
|
||||
}
|
||||
}
|
||||
|
||||
fn app_server_errors_for_cwd(
|
||||
cwd: &Path,
|
||||
response: &SkillsListResponse,
|
||||
) -> Vec<AppServerSkillErrorInfo> {
|
||||
response
|
||||
.data
|
||||
.iter()
|
||||
.find(|entry| entry.cwd.as_path() == cwd)
|
||||
.map(|entry| entry.errors.clone())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn emit_app_server_skill_load_warnings(
|
||||
app_event_tx: &AppEventSender,
|
||||
errors: &[AppServerSkillErrorInfo],
|
||||
) {
|
||||
if errors.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let error_count = errors.len();
|
||||
app_event_tx.send(AppEvent::InsertHistoryCell(Box::new(
|
||||
crate::history_cell::new_warning_event(format!(
|
||||
"Skipped loading {error_count} skill(s) due to invalid SKILL.md files."
|
||||
)),
|
||||
)));
|
||||
|
||||
for error in errors {
|
||||
let path = error.path.display();
|
||||
let message = error.message.as_str();
|
||||
app_event_tx.send(AppEvent::InsertHistoryCell(Box::new(
|
||||
crate::history_cell::new_warning_event(format!("{path}: {message}")),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_project_config_warnings(app_event_tx: &AppEventSender, config: &Config) {
|
||||
let mut disabled_folders = Vec::new();
|
||||
|
||||
@@ -698,6 +739,7 @@ pub(crate) struct App {
|
||||
pending_shutdown_exit_thread_id: Option<ThreadId>,
|
||||
|
||||
windows_sandbox: WindowsSandboxState,
|
||||
next_app_server_request_id: i64,
|
||||
|
||||
thread_event_channels: HashMap<ThreadId, ThreadEventChannel>,
|
||||
thread_event_listener_tasks: HashMap<ThreadId, JoinHandle<()>>,
|
||||
@@ -790,6 +832,39 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn next_app_server_request_id(&mut self) -> AppServerRequestId {
|
||||
let request_id = self.next_app_server_request_id;
|
||||
self.next_app_server_request_id = self.next_app_server_request_id.saturating_add(1);
|
||||
AppServerRequestId::Integer(request_id)
|
||||
}
|
||||
|
||||
async fn refresh_skills(&mut self, app_server: &InProcessAppServerClient, force_reload: bool) {
|
||||
let response = app_server
|
||||
.request_typed::<SkillsListResponse>(ClientRequest::SkillsList {
|
||||
request_id: self.next_app_server_request_id(),
|
||||
params: SkillsListParams {
|
||||
cwds: Vec::new(),
|
||||
force_reload,
|
||||
per_cwd_extra_user_roots: None,
|
||||
},
|
||||
})
|
||||
.await;
|
||||
|
||||
match response {
|
||||
Ok(response) => {
|
||||
let cwd = self.chat_widget.config_ref().cwd.clone();
|
||||
let errors = app_server_errors_for_cwd(&cwd, &response);
|
||||
emit_app_server_skill_load_warnings(&self.app_event_tx, &errors);
|
||||
self.chat_widget
|
||||
.set_skills_from_app_server_response(&response);
|
||||
self.chat_widget.refresh_plugin_mentions();
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!(error = %err, "failed to refresh skills from embedded app server");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn rebuild_config_for_resume_or_fallback(
|
||||
&mut self,
|
||||
current_cwd: &Path,
|
||||
@@ -1943,6 +2018,7 @@ impl App {
|
||||
suppress_shutdown_complete: false,
|
||||
pending_shutdown_exit_thread_id: None,
|
||||
windows_sandbox: WindowsSandboxState::default(),
|
||||
next_app_server_request_id: 1,
|
||||
thread_event_channels: HashMap::new(),
|
||||
thread_event_listener_tasks: HashMap::new(),
|
||||
agent_navigation: AgentNavigationState::default(),
|
||||
@@ -2031,7 +2107,7 @@ impl App {
|
||||
app.active_thread_rx.is_some()
|
||||
) => {
|
||||
if let Some(event) = active {
|
||||
if let Err(err) = app.handle_active_thread_event(tui, event).await {
|
||||
if let Err(err) = app.handle_active_thread_event(tui, event, &app_server).await {
|
||||
break Err(err);
|
||||
}
|
||||
} else {
|
||||
@@ -3491,6 +3567,18 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_codex_event_now_with_app_server(
|
||||
&mut self,
|
||||
event: Event,
|
||||
app_server: &InProcessAppServerClient,
|
||||
) {
|
||||
let should_refresh_skills = matches!(event.msg, EventMsg::SessionConfigured(_));
|
||||
self.handle_codex_event_now(event);
|
||||
if should_refresh_skills {
|
||||
self.refresh_skills(app_server, true).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_codex_event_replay(&mut self, event: Event) {
|
||||
self.chat_widget.handle_codex_event_replay(event);
|
||||
}
|
||||
@@ -3500,7 +3588,12 @@ impl App {
|
||||
/// This function enforces shutdown intent routing: unexpected non-primary
|
||||
/// thread shutdowns fail over to the primary thread, while user-requested
|
||||
/// app exits consume only the tracked shutdown completion and then proceed.
|
||||
async fn handle_active_thread_event(&mut self, tui: &mut tui::Tui, event: Event) -> Result<()> {
|
||||
async fn handle_active_thread_event(
|
||||
&mut self,
|
||||
tui: &mut tui::Tui,
|
||||
event: Event,
|
||||
app_server: &InProcessAppServerClient,
|
||||
) -> Result<()> {
|
||||
// Capture this before any potential thread switch: we only want to clear
|
||||
// the exit marker when the currently active thread acknowledges shutdown.
|
||||
let pending_shutdown_exit_completed = matches!(&event.msg, EventMsg::ShutdownComplete)
|
||||
@@ -3540,7 +3633,8 @@ impl App {
|
||||
// thread, so unrelated shutdowns cannot consume this marker.
|
||||
self.pending_shutdown_exit_thread_id = None;
|
||||
}
|
||||
self.handle_codex_event_now(event);
|
||||
self.handle_codex_event_now_with_app_server(event, app_server)
|
||||
.await;
|
||||
if self.backtrack_render_pending {
|
||||
tui.frame_requester().schedule_frame();
|
||||
}
|
||||
@@ -3915,10 +4009,15 @@ mod tests {
|
||||
use crate::history_cell::new_session_info;
|
||||
use crate::multi_agents::AgentPickerThreadEntry;
|
||||
use assert_matches::assert_matches;
|
||||
use codex_app_server_client::DEFAULT_IN_PROCESS_CHANNEL_CAPACITY;
|
||||
use codex_app_server_client::InProcessClientStartArgs;
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_core::CodexAuth;
|
||||
use codex_core::config::ConfigBuilder;
|
||||
use codex_core::config::ConfigOverrides;
|
||||
use codex_core::config::types::ModelAvailabilityNuxConfig;
|
||||
use codex_core::config_loader::CloudRequirementsLoader;
|
||||
use codex_core::config_loader::LoaderOverrides;
|
||||
use codex_otel::SessionTelemetry;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
@@ -3948,6 +4047,7 @@ mod tests {
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use tempfile::TempDir;
|
||||
use tempfile::tempdir;
|
||||
use tokio::time;
|
||||
|
||||
@@ -5647,6 +5747,7 @@ mod tests {
|
||||
suppress_shutdown_complete: false,
|
||||
pending_shutdown_exit_thread_id: None,
|
||||
windows_sandbox: WindowsSandboxState::default(),
|
||||
next_app_server_request_id: 1,
|
||||
thread_event_channels: HashMap::new(),
|
||||
thread_event_listener_tasks: HashMap::new(),
|
||||
agent_navigation: AgentNavigationState::default(),
|
||||
@@ -5707,6 +5808,7 @@ mod tests {
|
||||
suppress_shutdown_complete: false,
|
||||
pending_shutdown_exit_thread_id: None,
|
||||
windows_sandbox: WindowsSandboxState::default(),
|
||||
next_app_server_request_id: 1,
|
||||
thread_event_channels: HashMap::new(),
|
||||
thread_event_listener_tasks: HashMap::new(),
|
||||
agent_navigation: AgentNavigationState::default(),
|
||||
@@ -5721,6 +5823,34 @@ mod tests {
|
||||
)
|
||||
}
|
||||
|
||||
fn write_skill(root: &TempDir, name: &str, description: &str) {
|
||||
let skill_dir = root.path().join("skills").join(name);
|
||||
std::fs::create_dir_all(&skill_dir).expect("skill dir");
|
||||
let content = format!("---\nname: {name}\ndescription: {description}\n---\n\n# {name}\n");
|
||||
std::fs::write(skill_dir.join("SKILL.md"), content).expect("skill file");
|
||||
}
|
||||
|
||||
async fn start_test_embedded_app_server(config: Config) -> InProcessAppServerClient {
|
||||
InProcessAppServerClient::start(InProcessClientStartArgs {
|
||||
arg0_paths: Arg0DispatchPaths::default(),
|
||||
config: Arc::new(config),
|
||||
cli_overrides: Vec::new(),
|
||||
loader_overrides: LoaderOverrides::default(),
|
||||
cloud_requirements: CloudRequirementsLoader::default(),
|
||||
feedback: codex_feedback::CodexFeedback::new(),
|
||||
config_warnings: Vec::new(),
|
||||
session_source: SessionSource::Cli,
|
||||
enable_codex_api_key_env: false,
|
||||
client_name: "codex-tui-test".to_string(),
|
||||
client_version: "0.0.0-test".to_string(),
|
||||
experimental_api: true,
|
||||
opt_out_notification_methods: Vec::new(),
|
||||
channel_capacity: DEFAULT_IN_PROCESS_CHANNEL_CAPACITY,
|
||||
})
|
||||
.await
|
||||
.expect("embedded app server should start")
|
||||
}
|
||||
|
||||
fn next_user_turn_op(op_rx: &mut tokio::sync::mpsc::UnboundedReceiver<Op>) -> Op {
|
||||
let mut seen = Vec::new();
|
||||
while let Ok(op) = op_rx.try_recv() {
|
||||
@@ -5732,6 +5862,105 @@ mod tests {
|
||||
panic!("expected UserTurn op, saw: {seen:?}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_configured_fetches_skills_via_embedded_app_server() {
|
||||
let skills_home = TempDir::new().expect("temp dir");
|
||||
write_skill(&skills_home, "repo-scout", "Scan the repo");
|
||||
|
||||
let workspace = skills_home.path().join("workspace");
|
||||
std::fs::create_dir_all(&workspace).expect("workspace dir");
|
||||
|
||||
let mut app_server_config = ConfigBuilder::default()
|
||||
.codex_home(skills_home.path().to_path_buf())
|
||||
.build()
|
||||
.await
|
||||
.expect("config");
|
||||
app_server_config.cwd = workspace.clone();
|
||||
let app_server = start_test_embedded_app_server(app_server_config).await;
|
||||
|
||||
let (mut app, _app_event_rx, mut op_rx) = make_test_app_with_channels().await;
|
||||
app.handle_codex_event_now_with_app_server(
|
||||
Event {
|
||||
id: "session".to_string(),
|
||||
msg: EventMsg::SessionConfigured(SessionConfiguredEvent {
|
||||
session_id: ThreadId::new(),
|
||||
forked_from_id: None,
|
||||
thread_name: None,
|
||||
model: "test-model".to_string(),
|
||||
model_provider_id: "test-provider".to_string(),
|
||||
service_tier: None,
|
||||
approval_policy: AskForApproval::Never,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
cwd: workspace,
|
||||
reasoning_effort: None,
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
initial_messages: None,
|
||||
network_proxy: None,
|
||||
rollout_path: None,
|
||||
}),
|
||||
},
|
||||
&app_server,
|
||||
)
|
||||
.await;
|
||||
|
||||
let skills = app.chat_widget.skills().expect("skills should be present");
|
||||
assert!(skills.iter().any(|skill| skill.name == "repo-scout"));
|
||||
assert_eq!(op_rx.try_recv(), Ok(Op::ListCustomPrompts));
|
||||
assert!(
|
||||
op_rx.try_recv().is_err(),
|
||||
"session configuration should not submit a legacy ListSkills op"
|
||||
);
|
||||
|
||||
app_server
|
||||
.shutdown()
|
||||
.await
|
||||
.expect("app server shutdown should succeed");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn app_server_skill_warnings_match_legacy_warning_copy() {
|
||||
let (app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await;
|
||||
let response = SkillsListResponse {
|
||||
data: vec![codex_app_server_protocol::SkillsListEntry {
|
||||
cwd: PathBuf::from("/tmp/project"),
|
||||
skills: Vec::new(),
|
||||
errors: vec![AppServerSkillErrorInfo {
|
||||
path: PathBuf::from("/tmp/project/broken/SKILL.md"),
|
||||
message: "missing description".to_string(),
|
||||
}],
|
||||
}],
|
||||
};
|
||||
|
||||
let errors = app_server_errors_for_cwd(Path::new("/tmp/project"), &response);
|
||||
emit_app_server_skill_load_warnings(&app.app_event_tx, &errors);
|
||||
|
||||
let summary = match app_event_rx.try_recv() {
|
||||
Ok(AppEvent::InsertHistoryCell(cell)) => cell,
|
||||
other => panic!("expected summary warning cell, got {other:?}"),
|
||||
};
|
||||
let summary_text = summary
|
||||
.display_lines(120)
|
||||
.into_iter()
|
||||
.map(|line| line.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
assert!(summary_text.contains("Skipped loading 1 skill(s)"));
|
||||
|
||||
let detail = match app_event_rx.try_recv() {
|
||||
Ok(AppEvent::InsertHistoryCell(cell)) => cell,
|
||||
other => panic!("expected detail warning cell, got {other:?}"),
|
||||
};
|
||||
let detail_text = detail
|
||||
.display_lines(120)
|
||||
.into_iter()
|
||||
.map(|line| line.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
assert!(detail_text.contains("/tmp/project/broken/SKILL.md"));
|
||||
assert!(detail_text.contains("missing description"));
|
||||
}
|
||||
|
||||
fn test_session_telemetry(config: &Config, model: &str) -> SessionTelemetry {
|
||||
let model_info = codex_core::test_support::construct_model_info_offline(model, config);
|
||||
SessionTelemetry::new(
|
||||
|
||||
@@ -213,10 +213,10 @@ use crate::clipboard_paste::pasted_image_format;
|
||||
use crate::history_cell;
|
||||
use crate::tui::FrameRequester;
|
||||
use crate::ui_consts::LIVE_PREFIX_COLS;
|
||||
use codex_app_server_protocol::SkillMetadata;
|
||||
use codex_chatgpt::connectors;
|
||||
use codex_chatgpt::connectors::AppInfo;
|
||||
use codex_core::plugins::PluginCapabilitySummary;
|
||||
use codex_core::skills::model::SkillMetadata;
|
||||
use codex_file_search::FileMatch;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
@@ -3584,7 +3584,7 @@ impl ChatComposer {
|
||||
description,
|
||||
insert_text: format!("${skill_name}"),
|
||||
search_terms,
|
||||
path: Some(skill.path_to_skills_md.to_string_lossy().into_owned()),
|
||||
path: Some(skill.path.to_string_lossy().into_owned()),
|
||||
category_tag: Some("[Skill]".to_string()),
|
||||
sort_rank: 1,
|
||||
});
|
||||
@@ -4494,6 +4494,8 @@ impl Drop for ChatComposer {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_app_server_protocol::SkillInterface;
|
||||
use codex_app_server_protocol::SkillScope;
|
||||
use image::ImageBuffer;
|
||||
use image::Rgba;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -5355,7 +5357,7 @@ mod tests {
|
||||
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 {
|
||||
interface: Some(SkillInterface {
|
||||
display_name: Some("Google Calendar".to_string()),
|
||||
short_description: None,
|
||||
icon_small: None,
|
||||
@@ -5364,11 +5366,9 @@ mod tests {
|
||||
default_prompt: None,
|
||||
}),
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
managed_network_override: None,
|
||||
path_to_skills_md: PathBuf::from("/tmp/repo/google-calendar/SKILL.md"),
|
||||
scope: codex_protocol::protocol::SkillScope::Repo,
|
||||
path: PathBuf::from("/tmp/repo/google-calendar/SKILL.md"),
|
||||
scope: SkillScope::Repo,
|
||||
enabled: true,
|
||||
}]));
|
||||
composer.set_plugin_mentions(Some(vec![PluginCapabilitySummary {
|
||||
config_name: "google-calendar@debug".to_string(),
|
||||
|
||||
@@ -27,9 +27,9 @@ use crate::render::renderable::Renderable;
|
||||
use crate::render::renderable::RenderableItem;
|
||||
use crate::tui::FrameRequester;
|
||||
use bottom_pane_view::BottomPaneView;
|
||||
use codex_app_server_protocol::SkillMetadata;
|
||||
use codex_core::features::Features;
|
||||
use codex_core::plugins::PluginCapabilitySummary;
|
||||
use codex_core::skills::model::SkillMetadata;
|
||||
use codex_file_search::FileMatch;
|
||||
use codex_protocol::request_user_input::RequestUserInputEvent;
|
||||
use codex_protocol::user_input::TextElement;
|
||||
@@ -1234,8 +1234,8 @@ mod tests {
|
||||
use crate::app_event::AppEvent;
|
||||
use crate::status_indicator_widget::STATUS_DETAILS_DEFAULT_MAX_LINES;
|
||||
use crate::status_indicator_widget::StatusDetailsCapitalization;
|
||||
use codex_app_server_protocol::SkillScope;
|
||||
use codex_protocol::protocol::Op;
|
||||
use codex_protocol::protocol::SkillScope;
|
||||
use crossterm::event::KeyEventKind;
|
||||
use crossterm::event::KeyModifiers;
|
||||
use insta::assert_snapshot;
|
||||
@@ -1673,11 +1673,9 @@ mod tests {
|
||||
short_description: None,
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
managed_network_override: None,
|
||||
path_to_skills_md: PathBuf::from("test-skill"),
|
||||
path: PathBuf::from("test-skill"),
|
||||
scope: SkillScope::User,
|
||||
enabled: true,
|
||||
}]),
|
||||
});
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ use crate::status::rate_limit_snapshot_display_for_limit;
|
||||
use crate::text_formatting::proper_join;
|
||||
use crate::version::CODEX_CLI_VERSION;
|
||||
use codex_app_server_protocol::ConfigLayerSource;
|
||||
use codex_app_server_protocol::SkillMetadata as AppServerSkillMetadata;
|
||||
use codex_backend_client::Client as BackendClient;
|
||||
use codex_chatgpt::connectors;
|
||||
use codex_core::config::Config;
|
||||
@@ -69,7 +70,6 @@ use codex_core::mcp::McpManager;
|
||||
use codex_core::models_manager::manager::ModelsManager;
|
||||
use codex_core::plugins::PluginsManager;
|
||||
use codex_core::project_doc::DEFAULT_PROJECT_DOC_FILENAME;
|
||||
use codex_core::skills::model::SkillMetadata;
|
||||
use codex_core::terminal::TerminalName;
|
||||
use codex_core::terminal::terminal_info;
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -128,7 +128,6 @@ use codex_protocol::protocol::PatchApplyBeginEvent;
|
||||
use codex_protocol::protocol::RateLimitSnapshot;
|
||||
use codex_protocol::protocol::ReviewRequest;
|
||||
use codex_protocol::protocol::ReviewTarget;
|
||||
use codex_protocol::protocol::SkillMetadata as ProtocolSkillMetadata;
|
||||
use codex_protocol::protocol::StreamErrorEvent;
|
||||
use codex_protocol::protocol::TerminalInteractionEvent;
|
||||
use codex_protocol::protocol::TokenUsage;
|
||||
@@ -582,7 +581,7 @@ pub(crate) struct ChatWidget {
|
||||
running_commands: HashMap<String, RunningCommand>,
|
||||
pending_collab_spawn_requests: HashMap<String, multi_agents::SpawnRequestSummary>,
|
||||
suppressed_exec_calls: HashSet<String>,
|
||||
skills_all: Vec<ProtocolSkillMetadata>,
|
||||
skills_all: Vec<AppServerSkillMetadata>,
|
||||
skills_initial_state: Option<HashMap<PathBuf, bool>>,
|
||||
last_unified_wait: Option<UnifiedExecWaitState>,
|
||||
unified_exec_wait_streak: Option<UnifiedExecWaitStreak>,
|
||||
@@ -1301,10 +1300,6 @@ impl ChatWidget {
|
||||
}
|
||||
// Ask codex-core to enumerate custom prompts for this session.
|
||||
self.submit_op(Op::ListCustomPrompts);
|
||||
self.submit_op(Op::ListSkills {
|
||||
cwds: Vec::new(),
|
||||
force_reload: true,
|
||||
});
|
||||
if self.connectors_enabled() {
|
||||
self.prefetch_connectors();
|
||||
}
|
||||
@@ -1370,7 +1365,7 @@ impl ChatWidget {
|
||||
}
|
||||
}
|
||||
|
||||
fn set_skills(&mut self, skills: Option<Vec<SkillMetadata>>) {
|
||||
fn set_skills(&mut self, skills: Option<Vec<AppServerSkillMetadata>>) {
|
||||
self.bottom_pane.set_skills(skills);
|
||||
}
|
||||
|
||||
@@ -4594,14 +4589,12 @@ impl ChatWidget {
|
||||
.strip_prefix("skill://")
|
||||
.unwrap_or(binding.path.as_str());
|
||||
let path = Path::new(path);
|
||||
if let Some(skill) = skills
|
||||
.iter()
|
||||
.find(|skill| skill.path_to_skills_md.as_path() == path)
|
||||
&& selected_skill_paths.insert(skill.path_to_skills_md.clone())
|
||||
if let Some(skill) = skills.iter().find(|skill| skill.path.as_path() == path)
|
||||
&& selected_skill_paths.insert(skill.path.clone())
|
||||
{
|
||||
items.push(UserInput::Skill {
|
||||
name: skill.name.clone(),
|
||||
path: skill.path_to_skills_md.clone(),
|
||||
path: skill.path.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -4609,13 +4602,13 @@ impl ChatWidget {
|
||||
let skill_mentions = find_skill_mentions_with_tool_mentions(&mentions, skills);
|
||||
for skill in skill_mentions {
|
||||
if bound_names.contains(skill.name.as_str())
|
||||
|| !selected_skill_paths.insert(skill.path_to_skills_md.clone())
|
||||
|| !selected_skill_paths.insert(skill.path.clone())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
items.push(UserInput::Skill {
|
||||
name: skill.name.clone(),
|
||||
path: skill.path_to_skills_md.clone(),
|
||||
path: skill.path.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -8329,6 +8322,11 @@ impl ChatWidget {
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn skills(&self) -> Option<&Vec<AppServerSkillMetadata>> {
|
||||
self.bottom_pane.skills()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn pending_thread_approvals(&self) -> &[String] {
|
||||
self.bottom_pane.pending_thread_approvals()
|
||||
@@ -8476,7 +8474,7 @@ impl ChatWidget {
|
||||
self.bottom_pane.set_connectors_snapshot(Some(snapshot));
|
||||
}
|
||||
|
||||
fn refresh_plugin_mentions(&mut self) {
|
||||
pub(crate) fn refresh_plugin_mentions(&mut self) {
|
||||
if !self.config.features.enabled(Feature::Plugins) {
|
||||
self.bottom_pane.set_plugin_mentions(None);
|
||||
return;
|
||||
|
||||
@@ -12,16 +12,20 @@ use crate::bottom_pane::SkillsToggleView;
|
||||
use crate::bottom_pane::popup_consts::standard_popup_hint_line;
|
||||
use crate::skills_helpers::skill_description;
|
||||
use crate::skills_helpers::skill_display_name;
|
||||
use codex_app_server_protocol::SkillDependencies as AppServerSkillDependencies;
|
||||
use codex_app_server_protocol::SkillInterface as AppServerSkillInterface;
|
||||
use codex_app_server_protocol::SkillMetadata as AppServerSkillMetadata;
|
||||
use codex_app_server_protocol::SkillScope as AppServerSkillScope;
|
||||
use codex_app_server_protocol::SkillToolDependency as AppServerSkillToolDependency;
|
||||
use codex_app_server_protocol::SkillsListEntry as AppServerSkillsListEntry;
|
||||
use codex_app_server_protocol::SkillsListResponse;
|
||||
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;
|
||||
use codex_core::skills::model::SkillToolDependency;
|
||||
use codex_protocol::protocol::ListSkillsResponseEvent;
|
||||
use codex_protocol::protocol::SkillMetadata as ProtocolSkillMetadata;
|
||||
use codex_protocol::protocol::SkillsListEntry;
|
||||
use codex_protocol::protocol::SkillMetadata as LegacySkillMetadata;
|
||||
use codex_protocol::protocol::SkillScope as LegacySkillScope;
|
||||
use codex_protocol::protocol::SkillsListEntry as LegacySkillsListEntry;
|
||||
|
||||
impl ChatWidget {
|
||||
pub(crate) fn open_skills_list(&mut self) {
|
||||
@@ -75,11 +79,10 @@ impl ChatWidget {
|
||||
.skills_all
|
||||
.iter()
|
||||
.map(|skill| {
|
||||
let core_skill = protocol_skill_to_core(skill);
|
||||
let display_name = skill_display_name(&core_skill).to_string();
|
||||
let description = skill_description(&core_skill).to_string();
|
||||
let name = core_skill.name.clone();
|
||||
let path = core_skill.path_to_skills_md;
|
||||
let display_name = skill_display_name(skill).to_string();
|
||||
let description = skill_description(skill).to_string();
|
||||
let name = skill.name.clone();
|
||||
let path = skill.path.clone();
|
||||
SkillsToggleItem {
|
||||
name: display_name,
|
||||
skill_name: name,
|
||||
@@ -101,7 +104,13 @@ impl ChatWidget {
|
||||
skill.enabled = enabled;
|
||||
}
|
||||
}
|
||||
self.set_skills(Some(enabled_skills_for_mentions(&self.skills_all)));
|
||||
let enabled_skills = self
|
||||
.skills_all
|
||||
.iter()
|
||||
.filter(|skill| skill.enabled)
|
||||
.cloned()
|
||||
.collect();
|
||||
self.set_skills(Some(enabled_skills));
|
||||
}
|
||||
|
||||
pub(crate) fn handle_manage_skills_closed(&mut self) {
|
||||
@@ -138,13 +147,34 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
pub(crate) fn set_skills_from_response(&mut self, response: &ListSkillsResponseEvent) {
|
||||
let skills = skills_for_cwd(&self.config.cwd, &response.skills);
|
||||
let skills = legacy_skills_for_cwd(&self.config.cwd, &response.skills)
|
||||
.iter()
|
||||
.map(legacy_skill_to_app_server)
|
||||
.collect();
|
||||
self.set_skills_from_listing(skills);
|
||||
}
|
||||
|
||||
pub(crate) fn set_skills_from_app_server_response(&mut self, response: &SkillsListResponse) {
|
||||
let skills = app_server_skills_for_cwd(&self.config.cwd, &response.data);
|
||||
self.set_skills_from_listing(skills);
|
||||
}
|
||||
|
||||
fn set_skills_from_listing(&mut self, skills: Vec<AppServerSkillMetadata>) {
|
||||
self.skills_all = skills;
|
||||
self.set_skills(Some(enabled_skills_for_mentions(&self.skills_all)));
|
||||
let enabled_skills = self
|
||||
.skills_all
|
||||
.iter()
|
||||
.filter(|skill| skill.enabled)
|
||||
.cloned()
|
||||
.collect();
|
||||
self.set_skills(Some(enabled_skills));
|
||||
}
|
||||
}
|
||||
|
||||
fn skills_for_cwd(cwd: &Path, skills_entries: &[SkillsListEntry]) -> Vec<ProtocolSkillMetadata> {
|
||||
fn app_server_skills_for_cwd(
|
||||
cwd: &Path,
|
||||
skills_entries: &[AppServerSkillsListEntry],
|
||||
) -> Vec<AppServerSkillMetadata> {
|
||||
skills_entries
|
||||
.iter()
|
||||
.find(|entry| entry.cwd.as_path() == cwd)
|
||||
@@ -152,35 +182,41 @@ fn skills_for_cwd(cwd: &Path, skills_entries: &[SkillsListEntry]) -> Vec<Protoco
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn enabled_skills_for_mentions(skills: &[ProtocolSkillMetadata]) -> Vec<SkillMetadata> {
|
||||
skills
|
||||
fn legacy_skills_for_cwd(
|
||||
cwd: &Path,
|
||||
skills_entries: &[LegacySkillsListEntry],
|
||||
) -> Vec<LegacySkillMetadata> {
|
||||
skills_entries
|
||||
.iter()
|
||||
.filter(|skill| skill.enabled)
|
||||
.map(protocol_skill_to_core)
|
||||
.collect()
|
||||
.find(|entry| entry.cwd.as_path() == cwd)
|
||||
.map(|entry| entry.skills.clone())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn protocol_skill_to_core(skill: &ProtocolSkillMetadata) -> SkillMetadata {
|
||||
SkillMetadata {
|
||||
fn legacy_skill_to_app_server(skill: &LegacySkillMetadata) -> AppServerSkillMetadata {
|
||||
AppServerSkillMetadata {
|
||||
name: skill.name.clone(),
|
||||
description: skill.description.clone(),
|
||||
short_description: skill.short_description.clone(),
|
||||
interface: skill.interface.clone().map(|interface| SkillInterface {
|
||||
display_name: interface.display_name,
|
||||
short_description: interface.short_description,
|
||||
icon_small: interface.icon_small,
|
||||
icon_large: interface.icon_large,
|
||||
brand_color: interface.brand_color,
|
||||
default_prompt: interface.default_prompt,
|
||||
}),
|
||||
interface: skill
|
||||
.interface
|
||||
.clone()
|
||||
.map(|interface| AppServerSkillInterface {
|
||||
display_name: interface.display_name,
|
||||
short_description: interface.short_description,
|
||||
icon_small: interface.icon_small,
|
||||
icon_large: interface.icon_large,
|
||||
brand_color: interface.brand_color,
|
||||
default_prompt: interface.default_prompt,
|
||||
}),
|
||||
dependencies: skill
|
||||
.dependencies
|
||||
.clone()
|
||||
.map(|dependencies| SkillDependencies {
|
||||
.map(|dependencies| AppServerSkillDependencies {
|
||||
tools: dependencies
|
||||
.tools
|
||||
.into_iter()
|
||||
.map(|tool| SkillToolDependency {
|
||||
.map(|tool| AppServerSkillToolDependency {
|
||||
r#type: tool.r#type,
|
||||
value: tool.value,
|
||||
description: tool.description,
|
||||
@@ -190,11 +226,18 @@ fn protocol_skill_to_core(skill: &ProtocolSkillMetadata) -> SkillMetadata {
|
||||
})
|
||||
.collect(),
|
||||
}),
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
managed_network_override: None,
|
||||
path_to_skills_md: skill.path.clone(),
|
||||
scope: skill.scope,
|
||||
path: skill.path.clone(),
|
||||
scope: legacy_skill_scope_to_app_server(skill.scope),
|
||||
enabled: skill.enabled,
|
||||
}
|
||||
}
|
||||
|
||||
fn legacy_skill_scope_to_app_server(scope: LegacySkillScope) -> AppServerSkillScope {
|
||||
match scope {
|
||||
LegacySkillScope::User => AppServerSkillScope::User,
|
||||
LegacySkillScope::Repo => AppServerSkillScope::Repo,
|
||||
LegacySkillScope::System => AppServerSkillScope::System,
|
||||
LegacySkillScope::Admin => AppServerSkillScope::Admin,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,8 +260,8 @@ pub(crate) fn collect_tool_mentions(
|
||||
|
||||
pub(crate) fn find_skill_mentions_with_tool_mentions(
|
||||
mentions: &ToolMentions,
|
||||
skills: &[SkillMetadata],
|
||||
) -> Vec<SkillMetadata> {
|
||||
skills: &[AppServerSkillMetadata],
|
||||
) -> Vec<AppServerSkillMetadata> {
|
||||
let mention_skill_paths: HashSet<&str> = mentions
|
||||
.linked_paths
|
||||
.values()
|
||||
@@ -228,26 +271,26 @@ pub(crate) fn find_skill_mentions_with_tool_mentions(
|
||||
|
||||
let mut seen_names = HashSet::new();
|
||||
let mut seen_paths = HashSet::new();
|
||||
let mut matches: Vec<SkillMetadata> = Vec::new();
|
||||
let mut matches: Vec<AppServerSkillMetadata> = Vec::new();
|
||||
|
||||
for skill in skills {
|
||||
if seen_paths.contains(&skill.path_to_skills_md) {
|
||||
if seen_paths.contains(&skill.path) {
|
||||
continue;
|
||||
}
|
||||
let path_str = skill.path_to_skills_md.to_string_lossy();
|
||||
let path_str = skill.path.to_string_lossy();
|
||||
if mention_skill_paths.contains(path_str.as_ref()) {
|
||||
seen_paths.insert(skill.path_to_skills_md.clone());
|
||||
seen_paths.insert(skill.path.clone());
|
||||
seen_names.insert(skill.name.clone());
|
||||
matches.push(skill.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for skill in skills {
|
||||
if seen_paths.contains(&skill.path_to_skills_md) {
|
||||
if seen_paths.contains(&skill.path) {
|
||||
continue;
|
||||
}
|
||||
if mentions.names.contains(&skill.name) && seen_names.insert(skill.name.clone()) {
|
||||
seen_paths.insert(skill.path_to_skills_md.clone());
|
||||
seen_paths.insert(skill.path.clone());
|
||||
matches.push(skill.clone());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,11 @@ use crate::history_cell::UserHistoryCell;
|
||||
use crate::test_backend::VT100Backend;
|
||||
use crate::tui::FrameRequester;
|
||||
use assert_matches::assert_matches;
|
||||
use codex_app_server_protocol::SkillInterface;
|
||||
use codex_app_server_protocol::SkillMetadata;
|
||||
use codex_app_server_protocol::SkillScope;
|
||||
use codex_app_server_protocol::SkillsListEntry;
|
||||
use codex_app_server_protocol::SkillsListResponse;
|
||||
use codex_core::CodexAuth;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::ConfigBuilder;
|
||||
@@ -30,7 +35,6 @@ use codex_core::features::FEATURES;
|
||||
use codex_core::features::Feature;
|
||||
use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig;
|
||||
use codex_core::models_manager::manager::ModelsManager;
|
||||
use codex_core::skills::model::SkillMetadata;
|
||||
use codex_core::terminal::TerminalName;
|
||||
use codex_otel::RuntimeMetricsSummary;
|
||||
use codex_otel::SessionTelemetry;
|
||||
@@ -88,7 +92,6 @@ use codex_protocol::protocol::RateLimitWindow;
|
||||
use codex_protocol::protocol::ReviewRequest;
|
||||
use codex_protocol::protocol::ReviewTarget;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SkillScope;
|
||||
use codex_protocol::protocol::StreamErrorEvent;
|
||||
use codex_protocol::protocol::TerminalInteractionEvent;
|
||||
use codex_protocol::protocol::ThreadRolledBackEvent;
|
||||
@@ -996,11 +999,9 @@ async fn submission_prefers_selected_duplicate_skill_path() {
|
||||
short_description: None,
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
managed_network_override: None,
|
||||
path_to_skills_md: repo_skill_path,
|
||||
path: repo_skill_path,
|
||||
scope: SkillScope::Repo,
|
||||
enabled: true,
|
||||
},
|
||||
SkillMetadata {
|
||||
name: "figma".to_string(),
|
||||
@@ -1008,11 +1009,9 @@ async fn submission_prefers_selected_duplicate_skill_path() {
|
||||
short_description: None,
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
managed_network_override: None,
|
||||
path_to_skills_md: user_skill_path.clone(),
|
||||
path: user_skill_path.clone(),
|
||||
scope: SkillScope::User,
|
||||
enabled: true,
|
||||
},
|
||||
]));
|
||||
|
||||
@@ -1041,6 +1040,94 @@ async fn submission_prefers_selected_duplicate_skill_path() {
|
||||
assert_eq!(selected_skill_paths, vec![user_skill_path]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn app_server_skills_response_uses_active_cwd_and_enabled_mentions() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await;
|
||||
let active_cwd = PathBuf::from("/tmp/project");
|
||||
chat.config.cwd = active_cwd.clone();
|
||||
|
||||
chat.set_skills_from_app_server_response(&SkillsListResponse {
|
||||
data: vec![
|
||||
SkillsListEntry {
|
||||
cwd: PathBuf::from("/tmp/other"),
|
||||
skills: vec![SkillMetadata {
|
||||
name: "ignored".to_string(),
|
||||
description: "Other cwd".to_string(),
|
||||
short_description: None,
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
path: PathBuf::from("/tmp/other/ignored/SKILL.md"),
|
||||
scope: SkillScope::Repo,
|
||||
enabled: true,
|
||||
}],
|
||||
errors: Vec::new(),
|
||||
},
|
||||
SkillsListEntry {
|
||||
cwd: active_cwd,
|
||||
skills: vec![
|
||||
SkillMetadata {
|
||||
name: "enabled-skill".to_string(),
|
||||
description: "Enabled".to_string(),
|
||||
short_description: None,
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
path: PathBuf::from("/tmp/project/enabled/SKILL.md"),
|
||||
scope: SkillScope::Repo,
|
||||
enabled: true,
|
||||
},
|
||||
SkillMetadata {
|
||||
name: "disabled-skill".to_string(),
|
||||
description: "Disabled".to_string(),
|
||||
short_description: None,
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
path: PathBuf::from("/tmp/project/disabled/SKILL.md"),
|
||||
scope: SkillScope::Repo,
|
||||
enabled: false,
|
||||
},
|
||||
],
|
||||
errors: Vec::new(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
let skills = chat.skills().expect("skills should be populated");
|
||||
assert_eq!(skills.len(), 1);
|
||||
assert_eq!(skills[0].name, "enabled-skill");
|
||||
assert_eq!(
|
||||
skills[0].path,
|
||||
PathBuf::from("/tmp/project/enabled/SKILL.md")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn manage_skills_popup_uses_app_server_display_name_and_short_description() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await;
|
||||
chat.skills_all = vec![SkillMetadata {
|
||||
name: "google-calendar-skill".to_string(),
|
||||
description: "Long description".to_string(),
|
||||
short_description: Some("Legacy short description".to_string()),
|
||||
interface: Some(SkillInterface {
|
||||
display_name: Some("Google Calendar".to_string()),
|
||||
short_description: Some("Plan events quickly".to_string()),
|
||||
icon_small: None,
|
||||
icon_large: None,
|
||||
brand_color: None,
|
||||
default_prompt: None,
|
||||
}),
|
||||
dependencies: None,
|
||||
path: PathBuf::from("/tmp/project/google-calendar/SKILL.md"),
|
||||
scope: SkillScope::Repo,
|
||||
enabled: true,
|
||||
}];
|
||||
|
||||
chat.open_manage_skills_popup();
|
||||
|
||||
let popup = render_bottom_popup(&chat, 72);
|
||||
assert!(popup.contains("Google Calendar"));
|
||||
assert!(popup.contains("Plan events quickly"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn blocked_image_restore_preserves_mention_bindings() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use codex_core::skills::model::SkillMetadata;
|
||||
use codex_app_server_protocol::SkillMetadata;
|
||||
use codex_utils_fuzzy_match::fuzzy_match;
|
||||
|
||||
use crate::text_formatting::truncate_text;
|
||||
|
||||
Reference in New Issue
Block a user