Compare commits

...

3 Commits

Author SHA1 Message Date
Casey Chow
468faf41dc feat(core): add reload_plugins tool 2026-05-19 12:53:21 -04:00
Casey Chow
7f078f3a84 feat(cli): add plugin reload command 2026-05-19 12:53:21 -04:00
Casey Chow
c280fcc7ed feat(tui): add /reload-plugins command 2026-05-19 12:51:51 -04:00
21 changed files with 352 additions and 4 deletions

1
codex-rs/Cargo.lock generated
View File

@@ -2223,6 +2223,7 @@ dependencies = [
"clap_complete",
"codex-api",
"codex-app-server",
"codex-app-server-client",
"codex-app-server-daemon",
"codex-app-server-protocol",
"codex-app-server-test-client",

View File

@@ -212,7 +212,7 @@ Example with notification opt-out:
- `plugin/uninstall` — uninstall a local plugin by `pluginId` in `<plugin>@<marketplace>` form by removing its cached files and clearing its user-level config entry, or uninstall a remote ChatGPT plugin by backend `pluginId` by forwarding the uninstall to the ChatGPT plugin backend and removing any downloaded remote-plugin cache (**under development; do not call from production clients yet**).
- `mcpServer/oauth/login` — start an OAuth login for a configured MCP server; returns an `authorization_url` and later emits `mcpServer/oauthLogin/completed` once the browser flow finishes.
- `tool/requestUserInput` — prompt the user with 13 short questions for a tool call and return their answers (experimental).
- `config/mcpServer/reload` — reload MCP server config from disk and queue a refresh for loaded threads (applied on each thread's next active turn); returns `{}`. Use this after editing `config.toml` without restarting the server.
- `config/mcpServer/reload` — reload MCP server config from disk/cache, clear plugin and skill caches, and queue an MCP refresh for loaded threads (applied on each thread's next active turn); returns `{}`. Use this after editing `config.toml` or local plugin state without restarting the server.
- `mcpServerStatus/list` — enumerate configured MCP servers with their tools and auth status, plus resources/resource templates for `full` detail; supports cursor+limit pagination. If `detail` is omitted, the server defaults to `full`.
- `mcpServer/resource/read` — read a resource from a configured MCP server by optional `threadId`, `server`, and `uri`, returning text/blob resource `contents`. If `threadId` is omitted, the server reads from the latest MCP config directly.
- `mcpServer/tool/call` — call a tool on a thread's configured MCP server by `threadId`, `server`, `tool`, optional `arguments`, and optional `_meta`, returning the MCP tool result.

View File

@@ -9,10 +9,16 @@ use std::io;
use std::sync::Arc;
use tracing::warn;
fn clear_plugin_related_caches(thread_manager: &ThreadManager) {
thread_manager.plugins_manager().clear_cache();
thread_manager.skills_manager().clear_cache();
}
pub(crate) async fn queue_strict_refresh(
thread_manager: &Arc<ThreadManager>,
config_manager: &ConfigManager,
) -> io::Result<()> {
clear_plugin_related_caches(thread_manager.as_ref());
config_manager
.load_latest_config(/*fallback_cwd*/ None)
.await?;
@@ -36,6 +42,7 @@ pub(crate) async fn queue_best_effort_refresh(
thread_manager: &Arc<ThreadManager>,
config_manager: &ConfigManager,
) {
clear_plugin_related_caches(thread_manager.as_ref());
for thread_id in thread_manager.list_thread_ids().await {
let thread = match thread_manager.get_thread(thread_id).await {
Ok(thread) => thread,

View File

@@ -22,6 +22,7 @@ anyhow = { workspace = true }
clap = { workspace = true, features = ["derive"] }
clap_complete = { workspace = true }
codex-app-server = { workspace = true }
codex-app-server-client = { workspace = true }
codex-app-server-daemon = { workspace = true }
codex-app-server-protocol = { workspace = true }
codex-app-server-test-client = { workspace = true }

View File

@@ -957,6 +957,12 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
prepend_config_flags(&mut marketplace_cli.config_overrides, config_overrides);
marketplace_cli.run().await?;
}
PluginSubcommand::Reload(args) => {
let overrides = config_overrides
.parse_overrides()
.map_err(anyhow::Error::msg)?;
plugin_cmd::run_plugin_reload(overrides, args).await?;
}
PluginSubcommand::Remove(args) => {
let overrides = config_overrides
.parse_overrides()
@@ -2540,6 +2546,21 @@ mod tests {
assert!(matches!(cli.subcommand, Some(Subcommand::Plugin(_))));
}
#[test]
fn plugin_reload_parses_under_plugin() {
let cli = MultitoolCli::try_parse_from(["codex", "plugin", "reload"]).expect("parse");
assert!(matches!(cli.subcommand, Some(Subcommand::Plugin(_))));
}
#[test]
fn plugins_still_parses_as_prompt_text() {
let cli = MultitoolCli::try_parse_from(["codex", "plugins"]).expect("parse");
assert!(cli.subcommand.is_none());
assert_eq!(cli.interactive.prompt.as_deref(), Some("plugins"));
}
#[test]
fn update_parses_as_update_subcommand() {
let cli = MultitoolCli::try_parse_from(["codex", "update"]).expect("parse");

View File

@@ -1,6 +1,5 @@
use anyhow::Context;
use anyhow::Result;
use anyhow::bail;
use clap::Parser;
use codex_core::config::Config;
use codex_core::config::find_codex_home;
@@ -12,6 +11,7 @@ use codex_core_plugins::installed_marketplaces::marketplace_install_root;
use codex_core_plugins::installed_marketplaces::resolve_configured_marketplace_root;
use codex_core_plugins::marketplace::MarketplaceListError;
use codex_core_plugins::marketplace::find_marketplace_manifest_path;
use codex_core_skills::SkillsManager;
use codex_plugin::PluginId;
use codex_plugin::validate_plugin_segment;
use codex_utils_cli::CliConfigOverrides;
@@ -43,6 +43,9 @@ pub enum PluginSubcommand {
/// Add, list, upgrade, or remove configured plugin marketplaces.
Marketplace(MarketplaceCli),
/// Reload Codex's local installed-plugin state from disk/cache and refresh plugin-backed runtime state.
Reload(ReloadPluginsArgs),
/// Remove an installed plugin from local config and cache.
///
/// Pass either `PLUGIN@MARKETPLACE` or pass `PLUGIN` with
@@ -91,6 +94,14 @@ pub struct RemovePluginArgs {
marketplace_name: Option<String>,
}
#[derive(Debug, Parser)]
#[command(
bin_name = "codex plugin reload",
about = "Reload Codex's local installed-plugin state from marketplaces",
after_help = "What this does:\n Clears Codex's local plugin and skill caches so the next local load re-scans installed plugin state from disk/cache.\n\nWhat happens next:\n Updated plugin context is picked up in new CLI sessions and other fresh loads.\n Existing app-server-backed threads are not refreshed.\n Plugin instructions and skills already loaded into an existing thread are not reread."
)]
pub struct ReloadPluginsArgs;
pub async fn run_plugin_add(
overrides: Vec<(String, toml::Value)>,
args: AddPluginArgs,
@@ -203,6 +214,28 @@ pub async fn run_plugin_remove(
Ok(())
}
pub async fn run_plugin_reload(
overrides: Vec<(String, toml::Value)>,
_: ReloadPluginsArgs,
) -> Result<()> {
let config = Config::load_with_cli_overrides(overrides)
.await
.context("failed to load configuration")?;
let codex_home = find_codex_home().context("failed to resolve CODEX_HOME")?;
let plugins_manager = PluginsManager::new(codex_home.to_path_buf());
let skills_manager = SkillsManager::new(codex_home, config.bundled_skills_enabled());
plugins_manager.clear_cache();
skills_manager.clear_cache();
println!("Cleared Codex's local plugin and skill caches.");
println!("Updated plugin context will be picked up in new CLI sessions and other fresh loads.");
println!("Existing app-server-backed threads are not refreshed.");
println!("Plugin instructions and skills already loaded into an existing thread stay as-is.");
Ok(())
}
struct PluginCommandContext {
codex_home: PathBuf,
plugins_input: PluginsConfigInput,

View File

@@ -2,7 +2,6 @@ use anyhow::Result;
use codex_config::CONFIG_TOML_FILE;
use codex_config::MarketplaceConfigUpdate;
use codex_config::record_user_marketplace;
use predicates::prelude::PredicateBooleanExt;
use predicates::str::contains;
use std::path::Path;
use tempfile::TempDir;
@@ -293,6 +292,32 @@ async fn plugin_remove_works_after_marketplace_is_removed() -> Result<()> {
Ok(())
}
#[tokio::test]
async fn plugin_reload_command_help_uses_singular_plugin_name() -> Result<()> {
let codex_home = TempDir::new()?;
codex_command(codex_home.path())?
.args(["plugin", "reload", "--help"])
.assert()
.success()
.stdout(contains("Usage: codex plugin reload"))
.stdout(contains(
"Reload Codex's local installed-plugin state from marketplaces",
))
.stdout(contains("Clears Codex's local plugin and skill caches"))
.stdout(contains(
"Updated plugin context is picked up in new CLI sessions and other fresh loads",
))
.stdout(contains(
"Existing app-server-backed threads are not refreshed",
))
.stdout(contains(
"Plugin instructions and skills already loaded into an existing thread are not reread",
));
Ok(())
}
#[tokio::test]
async fn plugin_add_rejects_cached_plugins_without_authorizing_marketplace_snapshot() -> Result<()>
{

View File

@@ -414,6 +414,10 @@ impl Session {
self.services.agent_control.session_id()
}
pub(crate) fn plugins_manager(&self) -> Arc<PluginsManager> {
Arc::clone(&self.services.plugins_manager)
}
#[instrument(name = "session_init", level = "info", skip_all)]
#[allow(clippy::too_many_arguments)]
#[expect(

View File

@@ -15,6 +15,8 @@ pub(crate) mod multi_agents_spec;
pub(crate) mod multi_agents_v2;
mod plan;
pub(crate) mod plan_spec;
mod reload_plugins;
pub(crate) mod reload_plugins_spec;
mod request_permissions;
mod request_plugin_install;
pub(crate) mod request_plugin_install_spec;
@@ -59,6 +61,7 @@ pub use mcp_resource::ListMcpResourceTemplatesHandler;
pub use mcp_resource::ListMcpResourcesHandler;
pub use mcp_resource::ReadMcpResourceHandler;
pub use plan::PlanHandler;
pub use reload_plugins::ReloadPluginsHandler;
pub use request_permissions::RequestPermissionsHandler;
pub use request_plugin_install::RequestPluginInstallHandler;
pub use request_user_input::RequestUserInputHandler;

View File

@@ -0,0 +1,73 @@
use crate::function_tool::FunctionCallError;
use crate::tools::context::FunctionToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolOutput;
use crate::tools::context::ToolPayload;
use crate::tools::context::boxed_tool_output;
use crate::tools::handlers::reload_plugins_spec::RELOAD_PLUGINS_TOOL_NAME;
use crate::tools::handlers::reload_plugins_spec::create_reload_plugins_tool;
use crate::tools::registry::CoreToolRuntime;
use crate::tools::registry::ToolExecutor;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
pub struct ReloadPluginsHandler;
#[async_trait::async_trait]
impl ToolExecutor<ToolInvocation> for ReloadPluginsHandler {
fn tool_name(&self) -> ToolName {
ToolName::plain(RELOAD_PLUGINS_TOOL_NAME)
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_reload_plugins_tool())
}
async fn handle(
&self,
invocation: ToolInvocation,
) -> Result<Box<dyn ToolOutput>, FunctionCallError> {
let ToolInvocation {
payload,
session,
turn,
..
} = invocation;
match payload {
ToolPayload::Function { arguments } if arguments.trim() == "{}" => {}
ToolPayload::Function { arguments } if arguments.trim().is_empty() => {}
ToolPayload::Function { .. } => {
return Err(FunctionCallError::RespondToModel(
"reload_plugins does not accept arguments".to_string(),
));
}
_ => {
return Err(FunctionCallError::Fatal(format!(
"{RELOAD_PLUGINS_TOOL_NAME} handler received unsupported payload"
)));
}
}
session.reload_user_config_layer().await;
let config = session.get_config().await;
let mcp_config = config
.to_mcp_config(session.plugins_manager().as_ref())
.await;
session
.refresh_mcp_servers_now(
turn.as_ref(),
mcp_config.configured_mcp_servers.clone(),
mcp_config.mcp_oauth_credentials_store_mode,
Some(session.mcp_elicitation_reviewer()),
)
.await;
Ok(boxed_tool_output(FunctionToolOutput::from_text(
"{\"reloaded\":true}".to_string(),
Some(true),
)))
}
}
impl CoreToolRuntime for ReloadPluginsHandler {}

View File

@@ -0,0 +1,33 @@
use codex_tools::JsonSchema;
use codex_tools::ResponsesApiTool;
use codex_tools::ToolSpec;
use std::collections::BTreeMap;
pub const RELOAD_PLUGINS_TOOL_NAME: &str = "reload_plugins";
pub fn create_reload_plugins_tool() -> ToolSpec {
ToolSpec::Function(ResponsesApiTool {
name: RELOAD_PLUGINS_TOOL_NAME.to_string(),
description: "Reload local plugin configuration, clear plugin and skill caches, and rebuild MCP tools from the refreshed plugin state for the current turn. Plugin instructions or skills already injected into an older thread remain unchanged; use a new thread to pick up newly loaded plugin context.".to_string(),
strict: false,
defer_loading: None,
parameters: JsonSchema::object(BTreeMap::new(), Some(Vec::new()), Some(false.into())),
output_schema: None,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reload_plugins_tool_has_no_arguments() {
let ToolSpec::Function(tool) = create_reload_plugins_tool() else {
panic!("reload_plugins should be a function tool");
};
assert_eq!(tool.name, RELOAD_PLUGINS_TOOL_NAME);
assert_eq!(tool.parameters.required, Some(Vec::new()));
assert_eq!(tool.parameters.properties, Some(BTreeMap::new()));
}
}

View File

@@ -16,6 +16,7 @@ use crate::tools::handlers::ListMcpResourcesHandler;
use crate::tools::handlers::McpHandler;
use crate::tools::handlers::PlanHandler;
use crate::tools::handlers::ReadMcpResourceHandler;
use crate::tools::handlers::ReloadPluginsHandler;
use crate::tools::handlers::RequestPermissionsHandler;
use crate::tools::handlers::RequestPluginInstallHandler;
use crate::tools::handlers::RequestUserInputHandler;
@@ -45,6 +46,7 @@ use crate::tools::handlers::multi_agents_v2::ListAgentsHandler as ListAgentsHand
use crate::tools::handlers::multi_agents_v2::SendMessageHandler as SendMessageHandlerV2;
use crate::tools::handlers::multi_agents_v2::SpawnAgentHandler as SpawnAgentHandlerV2;
use crate::tools::handlers::multi_agents_v2::WaitAgentHandler as WaitAgentHandlerV2;
use crate::tools::handlers::reload_plugins_spec::RELOAD_PLUGINS_TOOL_NAME;
use crate::tools::handlers::view_image_spec::ViewImageToolOptions;
use crate::tools::hosted_spec::WebSearchToolOptions;
use crate::tools::hosted_spec::create_image_generation_tool;
@@ -435,6 +437,14 @@ fn collect_tool_executors(
executors.push(Arc::new(TestSyncHandler));
}
if config
.experimental_supported_tools
.iter()
.any(|tool| tool == RELOAD_PLUGINS_TOOL_NAME)
{
executors.push(Arc::new(ReloadPluginsHandler));
}
if config.environment_mode.has_environment() {
let include_environment_id =
matches!(config.environment_mode, ToolEnvironmentMode::Multiple);

View File

@@ -14,6 +14,7 @@ use crate::tools::handlers::multi_agents_spec::create_spawn_agent_tool_v2;
use crate::tools::handlers::multi_agents_spec::create_wait_agent_tool_v1;
use crate::tools::handlers::multi_agents_spec::create_wait_agent_tool_v2;
use crate::tools::handlers::plan_spec::create_update_plan_tool;
use crate::tools::handlers::reload_plugins_spec::RELOAD_PLUGINS_TOOL_NAME;
use crate::tools::handlers::request_user_input_spec::REQUEST_USER_INPUT_TOOL_NAME;
use crate::tools::handlers::request_user_input_spec::create_request_user_input_tool;
use crate::tools::handlers::request_user_input_spec::request_user_input_tool_description;
@@ -1392,6 +1393,36 @@ fn test_test_model_info_includes_sync_tool() {
assert!(tools.iter().any(|tool| tool.name() == "test_sync_tool"));
}
#[test]
fn test_model_info_includes_reload_plugins_tool() {
let mut model_info = model_info();
model_info.experimental_supported_tools = vec![RELOAD_PLUGINS_TOOL_NAME.to_string()];
let features = Features::with_defaults();
let available_models = Vec::new();
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
available_models: &available_models,
features: &features,
image_generation_tool_auth_allowed: true,
web_search_mode: Some(WebSearchMode::Cached),
session_source: SessionSource::Cli,
permission_profile: &PermissionProfile::Disabled,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
});
let (tools, _) = build_specs(
&tools_config,
/*mcp_tools*/ None,
/*deferred_mcp_tools*/ None,
&[],
);
assert!(
tools
.iter()
.any(|tool| tool.name() == RELOAD_PLUGINS_TOOL_NAME)
);
}
#[test]
fn test_build_specs_mcp_tools_converted() {
let model_info = model_info();

View File

@@ -685,6 +685,12 @@ impl App {
self.refresh_in_memory_config_from_disk().await?;
Ok(true)
}
AppCommand::ReloadPlugins => {
app_server.reload_user_config().await?;
app_server.reload_mcp_servers().await?;
self.refresh_in_memory_config_from_disk().await?;
Ok(true)
}
AppCommand::OverrideTurnContext { .. } => Ok(true),
AppCommand::ApproveGuardianDeniedAction { event } => {
app_server

View File

@@ -88,6 +88,7 @@ pub(crate) enum AppCommand {
response: RequestPermissionsResponse,
},
ReloadUserConfig,
ReloadPlugins,
ListSkills {
cwds: Vec<PathBuf>,
force_reload: bool,
@@ -243,6 +244,10 @@ impl AppCommand {
Self::ReloadUserConfig
}
pub(crate) fn reload_plugins() -> Self {
Self::ReloadPlugins
}
pub(crate) fn list_skills(cwds: Vec<PathBuf>, force_reload: bool) -> Self {
Self::ListSkills { cwds, force_reload }
}

View File

@@ -30,6 +30,7 @@ use codex_app_server_protocol::GetAccountRateLimitsResponse;
use codex_app_server_protocol::GetAccountResponse;
use codex_app_server_protocol::JSONRPCErrorError;
use codex_app_server_protocol::LogoutAccountResponse;
use codex_app_server_protocol::McpServerRefreshResponse;
use codex_app_server_protocol::MemoryResetResponse;
use codex_app_server_protocol::Model as ApiModel;
use codex_app_server_protocol::ModelListParams;
@@ -922,6 +923,19 @@ impl AppServerSession {
Ok(())
}
pub(crate) async fn reload_mcp_servers(&mut self) -> Result<()> {
let request_id = self.next_request_id();
let _: McpServerRefreshResponse = self
.client
.request_typed(ClientRequest::McpServerRefresh {
request_id,
params: None,
})
.await
.wrap_err("config/mcpServer/reload failed in TUI")?;
Ok(())
}
pub(crate) async fn thread_realtime_start(
&mut self,
thread_id: ThreadId,

View File

@@ -74,7 +74,10 @@ pub(crate) fn builtins_for_input(flags: BuiltinCommandFlags) -> Vec<(&'static st
.filter(|(_, cmd)| flags.allow_elevate_sandbox || *cmd != SlashCommand::ElevateSandbox)
.filter(|(_, cmd)| flags.collaboration_modes_enabled || *cmd != SlashCommand::Plan)
.filter(|(_, cmd)| flags.connectors_enabled || *cmd != SlashCommand::Apps)
.filter(|(_, cmd)| flags.plugins_command_enabled || *cmd != SlashCommand::Plugins)
.filter(|(_, cmd)| {
flags.plugins_command_enabled
|| !matches!(cmd, SlashCommand::Plugins | SlashCommand::ReloadPlugins)
})
.filter(|(_, cmd)| flags.goal_command_enabled || *cmd != SlashCommand::Goal)
.filter(|(_, cmd)| flags.personality_command_enabled || *cmd != SlashCommand::Personality)
.filter(|(_, cmd)| flags.realtime_conversation_enabled || *cmd != SlashCommand::Realtime)

View File

@@ -420,6 +420,16 @@ impl ChatWidget {
SlashCommand::Plugins => {
self.add_plugins_output();
}
SlashCommand::ReloadPlugins => {
self.add_info_message(
"Plugin reload requested.".to_string(),
Some(
"Active threads will refresh MCP tools on the next turn. Skills already loaded into existing threads are not reread.".to_string(),
),
);
self.submit_op(AppCommand::reload_plugins());
self.refresh_skills_for_current_cwd(/*force_reload*/ true);
}
SlashCommand::Rollout => {
if let Some(path) = self.rollout_path() {
self.add_info_message(
@@ -935,6 +945,7 @@ impl ChatWidget {
| SlashCommand::Mcp
| SlashCommand::Apps
| SlashCommand::Plugins
| SlashCommand::ReloadPlugins
| SlashCommand::Rollout
| SlashCommand::Copy
| SlashCommand::Raw

View File

@@ -0,0 +1,6 @@
---
source: tui/src/chatwidget/tests/slash_commands.rs
assertion_line: 1752
expression: rendered
---
• Plugins reload requested. Loaded threads queue a best-effort MCP tools refresh on the next turn; plugin instructions and skills already loaded into an older thread stay as-is, so updated plugin context is visible in new threads.

View File

@@ -1737,6 +1737,53 @@ async fn slash_mcp_requests_inventory_via_app_server() {
assert!(op_rx.try_recv().is_err(), "expected no core op to be sent");
}
#[tokio::test]
async fn slash_reload_plugins_requests_reload_and_explains_thread_boundary() {
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.dispatch_command(SlashCommand::ReloadPlugins);
let cells = drain_insert_history(&mut rx);
let rendered = cells
.iter()
.map(|cell| lines_to_single_string(cell))
.collect::<Vec<_>>()
.join("\n");
assert_chatwidget_snapshot!("slash_reload_plugins_info_message", rendered);
assert!(
rendered.contains("Plugins reload requested."),
"expected reload request message, got: {rendered:?}"
);
assert_matches!(op_rx.try_recv(), Ok(Op::ReloadPlugins));
assert_matches!(
op_rx.try_recv(),
Ok(Op::ListSkills {
force_reload: true,
..
})
);
}
#[tokio::test]
async fn slash_reload_plugins_is_disabled_while_task_running() {
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.bottom_pane.set_task_running(/*running*/ true);
chat.dispatch_command(SlashCommand::ReloadPlugins);
let cells = drain_insert_history(&mut rx);
let rendered = cells
.iter()
.map(|cell| lines_to_single_string(cell))
.collect::<Vec<_>>()
.join("\n");
assert!(
rendered.contains("'/reload-plugins' is disabled while a task is in progress."),
"expected disabled-command message, got: {rendered:?}"
);
assert!(op_rx.try_recv().is_err(), "expected no core op to be sent");
}
#[tokio::test]
async fn slash_mcp_verbose_requests_full_inventory_via_app_server() {
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;

View File

@@ -52,6 +52,7 @@ pub enum SlashCommand {
Mcp,
Apps,
Plugins,
ReloadPlugins,
Logout,
Quit,
Exit,
@@ -128,6 +129,7 @@ impl SlashCommand {
SlashCommand::Mcp => "list configured MCP tools; use /mcp verbose for details",
SlashCommand::Apps => "manage apps",
SlashCommand::Plugins => "browse plugins",
SlashCommand::ReloadPlugins => "reload plugins and configured MCP servers",
SlashCommand::Logout => "log out of Codex",
SlashCommand::Rollout => "print the rollout file path",
SlashCommand::TestApproval => "test approval request",
@@ -193,6 +195,7 @@ impl SlashCommand {
| SlashCommand::Plan
| SlashCommand::Clear
| SlashCommand::Logout
| SlashCommand::ReloadPlugins
| SlashCommand::MemoryDrop
| SlashCommand::MemoryUpdate => false,
SlashCommand::Diff
@@ -268,6 +271,17 @@ mod tests {
assert_eq!(SlashCommand::from_str("pet"), Ok(SlashCommand::Pets));
}
#[test]
fn reload_plugins_command_uses_expected_name() {
assert_eq!(SlashCommand::ReloadPlugins.command(), "reload-plugins");
assert_eq!(
SlashCommand::from_str("reload-plugins"),
Ok(SlashCommand::ReloadPlugins)
);
assert!(!SlashCommand::ReloadPlugins.available_during_task());
assert!(!SlashCommand::ReloadPlugins.supports_inline_args());
}
#[test]
fn certain_commands_are_available_during_task() {
assert!(SlashCommand::Goal.available_during_task());