mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Reimplement skills loading using SkillsManager + skills/list op. (#7914)
refactor the way we load and manage skills: 1. Move skill discovery/caching into SkillsManager and reuse it across sessions. 2. Add the skills/list API (Op::ListSkills/SkillsListResponse) to fetch skills for one or more cwds. Also update app-server for VSCE/App; 3. Trigger skills/list during session startup so UIs preload skills and handle errors immediately.
This commit is contained in:
+22
-11
@@ -31,9 +31,10 @@ use codex_core::openai_models::model_presets::HIDE_GPT5_1_MIGRATION_PROMPT_CONFI
|
||||
use codex_core::openai_models::models_manager::ModelsManager;
|
||||
use codex_core::protocol::EventMsg;
|
||||
use codex_core::protocol::FinalOutput;
|
||||
use codex_core::protocol::ListSkillsResponseEvent;
|
||||
use codex_core::protocol::Op;
|
||||
use codex_core::protocol::SessionSource;
|
||||
use codex_core::protocol::SkillLoadOutcomeInfo;
|
||||
use codex_core::protocol::SkillErrorInfo;
|
||||
use codex_core::protocol::TokenUsage;
|
||||
use codex_core::skills::SkillError;
|
||||
use codex_protocol::ConversationId;
|
||||
@@ -50,6 +51,7 @@ use ratatui::text::Line;
|
||||
use ratatui::widgets::Paragraph;
|
||||
use ratatui::widgets::Wrap;
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
@@ -96,9 +98,8 @@ fn session_summary(
|
||||
})
|
||||
}
|
||||
|
||||
fn skill_errors_from_outcome(outcome: &SkillLoadOutcomeInfo) -> Vec<SkillError> {
|
||||
outcome
|
||||
.errors
|
||||
fn skill_errors_from_info(errors: &[SkillErrorInfo]) -> Vec<SkillError> {
|
||||
errors
|
||||
.iter()
|
||||
.map(|err| SkillError {
|
||||
path: err.path.clone(),
|
||||
@@ -107,6 +108,15 @@ fn skill_errors_from_outcome(outcome: &SkillLoadOutcomeInfo) -> Vec<SkillError>
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn errors_for_cwd(cwd: &Path, response: &ListSkillsResponseEvent) -> Vec<SkillErrorInfo> {
|
||||
response
|
||||
.skills
|
||||
.iter()
|
||||
.find(|entry| entry.cwd.as_path() == cwd)
|
||||
.map(|entry| entry.errors.clone())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct SessionSummary {
|
||||
usage_line: String,
|
||||
@@ -698,11 +708,14 @@ impl App {
|
||||
self.suppress_shutdown_complete = false;
|
||||
return Ok(true);
|
||||
}
|
||||
if let EventMsg::SessionConfigured(cfg) = &event.msg
|
||||
&& let Some(outcome) = cfg.skill_load_outcome.as_ref()
|
||||
&& !outcome.errors.is_empty()
|
||||
{
|
||||
let errors = skill_errors_from_outcome(outcome);
|
||||
if let EventMsg::ListSkillsResponse(response) = &event.msg {
|
||||
let cwd = self.chat_widget.config_ref().cwd.clone();
|
||||
let errors = errors_for_cwd(&cwd, response);
|
||||
if errors.is_empty() {
|
||||
self.chat_widget.handle_codex_event(event);
|
||||
return Ok(true);
|
||||
}
|
||||
let errors = skill_errors_from_info(&errors);
|
||||
match run_skill_error_prompt(tui, &errors).await {
|
||||
SkillErrorPromptOutcome::Exit => {
|
||||
self.chat_widget.submit_op(Op::Shutdown);
|
||||
@@ -1392,7 +1405,6 @@ mod tests {
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
initial_messages: None,
|
||||
skill_load_outcome: None,
|
||||
rollout_path: PathBuf::new(),
|
||||
};
|
||||
Arc::new(new_session_info(
|
||||
@@ -1448,7 +1460,6 @@ mod tests {
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
initial_messages: None,
|
||||
skill_load_outcome: None,
|
||||
rollout_path: PathBuf::new(),
|
||||
};
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ use codex_core::protocol::ExecCommandEndEvent;
|
||||
use codex_core::protocol::ExecCommandSource;
|
||||
use codex_core::protocol::ExitedReviewModeEvent;
|
||||
use codex_core::protocol::ListCustomPromptsResponseEvent;
|
||||
use codex_core::protocol::ListSkillsResponseEvent;
|
||||
use codex_core::protocol::McpListToolsResponseEvent;
|
||||
use codex_core::protocol::McpStartupCompleteEvent;
|
||||
use codex_core::protocol::McpStartupStatus;
|
||||
@@ -44,7 +45,7 @@ use codex_core::protocol::PatchApplyBeginEvent;
|
||||
use codex_core::protocol::RateLimitSnapshot;
|
||||
use codex_core::protocol::ReviewRequest;
|
||||
use codex_core::protocol::ReviewTarget;
|
||||
use codex_core::protocol::SkillLoadOutcomeInfo;
|
||||
use codex_core::protocol::SkillsListEntry;
|
||||
use codex_core::protocol::StreamErrorEvent;
|
||||
use codex_core::protocol::TaskCompleteEvent;
|
||||
use codex_core::protocol::TerminalInteractionEvent;
|
||||
@@ -392,7 +393,7 @@ impl ChatWidget {
|
||||
fn on_session_configured(&mut self, event: codex_core::protocol::SessionConfiguredEvent) {
|
||||
self.bottom_pane
|
||||
.set_history_metadata(event.history_log_id, event.history_entry_count);
|
||||
self.set_skills_from_outcome(event.skill_load_outcome.as_ref());
|
||||
self.set_skills(None);
|
||||
self.conversation_id = Some(event.session_id);
|
||||
self.current_rollout_path = Some(event.rollout_path.clone());
|
||||
let initial_messages = event.initial_messages.clone();
|
||||
@@ -409,6 +410,7 @@ 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() });
|
||||
if let Some(user_message) = self.initial_user_message.take() {
|
||||
self.submit_user_message(user_message);
|
||||
}
|
||||
@@ -417,11 +419,15 @@ impl ChatWidget {
|
||||
}
|
||||
}
|
||||
|
||||
fn set_skills_from_outcome(&mut self, outcome: Option<&SkillLoadOutcomeInfo>) {
|
||||
let skills = outcome.map(skills_from_outcome);
|
||||
fn set_skills(&mut self, skills: Option<Vec<SkillMetadata>>) {
|
||||
self.bottom_pane.set_skills(skills);
|
||||
}
|
||||
|
||||
fn set_skills_from_response(&mut self, response: &ListSkillsResponseEvent) {
|
||||
let skills = skills_for_cwd(&self.config.cwd, &response.skills);
|
||||
self.set_skills(Some(skills));
|
||||
}
|
||||
|
||||
pub(crate) fn open_feedback_note(
|
||||
&mut self,
|
||||
category: crate::app_event::FeedbackCategory,
|
||||
@@ -1879,6 +1885,7 @@ impl ChatWidget {
|
||||
EventMsg::GetHistoryEntryResponse(ev) => self.on_get_history_entry_response(ev),
|
||||
EventMsg::McpListToolsResponse(ev) => self.on_list_mcp_tools(ev),
|
||||
EventMsg::ListCustomPromptsResponse(ev) => self.on_list_custom_prompts(ev),
|
||||
EventMsg::ListSkillsResponse(ev) => self.on_list_skills(ev),
|
||||
EventMsg::ShutdownComplete => self.on_shutdown_complete(),
|
||||
EventMsg::TurnDiff(TurnDiffEvent { unified_diff }) => self.on_turn_diff(unified_diff),
|
||||
EventMsg::DeprecationNotice(ev) => self.on_deprecation_notice(ev),
|
||||
@@ -3092,6 +3099,10 @@ impl ChatWidget {
|
||||
self.bottom_pane.set_custom_prompts(ev.custom_prompts);
|
||||
}
|
||||
|
||||
fn on_list_skills(&mut self, ev: ListSkillsResponseEvent) {
|
||||
self.set_skills_from_response(&ev);
|
||||
}
|
||||
|
||||
pub(crate) fn open_review_popup(&mut self) {
|
||||
let mut items: Vec<SelectionItem> = Vec::new();
|
||||
|
||||
@@ -3476,18 +3487,6 @@ pub(crate) fn show_review_commit_picker_with_entries(
|
||||
});
|
||||
}
|
||||
|
||||
fn skills_from_outcome(outcome: &SkillLoadOutcomeInfo) -> Vec<SkillMetadata> {
|
||||
outcome
|
||||
.skills
|
||||
.iter()
|
||||
.map(|skill| SkillMetadata {
|
||||
name: skill.name.clone(),
|
||||
description: skill.description.clone(),
|
||||
path: skill.path.clone(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn find_skill_mentions(text: &str, skills: &[SkillMetadata]) -> Vec<SkillMetadata> {
|
||||
let mut seen: HashSet<String> = HashSet::new();
|
||||
let mut matches: Vec<SkillMetadata> = Vec::new();
|
||||
@@ -3504,5 +3503,24 @@ fn find_skill_mentions(text: &str, skills: &[SkillMetadata]) -> Vec<SkillMetadat
|
||||
matches
|
||||
}
|
||||
|
||||
fn skills_for_cwd(cwd: &Path, skills_entries: &[SkillsListEntry]) -> Vec<SkillMetadata> {
|
||||
skills_entries
|
||||
.iter()
|
||||
.find(|entry| entry.cwd.as_path() == cwd)
|
||||
.map(|entry| {
|
||||
entry
|
||||
.skills
|
||||
.iter()
|
||||
.map(|skill| SkillMetadata {
|
||||
name: skill.name.clone(),
|
||||
description: skill.description.clone(),
|
||||
path: skill.path.clone(),
|
||||
scope: skill.scope,
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests;
|
||||
|
||||
@@ -125,7 +125,6 @@ fn resumed_initial_messages_render_history() {
|
||||
message: "assistant reply".to_string(),
|
||||
}),
|
||||
]),
|
||||
skill_load_outcome: None,
|
||||
rollout_path: rollout_file.path().to_path_buf(),
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user