-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Slash commands #5718
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Slash commands #5718
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR implements slash commands functionality, allowing users to assign custom slash commands (like /myrecipe) to recipes for quick execution in chat. The implementation also includes significant refactoring of the scheduling system to better integrate with recipe management.
Key changes:
- New slash command system that stores command-to-recipe mappings in the config
- Updated recipe manifest to include both
schedule_cronandslash_commandfields - Refactored scheduler to support direct recipe scheduling without copying files
- UI updates to manage slash commands and schedules directly from the recipes view
Reviewed Changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| ui/desktop/src/recipe/recipe_management.ts | Updated to use new RecipeManifest type instead of RecipeManifestResponse |
| ui/desktop/src/components/recipes/RecipesView.tsx | Added slash command dialog, schedule management UI, and visual indicators for recipes with commands/schedules |
| ui/desktop/src/components/recipes/CreateEditRecipeModal.tsx | Removed inline schedule creation modal (moved to recipes view) |
| ui/desktop/src/api/types.gen.ts | Generated types for RecipeManifest, ScheduleRecipeRequest, and SetSlashCommandRequest |
| ui/desktop/src/api/sdk.gen.ts | Generated SDK functions for scheduleRecipe and setRecipeSlashCommand endpoints |
| ui/desktop/openapi.json | OpenAPI schema updates for new endpoints and types |
| crates/goose/src/slash_commands.rs | New module implementing slash command storage, lookup, and recipe resolution |
| crates/goose/src/scheduler_trait.rs | Updated trait to support recipe scheduling and optional file copying |
| crates/goose/src/scheduler.rs | Implemented schedule_recipe method and refactored to support in-place recipe scheduling |
| crates/goose/src/recipe/local_recipes.rs | Minor code cleanup (std::env to env) |
| crates/goose/src/lib.rs | Added slash_commands module export |
| crates/goose/src/execution/manager.rs | Changed scheduler() to return Arc directly instead of Result |
| crates/goose/src/agents/schedule_tool.rs | Updated calls to pass make_copy parameter to scheduler methods |
| crates/goose/src/agents/agent.rs | Implemented slash command resolution in message handling |
| crates/goose-server/src/state.rs | Updated scheduler() method signature |
| crates/goose-server/src/routes/schedule.rs | Updated all scheduler calls to use new signature |
| crates/goose-server/src/routes/recipe_utils.rs | Renamed RecipeManifestWithPath to RecipeManifest and added schedule/slash command fields |
| crates/goose-server/src/routes/recipe.rs | Added schedule_recipe and set_recipe_slash_command endpoints, updated list to populate new fields |
| crates/goose-server/src/openapi.rs | Registered new endpoints and schemas in OpenAPI documentation |
| <input | ||
| type="text" | ||
| value={slashCommand} | ||
| onChange={(e) => setSlashCommand(e.target.value)} | ||
| placeholder="command-name" | ||
| className="flex-1 px-3 py-2 border rounded text-sm" | ||
| /> |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The slash command input field lacks validation. Users could enter invalid characters (spaces, special characters), empty strings after trimming, or excessively long commands. Consider adding validation to restrict input to alphanumeric characters and hyphens, and enforce a reasonable length limit.
| ) | ||
| .await?; | ||
| } else { | ||
| SessionManager::add_message(&session_config.id, &user_message).await?; |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a slash command is used but doesn't match any recipe (recipe is None), the user message is still processed normally without any indication that the slash command was not recognized. Consider providing feedback to the user when they use an invalid slash command.
| SessionManager::add_message(&session_config.id, &user_message).await?; | |
| // If the message starts with a slash but is not a recognized command, inform the user. | |
| if message_text.trim().starts_with('/') { | |
| let command = message_text.trim().split_whitespace().next().unwrap_or(""); | |
| let system_message = Message::system() | |
| .with_text(format!("Unrecognized slash command: {}", command)); | |
| SessionManager::add_message(&session_config.id, &system_message).await?; | |
| } else { | |
| SessionManager::add_message(&session_config.id, &user_message).await?; | |
| } |
| let prompt = [recipe.instructions.as_deref(), recipe.prompt.as_deref()] | ||
| .into_iter() | ||
| .flatten() | ||
| .collect::<Vec<_>>() | ||
| .join("\n\n"); | ||
| let prompt_message = Message::user() | ||
| .with_text(prompt) | ||
| .with_visibility(false, true); | ||
| SessionManager::add_message(&session_config.id, &prompt_message).await?; | ||
| SessionManager::add_message( | ||
| &session_config.id, | ||
| &user_message.with_visibility(true, false), | ||
| ) | ||
| .await?; |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If both recipe.instructions and recipe.prompt are None, an empty prompt message will be added to the session. Consider adding a check to ensure at least one is present, or handle the empty case differently.
| pub fn set_recipe_slash_command(recipe_path: PathBuf, command: Option<String>) -> Result<()> { | ||
| let recipe_path_str = recipe_path.to_string_lossy().to_string(); | ||
|
|
||
| let mut commands = list_commands(); | ||
| commands.retain(|mapping| mapping.recipe_path != recipe_path_str); | ||
|
|
||
| if let Some(cmd) = command { | ||
| let normalized_cmd = cmd.trim_start_matches('/').to_lowercase(); | ||
| if !normalized_cmd.is_empty() { | ||
| commands.push(SlashCommandMapping { | ||
| command: normalized_cmd, | ||
| recipe_path: recipe_path_str, | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| save_slash_commands(commands) | ||
| } |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function allows multiple recipes to have the same slash command, as it only removes the previous command for the same recipe path. This could cause confusion when resolving commands since get_recipe_for_command will return the first match. Consider adding validation to prevent duplicate slash commands across different recipes.
|
It's good, but just have a thought around this, as we're soon going to have the same slash commands same as CLI in the desktop app, based on the changes/featues requested in this issue: #3919 This implementation is good, but maybe it's not the best way to call recipes and extensions as reference or to load them. I shared this earlier in the Discord discussion, it might be better if we use maybe something like this : very basic and example implementation I created way back : not sure but I think this might be better to adopt and use :) |
crates/goose/src/slash_commands.rs
Outdated
| return None; | ||
| } | ||
| let recipe_content = std::fs::read_to_string(&recipe_path).ok()?; | ||
| let recipe: Recipe = serde_yaml::from_str(&recipe_content).ok()?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- could use Recipe.from_content function with consistent logic.
- this function is used in agent reply function to get the recipe from slash command and then extract the prompts and instructions. If the recipe has parameters, currently the parameters won't be resolved. Shall we ask the agent to prompt users to enter the parameters?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah good points. parameters I think we should get from suggestions/autocomplete /cc @spencrmartin
maybe. custom slash commands seem pretty standard - you want to execute a command and it just expands. fuzzy search is I think a bit of a different thing where you want to add something to a conversation like a file - adding a recipe that way would insert the recipe contents in the conversation I would think? |
Yeah but I think if the recipe contents get inserted, it will get executed, not like just adding it as prompt. It’s like calling the recipe. But yeah maybe slash commands can be the way to go for now, and later we might edit it in fuzzy search to execute directly after selecting it after seeing the response if needed. I was thinking of it like: if I want to call the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
Copilot reviewed 28 out of 28 changed files in this pull request and generated 8 comments.
| const isSlashCommand = text.startsWith('/'); | ||
| const beforeCursor = text.slice(0, cursorPosition); | ||
| const lastAtIndex = beforeCursor.lastIndexOf('@'); | ||
| const lastAtIndex = isSlashCommand ? 0 : beforeCursor.lastIndexOf('@'); |
Copilot
AI
Nov 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Slash command detection is incorrect. When isSlashCommand is true, lastAtIndex is set to 0, but the logic below expects it to be the position of '@'. This causes the mention popover to open incorrectly. Should check text.startsWith('/') and handle separately, or set lastAtIndex to -1 when it's a slash command that doesn't have '@'.
| const lastAtIndex = isSlashCommand ? 0 : beforeCursor.lastIndexOf('@'); | |
| const lastAtIndex = beforeCursor.lastIndexOf('@'); |
| const displayItem = displayItems[index]; | ||
| onSelect( | ||
| ['Builtin', 'Recipe'].includes(displayItem.itemType) | ||
| ? '/' + displayItem.name |
Copilot
AI
Nov 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This prepends '/' to the command name, but the input already starts with '/' for slash commands. This will result in '//' being inserted. The slash should not be added here since it's already part of the input.
| ? '/' + displayItem.name | |
| ? (displayItem.name.startsWith('/') ? displayItem.name : '/' + displayItem.name) |
| let is_manual_compact = MANUAL_COMPACT_TRIGGERS.contains(&message_text.trim()); | ||
|
|
||
| let slash_command_recipe = if message_text.trim().starts_with('/') { | ||
| let command = message_text.split_whitespace().next(); |
Copilot
AI
Nov 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The command extracted by split_whitespace().next() will include the leading '/', but resolve_slash_command expects just the command name (it does trim_start_matches('/') internally). This works but relies on resolve_slash_command to strip it. More fragile than extracting the command without '/' here.
| let command = message_text.split_whitespace().next(); | |
| let command = message_text.split_whitespace().next().map(|c| c.trim_start_matches('/')); |
| pub struct PricingQuery { | ||
| /// If true, only return pricing for configured providers. If false, return all. | ||
| pub configured_only: Option<bool>, | ||
| pub configured_only: bool, |
Copilot
AI
Nov 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed from Option<bool> to bool, but this is a breaking change for API consumers who may not provide this field. Should remain Option<bool> with a default, or use #[serde(default)] to provide backwards compatibility.
| pub configured_only: bool, | |
| #[serde(default)] | |
| pub configured_only: Option<bool>, |
| const hasExtension = item.includes('.'); | ||
| const ext = item.split('.').pop()?.toLowerCase(); | ||
| const commonExtensions = [ | ||
| // Code items |
Copilot
AI
Nov 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The comments say 'Code items' (line 204) but the list includes documentation files like 'txt' and 'md'. Should either fix the comment to say 'Code and documentation files' or restructure the grouping.
| // Code items | |
| // Common code, documentation, config, and asset file extensions |
| let prompt = [recipe.instructions.as_deref(), recipe.prompt.as_deref()] | ||
| .into_iter() | ||
| .flatten() | ||
| .collect::<Vec<_>>() | ||
| .join("\n\n"); |
Copilot
AI
Nov 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] Creates an intermediate Vec just to join strings. More efficient to use filter_map or collect into a String directly: [recipe.instructions.as_deref(), recipe.prompt.as_deref()].into_iter().flatten().collect::<Vec<_>>().join(\"\\n\\n\") doesn't need the intermediate Vec allocation.
| </div> | ||
| </> | ||
| )} | ||
| {!isLoading && displayItems.length === 0 && query && ( |
Copilot
AI
Nov 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This negation always evaluates to true.
| </div> | ||
| )} | ||
|
|
||
| {!isLoading && displayItems.length === 0 && !query && ( |
Copilot
AI
Nov 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This negation always evaluates to true.
alexhancock
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, and a great starting point for slash commands
|
|
||
| #[utoipa::path( | ||
| get, | ||
| path = "/config/slash_commands", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like this might be more natural to have its own set of routes vs being under config. I think of config_managment.rs routes as just about changing settings, but with this change slash commands are becoming a feature of their own. Similar to how recipes have their own route file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe? this does just write something into the settings though. it is more similar to extensions and providers; sure they are their own features, but in terms of routes here we expose how to change it in the config file.
recipes are stored in their own folder(s).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
Copilot reviewed 28 out of 28 changed files in this pull request and generated 5 comments.
| </div> | ||
| )} | ||
| {slash_command && ( | ||
| <div className="flex items-center text-purple-600 dark:text-purple-400"> |
Copilot
AI
Nov 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] Missing icon for the slash command display. The Terminal icon is used for the button, but the actual slash command shown in line 338 (/{slash_command}) should probably also have a Terminal icon like the schedule has a Clock icon, for visual consistency.
| <div className="flex items-center text-purple-600 dark:text-purple-400"> | |
| <div className="flex items-center text-purple-600 dark:text-purple-400"> | |
| <Terminal className="w-3 h-3 mr-1" /> |
| let existing_job_id = { | ||
| let jobs_guard = self.jobs.lock().await; | ||
| jobs_guard | ||
| .iter() | ||
| .find(|(_, (_, job))| job.source == recipe_path_str) | ||
| .map(|(id, _)| id.clone()) | ||
| }; | ||
|
|
||
| match cron_schedule { | ||
| Some(cron) => { | ||
| if let Some(job_id) = existing_job_id { | ||
| self.update_schedule(&job_id, cron).await | ||
| } else { | ||
| let job_id = self.generate_unique_job_id(&recipe_path).await; | ||
| let job = ScheduledJob { | ||
| id: job_id, | ||
| source: recipe_path_str, | ||
| cron, | ||
| last_run: None, | ||
| currently_running: false, | ||
| paused: false, | ||
| current_session_id: None, | ||
| process_start_time: None, | ||
| }; | ||
| self.add_scheduled_job(job, false).await | ||
| } | ||
| } | ||
| None => { | ||
| if let Some(job_id) = existing_job_id { | ||
| self.remove_scheduled_job(&job_id, false).await | ||
| } else { | ||
| Ok(()) | ||
| } | ||
| } | ||
| } | ||
| } |
Copilot
AI
Nov 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential race condition: schedule_recipe finds an existing job, then calls update_schedule or remove_scheduled_job, but another operation could modify/remove the job between the check and the action. Consider holding the lock across the entire operation or handling the case where the job no longer exists.
| <input | ||
| type="text" | ||
| value={slashCommand} | ||
| onChange={(e) => setSlashCommand(e.target.value)} | ||
| placeholder="command-name" | ||
| className="flex-1 px-3 py-2 border rounded text-sm" | ||
| /> |
Copilot
AI
Nov 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing input validation for slash command names. Users can enter invalid characters (spaces, special characters, etc.) or conflicting names (like "compact" which is a builtin). Consider adding validation to ensure command names are alphanumeric/hyphen/underscore only and don't conflict with builtin commands.
| commands.push(SlashCommand { | ||
| command: "compact".to_string(), | ||
| help: "Compact the current conversation to save tokens".to_string(), | ||
| command_type: CommandType::Builtin, | ||
| }); |
Copilot
AI
Nov 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing builtin slash commands in the API response. The get_slash_commands endpoint only includes /compact but not /summarize, even though both are defined in MANUAL_COMPACT_TRIGGERS. This inconsistency could confuse users about which builtin commands are available.
| </div> | ||
| </> | ||
| )} | ||
| {!isLoading && displayItems.length === 0 && query && ( |
Copilot
AI
Nov 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This negation always evaluates to true.
| {!isLoading && displayItems.length === 0 && query && ( | |
| {displayItems.length === 0 && query && ( |
* main: (48 commits) [fix] generic check for gemini compat (#5842) Add scheduler to diagnostics (#5849) Cors and token (#5850) fix sessions coming back with empty messages (#5841) markdown export from URL (#5830) Next camp refactor live (#5706) Add out of context compaction test via error proxy (#5805) fix: Add backward compatibility for conversationCompacted message type (#5819) Add /agent/stop endpoint, make max active agents configurable (#5826) Handle 404s (#5791) Persist provider name and model config in the session (#5419) Comment out the flaky mcp callers (#5827) Slash commands (#5718) fix: remove setx calls to not permanently edit the windows shell PATH (#5821) fix: Parse maas models for gcp vertex provider (#5816) fix: support Gemini 3's thought signatures (#5806) chore: Add Adrian Cole to Maintainers (#5815) [MCP-UI] Proxy and Better Message Handling (#5487) Release 1.15.0 Document New Window menu in macOS dock (#5811) ...
Co-authored-by: Douwe Osinga <[email protected]> Signed-off-by: Blair Allan <[email protected]>
|
@DOsinga looking to add support for this into https://github.com/block/ai-rules/ |

Summary
implements slash commands.
slash-joke.mp4