Load recipe deeplinks in single window when app is closed (#4048)

This commit is contained in:
Zane
2025-08-13 14:52:33 -07:00
committed by GitHub
parent 1a58892948
commit 68dd3281f8
8 changed files with 123 additions and 53 deletions
@@ -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 // Force Launch Services to rebuild its database
console.log('Rebuilding Launch Services database...'); console.log('Rebuilding Launch Services database...');
try { try {
+90 -39
View File
@@ -184,7 +184,23 @@ const PairRouteWrapper = ({
// Check if we have a resumed session or recipe config from navigation state // Check if we have a resumed session or recipe config from navigation state
useEffect(() => { 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) { if (!location.state) {
console.log('No navigation state, preserving existing chat state'); console.log('No navigation state, preserving existing chat state');
return; return;
@@ -232,8 +248,6 @@ const PairRouteWrapper = ({
// Clear the navigation state to prevent reloading on navigation // Clear the navigation state to prevent reloading on navigation
window.history.replaceState({}, document.title); window.history.replaceState({}, document.title);
} else if (recipeConfig && !chatRef.current.recipeConfig) { } else if (recipeConfig && !chatRef.current.recipeConfig) {
// Only set recipe config if we don't already have one (e.g., from deeplinks)
const updatedChat: ChatType = { const updatedChat: ChatType = {
...chatRef.current, ...chatRef.current,
recipeConfig: recipeConfig, recipeConfig: recipeConfig,
@@ -824,6 +838,59 @@ export default function App() {
return; 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) {
if (viewType === 'recipeEditor' && recipeConfig) { if (viewType === 'recipeEditor' && recipeConfig) {
// Handle recipe editor deep link - use hash routing // Handle recipe editor deep link - use hash routing
@@ -925,40 +992,6 @@ export default function App() {
} }
await Promise.all(initPromises); 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) { } catch (error) {
console.error('Error in system initialization:', error); console.error('Error in system initialization:', error);
if (error instanceof MalformedConfigError) { if (error instanceof MalformedConfigError) {
@@ -1051,9 +1084,25 @@ export default function App() {
// Handle recipe decode events from main process // Handle recipe decode events from main process
useEffect(() => { 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 handleRecipeDecoded = (_event: IpcRendererEvent, ...args: unknown[]) => {
const decodedRecipe = args[0] as Recipe; const decodedRecipe = args[0] as Recipe;
console.log('[App] Recipe decoded successfully:', decodedRecipe);
// Update the pair chat with the decoded recipe // Update the pair chat with the decoded recipe
setPairChat((prevChat) => ({ setPairChat((prevChat) => ({
@@ -1079,14 +1128,16 @@ export default function App() {
window.location.hash = '#/recipes'; window.location.hash = '#/recipes';
}; };
window.electron.on('load-recipe-deeplink', handleLoadRecipeDeeplink);
window.electron.on('recipe-decoded', handleRecipeDecoded); window.electron.on('recipe-decoded', handleRecipeDecoded);
window.electron.on('recipe-decode-error', handleRecipeDecodeError); window.electron.on('recipe-decode-error', handleRecipeDecodeError);
return () => { return () => {
window.electron.off('load-recipe-deeplink', handleLoadRecipeDeeplink);
window.electron.off('recipe-decoded', handleRecipeDecoded); window.electron.off('recipe-decoded', handleRecipeDecoded);
window.electron.off('recipe-decode-error', handleRecipeDecodeError); window.electron.off('recipe-decode-error', handleRecipeDecodeError);
}; };
}, [setPairChat]); }, [setPairChat, pairChat.id]);
useEffect(() => { useEffect(() => {
console.log('Setting up keyboard shortcuts'); console.log('Setting up keyboard shortcuts');
@@ -14,6 +14,7 @@ import {
import { ChatSmart, Gear } from '../icons'; import { ChatSmart, Gear } from '../icons';
import { ViewOptions, View } from '../../App'; import { ViewOptions, View } from '../../App';
import { useChatContext } from '../../contexts/ChatContext'; import { useChatContext } from '../../contexts/ChatContext';
import { DEFAULT_CHAT_TITLE } from '../../contexts/ChatContext';
interface SidebarProps { interface SidebarProps {
onSelectSession: (sessionId: string) => void; onSelectSession: (sessionId: string) => void;
@@ -115,7 +116,7 @@ const AppSidebar: React.FC<SidebarProps> = ({ currentPath }) => {
if ( if (
currentPath === '/pair' && currentPath === '/pair' &&
chatContext?.chat?.title && chatContext?.chat?.title &&
chatContext.chat.title !== 'New Chat' chatContext.chat.title !== DEFAULT_CHAT_TITLE
) { ) {
titleBits.push(chatContext.chat.title); titleBits.push(chatContext.chat.title);
} else if (currentPath !== '/' && currentItem) { } else if (currentPath !== '/' && currentItem) {
+3 -2
View File
@@ -30,6 +30,7 @@ import { ChatContextManagerProvider } from './context_management/ChatContextMana
import 'react-toastify/dist/ReactToastify.css'; import 'react-toastify/dist/ReactToastify.css';
import { ChatType } from '../types/chat'; import { ChatType } from '../types/chat';
import { DEFAULT_CHAT_TITLE } from '../contexts/ChatContext';
export default function Hub({ export default function Hub({
chat: _chat, chat: _chat,
@@ -57,7 +58,7 @@ export default function Hub({
const newChatId = generateSessionId(); const newChatId = generateSessionId();
const newPairChat = { const newPairChat = {
id: newChatId, // This generates a unique ID each time id: newChatId, // This generates a unique ID each time
title: 'New Chat', title: DEFAULT_CHAT_TITLE,
messages: [], // Always start with empty messages messages: [], // Always start with empty messages
messageHistoryIndex: 0, messageHistoryIndex: 0,
recipeConfig: null, // Clear recipe for new chats from Hub recipeConfig: null, // Clear recipe for new chats from Hub
@@ -68,10 +69,10 @@ export default function Hub({
setPairChat(newPairChat); setPairChat(newPairChat);
// Navigate to pair page with the message to be submitted immediately // Navigate to pair page with the message to be submitted immediately
// No delay needed since we're updating state synchronously
setView('pair', { setView('pair', {
disableAnimation: true, disableAnimation: true,
initialMessage: combinedTextFromInput, initialMessage: combinedTextFromInput,
resetChat: true,
}); });
} }
+18 -1
View File
@@ -35,6 +35,7 @@ import 'react-toastify/dist/ReactToastify.css';
import { cn } from '../utils'; import { cn } from '../utils';
import { ChatType } from '../types/chat'; import { ChatType } from '../types/chat';
import { DEFAULT_CHAT_TITLE } from '../contexts/ChatContext';
export default function Pair({ export default function Pair({
chat, chat,
@@ -80,6 +81,22 @@ export default function Pair({
// Handle initial message from hub page // Handle initial message from hub page
useEffect(() => { useEffect(() => {
const messageFromHub = location.state?.initialMessage; 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 // Reset processing state when we have a new message from hub
if (messageFromHub) { if (messageFromHub) {
@@ -100,7 +117,7 @@ export default function Pair({
window.history.replaceState({}, '', '/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 // Auto-submit the initial message after it's been set and component is ready
useEffect(() => { useEffect(() => {
+3 -1
View File
@@ -4,6 +4,8 @@ import { generateSessionId } from '../sessions';
import { Recipe } from '../recipe'; import { Recipe } from '../recipe';
import { useDraftContext } from './DraftContext'; import { useDraftContext } from './DraftContext';
export const DEFAULT_CHAT_TITLE = 'New Chat';
interface ChatContextType { interface ChatContextType {
chat: ChatType; chat: ChatType;
setChat: (chat: ChatType) => void; setChat: (chat: ChatType) => void;
@@ -53,7 +55,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
const newSessionId = generateSessionId(); const newSessionId = generateSessionId();
setChat({ setChat({
id: newSessionId, id: newSessionId,
title: 'New Chat', title: DEFAULT_CHAT_TITLE,
messages: [], messages: [],
messageHistoryIndex: 0, messageHistoryIndex: 0,
recipeConfig: null, // Clear recipe when resetting chat recipeConfig: null, // Clear recipe when resetting chat
+2 -1
View File
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
import { ChatType } from '../types/chat'; import { ChatType } from '../types/chat';
import { fetchSessionDetails, generateSessionId } from '../sessions'; import { fetchSessionDetails, generateSessionId } from '../sessions';
import { View, ViewOptions } from '../App'; import { View, ViewOptions } from '../App';
import { DEFAULT_CHAT_TITLE } from '../contexts/ChatContext';
type UseChatArgs = { type UseChatArgs = {
setIsLoadingSession: (isLoading: boolean) => void; setIsLoadingSession: (isLoading: boolean) => void;
@@ -11,7 +12,7 @@ type UseChatArgs = {
export const useChat = ({ setIsLoadingSession, setView, setPairChat }: UseChatArgs) => { export const useChat = ({ setIsLoadingSession, setView, setPairChat }: UseChatArgs) => {
const [chat, setChat] = useState<ChatType>({ const [chat, setChat] = useState<ChatType>({
id: generateSessionId(), id: generateSessionId(),
title: 'New Chat', title: DEFAULT_CHAT_TITLE,
messages: [], messages: [],
messageHistoryIndex: 0, messageHistoryIndex: 0,
recipeConfig: null, // Initialize with no recipe recipeConfig: null, // Initialize with no recipe
+5
View File
@@ -735,6 +735,11 @@ const createChat = async (
: `?view=${encodeURIComponent(viewType)}`; : `?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 // Increment window counter to track number of windows
const windowId = ++windowCounter; const windowId = ++windowCounter;