Skip to content

Conversation

@DOsinga
Copy link
Collaborator

@DOsinga DOsinga commented Nov 13, 2025

Summary

implements slash commands.

slash-joke.mp4

Copilot AI review requested due to automatic review settings November 13, 2025 16:04
Copy link
Contributor

Copilot AI left a 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_cron and slash_command fields
  • 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

Comment on lines +613 to +619
<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"
/>
Copy link

Copilot AI Nov 13, 2025

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.

Copilot uses AI. Check for mistakes.
)
.await?;
} else {
SessionManager::add_message(&session_config.id, &user_message).await?;
Copy link

Copilot AI Nov 13, 2025

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.

Suggested change
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?;
}

Copilot uses AI. Check for mistakes.
Comment on lines +791 to +804
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?;
Copy link

Copilot AI Nov 13, 2025

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.

Copilot uses AI. Check for mistakes.
Comment on lines +36 to +53
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)
}
Copy link

Copilot AI Nov 13, 2025

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.

Copilot uses AI. Check for mistakes.
@Abhijay007
Copy link
Collaborator

Abhijay007 commented Nov 14, 2025

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
and ongoing work in this branch: https://github.com/block/goose/compare/spence+alexhancock/text-input-rebased.

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 @ with fuzzy search for calling recipes and extensions. It would feel more convenient for people coming to Goose from platforms like Zed or Cursor, since they already use@ to trigger features or reference anything in the chat, like past conversations, configs, or files etc.

maybe something like this : very basic and example implementation I created way back :

addContent

not sure but I think this might be better to adopt and use :)

return None;
}
let recipe_content = std::fs::read_to_string(&recipe_path).ok()?;
let recipe: Recipe = serde_yaml::from_str(&recipe_content).ok()?;
Copy link
Contributor

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?

Copy link
Collaborator Author

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

@DOsinga
Copy link
Collaborator Author

DOsinga commented Nov 14, 2025

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 and ongoing work in this branch: https://github.com/block/goose/compare/spence+alexhancock/text-input-rebased.

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 @ with fuzzy search for calling recipes and extensions. It would feel more convenient for people coming to Goose from platforms like Zed or Cursor, since they already use@ to trigger features or reference anything in the chat, like past conversations, configs, or files etc.

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 :)

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?

@Abhijay007
Copy link
Collaborator

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 /joke recipe, I just do @, go to recipes and search for joke, and once selected it becomes /joke in the chat, and on hitting enter it just gets executed."

Copilot AI review requested due to automatic review settings November 18, 2025 14:47
Copy link
Contributor

Copilot AI left a 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('@');
Copy link

Copilot AI Nov 18, 2025

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 '@'.

Suggested change
const lastAtIndex = isSlashCommand ? 0 : beforeCursor.lastIndexOf('@');
const lastAtIndex = beforeCursor.lastIndexOf('@');

Copilot uses AI. Check for mistakes.
const displayItem = displayItems[index];
onSelect(
['Builtin', 'Recipe'].includes(displayItem.itemType)
? '/' + displayItem.name
Copy link

Copilot AI Nov 18, 2025

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.

Suggested change
? '/' + displayItem.name
? (displayItem.name.startsWith('/') ? displayItem.name : '/' + displayItem.name)

Copilot uses AI. Check for mistakes.
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();
Copy link

Copilot AI Nov 18, 2025

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.

Suggested change
let command = message_text.split_whitespace().next();
let command = message_text.split_whitespace().next().map(|c| c.trim_start_matches('/'));

Copilot uses AI. Check for mistakes.
pub struct PricingQuery {
/// If true, only return pricing for configured providers. If false, return all.
pub configured_only: Option<bool>,
pub configured_only: bool,
Copy link

Copilot AI Nov 18, 2025

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.

Suggested change
pub configured_only: bool,
#[serde(default)]
pub configured_only: Option<bool>,

Copilot uses AI. Check for mistakes.
const hasExtension = item.includes('.');
const ext = item.split('.').pop()?.toLowerCase();
const commonExtensions = [
// Code items
Copy link

Copilot AI Nov 18, 2025

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.

Suggested change
// Code items
// Common code, documentation, config, and asset file extensions

Copilot uses AI. Check for mistakes.
Comment on lines +794 to +798
let prompt = [recipe.instructions.as_deref(), recipe.prompt.as_deref()]
.into_iter()
.flatten()
.collect::<Vec<_>>()
.join("\n\n");
Copy link

Copilot AI Nov 18, 2025

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.

Copilot uses AI. Check for mistakes.
</div>
</>
)}
{!isLoading && displayItems.length === 0 && query && (
Copy link

Copilot AI Nov 18, 2025

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.

Copilot uses AI. Check for mistakes.
</div>
)}

{!isLoading && displayItems.length === 0 && !query && (
Copy link

Copilot AI Nov 18, 2025

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.

Copilot uses AI. Check for mistakes.
@alexhancock alexhancock self-requested a review November 19, 2025 16:39
Copy link
Collaborator

@alexhancock alexhancock left a 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",
Copy link
Collaborator

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.

Copy link
Collaborator Author

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).

Copilot AI review requested due to automatic review settings November 20, 2025 18:50
Copy link
Contributor

Copilot AI left a 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">
Copy link

Copilot AI Nov 20, 2025

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.

Suggested change
<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" />

Copilot uses AI. Check for mistakes.
Comment on lines +330 to +365
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(())
}
}
}
}
Copy link

Copilot AI Nov 20, 2025

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.

Copilot uses AI. Check for mistakes.
Comment on lines +617 to +623
<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"
/>
Copy link

Copilot AI Nov 20, 2025

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.

Copilot uses AI. Check for mistakes.
Comment on lines +426 to +430
commands.push(SlashCommand {
command: "compact".to_string(),
help: "Compact the current conversation to save tokens".to_string(),
command_type: CommandType::Builtin,
});
Copy link

Copilot AI Nov 20, 2025

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.

Copilot uses AI. Check for mistakes.
</div>
</>
)}
{!isLoading && displayItems.length === 0 && query && (
Copy link

Copilot AI Nov 20, 2025

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.

Suggested change
{!isLoading && displayItems.length === 0 && query && (
{displayItems.length === 0 && query && (

Copilot uses AI. Check for mistakes.
@DOsinga DOsinga merged commit bdf68bb into main Nov 20, 2025
26 of 27 checks passed
@DOsinga DOsinga deleted the slash-commands branch November 20, 2025 20:14
michaelneale added a commit that referenced this pull request Nov 24, 2025
* 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)
  ...
BlairAllan pushed a commit to BlairAllan/goose that referenced this pull request Nov 29, 2025
Co-authored-by: Douwe Osinga <[email protected]>
Signed-off-by: Blair Allan <[email protected]>
@jonandersen
Copy link
Contributor

@DOsinga looking to add support for this into https://github.com/block/ai-rules/
is there a repo level folder we can use e.g .agents/commands ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants