diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx
index 9a81f4c97c..2e4119ef8c 100644
--- a/ui/desktop/src/App.tsx
+++ b/ui/desktop/src/App.tsx
@@ -24,7 +24,7 @@ import { TabbedPairRoute } from './components/TabbedPairRoute';
import SettingsView, { SettingsViewOptions } from './components/settings/SettingsView';
import SessionsView from './components/sessions/SessionsView';
import SharedSessionView from './components/sessions/SharedSessionView';
-import SchedulesView from './components/schedule/SchedulesView';
+import MarketplaceView from './components/marketplace/MarketplaceView';
import ProviderSettings from './components/settings/providers/ProviderSettingsPage';
import { AppLayout } from './components/Layout/AppLayout';
import { ChatProvider } from './contexts/ChatContext';
@@ -139,8 +139,7 @@ const SessionsRoute = () => {
};
const SchedulesRoute = () => {
- const navigate = useNavigate();
- return navigate('/')} />;
+ return ;
};
const RecipesRoute = () => {
diff --git a/ui/desktop/src/assets/Apps.png b/ui/desktop/src/assets/Apps.png
new file mode 100644
index 0000000000..00c1fa9192
Binary files /dev/null and b/ui/desktop/src/assets/Apps.png differ
diff --git a/ui/desktop/src/assets/GooseApp.png b/ui/desktop/src/assets/GooseApp.png
new file mode 100644
index 0000000000..582bc2a988
Binary files /dev/null and b/ui/desktop/src/assets/GooseApp.png differ
diff --git a/ui/desktop/src/assets/extensions.png b/ui/desktop/src/assets/extensions.png
new file mode 100644
index 0000000000..9c60858e9e
Binary files /dev/null and b/ui/desktop/src/assets/extensions.png differ
diff --git a/ui/desktop/src/assets/g2.png b/ui/desktop/src/assets/g2.png
new file mode 100644
index 0000000000..91d4607998
Binary files /dev/null and b/ui/desktop/src/assets/g2.png differ
diff --git a/ui/desktop/src/assets/gooseslantIOS.png b/ui/desktop/src/assets/gooseslantIOS.png
new file mode 100644
index 0000000000..fd41b3d50e
Binary files /dev/null and b/ui/desktop/src/assets/gooseslantIOS.png differ
diff --git a/ui/desktop/src/assets/peeps.png b/ui/desktop/src/assets/peeps.png
new file mode 100644
index 0000000000..a554bfae89
Binary files /dev/null and b/ui/desktop/src/assets/peeps.png differ
diff --git a/ui/desktop/src/assets/sellers.png b/ui/desktop/src/assets/sellers.png
new file mode 100644
index 0000000000..f70fae1b82
Binary files /dev/null and b/ui/desktop/src/assets/sellers.png differ
diff --git a/ui/desktop/src/components/GooseSidebar/AppSidebar.tsx b/ui/desktop/src/components/GooseSidebar/AppSidebar.tsx
index 1076f6f0a2..260454fd08 100644
--- a/ui/desktop/src/components/GooseSidebar/AppSidebar.tsx
+++ b/ui/desktop/src/components/GooseSidebar/AppSidebar.tsx
@@ -73,9 +73,9 @@ const menuItems: NavigationEntry[] = [
{
type: 'item',
path: '/schedules',
- label: 'Scheduler',
+ label: 'Marketplace',
icon: Clock,
- tooltip: 'Manage scheduled runs',
+ tooltip: 'Browse marketplace',
},
{
type: 'item',
diff --git a/ui/desktop/src/components/Layout/TopNavigation.tsx b/ui/desktop/src/components/Layout/TopNavigation.tsx
index c23afc6524..be72d1b0a3 100644
--- a/ui/desktop/src/components/Layout/TopNavigation.tsx
+++ b/ui/desktop/src/components/Layout/TopNavigation.tsx
@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
-import { Home, History, FileText, Clock, Puzzle, Settings as SettingsIcon, GripVertical, Users, Hash } from 'lucide-react';
+import { Home, History, FileText, Clock, Puzzle, Settings as SettingsIcon, GripVertical, Users, Hash, ShoppingBag } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { ChatSmart } from '../icons';
import { listSessions, getSessionInsights } from '../../api';
@@ -258,10 +258,8 @@ export const TopNavigation: React.FC = ({ isExpanded, setIsE
{
id: 'scheduler',
path: '/schedules',
- label: 'Scheduler',
- icon: Clock,
- getTag: () => `${scheduledTodayCount} today`,
- tagAlign: 'left',
+ label: 'Marketplace',
+ icon: ShoppingBag,
},
{
id: 'extensions',
diff --git a/ui/desktop/src/components/marketplace/MarketplaceView.tsx b/ui/desktop/src/components/marketplace/MarketplaceView.tsx
new file mode 100644
index 0000000000..d38d9445d0
--- /dev/null
+++ b/ui/desktop/src/components/marketplace/MarketplaceView.tsx
@@ -0,0 +1,1194 @@
+import React, { useState } from 'react';
+import { motion } from 'framer-motion';
+import {
+ Package,
+ Download,
+ Star,
+ Search,
+ TrendingUp,
+ Clock
+} from 'lucide-react';
+import GooseAppImage from '../../assets/GooseApp.png';
+import AppsImage from '../../assets/Apps.png';
+import G2Image from '../../assets/g2.png';
+import PeepsImage from '../../assets/peeps.png';
+import ExtensionsImage from '../../assets/extensions.png';
+import SellersImage from '../../assets/sellers.png';
+
+interface MarketplaceItem {
+ id: string;
+ name: string;
+ description: string;
+ type: 'recipe' | 'extension' | 'template';
+ author: string;
+ downloads: number;
+ rating: number;
+ lastUpdated: Date;
+ tags: string[];
+ icon?: string;
+ featured?: boolean;
+ fullWidth?: boolean;
+ hero?: boolean;
+ gradient?: string;
+ tagline?: string;
+ image?: string;
+}
+
+// Mock data - will be replaced with real data later
+const mockItems: MarketplaceItem[] = [
+ // 1. First hero card
+ {
+ id: 'hero-1',
+ name: 'Goose iOS',
+ description: 'Your AI development companion on the go. Access powerful coding assistance, run recipes, and manage your projects from anywhere. Available now on the App Store.',
+ type: 'extension',
+ author: 'Block Team',
+ downloads: 25680,
+ rating: 5.0,
+ lastUpdated: new Date('2024-01-29'),
+ tags: ['ios', 'mobile', 'app', 'hero'],
+ featured: true,
+ hero: true,
+ gradient: 'from-indigo-600 via-purple-600 to-pink-600',
+ tagline: '๐ฑ Now on iOS',
+ },
+ // 2. Two featured cards
+ {
+ id: 'featured-1',
+ name: 'AI Code Assistant Pro',
+ description: 'Transform your development workflow with AI-powered code generation, intelligent refactoring, and real-time code reviews. Supports 50+ languages and frameworks.',
+ type: 'extension',
+ author: 'Block Team',
+ downloads: 15420,
+ rating: 4.9,
+ lastUpdated: new Date('2024-01-28'),
+ tags: ['ai', 'code-generation', 'productivity', 'featured'],
+ featured: true,
+ fullWidth: true,
+ gradient: 'from-purple-500 via-pink-500 to-red-500',
+ tagline: '๐ Most Popular Extension',
+ image: 'apps',
+ },
+ {
+ id: 'featured-2',
+ name: 'Full-Stack Deployment Suite',
+ description: 'Deploy anywhere in seconds. One-click deployment to AWS, Azure, GCP, Vercel, and more. Includes CI/CD pipelines, monitoring, and automatic rollbacks.',
+ type: 'template',
+ author: 'Block Team',
+ downloads: 12890,
+ rating: 4.9,
+ lastUpdated: new Date('2024-01-27'),
+ tags: ['deployment', 'devops', 'cloud', 'featured'],
+ featured: true,
+ fullWidth: true,
+ gradient: 'from-blue-500 via-cyan-500 to-teal-500',
+ tagline: 'โก Deploy in Seconds',
+ image: 'g2',
+ },
+ // 3. Second hero card (full width)
+ {
+ id: 'hero-2',
+ name: 'Enterprise Solutions',
+ description: 'Scale your development with enterprise-grade tools, advanced security features, and dedicated support. Built for teams that demand the best.',
+ type: 'extension',
+ author: 'Block Team',
+ downloads: 18500,
+ rating: 5.0,
+ lastUpdated: new Date('2024-01-30'),
+ tags: ['enterprise', 'security', 'teams', 'hero'],
+ featured: true,
+ hero: true,
+ gradient: 'from-blue-600 to-blue-600',
+ tagline: '๐ข For Teams',
+ image: 'peeps',
+ },
+ // 4. Second set of featured cards
+ {
+ id: 'featured-3',
+ name: 'Cloud Infrastructure Manager',
+ description: 'Manage your cloud infrastructure across AWS, Azure, and GCP from a single interface. Automated provisioning, monitoring, and cost optimization.',
+ type: 'extension',
+ author: 'Block Team',
+ downloads: 9850,
+ rating: 4.8,
+ lastUpdated: new Date('2024-01-26'),
+ tags: ['cloud', 'infrastructure', 'devops', 'featured'],
+ featured: true,
+ fullWidth: true,
+ gradient: 'from-green-500 via-emerald-500 to-teal-500',
+ tagline: 'โ๏ธ Cloud Native',
+ image: 'extensions',
+ },
+ {
+ id: 'featured-4',
+ name: 'API Testing Suite',
+ description: 'Comprehensive API testing with automated test generation, load testing, and real-time monitoring. Supports REST, GraphQL, and WebSocket APIs.',
+ type: 'template',
+ author: 'Block Team',
+ downloads: 11200,
+ rating: 4.9,
+ lastUpdated: new Date('2024-01-28'),
+ tags: ['testing', 'api', 'automation', 'featured'],
+ featured: true,
+ fullWidth: true,
+ gradient: 'from-orange-500 via-red-500 to-pink-500',
+ tagline: '๐งช Test Everything',
+ image: 'sellers',
+ },
+ // 5. Rest of the regular tiles
+ {
+ id: '1',
+ name: 'Python Code Review',
+ description: 'Automated code review assistant for Python projects',
+ type: 'recipe',
+ author: 'Block Team',
+ downloads: 1250,
+ rating: 4.8,
+ lastUpdated: new Date('2024-01-15'),
+ tags: ['python', 'code-review', 'quality'],
+ },
+ {
+ id: '2',
+ name: 'React Component Generator',
+ description: 'Generate React components with TypeScript and tests',
+ type: 'recipe',
+ author: 'Community',
+ downloads: 890,
+ rating: 4.5,
+ lastUpdated: new Date('2024-01-10'),
+ tags: ['react', 'typescript', 'components'],
+ },
+ {
+ id: '3',
+ name: 'API Documentation Writer',
+ description: 'Automatically generate API documentation from code',
+ type: 'recipe',
+ author: 'Block Team',
+ downloads: 2100,
+ rating: 4.9,
+ lastUpdated: new Date('2024-01-20'),
+ tags: ['documentation', 'api', 'openapi'],
+ },
+ {
+ id: '4',
+ name: 'Git Workflow Helper',
+ description: 'Streamline your git workflow with smart commit messages and branch management',
+ type: 'recipe',
+ author: 'Community',
+ downloads: 1580,
+ rating: 4.7,
+ lastUpdated: new Date('2024-01-18'),
+ tags: ['git', 'workflow', 'automation'],
+ },
+ {
+ id: '5',
+ name: 'Database Schema Designer',
+ description: 'Design and generate database schemas with best practices',
+ type: 'extension',
+ author: 'Block Team',
+ downloads: 2340,
+ rating: 4.9,
+ lastUpdated: new Date('2024-01-22'),
+ tags: ['database', 'schema', 'sql'],
+ },
+ {
+ id: '6',
+ name: 'Test Suite Generator',
+ description: 'Automatically generate comprehensive test suites for your code',
+ type: 'recipe',
+ author: 'Community',
+ downloads: 1120,
+ rating: 4.6,
+ lastUpdated: new Date('2024-01-12'),
+ tags: ['testing', 'automation', 'quality'],
+ },
+ {
+ id: '7',
+ name: 'REST API Builder',
+ description: 'Scaffold complete REST APIs with authentication and validation',
+ type: 'template',
+ author: 'Block Team',
+ downloads: 3200,
+ rating: 4.8,
+ lastUpdated: new Date('2024-01-25'),
+ tags: ['api', 'rest', 'backend'],
+ },
+ {
+ id: '8',
+ name: 'Docker Compose Generator',
+ description: 'Create optimized Docker Compose configurations for your stack',
+ type: 'recipe',
+ author: 'Community',
+ downloads: 1890,
+ rating: 4.7,
+ lastUpdated: new Date('2024-01-19'),
+ tags: ['docker', 'devops', 'containers'],
+ },
+ {
+ id: '9',
+ name: 'GraphQL Schema Builder',
+ description: 'Design and implement GraphQL schemas with resolvers',
+ type: 'extension',
+ author: 'Block Team',
+ downloads: 1450,
+ rating: 4.6,
+ lastUpdated: new Date('2024-01-16'),
+ tags: ['graphql', 'api', 'schema'],
+ },
+ {
+ id: '10',
+ name: 'CI/CD Pipeline Creator',
+ description: 'Generate CI/CD pipelines for GitHub Actions, GitLab, and more',
+ type: 'recipe',
+ author: 'Community',
+ downloads: 2670,
+ rating: 4.9,
+ lastUpdated: new Date('2024-01-23'),
+ tags: ['ci-cd', 'devops', 'automation'],
+ },
+ {
+ id: '11',
+ name: 'Microservices Template',
+ description: 'Complete microservices architecture with service mesh',
+ type: 'template',
+ author: 'Block Team',
+ downloads: 2890,
+ rating: 4.8,
+ lastUpdated: new Date('2024-01-21'),
+ tags: ['microservices', 'architecture', 'kubernetes'],
+ },
+ {
+ id: '12',
+ name: 'Code Refactoring Assistant',
+ description: 'Intelligent code refactoring suggestions and automated improvements',
+ type: 'recipe',
+ author: 'Community',
+ downloads: 1340,
+ rating: 4.5,
+ lastUpdated: new Date('2024-01-14'),
+ tags: ['refactoring', 'code-quality', 'optimization'],
+ },
+ {
+ id: '13',
+ name: 'Security Audit Tool',
+ description: 'Scan your codebase for security vulnerabilities and best practices',
+ type: 'extension',
+ author: 'Block Team',
+ downloads: 3450,
+ rating: 4.9,
+ lastUpdated: new Date('2024-01-24'),
+ tags: ['security', 'audit', 'vulnerability'],
+ },
+ {
+ id: '14',
+ name: 'TypeScript Migrator',
+ description: 'Migrate JavaScript projects to TypeScript with type inference',
+ type: 'recipe',
+ author: 'Community',
+ downloads: 1670,
+ rating: 4.7,
+ lastUpdated: new Date('2024-01-17'),
+ tags: ['typescript', 'migration', 'javascript'],
+ },
+ {
+ id: '15',
+ name: 'Mobile App Starter',
+ description: 'React Native starter template with navigation and state management',
+ type: 'template',
+ author: 'Block Team',
+ downloads: 2120,
+ rating: 4.6,
+ lastUpdated: new Date('2024-01-13'),
+ tags: ['react-native', 'mobile', 'starter'],
+ },
+ {
+ id: '16',
+ name: 'Performance Optimizer',
+ description: 'Analyze and optimize application performance bottlenecks',
+ type: 'recipe',
+ author: 'Community',
+ downloads: 1980,
+ rating: 4.8,
+ lastUpdated: new Date('2024-01-20'),
+ tags: ['performance', 'optimization', 'profiling'],
+ },
+ {
+ id: '17',
+ name: 'Kubernetes Deployer',
+ description: 'Generate Kubernetes manifests and deployment strategies',
+ type: 'extension',
+ author: 'Block Team',
+ downloads: 2560,
+ rating: 4.7,
+ lastUpdated: new Date('2024-01-22'),
+ tags: ['kubernetes', 'deployment', 'devops'],
+ },
+ {
+ id: '18',
+ name: 'Accessibility Checker',
+ description: 'Ensure your web apps meet WCAG accessibility standards',
+ type: 'recipe',
+ author: 'Community',
+ downloads: 1230,
+ rating: 4.6,
+ lastUpdated: new Date('2024-01-11'),
+ tags: ['accessibility', 'a11y', 'wcag'],
+ },
+ {
+ id: '19',
+ name: 'Serverless Framework',
+ description: 'Deploy serverless functions to AWS Lambda, Azure, and GCP',
+ type: 'template',
+ author: 'Block Team',
+ downloads: 2780,
+ rating: 4.8,
+ lastUpdated: new Date('2024-01-26'),
+ tags: ['serverless', 'lambda', 'cloud'],
+ },
+ {
+ id: '20',
+ name: 'E2E Test Automation',
+ description: 'Playwright and Cypress test automation for web applications',
+ type: 'recipe',
+ author: 'Community',
+ downloads: 1560,
+ rating: 4.7,
+ lastUpdated: new Date('2024-01-19'),
+ tags: ['testing', 'e2e', 'automation'],
+ },
+ {
+ id: '21',
+ name: 'Monorepo Manager',
+ description: 'Manage monorepos with Nx, Turborepo, or Lerna',
+ type: 'extension',
+ author: 'Block Team',
+ downloads: 2140,
+ rating: 4.6,
+ lastUpdated: new Date('2024-01-21'),
+ tags: ['monorepo', 'workspace', 'tooling'],
+ },
+ {
+ id: '22',
+ name: 'CSS Framework Generator',
+ description: 'Generate custom Tailwind, Bootstrap, or Material UI themes',
+ type: 'recipe',
+ author: 'Community',
+ downloads: 1890,
+ rating: 4.5,
+ lastUpdated: new Date('2024-01-15'),
+ tags: ['css', 'styling', 'themes'],
+ },
+ {
+ id: '23',
+ name: 'Data Migration Tool',
+ description: 'Migrate data between databases with validation and rollback',
+ type: 'extension',
+ author: 'Block Team',
+ downloads: 1720,
+ rating: 4.8,
+ lastUpdated: new Date('2024-01-23'),
+ tags: ['database', 'migration', 'etl'],
+ },
+ {
+ id: '24',
+ name: 'Logging & Monitoring',
+ description: 'Integrate structured logging and monitoring solutions',
+ type: 'recipe',
+ author: 'Community',
+ downloads: 2340,
+ rating: 4.7,
+ lastUpdated: new Date('2024-01-20'),
+ tags: ['logging', 'monitoring', 'observability'],
+ },
+ {
+ id: '25',
+ name: 'OAuth Provider Setup',
+ description: 'Implement OAuth 2.0 authentication with multiple providers',
+ type: 'template',
+ author: 'Block Team',
+ downloads: 3120,
+ rating: 4.9,
+ lastUpdated: new Date('2024-01-25'),
+ tags: ['auth', 'oauth', 'security'],
+ },
+ {
+ id: '26',
+ name: 'Markdown Documentation',
+ description: 'Generate beautiful documentation sites from markdown files',
+ type: 'recipe',
+ author: 'Community',
+ downloads: 1450,
+ rating: 4.6,
+ lastUpdated: new Date('2024-01-16'),
+ tags: ['documentation', 'markdown', 'static-site'],
+ },
+ {
+ id: '27',
+ name: 'WebSocket Server',
+ description: 'Real-time WebSocket server with Socket.io or native WS',
+ type: 'template',
+ author: 'Block Team',
+ downloads: 1980,
+ rating: 4.7,
+ lastUpdated: new Date('2024-01-22'),
+ tags: ['websocket', 'realtime', 'socket.io'],
+ },
+ {
+ id: '28',
+ name: 'Code Formatter',
+ description: 'Auto-format code with Prettier, ESLint, and custom rules',
+ type: 'recipe',
+ author: 'Community',
+ downloads: 2890,
+ rating: 4.8,
+ lastUpdated: new Date('2024-01-24'),
+ tags: ['formatting', 'linting', 'code-quality'],
+ },
+ {
+ id: '29',
+ name: 'Feature Flag Manager',
+ description: 'Implement feature flags with LaunchDarkly or custom solution',
+ type: 'extension',
+ author: 'Block Team',
+ downloads: 1670,
+ rating: 4.6,
+ lastUpdated: new Date('2024-01-18'),
+ tags: ['feature-flags', 'deployment', 'configuration'],
+ },
+ {
+ id: '30',
+ name: 'Email Template Builder',
+ description: 'Create responsive email templates with MJML or React Email',
+ type: 'recipe',
+ author: 'Community',
+ downloads: 1340,
+ rating: 4.5,
+ lastUpdated: new Date('2024-01-14'),
+ tags: ['email', 'templates', 'mjml'],
+ },
+ {
+ id: '31',
+ name: 'Payment Integration',
+ description: 'Integrate Stripe, PayPal, or Square payment processing',
+ type: 'template',
+ author: 'Block Team',
+ downloads: 3450,
+ rating: 4.9,
+ lastUpdated: new Date('2024-01-27'),
+ tags: ['payments', 'stripe', 'ecommerce'],
+ },
+ {
+ id: '32',
+ name: 'Caching Strategy',
+ description: 'Implement Redis, Memcached, or in-memory caching',
+ type: 'recipe',
+ author: 'Community',
+ downloads: 2120,
+ rating: 4.7,
+ lastUpdated: new Date('2024-01-21'),
+ tags: ['caching', 'redis', 'performance'],
+ },
+ {
+ id: '33',
+ name: 'Internationalization',
+ description: 'Add i18n support with react-intl or next-i18next',
+ type: 'extension',
+ author: 'Block Team',
+ downloads: 1890,
+ rating: 4.6,
+ lastUpdated: new Date('2024-01-19'),
+ tags: ['i18n', 'localization', 'translation'],
+ },
+ {
+ id: '34',
+ name: 'Error Tracking',
+ description: 'Integrate Sentry, Rollbar, or custom error tracking',
+ type: 'recipe',
+ author: 'Community',
+ downloads: 2560,
+ rating: 4.8,
+ lastUpdated: new Date('2024-01-23'),
+ tags: ['errors', 'monitoring', 'sentry'],
+ },
+ {
+ id: '35',
+ name: 'Admin Dashboard',
+ description: 'Full-featured admin dashboard with charts and tables',
+ type: 'template',
+ author: 'Block Team',
+ downloads: 3890,
+ rating: 4.9,
+ lastUpdated: new Date('2024-01-26'),
+ tags: ['admin', 'dashboard', 'ui'],
+ },
+ {
+ id: '36',
+ name: 'Rate Limiter',
+ description: 'API rate limiting with Redis or in-memory storage',
+ type: 'recipe',
+ author: 'Community',
+ downloads: 1780,
+ rating: 4.7,
+ lastUpdated: new Date('2024-01-17'),
+ tags: ['rate-limiting', 'api', 'security'],
+ },
+ {
+ id: '37',
+ name: 'Search Engine',
+ description: 'Integrate Elasticsearch, Algolia, or MeiliSearch',
+ type: 'extension',
+ author: 'Block Team',
+ downloads: 2340,
+ rating: 4.8,
+ lastUpdated: new Date('2024-01-24'),
+ tags: ['search', 'elasticsearch', 'indexing'],
+ },
+ {
+ id: '38',
+ name: 'Image Optimizer',
+ description: 'Optimize and transform images with Sharp or Cloudinary',
+ type: 'recipe',
+ author: 'Community',
+ downloads: 1560,
+ rating: 4.6,
+ lastUpdated: new Date('2024-01-15'),
+ tags: ['images', 'optimization', 'cdn'],
+ },
+ {
+ id: '39',
+ name: 'Queue Worker',
+ description: 'Background job processing with Bull, BullMQ, or SQS',
+ type: 'template',
+ author: 'Block Team',
+ downloads: 2670,
+ rating: 4.8,
+ lastUpdated: new Date('2024-01-25'),
+ tags: ['queue', 'workers', 'background-jobs'],
+ },
+ {
+ id: '40',
+ name: 'Form Builder',
+ description: 'Dynamic form builder with validation and conditional logic',
+ type: 'recipe',
+ author: 'Community',
+ downloads: 1920,
+ rating: 4.5,
+ lastUpdated: new Date('2024-01-18'),
+ tags: ['forms', 'validation', 'ui'],
+ },
+ {
+ id: '41',
+ name: 'Notification System',
+ description: 'Multi-channel notifications: email, SMS, push, in-app',
+ type: 'extension',
+ author: 'Block Team',
+ downloads: 2890,
+ rating: 4.9,
+ lastUpdated: new Date('2024-01-26'),
+ tags: ['notifications', 'messaging', 'alerts'],
+ },
+ {
+ id: '42',
+ name: 'PDF Generator',
+ description: 'Generate PDFs from HTML with Puppeteer or PDFKit',
+ type: 'recipe',
+ author: 'Community',
+ downloads: 1450,
+ rating: 4.6,
+ lastUpdated: new Date('2024-01-16'),
+ tags: ['pdf', 'documents', 'generation'],
+ },
+ {
+ id: '43',
+ name: 'Analytics Dashboard',
+ description: 'Custom analytics with Google Analytics, Mixpanel, or Amplitude',
+ type: 'template',
+ author: 'Block Team',
+ downloads: 3120,
+ rating: 4.8,
+ lastUpdated: new Date('2024-01-27'),
+ tags: ['analytics', 'tracking', 'metrics'],
+ },
+ {
+ id: '44',
+ name: 'API Versioning',
+ description: 'Implement API versioning strategies and deprecation',
+ type: 'recipe',
+ author: 'Community',
+ downloads: 1680,
+ rating: 4.7,
+ lastUpdated: new Date('2024-01-20'),
+ tags: ['api', 'versioning', 'backwards-compatibility'],
+ },
+ {
+ id: '45',
+ name: 'State Machine',
+ description: 'Implement state machines with XState or custom logic',
+ type: 'extension',
+ author: 'Block Team',
+ downloads: 1340,
+ rating: 4.6,
+ lastUpdated: new Date('2024-01-19'),
+ tags: ['state-machine', 'workflow', 'xstate'],
+ },
+ {
+ id: '46',
+ name: 'Blog Platform',
+ description: 'Complete blog platform with CMS, comments, and SEO',
+ type: 'template',
+ author: 'Community',
+ downloads: 2450,
+ rating: 4.7,
+ lastUpdated: new Date('2024-01-22'),
+ tags: ['blog', 'cms', 'content'],
+ },
+ {
+ id: '47',
+ name: 'Backup Automation',
+ description: 'Automated backups for databases and file systems',
+ type: 'recipe',
+ author: 'Block Team',
+ downloads: 1890,
+ rating: 4.8,
+ lastUpdated: new Date('2024-01-24'),
+ tags: ['backup', 'disaster-recovery', 'automation'],
+ },
+ {
+ id: '48',
+ name: 'Chat Application',
+ description: 'Real-time chat with rooms, DMs, and file sharing',
+ type: 'template',
+ author: 'Community',
+ downloads: 3340,
+ rating: 4.9,
+ lastUpdated: new Date('2024-01-28'),
+ tags: ['chat', 'messaging', 'realtime'],
+ },
+];
+
+const HeroCard: React.FC<{
+ item: MarketplaceItem;
+ onInstall: (item: MarketplaceItem) => void;
+}> = ({ item, onInstall }) => {
+ const [isHovered, setIsHovered] = useState(false);
+
+ const formatDownloads = (count: number) => {
+ if (count >= 1000) return `${(count / 1000).toFixed(1)}k`;
+ return count.toString();
+ };
+
+ return (
+ setIsHovered(true)}
+ onHoverEnd={() => setIsHovered(false)}
+ className={`
+ relative cursor-pointer overflow-hidden
+ col-span-full
+ rounded-2xl
+ bg-background-default
+ min-h-[640px] md:min-h-[500px] sm:min-h-[700px]
+ `}
+ >
+
+ {/* Tagline badge - Top left */}
+ {item.tagline && (
+
+ {item.tagline}
+
+ )}
+
+ {/* Content - Left side with text - Bottom aligned on desktop, top on mobile */}
+
+
+ {item.name}
+
+
+ {item.description}
+
+
+ {/* Stats */}
+
+
+
+ {formatDownloads(item.downloads)} downloads
+
+
+
+ {item.rating} rating
+
+
+ {item.type}
+
+
+
+
+ {/* Hero image - responsive positioning */}
+ {/* Different images based on item.image property */}
+ {item.image === 'peeps' ? (
+ // Custom image positioned on the right bottom (like peeps.png for Enterprise Solutions)
+
+
+
+ ) : item.image ? (
+ // Other custom images - full width
+
+
+
+ ) : (
+ // Default phone image with bottom cutoff (for Goose iOS)
+
+
+
+ )}
+
+ {/* Install button - bottom right */}
+ {
+ e.stopPropagation();
+ onInstall(item);
+ }}
+ className="absolute bottom-6 right-6 md:bottom-12 md:right-12 z-20 w-12 h-12 md:w-16 md:h-16 rounded-full bg-background-accent text-text-on-accent hover:bg-background-medium hover:text-text-default transition-colors flex items-center justify-center"
+ >
+
+
+
+ );
+};
+
+const FeaturedCard: React.FC<{
+ item: MarketplaceItem;
+ onInstall: (item: MarketplaceItem) => void;
+}> = ({ item, onInstall }) => {
+ const [isHovered, setIsHovered] = useState(false);
+
+ const formatDownloads = (count: number) => {
+ if (count >= 1000) return `${(count / 1000).toFixed(1)}k`;
+ return count.toString();
+ };
+
+ return (
+ setIsHovered(true)}
+ onHoverEnd={() => setIsHovered(false)}
+ className={`
+ relative cursor-pointer overflow-hidden
+ rounded-2xl
+ bg-background-default
+ flex flex-col
+ min-h-[400px]
+ `}
+ >
+ {/* Image Section - Takes up most of the space */}
+
+ {/* Show actual image if provided, otherwise show gradient with pattern */}
+ {item.image === 'apps' ? (
+
+ ) : item.image === 'g2' ? (
+
+ ) : item.image === 'peeps' ? (
+
+ ) : item.image === 'extensions' ? (
+
+ ) : item.image === 'sellers' ? (
+
+ ) : (
+ /* Animated background pattern for gradient cards */
+
+ )}
+
+ {/* Tagline badge - top left */}
+ {item.tagline && (
+
+ {item.tagline}
+
+ )}
+
+ {/* Install button - top right */}
+
{
+ e.stopPropagation();
+ onInstall(item);
+ }}
+ className="absolute top-4 right-4 z-10 w-10 h-10 rounded-full text-white hover:bg-white hover:text-gray-900 transition-colors flex items-center justify-center"
+ >
+
+
+
+
+ {/* Text Content Section - Compact at bottom */}
+
+
+
+ {item.type}
+
+
+ {item.name}
+
+
+
+
+ {item.description}
+
+
+ {/* Stats at bottom */}
+
+
+
+ {item.rating}
+
+
+
+ {formatDownloads(item.downloads)}
+
+
+
+
+ );
+};
+
+const MarketplaceCard: React.FC<{
+ item: MarketplaceItem;
+ onInstall: (item: MarketplaceItem) => void;
+}> = ({ item, onInstall }) => {
+ const [isHovered, setIsHovered] = useState(false);
+
+ const formatDownloads = (count: number) => {
+ if (count >= 1000) return `${(count / 1000).toFixed(1)}k`;
+ return count.toString();
+ };
+
+ const formatDate = (date: Date) => {
+ const now = new Date();
+ const diff = now.getTime() - date.getTime();
+ const days = Math.floor(diff / (1000 * 60 * 60 * 24));
+
+ if (days === 0) return 'Today';
+ if (days === 1) return 'Yesterday';
+ if (days < 7) return `${days}d ago`;
+ if (days < 30) return `${Math.floor(days / 7)}w ago`;
+ return `${Math.floor(days / 30)}mo ago`;
+ };
+
+ return (
+ setIsHovered(true)}
+ onHoverEnd={() => setIsHovered(false)}
+ className="
+ relative cursor-pointer group
+ bg-background-default
+ px-6 py-6
+ transition-colors duration-200
+ hover:bg-background-medium
+ aspect-square
+ flex flex-col justify-between
+ rounded-2xl
+ "
+ >
+ {/* Icon/Type indicator in top left */}
+
+
+
+ {/* Type badge */}
+
+ {item.type}
+
+
+
+ {/* Rating in top right */}
+
+
+ {item.rating}
+
+
+ {/* Install button (visible on hover) */}
+ {
+ e.stopPropagation();
+ onInstall(item);
+ }}
+ className="absolute top-14 right-4 px-3 py-1.5 rounded-lg bg-green-500 text-white text-sm font-medium hover:bg-green-600 transition-colors flex items-center gap-1"
+ >
+
+ Install
+
+
+ {/* Content at bottom */}
+
+
+ {item.name}
+
+
+ {item.description}
+
+
+ {/* Stats row */}
+
+
+
+ {formatDownloads(item.downloads)}
+
+
+
+ {formatDate(item.lastUpdated)}
+
+
+
+ {/* Author */}
+
+ by {item.author}
+
+
+
+ );
+};
+
+const EmptyMarketplaceTile: React.FC = () => {
+ return (
+
+ );
+};
+
+const MarketplaceView: React.FC = () => {
+ const [searchQuery, setSearchQuery] = useState('');
+ const [selectedFilter, setSelectedFilter] = useState<'all' | 'recipe' | 'extension' | 'template'>('all');
+ const [items] = useState(mockItems);
+
+ const filteredItems = items.filter(item => {
+ const matchesSearch = item.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ item.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ item.tags.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase()));
+ const matchesFilter = selectedFilter === 'all' || item.type === selectedFilter;
+ return matchesSearch && matchesFilter;
+ });
+
+ const handleInstall = (item: MarketplaceItem) => {
+ console.log('Installing:', item);
+ // TODO: Implement installation logic
+ };
+
+ // Calculate empty tiles to fill viewport
+ const calculateEmptyTiles = (itemCount: number) => {
+ const minTiles = 12; // Minimum 2 rows
+ return Math.max(0, minTiles - itemCount);
+ };
+
+ return (
+
+ {/* Header Section */}
+
+
+
+
Marketplace
+
Discover recipes, extensions, and templates
+
+
+
+
+ Trending
+
+
+
+
+ {/* Search and Filters */}
+
+
+
+ setSearchQuery(e.target.value)}
+ placeholder="Search marketplace..."
+ className="w-full pl-10 pr-4 py-2 rounded-lg border border-border-default bg-background-muted focus:outline-none focus:ring-2 focus:ring-background-accent text-sm"
+ />
+
+
+ {/* Filter buttons */}
+
+ {(['all', 'recipe', 'extension', 'template'] as const).map((filter) => (
+ setSelectedFilter(filter)}
+ className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors capitalize ${
+ selectedFilter === filter
+ ? 'bg-background-accent text-text-on-accent'
+ : 'border border-border-default text-text-default hover:bg-background-medium'
+ }`}
+ >
+ {filter}
+
+ ))}
+
+
+
+
+ {/* Content */}
+
+ {/* Render items in order, grouping featured cards together */}
+ {(() => {
+ const result: JSX.Element[] = [];
+ let featuredBuffer: MarketplaceItem[] = [];
+
+ filteredItems.forEach((item, index) => {
+ if (item.hero) {
+ // Hero card - always full width
+ // Flush any buffered featured cards first
+ if (featuredBuffer.length > 0) {
+ result.push(
+
+ {featuredBuffer.map((featuredItem) => (
+
+ ))}
+
+ );
+ featuredBuffer = [];
+ }
+ // Render hero card
+ result.push(
+
+
+
+ );
+ } else if (item.fullWidth) {
+ // Buffer featured cards (half-width cards in 2-column grid)
+ featuredBuffer.push(item);
+ } else {
+ // Flush any buffered featured cards first
+ if (featuredBuffer.length > 0) {
+ result.push(
+
+ {featuredBuffer.map((featuredItem) => (
+
+ ))}
+
+ );
+ featuredBuffer = [];
+ }
+ }
+ });
+
+ // Flush any remaining featured cards
+ if (featuredBuffer.length > 0) {
+ result.push(
+
+ {featuredBuffer.map((featuredItem) => (
+
+ ))}
+
+ );
+ }
+
+ return result;
+ })()}
+
+ {/* Regular Tiles Section */}
+ {filteredItems.filter(item => !item.hero && !item.fullWidth).length > 0 && (
+
+ {filteredItems
+ .filter(item => !item.hero && !item.fullWidth)
+ .map((item) => (
+
+ ))}
+
+ {/* Empty tiles */}
+ {Array.from({ length: calculateEmptyTiles(filteredItems.filter(i => !i.hero && !i.fullWidth).length) }).map((_, index) => (
+
+ ))}
+
+ )}
+
+ {/* Empty state */}
+ {filteredItems.length === 0 && (
+
+
+
No items found
+
+ Try adjusting your search or filters
+
+
+ )}
+
+
+ );
+};
+
+export default MarketplaceView;
diff --git a/ui/desktop/src/components/recipes/CreateRecipeForm.tsx b/ui/desktop/src/components/recipes/CreateRecipeForm.tsx
index b7093f73df..afbc6876a2 100644
--- a/ui/desktop/src/components/recipes/CreateRecipeForm.tsx
+++ b/ui/desktop/src/components/recipes/CreateRecipeForm.tsx
@@ -1,15 +1,18 @@
import { useState, useEffect } from 'react';
import { useForm } from '@tanstack/react-form';
import { z } from 'zod';
-import { FileText } from 'lucide-react';
+import { FileText, Clock, ChevronDown, ChevronUp, AlertCircle } from 'lucide-react';
+import { motion, AnimatePresence } from 'framer-motion';
import { Button } from '../ui/button';
import { Recipe } from '../../recipe';
-import { saveRecipe } from '../../recipe/recipeStorage';
+import { saveRecipe, listSavedRecipes } from '../../recipe/recipeStorage';
import { toastSuccess, toastError } from '../../toasts';
import { useEscapeKey } from '../../hooks/useEscapeKey';
import { RecipeNameField, recipeNameSchema } from './shared/RecipeNameField';
import { generateRecipeNameFromTitle } from './shared/recipeNameUtils';
import { validateJsonSchema, getValidationErrorMessages } from '../../recipe/validation';
+import { createSchedule } from '../../schedule';
+import cronstrue from 'cronstrue';
interface CreateRecipeFormProps {
isOpen: boolean;
@@ -27,14 +30,53 @@ const createRecipeSchema = z.object({
jsonSchema: z.string(),
recipeName: recipeNameSchema,
global: z.boolean(),
+ enableSchedule: z.boolean(),
+ cronExpression: z.string(),
+ scheduleId: z.string(),
});
export default function CreateRecipeForm({ isOpen, onClose, onSuccess }: CreateRecipeFormProps) {
const [creating, setCreating] = useState(false);
+ const [showAdvanced, setShowAdvanced] = useState(false);
+ const [cronPreview, setCronPreview] = useState('');
+ const [existingRecipes, setExistingRecipes] = useState([]);
+ const [recipeNameWarning, setRecipeNameWarning] = useState('');
// Handle Esc key for modal
useEscapeKey(isOpen, onClose);
+ // Load existing recipe names when modal opens
+ useEffect(() => {
+ if (isOpen) {
+ listSavedRecipes().then((recipes) => {
+ setExistingRecipes(recipes.map((r) => r.name));
+ }).catch((err) => {
+ console.error('Failed to load existing recipes:', err);
+ });
+ }
+ }, [isOpen]);
+
+ // Check for duplicate recipe names
+ const checkForDuplicate = (recipeName: string): string => {
+ if (!recipeName) return recipeName;
+
+ let finalName = recipeName;
+ let counter = 1;
+
+ while (existingRecipes.includes(finalName)) {
+ finalName = `${recipeName}-${counter}`;
+ counter++;
+ }
+
+ if (finalName !== recipeName) {
+ setRecipeNameWarning(`Recipe name "${recipeName}" already exists. Using "${finalName}" instead.`);
+ } else {
+ setRecipeNameWarning('');
+ }
+
+ return finalName;
+ };
+
const createRecipeForm = useForm({
defaultValues: {
title: '',
@@ -45,9 +87,12 @@ export default function CreateRecipeForm({ isOpen, onClose, onSuccess }: CreateR
jsonSchema: '',
recipeName: '',
global: true,
+ enableSchedule: false,
+ cronExpression: '0 0 * * *', // Daily at midnight
+ scheduleId: '',
},
validators: {
- onChange: createRecipeSchema,
+ onSubmit: createRecipeSchema,
},
onSubmit: async ({ value }) => {
setCreating(true);
@@ -86,11 +131,44 @@ export default function CreateRecipeForm({ isOpen, onClose, onSuccess }: CreateR
response: jsonSchemaObj ? { json_schema: jsonSchemaObj } : undefined,
};
- await saveRecipe(recipe, {
+ const savedRecipePath = await saveRecipe(recipe, {
name: value.recipeName.trim(),
global: value.global,
});
+ // Create schedule if enabled
+ if (value.enableSchedule && value.cronExpression && value.scheduleId) {
+ try {
+ // Get the recipe path - construct it based on global/local
+ const recipeSource = value.global
+ ? `~/.config/goose/recipes/${value.recipeName.trim()}.yaml`
+ : `.goose/recipes/${value.recipeName.trim()}.yaml`;
+
+ await createSchedule({
+ id: value.scheduleId.trim(),
+ recipe_source: recipeSource,
+ cron: value.cronExpression.trim(),
+ });
+
+ toastSuccess({
+ title: value.recipeName.trim(),
+ msg: 'Recipe and schedule created successfully',
+ });
+ } catch (scheduleError) {
+ console.error('Failed to create schedule:', scheduleError);
+ toastError({
+ title: 'Schedule Creation Failed',
+ msg: 'Recipe was created but schedule failed. You can create the schedule from the Scheduler view.',
+ traceback: scheduleError instanceof Error ? scheduleError.message : String(scheduleError),
+ });
+ }
+ } else {
+ toastSuccess({
+ title: value.recipeName.trim(),
+ msg: 'Recipe created successfully',
+ });
+ }
+
// Reset dialog state
createRecipeForm.reset({
title: '',
@@ -101,15 +179,14 @@ export default function CreateRecipeForm({ isOpen, onClose, onSuccess }: CreateR
jsonSchema: '',
recipeName: '',
global: true,
+ enableSchedule: false,
+ cronExpression: '0 0 * * *',
+ scheduleId: '',
});
+ setCronPreview('');
onClose();
onSuccess();
-
- toastSuccess({
- title: value.recipeName.trim(),
- msg: 'Recipe created successfully',
- });
} catch (error) {
console.error('Failed to create recipe:', error);
@@ -124,40 +201,26 @@ export default function CreateRecipeForm({ isOpen, onClose, onSuccess }: CreateR
},
});
- // Set default example values when the modal opens
+ // Clear form when modal opens
useEffect(() => {
if (isOpen) {
- // Set example values like the original did
- createRecipeForm.setFieldValue('title', 'Python Development Assistant');
- createRecipeForm.setFieldValue(
- 'description',
- 'A helpful assistant for Python development tasks including coding, debugging, and code review.'
- );
- createRecipeForm.setFieldValue(
- 'instructions',
- `You are an expert Python developer assistant. Help users with:
-
-1. Writing clean, efficient Python code
-2. Debugging and troubleshooting issues
-3. Code review and optimization suggestions
-4. Best practices and design patterns
-5. Testing and documentation
-
-Always provide clear explanations and working code examples.
-
-Parameters you can use:
-- {{project_type}}: The type of Python project (web, data science, CLI, etc.)
-- {{python_version}}: Target Python version`
- );
- createRecipeForm.setFieldValue(
- 'prompt',
- 'What Python development task can I help you with today?'
- );
- createRecipeForm.setFieldValue('activities', 'coding, debugging, testing, documentation');
- createRecipeForm.setFieldValue('recipeName', 'python-development-assistant');
- createRecipeForm.setFieldValue('global', true);
+ // Start with empty fields
+ createRecipeForm.reset({
+ title: '',
+ description: '',
+ instructions: '',
+ prompt: '',
+ activities: '',
+ jsonSchema: '',
+ recipeName: '',
+ global: true,
+ enableSchedule: false,
+ cronExpression: '0 0 * * *',
+ scheduleId: '',
+ });
+ setRecipeNameWarning('');
}
- }, [isOpen, createRecipeForm]);
+ }, [isOpen]);
const handleClose = () => {
// Reset form to default values
@@ -170,7 +233,12 @@ Parameters you can use:
jsonSchema: '',
recipeName: '',
global: true,
+ enableSchedule: false,
+ cronExpression: '0 0 * * *',
+ scheduleId: '',
});
+ setCronPreview('');
+ setRecipeNameWarning('');
onClose();
};
@@ -205,14 +273,18 @@ Parameters you can use:
onChange={(e) => {
const value = e.target.value;
field.handleChange(value);
- // Auto-generate recipe name when title changes
+ // Auto-generate recipe name when title changes and check for duplicates
if (value.trim()) {
const suggestedName = generateRecipeNameFromTitle(value);
- createRecipeForm.setFieldValue('recipeName', suggestedName);
+ const finalName = checkForDuplicate(suggestedName);
+ createRecipeForm.setFieldValue('recipeName', finalName);
+ } else {
+ createRecipeForm.setFieldValue('recipeName', '');
+ setRecipeNameWarning('');
}
}}
onBlur={field.handleBlur}
- className={`w-full p-3 border rounded-lg bg-background-default text-text-standard focus:outline-none focus:ring-2 focus:ring-blue-500 ${
+ className={`w-full p-3 border rounded-lg bg-background-default text-text-standard focus:outline-none focus:ring-2 focus:ring-blue-500 placeholder:text-text-muted/50 ${
field.state.meta.errors.length > 0 ? 'border-red-500' : 'border-border-subtle'
}`}
placeholder="Recipe title"
@@ -225,6 +297,12 @@ Parameters you can use:
: field.state.meta.errors[0]?.message || String(field.state.meta.errors[0])}
)}
+ {recipeNameWarning && (
+
+
+
{recipeNameWarning}
+
+ )}
)}
@@ -244,7 +322,7 @@ Parameters you can use:
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
- className={`w-full p-3 border rounded-lg bg-background-default text-text-standard focus:outline-none focus:ring-2 focus:ring-blue-500 ${
+ className={`w-full p-3 border rounded-lg bg-background-default text-text-standard focus:outline-none focus:ring-2 focus:ring-blue-500 placeholder:text-text-muted/50 ${
field.state.meta.errors.length > 0 ? 'border-red-500' : 'border-border-subtle'
}`}
placeholder="Brief description of what this recipe does"
@@ -274,15 +352,17 @@ Parameters you can use:
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
- className={`w-full p-3 border rounded-lg bg-background-default text-text-standard focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none font-mono text-sm ${
+ className={`w-full p-3 border rounded-lg bg-background-default text-text-standard focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none font-mono text-sm placeholder:text-text-muted/50 ${
field.state.meta.errors.length > 0 ? 'border-red-500' : 'border-border-subtle'
}`}
placeholder="Detailed instructions for the AI agent..."
rows={8}
/>
-
- Use {`{{parameter_name}}`} to define parameters that users can fill in
-
+ {field.state.value && (
+
+ Use {`{{parameter_name}}`} to define parameters that users can fill in
+
+ )}
{field.state.meta.errors.length > 0 && (
{typeof field.state.meta.errors[0] === 'string'
@@ -294,71 +374,220 @@ Parameters you can use:
)}
-
+ {/* Recipe name is auto-generated from title - hidden from UI but kept in form state */}
+
{(field) => (
-
-
- Initial Prompt (Optional)
+ field.handleChange(e.target.value)}
+ />
+ )}
+
+
+ {/* Schedule Section */}
+
+ {(field) => (
+
+
+
+
+
+ Schedule this recipe
+
+
+ {
+ const newValue = !field.state.value;
+ field.handleChange(newValue);
+ if (newValue) {
+ const recipeName = createRecipeForm.getFieldValue('recipeName');
+ createRecipeForm.setFieldValue('scheduleId', recipeName || 'scheduled-recipe');
+ }
+ }}
+ className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 ${
+ field.state.value ? 'bg-blue-600' : 'bg-gray-300 dark:bg-gray-600'
+ }`}
+ >
+
+
-
)}
-
- {(field) => (
-
-
+ setShowAdvanced(!showAdvanced)}
+ className="w-full flex items-center justify-between p-4 hover:bg-background-muted transition-colors rounded-lg"
+ >
+ Advanced Settings
+ {showAdvanced ? (
+
+ ) : (
+
+ )}
+
+
+
+ {showAdvanced && (
+
- Activities (Optional)
-
-
field.handleChange(e.target.value)}
- onBlur={field.handleBlur}
- className="w-full p-3 border border-border-subtle rounded-lg bg-background-default text-text-standard focus:outline-none focus:ring-2 focus:ring-blue-500"
- placeholder="coding, debugging, testing, documentation (comma-separated)"
- />
-
- Comma-separated list of activities this recipe helps with
-
-
- )}
-
+
+
+ {(field) => (
+
+
+ Initial Prompt (Optional)
+
+
+ )}
+
-
- {(field) => (
-
-
- Response JSON Schema (Optional)
-
-
+ )}
+
-
- {(field) => (
-
- typeof error === 'string' ? error : error?.message || String(error)
- )}
- />
- )}
-
-
-
- {(field) => (
-
- )}
-
+
+ {(field) => (
+
+ )}
+
+
+
+ )}
+
+
diff --git a/ui/desktop/src/components/recipes/RecipesView.tsx b/ui/desktop/src/components/recipes/RecipesView.tsx
index 29f3b1a459..c4543cec63 100644
--- a/ui/desktop/src/components/recipes/RecipesView.tsx
+++ b/ui/desktop/src/components/recipes/RecipesView.tsx
@@ -1,6 +1,6 @@
-import { useState, useEffect, useMemo } from 'react';
+import { useState, useEffect, useMemo, useCallback } from 'react';
import { listSavedRecipes, convertToLocaleDateString } from '../../recipe/recipeStorage';
-import { FileText, Trash2, Bot, Calendar, AlertCircle } from 'lucide-react';
+import { FileText, Trash2, Bot, Calendar, AlertCircle, Clock, Play, Pause, Edit, Square, Eye, Filter } from 'lucide-react';
import { ScrollArea } from '../ui/scroll-area';
import { Card } from '../ui/card';
import { Button } from '../ui/button';
@@ -13,9 +13,31 @@ import { deleteRecipe, RecipeManifestResponse } from '../../api';
import CreateRecipeForm, { CreateRecipeButton } from './CreateRecipeForm';
import ImportRecipeForm, { ImportRecipeButton } from './ImportRecipeForm';
import { filterValidUsedParameters } from '../../utils/providerUtils';
+import {
+ listSchedules,
+ pauseSchedule,
+ unpauseSchedule,
+ deleteSchedule as deleteScheduleApi,
+ killRunningJob,
+ inspectRunningJob,
+ ScheduledJob
+} from '../../schedule';
+import { CreateScheduleModal, NewSchedulePayload } from '../schedule/CreateScheduleModal';
+import { EditScheduleModal } from '../schedule/EditScheduleModal';
+import ScheduleDetailView from '../schedule/ScheduleDetailView';
+import cronstrue from 'cronstrue';
+import { formatToLocalDateWithTimezone } from '../../utils/date';
+
+// Extended type that combines recipe with schedule info
+type RecipeWithSchedule = RecipeManifestResponse & {
+ schedule?: ScheduledJob;
+};
+
+type FilterType = 'all' | 'scheduled' | 'unscheduled';
export default function RecipesView() {
const [savedRecipes, setSavedRecipes] = useState
([]);
+ const [schedules, setSchedules] = useState([]);
const [loading, setLoading] = useState(true);
const [showSkeleton, setShowSkeleton] = useState(true);
const [error, setError] = useState(null);
@@ -23,10 +45,24 @@ export default function RecipesView() {
const [showPreview, setShowPreview] = useState(false);
const [showContent, setShowContent] = useState(false);
const [previewDeeplink, setPreviewDeeplink] = useState('');
+ const [filterType, setFilterType] = useState('all');
// Form dialog states
const [showCreateDialog, setShowCreateDialog] = useState(false);
const [showImportDialog, setShowImportDialog] = useState(false);
+ const [showScheduleModal, setShowScheduleModal] = useState(false);
+ const [showEditScheduleModal, setShowEditScheduleModal] = useState(false);
+ const [selectedRecipeForSchedule, setSelectedRecipeForSchedule] = useState(null);
+ const [editingSchedule, setEditingSchedule] = useState(null);
+
+ // Schedule detail view
+ const [viewingScheduleId, setViewingScheduleId] = useState(null);
+
+ // Action loading states
+ const [pausingScheduleIds, setPausingScheduleIds] = useState>(new Set());
+ const [deletingScheduleIds, setDeletingScheduleIds] = useState>(new Set());
+ const [killingScheduleIds, setKillingScheduleIds] = useState>(new Set());
+ const [inspectingScheduleIds, setInspectingScheduleIds] = useState>(new Set());
useEffect(() => {
loadSavedRecipes();
@@ -57,8 +93,15 @@ export default function RecipesView() {
setShowSkeleton(true);
setShowContent(false);
setError(null);
- const recipeManifestResponses = await listSavedRecipes();
+
+ // Load both recipes and schedules in parallel
+ const [recipeManifestResponses, schedulesData] = await Promise.all([
+ listSavedRecipes(),
+ listSchedules().catch(() => []) // Don't fail if schedules can't load
+ ]);
+
setSavedRecipes(recipeManifestResponses);
+ setSchedules(schedulesData);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load recipes');
console.error('Failed to load saved recipes:', err);
@@ -150,65 +193,115 @@ export default function RecipesView() {
selectedRecipe?.recipe.prompt,
]);
+ // Helper to find schedule for a recipe
+ const findScheduleForRecipe = useCallback((recipeId: string) => {
+ return schedules.find(schedule => {
+ // Match by recipe ID in the source path
+ return schedule.source.includes(recipeId);
+ });
+ }, [schedules]);
+
// Render a recipe item
const RecipeItem = ({
recipeManifestResponse,
- recipeManifestResponse: { recipe, lastModified },
+ recipeManifestResponse: { recipe, lastModified, id },
}: {
recipeManifestResponse: RecipeManifestResponse;
- }) => (
-
-
-
-
-
{recipe.title}
-
-
{recipe.description}
-
-
- {convertToLocaleDateString(lastModified)}
-
-
+ }) => {
+ const schedule = findScheduleForRecipe(id);
+
+ // Get human-readable cron if scheduled
+ let readableCron = '';
+ if (schedule) {
+ try {
+ readableCron = cronstrue.toString(schedule.cron);
+ } catch (e) {
+ readableCron = schedule.cron;
+ }
+ }
-
-
{
- e.stopPropagation();
- handleLoadRecipe(recipe);
- }}
- size="sm"
- className="h-8"
- >
-
- Use
-
-
{
- e.stopPropagation();
- handlePreviewRecipe(recipeManifestResponse);
- }}
- variant="outline"
- size="sm"
- className="h-8"
- >
-
- Preview
-
-
{
- e.stopPropagation();
- handleDeleteRecipe(recipeManifestResponse);
- }}
- variant="ghost"
- size="sm"
- className="h-8 text-red-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20"
- >
-
-
+ return (
+
+
+
+
+
{recipe.title}
+ {schedule && (
+ <>
+
+
+ Scheduled
+
+ {schedule.currently_running && (
+
+
+ Running
+
+ )}
+ {schedule.paused && (
+
+
+ Paused
+
+ )}
+ >
+ )}
+
+
{recipe.description}
+
+
+
+ {convertToLocaleDateString(lastModified)}
+
+ {schedule && (
+
+
+ {readableCron}
+
+ )}
+
+
+
+
+ {
+ e.stopPropagation();
+ handleLoadRecipe(recipe);
+ }}
+ size="sm"
+ className="h-8"
+ >
+
+ Use
+
+ {
+ e.stopPropagation();
+ handlePreviewRecipe(recipeManifestResponse);
+ }}
+ variant="outline"
+ size="sm"
+ className="h-8"
+ >
+
+ Preview
+
+ {
+ e.stopPropagation();
+ handleDeleteRecipe(recipeManifestResponse);
+ }}
+ variant="ghost"
+ size="sm"
+ className="h-8 text-red-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20"
+ >
+
+
+
-
-
- );
+
+ );
+ };
// Render skeleton loader for recipe items
const RecipeSkeleton = () => (
diff --git a/ui/desktop/src/components/recipes/recipeScheduleHelpers.ts b/ui/desktop/src/components/recipes/recipeScheduleHelpers.ts
new file mode 100644
index 0000000000..740cc2175d
--- /dev/null
+++ b/ui/desktop/src/components/recipes/recipeScheduleHelpers.ts
@@ -0,0 +1,52 @@
+import { RecipeManifestResponse } from '../../api';
+import { ScheduledJob } from '../../schedule';
+
+export type RecipeWithSchedule = RecipeManifestResponse & {
+ schedule?: ScheduledJob;
+};
+
+/**
+ * Merges recipes with their corresponding schedules
+ * Matches schedules to recipes based on the schedule's source path
+ */
+export function mergeRecipesWithSchedules(
+ recipes: RecipeManifestResponse[],
+ schedules: ScheduledJob[]
+): RecipeWithSchedule[] {
+ return recipes.map((recipe) => {
+ // Find schedule that matches this recipe's path
+ // The schedule.source contains the recipe file path
+ const schedule = schedules.find((s) => {
+ // Extract filename from source path for comparison
+ const sourcePath = s.source;
+ const recipeId = recipe.id;
+
+ // Check if the schedule source contains the recipe ID
+ // or if it matches the recipe name/path
+ return sourcePath.includes(recipeId) || sourcePath.includes(recipe.name);
+ });
+
+ return {
+ ...recipe,
+ schedule,
+ };
+ });
+}
+
+/**
+ * Filters recipes based on schedule status
+ */
+export function filterRecipesByScheduleStatus(
+ recipes: RecipeWithSchedule[],
+ filterType: 'all' | 'scheduled' | 'unscheduled'
+): RecipeWithSchedule[] {
+ switch (filterType) {
+ case 'scheduled':
+ return recipes.filter((r) => r.schedule !== undefined);
+ case 'unscheduled':
+ return recipes.filter((r) => r.schedule === undefined);
+ case 'all':
+ default:
+ return recipes;
+ }
+}
diff --git a/ui/desktop/src/components/schedule/SchedulesView.tsx b/ui/desktop/src/components/schedule/SchedulesView.tsx
index 2ada601020..9a16ea29e9 100644
--- a/ui/desktop/src/components/schedule/SchedulesView.tsx
+++ b/ui/desktop/src/components/schedule/SchedulesView.tsx
@@ -564,7 +564,7 @@ const SchedulesView: React.FC
= ({ onClose: _onClose }) => {
-
Scheduler
+
Marketplace