|
@@ -278,6 +278,12 @@ pub enum SlashCommand {
|
|
|
action: Option<String>,
|
|
action: Option<String>,
|
|
|
target: Option<String>,
|
|
target: Option<String>,
|
|
|
},
|
|
},
|
|
|
|
|
+ Agents {
|
|
|
|
|
+ args: Option<String>,
|
|
|
|
|
+ },
|
|
|
|
|
+ Skills {
|
|
|
|
|
+ args: Option<String>,
|
|
|
|
|
+ },
|
|
|
Unknown(String),
|
|
Unknown(String),
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -339,13 +345,19 @@ impl SlashCommand {
|
|
|
action: parts.next().map(ToOwned::to_owned),
|
|
action: parts.next().map(ToOwned::to_owned),
|
|
|
target: parts.next().map(ToOwned::to_owned),
|
|
target: parts.next().map(ToOwned::to_owned),
|
|
|
},
|
|
},
|
|
|
- "plugins" => Self::Plugins {
|
|
|
|
|
|
|
+ "plugin" | "plugins" | "marketplace" => Self::Plugins {
|
|
|
action: parts.next().map(ToOwned::to_owned),
|
|
action: parts.next().map(ToOwned::to_owned),
|
|
|
target: {
|
|
target: {
|
|
|
let remainder = parts.collect::<Vec<_>>().join(" ");
|
|
let remainder = parts.collect::<Vec<_>>().join(" ");
|
|
|
(!remainder.is_empty()).then_some(remainder)
|
|
(!remainder.is_empty()).then_some(remainder)
|
|
|
},
|
|
},
|
|
|
},
|
|
},
|
|
|
|
|
+ "agents" => Self::Agents {
|
|
|
|
|
+ args: remainder_after_command(trimmed, command),
|
|
|
|
|
+ },
|
|
|
|
|
+ "skills" => Self::Skills {
|
|
|
|
|
+ args: remainder_after_command(trimmed, command),
|
|
|
|
|
+ },
|
|
|
other => Self::Unknown(other.to_string()),
|
|
other => Self::Unknown(other.to_string()),
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
@@ -384,12 +396,27 @@ pub fn render_slash_command_help() -> String {
|
|
|
Some(argument_hint) => format!("/{} {}", spec.name, argument_hint),
|
|
Some(argument_hint) => format!("/{} {}", spec.name, argument_hint),
|
|
|
None => format!("/{}", spec.name),
|
|
None => format!("/{}", spec.name),
|
|
|
};
|
|
};
|
|
|
|
|
+ let alias_suffix = if spec.aliases.is_empty() {
|
|
|
|
|
+ String::new()
|
|
|
|
|
+ } else {
|
|
|
|
|
+ format!(
|
|
|
|
|
+ " (aliases: {})",
|
|
|
|
|
+ spec.aliases
|
|
|
|
|
+ .iter()
|
|
|
|
|
+ .map(|alias| format!("/{alias}"))
|
|
|
|
|
+ .collect::<Vec<_>>()
|
|
|
|
|
+ .join(", ")
|
|
|
|
|
+ )
|
|
|
|
|
+ };
|
|
|
let resume = if spec.resume_supported {
|
|
let resume = if spec.resume_supported {
|
|
|
" [resume]"
|
|
" [resume]"
|
|
|
} else {
|
|
} else {
|
|
|
""
|
|
""
|
|
|
};
|
|
};
|
|
|
- lines.push(format!(" {name:<20} {}{}", spec.summary, resume));
|
|
|
|
|
|
|
+ lines.push(format!(
|
|
|
|
|
+ " {name:<20} {}{alias_suffix}{resume}",
|
|
|
|
|
+ spec.summary
|
|
|
|
|
+ ));
|
|
|
}
|
|
}
|
|
|
lines.join("\n")
|
|
lines.join("\n")
|
|
|
}
|
|
}
|
|
@@ -406,6 +433,45 @@ pub struct PluginsCommandResult {
|
|
|
pub reload_runtime: bool,
|
|
pub reload_runtime: bool,
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
|
|
|
|
+enum DefinitionSource {
|
|
|
|
|
+ ProjectCodex,
|
|
|
|
|
+ ProjectClaude,
|
|
|
|
|
+ UserCodexHome,
|
|
|
|
|
+ UserCodex,
|
|
|
|
|
+ UserClaude,
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+impl DefinitionSource {
|
|
|
|
|
+ fn label(self) -> &'static str {
|
|
|
|
|
+ match self {
|
|
|
|
|
+ Self::ProjectCodex => "Project (.codex)",
|
|
|
|
|
+ Self::ProjectClaude => "Project (.claude)",
|
|
|
|
|
+ Self::UserCodexHome => "User ($CODEX_HOME)",
|
|
|
|
|
+ Self::UserCodex => "User (~/.codex)",
|
|
|
|
|
+ Self::UserClaude => "User (~/.claude)",
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
|
+struct AgentSummary {
|
|
|
|
|
+ name: String,
|
|
|
|
|
+ description: Option<String>,
|
|
|
|
|
+ model: Option<String>,
|
|
|
|
|
+ reasoning_effort: Option<String>,
|
|
|
|
|
+ source: DefinitionSource,
|
|
|
|
|
+ shadowed_by: Option<DefinitionSource>,
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
|
+struct SkillSummary {
|
|
|
|
|
+ name: String,
|
|
|
|
|
+ description: Option<String>,
|
|
|
|
|
+ source: DefinitionSource,
|
|
|
|
|
+ shadowed_by: Option<DefinitionSource>,
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
#[allow(clippy::too_many_lines)]
|
|
#[allow(clippy::too_many_lines)]
|
|
|
pub fn handle_plugins_slash_command(
|
|
pub fn handle_plugins_slash_command(
|
|
|
action: Option<&str>,
|
|
action: Option<&str>,
|
|
@@ -518,6 +584,26 @@ pub fn handle_plugins_slash_command(
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+pub fn handle_agents_slash_command(args: Option<&str>, cwd: &Path) -> std::io::Result<String> {
|
|
|
|
|
+ if let Some(args) = args.filter(|value| !value.trim().is_empty()) {
|
|
|
|
|
+ return Ok(format!("Usage: /agents\nUnexpected arguments: {args}"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let roots = discover_definition_roots(cwd, "agents");
|
|
|
|
|
+ let agents = load_agents_from_roots(&roots)?;
|
|
|
|
|
+ Ok(render_agents_report(&agents))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+pub fn handle_skills_slash_command(args: Option<&str>, cwd: &Path) -> std::io::Result<String> {
|
|
|
|
|
+ if let Some(args) = args.filter(|value| !value.trim().is_empty()) {
|
|
|
|
|
+ return Ok(format!("Usage: /skills\nUnexpected arguments: {args}"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let roots = discover_definition_roots(cwd, "skills");
|
|
|
|
|
+ let skills = load_skills_from_roots(&roots)?;
|
|
|
|
|
+ Ok(render_skills_report(&skills))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
#[must_use]
|
|
#[must_use]
|
|
|
pub fn render_plugins_report(plugins: &[PluginSummary]) -> String {
|
|
pub fn render_plugins_report(plugins: &[PluginSummary]) -> String {
|
|
|
let mut lines = vec!["Plugins".to_string()];
|
|
let mut lines = vec!["Plugins".to_string()];
|
|
@@ -570,6 +656,303 @@ fn resolve_plugin_target(
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+fn discover_definition_roots(cwd: &Path, leaf: &str) -> Vec<(DefinitionSource, PathBuf)> {
|
|
|
|
|
+ let mut roots = Vec::new();
|
|
|
|
|
+
|
|
|
|
|
+ for ancestor in cwd.ancestors() {
|
|
|
|
|
+ push_unique_root(
|
|
|
|
|
+ &mut roots,
|
|
|
|
|
+ DefinitionSource::ProjectCodex,
|
|
|
|
|
+ ancestor.join(".codex").join(leaf),
|
|
|
|
|
+ );
|
|
|
|
|
+ push_unique_root(
|
|
|
|
|
+ &mut roots,
|
|
|
|
|
+ DefinitionSource::ProjectClaude,
|
|
|
|
|
+ ancestor.join(".claude").join(leaf),
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if let Ok(codex_home) = env::var("CODEX_HOME") {
|
|
|
|
|
+ push_unique_root(
|
|
|
|
|
+ &mut roots,
|
|
|
|
|
+ DefinitionSource::UserCodexHome,
|
|
|
|
|
+ PathBuf::from(codex_home).join(leaf),
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if let Some(home) = env::var_os("HOME") {
|
|
|
|
|
+ let home = PathBuf::from(home);
|
|
|
|
|
+ push_unique_root(
|
|
|
|
|
+ &mut roots,
|
|
|
|
|
+ DefinitionSource::UserCodex,
|
|
|
|
|
+ home.join(".codex").join(leaf),
|
|
|
|
|
+ );
|
|
|
|
|
+ push_unique_root(
|
|
|
|
|
+ &mut roots,
|
|
|
|
|
+ DefinitionSource::UserClaude,
|
|
|
|
|
+ home.join(".claude").join(leaf),
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ roots
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+fn push_unique_root(
|
|
|
|
|
+ roots: &mut Vec<(DefinitionSource, PathBuf)>,
|
|
|
|
|
+ source: DefinitionSource,
|
|
|
|
|
+ path: PathBuf,
|
|
|
|
|
+) {
|
|
|
|
|
+ if path.is_dir() && !roots.iter().any(|(_, existing)| existing == &path) {
|
|
|
|
|
+ roots.push((source, path));
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+fn load_agents_from_roots(
|
|
|
|
|
+ roots: &[(DefinitionSource, PathBuf)],
|
|
|
|
|
+) -> std::io::Result<Vec<AgentSummary>> {
|
|
|
|
|
+ let mut agents = Vec::new();
|
|
|
|
|
+ let mut active_sources = BTreeMap::<String, DefinitionSource>::new();
|
|
|
|
|
+
|
|
|
|
|
+ for (source, root) in roots {
|
|
|
|
|
+ let mut root_agents = Vec::new();
|
|
|
|
|
+ for entry in fs::read_dir(root)? {
|
|
|
|
|
+ let entry = entry?;
|
|
|
|
|
+ if entry.path().extension().is_none_or(|ext| ext != "toml") {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ let contents = fs::read_to_string(entry.path())?;
|
|
|
|
|
+ let fallback_name = entry
|
|
|
|
|
+ .path()
|
|
|
|
|
+ .file_stem()
|
|
|
|
|
+ .map(|stem| stem.to_string_lossy().to_string())
|
|
|
|
|
+ .unwrap_or_else(|| entry.file_name().to_string_lossy().to_string());
|
|
|
|
|
+ root_agents.push(AgentSummary {
|
|
|
|
|
+ name: parse_toml_string(&contents, "name").unwrap_or(fallback_name),
|
|
|
|
|
+ description: parse_toml_string(&contents, "description"),
|
|
|
|
|
+ model: parse_toml_string(&contents, "model"),
|
|
|
|
|
+ reasoning_effort: parse_toml_string(&contents, "model_reasoning_effort"),
|
|
|
|
|
+ source: *source,
|
|
|
|
|
+ shadowed_by: None,
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ root_agents.sort_by(|left, right| left.name.cmp(&right.name));
|
|
|
|
|
+
|
|
|
|
|
+ for mut agent in root_agents {
|
|
|
|
|
+ let key = agent.name.to_ascii_lowercase();
|
|
|
|
|
+ if let Some(existing) = active_sources.get(&key) {
|
|
|
|
|
+ agent.shadowed_by = Some(*existing);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ active_sources.insert(key, agent.source);
|
|
|
|
|
+ }
|
|
|
|
|
+ agents.push(agent);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Ok(agents)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+fn load_skills_from_roots(
|
|
|
|
|
+ roots: &[(DefinitionSource, PathBuf)],
|
|
|
|
|
+) -> std::io::Result<Vec<SkillSummary>> {
|
|
|
|
|
+ let mut skills = Vec::new();
|
|
|
|
|
+ let mut active_sources = BTreeMap::<String, DefinitionSource>::new();
|
|
|
|
|
+
|
|
|
|
|
+ for (source, root) in roots {
|
|
|
|
|
+ let mut root_skills = Vec::new();
|
|
|
|
|
+ for entry in fs::read_dir(root)? {
|
|
|
|
|
+ let entry = entry?;
|
|
|
|
|
+ if !entry.path().is_dir() {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ let skill_path = entry.path().join("SKILL.md");
|
|
|
|
|
+ if !skill_path.is_file() {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ let contents = fs::read_to_string(skill_path)?;
|
|
|
|
|
+ let (name, description) = parse_skill_frontmatter(&contents);
|
|
|
|
|
+ root_skills.push(SkillSummary {
|
|
|
|
|
+ name: name.unwrap_or_else(|| entry.file_name().to_string_lossy().to_string()),
|
|
|
|
|
+ description,
|
|
|
|
|
+ source: *source,
|
|
|
|
|
+ shadowed_by: None,
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ root_skills.sort_by(|left, right| left.name.cmp(&right.name));
|
|
|
|
|
+
|
|
|
|
|
+ for mut skill in root_skills {
|
|
|
|
|
+ let key = skill.name.to_ascii_lowercase();
|
|
|
|
|
+ if let Some(existing) = active_sources.get(&key) {
|
|
|
|
|
+ skill.shadowed_by = Some(*existing);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ active_sources.insert(key, skill.source);
|
|
|
|
|
+ }
|
|
|
|
|
+ skills.push(skill);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Ok(skills)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+fn parse_toml_string(contents: &str, key: &str) -> Option<String> {
|
|
|
|
|
+ let prefix = format!("{key} =");
|
|
|
|
|
+ for line in contents.lines() {
|
|
|
|
|
+ let trimmed = line.trim();
|
|
|
|
|
+ if trimmed.starts_with('#') {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ let Some(value) = trimmed.strip_prefix(&prefix) else {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ };
|
|
|
|
|
+ let value = value.trim();
|
|
|
|
|
+ let Some(value) = value
|
|
|
|
|
+ .strip_prefix('"')
|
|
|
|
|
+ .and_then(|value| value.strip_suffix('"'))
|
|
|
|
|
+ else {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ };
|
|
|
|
|
+ if !value.is_empty() {
|
|
|
|
|
+ return Some(value.to_string());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ None
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+fn parse_skill_frontmatter(contents: &str) -> (Option<String>, Option<String>) {
|
|
|
|
|
+ let mut lines = contents.lines();
|
|
|
|
|
+ if lines.next().map(str::trim) != Some("---") {
|
|
|
|
|
+ return (None, None);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let mut name = None;
|
|
|
|
|
+ let mut description = None;
|
|
|
|
|
+ for line in lines {
|
|
|
|
|
+ let trimmed = line.trim();
|
|
|
|
|
+ if trimmed == "---" {
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ if let Some(value) = trimmed.strip_prefix("name:") {
|
|
|
|
|
+ let value = value.trim();
|
|
|
|
|
+ if !value.is_empty() {
|
|
|
|
|
+ name = Some(value.to_string());
|
|
|
|
|
+ }
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ if let Some(value) = trimmed.strip_prefix("description:") {
|
|
|
|
|
+ let value = value.trim();
|
|
|
|
|
+ if !value.is_empty() {
|
|
|
|
|
+ description = Some(value.to_string());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ (name, description)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+fn render_agents_report(agents: &[AgentSummary]) -> String {
|
|
|
|
|
+ if agents.is_empty() {
|
|
|
|
|
+ return "No agents found.".to_string();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let total_active = agents
|
|
|
|
|
+ .iter()
|
|
|
|
|
+ .filter(|agent| agent.shadowed_by.is_none())
|
|
|
|
|
+ .count();
|
|
|
|
|
+ let mut lines = vec![
|
|
|
|
|
+ "Agents".to_string(),
|
|
|
|
|
+ format!(" {total_active} active agents"),
|
|
|
|
|
+ String::new(),
|
|
|
|
|
+ ];
|
|
|
|
|
+
|
|
|
|
|
+ for source in [
|
|
|
|
|
+ DefinitionSource::ProjectCodex,
|
|
|
|
|
+ DefinitionSource::ProjectClaude,
|
|
|
|
|
+ DefinitionSource::UserCodexHome,
|
|
|
|
|
+ DefinitionSource::UserCodex,
|
|
|
|
|
+ DefinitionSource::UserClaude,
|
|
|
|
|
+ ] {
|
|
|
|
|
+ let group = agents
|
|
|
|
|
+ .iter()
|
|
|
|
|
+ .filter(|agent| agent.source == source)
|
|
|
|
|
+ .collect::<Vec<_>>();
|
|
|
|
|
+ if group.is_empty() {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ lines.push(format!("{}:", source.label()));
|
|
|
|
|
+ for agent in group {
|
|
|
|
|
+ let detail = agent_detail(agent);
|
|
|
|
|
+ match agent.shadowed_by {
|
|
|
|
|
+ Some(winner) => lines.push(format!(" (shadowed by {}) {detail}", winner.label())),
|
|
|
|
|
+ None => lines.push(format!(" {detail}")),
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ lines.push(String::new());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ lines.join("\n").trim_end().to_string()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+fn agent_detail(agent: &AgentSummary) -> String {
|
|
|
|
|
+ let mut parts = vec![agent.name.clone()];
|
|
|
|
|
+ if let Some(description) = &agent.description {
|
|
|
|
|
+ parts.push(description.clone());
|
|
|
|
|
+ }
|
|
|
|
|
+ if let Some(model) = &agent.model {
|
|
|
|
|
+ parts.push(model.clone());
|
|
|
|
|
+ }
|
|
|
|
|
+ if let Some(reasoning) = &agent.reasoning_effort {
|
|
|
|
|
+ parts.push(reasoning.clone());
|
|
|
|
|
+ }
|
|
|
|
|
+ parts.join(" · ")
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+fn render_skills_report(skills: &[SkillSummary]) -> String {
|
|
|
|
|
+ if skills.is_empty() {
|
|
|
|
|
+ return "No skills found.".to_string();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let total_active = skills
|
|
|
|
|
+ .iter()
|
|
|
|
|
+ .filter(|skill| skill.shadowed_by.is_none())
|
|
|
|
|
+ .count();
|
|
|
|
|
+ let mut lines = vec![
|
|
|
|
|
+ "Skills".to_string(),
|
|
|
|
|
+ format!(" {total_active} available skills"),
|
|
|
|
|
+ String::new(),
|
|
|
|
|
+ ];
|
|
|
|
|
+
|
|
|
|
|
+ for source in [
|
|
|
|
|
+ DefinitionSource::ProjectCodex,
|
|
|
|
|
+ DefinitionSource::ProjectClaude,
|
|
|
|
|
+ DefinitionSource::UserCodexHome,
|
|
|
|
|
+ DefinitionSource::UserCodex,
|
|
|
|
|
+ DefinitionSource::UserClaude,
|
|
|
|
|
+ ] {
|
|
|
|
|
+ let group = skills
|
|
|
|
|
+ .iter()
|
|
|
|
|
+ .filter(|skill| skill.source == source)
|
|
|
|
|
+ .collect::<Vec<_>>();
|
|
|
|
|
+ if group.is_empty() {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ lines.push(format!("{}:", source.label()));
|
|
|
|
|
+ for skill in group {
|
|
|
|
|
+ let detail = match &skill.description {
|
|
|
|
|
+ Some(description) => format!("{} · {}", skill.name, description),
|
|
|
|
|
+ None => skill.name.clone(),
|
|
|
|
|
+ };
|
|
|
|
|
+ match skill.shadowed_by {
|
|
|
|
|
+ Some(winner) => lines.push(format!(" (shadowed by {}) {detail}", winner.label())),
|
|
|
|
|
+ None => lines.push(format!(" {detail}")),
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ lines.push(String::new());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ lines.join("\n").trim_end().to_string()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
#[must_use]
|
|
#[must_use]
|
|
|
pub fn handle_slash_command(
|
|
pub fn handle_slash_command(
|
|
|
input: &str,
|
|
input: &str,
|
|
@@ -617,6 +1000,8 @@ pub fn handle_slash_command(
|
|
|
| SlashCommand::Export { .. }
|
|
| SlashCommand::Export { .. }
|
|
|
| SlashCommand::Session { .. }
|
|
| SlashCommand::Session { .. }
|
|
|
| SlashCommand::Plugins { .. }
|
|
| SlashCommand::Plugins { .. }
|
|
|
|
|
+ | SlashCommand::Agents { .. }
|
|
|
|
|
+ | SlashCommand::Skills { .. }
|
|
|
| SlashCommand::Unknown(_) => None,
|
|
| SlashCommand::Unknown(_) => None,
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|