mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
Fix recipe creation not working from current chat session
This commit is contained in:
@@ -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
@@ -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');
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 {};
|
||||
Reference in New Issue
Block a user