Fix recipe creation not working from current chat session

This commit is contained in:
Zane Staggs
2025-07-15 13:57:51 -07:00
parent 92f3d7f672
commit 3fb0bc542a
9 changed files with 156 additions and 68 deletions
+5 -2
View File
@@ -168,7 +168,7 @@ async fn get_session_insights(
// Calculate insights
let total_sessions = sessions.len();
// Debug: Log if we have very few sessions, which might indicate filtering issues
if total_sessions == 0 {
info!("Warning: No sessions found with descriptions");
@@ -191,7 +191,10 @@ async fn get_session_insights(
total_tokens += tokens as i64;
} else if tokens < 0 {
// Log negative token values for debugging
info!("Warning: Session {} has negative accumulated_total_tokens: {}", session.id, tokens);
info!(
"Warning: Session {} has negative accumulated_total_tokens: {}",
session.id, tokens
);
}
}
+35 -17
View File
@@ -431,7 +431,8 @@ const RecipeEditorRoute = () => {
}
if (!config) {
config = window.electron.getConfig().recipeConfig;
const electronConfig = window.electron.getConfig();
config = electronConfig.recipeConfig;
}
return <RecipeEditor config={config} />;
@@ -767,27 +768,28 @@ export default function App() {
if (viewType) {
if (viewType === 'recipeEditor' && recipeConfig) {
console.log('Setting view to recipeEditor with config:', recipeConfig);
// Handle recipe editor deep link
window.history.replaceState({ config: recipeConfig }, '', '/recipe-editor');
// Handle recipe editor deep link - use hash routing
window.location.hash = '#/recipe-editor';
window.history.replaceState({ config: recipeConfig }, '', '#/recipe-editor');
} else {
// Handle other deep links by redirecting to appropriate route
const routeMap: Record<string, string> = {
chat: '/',
pair: '/pair',
settings: '/settings',
sessions: '/sessions',
schedules: '/schedules',
recipes: '/recipes',
permission: '/permission',
ConfigureProviders: '/configure-providers',
sharedSession: '/shared-session',
recipeEditor: '/recipe-editor',
welcome: '/welcome',
chat: '#/',
pair: '#/pair',
settings: '#/settings',
sessions: '#/sessions',
schedules: '#/schedules',
recipes: '#/recipes',
permission: '#/permission',
ConfigureProviders: '#/configure-providers',
sharedSession: '#/shared-session',
recipeEditor: '#/recipe-editor',
welcome: '#/welcome',
};
const route = routeMap[viewType];
if (route) {
window.location.hash = route;
window.history.replaceState({}, '', route);
}
}
@@ -859,7 +861,12 @@ export default function App() {
});
// Check if we have a recipe config from a deeplink
if (recipeConfig && typeof recipeConfig === 'object') {
// But skip navigation if we're ignoring recipe config changes (to prevent conflicts with new window creation)
if (
recipeConfig &&
typeof recipeConfig === 'object' &&
!window.sessionStorage.getItem('ignoreRecipeConfigChanges')
) {
console.log(
'Recipe deeplink detected, navigating to pair view with config:',
recipeConfig
@@ -882,6 +889,10 @@ export default function App() {
'',
'#/pair'
);
} else if (window.sessionStorage.getItem('ignoreRecipeConfigChanges')) {
console.log(
'Ignoring recipe config changes to prevent navigation conflicts with new window creation'
);
} else {
// Only navigate to chat route if we're not already on a valid route
const currentHash = window.location.hash;
@@ -941,12 +952,19 @@ export default function App() {
// Handle navigation to pair view for recipe deeplinks after router is ready
useEffect(() => {
const recipeConfig = window.appConfig.get('recipeConfig');
if (recipeConfig && typeof recipeConfig === 'object' && window.location.hash === '#/') {
if (
recipeConfig &&
typeof recipeConfig === 'object' &&
window.location.hash === '#/' &&
!window.sessionStorage.getItem('ignoreRecipeConfigChanges')
) {
console.log('Router ready - navigating to pair view for recipe deeplink:', recipeConfig);
// Small delay to ensure router is fully initialized
setTimeout(() => {
window.location.hash = '#/pair';
}, 100);
} else if (window.sessionStorage.getItem('ignoreRecipeConfigChanges')) {
console.log('Router ready - ignoring recipe config navigation due to new window creation');
}
}, []);
+26 -4
View File
@@ -188,10 +188,14 @@ function BaseChatContent({
});
// Use shared recipe manager
const { recipeConfig, initialPrompt, isGeneratingRecipe, handleAutoExecution } = useRecipeManager(
messages,
location.state
);
const {
recipeConfig,
initialPrompt,
isGeneratingRecipe,
handleAutoExecution,
recipeError,
setRecipeError,
} = useRecipeManager(messages, location.state);
// Reset recipe usage tracking when recipe changes
useEffect(() => {
@@ -487,6 +491,24 @@ function BaseChatContent({
}}
summaryContent={summaryContent}
/>
{/* Recipe Error Modal */}
{recipeError && (
<div className="fixed inset-0 z-[300] flex items-center justify-center bg-black/50">
<div className="bg-background-default border border-borderSubtle rounded-lg p-6 w-96 max-w-[90vw]">
<h3 className="text-lg font-medium text-textProminent mb-4">Recipe Creation Failed</h3>
<p className="text-textStandard mb-6">{recipeError}</p>
<div className="flex justify-end">
<button
onClick={() => setRecipeError(null)}
className="px-4 py-2 bg-textProminent text-bgApp rounded-lg hover:bg-opacity-90 transition-colors"
>
OK
</button>
</div>
</div>
</div>
)}
</div>
);
}
+1
View File
@@ -1215,6 +1215,7 @@ export default function ChatInput({
setView={setView}
alerts={alerts}
recipeConfig={recipeConfig}
hasMessages={messages.length > 0}
/>
</div>
</Tooltip>
@@ -80,30 +80,18 @@ export default function ProgressiveMessageList({
// Simple progressive loading - start immediately when component mounts if needed
useEffect(() => {
console.log(
`ProgressiveMessageList: ${messages.length} messages, threshold: ${showLoadingThreshold}, renderedCount: ${renderedCount}`
);
if (messages.length <= showLoadingThreshold) {
// Small list - render all immediately
console.log('ProgressiveMessageList: Small list, rendering all immediately');
setRenderedCount(messages.length);
setIsLoading(false);
return;
}
console.log('ProgressiveMessageList: Large list, starting progressive loading');
// Large list - start progressive loading
const loadNextBatch = () => {
setRenderedCount((current) => {
const nextCount = Math.min(current + batchSize, messages.length);
console.log(
`ProgressiveMessageList: Loading batch, now showing ${nextCount}/${messages.length}`
);
if (nextCount >= messages.length) {
console.log('ProgressiveMessageList: All messages loaded');
setIsLoading(false);
// Trigger scroll to bottom
window.setTimeout(() => {
+16 -21
View File
@@ -352,7 +352,7 @@ export default function RecipeEditor({ config }: RecipeEditorProps) {
setErrors({ ...errors, title: undefined });
}
}}
className={`w-full p-3 border rounded-lg bg-background-default text-textStandard focus:outline-none focus:ring-2 focus:ring-borderProminent ${
className={`w-full max-w-full p-3 border rounded-lg bg-background-default text-textStandard focus:outline-none focus:ring-2 focus:ring-borderProminent overflow-hidden ${
errors.title ? 'border-red-500' : 'border-borderSubtle'
}`}
placeholder="Agent Recipe Title (required)"
@@ -375,7 +375,7 @@ export default function RecipeEditor({ config }: RecipeEditorProps) {
setErrors({ ...errors, description: undefined });
}
}}
className={`w-full p-3 border rounded-lg bg-background-default text-textStandard focus:outline-none focus:ring-2 focus:ring-borderProminent ${
className={`w-full max-w-full p-3 border rounded-lg bg-background-default text-textStandard focus:outline-none focus:ring-2 focus:ring-borderProminent overflow-hidden ${
errors.description ? 'border-red-500' : 'border-borderSubtle'
}`}
placeholder="Description (required)"
@@ -423,21 +423,21 @@ export default function RecipeEditor({ config }: RecipeEditorProps) {
</div>
{/* Deep Link Display */}
<div className="w-full p-4 bg-bgSubtle rounded-lg">
<div className="w-full p-4 bg-bgSubtle rounded-lg overflow-hidden">
{!requiredFieldsAreFilled() ? (
<div className="text-sm text-textSubtle text-xs text-textSubtle">
Fill in required fields to generate link
</div>
) : (
<div className="flex items-center justify-between mb-2">
<div className="text-sm text-textSubtle text-xs text-textSubtle">
<div className="flex items-center justify-between mb-2 gap-4">
<div className="text-sm text-textSubtle text-xs text-textSubtle flex-shrink-0">
Copy this link to share with friends or paste directly in Chrome to open
</div>
<Button
onClick={() => validateForm() && handleCopy()}
variant="ghost"
size="sm"
className="ml-4 p-2 hover:bg-background-default rounded-lg transition-colors flex items-center disabled:opacity-50 disabled:hover:bg-transparent"
className="p-2 hover:bg-background-default rounded-lg transition-colors flex items-center disabled:opacity-50 disabled:hover:bg-transparent flex-shrink-0"
>
{copied ? (
<Check className="w-4 h-4 text-green-500" />
@@ -451,11 +451,14 @@ export default function RecipeEditor({ config }: RecipeEditorProps) {
</div>
)}
{requiredFieldsAreFilled() && (
<div
onClick={() => validateForm() && handleCopy()}
className={`text-sm truncate dark:text-white font-mono ${!title.trim() || !description.trim() ? 'text-textDisabled' : 'text-textStandard'}`}
>
{deeplink}
<div className="w-full overflow-hidden">
<div
onClick={() => validateForm() && handleCopy()}
className={`text-sm dark:text-white font-mono cursor-pointer hover:bg-background-default p-2 rounded transition-colors overflow-x-auto whitespace-nowrap ${!title.trim() || !description.trim() ? 'text-textDisabled' : 'text-textStandard'}`}
style={{ maxWidth: '500px', width: '100%' }}
>
{deeplink}
</div>
</div>
)}
</div>
@@ -507,17 +510,9 @@ export default function RecipeEditor({ config }: RecipeEditorProps) {
onClose={() => setIsScheduleModalOpen(false)}
recipe={getCurrentConfig()}
onCreateSchedule={(deepLink) => {
// Open the schedules view with the deep link pre-filled
window.electron.createChatWindow(
undefined,
undefined,
undefined,
undefined,
undefined,
'schedules'
);
// Store the deep link in localStorage for the schedules view to pick up
// Navigate to the schedules view with the deep link pre-filled
localStorage.setItem('pendingScheduleDeepLink', deepLink);
navigate('/schedules');
}}
/>
@@ -25,12 +25,14 @@ interface ModelsBottomBarProps {
setView: (view: View) => void;
alerts: Alert[];
recipeConfig?: Recipe | null;
hasMessages?: boolean; // Add prop to know if there are messages to create a recipe from
}
export default function ModelsBottomBar({
dropdownRef,
setView,
alerts,
recipeConfig,
hasMessages = false,
}: ModelsBottomBarProps) {
const {
currentModel,
@@ -243,10 +245,18 @@ export default function ModelsBottomBar({
)}
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => setView('recipeEditor')}>
<span>Create a recipe</span>
<ChefHat className="ml-auto h-4 w-4" />
</DropdownMenuItem>
{/* Only show "Create a recipe from this session" when there are messages to create from */}
{hasMessages && (
<DropdownMenuItem
onClick={() => {
// Signal to create an agent from the current chat
window.dispatchEvent(new CustomEvent('make-agent-from-chat'));
}}
>
<span>Create a recipe from this session</span>
<ChefHat className="ml-auto h-4 w-4" />
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
+48 -8
View File
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react';
import { useEffect, useMemo, useState, useRef } from 'react';
import { createRecipe, Recipe } from '../recipe';
import { Message, createUserMessage } from '../types/message';
import { updateSystemPromptWithParameters } from '../utils/providerUtils';
@@ -15,10 +15,20 @@ export const useRecipeManager = (messages: Message[], locationState?: LocationSt
const [isParameterModalOpen, setIsParameterModalOpen] = useState(false);
const [recipeParameters, setRecipeParameters] = useState<Record<string, string> | null>(null);
const [readyForAutoUserPrompt, setReadyForAutoUserPrompt] = useState(false);
const [recipeError, setRecipeError] = useState<string | null>(null);
// Get chat context to access persisted recipe
const chatContext = useChatContext();
// Use a ref to capture the current messages for the event handler
const messagesRef = useRef(messages);
const isCreatingRecipeRef = useRef(false);
// Update the ref when messages change
useEffect(() => {
messagesRef.current = messages;
}, [messages]);
// Get recipeConfig from multiple sources with priority:
// 1. Chat context (persisted recipe)
// 2. Navigation state (from recipes view)
@@ -149,13 +159,27 @@ export const useRecipeManager = (messages: Message[], locationState?: LocationSt
// Listen for make-agent-from-chat event
useEffect(() => {
const handleMakeAgent = async () => {
// Prevent duplicate calls using global flag
if (window.isCreatingRecipe) {
return;
}
// Prevent duplicate calls using local ref
if (isCreatingRecipeRef.current) {
return;
}
window.electron.logInfo('Making recipe from chat...');
// Set both local and global flags
isCreatingRecipeRef.current = true;
window.isCreatingRecipe = true;
setIsGeneratingRecipe(true);
try {
// Create recipe directly from chat messages
// Create recipe directly from chat messages using the ref to get current messages
const createRecipeRequest = {
messages: messages,
messages: messagesRef.current,
title: '',
description: '',
};
@@ -166,16 +190,14 @@ export const useRecipeManager = (messages: Message[], locationState?: LocationSt
throw new Error(`Failed to create recipe: ${response.error}`);
}
window.electron.logInfo('Created recipe:');
window.electron.logInfo(JSON.stringify(response.recipe, null, 2));
window.electron.logInfo('Created recipe successfully');
// First, verify the recipe data
// Verify the recipe data
if (!response.recipe) {
throw new Error('No recipe data received');
}
// Create a new window for the recipe editor
console.log('Opening recipe editor with config:', response.recipe);
const recipeConfig = {
id: response.recipe.title || 'untitled',
title: response.recipe.title || 'Untitled Recipe',
@@ -184,6 +206,12 @@ export const useRecipeManager = (messages: Message[], locationState?: LocationSt
activities: response.recipe.activities || [],
prompt: response.recipe.prompt || '',
};
// Set a flag to prevent the current window from reacting to recipe config changes
// This prevents navigation conflicts when creating new windows
window.sessionStorage.setItem('ignoreRecipeConfigChanges', 'true');
// Create the new window
window.electron.createChatWindow(
undefined, // query
undefined, // dir
@@ -194,11 +222,21 @@ export const useRecipeManager = (messages: Message[], locationState?: LocationSt
);
window.electron.logInfo('Opening recipe editor window');
// Clear the ignore flag after a short delay
setTimeout(() => {
window.sessionStorage.removeItem('ignoreRecipeConfigChanges');
}, 1000);
} catch (error) {
window.electron.logInfo('Failed to create recipe:');
const errorMessage = error instanceof Error ? error.message : String(error);
window.electron.logInfo(errorMessage);
// Set the error state to show in modal
setRecipeError(errorMessage);
} finally {
isCreatingRecipeRef.current = false;
window.isCreatingRecipe = false;
setIsGeneratingRecipe(false);
}
};
@@ -208,7 +246,7 @@ export const useRecipeManager = (messages: Message[], locationState?: LocationSt
return () => {
window.removeEventListener('make-agent-from-chat', handleMakeAgent);
};
}, [messages]);
}, []);
return {
recipeConfig,
@@ -219,5 +257,7 @@ export const useRecipeManager = (messages: Message[], locationState?: LocationSt
recipeParameters,
handleParameterSubmit,
handleAutoExecution,
recipeError,
setRecipeError,
};
};
@@ -1,3 +1,5 @@
/// <reference types="vite/client" />
declare module '*.json' {
const value: Record<string, unknown>;
export default value;
@@ -49,3 +51,12 @@ declare namespace React {
WebkitAppRegion?: 'drag' | 'no-drag';
}
}
// Extend Window interface to include global recipe creation flag
declare global {
interface Window {
isCreatingRecipe?: boolean;
}
}
export {};