Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion ui/desktop/scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
```

95 changes: 95 additions & 0 deletions ui/desktop/scripts/unregister-deeplink-protocols.js
Original file line number Diff line number Diff line change
@@ -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' });
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks like it will only work on mac...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yup comment at top says only mac for now


// 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();
39 changes: 39 additions & 0 deletions ui/desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
8 changes: 4 additions & 4 deletions ui/desktop/src/components/BaseChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
(() => {
Expand All @@ -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 ? (
<Splash
<RecipeActivities
append={(text: string) => appendWithTracking(text)}
activities={
Array.isArray(recipeConfig.activities) ? recipeConfig.activities : null
Expand Down
Original file line number Diff line number Diff line change
@@ -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:"
Expand All @@ -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 (
<div className="flex flex-col px-6">
Expand All @@ -36,7 +36,10 @@ export default function Splash({ append, activities, title }: SplashProps) {

{messagePill && (
<div className="mb-4 p-3 rounded-lg border animate-[fadein_500ms_ease-in_forwards]">
{messagePill.replace(/^message:/i, '').trim()}
<MarkdownContent
content={messagePill.replace(/^message:/i, '').trim()}
className="text-sm"
/>
</div>
)}

Expand All @@ -56,23 +59,5 @@ export default function Splash({ append, activities, title }: SplashProps) {
);
}

// Default splash screen (no recipe) - show greeting and title if provided
return (
<div className="flex flex-col">
{title && (
<div className="flex items-center px-4 py-2 mb-4">
<span className="w-2 h-2 rounded-full bg-blockTeal mr-2" />
<span className="text-sm">
<span className="text-text-muted">Agent</span>{' '}
<span className="text-text-default">{title}</span>
</span>
</div>
)}

{/* Compact greeting section */}
<div className="flex flex-col px-6 mb-0">
<Greeting className="text-text-prominent text-4xl font-light mb-2" />
</div>
</div>
);
return null;
}
69 changes: 66 additions & 3 deletions ui/desktop/src/components/RecipeActivityEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { Button } from './ui/button';

export default function RecipeActivityEditor({
Expand All @@ -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()]);
Expand All @@ -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 (
<div>
<label htmlFor="activities" className="block text-md text-textProminent mb-2 font-bold">
Activities
</label>
<p className="text-textSubtle space-y-2 pb-2">
<p className="text-textSubtle space-y-2 pb-4">
The top-line prompts and activities that will display within your goose home page.
</p>

{/* Message Field */}
<div className="mb-6">
<label htmlFor="message" className="block text-sm font-medium text-textStandard mb-2">
Message (Optional)
</label>
<p className="text-xs text-textSubtle mb-2">
A formatted message that will appear at the top of the recipe. Supports markdown
formatting.
</p>
<textarea
id="message"
value={messageContent}
onChange={(e) => handleMessageChange(e.target.value)}
className="w-full px-4 py-3 border rounded-lg bg-background-default text-textStandard placeholder-textPlaceholder focus:outline-none focus:ring-2 focus:ring-borderProminent resize-vertical"
placeholder="Enter a message for your recipe (supports **bold**, *italic*, `code`, etc.)"
rows={3}
/>
</div>

{/* Regular Activities */}
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-textStandard mb-2">
Activity Buttons
</label>
<p className="text-xs text-textSubtle mb-3">
Clickable buttons that will appear below the message.
</p>
</div>

<div className="flex flex-wrap gap-3">
{activities.map((activity, index) => (
{nonMessageActivities.map((activity, index) => (
<div
key={index}
className="inline-flex items-center bg-background-default border-2 border-borderSubtle rounded-full px-4 py-2 text-sm text-textStandard"
Expand Down
Loading
Loading