diff --git a/ui/desktop/scripts/unregister-deeplink-protocols.js b/ui/desktop/scripts/unregister-deeplink-protocols.js index 402ec2d92a50..79ffbf6774f6 100755 --- a/ui/desktop/scripts/unregister-deeplink-protocols.js +++ b/ui/desktop/scripts/unregister-deeplink-protocols.js @@ -62,14 +62,6 @@ function unregisterAllProtocolHandlers() { } }); - // 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 { diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index 2fc1173d19df..73349198d524 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -184,7 +184,23 @@ const PairRouteWrapper = ({ // Check if we have a resumed session or recipe config from navigation state useEffect(() => { - // Only process if we actually have navigation state + const appConfig = window.appConfig?.get('recipe'); + if (appConfig && !chatRef.current.recipeConfig) { + const recipe = appConfig as Recipe; + + const updatedChat: ChatType = { + ...chatRef.current, + recipeConfig: recipe, + title: recipe.title || chatRef.current.title, + messages: [], // Start fresh for recipe from deeplink + messageHistoryIndex: 0, + }; + setChat(updatedChat); + setPairChat(updatedChat); + return; + } + + // Only process navigation state if we actually have it if (!location.state) { console.log('No navigation state, preserving existing chat state'); return; @@ -232,8 +248,6 @@ const PairRouteWrapper = ({ // Clear the navigation state to prevent reloading on navigation window.history.replaceState({}, document.title); } else if (recipeConfig && !chatRef.current.recipeConfig) { - // Only set recipe config if we don't already have one (e.g., from deeplinks) - const updatedChat: ChatType = { ...chatRef.current, recipeConfig: recipeConfig, @@ -824,6 +838,59 @@ export default function App() { return; } + // Check for recipe config - this also needs provider initialization + if (recipeConfig && typeof recipeConfig === 'object') { + console.log('Recipe deeplink detected, initializing system for recipe'); + + const initializeForRecipe = async () => { + try { + await initConfig(); + await readAllConfig({ throwOnError: true }); + + const config = window.electron.getConfig(); + const provider = (await read('GOOSE_PROVIDER', false)) ?? config.GOOSE_DEFAULT_PROVIDER; + const model = (await read('GOOSE_MODEL', false)) ?? config.GOOSE_DEFAULT_MODEL; + + if (provider && model) { + await initializeSystem(provider as string, model as string, { + getExtensions, + addExtension, + }); + + // Set up the recipe in pair chat after system is initialized + setPairChat((prevChat) => ({ + ...prevChat, + recipeConfig: recipeConfig as Recipe, + title: (recipeConfig as Recipe)?.title || 'Recipe Chat', + messages: [], // Start fresh for recipe + messageHistoryIndex: 0, + })); + + // Navigate to pair view + window.location.hash = '#/pair'; + window.history.replaceState( + { + recipeConfig: recipeConfig, + resetChat: true, + }, + '', + '#/pair' + ); + } else { + throw new Error('No provider/model configured for recipe'); + } + } catch (error) { + console.error('Failed to initialize system for recipe:', error); + setFatalError( + `Failed to initialize system for recipe: ${error instanceof Error ? error.message : 'Unknown error'}` + ); + } + }; + + initializeForRecipe(); + return; + } + if (viewType) { if (viewType === 'recipeEditor' && recipeConfig) { // Handle recipe editor deep link - use hash routing @@ -925,40 +992,6 @@ export default function App() { } await Promise.all(initPromises); - - const recipeConfig = window.appConfig.get('recipe'); - if ( - recipeConfig && - typeof recipeConfig === 'object' && - !window.sessionStorage.getItem('ignoreRecipeConfigChanges') - ) { - console.log( - 'Recipe deeplink detected, navigating to pair view with config:', - recipeConfig - ); - // Set the recipe config in the pair chat state - setPairChat((prevChat) => ({ - ...prevChat, - recipeConfig: recipeConfig as Recipe, - title: (recipeConfig as Recipe).title || 'Recipe Chat', - messages: [], // Start fresh for recipe - messageHistoryIndex: 0, - })); - // Navigate to pair view with recipe config using hash routing - window.location.hash = '#/pair'; - window.history.replaceState( - { - recipeConfig: recipeConfig, - resetChat: true, - }, - '', - '#/pair' - ); - } else if (window.sessionStorage.getItem('ignoreRecipeConfigChanges')) { - console.log( - 'Ignoring recipe config changes to prevent navigation conflicts with new window creation' - ); - } } catch (error) { console.error('Error in system initialization:', error); if (error instanceof MalformedConfigError) { @@ -1051,9 +1084,25 @@ export default function App() { // Handle recipe decode events from main process useEffect(() => { + const handleLoadRecipeDeeplink = (_event: IpcRendererEvent, ...args: unknown[]) => { + const recipeDeeplink = args[0] as string; + const scheduledJobId = args[1] as string | undefined; + + // Store the deeplink info in app config for processing + const config = window.electron.getConfig(); + config.recipeDeeplink = recipeDeeplink; + if (scheduledJobId) { + config.scheduledJobId = scheduledJobId; + } + + // Navigate to pair view to handle the recipe loading + if (window.location.hash !== '#/pair') { + window.location.hash = '#/pair'; + } + }; + 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) => ({ @@ -1079,14 +1128,16 @@ export default function App() { window.location.hash = '#/recipes'; }; + window.electron.on('load-recipe-deeplink', handleLoadRecipeDeeplink); window.electron.on('recipe-decoded', handleRecipeDecoded); window.electron.on('recipe-decode-error', handleRecipeDecodeError); return () => { + window.electron.off('load-recipe-deeplink', handleLoadRecipeDeeplink); window.electron.off('recipe-decoded', handleRecipeDecoded); window.electron.off('recipe-decode-error', handleRecipeDecodeError); }; - }, [setPairChat]); + }, [setPairChat, pairChat.id]); useEffect(() => { console.log('Setting up keyboard shortcuts'); diff --git a/ui/desktop/src/components/GooseSidebar/AppSidebar.tsx b/ui/desktop/src/components/GooseSidebar/AppSidebar.tsx index 49c9adea373b..0c2389ade119 100644 --- a/ui/desktop/src/components/GooseSidebar/AppSidebar.tsx +++ b/ui/desktop/src/components/GooseSidebar/AppSidebar.tsx @@ -14,6 +14,7 @@ import { import { ChatSmart, Gear } from '../icons'; import { ViewOptions, View } from '../../App'; import { useChatContext } from '../../contexts/ChatContext'; +import { DEFAULT_CHAT_TITLE } from '../../contexts/ChatContext'; interface SidebarProps { onSelectSession: (sessionId: string) => void; @@ -115,7 +116,7 @@ const AppSidebar: React.FC = ({ currentPath }) => { if ( currentPath === '/pair' && chatContext?.chat?.title && - chatContext.chat.title !== 'New Chat' + chatContext.chat.title !== DEFAULT_CHAT_TITLE ) { titleBits.push(chatContext.chat.title); } else if (currentPath !== '/' && currentItem) { diff --git a/ui/desktop/src/components/hub.tsx b/ui/desktop/src/components/hub.tsx index 5a9a715f302b..9ecc64bec7a2 100644 --- a/ui/desktop/src/components/hub.tsx +++ b/ui/desktop/src/components/hub.tsx @@ -30,6 +30,7 @@ import { ChatContextManagerProvider } from './context_management/ChatContextMana import 'react-toastify/dist/ReactToastify.css'; import { ChatType } from '../types/chat'; +import { DEFAULT_CHAT_TITLE } from '../contexts/ChatContext'; export default function Hub({ chat: _chat, @@ -57,7 +58,7 @@ export default function Hub({ const newChatId = generateSessionId(); const newPairChat = { id: newChatId, // This generates a unique ID each time - title: 'New Chat', + title: DEFAULT_CHAT_TITLE, messages: [], // Always start with empty messages messageHistoryIndex: 0, recipeConfig: null, // Clear recipe for new chats from Hub @@ -68,10 +69,10 @@ export default function Hub({ setPairChat(newPairChat); // Navigate to pair page with the message to be submitted immediately - // No delay needed since we're updating state synchronously setView('pair', { disableAnimation: true, initialMessage: combinedTextFromInput, + resetChat: true, }); } diff --git a/ui/desktop/src/components/pair.tsx b/ui/desktop/src/components/pair.tsx index ae068f46ba04..d932f437718f 100644 --- a/ui/desktop/src/components/pair.tsx +++ b/ui/desktop/src/components/pair.tsx @@ -35,6 +35,7 @@ import 'react-toastify/dist/ReactToastify.css'; import { cn } from '../utils'; import { ChatType } from '../types/chat'; +import { DEFAULT_CHAT_TITLE } from '../contexts/ChatContext'; export default function Pair({ chat, @@ -80,6 +81,22 @@ export default function Pair({ // Handle initial message from hub page useEffect(() => { const messageFromHub = location.state?.initialMessage; + const resetChat = location.state?.resetChat; + + // If we have a resetChat flag from Hub, clear any existing recipe config + // This scenario occurs when a user navigates from Hub to start a new chat, + // ensuring any previous recipe configuration is cleared for a fresh start + if (resetChat) { + const newChat: ChatType = { + ...chat, + recipeConfig: null, + recipeParameters: null, + title: DEFAULT_CHAT_TITLE, + messages: [], // Clear messages for fresh start + messageHistoryIndex: 0, + }; + setChat(newChat); + } // Reset processing state when we have a new message from hub if (messageFromHub) { @@ -100,7 +117,7 @@ export default function Pair({ window.history.replaceState({}, '', '/pair'); } } - }, [location.state, hasProcessedInitialInput, initialMessage, chat]); + }, [location.state, hasProcessedInitialInput, initialMessage, chat, setChat]); // Auto-submit the initial message after it's been set and component is ready useEffect(() => { diff --git a/ui/desktop/src/contexts/ChatContext.tsx b/ui/desktop/src/contexts/ChatContext.tsx index 7788ed70a7c8..8ae5ccd47d72 100644 --- a/ui/desktop/src/contexts/ChatContext.tsx +++ b/ui/desktop/src/contexts/ChatContext.tsx @@ -4,6 +4,8 @@ import { generateSessionId } from '../sessions'; import { Recipe } from '../recipe'; import { useDraftContext } from './DraftContext'; +export const DEFAULT_CHAT_TITLE = 'New Chat'; + interface ChatContextType { chat: ChatType; setChat: (chat: ChatType) => void; @@ -53,7 +55,7 @@ export const ChatProvider: React.FC = ({ const newSessionId = generateSessionId(); setChat({ id: newSessionId, - title: 'New Chat', + title: DEFAULT_CHAT_TITLE, messages: [], messageHistoryIndex: 0, recipeConfig: null, // Clear recipe when resetting chat diff --git a/ui/desktop/src/hooks/useChat.ts b/ui/desktop/src/hooks/useChat.ts index 7db74e5a6e65..755313c73d44 100644 --- a/ui/desktop/src/hooks/useChat.ts +++ b/ui/desktop/src/hooks/useChat.ts @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'; import { ChatType } from '../types/chat'; import { fetchSessionDetails, generateSessionId } from '../sessions'; import { View, ViewOptions } from '../App'; +import { DEFAULT_CHAT_TITLE } from '../contexts/ChatContext'; type UseChatArgs = { setIsLoadingSession: (isLoading: boolean) => void; @@ -11,7 +12,7 @@ type UseChatArgs = { export const useChat = ({ setIsLoadingSession, setView, setPairChat }: UseChatArgs) => { const [chat, setChat] = useState({ id: generateSessionId(), - title: 'New Chat', + title: DEFAULT_CHAT_TITLE, messages: [], messageHistoryIndex: 0, recipeConfig: null, // Initialize with no recipe diff --git a/ui/desktop/src/main.ts b/ui/desktop/src/main.ts index ea9721056f6c..4dda1a9a677f 100644 --- a/ui/desktop/src/main.ts +++ b/ui/desktop/src/main.ts @@ -735,6 +735,11 @@ const createChat = async ( : `?view=${encodeURIComponent(viewType)}`; } + // For recipe deeplinks, navigate directly to pair view + if (recipe || recipeDeeplink) { + queryParams = queryParams ? `${queryParams}&view=pair` : `?view=pair`; + } + // Increment window counter to track number of windows const windowId = ++windowCounter;