From 3b81b9cd507a2b5e8f0291bc3652d4f2f4e85f4e Mon Sep 17 00:00:00 2001 From: Zane Staggs Date: Fri, 11 Jul 2025 14:41:22 -0700 Subject: [PATCH] persist recipe across route changes --- ui/desktop/src/App.tsx | 1 + ui/desktop/src/components/BaseChat.tsx | 2 ++ ui/desktop/src/components/hub.tsx | 3 +++ ui/desktop/src/components/pair.tsx | 3 +++ ui/desktop/src/contexts/ChatContext.tsx | 20 ++++++++++++++++ ui/desktop/src/hooks/useChat.ts | 2 ++ ui/desktop/src/hooks/useRecipeManager.ts | 29 ++++++++++++++++++++---- 7 files changed, 55 insertions(+), 5 deletions(-) diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index e7498fb3b9..60674db9ad 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -609,6 +609,7 @@ export default function App() { title: 'Pair Chat', messages: [], messageHistoryIndex: 0, + recipeConfig: null, // Initialize with no recipe }); const { getExtensions, addExtension, read } = useConfig(); diff --git a/ui/desktop/src/components/BaseChat.tsx b/ui/desktop/src/components/BaseChat.tsx index 48472e2a2f..a13e13fabf 100644 --- a/ui/desktop/src/components/BaseChat.tsx +++ b/ui/desktop/src/components/BaseChat.tsx @@ -67,6 +67,7 @@ import { useSessionContinuation } from '../hooks/useSessionContinuation'; import { useFileDrop } from '../hooks/useFileDrop'; import { useCostTracking } from '../hooks/useCostTracking'; import { Message } from '../types/message'; +import { Recipe } from '../recipe'; // Context for sharing current model info const CurrentModelContext = createContext<{ model: string; mode: string } | null>(null); @@ -77,6 +78,7 @@ export interface ChatType { title: string; messageHistoryIndex: number; messages: Message[]; + recipeConfig?: Recipe | null; // Add recipe configuration to chat state } interface BaseChatProps { diff --git a/ui/desktop/src/components/hub.tsx b/ui/desktop/src/components/hub.tsx index af2b44e04f..6ffa7d9067 100644 --- a/ui/desktop/src/components/hub.tsx +++ b/ui/desktop/src/components/hub.tsx @@ -27,6 +27,7 @@ import { SessionInsights } from './sessions/SessionsInsights'; import ChatInput from './ChatInput'; import { generateSessionId } from '../sessions'; import { ChatContextManagerProvider } from './context_management/ChatContextManager'; +import { Recipe } from '../recipe'; import 'react-toastify/dist/ReactToastify.css'; export interface ChatType { @@ -34,6 +35,7 @@ export interface ChatType { title: string; messageHistoryIndex: number; messages: Message[]; + recipeConfig?: Recipe | null; // Add recipe configuration to chat state } export default function Hub({ @@ -65,6 +67,7 @@ export default function Hub({ title: 'New Chat', messages: [], // Always start with empty messages messageHistoryIndex: 0, + recipeConfig: null, // Clear recipe for new chats from Hub }; // Update the PAIR chat state (not the hub chat state) diff --git a/ui/desktop/src/components/pair.tsx b/ui/desktop/src/components/pair.tsx index cd51d3dbb5..792a591db0 100644 --- a/ui/desktop/src/components/pair.tsx +++ b/ui/desktop/src/components/pair.tsx @@ -31,6 +31,7 @@ import { Message } from '../types/message'; import BaseChat from './BaseChat'; import ParameterInputModal from './ParameterInputModal'; import { useRecipeManager } from '../hooks/useRecipeManager'; +import { Recipe } from '../recipe'; import 'react-toastify/dist/ReactToastify.css'; export interface ChatType { @@ -38,6 +39,7 @@ export interface ChatType { title: string; messageHistoryIndex: number; messages: Message[]; + recipeConfig?: Recipe | null; // Add recipe configuration to chat state } export default function Pair({ @@ -74,6 +76,7 @@ export default function Pair({ title: location.state.recipeConfig.title || 'Recipe Chat', messages: [], // Clear messages to start fresh messageHistoryIndex: 0, + recipeConfig: location.state.recipeConfig, // Set the recipe config in chat state }; setChat(newChat); diff --git a/ui/desktop/src/contexts/ChatContext.tsx b/ui/desktop/src/contexts/ChatContext.tsx index fa66d4c25f..7854273235 100644 --- a/ui/desktop/src/contexts/ChatContext.tsx +++ b/ui/desktop/src/contexts/ChatContext.tsx @@ -1,12 +1,15 @@ import React, { createContext, useContext, ReactNode } from 'react'; import { ChatType } from '../components/BaseChat'; import { generateSessionId } from '../sessions'; +import { Recipe } from '../recipe'; interface ChatContextType { chat: ChatType; setChat: (chat: ChatType) => void; resetChat: () => void; hasActiveSession: boolean; + setRecipeConfig: (recipe: Recipe | null) => void; + clearRecipeConfig: () => void; } const ChatContext = createContext(undefined); @@ -25,6 +28,21 @@ export const ChatProvider: React.FC = ({ children, chat, setC title: 'New Chat', messages: [], messageHistoryIndex: 0, + recipeConfig: null, // Clear recipe when resetting chat + }); + }; + + const setRecipeConfig = (recipe: Recipe | null) => { + setChat({ + ...chat, + recipeConfig: recipe, + }); + }; + + const clearRecipeConfig = () => { + setChat({ + ...chat, + recipeConfig: null, }); }; @@ -35,6 +53,8 @@ export const ChatProvider: React.FC = ({ children, chat, setC setChat, resetChat, hasActiveSession, + setRecipeConfig, + clearRecipeConfig, }; return {children}; diff --git a/ui/desktop/src/hooks/useChat.ts b/ui/desktop/src/hooks/useChat.ts index 6b703f6deb..c3213b8f19 100644 --- a/ui/desktop/src/hooks/useChat.ts +++ b/ui/desktop/src/hooks/useChat.ts @@ -14,6 +14,7 @@ export const useChat = ({ setIsLoadingSession, setView, setPairChat }: UseChatAr title: 'New Chat', messages: [], messageHistoryIndex: 0, + recipeConfig: null, // Initialize with no recipe }); // Check for resumeSessionId in URL parameters @@ -37,6 +38,7 @@ export const useChat = ({ setIsLoadingSession, setView, setPairChat }: UseChatAr title: sessionDetails.metadata?.description || `ID: ${sessionDetails.session_id}`, messages: sessionDetails.messages, messageHistoryIndex: sessionDetails.messages.length, + recipeConfig: null, // Sessions don't have recipes by default }; setChat(sessionChat); diff --git a/ui/desktop/src/hooks/useRecipeManager.ts b/ui/desktop/src/hooks/useRecipeManager.ts index 0861db9a04..39598938c6 100644 --- a/ui/desktop/src/hooks/useRecipeManager.ts +++ b/ui/desktop/src/hooks/useRecipeManager.ts @@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from 'react'; import { createRecipe, Recipe } from '../recipe'; import { Message, createUserMessage } from '../types/message'; import { updateSystemPromptWithParameters } from '../utils/providerUtils'; +import { useChatContext } from '../contexts/ChatContext'; interface LocationState { recipeConfig?: Recipe; @@ -15,18 +16,36 @@ export const useRecipeManager = (messages: Message[], locationState?: LocationSt const [recipeParameters, setRecipeParameters] = useState | null>(null); const [readyForAutoUserPrompt, setReadyForAutoUserPrompt] = useState(false); + // Get chat context to access persisted recipe + const { chat, setRecipeConfig } = useChatContext(); + // Get recipeConfig from multiple sources with priority: - // 1. Navigation state (from recipes view) - // 2. App config (from deeplinks) + // 1. Chat context (persisted recipe) + // 2. Navigation state (from recipes view) + // 3. App config (from deeplinks) const recipeConfig = useMemo(() => { - // First check if recipe config is passed via navigation state + // First check if we have a persisted recipe in chat context + if (chat.recipeConfig) { + return chat.recipeConfig; + } + + // Then check if recipe config is passed via navigation state if (locationState?.recipeConfig) { + // Persist the recipe to chat context for future use + setRecipeConfig(locationState.recipeConfig); return locationState.recipeConfig as Recipe; } // Fallback to app config (from deeplinks) - return window.appConfig.get('recipeConfig') as Recipe | null; - }, [locationState]); + const appRecipeConfig = window.appConfig.get('recipeConfig') as Recipe | null; + if (appRecipeConfig) { + // Persist the recipe to chat context for future use + setRecipeConfig(appRecipeConfig); + return appRecipeConfig; + } + + return null; + }, [chat.recipeConfig, locationState, setRecipeConfig]); // Show parameter modal if recipe has parameters and they haven't been set yet useEffect(() => {