refactoring: removed create recipe in cli (#11942)

This commit is contained in:
Lifei Zhou 2026-09-09 20:52:42 +00:00 • committed by GitHub
parent 925948dc23
commit 32bf18e0c2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 41 additions and 498 deletions

View file

@ -243,7 +243,6 @@ impl GooseCompleter {
"/builtin".to_string(),
"/mode".to_string(),
"/model".to_string(),
"/recipe".to_string(),
"/new".to_string(),
];
commands.extend(

View file

@ -28,7 +28,6 @@ pub enum InputResult {
EndPlan,
Clear,
New,
Recipe(Option<String>),
Compact,
ToggleFullToolOutput,
Edit(Option<String>),
@ -241,7 +240,6 @@ fn handle_slash_command(input: &str) -> Option<InputResult> {
const CMD_ENDPLAN: &str = "/endplan";
const CMD_CLEAR: &str = "/clear";
const CMD_NEW: &str = "/new";
const CMD_RECIPE: &str = "/recipe";
const CMD_COMPACT: &str = "/compact";
const CMD_SUMMARIZE_DEPRECATED: &str = "/summarize";
const CMD_EDIT: &str = "/edit";
@ -338,7 +336,6 @@ fn handle_slash_command(input: &str) -> Option<InputResult> {
s if s == CMD_ENDPLAN => Some(InputResult::EndPlan),
s if s == CMD_CLEAR => Some(InputResult::Clear),
s if s == CMD_NEW => Some(InputResult::New),
s if s.starts_with(CMD_RECIPE) => parse_recipe_command(s),
s if s == CMD_COMPACT => Some(InputResult::Compact),
// Match "/skills" exactly or "/skills " with args - avoids matching e.g. "/skillsextra"
s if s == CMD_SKILLS || s.starts_with(&format!("{CMD_SKILLS} ")) => {
@ -371,31 +368,6 @@ fn handle_slash_command(input: &str) -> Option<InputResult> {
}
}
fn parse_recipe_command(s: &str) -> Option<InputResult> {
const CMD_RECIPE: &str = "/recipe";
if s == CMD_RECIPE {
// No filepath provided, use default
return Some(InputResult::Recipe(None));
}
// Extract the filepath from the command
let filepath = s.get(CMD_RECIPE.len()..).unwrap_or("").trim();
if filepath.is_empty() {
return Some(InputResult::Recipe(None));
}
// Validate that the filepath ends with .yaml
if !filepath.to_lowercase().ends_with(".yaml") {
println!("{}", console::style("Filepath must end with .yaml").red());
return Some(InputResult::Retry);
}
// Return the filepath for validation in the handler
Some(InputResult::Recipe(Some(filepath.to_string())))
}
fn parse_prompts_command(args: &str) -> Option<InputResult> {
let parts: Vec<String> = shlex::split(args).unwrap_or_default();
@ -486,8 +458,6 @@ fn help_text() -> String {
The model is used based on $GOOSE_PLANNER_PROVIDER and $GOOSE_PLANNER_MODEL environment variables.
If no model is set, the default model is used.
/endplan - Exit plan mode and return to 'normal' goose mode.
/recipe [filepath] - Generate a recipe from the current conversation and save it to the specified filepath (must end with .yaml).
If no filepath is provided, it will be saved to ./recipe.yaml.
/compact - Compact the current conversation to reduce context length while preserving key information.
{additional_builtin_help}/status - Show session status: model, provider, mode, and token usage.
/edit [text] - Open your prompt editor to compose a message. Optionally pre-fill with text.
@ -845,26 +815,9 @@ mod tests {
}
#[test]
fn test_recipe_command() {
// Test recipe with no filepath
if let Some(InputResult::Recipe(filepath)) = handle_slash_command("/recipe") {
assert!(filepath.is_none());
} else {
panic!("Expected Recipe");
}
// Test recipe with filepath
if let Some(InputResult::Recipe(filepath)) =
handle_slash_command("/recipe /path/to/file.yaml")
{
assert_eq!(filepath, Some("/path/to/file.yaml".to_string()));
} else {
panic!("Expected recipe with filepath");
}
// Test recipe with invalid extension
let result = handle_slash_command("/recipe /path/to/file.txt");
assert!(matches!(result, Some(InputResult::Retry)));
fn recipe_command_is_not_builtin() {
assert!(handle_slash_command("/recipe").is_none());
assert!(handle_slash_command("/recipe recipe.yaml").is_none());
}
// --- should_use_editor_always tests ---

View file

@ -31,7 +31,7 @@ use goose::providers::base::Provider;
use goose::providers::base::ProviderUsage;
use goose::utils::safe_truncate;
use anyhow::{Context, Result};
use anyhow::Result;
use completion::GooseCompleter;
use goose::agents::extension::{Envs, ExtensionConfig, PLATFORM_EXTENSIONS};
use goose::agents::types::RetryConfig;
@ -807,10 +807,6 @@ impl CliSession {
history.save(editor);
self.handle_prompt_command(opts).await?;
}
InputResult::Recipe(filepath_opt) => {
history.save(editor);
self.handle_recipe(filepath_opt).await;
}
InputResult::Compact => {
history.save(editor);
self.handle_compact().await?;
@ -1299,37 +1295,6 @@ impl CliSession {
Ok(new_session_id)
}
async fn handle_recipe(&mut self, filepath_opt: Option<String>) {
println!("{}", console::style("Generating Recipe").green());
output::show_thinking();
let recipe = self
.agent
.create_recipe(&self.session_id, self.messages.clone())
.await;
output::hide_thinking();
match recipe {
Ok(recipe) => {
let filepath_str = filepath_opt.as_deref().unwrap_or("recipe.yaml");
match self.save_recipe(&recipe, filepath_str) {
Ok(path) => println!(
"{}",
console::style(format!("Saved recipe to {}", path.display())).green()
),
Err(e) => println!("{}", console::style(e).red()),
}
}
Err(e) => {
println!(
"{}: {:?}",
console::style("Failed to generate recipe").red(),
e
);
}
}
}
async fn handle_load_skills(&mut self, names: &[String]) -> Result<()> {
// NOTE: We don't validate the skill names here because the load_skill tool will
// handle that and provide feedback to the user if any skill names are invalid.
@ -2209,49 +2174,6 @@ impl CliSession {
Ok(())
}
/// Save a recipe to a file
///
/// # Arguments
/// * `recipe` - The recipe to save
/// * `filepath_str` - The path to save the recipe to
///
/// # Returns
/// * `Result<PathBuf, String>` - The path the recipe was saved to or an error message
fn save_recipe(
&self,
recipe: &goose::recipe::Recipe,
filepath_str: &str,
) -> anyhow::Result<PathBuf> {
let path_buf = PathBuf::from(filepath_str);
let mut path = path_buf.clone();
// Update the final path if it's relative
if path_buf.is_relative() {
// If the path is relative, resolve it relative to the current working directory
let cwd = std::env::current_dir().context("Failed to get current directory")?;
path = cwd.join(&path_buf);
}
// Check if parent directory exists
if let Some(parent) = path.parent() {
if !parent.exists() {
return Err(anyhow::anyhow!(
"Directory '{}' does not exist",
parent.display()
));
}
}
// Try creating the file
let file = std::fs::File::create(path.as_path())
.context(format!("Failed to create file '{}'", path.display()))?;
// Write YAML
serde_yaml::to_writer(file, recipe).context("Failed to save recipe")?;
Ok(path)
}
fn push_message(&mut self, message: Message) {
self.messages.push(message);
}

View file

@ -49,7 +49,7 @@ use crate::agents::types::{
use crate::agents::AgentEvent;
use crate::config::extensions::name_to_key;
use crate::config::permission::PermissionManager;
use crate::config::{get_enabled_extensions, Config, GooseMode};
use crate::config::{Config, GooseMode};
use crate::context_mgmt::{
check_if_compaction_needed, compact_messages, DEFAULT_COMPACTION_THRESHOLD,
};
@ -57,14 +57,12 @@ use crate::conversation::message::{
ActionRequiredData, InferenceMetadata, Message, MessageContent, MessageUsage, ProviderMetadata,
SystemNotificationType,
};
use crate::conversation::{
debug_conversation_fix, fix_conversation, merge_consecutive_messages_for_request, Conversation,
};
use crate::conversation::{debug_conversation_fix, fix_conversation, Conversation};
use crate::permission::permission_inspector::PermissionInspector;
use crate::permission::permission_judge::PermissionCheckResult;
use crate::permission::{Permission, PermissionConfirmation};
use crate::providers::base::{PermissionRouting, Provider};
use crate::recipe::{Author, Recipe, Response, Settings};
use crate::recipe::Response;
use crate::scheduler_trait::SchedulerTrait;
use crate::security::adversary_inspector::AdversaryInspector;
use crate::security::egress_inspector::EgressInspector;
@ -77,7 +75,6 @@ use crate::utils::is_token_cancelled;
use goose_providers::conversation::token_usage::{ProviderUsage, Usage};
use goose_providers::errors::ProviderError;
use goose_providers::thinking::{ThinkingEffort, ThinkingEffortSupport};
use regex::Regex;
use rmcp::model::{
CallToolRequestParams, CallToolResult, ContentBlock, ElicitationAction, ErrorCode, ErrorData,
GetPromptResult, Prompt, ProtocolVersion, Tool,
@ -4000,234 +3997,6 @@ impl Agent {
"plan.md", &context,
)?)
}
pub async fn create_recipe(
&self,
session_id: &str,
mut messages: Conversation,
) -> Result<Recipe> {
tracing::info!("Starting recipe creation with {} messages", messages.len());
let session = self
.config
.session_manager
.get_session(session_id, false)
.await?;
let extensions_info = self
.extension_manager
.get_extensions_info(&session.working_dir)
.await;
tracing::debug!("Retrieved {} extensions info", extensions_info.len());
let model_config = self.model_config_for_session(session_id).await?;
let model_name = &model_config.model_name;
tracing::debug!("Using model: {}", model_name);
let goose_mode = *self.current_goose_mode.lock().await;
let prompt_manager = self.prompt_manager.lock().await;
let system_prompt = prompt_manager
.builder()
.with_extensions(extensions_info.into_iter())
.with_goose_mode(goose_mode)
.build();
let recipe_prompt = prompt_manager.get_recipe_prompt().await;
let tools: Vec<_> = self
.extension_manager
.get_prefixed_tools(session_id, None)
.await
.map_err(|e| {
tracing::error!("Failed to get tools for recipe creation: {}", e);
e
})?
.into_iter()
.filter(super::reply_parts::is_tool_visible_to_model)
.collect();
messages = Conversation::new_unvalidated(recipe_conversation_history(&messages));
messages.push(Message::user().with_text(recipe_prompt));
let (messages, issues) = fix_conversation(messages);
if !issues.is_empty() {
issues
.iter()
.for_each(|issue| tracing::warn!(recipe.conversation.issue = issue));
}
let messages = Conversation::new_unvalidated(merge_consecutive_messages_for_request(
messages.messages().clone(),
));
tracing::debug!(
"Added recipe prompt to messages, total messages: {}",
messages.len()
);
tracing::info!("Calling provider to generate recipe content");
let provider = self.provider.lock().await;
let provider = provider.as_ref().ok_or_else(|| {
let error = anyhow!("Provider not available during recipe creation");
tracing::error!("{}", error);
error
})?;
let (result, _usage) = crate::session_context::with_session_id(
Some(session_id.to_string()),
provider.complete(&model_config, &system_prompt, messages.messages(), &tools),
)
.await
.map_err(|e| {
tracing::error!("Provider completion failed during recipe creation: {}", e);
e
})?;
let content = result.as_concat_text();
tracing::debug!(
"Provider returned content with {} characters",
content.len()
);
// the response may be contained in ```json ```, strip that before parsing json
let re = Regex::new(r"(?s)```[^\n]*\n(.*?)\n```").unwrap();
let clean_content = re
.captures(&content)
.and_then(|caps| caps.get(1).map(|m| m.as_str()))
.unwrap_or(&content)
.trim()
.to_string();
let (instructions, activities) =
if let Ok(json_content) = serde_json::from_str::<Value>(&clean_content) {
let instructions = json_content
.get("instructions")
.ok_or_else(|| anyhow!("Missing 'instructions' in json response"))?
.as_str()
.ok_or_else(|| anyhow!("instructions' is not a string"))?
.to_string();
let activities = json_content
.get("activities")
.ok_or_else(|| anyhow!("Missing 'activities' in json response"))?
.as_array()
.ok_or_else(|| anyhow!("'activities' is not an array'"))?
.iter()
.map(|act| {
act.as_str()
.map(|s| s.to_string())
.ok_or(anyhow!("'activities' array element is not a string"))
})
.collect::<Result<_, _>>()?;
(instructions, activities)
} else {
tracing::warn!("Failed to parse JSON, falling back to string parsing");
// If we can't get valid JSON, try string parsing
// Use split_once to get the content after "Instructions:".
let after_instructions = content
.split_once("instructions:")
.map(|(_, rest)| rest)
.unwrap_or(&content);
// Split once more to separate instructions from activities.
let (instructions_part, activities_text) = after_instructions
.split_once("activities:")
.unwrap_or((after_instructions, ""));
let instructions = instructions_part
.trim_end_matches(|c: char| c.is_whitespace() || c == '#')
.trim()
.to_string();
let activities_text = activities_text.trim();
// Regex to remove bullet markers or numbers with an optional dot.
let bullet_re = Regex::new(r"^[•\-*\d]+\.?\s*").expect("Invalid regex");
// Process each line in the activities section.
let activities: Vec<String> = activities_text
.lines()
.map(|line| bullet_re.replace(line, "").to_string())
.map(|s| s.trim().to_string())
.filter(|line| !line.is_empty())
.collect();
(instructions, activities)
};
let extension_configs = get_enabled_extensions();
let author = Author {
contact: std::env::var("USER")
.or_else(|_| std::env::var("USERNAME"))
.ok(),
metadata: None,
};
// Ideally we'd get the name of the provider we are using from the provider itself,
// but it doesn't know and the plumbing looks complicated.
let config = Config::global();
let provider_name: String = config
.get_goose_provider()
.expect("No provider configured. Run 'goose configure' first");
let settings = Settings {
goose_provider: Some(provider_name.clone()),
goose_model: Some(model_name.clone()),
temperature: Some(model_config.temperature.unwrap_or(0.0)),
max_turns: None,
};
tracing::debug!(
"Building recipe with {} activities and {} extensions",
activities.len(),
extension_configs.len()
);
let (title, description) =
if let Ok(json_content) = serde_json::from_str::<Value>(&clean_content) {
let title = json_content
.get("title")
.and_then(|t| t.as_str())
.unwrap_or("Custom recipe from chat")
.to_string();
let description = json_content
.get("description")
.and_then(|d| d.as_str())
.unwrap_or("a custom recipe instance from this chat session")
.to_string();
(title, description)
} else {
(
"Custom recipe from chat".to_string(),
"a custom recipe instance from this chat session".to_string(),
)
};
let recipe = Recipe::builder()
.title(title)
.description(description)
.instructions(instructions)
.activities(activities)
.extensions(extension_configs)
.settings(settings)
.author(author)
.build()
.map_err(|e| {
tracing::error!("Failed to build recipe: {}", e);
anyhow!("Recipe build failed: {}", e)
})?;
tracing::info!("Recipe creation completed successfully");
Ok(recipe)
}
}
fn recipe_conversation_history(messages: &Conversation) -> Vec<Message> {
// The recipe prompt has no turn-context instructions; drop the blocks.
messages
.agent_visible_messages()
.into_iter()
.filter(|message| !message.is_turn_context())
.collect()
}
#[cfg(test)]
@ -4318,25 +4087,6 @@ mod tests {
);
}
#[test]
fn recipe_history_excludes_turn_context_events() {
use crate::conversation::message::MessageMetadata;
let history = Conversation::new_unvalidated([
Message::user().with_text("build me a recipe"),
Message::user()
.with_text("<turn-context>cwd /repo</turn-context>")
.with_metadata(MessageMetadata::agent_only().with_turn_context()),
Message::assistant().with_text("on it"),
]);
let texts: Vec<String> = recipe_conversation_history(&history)
.iter()
.map(|message| message.as_concat_text())
.collect();
assert_eq!(texts, ["build me a recipe", "on it"]);
}
async fn tracing_test_agent_and_session() -> (Agent, Session, TempDir) {
let data_dir = TempDir::new().unwrap();
let data_path = data_dir.path().to_path_buf();

View file

@ -3,8 +3,6 @@ use chrono::DateTime;
use chrono::Utc;
use indexmap::IndexMap;
use serde::Serialize;
use serde_json::Value;
use std::collections::HashMap;
use crate::agents::{extension::ExtensionInfo, moim};
use crate::hints::load_hints::build_gitignore;
@ -266,12 +264,6 @@ impl PromptManager {
goose_mode: None,
}
}
pub async fn get_recipe_prompt(&self) -> String {
let context: HashMap<&str, Value> = HashMap::new();
prompt_template::render_template("recipe.md", &context)
.unwrap_or_else(|_| "The recipe prompt is busted. Tell the user.".to_string())
}
}
#[cfg(test)]

View file

@ -23,10 +23,6 @@ static TEMPLATE_REGISTRY: &[(&str, &str)] = &[
"subagent_system.md",
"System prompt for subagents spawned to handle specific tasks",
),
(
"recipe.md",
"Prompt for generating recipe files from conversations",
),
(
"apps_create.md",
"Prompt for generating new Goose apps based on the user instructions",

View file

@ -1,20 +0,0 @@
Based on our conversation so far, could you create:
1. A concise title (5-10 words) that captures the main topic or task
2. A brief description (1-2 sentences) that summarizes what this recipe helps with
3. A concise set of instructions (1-2 paragraphs) that describe what you've been helping with. Make the instructions generic, and higher-level so that can be re-used across various similar tasks. Pay special attention if any output styles or formats are requested (and make it clear), and note any non standard tools used or required.
4. A list of 3-5 example activities (as a few words each at most) that would be relevant to this topic
Format your response in _VALID_ json, with keys being `title`, `description`, `instructions` (string), and `activities` (array of strings).
For example, perhaps we have been discussing fruit and you might write:
{
"title": "Fruit Information Assistant",
"description": "A recipe for finding and sharing information about different types of fruit.",
"instructions": "Using web searches we find pictures of fruit, and always check what language to reply in.",
"activities": [
"Show pics of apples",
"say a random fruit",
"share a fruit fact"
]
}

View file

@ -96,7 +96,6 @@ The following default templates can be customized.
| [compaction.md](https://github.com/aaif-goose/goose/blob/main/crates/goose/src/prompts/compaction.md) | Prompt for summarizing conversation history when context limits are reached | Desktop and CLI |
| [permission_judge.md](https://github.com/aaif-goose/goose/blob/main/crates/goose/src/prompts/permission_judge.md) | Prompt for analyzing tool operations for read-only detection | Desktop and CLI |
| [plan.md](https://github.com/aaif-goose/goose/blob/main/crates/goose/src/prompts/plan.md) | Instructions for creating detailed, actionable plans with clarifying questions | CLI only |
| [recipe.md](https://github.com/aaif-goose/goose/blob/main/crates/goose/src/prompts/recipe.md) | Prompt for generating recipe files from conversations | Desktop and CLI |
| [subagent_system.md](https://github.com/aaif-goose/goose/blob/main/crates/goose/src/prompts/subagent_system.md) | System prompt for subagents spawned to handle specific tasks | Desktop and CLI |
| [tiny_model_system.md](https://github.com/aaif-goose/goose/blob/main/crates/goose/src/prompts/tiny_model_system.md) | System prompt for tiny local models using shell command emulation | CLI |
| [session_name.md](https://github.com/aaif-goose/goose/blob/main/crates/goose/src/prompts/session_name.md) | Prompt for generating short session names from conversation history | Desktop and CLI |

View file

@ -85,7 +85,7 @@ When you run a recipe using a slash command, the recipe's instructions and promp
- Slash commands accept only one [parameter](/docs/guides/recipes/recipe-reference#parameters). Any additional parameters in the recipe must have default values.
- Command names are case-insensitive (`/Bug` and `/bug` are treated as the same command).
- Command names must be unique and contain no spaces.
- You cannot use names that conflict with [built-in CLI slash commands](/docs/guides/goose-cli-commands#slash-commands) like `/recipe`, `/compact`, or `/help`.
- You cannot use names that conflict with [built-in CLI slash commands](/docs/guides/goose-cli-commands#slash-commands) like `/compact` or `/help`.
- If the recipe file is missing or invalid, the command will be treated as regular text sent to the model.
## Additional Resources

View file

@ -797,7 +797,6 @@ Once you're in an interactive session (via `goose session` or `goose run --inter
- **`/plan <message_text>`** - Enter 'plan' mode with optional message. Create a plan based on the current messages and ask user if they want to act on it
- **`/prompt <n> [--info] [key=value...]`** - Get prompt info or execute a prompt
- **`/prompts [--extension <name>]`** - List all available prompts, optionally filtered by extension
- **`/recipe [filepath]`** - Generate a recipe from the current conversation and save it to the specified filepath (must end with .yaml). If no filepath is provided, it will be saved to ./recipe.yaml
- **`/compact`** - Compact and summarize the current conversation to reduce context length while preserving key information
- **`/r`** - Toggle full tool output display (show complete tool parameters without truncation)
- **`/skills [<name>...]`** - List available skills, or load one or more skills by name

View file

@ -1,7 +1,7 @@
---
sidebar_position: 1
title: Reusable Recipes
description: "Share a goose session setup (including tools, goals, and instructions) as a reusable recipe that others can launch with a single click"
description: "Package tools, goals, and instructions as a reusable recipe that others can launch with a single click"
---
import Tabs from '@theme/Tabs';
@ -9,57 +9,27 @@ import TabItem from '@theme/TabItem';
import { PanelLeft, ChefHat, SquarePen, Link, Clock, Terminal, Share2 } from 'lucide-react';
import RecipeFields from '@site/src/components/RecipeFields';
Sometimes you finish a task in goose and realize, "Hey, this setup could be useful again." Maybe you have curated a great combination of tools, defined a clear goal, and want to preserve that flow. Or maybe you're trying to help someone else replicate what you just did without walking them through it step by step.
You can turn your current goose session into a reusable recipe that includes the tools, goals, and setup you're using right now and package it into a new Agent that others (or future you) can launch with a single click.
Recipes package tools, goals, and instructions into reusable workflows that others (or future you) can launch with a single click.
## Create Recipe
<Tabs groupId="interface">
<TabItem value="ui" label="goose Desktop" default>
Create a recipe from the current session or from a template.
<Tabs>
<TabItem value="session" label="Current Session" default>
1. While in the session you want to save as a recipe, click the <ChefHat className="inline" size={16} /> button at the bottom of the app
2. In the dialog that opens, review and edit the recipe fields as needed:
<RecipeFields />
3. When you're finished, you can:
- Click `Create Recipe` to save the recipe to your Recipe Library
- Click `Create & Run Recipe` to save and immediately run the recipe in a new session
</TabItem>
<TabItem value="new" label="Template">
1. Click the <PanelLeft className="inline" size={16} /> button in the top-left to open the sidebar
2. Click `Recipes` in the sidebar
3. Click `Create Recipe`
4. In the dialog that opens, fill in the recipe fields as needed:
<RecipeFields />
5. When you're finished, you can:
- Copy the recipe link to share the recipe with others
- Click `Save Recipe` to save the recipe to your Recipe Library
- Click `Save & Run Recipe` to save and immediately run the recipe in a new session
</TabItem>
</Tabs>
:::warning
You cannot create a recipe from an existing recipe session, but you can view or [edit the recipe](#edit-recipe).
:::
1. Click the <PanelLeft className="inline" size={16} /> button in the top-left to open the sidebar
2. Click `Recipes` in the sidebar
3. Click `Create Recipe`
4. In the dialog that opens, fill in the recipe fields as needed:
<RecipeFields />
5. When you're finished, you can:
- Copy the recipe link to share the recipe with others
- Click `Save Recipe` to save the recipe to your Recipe Library
- Click `Save & Run Recipe` to save and immediately run the recipe in a new session
</TabItem>
<TabItem value="cli" label="goose CLI">
Recipe files can be either JSON (.json) or YAML (.yaml) files. While in a [session](/docs/guides/sessions/session-management#start-session), run this command to generate a recipe.yaml file in your current directory:
```sh
/recipe
```
If you want to specify a different name, you can provide it as an argument:
```sh
/recipe my-custom-recipe.yaml
```
Create a JSON or YAML file in your editor using the recipe structure below.
<details>
<summary>recipe file structure</summary>
@ -92,10 +62,6 @@ You can turn your current goose session into a reusable recipe that includes the
For detailed descriptions and example configurations of all recipe fields, see the [Recipe Reference Guide](/docs/guides/recipes/recipe-reference).
:::warning
You cannot create a recipe from an existing recipe session - the `/recipe` command will not work.
:::
:::tip Validate Your Recipe
You should [validate your recipe](#validate-recipe) to verify that it's complete and properly formatted.
:::
@ -161,10 +127,6 @@ You can turn your current goose session into a reusable recipe that includes the
</TabItem>
</Tabs>
:::tip Customize Recipe Generation
You can customize how goose generates recipes by editing the `recipe.md` [prompt template](/docs/guides/context-engineering/prompt-templates).
:::
## Edit Recipe
<Tabs groupId="interface">
<TabItem value="ui" label="goose Desktop" default>

View file

@ -44,8 +44,8 @@ Before saving recipes, it's important to understand where they can be stored and
**Save New Recipe:**
1. To create a recipe from your chat session, see [Create Recipe](/docs/guides/recipes/session-recipes#create-recipe)
2. Once in the Recipe Editor, click `Save Recipe` to save it to your Recipe Library
1. Open `Recipes` from the sidebar and click `Create Recipe`
2. Complete the Recipe Editor, then click `Save Recipe` to save it to your Recipe Library
**Save Modified Recipe:**
@ -61,15 +61,7 @@ When you modify and save a recipe with a new name, a new recipe and new link are
</TabItem>
<TabItem value="cli" label="goose CLI">
When you [create a recipe](/docs/guides/recipes/recipe-reference), it gets saved to:
* Your working directory by default: `./recipe.yaml`
* Any path you specify: `/recipe /path/to/my-recipe.yaml`
* Local project recipes: `/recipe .goose/recipes/my-recipe.yaml`
:::note
The CLI saves recipes as `.yaml` files. While the CLI can run recipes in `.json` format, it does not provide an option to save recipes as JSON.
:::
Create recipe files with your preferred editor. Store project-specific recipes in `.goose/recipes/` and global recipes in `~/.config/goose/recipes/`. The CLI can run recipes in either YAML or JSON format.
</TabItem>
</Tabs>

View file

@ -46,7 +46,7 @@ Commit your code changes early and often. This allows you to rollback any unexpe
Administrators can use an [allowlist](/docs/guides/allowlist) to restrict goose to approved extensions only. This helps prevent risky installs from unknown MCP servers.
### Set up starter templates
You can turn a successful session into a reusable "[recipe](/docs/guides/recipes/session-recipes)" to share with others or use again later—no need to start from scratch.
Create a reusable [recipe](/docs/guides/recipes/session-recipes) for workflows you want to share or run again later.
### Embrace an experimental mindset
You don’t need to get it right the first time. Iterating on prompts and tools is part of the workflow.

View file

@ -234,7 +234,7 @@ const i18n = defineMessages({
},
noSavedRecipesDescription: {
id: 'recipesView.noSavedRecipesDescription',
defaultMessage: 'Recipe saved from chats will show up here.',
defaultMessage: 'Saved recipes will show up here.',
},
noMatchingRecipes: {
id: 'recipesView.noMatchingRecipes',

View file

@ -3159,7 +3159,7 @@
"defaultMessage": "Keine gespeicherten Rezepte"
},
"recipesView.noSavedRecipesDescription": {
"defaultMessage": "Aus Chats gespeicherte Rezepte werden hier angezeigt."
"defaultMessage": "Gespeicherte Rezepte werden hier angezeigt."
},
"recipesView.openInNewWindow": {
"defaultMessage": "In neuem Fenster öffnen"

View file

@ -3207,7 +3207,7 @@
"defaultMessage": "No saved recipes"
},
"recipesView.noSavedRecipesDescription": {
"defaultMessage": "Recipe saved from chats will show up here."
"defaultMessage": "Saved recipes will show up here."
},
"recipesView.openInNewWindow": {
"defaultMessage": "Open in new window"

View file

@ -3159,7 +3159,7 @@
"defaultMessage": "No hay recetas guardadas"
},
"recipesView.noSavedRecipesDescription": {
"defaultMessage": "Las recetas guardadas desde los chats aparecerán aquí."
"defaultMessage": "Las recetas guardadas aparecerán aquí."
},
"recipesView.openInNewWindow": {
"defaultMessage": "Abrir en una ventana nueva"

View file

@ -3159,7 +3159,7 @@
"defaultMessage": "Aucune recette enregistrée"
},
"recipesView.noSavedRecipesDescription": {
"defaultMessage": "Les recettes enregistrées à partir des conversations apparaîtront ici."
"defaultMessage": "Les recettes enregistrées apparaîtront ici."
},
"recipesView.openInNewWindow": {
"defaultMessage": "Ouvrir dans une nouvelle fenêtre"

View file

@ -3159,7 +3159,7 @@
"defaultMessage": "कोई सहेजी गई रेसिपी नहीं"
},
"recipesView.noSavedRecipesDescription": {
"defaultMessage": "चैट से सहेजी गई रेसिपी यहां दिखाई देगी."
"defaultMessage": "सहेजी गई रेसिपी यहां दिखाई देगी."
},
"recipesView.openInNewWindow": {
"defaultMessage": "नई विंडो में खोलें"

View file

@ -3159,7 +3159,7 @@
"defaultMessage": "Tidak ada resep tersimpan"
},
"recipesView.noSavedRecipesDescription": {
"defaultMessage": "Resep yang disimpan dari obrolan akan muncul di sini."
"defaultMessage": "Resep yang disimpan akan muncul di sini."
},
"recipesView.openInNewWindow": {
"defaultMessage": "Buka di jendela baru"

View file

@ -3159,7 +3159,7 @@
"defaultMessage": "Nessuna ricetta salvata"
},
"recipesView.noSavedRecipesDescription": {
"defaultMessage": "Le ricette salvate dalle chat verranno visualizzate qui."
"defaultMessage": "Le ricette salvate verranno visualizzate qui."
},
"recipesView.openInNewWindow": {
"defaultMessage": "Apri in una nuova finestra"

View file

@ -3159,7 +3159,7 @@
"defaultMessage": "保存済みのレシピはありません"
},
"recipesView.noSavedRecipesDescription": {
"defaultMessage": "チャットから保存したレシピがここに表示されます。"
"defaultMessage": "保存したレシピがここに表示されます。"
},
"recipesView.openInNewWindow": {
"defaultMessage": "新しいウィンドウで開く"

View file

@ -3159,7 +3159,7 @@
"defaultMessage": "저장된 레시피가 없습니다."
},
"recipesView.noSavedRecipesDescription": {
"defaultMessage": "채팅에서 저장된 레시피가 여기에 표시됩니다."
"defaultMessage": "저장된 레시피가 여기에 표시됩니다."
},
"recipesView.openInNewWindow": {
"defaultMessage": "새 창에서 열기"

View file

@ -3159,7 +3159,7 @@
"defaultMessage": "Tiada resipi disimpan"
},
"recipesView.noSavedRecipesDescription": {
"defaultMessage": "Resipi yang disimpan daripada sembang akan dipaparkan di sini."
"defaultMessage": "Resipi yang disimpan akan dipaparkan di sini."
},
"recipesView.openInNewWindow": {
"defaultMessage": "Buka dalam tetingkap baharu"

View file

@ -3159,7 +3159,7 @@
"defaultMessage": "Sem receitas guardadas"
},
"recipesView.noSavedRecipesDescription": {
"defaultMessage": "As receitas guardadas a partir de conversas aparecerão aqui."
"defaultMessage": "As receitas guardadas aparecerão aqui."
},
"recipesView.openInNewWindow": {
"defaultMessage": "Abrir numa nova janela"

View file

@ -3159,7 +3159,7 @@
"defaultMessage": "Нет сохраненных рецептов"
},
"recipesView.noSavedRecipesDescription": {
"defaultMessage": "Рецепты, сохраненные из чатов, появятся здесь."
"defaultMessage": "Сохраненные рецепты появятся здесь."
},
"recipesView.openInNewWindow": {
"defaultMessage": "Открыть в новом окне"

View file

@ -3159,7 +3159,7 @@
"defaultMessage": "Kayıtlı tarif yok"
},
"recipesView.noSavedRecipesDescription": {
"defaultMessage": "Sohbetlerden kaydedilen tarif burada görünecek."
"defaultMessage": "Kaydedilen tarifler burada görünecek."
},
"recipesView.openInNewWindow": {
"defaultMessage": "Yeni pencerede aç"

View file

@ -3159,7 +3159,7 @@
"defaultMessage": "Chưa có công thức nào được lưu"
},
"recipesView.noSavedRecipesDescription": {
"defaultMessage": "Các công thức được lưu từ cuộc trò chuyện sẽ hiển thị ở đây."
"defaultMessage": "Các công thức đã lưu sẽ hiển thị ở đây."
},
"recipesView.openInNewWindow": {
"defaultMessage": "Mở trong cửa sổ mới"

View file

@ -3159,7 +3159,7 @@
"defaultMessage": "暂无已保存的配方"
},
"recipesView.noSavedRecipesDescription": {
"defaultMessage": "从聊天中保存的配方会显示在这里。"
"defaultMessage": "已保存的配方会显示在这里。"
},
"recipesView.openInNewWindow": {
"defaultMessage": "在新窗口中打开"

View file

@ -3159,7 +3159,7 @@
"defaultMessage": "尚無已儲存的食譜"
},
"recipesView.noSavedRecipesDescription": {
"defaultMessage": "從聊天儲存的食譜會顯示在這裡。"
"defaultMessage": "已儲存的食譜會顯示在這裡。"
},
"recipesView.openInNewWindow": {
"defaultMessage": "在新視窗中開啟"

View file

@ -145,7 +145,6 @@ export type AnalyticsEvent =
}
| { name: 'input_mode_changed'; properties: { from_mode: string; to_mode: string } }
| { name: 'input_diagnostics_opened'; properties: Record<string, never> }
| { name: 'input_create_recipe_opened'; properties: Record<string, never> }
| { name: 'input_edit_recipe_opened'; properties: Record<string, never> }
// Auto-update tracking events
| {