diff --git a/ui/desktop/scripts/README.md b/ui/desktop/scripts/README.md index 92c8313fe675..a5b325aa5459 100644 --- a/ui/desktop/scripts/README.md +++ b/ui/desktop/scripts/README.md @@ -6,4 +6,19 @@ Put `goosey` in your $PATH if you want to launch via: goosey . ``` -This will open goose GUI from any path you specify \ No newline at end of file +This will open goose GUI from any path you specify + +# Unregister Deeplink Protocols (macos only) + +`unregister-deeplink-protocols.js` is a script to unregister the deeplink protocol used by goose like `goose://`. +This is handy when you want to test deeplinks with the development version of Goose. + +# Usage + +To unregister the deeplink protocols, run the following command in your terminal: +Then launch Goose again and your deeplinks should work from the latest launched goose application as it is registered on startup. + +```bash +node scripts/unregister-deeplink-protocols.js +``` + diff --git a/ui/desktop/scripts/unregister-deeplink-protocols.js b/ui/desktop/scripts/unregister-deeplink-protocols.js new file mode 100755 index 000000000000..402ec2d92a50 --- /dev/null +++ b/ui/desktop/scripts/unregister-deeplink-protocols.js @@ -0,0 +1,95 @@ +#!/usr/bin/env node + +/** + * Script to unregister ALL goose:// protocol handlers + * Usage: node scripts/unregister-deeplink-protocols.js + */ + +const { execSync } = require('child_process'); + +const PROTOCOL = 'goose'; + +function unregisterAllProtocolHandlers() { + console.log('Unregistering ALL goose:// protocol handlers...'); + + try { + // Get all registered Goose apps + console.log('Finding all registered Goose applications...'); + const lsregisterOutput = execSync(`/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister -dump | grep -B 10 -A 10 "claimed schemes:.*${PROTOCOL}:"`, { encoding: 'utf8' }); + + // Extract app paths from the output + const pathMatches = lsregisterOutput.match(/path:\s+(.+\.app)/g); + const uniquePaths = new Set(); + + if (pathMatches) { + pathMatches.forEach(match => { + const path = match.replace(/path:\s+/, '').trim(); + if (path.includes('Goose') || path.includes('goose')) { + uniquePaths.add(path); + } + }); + } + + console.log(`Found ${uniquePaths.size} Goose app(s) to unregister:`); + uniquePaths.forEach(path => console.log(` - ${path}`)); + + // Unregister each app + let unregisteredCount = 0; + uniquePaths.forEach(appPath => { + try { + console.log(`Unregistering: ${appPath}`); + execSync(`/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister -u "${appPath}"`, { stdio: 'ignore' }); + unregisteredCount++; + } catch (error) { + console.log(` Warning: Could not unregister ${appPath} (may already be unregistered)`); + } + }); + + // Also try to unregister by bundle identifier + console.log('\nUnregistering by bundle identifier...'); + const bundleIds = [ + 'com.electron.goose', + 'com.block.goose', + 'com.block.goose.dev' + ]; + + bundleIds.forEach(bundleId => { + try { + console.log(`Unregistering bundle: ${bundleId}`); + execSync(`/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister -u "${bundleId}"`, { stdio: 'ignore' }); + } catch (error) { + // Ignore errors for bundle IDs that don't exist + } + }); + + // Clean up temporary files + console.log('\nCleaning up temporary files...'); + try { + execSync('rm -rf /tmp/GooseDevApp.app', { stdio: 'ignore' }); + } catch (error) { + // Ignore cleanup errors + } + + // Force Launch Services to rebuild its database + console.log('Rebuilding Launch Services database...'); + try { + execSync('/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister -kill -r -domain local -domain system -domain user', { stdio: 'ignore' }); + } catch (error) { + console.log('Warning: Could not rebuild Launch Services database'); + } + + console.log(`\n✅ Successfully processed ${unregisteredCount} Goose applications`); + console.log('All goose:// protocol handlers have been unregistered.'); + console.log('\nNote: You may need to restart your system for changes to take full effect.'); + + } catch (error) { + console.error('Error during unregistration:', error.message); + console.log('\nManual cleanup options:'); + console.log('1. Use Activity Monitor to quit all Goose processes'); + console.log('2. Delete Goose apps from Applications folder'); + console.log('3. Run: sudo /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister -kill -r -domain local -domain system -domain user'); + } +} + +// Run the unregistration immediately +unregisterAllProtocolHandlers(); diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index 79c3ca219c7b..9e8fc0b883d4 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -1063,6 +1063,45 @@ export default function App() { }; }, [setSharedSessionError]); + // Handle recipe decode events from main process + useEffect(() => { + const handleRecipeDecoded = (_event: IpcRendererEvent, ...args: unknown[]) => { + const decodedRecipe = args[0] as Recipe; + console.log('[App] Recipe decoded successfully:', decodedRecipe); + + // Update the pair chat with the decoded recipe + setPairChat((prevChat) => ({ + ...prevChat, + recipeConfig: decodedRecipe, + title: decodedRecipe.title || 'Recipe Chat', + messages: [], // Start fresh for recipe + messageHistoryIndex: 0, + })); + + // Navigate to pair view if not already there + if (window.location.hash !== '#/pair') { + window.location.hash = '#/pair'; + } + }; + + const handleRecipeDecodeError = (_event: IpcRendererEvent, ...args: unknown[]) => { + const errorMessage = args[0] as string; + console.error('[App] Recipe decode error:', errorMessage); + + // Show error to user - you could add a toast notification here + // For now, just log the error and navigate to recipes page + window.location.hash = '#/recipes'; + }; + + window.electron.on('recipe-decoded', handleRecipeDecoded); + window.electron.on('recipe-decode-error', handleRecipeDecodeError); + + return () => { + window.electron.off('recipe-decoded', handleRecipeDecoded); + window.electron.off('recipe-decode-error', handleRecipeDecodeError); + }; + }, [setPairChat]); + useEffect(() => { console.log('Setting up keyboard shortcuts'); const handleKeyDown = (event: KeyboardEvent) => { diff --git a/ui/desktop/src/components/BaseChat.tsx b/ui/desktop/src/components/BaseChat.tsx index 9897c0f49953..e43762a074b3 100644 --- a/ui/desktop/src/components/BaseChat.tsx +++ b/ui/desktop/src/components/BaseChat.tsx @@ -48,7 +48,7 @@ import { SearchView } from './conversation/SearchView'; import { AgentHeader } from './AgentHeader'; import LayingEggLoader from './LayingEggLoader'; import LoadingGoose from './LoadingGoose'; -import Splash from './Splash'; +import RecipeActivities from './RecipeActivities'; import PopularChatTopics from './PopularChatTopics'; import ProgressiveMessageList from './ProgressiveMessageList'; import { SessionSummaryModal } from './context_management/SessionSummaryModal'; @@ -366,7 +366,7 @@ function BaseChatContent({ {/* Custom content before messages */} {renderBeforeMessages && renderBeforeMessages()} - {/* Messages or Splash or Popular Topics */} + {/* Messages or RecipeActivities or Popular Topics */} { // Check if we should show splash instead of messages (() => { @@ -377,9 +377,9 @@ function BaseChatContent({ return shouldShowSplash; })() ? ( <> - {/* Show Splash when we have a recipe config and user hasn't started using it */} + {/* Show RecipeActivities when we have a recipe config and user hasn't started using it */} {recipeConfig ? ( - appendWithTracking(text)} activities={ Array.isArray(recipeConfig.activities) ? recipeConfig.activities : null diff --git a/ui/desktop/src/components/Splash.tsx b/ui/desktop/src/components/RecipeActivities.tsx similarity index 63% rename from ui/desktop/src/components/Splash.tsx rename to ui/desktop/src/components/RecipeActivities.tsx index b88e5df62576..4bf90ce685e7 100644 --- a/ui/desktop/src/components/Splash.tsx +++ b/ui/desktop/src/components/RecipeActivities.tsx @@ -1,18 +1,18 @@ import { Card } from './ui/card'; import { gsap } from 'gsap'; -import { Greeting } from './common/Greeting'; import GooseLogo from './GooseLogo'; +import MarkdownContent from './MarkdownContent'; // Register GSAP plugins gsap.registerPlugin(); -interface SplashProps { +interface RecipeActivitiesProps { append: (text: string) => void; activities: string[] | null; title?: string; } -export default function Splash({ append, activities, title }: SplashProps) { +export default function RecipeActivities({ append, activities }: RecipeActivitiesProps) { const pills = activities || []; // Find any pill that starts with "message:" @@ -25,7 +25,7 @@ export default function Splash({ append, activities, title }: SplashProps) { ? [...pills.slice(0, messagePillIndex), ...pills.slice(messagePillIndex + 1)] : pills; - // If we have activities (recipe mode), show a simplified version without greeting + // If we have activities or instructions (recipe mode), show a simplified version without greeting if (activities && activities.length > 0) { return (
@@ -36,7 +36,10 @@ export default function Splash({ append, activities, title }: SplashProps) { {messagePill && (
- {messagePill.replace(/^message:/i, '').trim()} +
)} @@ -56,23 +59,5 @@ export default function Splash({ append, activities, title }: SplashProps) { ); } - // Default splash screen (no recipe) - show greeting and title if provided - return ( -
- {title && ( -
- - - Agent{' '} - {title} - -
- )} - - {/* Compact greeting section */} -
- -
-
- ); + return null; } diff --git a/ui/desktop/src/components/RecipeActivityEditor.tsx b/ui/desktop/src/components/RecipeActivityEditor.tsx index 48e191a0138d..beba0af67054 100644 --- a/ui/desktop/src/components/RecipeActivityEditor.tsx +++ b/ui/desktop/src/components/RecipeActivityEditor.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { Button } from './ui/button'; export default function RecipeActivityEditor({ @@ -9,6 +9,23 @@ export default function RecipeActivityEditor({ setActivities: (prev: string[]) => void; }) { const [newActivity, setNewActivity] = useState(''); + const [messageContent, setMessageContent] = useState(''); + + // Extract message content from activities on component mount and when activities change + useEffect(() => { + const messageActivity = activities.find((activity) => + activity.toLowerCase().startsWith('message:') + ); + if (messageActivity) { + setMessageContent(messageActivity.replace(/^message:/i, '').trim()); + } + }, [activities]); + + // Get activities that are not messages + const nonMessageActivities = activities.filter( + (activity) => !activity.toLowerCase().startsWith('message:') + ); + const handleAddActivity = () => { if (newActivity.trim()) { setActivities([...activities, newActivity.trim()]); @@ -19,17 +36,63 @@ export default function RecipeActivityEditor({ const handleRemoveActivity = (activity: string) => { setActivities(activities.filter((a) => a !== activity)); }; + + const handleMessageChange = (value: string) => { + setMessageContent(value); + + // Update activities array - remove existing message and add new one if not empty + const otherActivities = activities.filter( + (activity) => !activity.toLowerCase().startsWith('message:') + ); + + if (value.trim()) { + setActivities([`message:${value}`, ...otherActivities]); + } else { + setActivities(otherActivities); + } + }; + return (
-

+

The top-line prompts and activities that will display within your goose home page.

+ + {/* Message Field */} +
+ +

+ A formatted message that will appear at the top of the recipe. Supports markdown + formatting. +

+