diff --git a/documentation/docusaurus.config.ts b/documentation/docusaurus.config.ts
index 86ad7a4e..e83e4162 100644
--- a/documentation/docusaurus.config.ts
+++ b/documentation/docusaurus.config.ts
@@ -141,17 +141,30 @@ const config: Config = {
position: "left",
label: "Tutorials",
},
- {
- to: "/prompt-library",
- position: "left",
- label: "Prompts",
- },
- {
- to: "/extensions",
- label: "Extensions",
- position: "left",
- },
{ to: "/blog", label: "Blog", position: "left" },
+ {
+ type: 'dropdown',
+ label: 'Resources',
+ position: 'left',
+ items: [
+ {
+ to: '/extensions',
+ label: 'Extensions',
+ },
+ {
+ to: '/recipe-generator',
+ label: 'Recipe Generator',
+ },
+ {
+ to: '/prompt-library',
+ label: 'Prompt Library',
+ },
+ {
+ href: 'https://block.github.io/goose/install-link-generator/',
+ label: 'Install Link Generator',
+ },
+ ],
+ },
{
href: "https://discord.gg/block-opensource",
diff --git a/documentation/src/css/custom.css b/documentation/src/css/custom.css
index 28d8f6ac..d25d59f9 100644
--- a/documentation/src/css/custom.css
+++ b/documentation/src/css/custom.css
@@ -306,6 +306,25 @@ html[data-theme="light"] .hide-in-light {
align-items: center;
}
+/* Dropdown styles */
+.navbar__link--active {
+ color: var(--text-prominent);
+}
+
+.dropdown__menu {
+ background-color: var(--background-app);
+ border-color: var(--border-subtle);
+}
+
+.dropdown__link {
+ color: var(--text-standard);
+}
+
+.dropdown__link:hover {
+ background-color: var(--background-subtle);
+ color: var(--text-prominent);
+}
+
.iconExternalLink_nPIU {
margin-left: 8px !important;
}
diff --git a/documentation/src/pages/extensions/index.tsx b/documentation/src/pages/extensions/index.tsx
index 4818c454..d9649697 100644
--- a/documentation/src/pages/extensions/index.tsx
+++ b/documentation/src/pages/extensions/index.tsx
@@ -4,6 +4,8 @@ import type { MCPServer } from "@site/src/types/server";
import { fetchMCPServers, searchMCPServers } from "@site/src/utils/mcp-servers";
import { motion } from "framer-motion";
import Layout from "@theme/Layout";
+import Link from "@docusaurus/Link";
+import { Wand2 } from "lucide-react";
export default function HomePage() {
const [servers, setServers] = useState([]);
@@ -52,13 +54,21 @@ export default function HomePage() {
-
-
setSearchQuery(e.target.value)}
- />
+
+
+ setSearchQuery(e.target.value)}
+ />
+
+
+
+
+ Recipe Generator
+
+
{error && (
diff --git a/documentation/src/pages/recipe-generator.tsx b/documentation/src/pages/recipe-generator.tsx
new file mode 100644
index 00000000..12d73ca1
--- /dev/null
+++ b/documentation/src/pages/recipe-generator.tsx
@@ -0,0 +1,439 @@
+import React, { useState, useCallback, useMemo } from 'react';
+import Layout from "@theme/Layout";
+import { ArrowLeft, Copy, Check, Plus, X } from "lucide-react";
+import { Button } from "@site/src/components/ui/button";
+import Link from "@docusaurus/Link";
+
+export default function RecipeGenerator() {
+ // State management
+ const [title, setTitle] = useState('');
+ const [description, setDescription] = useState('');
+ const [instructions, setInstructions] = useState('');
+ const [activities, setActivities] = useState([]);
+ const [newActivity, setNewActivity] = useState('');
+ const [copied, setCopied] = useState(false);
+ const [errors, setErrors] = useState({});
+ const [outputFormat, setOutputFormat] = useState('url'); // 'url' or 'yaml'
+ const [authorContact, setAuthorContact] = useState('');
+ const [extensionsList, setExtensionsList] = useState([
+ { type: 'builtin', name: 'developer', display_name: 'Developer', timeout: 300, bundled: true, enabled: false },
+ { type: 'builtin', name: 'googledrive', display_name: 'Google Drive', timeout: 300, bundled: true, enabled: false },
+ { type: 'builtin', name: 'computercontroller', display_name: 'Computer Controller', timeout: 300, bundled: true, enabled: false },
+ { type: 'builtin', name: 'jetbrains', display_name: 'JetBrains', timeout: 300, bundled: true, enabled: false },
+ { type: 'builtin', name: 'memory', display_name: 'Memory', timeout: 300, bundled: true, enabled: false },
+ {
+ type: 'stdio',
+ name: 'pdf-reader',
+ cmd: 'uvx',
+ args: ['mcp-read-pdf@latest'],
+ envs: {},
+ env_keys: [],
+ timeout: null,
+ description: "Read and analyze PDF documents",
+ enabled: false
+ }
+ ]);
+ const [prompt, setPrompt] = useState('');
+
+ // Add activity handler
+ const handleAddActivity = useCallback(() => {
+ if (newActivity.trim()) {
+ setActivities(prev => [...prev, newActivity.trim()]);
+ setNewActivity('');
+ }
+ }, [newActivity]);
+
+ // Remove activity handler
+ const handleRemoveActivity = useCallback((index) => {
+ setActivities(prev => prev.filter((_, i) => i !== index));
+ }, []);
+
+ // Toggle extension handler
+ const toggleExtension = useCallback((index) => {
+ setExtensionsList(prev => {
+ const updated = [...prev];
+ updated[index] = { ...updated[index], enabled: !updated[index].enabled };
+ return updated;
+ });
+ }, []);
+
+ // Form validation
+ const validateForm = useCallback(() => {
+ const newErrors = {};
+
+ if (!title.trim()) {
+ newErrors.title = 'Title is required';
+ }
+ if (!description.trim()) {
+ newErrors.description = 'Description is required';
+ }
+ if (!instructions.trim()) {
+ newErrors.instructions = 'Instructions are required';
+ }
+
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ }, [title, description, instructions]);
+
+ // Generate output with useMemo to prevent re-renders
+ const recipeOutput = useMemo(() => {
+ // Only generate if we have the required fields
+ if (!title.trim() || !description.trim() || !instructions.trim()) {
+ return '';
+ }
+
+ try {
+ if (outputFormat === 'url') {
+ const recipeConfig = {
+ version: "1.0.0",
+ title,
+ description,
+ instructions,
+ activities: activities.length > 0 ? activities : undefined
+ };
+
+ // Filter out undefined values
+ Object.keys(recipeConfig).forEach(key => {
+ if (recipeConfig[key] === undefined) {
+ delete recipeConfig[key];
+ }
+ });
+
+ // Use window.btoa for browser compatibility
+ return `goose://recipe?config=${window.btoa(JSON.stringify(recipeConfig))}`;
+ } else {
+ // Generate YAML format
+ const enabledExtensions = extensionsList.filter(ext => ext.enabled);
+
+ let yaml = `version: 1.0.0
+title: ${title}
+description: ${description}
+instructions: ${instructions}
+`;
+
+ if (authorContact) {
+ yaml += `author:
+ contact: ${authorContact}
+`;
+ }
+
+ if (enabledExtensions.length > 0) {
+ yaml += `extensions:
+`;
+ for (const ext of enabledExtensions) {
+ if (ext.type === 'builtin') {
+ yaml += `- type: ${ext.type}
+ name: ${ext.name}
+ display_name: ${ext.display_name}
+ timeout: ${ext.timeout}
+ bundled: ${ext.bundled}
+`;
+ } else if (ext.type === 'stdio') {
+ yaml += `- type: ${ext.type}
+ name: ${ext.name}
+ cmd: ${ext.cmd}
+ args:
+ - ${ext.args.join('\n - ')}
+ envs: {}
+ env_keys: []
+ timeout: ${ext.timeout === null ? 'null' : ext.timeout}
+ description: ${ext.description}
+`;
+ }
+ }
+ }
+
+ if (prompt) {
+ yaml += `prompt: ${prompt}
+`;
+ }
+
+ return yaml;
+ }
+ } catch (error) {
+ console.error('Error generating recipe output:', error);
+ return '';
+ }
+ }, [title, description, instructions, activities, outputFormat, authorContact, extensionsList, prompt]);
+
+ // Copy handler
+ const handleCopy = useCallback(() => {
+ if (validateForm() && recipeOutput) {
+ navigator.clipboard.writeText(recipeOutput)
+ .then(() => {
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ })
+ .catch(err => console.error('Failed to copy output:', err));
+ }
+ }, [validateForm, recipeOutput]);
+
+ return (
+
+
+
+
+
Recipe Generator
+
+ Create a shareable Goose recipe URL that others can use to launch a session with your predefined settings.
+
+
+
+
+
Recipe Details
+
+ {/* Format Selection */}
+
+
+
+ {/* Title */}
+
+
+
setTitle(e.target.value)}
+ onBlur={validateForm}
+ className={`w-full p-3 border rounded-lg bg-bgSubtle text-textStandard ${
+ errors.title ? 'border-red-500' : 'border-borderSubtle'
+ }`}
+ placeholder="Enter a title for your recipe"
+ />
+ {errors.title &&
{errors.title}
}
+
+
+ {/* Description */}
+
+
+
setDescription(e.target.value)}
+ onBlur={validateForm}
+ className={`w-full p-3 border rounded-lg bg-bgSubtle text-textStandard ${
+ errors.description ? 'border-red-500' : 'border-borderSubtle'
+ }`}
+ placeholder="Enter a description for your recipe"
+ />
+ {errors.description &&
{errors.description}
}
+
+
+ {/* Instructions */}
+
+
+ {/* YAML-specific fields */}
+ {outputFormat === 'yaml' && (
+ <>
+
+
+ setAuthorContact(e.target.value)}
+ className="w-full p-3 border border-borderSubtle rounded-lg bg-bgSubtle text-textStandard"
+ placeholder="Enter author contact information"
+ />
+
+
+
+
+
+
+
+
+
+ {extensionsList.map((extension, index) => (
+
+ toggleExtension(index)}
+ className="mr-3"
+ />
+
+
+ ))}
+
+
+ >
+ )}
+
+ {/* Activities */}
+
+
+
+ {activities.map((activity, index) => (
+
+ {activity}
+
+
+ ))}
+
+
+
setNewActivity(e.target.value)}
+ onKeyDown={(e) => e.key === 'Enter' && e.preventDefault()}
+ onKeyPress={(e) => {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ handleAddActivity();
+ }
+ }}
+ className="flex-1 p-3 border border-borderSubtle rounded-lg bg-bgSubtle text-textStandard"
+ placeholder="Enter an activity"
+ />
+
+
+
+
+
+
+ {/* Generated Output */}
+
+
+ Generated Recipe {outputFormat === 'url' ? 'URL' : 'YAML'}
+
+
+
+
+ {recipeOutput || `Fill in the required fields to generate a ${outputFormat === 'url' ? 'URL' : 'YAML'}`}
+
+
+
+
+
+
+
+
+ {/* Instructions for Use */}
+
+
How to Use
+
+ - Fill in the required fields above to generate a recipe.
+ - Choose between URL format (for direct sharing) or YAML format (for configuration files).
+ - For URL format:
+
+ - Copy the generated URL using the "Copy URL" button.
+ - Share the URL with others who have Goose Desktop installed.
+ - When someone clicks the URL, it will open Goose Desktop with your recipe configuration.
+
+
+ - For YAML format:
+
+ - Copy the generated YAML using the "Copy YAML" button.
+ - Save it as a
.yaml file.
+ - Use with the CLI:
goose run --recipe your-recipe.yaml
+ - Or create a deeplink with:
goose recipe deeplink your-recipe.yaml
+
+
+
+
+
+
+ );
+}
\ No newline at end of file