Inject SKILL.md when it's explicitly mentioned. (#7763)

1. Skills load once in core at session start; the cached outcome is
reused across core and surfaced to TUI via SessionConfigured.
2. TUI detects explicit skill selections, and core injects the matching
SKILL.md content into the turn when a selected skill is present.
This commit is contained in:
xl-openai
2025-12-10 13:59:17 -08:00
committed by GitHub
parent eb2e5458cc
commit b36ecb6c32
21 changed files with 584 additions and 88 deletions

View File

@@ -44,6 +44,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::StreamErrorEvent;
use codex_core::protocol::TaskCompleteEvent;
use codex_core::protocol::TerminalInteractionEvent;
@@ -263,7 +264,6 @@ pub(crate) struct ChatWidgetInit {
pub(crate) auth_manager: Arc<AuthManager>,
pub(crate) models_manager: Arc<ModelsManager>,
pub(crate) feedback: codex_feedback::CodexFeedback,
pub(crate) skills: Option<Vec<SkillMetadata>>,
pub(crate) is_first_run: bool,
pub(crate) model_family: ModelFamily,
}
@@ -392,6 +392,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.conversation_id = Some(event.session_id);
self.current_rollout_path = Some(event.rollout_path.clone());
let initial_messages = event.initial_messages.clone();
@@ -416,6 +417,11 @@ impl ChatWidget {
}
}
fn set_skills_from_outcome(&mut self, outcome: Option<&SkillLoadOutcomeInfo>) {
let skills = outcome.map(skills_from_outcome);
self.bottom_pane.set_skills(skills);
}
pub(crate) fn open_feedback_note(
&mut self,
category: crate::app_event::FeedbackCategory,
@@ -1262,7 +1268,6 @@ impl ChatWidget {
auth_manager,
models_manager,
feedback,
skills,
is_first_run,
model_family,
} = common;
@@ -1285,7 +1290,7 @@ impl ChatWidget {
placeholder_text: placeholder,
disable_paste_burst: config.disable_paste_burst,
animations_enabled: config.animations,
skills,
skills: None,
}),
active_cell: None,
config,
@@ -1348,7 +1353,6 @@ impl ChatWidget {
auth_manager,
models_manager,
feedback,
skills,
model_family,
..
} = common;
@@ -1371,7 +1375,7 @@ impl ChatWidget {
placeholder_text: placeholder,
disable_paste_burst: config.disable_paste_burst,
animations_enabled: config.animations,
skills,
skills: None,
}),
active_cell: None,
config,
@@ -1738,6 +1742,16 @@ impl ChatWidget {
items.push(UserInput::LocalImage { path });
}
if let Some(skills) = self.bottom_pane.skills() {
let skill_mentions = find_skill_mentions(&text, skills);
for skill in skill_mentions {
items.push(UserInput::Skill {
name: skill.name.clone(),
path: skill.path.clone(),
});
}
}
self.codex_op_tx
.send(Op::UserInput { items })
.unwrap_or_else(|e| {
@@ -3459,5 +3473,33 @@ 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();
for skill in skills {
if seen.contains(&skill.name) {
continue;
}
let needle = format!("${}", skill.name);
if text.contains(&needle) {
seen.insert(skill.name.clone());
matches.push(skill.clone());
}
}
matches
}
#[cfg(test)]
pub(crate) mod tests;