Load recipe deeplinks in single window when app is closed (#4048)
This commit is contained in:
@@ -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
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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,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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user